상호작용 모션 — 전부 피드백 목적으로만

모션은 이유가 있어야 붙인다. 여기 넣은 것은 전부 '지금 이걸 보고 있다'
또는 '이건 눌린다'를 말한다. 장식 목적 애니메이션은 넣지 않았다.

- 유리 카드 29곳: 커서 아래에서 3px 떠오르고 그림자가 깊어진다
- 링크 2곳: 밑줄이 왼쪽에서 자란다
- 쇼케이스 판 2곳: 1.2% 다가온다
- 스파인 정거장: 마커가 커지고 인용선이 밝아진다

전부 transform 과 opacity 만 움직인다(하드 게이트 8).
prefers-reduced-motion 안에 넣어 모션을 줄이는 설정에서는 전부 사라진다.
This commit is contained in:
Yun Chan 2026-08-20 12:25:22 +09:00
parent 85be000704
commit ed1f335a89
9 changed files with 57 additions and 8 deletions

View file

@ -33,7 +33,7 @@ const { c, ui } = Astro.props;
</div> </div>
<p class="hero-links"> <p class="hero-links">
{c.links.map((l) => <a href={l.href}>{l.label}</a>)} {c.links.map((l) => <a class="link-grow" href={l.href}>{l.label}</a>)}
</p> </p>
</div> </div>

View file

