diff --git a/src/island.rs b/src/island.rs index b92eb7d..9547d7c 100644 --- a/src/island.rs +++ b/src/island.rs @@ -3,7 +3,7 @@ use std::thread; use std::time::{Duration, Instant}; use amane::{ - Animation, Arc, Argument, Bus, Canvas, Cap, Center, Color, Column, Easing, End, Full, + Animation, Arc, Argument, Bus, Canvas, Cap, Center, Color, Column, Easing, End, Full, Gradient, Image, InputArea, Layer, LayerWindow, Line, Media, MediaPlayer, Padding, Palette, Parent, Path, Pointer, Rectangle, Row, Scroll, ScrollArea, Service, Shadow, Shape, SpaceBetween, Stack, Start, Text, Vertical, Weight, Widget, Zone, children, shapes, @@ -92,7 +92,6 @@ const CHIP: Color = Color::rgba(255, 255, 255, 18); const CHIP_OPEN: Color = Color::rgba(255, 255, 255, 34); const TRACK: Color = Color::rgba(255, 255, 255, 24); const EDGE: Color = Color::rgba(255, 255, 255, 26); -const SURFACE_TOP: Color = Color::rgb(30, 21, 38); const SURFACE_BOTTOM: Color = Color::rgb(13, 10, 18); const MENU_BG: Color = Color::rgb(35, 26, 44); @@ -185,9 +184,10 @@ pub fn view() -> LayerWindow { let width = mix(COMPACT_W, WIDE_W, grow); let height = mix(COMPACT_H, WIDE_H, grow); + let radius = mix(COMPACT_H / 2.0, WIDE_RADIUS, grow); - // solid, tinted toward the cover: gradients were this iGPU's most expensive fill - let surface = tint(palette.dominant()); + // a gradient of the cover's own colours, for the pill and the card alike + let colors = backdrop_colors(&palette); // at most one face at a time, each faded by a veil instead of a gpu layer let face = if reveal > 0.0 { @@ -200,9 +200,9 @@ pub fn view() -> LayerWindow { levels: &spectrum.levels, }; - veiled(card(&scene), WIDE_W, WIDE_H, WIDE_RADIUS, reveal, surface) + veiled(card(&scene), width, height, radius, reveal, &colors) } else if pill > 0.0 { - veiled(compact(player, &art.path, &spectrum.levels), COMPACT_W, COMPACT_H, COMPACT_H / 2.0, pill, surface) + veiled(compact(player, &art.path, &spectrum.levels), width, height, radius, pill, &colors) } else { // mid-grow: the empty shape alone, the cheapest frames of the animation Rectangle::new().width(0.0).height(0.0) @@ -211,8 +211,8 @@ pub fn view() -> LayerWindow { let shape = Rectangle::new() .width(width) .height(height) - .radius(mix(COMPACT_H / 2.0, WIDE_RADIUS, grow)) - .fill(surface) + .radius(radius) + .fill(backdrop(&colors, 255)) .border(1.0, EDGE); /* @@ -377,11 +377,12 @@ fn phases(timeline: f32) -> (f32, f32, f32) { } /* - * fades a face in over the island's solid surface by laying that surface over it, + * fades a face in over the island's backdrop by laying the same gradient over it, * thinning as `shown` rises: the same picture as .opacity(shown), without a gpu layer. - * inset by the 1 px rim, so the border stays visible + * the veil spans the whole shape (minus the 1 px rim), so its gradient lines up with + * the backdrop's exactly; amane places gradients by each rectangle's own bounds */ -fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, surface: Color) -> Rectangle { +fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle { if shown >= 1.0 { return face; } @@ -390,12 +391,42 @@ fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, sur .width(width - 2.0) .height(height - 2.0) .radius(radius - 1.0) - .fill(faded(surface, ((1.0 - shown) * 255.0).round() as u8)) + .fill(backdrop(colors, ((1.0 - shown) * 255.0).round() as u8)) .translate(1.0, 1.0); + let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face); + Rectangle::new().width(width).height(height).child(Stack::new(children![face, veil]).width(width).height(height)) } +// the cover's colours for the backdrop: most common, second most common, darkest +fn backdrop_colors(palette: &Palette) -> [Color; 3] { + let first = palette.dominant(); + let second = palette.colors().get(1).copied().unwrap_or(first); + + // darker toward the bottom right, where the equalizer and presets sit + [deepen(first, 0.22), deepen(second, 0.15), deepen(palette.background(), 0.07)] +} + +/* + * one gradient for the pill, the card and the veils; 150 degrees runs from the + * top left to the bottom right, like the reference's light + */ +fn backdrop(colors: &[Color; 3], alpha: u8) -> Gradient { + Gradient::linear(150.0, [(0.0, faded(colors[0], alpha)), (0.55, faded(colors[1], alpha)), (1.0, faded(colors[2], alpha))]) +} + +// scaled down, keeping its hue, until its brightness is at most `max` (0 to 1), so white text reads on it +fn deepen(color: Color, max: f32) -> Color { + let (red, green, blue) = (f32::from(color.red()), f32::from(color.green()), f32::from(color.blue())); + + let brightness = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255.0; + + let scale = if brightness > max { max / brightness } else { 1.0 }; + + Color::rgb((red * scale) as u8, (green * scale) as u8, (blue * scale) as u8) +} + fn faded(color: Color, alpha: u8) -> Color { Color::rgba(color.red(), color.green(), color.blue(), alpha) } @@ -431,13 +462,6 @@ fn card(scene: &Scene) -> Rectangle { Rectangle::new().width(WIDE_W).height(WIDE_H).child(Stack::new(layers).width(WIDE_W).height(WIDE_H)) } -// the surface colour leaning a little toward the cover's -fn tint(color: Color) -> Color { - let lean = |surface: u8, cover: u8| (f32::from(surface) * 0.7 + f32::from(cover) * 0.3) as u8; - - Color::rgb(lean(SURFACE_TOP.red(), color.red()), lean(SURFACE_TOP.green(), color.green()), lean(SURFACE_TOP.blue(), color.blue())) -} - fn content(scene: &Scene) -> Rectangle { Rectangle::new().width(WIDE_W).height(WIDE_H).padding(PAD).child( Column::new(children![ @@ -1064,9 +1088,9 @@ fn cover(art: &str, size: f32, rim: f32, hole: bool) -> Rectangle { } /* - * the pill's visualiser: seven bars mirrored around the bass in the middle, from the - * shown player's own sound, glowing when loud; a pause sign while paused, and still - * bars while the capture starts or the player has no stream to listen to + * the pill's visualiser: seven bars from the shown player's own sound, glowing when + * loud; a pause sign while paused, and still bars while the capture starts or the + * player has no stream to listen to */ fn pill_levels(playing: bool, levels: &[f32]) -> Canvas { const BARS: usize = 7; @@ -1077,32 +1101,31 @@ fn pill_levels(playing: bool, levels: &[f32]) -> Canvas { return icon(Glyph::Pause, 13.0, DIM); } - let middle = BARS / 2; + // lows on the left, highs on the right, no mirroring; every bar grows up and down from the middle line + let middle = HEIGHT / 2.0; let mut glow = Path::new(); let mut body = Path::new(); for index in 0..BARS { - let distance = index.abs_diff(middle); - let level = if levels.is_empty() { - [0.85, 0.55, 0.7, 0.35][distance] + [0.55, 0.85, 0.6, 0.75, 0.45, 0.6, 0.35][index] } else { /* - * the loudest band of this bar's quarter of the spectrum, linear with a little + * the loudest band of this bar's seventh of the spectrum, linear with a little * gain: typical music sits at 0.15 to 0.35 here, and a lifting curve made every * bar look nearly full */ - (band(levels, distance, middle + 1) * 1.4).min(1.0) + (band(levels, index, BARS) * 1.4).min(1.0) }; - let half = 1.5 + level * (HEIGHT / 2.0 - 3.0); + let half = 1.5 + level * (middle - 3.0); let x = 3.0 + index as f32 * (WIDTH - 6.0) / (BARS - 1) as f32; - body = body.move_to(x, HEIGHT / 2.0 - half).line_to(x, HEIGHT / 2.0 + half); + body = body.move_to(x, middle - half).line_to(x, middle + half); if level > 0.3 { - glow = glow.move_to(x, HEIGHT / 2.0 - half).line_to(x, HEIGHT / 2.0 + half); + glow = glow.move_to(x, middle - half).line_to(x, middle + half); } }