diff --git a/src/island.rs b/src/island.rs index 8e8050b..9d79e33 100644 --- a/src/island.rs +++ b/src/island.rs @@ -28,6 +28,9 @@ const SHAPE_DONE: f32 = 0.6; const HOVER_MS: u64 = 220; const PILL_HOVER_GROW: f32 = 10.0; +// how much thicker, in px, the transport icons get when hovered +const BOLD: f32 = 2.0; + // sizes, in pixels const COMPACT_W: f32 = 220.0; const COMPACT_H: f32 = 38.0; @@ -121,6 +124,8 @@ enum Spot { Next, Preset(&'static str), Row(usize), + Band(usize), + Timeline, } pub struct Island { @@ -197,17 +202,6 @@ fn hovering(spot: Spot) -> impl Fn(bool) + 'static { } } -/* - * a soft pink halo that grows with `lit`; nothing at rest, since every shadow costs - * two gpu layers each frame - */ -fn glowing(rectangle: Rectangle, lit: f32, strength: f32) -> Rectangle { - if lit < 0.01 { - return rectangle; - } - - rectangle.shadow(Shadow::drop(PINK).opacity(strength * lit).blur(6.0 + 12.0 * lit)) -} // the picked player while it's still open, else the one Media calls active pub fn shown<'a>(media: &'a Media, selected: Option<&str>) -> Option<&'a MediaPlayer> { @@ -296,24 +290,19 @@ pub fn view() -> LayerWindow { .border(1.0, Color::blend(EDGE, faded(PINK, 110), lift)); /* - * amane redraws the whole window every frame, so big static effects are paid - * 60 times a second: the shadow belongs to the pill and the grow, and fades out - * with the card coming in. no clip anywhere: neither face ever overflows the shape + * one shadow, three looks: a dark drop shadow under the pill, a pink centred glow + * when the pill is hovered, and a faint pink glow around the open card. blur 22 + * fades out within ~28 px (amane blurs with a deviation of blur / 2), inside the + * window's SHADOW margin. no clip anywhere: neither face ever overflows the shape */ - let shape = if reveal < 1.0 { - /* - * hovered, the pill's own drop shadow turns into a glow: pink, centred, a little - * stronger, so it costs nothing the pill wasn't already paying - */ - let shadow = Shadow::drop(Color::blend(Color::BLACK, PINK, lift)) - .opacity(mix(0.55, 0.7, lift) * (1.0 - reveal)) - .blur(mix(24.0, 20.0, lift)) - .offset(0.0, mix(10.0, 0.0, lift)); + let glow = lift.max(reveal); - shape.shadow(shadow) - } else { - shape - }; + let shape = shape.shadow( + Shadow::drop(Color::blend(Color::BLACK, PINK, glow)) + .opacity(mix(mix(0.55, 0.7, lift), 0.3, reveal)) + .blur(mix(mix(24.0, 20.0, lift), 22.0, reveal)) + .offset(0.0, mix(10.0, 0.0, glow)), + ); let shape = shape .align_child(Center, Start) @@ -755,14 +744,18 @@ fn timeline(scene: &Scene) -> Column { // a live stream has no length to scale by, so it just counts up let shown = if length > 0.0 { fraction * length } else { position }; + // hovered, the track thickens and brightens and the knob grows + let lit = island.lit(Spot::Timeline); + let bar_h = BAR_H + 4.0 * lit; + let bar = Rectangle::new().width(SIDE_W).height(KNOB).align_child(Start, Center).child( Stack::new(children![ - Rectangle::new().width(SIDE_W).height(BAR_H).radius(Full).fill(TRACK).child( + Rectangle::new().width(SIDE_W).height(bar_h).radius(Full).fill(Color::blend(TRACK, CHIP_HOVER, lit)).child( Rectangle::new() - .width((SIDE_W * fraction).max(BAR_H)) - .height(BAR_H) + .width((SIDE_W * fraction).max(bar_h)) + .height(bar_h) .radius(Full) - .fill(PINK), + .fill(Color::blend(PINK, FLAME, 0.4 * lit)), ), Rectangle::new() .width(KNOB) @@ -770,10 +763,11 @@ fn timeline(scene: &Scene) -> Column { .radius(Full) .fill(Color::WHITE) .shadow(Shadow::drop("#000000").opacity(0.4).blur(6.0)) - .translate(SIDE_W * fraction - KNOB / 2.0, (BAR_H - KNOB) / 2.0), + .translate(SIDE_W * fraction - KNOB / 2.0, (bar_h - KNOB) / 2.0) + .scale(1.0 + 0.25 * lit), ]) .width(SIDE_W) - .height(BAR_H), + .height(bar_h), ); // live streams have no length, so there is nowhere to seek to @@ -781,7 +775,10 @@ fn timeline(scene: &Scene) -> Column { let name = player.name().to_string(); bar.cursor(Pointer) - .on_hover(leave_bar) + .on_hover(|inside| { + hovering(Spot::Timeline)(inside); + leave_bar(inside); + }) .on_drag(|point| { let mut island = Island::write(); @@ -859,32 +856,22 @@ fn controls(player: &MediaPlayer, island: &Island) -> Row { let toggle = player.clone(); let next = player.clone(); - /* - * hovered, play/pause swells, gains a halo and its pink warms toward white; - * previous and next do the same smaller, their icons turning pink - */ + // hovered, the transport icons go bold: thicker strokes, nothing else let lit = island.lit(Spot::PlayPause); - let ink = Color::blend(PINK, FLAME, 0.55 * lit); - let middle = Rectangle::new() - .width(CONTROLS_H) - .height(CONTROLS_H) - .radius(Full) - .fill(Color::blend(faded(PINK, 0), faded(PINK, 34), lit)) - .align_child(Center, Center) - .scale(1.0 + 0.1 * lit); + let middle = Rectangle::new().width(CONTROLS_H).height(CONTROLS_H).align_child(Center, Center); let middle = if player.playing() { - let bar = || Rectangle::new().width(10.0).height(30.0).radius(3.0).fill(ink); + let bar = || Rectangle::new().width(10.0 + BOLD * lit).height(30.0 + BOLD * lit).radius(3.0).fill(PINK); - middle.child(Row::new(children![bar(), bar()]).gap(7.0)) + middle.child(Row::new(children![bar(), bar()]).gap(7.0 - BOLD * lit)) } else { - middle.child(icon(Glyph::Play, 30.0, ink)) + middle.child(bold_icon(Glyph::Play, 30.0, PINK, BOLD * lit)) }; Row::new(children![ button(Glyph::Previous, island.lit(Spot::Previous)).on_hover(hovering(Spot::Previous)).on_click(move |_| previous.previous()), - glowing(middle, lit, 0.55).cursor(Pointer).on_hover(hovering(Spot::PlayPause)).on_click(move |_| toggle.play_pause()), + middle.cursor(Pointer).on_hover(hovering(Spot::PlayPause)).on_click(move |_| toggle.play_pause()), button(Glyph::Next, island.lit(Spot::Next)).on_hover(hovering(Spot::Next)).on_click(move |_| next.next()), ]) .gap(26.0) @@ -894,19 +881,14 @@ fn controls(player: &MediaPlayer, island: &Island) -> Row { .height(CONTROLS_H) } -// a round icon button: hovered, it swells, its icon turns pink and a soft pink halo grows around it +// a transport icon button, going bold as `lit` rises fn button(glyph: Glyph, lit: f32) -> Rectangle { - let circle = Rectangle::new() + Rectangle::new() .width(40.0) .height(40.0) - .radius(Full) - .fill(Color::blend(faded(PINK, 0), faded(PINK, 26), lit)) .align_child(Center, Center) - .scale(1.0 + 0.12 * lit) .cursor(Pointer) - .child(icon(glyph, 18.0, Color::blend(TEXT, PINK, lit))); - - glowing(circle, lit, 0.4) + .child(bold_icon(glyph, 18.0, TEXT, BOLD * lit)) } fn eq_header(scene: &Scene) -> Row { @@ -982,21 +964,25 @@ fn equalizer(scene: &Scene) -> Column { for (band, (frequency, gain)) in BANDS.iter().zip(gains).enumerate() { let y = knob_y(gain); + // hovered, a band's track widens and brightens, its knob grows and its label turns pink + let lit = scene.island.lit(Spot::Band(band)); + let track_w = TRACK_W + 4.0 * lit; + tracks.push(Box::new( Rectangle::new().width(column).height(SLIDER_H).align_child(Center, Start).child( Rectangle::new() - .width(TRACK_W) + .width(track_w) .height(SLIDER_H) - .radius(TRACK_W / 2.0) - .fill(TRACK) + .radius(track_w / 2.0) + .fill(Color::blend(TRACK, CHIP_HOVER, lit)) .align_child(Center, End) .child( // rounded itself rather than clipped by the track: clips are gpu layers Rectangle::new() - .width(TRACK_W) + .width(track_w) .height(SLIDER_H - y) - .radius(TRACK_W / 2.0) - .fill(PINK), + .radius(track_w / 2.0) + .fill(Color::blend(PINK, FLAME, 0.45 * lit)), ), ), )); @@ -1010,6 +996,7 @@ fn equalizer(scene: &Scene) -> Column { .height(SLIDER_H) .align_child(Center, Start) .cursor(Pointer) + .on_hover(hovering(Spot::Band(band))) .on_drag(move |point| Sound::set_band(&player, &identity, band, gain_at(point.y))) .child( // a pink ring instead of a blurred shadow: ten blurs per frame cost the gpu more than the rest of the card @@ -1018,15 +1005,16 @@ fn equalizer(scene: &Scene) -> Column { .height(KNOB) .radius(Full) .fill(Color::WHITE) - .border(2.0, faded(PINK, 150)) - .translate(0.0, y - KNOB / 2.0), + .border(2.0, Color::blend(faded(PINK, 150), PINK, lit)) + .translate(0.0, y - KNOB / 2.0) + .scale(1.0 + 0.25 * lit), ), )); let label = if *frequency >= 1000 { format!("{}k", frequency / 1000) } else { frequency.to_string() }; labels.push(Box::new( - Rectangle::new().width(column).height(LABEL_H).align_child(Center, Center).child(Text::new(label).size(10.0).color(DIM)), + Rectangle::new().width(column).height(LABEL_H).align_child(Center, Center).child(Text::new(label).size(10.0).color(Color::blend(DIM, PINK, lit))), )); } @@ -1299,10 +1287,15 @@ enum Glyph { // drawn, not typed, so the island looks the same whatever fonts are installed fn icon(glyph: Glyph, size: f32, color: Color) -> Canvas { + bold_icon(glyph, size, color, 0.0) +} + +// play, pause, previous and next thickened by an outline `weight` px wide in their own colour; 0 draws none +fn bold_icon(glyph: Glyph, size: f32, color: Color, weight: f32) -> Canvas { let s = size; let triangle = |x0: f32, x1: f32| { - Path::new().move_to(x0, 0.14 * s).line_to(x1, 0.5 * s).line_to(x0, 0.86 * s).close().fill(color) + Path::new().move_to(x0, 0.14 * s).line_to(x1, 0.5 * s).line_to(x0, 0.86 * s).close().fill(color).stroke(weight, color) }; let bar = |x0: f32, x1: f32| { @@ -1313,6 +1306,7 @@ fn icon(glyph: Glyph, size: f32, color: Color) -> Canvas { .line_to(x0, 0.86 * s) .close() .fill(color) + .stroke(weight, color) }; let speaker = || {