feat(seasonal): New Year redesign (#230) — awaiting human review #472
@@ -9,28 +9,16 @@ import { keyframes } from '@vanilla-extract/css';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firework burst — a thin spark ring expands from a pinpoint, brightens, then
|
* Firework burst — the spark burst pops open from a point, drifts down a little
|
||||||
* fades as it grows. Scale + opacity only; the ring is a radial-gradient border
|
* as it spreads (gravity) and fades fast. Scale + translate + opacity only. Long
|
||||||
* supplied inline. Long pauses between bursts come from a low keyframe-duty:
|
* gaps between bursts come from a low keyframe duty: invisible most of the cycle.
|
||||||
* the ring spends most of the cycle collapsed and invisible.
|
|
||||||
*/
|
*/
|
||||||
export const animBurst = keyframes({
|
export const animBurst = keyframes({
|
||||||
'0%': { transform: 'scale(0.05)', opacity: '0' },
|
'0%': { transform: 'translate3d(0, 0, 0) scale(0.15)', opacity: '0' },
|
||||||
'4%': { transform: 'scale(0.12)', opacity: '0.95' },
|
'3%': { transform: 'translate3d(0, 0, 0) scale(0.4)', opacity: '1' },
|
||||||
'22%': { transform: 'scale(1)', opacity: '0.55' },
|
'14%': { transform: 'translate3d(0, 6px, 0) scale(1)', opacity: '0.75' },
|
||||||
'34%': { transform: 'scale(1.25)', opacity: '0' },
|
'24%': { transform: 'translate3d(0, 14px, 0) scale(1.1)', opacity: '0' },
|
||||||
'100%': { transform: 'scale(1.25)', opacity: '0' },
|
'100%': { transform: 'translate3d(0, 14px, 0) scale(1.1)', 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' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
|
|||||||
import { SeasonalOverlayProps } from '../types';
|
import { SeasonalOverlayProps } from '../types';
|
||||||
import {
|
import {
|
||||||
animBurst,
|
animBurst,
|
||||||
animCoreFlash,
|
|
||||||
animShimmer,
|
animShimmer,
|
||||||
animConfettiFall,
|
animConfettiFall,
|
||||||
animConfettiSway,
|
animConfettiSway,
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* New Year overlay — a midnight celebration. Layered oklch gradients sink the
|
* 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
|
* champagne-gold shimmer sweeps across, confetti slivers tumble down, and
|
||||||
* sparkle stars twinkle. All motion is transform/opacity only.
|
* 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
|
* position:fixed, or pointer-events here — kept well below opaque so chat text
|
||||||
* stays WCAG-AA legible.
|
* stays WCAG-AA legible.
|
||||||
*
|
*
|
||||||
* REDUCED MOTION: when `reduced`, render a static but gorgeous scene (a frozen
|
* REDUCED MOTION: when `reduced`, render a static but gorgeous scene (two still
|
||||||
* firework bloom mid-burst, scattered gold confetti, a still shimmer band) with
|
* bursts peeking in from the top corners, scattered confetti, a still shimmer) with
|
||||||
* no `animation` at all. The settings preview always passes reduced=true.
|
* no `animation` at all. The settings preview always passes reduced=true.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -89,16 +88,44 @@ const sparkleUri = (color: string) =>
|
|||||||
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 0 L14 10 L24 12 L14 14 L12 24 L10 14 L0 12 L10 10 Z' fill='${color}'/></svg>`,
|
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 0 L14 10 L24 12 L14 14 L12 24 L10 14 L0 12 L10 10 Z' fill='${color}'/></svg>`,
|
||||||
)}")`;
|
)}")`;
|
||||||
|
|
||||||
|
// [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 (
|
||||||
|
`<path d='M${p(r1)} L${p(r2)}' stroke='${ring}' stroke-width='1.6' stroke-linecap='round' stroke-opacity='0.85'/>` +
|
||||||
|
`<circle cx='${(50 + c * (r2 + 4)).toFixed(1)}' cy='${(50 + si * (r2 + 4)).toFixed(1)}' r='1.5' fill='${core}'/>`
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
return `url("data:image/svg+xml,${encodeURIComponent(
|
||||||
|
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='6' fill='${core}' opacity='0.5'/>${rays}</svg>`,
|
||||||
|
)}")`;
|
||||||
|
};
|
||||||
|
|
||||||
export function NewYearOverlay({ reduced }: SeasonalOverlayProps) {
|
export function NewYearOverlay({ reduced }: SeasonalOverlayProps) {
|
||||||
const bursts = useMemo<Burst[]>(
|
const bursts = useMemo<Burst[]>(
|
||||||
() =>
|
() =>
|
||||||
// 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) => {
|
Array.from({ length: 7 }, (_, i) => {
|
||||||
const hue = BURST_HUES[i % BURST_HUES.length];
|
const hue = BURST_HUES[i % BURST_HUES.length];
|
||||||
|
const edge = i % 3 === 2;
|
||||||
|
const right = rand(i + 51) > 0.5;
|
||||||
return {
|
return {
|
||||||
top: 8 + rand(i + 1) * 48,
|
top: edge ? 22 + rand(i + 1) * 45 : -2 + rand(i + 1) * 5,
|
||||||
left: 8 + rand(i + 11) * 84,
|
left: edge
|
||||||
size: 130 + Math.floor(rand(i + 21) * 110),
|
? 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],
|
ring: hue[0],
|
||||||
core: hue[1],
|
core: hue[1],
|
||||||
duration: 6.5 + rand(i + 31) * 4,
|
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
|
{/* Fireworks — spark bursts that pop, drift down a little and fade. In
|
||||||
freeze the first burst mid-bloom and drop the rest. */}
|
reduced mode, two still bursts peek in from the top corners instead
|
||||||
{(reduced ? bursts.slice(0, 1) : bursts).map((b, i) => (
|
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) => (
|
||||||
<div
|
<div
|
||||||
key={`b${i}`}
|
key={`b${i}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -188,57 +222,15 @@ export function NewYearOverlay({ reduced }: SeasonalOverlayProps) {
|
|||||||
height: `${b.size}px`,
|
height: `${b.size}px`,
|
||||||
marginLeft: `${-b.size / 2}px`,
|
marginLeft: `${-b.size / 2}px`,
|
||||||
marginTop: `${-b.size / 2}px`,
|
marginTop: `${-b.size / 2}px`,
|
||||||
|
backgroundImage: burstUri(b.ring, b.core),
|
||||||
|
backgroundSize: '100% 100%',
|
||||||
|
opacity: reduced ? 0.35 : undefined,
|
||||||
|
animation: reduced
|
||||||
|
? 'none'
|
||||||
|
: `${animBurst} ${b.duration}s ease-out ${b.delay}s infinite`,
|
||||||
|
willChange: reduced ? undefined : 'transform, opacity',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{/* Spark ring */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
borderRadius: '50%',
|
|
||||||
willChange: reduced ? undefined : 'transform, opacity',
|
|
||||||
background: `radial-gradient(circle, transparent 56%, ${b.ring} 64%, transparent 74%)`,
|
|
||||||
transform: reduced ? 'scale(0.82)' : undefined,
|
|
||||||
opacity: reduced ? 0.6 : undefined,
|
|
||||||
animation: reduced
|
|
||||||
? 'none'
|
|
||||||
: `${animBurst} ${b.duration}s ease-out ${b.delay}s infinite`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* Inner secondary ring for a fuller bloom */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: '18%',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: `radial-gradient(circle, transparent 50%, ${b.ring} 60%, transparent 72%)`,
|
|
||||||
transform: reduced ? 'scale(0.7)' : undefined,
|
|
||||||
opacity: reduced ? 0.4 : undefined,
|
|
||||||
animation: reduced
|
|
||||||
? 'none'
|
|
||||||
: `${animBurst} ${b.duration}s ease-out ${b.delay + 0.12}s infinite`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* Core flash */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '50%',
|
|
||||||
top: '50%',
|
|
||||||
width: '14%',
|
|
||||||
height: '14%',
|
|
||||||
marginLeft: '-7%',
|
|
||||||
marginTop: '-7%',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: `radial-gradient(circle, ${b.core} 0%, transparent 70%)`,
|
|
||||||
transform: reduced ? 'scale(0.9)' : undefined,
|
|
||||||
opacity: reduced ? 0.85 : undefined,
|
|
||||||
animation: reduced
|
|
||||||
? 'none'
|
|
||||||
: `${animCoreFlash} ${b.duration}s ease-out ${b.delay}s infinite`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Twinkling sparkle stars. */}
|
{/* Twinkling sparkle stars. */}
|
||||||
|
|||||||
Reference in New Issue
Block a user