Compare commits
No commits in common. "worktree-crt-effects" and "main" have entirely different histories.
worktree-c
...
main
2 changed files with 186 additions and 178 deletions
|
|
@ -2,20 +2,6 @@
|
||||||
@import "reset.css" layer(reset);
|
@import "reset.css" layer(reset);
|
||||||
@import "theme.css" layer(theme);
|
@import "theme.css" layer(theme);
|
||||||
|
|
||||||
/* The chromatic-aberration offset, as a 0..1 multiplier of
|
|
||||||
--crt-aberration-max. Registering it gives it a typed initial value, so the
|
|
||||||
calc() in body's text-shadow stays valid when the animation is not running.
|
|
||||||
Note the var(--crt-shift, 0) fallbacks at the use sites: without one, an
|
|
||||||
unresolved --crt-shift makes the calc() invalid at computed-value time,
|
|
||||||
which drops the *whole* text-shadow — glow included — not just the smear.
|
|
||||||
The animation itself does not depend on this rule, because step-end never
|
|
||||||
interpolates; it only ever substitutes a literal keyframe value. */
|
|
||||||
@property --crt-shift {
|
|
||||||
syntax: "<number>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "unscii16";
|
font-family: "unscii16";
|
||||||
src:
|
src:
|
||||||
|
|
@ -59,37 +45,13 @@
|
||||||
:root {
|
:root {
|
||||||
/* Color */
|
/* Color */
|
||||||
--color-fg: oklch(1 0 0);
|
--color-fg: oklch(1 0 0);
|
||||||
/* 0.62, not 0.5: at 0.5 this was 3.5:1 on the background, under AA, and
|
--color-mg: oklch(0.5 0 0);
|
||||||
1.7:1 against the centre of the CRT vignette. 0.62 gives 5.8:1 / 4.4:1. */
|
|
||||||
--color-mg: oklch(0.62 0 0);
|
|
||||||
--color-bg: oklch(0 0 0);
|
--color-bg: oklch(0 0 0);
|
||||||
--color-accent: oklch(0.6 0.18 142.5);
|
--color-accent: oklch(0.6 0.18 142.5);
|
||||||
--color-accent-bright: oklch(0.8 0.18 142.5);
|
--color-accent-bright: oklch(0.8 0.18 142.5);
|
||||||
--color-glow: oklch(0.8328 0 0);
|
--color-glow: oklch(0.8328 0 0);
|
||||||
--color-accent-transparent: oklch(0.6 0.18 142.5 / 50%);
|
--color-accent-transparent: oklch(0.6 0.18 142.5 / 50%);
|
||||||
|
|
||||||
/* CRT
|
|
||||||
The raster (scanline + aperture grille) and the phosphor (vignette,
|
|
||||||
glow, chromatic aberration). Every value the effect uses lives here so
|
|
||||||
the whole look can be retuned without touching the keyframes. */
|
|
||||||
--crt-scanline-pitch: 2px;
|
|
||||||
--crt-grille-pitch: 3px;
|
|
||||||
--crt-scanline-color: rgb(0 0 0 / 0.25);
|
|
||||||
--crt-grille-r: rgb(255 0 0 / 0.06);
|
|
||||||
--crt-grille-g: rgb(0 255 0 / 0.02);
|
|
||||||
--crt-grille-b: rgb(0 0 255 / 0.06);
|
|
||||||
/* Deliberately not --color-accent-transparent, which ProjectCard uses for
|
|
||||||
its box-shadow glow and wants at 50%. As a full-screen wash behind text,
|
|
||||||
50% costs too much contrast; 25% keeps --color-mg above 4.5:1. */
|
|
||||||
--crt-vignette-core: oklch(0.6 0.18 142.5 / 25%);
|
|
||||||
--crt-vignette-edge: var(--color-bg);
|
|
||||||
--crt-shadow-cool: rgb(0 30 255 / 0.5);
|
|
||||||
--crt-shadow-warm: rgb(255 0 80 / 0.3);
|
|
||||||
--crt-glow-blur: 3px;
|
|
||||||
/* In em so the smear scales with the fluid root font-size. */
|
|
||||||
--crt-aberration-max: 0.15em;
|
|
||||||
--crt-flicker-base: 0.03;
|
|
||||||
|
|
||||||
/* Spacing */
|
/* Spacing */
|
||||||
--gutter-large: clamp(3ch, 2.5vmax, 3.75ch);
|
--gutter-large: clamp(3ch, 2.5vmax, 3.75ch);
|
||||||
--stack-large: clamp(1.25ex, 2.5vmax, 1.75ex);
|
--stack-large: clamp(1.25ex, 2.5vmax, 1.75ex);
|
||||||
|
|
@ -150,16 +112,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The raster is specified in CSS px, so on a 2dppx panel a 2px scanline is
|
|
||||||
4 device px — coarse, and on fractional DPR it resamples into moire.
|
|
||||||
Tightening the pitch keeps the apparent scale roughly constant. */
|
|
||||||
@media (min-resolution: 2dppx) {
|
|
||||||
:root {
|
|
||||||
--crt-scanline-pitch: 1.5px;
|
|
||||||
--crt-grille-pitch: 2.25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
312
public/theme.css
312
public/theme.css
|
|
@ -39,21 +39,12 @@ html {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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 {
|
body {
|
||||||
animation: crt-aberration 1.6s step-end infinite;
|
animation: textShadow 1.6s 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-color: var(--color-bg);
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
var(--crt-vignette-core),
|
var(--color-accent-transparent),
|
||||||
var(--crt-vignette-edge) 120%
|
black 120%
|
||||||
);
|
);
|
||||||
color: var(--color-fg);
|
color: var(--color-fg);
|
||||||
column-gap: var(--gutter-large);
|
column-gap: var(--gutter-large);
|
||||||
|
|
@ -67,56 +58,46 @@ body {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
row-gap: var(--stack-large);
|
row-gap: var(--stack-large);
|
||||||
/* Single source of truth for the glow: when the animation is off,
|
text-shadow: 0 0 5px var(--color-glow);
|
||||||
--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;
|
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 {
|
body::after {
|
||||||
content: " ";
|
content: " ";
|
||||||
inset: 0;
|
display: block;
|
||||||
pointer-events: none;
|
position: absolute;
|
||||||
position: fixed;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(18, 16, 16, 0.1);
|
||||||
|
background-attachment: fixed;
|
||||||
|
opacity: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: flicker 15s infinite;
|
||||||
}
|
}
|
||||||
/* 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 {
|
body::before {
|
||||||
background-image:
|
content: " ";
|
||||||
repeating-linear-gradient(
|
display: block;
|
||||||
transparent 0 calc(var(--crt-scanline-pitch) / 2),
|
position: absolute;
|
||||||
var(--crt-scanline-color) 0 var(--crt-scanline-pitch)
|
top: 0;
|
||||||
),
|
left: 0;
|
||||||
repeating-linear-gradient(
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
||||||
|
linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
var(--crt-grille-r) 0 calc(var(--crt-grille-pitch) / 3),
|
rgba(255, 0, 0, 0.06),
|
||||||
var(--crt-grille-g) 0 calc(var(--crt-grille-pitch) / 3 * 2),
|
rgba(0, 255, 0, 0.02),
|
||||||
var(--crt-grille-b) 0 var(--crt-grille-pitch)
|
rgba(0, 0, 255, 0.06)
|
||||||
);
|
);
|
||||||
}
|
z-index: 2;
|
||||||
/* Phosphor flicker. The old version tinted with rgba(18, 16, 16, 0.1) at
|
background-size:
|
||||||
0.08-0.96 opacity: an effective alpha of 0.008-0.096 of a colour ~7%
|
100% 2px,
|
||||||
lighter than the background, i.e. a peak lift of under 2/255. It was
|
3px 100%;
|
||||||
invisible. Tinting with the accent at ~3% is small enough to cost no
|
background-attachment: fixed;
|
||||||
measurable contrast and large enough to actually see. */
|
pointer-events: none;
|
||||||
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,
|
||||||
:root[data-reduce-motion="on"] body::after {
|
:root[data-reduce-motion="on"] body::after {
|
||||||
|
|
@ -151,122 +132,197 @@ body::after {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes crt-flicker {
|
@keyframes flicker {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0.03;
|
opacity: 0.27861;
|
||||||
}
|
|
||||||
7% {
|
|
||||||
opacity: 0.024;
|
|
||||||
}
|
|
||||||
13% {
|
|
||||||
opacity: 0.041;
|
|
||||||
}
|
|
||||||
19% {
|
|
||||||
opacity: 0.028;
|
|
||||||
}
|
|
||||||
26% {
|
|
||||||
opacity: 0.019;
|
|
||||||
}
|
|
||||||
31% {
|
|
||||||
opacity: 0.036;
|
|
||||||
}
|
|
||||||
38% {
|
|
||||||
opacity: 0.026;
|
|
||||||
}
|
|
||||||
44% {
|
|
||||||
opacity: 0.048;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
opacity: 0.022;
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
opacity: 0.033;
|
|
||||||
}
|
|
||||||
63% {
|
|
||||||
opacity: 0.017;
|
|
||||||
}
|
|
||||||
70% {
|
|
||||||
opacity: 0.039;
|
|
||||||
}
|
|
||||||
77% {
|
|
||||||
opacity: 0.027;
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
opacity: 0.044;
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
opacity: 0.021;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.03;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* 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% {
|
|
||||||
--crt-shift: 0.113;
|
|
||||||
}
|
}
|
||||||
5% {
|
5% {
|
||||||
--crt-shift: 0.717;
|
opacity: 0.34769;
|
||||||
}
|
}
|
||||||
10% {
|
10% {
|
||||||
--crt-shift: 0.008;
|
opacity: 0.23604;
|
||||||
}
|
}
|
||||||
15% {
|
15% {
|
||||||
--crt-shift: 0.103;
|
opacity: 0.90626;
|
||||||
}
|
}
|
||||||
20% {
|
20% {
|
||||||
--crt-shift: 0.893;
|
opacity: 0.18128;
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
--crt-shift: 0.414;
|
opacity: 0.83891;
|
||||||
}
|
}
|
||||||
30% {
|
30% {
|
||||||
--crt-shift: 0.18;
|
opacity: 0.65583;
|
||||||
}
|
}
|
||||||
35% {
|
35% {
|
||||||
--crt-shift: 1;
|
opacity: 0.67807;
|
||||||
}
|
}
|
||||||
40% {
|
40% {
|
||||||
--crt-shift: 0.993;
|
opacity: 0.26559;
|
||||||
}
|
}
|
||||||
45% {
|
45% {
|
||||||
--crt-shift: 0.573;
|
opacity: 0.84693;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
--crt-shift: 0.021;
|
opacity: 0.96019;
|
||||||
}
|
}
|
||||||
55% {
|
55% {
|
||||||
--crt-shift: 0.61;
|
opacity: 0.08594;
|
||||||
}
|
}
|
||||||
60% {
|
60% {
|
||||||
--crt-shift: 0.565;
|
opacity: 0.20313;
|
||||||
}
|
}
|
||||||
65% {
|
65% {
|
||||||
--crt-shift: 0.735;
|
opacity: 0.71988;
|
||||||
}
|
}
|
||||||
70% {
|
70% {
|
||||||
--crt-shift: 0.125;
|
opacity: 0.53455;
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
--crt-shift: 0.486;
|
opacity: 0.37288;
|
||||||
}
|
}
|
||||||
80% {
|
80% {
|
||||||
--crt-shift: 0.021;
|
opacity: 0.71428;
|
||||||
}
|
}
|
||||||
85% {
|
85% {
|
||||||
--crt-shift: 0.025;
|
opacity: 0.70419;
|
||||||
}
|
}
|
||||||
90% {
|
90% {
|
||||||
--crt-shift: 0.884;
|
opacity: 0.7003;
|
||||||
}
|
}
|
||||||
95% {
|
95% {
|
||||||
--crt-shift: 0.56;
|
opacity: 0.36108;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
--crt-shift: 0.673;
|
opacity: 0.24387;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes textShadow {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
header,
|
header,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue