diff --git a/src/app/now-playing.tsx b/src/app/now-playing.tsx index 2bbfea4..3dbf012 100644 --- a/src/app/now-playing.tsx +++ b/src/app/now-playing.tsx @@ -34,10 +34,15 @@ const SUB_CONTROLS: { icon: IconName; label: string }[] = [ const DISMISS_DISTANCE = 140; const DISMISS_VELOCITY = 1000; -const MAX_CONTENT_WIDTH = 390; +const MAX_CONTENT_WIDTH = 408; +const CONTENT_SIDE_PADDING = spacing.lg; +const NARROW_CONTENT_SIDE_PADDING = spacing.md; const MEDIA_AREA_MIN = 220; const MEDIA_AREA_MAX = 360; const ART_SIZE_MAX = 340; +const SCOPE_HEIGHT_MIN = 150; +const SCOPE_HEIGHT_MAX = 220; +const SCOPE_HEIGHT_RATIO = 9 / 16; const HEADER_HEIGHT = 32; const CONTENT_TOP_PADDING = spacing.sm; const CONTENT_BOTTOM_PADDING = spacing.lg; @@ -63,6 +68,8 @@ interface NowPlayingLayout { contentWidth: number; mediaHeight: number; artSize: number; + scopeWidth: number; + scopeHeight: number; mediaTopMargin: number; mediaBottomGap: number; showSecondaryControls: boolean; @@ -73,7 +80,8 @@ function clamp(value: number, min: number, max: number): number { } function getNowPlayingLayout(windowWidth: number, availableHeight: number): NowPlayingLayout { - const contentPadding = windowWidth < 360 ? spacing.lg : spacing.xl; + const contentPadding = + windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING; const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH)); const mediaMax = Math.min(contentWidth, MEDIA_AREA_MAX); const mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN); @@ -96,12 +104,19 @@ function getNowPlayingLayout(windowWidth: number, availableHeight: number): NowP const heightBoundMedia = availableHeight - fixedHeight; const mediaHeight = Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)); const artSize = Math.min(mediaHeight, ART_SIZE_MAX); + const scopeWidth = contentWidth; + const scopeHeight = Math.min( + mediaHeight, + Math.round(clamp(scopeWidth * SCOPE_HEIGHT_RATIO, SCOPE_HEIGHT_MIN, SCOPE_HEIGHT_MAX)) + ); return { contentPadding, contentWidth, mediaHeight, artSize, + scopeWidth, + scopeHeight, mediaTopMargin, mediaBottomGap, showSecondaryControls, @@ -228,17 +243,18 @@ export default function NowPlayingScreen() { style={[ styles.scopeSurface, { - width: layout.contentWidth, - height: layout.mediaHeight, + width: layout.scopeWidth, + height: layout.scopeHeight, }, ]} > ) : ( diff --git a/src/components/SpectrumCurve.tsx b/src/components/SpectrumCurve.tsx index 730ad35..7e098b4 100644 --- a/src/components/SpectrumCurve.tsx +++ b/src/components/SpectrumCurve.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Canvas, Group, LinearGradient, Path, Skia, vec } from '@shopify/react-native-skia'; +import { Canvas, Group, LinearGradient, Path, Rect, Skia, vec } from '@shopify/react-native-skia'; import { colors } from '@/theme'; interface SpectrumCurveProps { @@ -14,6 +14,10 @@ interface SpectrumCurveProps { fillOpacity?: number; /** Adds a soft wider stroke under the line for a glow. */ glow?: boolean; + /** Fades the left/right edges into the background so stage views do not end abruptly. */ + edgeFade?: boolean; + edgeFadeColor?: string; + edgeFadeWidth?: number; } /** #rrggbb -> rgba() with the given alpha. */ @@ -69,6 +73,9 @@ export function SpectrumCurve({ lineWidth = 2, fillOpacity = 1, glow = false, + edgeFade = false, + edgeFadeColor = colors.bgPrimary, + edgeFadeWidth = 28, }: SpectrumCurveProps) { const pad = lineWidth; const { line, fill } = useMemo( @@ -107,6 +114,24 @@ export function SpectrumCurve({ strokeCap="round" color={color} /> + {edgeFade && ( + <> + + + + + + + + )} ); } diff --git a/src/components/Visualizer.tsx b/src/components/Visualizer.tsx index 4b220ef..f1fe13a 100644 --- a/src/components/Visualizer.tsx +++ b/src/components/Visualizer.tsx @@ -18,6 +18,7 @@ interface VisualizerProps { interactive?: boolean; showChrome?: boolean; mode?: Mode; + edgeFade?: boolean; } /** @@ -31,6 +32,7 @@ export function Visualizer({ interactive = true, showChrome = true, mode: controlledMode, + edgeFade = false, }: VisualizerProps) { const [uncontrolledMode, setUncontrolledMode] = useState('spectrum'); const mode = controlledMode ?? uncontrolledMode; @@ -56,7 +58,7 @@ export function Visualizer({ {mode === 'spectrum' ? ( - + ) : (