Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ deterministic for tests.

- Do not introduce small/medium/large feather variants.
- Do not add pixel-art feather sprites.
- Do not change the settings toggle, duration, fade, gravity, cursor bump, or
panel open/close lifecycle.
- Do not change the settings toggle, duration, fade, gravity, or cursor bump.
> Update (as shipped): the close burst was dropped — feathers now emit on
> panel *open* only, and closing simply clears them. The open/close *lifecycle*
> is otherwise unchanged.
- Do not add unrelated side-panel abstractions beyond what the emitter model
needs.

Expand Down
3 changes: 1 addition & 2 deletions src/app/panels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ impl HonkHonk {
return;
}
let panel = panel_geometry(self.window_size, effects_panel_view::EFFECTS_PANEL_W);
self.panel_flourish
.emit(panel, self.window_size, transition, now);
self.panel_flourish.emit(panel, self.window_size, now);
}
}

Expand Down
44 changes: 19 additions & 25 deletions src/ui/side_panel/flourish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,31 +74,37 @@ pub fn panel_burst_emitter(panel: PanelRect, window: (f32, f32)) -> BurstEmitter
let touches_right = win_w > 0.0 && panel.x + panel.w >= win_w - EDGE_EPS;
let touches_bottom = win_h > 0.0 && panel.y + panel.h >= win_h - EDGE_EPS;

