mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/auto-drive): gate weave animations to active phase
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::colors;
|
||||
use crate::auto_drive_strings;
|
||||
use crate::colors;
|
||||
use crate::header_wave::{HeaderBorderWeaveEffect, HeaderWaveEffect};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
@@ -10,7 +11,7 @@ use ratatui::widgets::{Block, Borders, Paragraph, Widget, Wrap};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const DRIVE_SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
const DRIVE_SPINNER_INTERVAL_MS: u64 = 120;
|
||||
@@ -43,6 +44,7 @@ pub(crate) struct AutoCoordinatorViewModel {
|
||||
pub button: Option<AutoCoordinatorButton>,
|
||||
pub manual_hint: Option<String>,
|
||||
pub ctrl_switch_hint: String,
|
||||
pub cli_running: bool,
|
||||
}
|
||||
|
||||
struct VariantContext {
|
||||
@@ -54,11 +56,29 @@ struct VariantContext {
|
||||
pub(crate) struct AutoCoordinatorView {
|
||||
model: AutoCoordinatorViewModel,
|
||||
app_event_tx: AppEventSender,
|
||||
header_wave: HeaderWaveEffect,
|
||||
header_border: HeaderBorderWeaveEffect,
|
||||
}
|
||||
|
||||
impl AutoCoordinatorView {
|
||||
pub fn new(model: AutoCoordinatorViewModel, app_event_tx: AppEventSender) -> Self {
|
||||
Self { model, app_event_tx }
|
||||
let now = Instant::now();
|
||||
let header_wave = {
|
||||
let effect = HeaderWaveEffect::new();
|
||||
effect.set_enabled(false, now);
|
||||
effect
|
||||
};
|
||||
let header_border = {
|
||||
let effect = HeaderBorderWeaveEffect::new();
|
||||
effect.set_enabled(false, now);
|
||||
effect
|
||||
};
|
||||
Self {
|
||||
model,
|
||||
app_event_tx,
|
||||
header_wave,
|
||||
header_border,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_context(&self) -> VariantContext {
|
||||
@@ -74,7 +94,7 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frame(&self, area: Rect, buf: &mut Buffer, title: &str) -> Option<Rect> {
|
||||
fn render_frame(&self, area: Rect, buf: &mut Buffer, title: &str, now: Instant) -> Option<Rect> {
|
||||
if area.width < 3 || area.height < 3 {
|
||||
return None;
|
||||
}
|
||||
@@ -89,6 +109,28 @@ impl AutoCoordinatorView {
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(colors::border()))
|
||||
.render(area, buf);
|
||||
if self.header_wave.is_enabled() {
|
||||
self.header_wave.render(area, buf, now);
|
||||
}
|
||||
if self.header_border.is_enabled() {
|
||||
self.header_border.render(area, buf, now);
|
||||
}
|
||||
// Reapply static title styling so animation never recolors it
|
||||
let title_style = Style::default()
|
||||
.fg(colors::text())
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let title_y = area.y;
|
||||
let title_start = area.x + 1;
|
||||
for (offset, ch) in title.chars().enumerate() {
|
||||
let x = title_start + offset as u16;
|
||||
if x >= area.x && x < area.x.saturating_add(area.width) {
|
||||
let mut ch_buf = [0u8; 4];
|
||||
let symbol = ch.encode_utf8(&mut ch_buf);
|
||||
let cell = &mut buf[(x, title_y)];
|
||||
cell.set_symbol(symbol);
|
||||
cell.set_style(title_style);
|
||||
}
|
||||
}
|
||||
Some(Rect {
|
||||
x: area.x + 1,
|
||||
y: area.y + 1,
|
||||
@@ -290,7 +332,25 @@ impl AutoCoordinatorView {
|
||||
}
|
||||
|
||||
fn render_classic(&self, area: Rect, buf: &mut Buffer, ctx: &VariantContext) {
|
||||
let Some(inner) = self.render_frame(area, buf, " Auto Drive ") else {
|
||||
let now = Instant::now();
|
||||
let waiting = self.model.waiting_for_response;
|
||||
let mut frame_needed = false;
|
||||
|
||||
let should_enable_border = waiting && !self.model.cli_running;
|
||||
let mut border_enabled = self.header_border.is_enabled();
|
||||
if should_enable_border && !border_enabled {
|
||||
self.header_border.set_enabled(true, now);
|
||||
border_enabled = true;
|
||||
} else if !should_enable_border && border_enabled {
|
||||
self.header_border.set_enabled(false, now);
|
||||
border_enabled = false;
|
||||
}
|
||||
|
||||
if border_enabled && self.header_border.schedule_if_needed(now) {
|
||||
frame_needed = true;
|
||||
}
|
||||
|
||||
let Some(inner) = self.render_frame(area, buf, " Auto Drive ", now) else {
|
||||
return;
|
||||
};
|
||||
let inner = self.apply_left_padding(inner, buf);
|
||||
@@ -349,8 +409,12 @@ impl AutoCoordinatorView {
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(inner, buf);
|
||||
|
||||
let mut next_interval = Duration::from_millis(DRIVE_SPINNER_INTERVAL_MS);
|
||||
if frame_needed {
|
||||
next_interval = next_interval.min(HeaderBorderWeaveEffect::FRAME_INTERVAL);
|
||||
}
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(DRIVE_SPINNER_INTERVAL_MS)));
|
||||
.send(AppEvent::ScheduleFrameIn(next_interval));
|
||||
}
|
||||
|
||||
fn apply_left_padding(&self, area: Rect, buf: &mut Buffer) -> Rect {
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use crate::header_wave::HeaderWaveEffect;
|
||||
use crate::header_wave::{HeaderBorderWeaveEffect, HeaderWaveEffect};
|
||||
use crate::auto_drive_strings;
|
||||
|
||||
use codex_common::elapsed::format_duration;
|
||||
@@ -164,6 +164,7 @@ const STATUS_LABEL_GAP: usize = 2;
|
||||
const STATUS_CONTENT_PREFIX: &str = " ";
|
||||
const STATUS_TOKENS_PREFIX: &str = " ";
|
||||
const AUTO_COUNTDOWN_SECONDS: u8 = 10;
|
||||
const ENABLE_WARP_STRIPES: bool = false;
|
||||
|
||||
fn status_field_prefix(label: &str) -> String {
|
||||
let padding = STATUS_LABEL_GAP
|
||||
@@ -516,6 +517,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
tools_state: ToolState,
|
||||
live_builder: RowBuilder,
|
||||
header_wave: HeaderWaveEffect,
|
||||
header_border: HeaderBorderWeaveEffect,
|
||||
// Store pending image paths keyed by their placeholder text
|
||||
pending_images: HashMap<String, PathBuf>,
|
||||
// (removed) pending non-image files are no longer tracked; non-image paths remain as plain text
|
||||
@@ -2760,6 +2762,15 @@ impl ChatWidget<'_> {
|
||||
live_builder: RowBuilder::new(usize::MAX),
|
||||
header_wave: {
|
||||
let effect = HeaderWaveEffect::new();
|
||||
if ENABLE_WARP_STRIPES {
|
||||
effect.set_enabled(true, Instant::now());
|
||||
} else {
|
||||
effect.set_enabled(false, Instant::now());
|
||||
}
|
||||
effect
|
||||
},
|
||||
header_border: {
|
||||
let effect = HeaderBorderWeaveEffect::new();
|
||||
effect.set_enabled(false, Instant::now());
|
||||
effect
|
||||
},
|
||||
@@ -3004,6 +3015,15 @@ impl ChatWidget<'_> {
|
||||
live_builder: RowBuilder::new(usize::MAX),
|
||||
header_wave: {
|
||||
let effect = HeaderWaveEffect::new();
|
||||
if ENABLE_WARP_STRIPES {
|
||||
effect.set_enabled(true, Instant::now());
|
||||
} else {
|
||||
effect.set_enabled(false, Instant::now());
|
||||
}
|
||||
effect
|
||||
},
|
||||
header_border: {
|
||||
let effect = HeaderBorderWeaveEffect::new();
|
||||
effect.set_enabled(false, Instant::now());
|
||||
effect
|
||||
},
|
||||
@@ -9558,6 +9578,44 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cli_running(&self) -> bool {
|
||||
if !self.exec.running_commands.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if !self.tools_state.running_custom_tools.is_empty()
|
||||
|| !self.tools_state.running_web_search.is_empty()
|
||||
|| !self.tools_state.running_wait_tools.is_empty()
|
||||
|| !self.tools_state.running_kill_tools.is_empty()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if self.stream.is_write_cycle_active() {
|
||||
return true;
|
||||
}
|
||||
if !self.active_task_ids.is_empty() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn update_header_border_activation(&self) {
|
||||
let now = Instant::now();
|
||||
let should_enable_header = self.auto_state.active && self.is_cli_running();
|
||||
let currently_enabled = self.header_border.is_enabled();
|
||||
if should_enable_header && !currently_enabled {
|
||||
self.header_border.set_enabled(true, now);
|
||||
} else if !should_enable_header && currently_enabled {
|
||||
self.header_border.set_enabled(false, now);
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_auto_drive_visuals(&mut self) {
|
||||
self.update_header_border_activation();
|
||||
if self.auto_state.active {
|
||||
self.auto_rebuild_live_ring();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_auto_command(&mut self, goal: Option<String>) {
|
||||
let provided = goal.unwrap_or_default();
|
||||
let trimmed = provided.trim();
|
||||
@@ -9572,10 +9630,10 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.bottom_pane
|
||||
.update_status_text("Auto Drive Goal".to_string());
|
||||
self.header_wave.set_enabled(false, Instant::now());
|
||||
self.push_background_tail(
|
||||
"Please enter the goal you would like to work autonomously towards.".to_string(),
|
||||
);
|
||||
self.update_header_border_activation();
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
@@ -9608,7 +9666,7 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.last_broadcast_thought = None;
|
||||
self.auto_state.seconds_remaining = AUTO_COUNTDOWN_SECONDS;
|
||||
self.auto_state.waiting_for_response = true;
|
||||
self.header_wave.set_enabled(true, Instant::now());
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.push_background_tail(format!("Auto Drive started: {goal_text}"));
|
||||
self.request_redraw();
|
||||
@@ -9642,6 +9700,7 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.placeholder_phrase =
|
||||
Some(auto_drive_strings::next_auto_drive_phrase().to_string());
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -9663,6 +9722,7 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.resume_after_manual_submit = false;
|
||||
self.auto_state.awaiting_submission = false;
|
||||
self.auto_state.waiting_for_response = false;
|
||||
self.update_header_border_activation();
|
||||
|
||||
match status {
|
||||
AutoCoordinatorStatus::Continue => {
|
||||
@@ -9805,6 +9865,7 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.bottom_pane
|
||||
.update_status_text("Auto Drive".to_string());
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -9835,7 +9896,10 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.update_status_text(String::new());
|
||||
}
|
||||
self.bottom_pane.ensure_input_focus();
|
||||
self.header_wave.set_enabled(false, Instant::now());
|
||||
if ENABLE_WARP_STRIPES {
|
||||
self.header_wave.set_enabled(false, Instant::now());
|
||||
}
|
||||
self.update_header_border_activation();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -9854,6 +9918,7 @@ impl ChatWidget<'_> {
|
||||
self.auto_state.placeholder_phrase = None;
|
||||
self.auto_state.thinking_prefix_stripped = false;
|
||||
self.auto_state.last_decision_display = None;
|
||||
self.update_header_border_activation();
|
||||
self.auto_rebuild_live_ring();
|
||||
self.request_redraw();
|
||||
self.auto_send_conversation();
|
||||
@@ -9890,6 +9955,7 @@ impl ChatWidget<'_> {
|
||||
};
|
||||
|
||||
let status_lines: Vec<String> = vec![append_thought_ellipsis(&status_text)];
|
||||
let cli_running = self.is_cli_running();
|
||||
|
||||
let prompt = self
|
||||
.auto_state
|
||||
@@ -9944,6 +10010,7 @@ impl ChatWidget<'_> {
|
||||
button,
|
||||
manual_hint,
|
||||
ctrl_switch_hint,
|
||||
cli_running,
|
||||
};
|
||||
|
||||
self.bottom_pane
|
||||
@@ -15764,20 +15831,33 @@ impl ChatWidget<'_> {
|
||||
let status_line = Line::from(status_spans);
|
||||
|
||||
let now = Instant::now();
|
||||
if self.header_wave.schedule_if_needed(now) {
|
||||
self.update_header_border_activation();
|
||||
let mut frame_needed = false;
|
||||
if ENABLE_WARP_STRIPES && self.header_wave.schedule_if_needed(now) {
|
||||
frame_needed = true;
|
||||
}
|
||||
if self.header_border.schedule_if_needed(now) {
|
||||
frame_needed = true;
|
||||
}
|
||||
if frame_needed {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ScheduleFrameIn(HeaderWaveEffect::FRAME_INTERVAL));
|
||||
.send(AppEvent::ScheduleFrameIn(HeaderBorderWeaveEffect::FRAME_INTERVAL));
|
||||
}
|
||||
|
||||
// Render the block first
|
||||
status_block.render(padded_area, buf);
|
||||
let wave_enabled = self.header_wave.is_enabled();
|
||||
let border_enabled = self.header_border.is_enabled();
|
||||
if wave_enabled {
|
||||
self.header_wave.render(padded_area, buf, now);
|
||||
}
|
||||
if border_enabled {
|
||||
self.header_border.render(padded_area, buf, now);
|
||||
}
|
||||
|
||||
// Then render the text inside with padding, centered
|
||||
let status_style = if wave_enabled {
|
||||
let effect_enabled = wave_enabled || border_enabled;
|
||||
let status_style = if effect_enabled {
|
||||
Style::default().fg(crate::colors::text())
|
||||
} else {
|
||||
Style::default()
|
||||
|
||||
@@ -244,6 +244,7 @@ pub(super) fn finalize_all_running_as_interrupted(chat: &mut ChatWidget<'_>) {
|
||||
chat.bottom_pane.set_task_running(false);
|
||||
}
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
}
|
||||
|
||||
pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
|
||||
@@ -364,6 +365,8 @@ pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
|
||||
chat.tools_state.running_web_search.remove(&call_id);
|
||||
}
|
||||
}
|
||||
|
||||
chat.refresh_auto_drive_visuals();
|
||||
}
|
||||
|
||||
pub(super) fn try_merge_completed_exec_at(chat: &mut ChatWidget<'_>, idx: usize) {
|
||||
@@ -579,6 +582,7 @@ pub(super) fn handle_exec_begin_now(
|
||||
_ => "exploring…",
|
||||
};
|
||||
chat.bottom_pane.update_status_text(status_text.to_string());
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -633,6 +637,7 @@ pub(super) fn handle_exec_begin_now(
|
||||
chat.bottom_pane
|
||||
.update_status_text(format!("running command: {}", preview_short));
|
||||
}
|
||||
chat.refresh_auto_drive_visuals();
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_end_now(
|
||||
@@ -645,6 +650,7 @@ pub(super) fn handle_exec_end_now(
|
||||
chat.exec.unsuppress_exec_end(&call_id);
|
||||
chat.ended_call_ids.insert(call_id);
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
}
|
||||
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
|
||||
@@ -655,6 +661,7 @@ pub(super) fn handle_exec_end_now(
|
||||
.remove(&super::ExecCallId(ev.call_id.clone()))
|
||||
{
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
}
|
||||
let ExecCommandEndEvent {
|
||||
@@ -738,6 +745,7 @@ pub(super) fn handle_exec_end_now(
|
||||
};
|
||||
chat.bottom_pane.update_status_text(status_text);
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -829,6 +837,7 @@ pub(super) fn handle_exec_end_now(
|
||||
.update_status_text(format!("command failed (exit {})", exit_code));
|
||||
}
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
}
|
||||
|
||||
// Stable ordering now inserts at the correct position; these helpers are removed.
|
||||
|
||||
@@ -6,6 +6,71 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Margin, Rect};
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub struct HeaderBorderWeaveEffect {
|
||||
enabled: Cell<bool>,
|
||||
started_at: Cell<Option<Instant>>,
|
||||
next_frame: Cell<Option<Instant>>,
|
||||
}
|
||||
|
||||
impl HeaderBorderWeaveEffect {
|
||||
pub const FRAME_INTERVAL: Duration = HeaderWaveEffect::FRAME_INTERVAL;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
enabled: Cell::new(false),
|
||||
started_at: Cell::new(None),
|
||||
next_frame: Cell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool, now: Instant) {
|
||||
self.enabled.set(enabled);
|
||||
if enabled {
|
||||
self.started_at.set(Some(now));
|
||||
self.next_frame.set(None);
|
||||
} else {
|
||||
self.started_at.set(None);
|
||||
self.next_frame.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled.get()
|
||||
}
|
||||
|
||||
pub fn schedule_if_needed(&self, now: Instant) -> bool {
|
||||
if !self.enabled.get() {
|
||||
return false;
|
||||
}
|
||||
match self.next_frame.get() {
|
||||
Some(due) if due > now => false,
|
||||
_ => {
|
||||
self.next_frame
|
||||
.set(Some(now + Self::FRAME_INTERVAL));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, area: Rect, buf: &mut Buffer, now: Instant) {
|
||||
if !self.enabled.get() {
|
||||
return;
|
||||
}
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return;
|
||||
}
|
||||
let start = self.started_at.get().unwrap_or_else(|| {
|
||||
self.started_at.set(Some(now));
|
||||
now
|
||||
});
|
||||
let elapsed = now.saturating_duration_since(start).as_secs_f32();
|
||||
let tick_interval = Self::FRAME_INTERVAL.as_secs_f32().max(0.001);
|
||||
let tick = (elapsed / tick_interval).floor() as u32;
|
||||
let phase = wrap_unit(elapsed * 0.25);
|
||||
render_border_weave(area, buf, phase, tick);
|
||||
}
|
||||
}
|
||||
|
||||
/// Declarative background animation used by the TUI status header.
|
||||
pub struct HeaderWaveEffect {
|
||||
enabled: Cell<bool>,
|
||||
@@ -115,3 +180,69 @@ fn spectral_color(t: f32) -> Color {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_border_weave(area: Rect, buf: &mut Buffer, phase: f32, tick: u32) {
|
||||
let positions = border_positions(area);
|
||||
if positions.is_empty() {
|
||||
return;
|
||||
}
|
||||
let base_color = crate::colors::border();
|
||||
let highlight = crate::colors::mix_toward(base_color, prism_color(phase), 0.75);
|
||||
for (idx, &(x, y)) in positions.iter().enumerate() {
|
||||
let tick_step = (tick as usize) % 12;
|
||||
let phase_idx = (idx + (12 - tick_step)) % 12;
|
||||
let color = if phase_idx < 4 {
|
||||
highlight
|
||||
} else if phase_idx < 8 {
|
||||
crate::colors::mix_toward(highlight, base_color, 0.5)
|
||||
} else {
|
||||
crate::colors::mix_toward(base_color, crate::colors::text_dim(), 0.35)
|
||||
};
|
||||
let cell = &mut buf[(x, y)];
|
||||
cell.set_fg(color);
|
||||
cell.set_bg(crate::colors::background());
|
||||
}
|
||||
}
|
||||
|
||||
fn border_positions(area: Rect) -> Vec<(u16, u16)> {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut positions = Vec::new();
|
||||
let x0 = area.x;
|
||||
let x1 = area.x + area.width.saturating_sub(1);
|
||||
let y0 = area.y;
|
||||
let y1 = area.y + area.height.saturating_sub(1);
|
||||
|
||||
for x in x0..=x1 {
|
||||
positions.push((x, y0));
|
||||
}
|
||||
if y1 > y0 {
|
||||
for y in y0 + 1..=y1 {
|
||||
positions.push((x1, y));
|
||||
}
|
||||
}
|
||||
if x1 > x0 {
|
||||
for x in (x0..x1).rev() {
|
||||
positions.push((x, y1));
|
||||
}
|
||||
}
|
||||
if y1 > y0 + 1 {
|
||||
for y in (y0 + 1..y1).rev() {
|
||||
positions.push((x0, y));
|
||||
}
|
||||
}
|
||||
|
||||
positions
|
||||
}
|
||||
|
||||
fn prism_color(t: f32) -> Color {
|
||||
let angle = wrap_unit(t) * TAU;
|
||||
let r = (angle.sin() * 0.5 + 0.5).powf(0.55);
|
||||
let g = ((angle + TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
|
||||
let b = ((angle + 2.0 * TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
|
||||
Color::Rgb(
|
||||
(r * 255.0).clamp(0.0, 255.0) as u8,
|
||||
(g * 255.0).clamp(0.0, 255.0) as u8,
|
||||
(b * 255.0).clamp(0.0, 255.0) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user