Files
cinny/src/app/styles/Animations.css.ts
T
jaredandClaude Opus 5 7aba71490a fix(a11y): reduced-motion for spinner/call-avatar; tap-to-skip boot; AA accent contrast
- SendingSpinClass and CallAvatarAnimation respect prefers-reduced-motion
  (static opacity fallback), matching MsgAppearClass.
- Terminal boot overlay dismisses on click/tap and is aria-hidden.
- Primary.Main: Midnight #6b7ca8 -> #6f80aa (4.29 -> 4.51:1), Lotus
  Terminal Light #c44e00 -> #ba4a00 (4.14 -> 4.52:1), hue preserved.

Fixes #84
Fixes #86
Fixes #52

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-12 20:28:41 -04:00

177 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { keyframes, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds';
const spin = keyframes({
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
const wobble = keyframes({
'0%': {
transform: 'translateX(0) rotateZ(0deg)',
},
'20%': {
transform: `translateX(-${toRem(4)}) rotateZ(-4deg)`,
},
'40%': {
transform: `translateX(${toRem(4)}) rotateZ(4deg)`,
},
'60%': {
transform: `translateX(-${toRem(3)}) rotateZ(-3deg)`,
},
'80%': {
transform: `translateX(${toRem(3)}) rotateZ(3deg)`,
},
'100%': {
transform: 'translateX(0) rotateZ(0deg)',
},
});
const glowPulse = keyframes({
'0%': {
boxShadow: `0 0 0 ${toRem(0)} ${color.Success.ContainerActive}`,
},
'100%': {
boxShadow: `0 0 0 ${toRem(8)} ${color.Success.ContainerActive}`,
},
});
// Wobble/glow/spin only run under the same no-preference guard as
// MsgAppearClass below; prefers-reduced-motion users get a static state.
export const WobbleAnimation = style({
'@media': {
'(prefers-reduced-motion: no-preference)': {
animation: `${wobble} 2000ms ease-in-out`,
animationIterationCount: 'infinite',
},
},
});
export const GlowAnimation = style({
'@media': {
'(prefers-reduced-motion: no-preference)': {
animation: `${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite',
},
},
});
export const CallAvatarAnimation = style({
'@media': {
'(prefers-reduced-motion: no-preference)': {
animation: `${wobble} 2000ms ease-in-out, ${glowPulse} 2000ms ease-out`,
animationIterationCount: 'infinite',
},
},
});
export const SendingSpinClass = style({
display: 'inline-block',
transformOrigin: 'center',
// Static fallback: dim instead of spinning when motion is reduced.
opacity: 0.6,
'@media': {
'(prefers-reduced-motion: no-preference)': {
animation: `${spin} 900ms linear infinite`,
opacity: 1,
},
},
});
const msgAppearKeyframe = keyframes({
from: { opacity: 0.4, transform: 'scale(0.97)' },
to: { opacity: 1, transform: 'scale(1)' },
});
export const MsgAppearClass = style({
'@media': {
'(prefers-reduced-motion: no-preference)': {
animation: `${msgAppearKeyframe} 150ms ease-out both`,
},
},
});
// ─── Animated chat background keyframes ───────────────────────────────────────
/**
* Digital Rain — vertical stripe scroll with a phosphor-glow flicker layered on top.
* Two stripe layers at different column widths and speeds create parallax depth.
*/
export const animRainKeyframe = keyframes({
from: { backgroundPosition: '0 0, 0 0' },
to: { backgroundPosition: '0 200px, 0 100px' },
});
/** Phosphor flicker brightness pulse layered over the rain scroll. */
export const animRainGlowKeyframe = keyframes({
'0%': { filter: 'brightness(0.85)' },
'30%': { filter: 'brightness(1.25)' },
'60%': { filter: 'brightness(0.9)' },
'80%': { filter: 'brightness(1.1)' },
'100%': { filter: 'brightness(0.85)' },
});
/**
* Star Drift — three dot layers, each moving by exactly one tile width/height per
* cycle so loops are seamless even though each layer drifts at a different speed.
* Layer sizes: 130 px, 190 px, 260 px → displacement equals one tile each.
*/
export const animStarsDriftKeyframe = keyframes({
from: { backgroundPosition: '0 0, 65px 32px, 32px 97px' },
to: { backgroundPosition: '-130px -130px, -125px -158px, -228px -163px' },
});
/**
* Grid Pulse — subtle backgroundSize breathe so the grid feels alive.
* Paired with animGridBrightnessKeyframe at a different period for organic feel.
*/
export const animGridPulseKeyframe = keyframes({
'0%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' },
'50%': { backgroundSize: '68px 68px, 68px 68px, 13px 13px, 13px 13px' },
'100%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' },
});
/** Brightness oscillation layered on top of the grid size pulse. */
export const animGridBrightnessKeyframe = keyframes({
'0%': { filter: 'brightness(0.8)' },
'50%': { filter: 'brightness(1.3)' },
'100%': { filter: 'brightness(0.8)' },
});
/**
* Aurora Flow — four gradient layers each travel a distinct path through the
* 200%–300% canvas, driven by one multi-stop keyframe so they never sync.
* Each background-position value corresponds to one radial-gradient layer.
*/
export const animAuroraKeyframe = keyframes({
'0%': { backgroundPosition: '0% 0%, 100% 0%, 50% 100%, 0% 50%' },
'20%': { backgroundPosition: '60% 40%, 20% 80%, 80% 20%, 100% 0%' },
'40%': { backgroundPosition: '100% 100%, 0% 100%, 100% 0%, 50% 50%' },
'60%': { backgroundPosition: '40% 80%, 80% 30%, 20% 70%, 0% 100%' },
'80%': { backgroundPosition: '0% 50%, 50% 0%, 0% 50%, 100% 50%' },
'100%': { backgroundPosition: '0% 0%, 100% 0%, 50% 100%, 0% 50%' },
});
/**
* Fireflies drift — three dot layers move at slightly different diagonal vectors
* (each offset equals exactly one tile so loops are seamless).
*/
export const animFirefliesKeyframe = keyframes({
from: { backgroundPosition: '0 0, 120px 80px, 60px 140px' },
to: { backgroundPosition: '-200px -200px, -80px -120px, -140px -60px' },
});
/** Brightness surge — gives fireflies a "glow pulse" life cycle. */
export const animFirefliesGlowKeyframe = keyframes({
'0%': { filter: 'brightness(0.4)' },
'50%': { filter: 'brightness(1.8)' },
'100%': { filter: 'brightness(0.4)' },
});
/** Opacity blink — runs at a prime period relative to the glow for organic feel. */
export const animFirefliesBlinkKeyframe = keyframes({
'0%': { opacity: 0.35 },
'50%': { opacity: 1 },
'100%': { opacity: 0.35 },
});