Fix scroll-driven proof graphics

This commit is contained in:
Yun Chan 2026-08-30 21:49:10 +09:00
parent dcb1415aaa
commit 3c39a1731d
4 changed files with 523 additions and 182 deletions

View file

@ -269,12 +269,30 @@ const copiedLabel = lang === "ko" ? "복사했어요" : "Copied";
}
.stage-list {
position: relative;
list-style: none;
margin: 0;
padding: 0;
padding: 0 0 0 var(--space-5);
border-top: var(--line-width) solid var(--line-inverse);
}
.stage-list::before,
.stage-list::after {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: 0;
width: 2px;
pointer-events: none;
}
.stage-list::before { background: var(--line-inverse); }
.stage-list::after {
background: var(--accent);
transform-origin: top center;
scale: 1 1;
}
.stage-list li {
position: relative;
display: grid;
@ -297,24 +315,21 @@ const copiedLabel = lang === "ko" ? "복사했어요" : "Copied";
}
@supports (animation-timeline: view()) {
.stage-list {
view-timeline-name: --process-list;
view-timeline-axis: block;
}
@media (prefers-reduced-motion: no-preference) {
.stage-list li::after {
content: "";
position: absolute;
inset-inline-start: 0;
bottom: calc(var(--line-width) * -1);
width: 2.5rem;
height: var(--line-width);
background: var(--accent);
transform-origin: left center;
animation: stage-rail-register linear both;
animation-timeline: view(block);
animation-range: entry 12% cover 32%;
.stage-list::after {
animation: process-rail-fill linear both;
animation-timeline: --process-list;
animation-range: entry 0% cover 72%;
}
@keyframes stage-rail-register {
from { opacity: 0.25; transform: scaleX(0); }
to { opacity: 1; transform: scaleX(1); }
@keyframes process-rail-fill {
from { scale: 1 0; }
to { scale: 1 1; }
}
}
}
@ -458,6 +473,7 @@ const copiedLabel = lang === "ko" ? "복사했어요" : "Copied";
@media (prefers-reduced-motion: reduce) {
.audit-details summary svg { transition: none; }
.stage-list::after { animation: none; scale: 1 1; }
}
@media (max-width: 68rem) {
@ -489,6 +505,7 @@ const copiedLabel = lang === "ko" ? "복사했어요" : "Copied";
grid-template-columns: 2rem minmax(0, 1fr);
gap: var(--space-2) var(--space-4);
}
.stage-list { padding-inline-start: var(--space-4); }
.stage-list p { grid-column: 2; }
}

View file

@ -62,14 +62,15 @@ const copy = lang === "en"
</header>
<div class="direction-stage">
<svg viewBox="0 0 760 380" aria-hidden="true" preserveAspectRatio="none">
<svg viewBox="0 0 760 380" aria-hidden="true" preserveAspectRatio="xMidYMid meet">
<path class="direction-field" d="M42 320 242 56l241 86 234 162-337 44Z" />
<path class="direction-field direction-field-inner" d="m130 295 134-177 190 68 157 109-231 30Z" />
<path class="direction-axis" d="M42 320 380 348 242 56m138 292 337-44M380 348 483 142" />
<path class="direction-vector" d="m194 275 186 73 175-99" />
<circle class="direction-node" cx="194" cy="275" r="7" />
<circle class="direction-node" cx="380" cy="348" r="7" />
<circle class="direction-node" cx="555" cy="249" r="7" />
<path class="direction-vector direction-segment direction-segment-1" d="M194 275 380 348" />
<path class="direction-vector direction-segment direction-segment-2" d="M380 348 555 249" />
<circle class="direction-node direction-node-1" cx="194" cy="275" r="7" />
<circle class="direction-node direction-node-2" cx="380" cy="348" r="7" />
<circle class="direction-node direction-node-3" cx="555" cy="249" r="7" />
</svg>
<ol class="direction-axes">
@ -94,8 +95,8 @@ const copy = lang === "en"
<div class="type-stage">
<div class="type-glyphs" role="img" aria-label={`${copy.tokens.specimen} ${copy.tokens.koreanSpecimen}`}>
<span aria-hidden="true">{copy.tokens.specimen}</span>
<span aria-hidden="true">{copy.tokens.koreanSpecimen}</span>
<span class="type-glyph type-glyph-latin" aria-hidden="true">{copy.tokens.specimen}</span>
<span class="type-glyph type-glyph-korean" aria-hidden="true">{copy.tokens.koreanSpecimen}</span>
</div>
<div class="type-baselines" aria-hidden="true">
@ -212,7 +213,7 @@ const copy = lang === "en"
.direction-stage {
position: relative;
min-height: 24rem;
aspect-ratio: 2 / 1;
overflow: clip;
background: var(--surface-soft);
border-bottom: var(--line-width) solid var(--line);
@ -252,6 +253,16 @@ const copy = lang === "en"
vector-effect: non-scaling-stroke;
}
.direction-segment-1 {
transform-box: fill-box;
transform-origin: left top;
}
.direction-segment-2 {
transform-box: fill-box;
transform-origin: left bottom;
}
.direction-node {
fill: var(--accent);
stroke: var(--surface-raised);
@ -355,7 +366,7 @@ const copy = lang === "en"
.token-register dd { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
.token-sample {
width: 2.75rem;
width: clamp(3.75rem, 6vw, 5rem);
height: 0.5rem;
border: var(--line-width) solid var(--line-inverse);
}
@ -444,8 +455,8 @@ const copy = lang === "en"
view-timeline-axis: block;
}
.specimen-proof {
view-timeline-name: --proof-specimen;
.proof-stage {
view-timeline-name: --proof-stage;
view-timeline-axis: block;
}
@ -453,17 +464,14 @@ const copy = lang === "en"
.direction-field-inner {
animation: direction-plane-in linear both;
animation-timeline: --direction-stage;
animation-range: entry 32% cover 58%;
animation-range: cover 30% cover 50%;
transform-box: fill-box;
transform-origin: center;
}
.direction-vector {
animation: direction-vector-in linear both;
.direction-segment {
animation: direction-segment-in linear both;
animation-timeline: --direction-stage;
animation-range: entry 64% cover 64%;
transform-box: fill-box;
transform-origin: left center;
}
.direction-node {
@ -473,31 +481,40 @@ const copy = lang === "en"
transform-origin: center;
}
.direction-node:nth-of-type(1) {
--direction-node-x: calc(var(--space-5) * -1);
--direction-node-y: var(--space-4);
animation-range: entry 64% cover 58%;
}
.direction-node-1 { animation-range: cover 18% cover 24%; }
.direction-segment-1 { animation-range: cover 20% cover 56%; }
.direction-node-2 { animation-range: cover 56% cover 62%; }
.direction-segment-2 { animation-range: cover 58% cover 80%; }
.direction-node-3 { animation-range: cover 80% cover 86%; }
.direction-node:nth-of-type(2) {
--direction-node-x: 0px;
--direction-node-y: var(--space-5);
animation-range: entry 88% cover 64%;
}
.direction-node:nth-of-type(3) {
--direction-node-x: var(--space-5);
--direction-node-y: calc(var(--space-4) * -1);
animation-range: entry 72% cover 64%;
}
.type-glyphs {
animation: type-register linear both;
.type-glyph {
animation-timeline: --type-stage;
animation-range: entry 12% cover 58%;
transform-origin: left bottom;
transform-origin: center bottom;
}
.type-glyph-latin {
animation: type-glyph-latin-in linear both;
animation-timeline: --type-stage;
animation-range: cover 20% cover 48%;
}
.type-glyph-korean {
animation: type-glyph-korean-in linear both;
animation-timeline: --type-stage;
animation-range: cover 30% cover 58%;
}
.type-baselines span {
animation: type-baseline-in linear both;
animation-timeline: --type-stage;
transform-origin: left center;
}
.type-baselines span:nth-child(1) { animation-range: cover 24% cover 54%; }
.type-baselines span:nth-child(2) { animation-range: cover 30% cover 60%; }
.type-baselines span:nth-child(3) { animation-range: cover 36% cover 66%; }
.type-baselines span:nth-child(4) { animation-range: cover 42% cover 72%; }
.token-sample {
animation: token-resolve linear both;
animation-timeline: --type-stage;
@ -505,94 +522,105 @@ const copy = lang === "en"
}
.token-register > div:nth-child(1) .token-sample {
animation-range: entry 60% cover 58%;
animation-range: cover 40% cover 84%;
}
.token-register > div:nth-child(2) .token-sample {
animation-range: entry 72% cover 64%;
animation-range: cover 46% cover 90%;
}
.token-register > div:nth-child(3) .token-sample {
animation-range: entry 84% cover 70%;
animation-range: cover 52% cover 96%;
}
.viewport {
.viewport-medium,
.viewport-narrow {
animation: frame-resolve linear both;
animation-timeline: --proof-specimen;
animation-range: entry 8% cover 32%;
animation-timeline: --proof-stage;
transform-origin: center bottom;
}
.viewport-narrow {
--frame-x: var(--space-6);
--frame-y: var(--space-3);
--frame-x: 190%;
--frame-y: 8%;
--frame-scale: 0.72;
animation-range: entry 25% cover 60%;
}
.viewport-medium {
--frame-x: var(--space-4);
--frame-y: var(--space-2);
}
.viewport-wide {
--frame-x: 0;
--frame-y: 0;
--frame-x: 95%;
--frame-y: 4%;
--frame-scale: 0.86;
animation-range: entry 10% cover 42%;
}
.check-register svg {
animation: check-pass linear both;
animation-timeline: --proof-specimen;
animation-timeline: --proof-stage;
transform-box: fill-box;
transform-origin: center;
transform-origin: left center;
}
.check-register li:nth-child(1) svg {
animation-range: entry 30% cover 42%;
animation-range: cover 34% cover 46%;
}
.check-register li:nth-child(2) svg {
animation-range: entry 34% cover 46%;
animation-range: cover 40% cover 52%;
}
.check-register li:nth-child(3) svg {
animation-range: entry 38% cover 50%;
animation-range: cover 46% cover 58%;
}
.check-register li:nth-child(4) svg {
animation-range: entry 42% cover 54%;
animation-range: cover 52% cover 64%;
}
@keyframes direction-plane-in {
from { opacity: 0.72; translate: 0 var(--space-5); scale: 0.92 0.92; }
from { opacity: 0.62; translate: 0 var(--space-6); scale: 0.84 0.84; }
to { opacity: 1; translate: 0 0; scale: 1 1; }
}
@keyframes direction-vector-in {
from { opacity: 0.08; translate: calc(var(--space-2) * -1) var(--space-1); scale: 0.12 1; }
to { opacity: 1; translate: 0 0; scale: 1 1; }
}
@keyframes direction-node-in {
from { opacity: 0.08; translate: var(--direction-node-x) var(--direction-node-y); scale: 0.35 0.35; }
to { opacity: 1; translate: 0 0; scale: 1 1; }
}
@keyframes type-register {
from { translate: 0 var(--space-5); scale: 0.92 0.92; }
to { translate: 0 0; scale: 1 1; }
}
@keyframes token-resolve {
from { opacity: 0.18; scale: 0.04 1; }
@keyframes direction-segment-in {
from { opacity: 0; scale: 0 0; }
5% { opacity: 1; }
to { opacity: 1; scale: 1 1; }
}
@keyframes direction-node-in {
from { opacity: 0; scale: 0.45 0.45; }
to { opacity: 1; scale: 1 1; }
}
@keyframes type-glyph-latin-in {
from { translate: 3rem 0; scale: 0.84 0.84; }
to { translate: 0 0; scale: 1 1; }
}
@keyframes type-glyph-korean-in {
from { translate: -2.5rem 0; scale: 0.82 0.82; }
to { translate: 0 0; scale: 1 1; }
}
@keyframes type-baseline-in {
from { scale: 0.08 1; }
to { scale: 1 1; }
}
@keyframes token-resolve {
from { scale: 0.05 1; }
to { scale: 1 1; }
}
@keyframes frame-resolve {
from { translate: var(--frame-x) var(--frame-y); }
to { translate: 0 0; }
from { translate: var(--frame-x) var(--frame-y); scale: var(--frame-scale) var(--frame-scale); }
to { translate: 0 0; scale: 1 1; }
}
@keyframes check-pass {
from { opacity: 0.2; translate: calc(var(--space-2) * -1) 0; }
to { opacity: 1; translate: 0 0; }
from { opacity: 0.28; translate: calc(var(--space-4) * -1) 0; scale: 0.35 1; }
to { opacity: 1; translate: 0 0; scale: 1 1; }
}
}
}
@ -608,58 +636,10 @@ const copy = lang === "en"
.specimen-direction,
.specimen-tokens,
.specimen-proof { grid-column: 1; margin-top: 0; }
.direction-stage { min-height: 21rem; }
.direction-stage { aspect-ratio: 16 / 9; }
.type-stage { min-height: 24rem; }
.proof-stage { grid-template-columns: minmax(0, 1fr); }
.viewport-family { min-height: 18rem; border-right: 0; border-bottom: var(--line-width) solid var(--line); }
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.direction-node:nth-of-type(1) {
--direction-node-x: calc(var(--space-4) * -1);
--direction-node-y: var(--space-3);
}
.direction-node:nth-of-type(2) {
--direction-node-y: var(--space-4);
}
.direction-node:nth-of-type(3) {
--direction-node-x: var(--space-4);
--direction-node-y: calc(var(--space-3) * -1);
}
.viewport-narrow {
--frame-x: var(--space-3);
--frame-y: var(--space-2);
}
.viewport-medium {
--frame-x: var(--space-2);
--frame-y: var(--space-1);
}
.viewport {
animation-range: entry 4% cover 28%;
}
.check-register li:nth-child(1) svg {
animation-range: entry 26% cover 38%;
}
.check-register li:nth-child(2) svg {
animation-range: entry 30% cover 42%;
}
.check-register li:nth-child(3) svg {
animation-range: entry 34% cover 46%;
}
.check-register li:nth-child(4) svg {
animation-range: entry 38% cover 50%;
}
}
}
}
@media (max-width: 28rem) {
@ -673,16 +653,22 @@ const copy = lang === "en"
@media (prefers-reduced-motion: reduce) {
.direction-field-inner,
.direction-vector,
.direction-segment,
.direction-node,
.type-glyphs,
.type-glyph,
.token-sample,
.viewport,
.viewport-medium,
.viewport-narrow,
.check-register svg {
animation: none;
opacity: 1;
translate: 0 0;
scale: 1 1;
}
.type-baselines span {
animation: none;
scale: 1 1;
}
}
</style>