From d3048ea7be94c58c88eb5791b0a545155a37c4c4 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sun, 25 Oct 2026 23:23:36 -0400 Subject: [PATCH] =?UTF-8?q?feat(seasonal):=20New=20Year=20redesign=20?= =?UTF-8?q?=E2=80=94=20spark=20bursts=20at=20the=20edges=20instead=20of=20?= =?UTF-8?q?rings=20over=20the=20text=20(#230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fireworks are spark bursts (14 thin rays with ember tips) that pop, drift down a little and fade within about a second, instead of large saturated rings that read as targets and sat on top of message text. - Bursts go off at the very top edge or along the side edges, away from the reading column, and are smaller (90-150px, was 130-240px). - Reduced motion: two faint still bursts peek in from the top corners instead of a full ring frozen over the timeline. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- .../components/seasonal/themes/NewYear.css.ts | 28 ++--- .../components/seasonal/themes/NewYear.tsx | 114 ++++++++---------- 2 files changed, 61 insertions(+), 81 deletions(-) diff --git a/src/app/components/seasonal/themes/NewYear.css.ts b/src/app/components/seasonal/themes/NewYear.css.ts index 590c33d77..a940d702e 100644 --- a/src/app/components/seasonal/themes/NewYear.css.ts +++ b/src/app/components/seasonal/themes/NewYear.css.ts @@ -9,28 +9,16 @@ import { keyframes } from '@vanilla-extract/css'; */ /** - * Firework burst — a thin spark ring expands from a pinpoint, brightens, then - * fades as it grows. Scale + opacity only; the ring is a radial-gradient border - * supplied inline. Long pauses between bursts come from a low keyframe-duty: - * the ring spends most of the cycle collapsed and invisible. + * Firework burst — the spark burst pops open from a point, drifts down a little + * as it spreads (gravity) and fades fast. Scale + translate + opacity only. Long + * gaps between bursts come from a low keyframe duty: invisible most of the cycle. */ export const animBurst = keyframes({ - '0%': { transform: 'scale(0.05)', opacity: '0' }, - '4%': { transform: 'scale(0.12)', opacity: '0.95' }, - '22%': { transform: 'scale(1)', opacity: '0.55' }, - '34%': { transform: 'scale(1.25)', opacity: '0' }, - '100%': { transform: 'scale(1.25)', opacity: '0' }, -}); - -/** - * Burst core flash — the bright pinpoint at a firework's origin pops just before - * the ring blooms, then quickly dims. Pairs with animBurst on the same cadence. - */ -export const animCoreFlash = keyframes({ - '0%': { transform: 'scale(0.2)', opacity: '0' }, - '3%': { transform: 'scale(1)', opacity: '1' }, - '14%': { transform: 'scale(0.6)', opacity: '0' }, - '100%': { transform: 'scale(0.6)', opacity: '0' }, + '0%': { transform: 'translate3d(0, 0, 0) scale(0.15)', opacity: '0' }, + '3%': { transform: 'translate3d(0, 0, 0) scale(0.4)', opacity: '1' }, + '14%': { transform: 'translate3d(0, 6px, 0) scale(1)', opacity: '0.75' }, + '24%': { transform: 'translate3d(0, 14px, 0) scale(1.1)', opacity: '0' }, + '100%': { transform: 'translate3d(0, 14px, 0) scale(1.1)', opacity: '0' }, }); /** diff --git a/src/app/components/seasonal/themes/NewYear.tsx b/src/app/components/seasonal/themes/NewYear.tsx index 782704951..d32671d65 100644 --- a/src/app/components/seasonal/themes/NewYear.tsx +++ b/src/app/components/seasonal/themes/NewYear.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from 'react'; import { SeasonalOverlayProps } from '../types'; import { animBurst, - animCoreFlash, animShimmer, animConfettiFall, animConfettiSway, @@ -12,7 +11,7 @@ import { /** * New Year overlay — a midnight celebration. Layered oklch gradients sink the - * app into a deep navy night; fireworks bloom as expanding spark rings, a + * app into a deep navy night; fireworks pop as spark bursts at the edges, a * champagne-gold shimmer sweeps across, confetti slivers tumble down, and * sparkle stars twinkle. All motion is transform/opacity only. * @@ -26,8 +25,8 @@ import { * position:fixed, or pointer-events here — kept well below opaque so chat text * stays WCAG-AA legible. * - * REDUCED MOTION: when `reduced`, render a static but gorgeous scene (a frozen - * firework bloom mid-burst, scattered gold confetti, a still shimmer band) with + * REDUCED MOTION: when `reduced`, render a static but gorgeous scene (two still + * bursts peeking in from the top corners, scattered confetti, a still shimmer) with * no `animation` at all. The settings preview always passes reduced=true. */ @@ -89,16 +88,44 @@ const sparkleUri = (color: string) => ``, )}")`; +// [Gitea #230] A firework is spark rays + ember dots radiating from a point +// (not a solid ring), drawn once as an SVG and scaled/faded by the keyframes. +const burstUri = (ring: string, core: string) => { + const rays = Array.from({ length: 14 }, (_, k) => { + const a = (k / 14) * Math.PI * 2 + (k % 2) * 0.12; + const [c, si] = [Math.cos(a), Math.sin(a)]; + const r1 = k % 2 ? 18 : 14; + const r2 = k % 2 ? 38 : 44; + const p = (r: number) => `${(50 + c * r).toFixed(1)} ${(50 + si * r).toFixed(1)}`; + return ( + `` + + `` + ); + }).join(''); + return `url("data:image/svg+xml,${encodeURIComponent( + `${rays}`, + )}")`; +}; + export function NewYearOverlay({ reduced }: SeasonalOverlayProps) { const bursts = useMemo( () => - // Bursts cluster in the upper two-thirds of the sky, away from typical text. + // [Gitea #230] Bursts go off in the top band or along the side edges, + // away from the reading column (top ones centred at the very top edge so they + // fan down through the header row and fade), and are kept small; the old ones were + // large rings anywhere in the upper two-thirds, over message text. Array.from({ length: 7 }, (_, i) => { const hue = BURST_HUES[i % BURST_HUES.length]; + const edge = i % 3 === 2; + const right = rand(i + 51) > 0.5; return { - top: 8 + rand(i + 1) * 48, - left: 8 + rand(i + 11) * 84, - size: 130 + Math.floor(rand(i + 21) * 110), + top: edge ? 22 + rand(i + 1) * 45 : -2 + rand(i + 1) * 5, + left: edge + ? right + ? 92 + rand(i + 11) * 6 + : 2 + rand(i + 11) * 6 + : 6 + rand(i + 11) * 88, + size: 90 + Math.floor(rand(i + 21) * 60), ring: hue[0], core: hue[1], duration: 6.5 + rand(i + 31) * 4, @@ -174,9 +201,16 @@ export function NewYearOverlay({ reduced }: SeasonalOverlayProps) { }} /> - {/* Fireworks — expanding spark rings + a core flash. In reduced mode we - freeze the first burst mid-bloom and drop the rest. */} - {(reduced ? bursts.slice(0, 1) : bursts).map((b, i) => ( + {/* Fireworks — spark bursts that pop, drift down a little and fade. In + reduced mode, two still bursts peek in from the top corners instead + of one frozen over the timeline. */} + {(reduced + ? [ + { ...bursts[0], top: -4, left: 1, size: 160 }, + { ...bursts[1], top: -3, left: 99, size: 130 }, + ] + : bursts + ).map((b, i) => (