island: cover gradient background, unmirrored pill levels
- the pill, the card and the fade veils share one 150-degree gradient from the cover's palette (most common, second, background), each darkened to a fixed brightness so white text stays readable; veils span the whole shape so their gradient lines up with the backdrop's - measured on the desktop: card open 3.1 ms gpu (solid fill was ~3), pill 1.7 ms (was 1.0), open/close animation unchanged (1 and 0 slow frames) - pill levels: lows left, highs right, no mirroring; every bar still grows up and down from the middle line
This commit is contained in:
+54
-31
@@ -3,7 +3,7 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use amane::{
|
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,
|
Image, InputArea, Layer, LayerWindow, Line, Media, MediaPlayer, Padding, Palette, Parent, Path,
|
||||||
Pointer, Rectangle, Row, Scroll, ScrollArea, Service, Shadow, Shape, SpaceBetween, Stack,
|
Pointer, Rectangle, Row, Scroll, ScrollArea, Service, Shadow, Shape, SpaceBetween, Stack,
|
||||||
Start, Text, Vertical, Weight, Widget, Zone, children, shapes,
|
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 CHIP_OPEN: Color = Color::rgba(255, 255, 255, 34);
|
||||||
const TRACK: Color = Color::rgba(255, 255, 255, 24);
|
const TRACK: Color = Color::rgba(255, 255, 255, 24);
|
||||||
const EDGE: Color = Color::rgba(255, 255, 255, 26);
|
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 SURFACE_BOTTOM: Color = Color::rgb(13, 10, 18);
|
||||||
const MENU_BG: Color = Color::rgb(35, 26, 44);
|
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 width = mix(COMPACT_W, WIDE_W, grow);
|
||||||
let height = mix(COMPACT_H, WIDE_H, 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
|
// a gradient of the cover's own colours, for the pill and the card alike
|
||||||
let surface = tint(palette.dominant());
|
let colors = backdrop_colors(&palette);
|
||||||
|
|
||||||
// at most one face at a time, each faded by a veil instead of a gpu layer
|
// at most one face at a time, each faded by a veil instead of a gpu layer
|
||||||
let face = if reveal > 0.0 {
|
let face = if reveal > 0.0 {
|
||||||
@@ -200,9 +200,9 @@ pub fn view() -> LayerWindow {
|
|||||||
levels: &spectrum.levels,
|
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 {
|
} 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 {
|
} else {
|
||||||
// mid-grow: the empty shape alone, the cheapest frames of the animation
|
// mid-grow: the empty shape alone, the cheapest frames of the animation
|
||||||
Rectangle::new().width(0.0).height(0.0)
|
Rectangle::new().width(0.0).height(0.0)
|
||||||
@@ -211,8 +211,8 @@ pub fn view() -> LayerWindow {
|
|||||||
let shape = Rectangle::new()
|
let shape = Rectangle::new()
|
||||||
.width(width)
|
.width(width)
|
||||||
.height(height)
|
.height(height)
|
||||||
.radius(mix(COMPACT_H / 2.0, WIDE_RADIUS, grow))
|
.radius(radius)
|
||||||
.fill(surface)
|
.fill(backdrop(&colors, 255))
|
||||||
.border(1.0, EDGE);
|
.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.
|
* 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 {
|
if shown >= 1.0 {
|
||||||
return face;
|
return face;
|
||||||
}
|
}
|
||||||
@@ -390,12 +391,42 @@ fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, sur
|
|||||||
.width(width - 2.0)
|
.width(width - 2.0)
|
||||||
.height(height - 2.0)
|
.height(height - 2.0)
|
||||||
.radius(radius - 1.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);
|
.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))
|
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 {
|
fn faded(color: Color, alpha: u8) -> Color {
|
||||||
Color::rgba(color.red(), color.green(), color.blue(), alpha)
|
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))
|
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 {
|
fn content(scene: &Scene) -> Rectangle {
|
||||||
Rectangle::new().width(WIDE_W).height(WIDE_H).padding(PAD).child(
|
Rectangle::new().width(WIDE_W).height(WIDE_H).padding(PAD).child(
|
||||||
Column::new(children![
|
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
|
* the pill's visualiser: seven bars from the shown player's own sound, glowing when
|
||||||
* shown player's own sound, glowing when loud; a pause sign while paused, and still
|
* loud; a pause sign while paused, and still bars while the capture starts or the
|
||||||
* bars while the capture starts or the player has no stream to listen to
|
* player has no stream to listen to
|
||||||
*/
|
*/
|
||||||
fn pill_levels(playing: bool, levels: &[f32]) -> Canvas {
|
fn pill_levels(playing: bool, levels: &[f32]) -> Canvas {
|
||||||
const BARS: usize = 7;
|
const BARS: usize = 7;
|
||||||
@@ -1077,32 +1101,31 @@ fn pill_levels(playing: bool, levels: &[f32]) -> Canvas {
|
|||||||
return icon(Glyph::Pause, 13.0, DIM);
|
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 glow = Path::new();
|
||||||
let mut body = Path::new();
|
let mut body = Path::new();
|
||||||
|
|
||||||
for index in 0..BARS {
|
for index in 0..BARS {
|
||||||
let distance = index.abs_diff(middle);
|
|
||||||
|
|
||||||
let level = if levels.is_empty() {
|
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 {
|
} 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
|
* gain: typical music sits at 0.15 to 0.35 here, and a lifting curve made every
|
||||||
* bar look nearly full
|
* 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;
|
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 {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user