Rework the CRT raster, glow and flicker effects
The three effects had drifted apart from each other and from the token system. Consolidate them behind --crt-* tokens and fix what was broken. - The animated text-shadow silently overrode the static one, so reduce-motion users saw a 5px white glow and everyone else a 3px currentColor glow. Both now come from one declaration, with the aberration offset factored out into --crt-shift. - Step the aberration instead of tweening it. text-shadow is not compositable and this one inherits to every glyph, so the smooth version repainted the whole document at refresh rate. - The flicker was invisible: a near-black tint at an effective alpha of 0.008-0.096 over a black background, a peak lift of under 2/255. Retint with the accent at ~3%. - Anchor the vignette to the viewport. It was sized to body's box, which is document-height, so on long pages the bloom sat off-screen. - Pin the overlays with position: fixed rather than painting document-sized layers, and give the grille hard stops so it is three stripes rather than a smooth R->G->B ramp. - Scale the raster pitch down at >=2dppx, where a 2px scanline is 4 device px and resamples into moire at fractional DPR. - Raise --color-mg to oklch(0.62): at 0.5 it was 3.5:1 on the background and 1.7:1 over the vignette centre. Halve the vignette alpha via its own token, keeping --color-accent-transparent at 50% for ProjectCard's glow. Collapses the two keyframe blocks from 193 lines to 40. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
89cfe7bdec
commit
0e6d352dca
2 changed files with 158 additions and 166 deletions
274
public/theme.css
274
public/theme.css
|
|
@ -39,12 +39,21 @@ html {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* The aberration is a discrete jitter, so it is stepped rather than
|
||||
interpolated: step-end repaints 21 times per cycle instead of once per
|
||||
frame. text-shadow is not compositable, and this one inherits to every
|
||||
glyph on the page, so a smooth tween would repaint the whole document at
|
||||
refresh rate. Stepping also reads more like chroma noise than a ramp does. */
|
||||
body {
|
||||
animation: textShadow 1.6s infinite;
|
||||
animation: crt-aberration 1.6s step-end infinite;
|
||||
/* Anchors the vignette to the viewport. Without it the gradient is sized to
|
||||
body's box, which is document-height, so on a long page its centre sits
|
||||
at the middle of the document and the bloom is off-screen at scroll-top. */
|
||||
background-attachment: fixed;
|
||||
background-color: var(--color-bg);
|
||||
background-image: radial-gradient(
|
||||
var(--color-accent-transparent),
|
||||
black 120%
|
||||
var(--crt-vignette-core),
|
||||
var(--crt-vignette-edge) 120%
|
||||
);
|
||||
color: var(--color-fg);
|
||||
column-gap: var(--gutter-large);
|
||||
|
|
@ -58,46 +67,56 @@ body {
|
|||
position: absolute;
|
||||
right: 0;
|
||||
row-gap: var(--stack-large);
|
||||
text-shadow: 0 0 5px var(--color-glow);
|
||||
/* Single source of truth for the glow: when the animation is off,
|
||||
--crt-shift is 0 and this is exactly the static glow. Previously the
|
||||
animation overrode a separate `0 0 5px var(--color-glow)` rule, so
|
||||
reduce-motion users saw a 5px white glow and everyone else a 3px
|
||||
currentColor one — two looks nobody had chosen. */
|
||||
text-shadow:
|
||||
calc(var(--crt-shift, 0) * var(--crt-aberration-max)) 0 1px
|
||||
var(--crt-shadow-cool),
|
||||
calc(var(--crt-shift, 0) * var(--crt-aberration-max) * -1) 0 1px
|
||||
var(--crt-shadow-warm),
|
||||
0 0 var(--crt-glow-blur) var(--color-glow);
|
||||
top: 0;
|
||||
}
|
||||
/* Fixed rather than absolute: these used to be document-sized layers that
|
||||
relied on background-attachment to keep the pattern still. Pinning them to
|
||||
the viewport gets the same effect while bounding the painted layer, and it
|
||||
covers the screen regardless of how tall the content is. */
|
||||
body::before,
|
||||
body::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(18, 16, 16, 0.1);
|
||||
background-attachment: fixed;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
content: "";
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
animation: flicker 15s infinite;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
}
|
||||
/* Scanlines and aperture grille. repeating-linear-gradient with hard stops
|
||||
replaces background-size tiling: the grille used to be a smooth R->G->B
|
||||
ramp across 3px, where a real grille is three discrete stripes. */
|
||||
body::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background:
|
||||
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
||||
linear-gradient(
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
transparent 0 calc(var(--crt-scanline-pitch) / 2),
|
||||
var(--crt-scanline-color) 0 var(--crt-scanline-pitch)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 0, 0, 0.06),
|
||||
rgba(0, 255, 0, 0.02),
|
||||
rgba(0, 0, 255, 0.06)
|
||||
var(--crt-grille-r) 0 calc(var(--crt-grille-pitch) / 3),
|
||||
var(--crt-grille-g) 0 calc(var(--crt-grille-pitch) / 3 * 2),
|
||||
var(--crt-grille-b) 0 var(--crt-grille-pitch)
|
||||
);
|
||||
z-index: 2;
|
||||
background-size:
|
||||
100% 2px,
|
||||
3px 100%;
|
||||
background-attachment: fixed;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Phosphor flicker. The old version tinted with rgba(18, 16, 16, 0.1) at
|
||||
0.08-0.96 opacity: an effective alpha of 0.008-0.096 of a colour ~7%
|
||||
lighter than the background, i.e. a peak lift of under 2/255. It was
|
||||
invisible. Tinting with the accent at ~3% is small enough to cost no
|
||||
measurable contrast and large enough to actually see. */
|
||||
body::after {
|
||||
animation: crt-flicker 3s step-end infinite;
|
||||
background: var(--color-accent);
|
||||
opacity: var(--crt-flicker-base);
|
||||
}
|
||||
:root[data-reduce-motion="on"] body,
|
||||
:root[data-reduce-motion="on"] body::after {
|
||||
|
|
@ -132,197 +151,122 @@ body::before {
|
|||
animation: none;
|
||||
}
|
||||
}
|
||||
@keyframes flicker {
|
||||
@keyframes crt-flicker {
|
||||
0% {
|
||||
opacity: 0.27861;
|
||||
opacity: 0.03;
|
||||
}
|
||||
5% {
|
||||
opacity: 0.34769;
|
||||
7% {
|
||||
opacity: 0.024;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.23604;
|
||||
13% {
|
||||
opacity: 0.041;
|
||||
}
|
||||
15% {
|
||||
opacity: 0.90626;
|
||||
19% {
|
||||
opacity: 0.028;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.18128;
|
||||
26% {
|
||||
opacity: 0.019;
|
||||
}
|
||||
25% {
|
||||
opacity: 0.83891;
|
||||
31% {
|
||||
opacity: 0.036;
|
||||
}
|
||||
30% {
|
||||
opacity: 0.65583;
|
||||
38% {
|
||||
opacity: 0.026;
|
||||
}
|
||||
35% {
|
||||
opacity: 0.67807;
|
||||
44% {
|
||||
opacity: 0.048;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.26559;
|
||||
51% {
|
||||
opacity: 0.022;
|
||||
}
|
||||
45% {
|
||||
opacity: 0.84693;
|
||||
58% {
|
||||
opacity: 0.033;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.96019;
|
||||
}
|
||||
55% {
|
||||
opacity: 0.08594;
|
||||
}
|
||||
60% {
|
||||
opacity: 0.20313;
|
||||
}
|
||||
65% {
|
||||
opacity: 0.71988;
|
||||
63% {
|
||||
opacity: 0.017;
|
||||
}
|
||||
70% {
|
||||
opacity: 0.53455;
|
||||
opacity: 0.039;
|
||||
}
|
||||
75% {
|
||||
opacity: 0.37288;
|
||||
77% {
|
||||
opacity: 0.027;
|
||||
}
|
||||
80% {
|
||||
opacity: 0.71428;
|
||||
83% {
|
||||
opacity: 0.044;
|
||||
}
|
||||
85% {
|
||||
opacity: 0.70419;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.7003;
|
||||
}
|
||||
95% {
|
||||
opacity: 0.36108;
|
||||
91% {
|
||||
opacity: 0.021;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.24387;
|
||||
opacity: 0.03;
|
||||
}
|
||||
}
|
||||
@keyframes textShadow {
|
||||
/* Normalised from the original per-keyframe pixel offsets, divided by their
|
||||
max (3.8969px), so the rhythm is unchanged but the amplitude is now set in
|
||||
one place by --crt-aberration-max. */
|
||||
@keyframes crt-aberration {
|
||||
0% {
|
||||
text-shadow:
|
||||
0.4389924193300864px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.4389924193300864px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.113;
|
||||
}
|
||||
5% {
|
||||
text-shadow:
|
||||
2.7928974010788217px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.7928974010788217px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.717;
|
||||
}
|
||||
10% {
|
||||
text-shadow:
|
||||
0.02956275843481219px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.02956275843481219px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.008;
|
||||
}
|
||||
15% {
|
||||
text-shadow:
|
||||
0.40218538552878136px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.40218538552878136px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.103;
|
||||
}
|
||||
20% {
|
||||
text-shadow:
|
||||
3.4794037899852017px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.4794037899852017px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.893;
|
||||
}
|
||||
25% {
|
||||
text-shadow:
|
||||
1.6125630401149584px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-1.6125630401149584px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.414;
|
||||
}
|
||||
30% {
|
||||
text-shadow:
|
||||
0.7015590085143956px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.7015590085143956px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.18;
|
||||
}
|
||||
35% {
|
||||
text-shadow:
|
||||
3.896914047650351px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.896914047650351px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 1;
|
||||
}
|
||||
40% {
|
||||
text-shadow:
|
||||
3.870905614848819px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.870905614848819px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.993;
|
||||
}
|
||||
45% {
|
||||
text-shadow:
|
||||
2.231056963361899px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.231056963361899px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.573;
|
||||
}
|
||||
50% {
|
||||
text-shadow:
|
||||
0.08084290417898504px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.08084290417898504px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.021;
|
||||
}
|
||||
55% {
|
||||
text-shadow:
|
||||
2.3758461067427543px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.3758461067427543px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.61;
|
||||
}
|
||||
60% {
|
||||
text-shadow:
|
||||
2.202193051050636px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.202193051050636px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.565;
|
||||
}
|
||||
65% {
|
||||
text-shadow:
|
||||
2.8638780614874975px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.8638780614874975px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.735;
|
||||
}
|
||||
70% {
|
||||
text-shadow:
|
||||
0.48874025155497314px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.48874025155497314px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.125;
|
||||
}
|
||||
75% {
|
||||
text-shadow:
|
||||
1.8948491305757957px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-1.8948491305757957px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.486;
|
||||
}
|
||||
80% {
|
||||
text-shadow:
|
||||
0.0833037308038857px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.0833037308038857px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.021;
|
||||
}
|
||||
85% {
|
||||
text-shadow:
|
||||
0.09769827255241735px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.09769827255241735px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.025;
|
||||
}
|
||||
90% {
|
||||
text-shadow:
|
||||
3.443339761481782px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.443339761481782px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.884;
|
||||
}
|
||||
95% {
|
||||
text-shadow:
|
||||
2.1841838852799786px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.1841838852799786px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.56;
|
||||
}
|
||||
100% {
|
||||
text-shadow:
|
||||
2.6208764473832513px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.6208764473832513px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.673;
|
||||
}
|
||||
}
|
||||
header,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue