island: glowing card, hover on eq bands and time bar, bold transport

- the open card keeps a faint pink glow: the pill's shadow blends from a
  dark drop shadow to a centred pink glow (opacity 0.3, blur 22, inside
  the window margin); measured 2.95 ms gpu with the card open, as before
- eq bands: hovered, the track widens 4 px and brightens, the knob grows
  25% with a full pink ring, the frequency label turns pink
- time bar: hovered, the track thickens 4 px and brightens, the knob grows
- previous/play-pause/next: no halo, circle or growth any more; they go
  bold instead (an outline of the glyph's own colour, 0 to 2 px; pause bars
  widen), and glowing() is gone
This commit is contained in:
server2
2026-10-06 16:58:05 +02:00
parent fc90260d5d
commit b74e727305
+62 -68
View File
@@ -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 = || {