@ -30,7 +30,7 @@ const { c, ui } = Astro.props;
<dl class="cmds"> <dl class="cmds">
{ {
c.commands.map((x) => ( c.commands.map((x) => (
<div class="cmd-row glass"> <div class="cmd-row glass lift">
<dt class="mono">{x.cmd}</dt> <dt class="mono">{x.cmd}</dt>
<dd class="muted">{x.what}</dd> <dd class="muted">{x.what}</dd>
</div> </div>

View file

@ -23,7 +23,7 @@ const { c } = Astro.props;
{ {
c.tiles.map((t, i) => ( c.tiles.map((t, i) => (
<li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}> <li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}>
<div class="shell"> <div class="shell lift">
<div class="core"> <div class="core">
<div class={`swatch swatch-${t.id}`} aria-hidden="true"></div> <div class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
</div> </div>

View file

@ -16,7 +16,7 @@ const rest = c.items.filter((m) => !m.feature);
<div class="full bento"> <div class="full bento">
{ {
feature.map((m, i) => ( feature.map((m, i) => (
<div class="cell cell-lg glass glass-2 reveal" style={`--reveal-delay:${i * 70}ms`}> <div class="cell cell-lg glass glass-2 lift reveal" style={`--reveal-delay:${i * 70}ms`}>
<span class="cell-value nums">{m.value}</span> <span class="cell-value nums">{m.value}</span>
<span class="cell-label">{m.label}</span> <span class="cell-label">{m.label}</span>
<span class="cell-note muted">{m.note}</span> <span class="cell-note muted">{m.note}</span>
@ -25,7 +25,7 @@ const rest = c.items.filter((m) => !m.feature);
} }
{ {
rest.map((m, i) => ( rest.map((m, i) => (
<div class="cell glass reveal" style={`--reveal-delay:${(i + 2) * 70}ms`}> <div class="cell glass lift reveal" style={`--reveal-delay:${(i + 2) * 70}ms`}>
<span class="cell-value nums">{m.value}</span> <span class="cell-value nums">{m.value}</span>
<span class="cell-label">{m.label}</span> <span class="cell-label">{m.label}</span>
<span class="cell-note muted">{m.note}</span> <span class="cell-note muted">{m.note}</span>

View file

@ -114,6 +114,15 @@ const { c } = Astro.props;
padding-left: var(--space-3); padding-left: var(--space-3);
border-left: 2px solid var(--accent); border-left: 2px solid var(--accent);
} }
@media (prefers-reduced-motion: no-preference) {
.stop::before {
transition: transform var(--dur-quick) var(--ease-out);
}
.stop:hover::before { transform: scale(1.5); }
.stop-becomes { transition: border-color var(--dur-quick) var(--ease-out); }
.stop:hover .stop-becomes { border-color: var(--ink); }
}
.stop-becomes.mono { .stop-becomes.mono {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--mono-size); font-size: var(--mono-size);

View file

@ -13,7 +13,7 @@ const { c } = Astro.props;
<div class="full log"> <div class="full log">
{ {
c.items.map((i, idx) => ( c.items.map((i, idx) => (
<article class="entry glass reveal" style={`--reveal-delay:${idx * 60}ms`}> <article class="entry glass lift reveal" style={`--reveal-delay:${idx * 60}ms`}>
<p class="caught"> <p class="caught">
<span class="tick" aria-hidden="true" /> <span class="tick" aria-hidden="true" />
{i.caught} {i.caught}

View file

@ -13,7 +13,7 @@ const { c } = Astro.props;
<dl class="full rule-list"> <dl class="full rule-list">
{ {
c.items.map((r, i) => ( c.items.map((r, i) => (
<div class="rule glass reveal" style={`--reveal-delay:${i * 55}ms`}> <div class="rule glass lift reveal" style={`--reveal-delay:${i * 55}ms`}>
<dt>{r.rule}</dt> <dt>{r.rule}</dt>
<dd class="muted">{r.why}</dd> <dd class="muted">{r.why}</dd>
</div> </div>

View file

@ -18,7 +18,7 @@ const shots = [shotCoffee, shotSaas];
{ {
c.items.map((item, i) => ( c.items.map((item, i) => (
<figure class:list={["work", "reveal", i === 0 ? "par-slow" : "par-fast"]} style={`--reveal-delay:${i * 90}ms`}> <figure class:list={["work", "reveal", i === 0 ? "par-slow" : "par-fast"]} style={`--reveal-delay:${i * 90}ms`}>
<div class="shell"> <div class="shell zoom-in">
<div class="core"> <div class="core">
<Image src={shots[i]!} alt={item.alt} widths={[480, 860, 1200]} <Image src={shots[i]!} alt={item.alt} widths={[480, 860, 1200]}
sizes="(max-width: 820px) 92vw, 44vw" loading="lazy" /> sizes="(max-width: 820px) 92vw, 44vw" loading="lazy" />

View file

@ -271,6 +271,46 @@ a:hover { color: var(--accent); }
.reveal.is-in { transform: none; } .reveal.is-in { transform: none; }
} }
/* ---- 상호작용 모션 ------------------------------------------
모션은 이유가 있어야 붙인다. 여기 있는 것은 전부 피드백이다.
"지금 이 카드를 보고 있다", "이 버튼은 눌린다" 말한다.
transform opacity 움직인다(하드 게이트 8). */
@media (prefers-reduced-motion: no-preference) {
/* 유리 카드는 커서 아래에서 아주 조금 떠오른다 */
.lift {
transition:
transform var(--dur-quick) var(--ease-out),
box-shadow var(--dur-quick) var(--ease-out);
}
.lift:hover {
transform: translate3d(0, -3px, 0);
box-shadow: var(--glass-hi), var(--shadow-3);
}
/* 링크 밑줄이 커서 쪽에서 자란다 */
.link-grow {
position: relative;
text-decoration: none;
}
.link-grow::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -3px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-quick) var(--ease-out);
}
.link-grow:hover::after { transform: scaleX(1); }
/* 쇼케이스 판은 들여다보면 조금 다가온다 */
.zoom-in { transition: transform var(--dur-normal) var(--ease-out); }
.zoom-in:hover { transform: scale(1.012); }
}
/* ---- 패럴랙스 ----------------------------------------------- /* ---- 패럴랙스 -----------------------------------------------
scroll-driven animation 으로만 만든다. scroll 리스너는 쓰지 않는다. scroll-driven animation 으로만 만든다. scroll 리스너는 쓰지 않는다.
움직이는 것은 transform 뿐이라 지원되지 않는 브라우저에서는 움직이는 것은 transform 뿐이라 지원되지 않는 브라우저에서는