if touches_right && panel.x > EDGE_EPS {
// Vertical edges need a non-degenerate height (and horizontal a width) to
// spread feathers along; a collapsed panel falls through to the center
// point rather than seeding a zero-length edge that stacks every particle.
let has_vertical_span = panel.h > EDGE_EPS;
let has_horizontal_span = panel.w > EDGE_EPS;

if touches_right && panel.x > EDGE_EPS && has_vertical_span {
return edge_line(
Point::new(panel.x, panel.y),
Point::new(panel.x, panel.y + panel.h),
-1.0,
0.0,
);
}
if touches_left && panel.x + panel.w < win_w - EDGE_EPS {
if touches_left && panel.x + panel.w < win_w - EDGE_EPS && has_vertical_span {
return edge_line(
Point::new(panel.x + panel.w, panel.y),
Point::new(panel.x + panel.w, panel.y + panel.h),
1.0,
0.0,
);
}
if touches_bottom && panel.y > EDGE_EPS {
if touches_bottom && panel.y > EDGE_EPS && has_horizontal_span {
return edge_line(
Point::new(panel.x, panel.y),
Point::new(panel.x + panel.w, panel.y),
0.0,
-1.0,
);
}
if touches_top && panel.y + panel.h < win_h - EDGE_EPS {
if touches_top && panel.y + panel.h < win_h - EDGE_EPS && has_horizontal_span {
return edge_line(
Point::new(panel.x, panel.y + panel.h),
Point::new(panel.x + panel.w, panel.y + panel.h),
Expand All @@ -111,15 +117,9 @@ pub fn panel_burst_emitter(panel: PanelRect, window: (f32, f32)) -> BurstEmitter
}

impl PanelFlourish {
pub fn emit(
&mut self,
panel: PanelRect,
window: (f32, f32),
transition: PanelTransition,
now: Instant,
) {
pub fn emit(&mut self, panel: PanelRect, window: (f32, f32), now: Instant) {
let emitter = panel_burst_emitter(panel, window);
self.particles = seed_particles(emitter, transition);
self.particles = seed_particles(emitter);
self.started = Some(now);
self.last_tick = Some(now);
}
Expand Down Expand Up @@ -167,13 +167,11 @@ fn edge_line(start: Point, end: Point, x: f32, y: f32) -> BurstEmitter {
})
}

fn seed_particles(emitter: BurstEmitter, transition: PanelTransition) -> Vec<FeatherParticle> {
(0..PARTICLES)
.map(|i| seed_particle(emitter, transition, i))
.collect()
fn seed_particles(emitter: BurstEmitter) -> Vec<FeatherParticle> {
(0..PARTICLES).map(|i| seed_particle(emitter, i)).collect()
}

fn seed_particle(emitter: BurstEmitter, transition: PanelTransition, i: usize) -> FeatherParticle {
fn seed_particle(emitter: BurstEmitter, i: usize) -> FeatherParticle {
let class = feather_class(i);
let params = class_params(class, i);
let dir = particle_direction(emitter, i);
Expand All @@ -188,18 +186,13 @@ fn seed_particle(emitter: BurstEmitter, transition: PanelTransition, i: usize) -
scale(dir, 5.0 + (i % 3) as f32 * 2.0),
scale(perp, scatter * 6.0),
);
let rotation_bias = match transition {
PanelTransition::Open => 0.0,
PanelTransition::Close => 0.35,
};

FeatherParticle {
class,
position: translate(emitter_point(emitter, i), offset),
velocity,
alpha: 1.0,
size: params.size,
rotation: rotation_bias + scatter * 0.45,
rotation: scatter * 0.45,
wobble_phase: i as f32 * 1.618_034,
wobble_frequency: params.wobble_frequency,
wobble_strength: params.wobble_strength,
Expand Down Expand Up @@ -306,8 +299,9 @@ fn tick_particle(particle: &mut FeatherParticle, dt: f32, cursor: Option<Point>)
particle.velocity = add(particle.velocity, cursor_bump(*particle, cursor, dt));
}

let wobble = (particle.wobble_phase + particle.wobble_frequency * dt).sin();
particle.wobble_phase += particle.wobble_frequency * dt;
let advance = particle.wobble_frequency * dt;
let wobble = (particle.wobble_phase + advance).sin();
particle.wobble_phase += advance;
particle.velocity.x += wobble * particle.wobble_strength * dt;
particle.velocity.y += GRAVITY * dt;

Expand Down
105 changes: 105 additions & 0 deletions tests/flourish_support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Shared fixtures and helpers for the panel-feather flourish integration
//! tests, used by `side_panel_flourish.rs` (motion) and
//! `side_panel_flourish_emitter.rs` (emitter geometry). Each test binary
//! compiles this module independently, so not every helper is used in both;
//! `allow(dead_code, unused_imports)` keeps the unused-in-one-binary warnings
//! quiet (each binary uses a different subset of the helpers and re-exports).
#![allow(dead_code, unused_imports)]

use std::time::{Duration, Instant};

pub use honkhonk::ui::side_panel::{
BurstEmitter, BurstLine, FeatherClass, FeatherParticle, PanelFlourish, PanelRect,
panel_burst_emitter,
};
pub use iced::{Point, Vector};

pub fn right_panel() -> PanelRect {
PanelRect {
x: 880.0,
y: 0.0,
w: 400.0,
h: 800.0,
center: Point::new(1080.0, 400.0),
}
}

pub fn left_panel() -> PanelRect {
PanelRect {
x: 0.0,
y: 0.0,
w: 320.0,
h: 700.0,
center: Point::new(160.0, 350.0),
}
}

pub fn top_panel() -> PanelRect {
PanelRect {
x: 120.0,
y: 0.0,
w: 720.0,
h: 180.0,
center: Point::new(480.0, 90.0),
}
}

pub fn bottom_panel() -> PanelRect {
PanelRect {
x: 120.0,
y: 620.0,
w: 720.0,
h: 180.0,
center: Point::new(480.0, 710.0),
}
}

pub fn floating_panel() -> PanelRect {
PanelRect {
x: 300.0,
y: 180.0,
w: 420.0,
h: 320.0,
center: Point::new(510.0, 340.0),
}
}

pub fn assert_line(actual: BurstLine, start: Point, end: Point, direction: Vector) {
assert_eq!(actual.start, start);
assert_eq!(actual.end, end);
assert_eq!(actual.direction, direction);
}

pub fn first_particle_of(flourish: &PanelFlourish, class: FeatherClass) -> FeatherParticle {
*flourish
.particles()
.iter()
.find(|p| p.class == class)
.expect("burst should include requested feather class")
}

pub fn size_range_for(flourish: &PanelFlourish, class: FeatherClass) -> f32 {
let sizes = flourish
.particles()
.iter()
.filter(|p| p.class == class)
.map(|p| p.size)
.collect::<Vec<_>>();
assert!(
sizes.len() > 1,
"burst should include multiple particles for {class:?}"
);
let min = sizes.iter().copied().fold(f32::INFINITY, f32::min);
let max = sizes.iter().copied().fold(f32::NEG_INFINITY, f32::max);
max - min
}

pub fn tick_for(flourish: &mut PanelFlourish, start: Instant, total: Duration) {
let total_ms = total.as_millis() as u64;
let mut elapsed_ms = 16;
while elapsed_ms < total_ms {
assert!(flourish.tick(start + Duration::from_millis(elapsed_ms), None));
elapsed_ms += 16;
}
assert!(flourish.tick(start + total, None));
}
Loading
Loading