06超大型引用文
引用符の後ろの言葉が、超大型サイズのままマスクで単語ごとに現れる。
使いどころ顧客の声・推薦文セクション
clip-path: inset()단어 단위 stagger@keyframes wipe-reveal

<main class="stage">
<figure class="quote is-demo">
<p class="quote__text">「スピードがチームの文化を変えた」</p>
<figcaption class="quote__cite">パク・ジフン・グロースリード</figcaption>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 왼쪽에서 오른쪽으로 한 번 걷히는 주기
$easing: $ease-out;
$color: $subject-ink; // 인용문 색
@include stage($stage-yellow);
.quote {
margin: 0;
text-align: center;
width: min(360px, 90vw);
}
// clip-path 로 왼쪽→오른쪽 와이프 — 01 의 세로 마스크와는 다른 방향
.quote__text {
margin: 0;
color: $color;
font-weight: 800;
font-size: clamp(22px, 6.5vw, 36px);
line-height: 1.3;
animation: wipe-reveal $duration $easing infinite;
}
@keyframes wipe-reveal {
0%, 4% { clip-path: inset(0 100% 0 0); }
55%, 80% { clip-path: inset(0 0 0 0); }
96%, 100% { clip-path: inset(0 100% 0 0); }
}
// 인용문 글자 낱개 — index.html 말미 스크립트가 텍스트를 이 span 으로 쪼개고
// 결정론 해시로 --nx0·--ny0·--nr0(출발점) · --wx·--wy·--wr(힘장 경유점) 을 심는다. (형태2차 한 겹, 077 품질기준선)
// 와이프와 겹쳐 — 걷히는 영역 안에서 글자가 흩어졌다 힘장을 따라 자리 잡는다
.char {
display: inline-block;
animation: char-flow $duration $ease-spring infinite;
}
.char--space { width: .28em; }
@keyframes char-flow {
0%, 4% { transform: translate(var(--nx0, 0), var(--ny0, 18px)) rotate(var(--nr0, 0deg)) scale(.55); opacity: 0; }
16% { transform: translate(var(--wx, 0), var(--wy, 5px)) rotate(var(--wr, 0deg)) scale(1.08); opacity: 1; }
30%, 78% { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
96%, 100% { transform: translate(var(--nx0, 0), var(--ny0, 18px)) rotate(var(--nr0, 0deg)) scale(.55); opacity: 0; }
}
.quote__cite {
display: block;
margin-top: $sp-4;
color: rgba($subject-ink, .72); // AA 4.5 (blog-design 글자 대비)
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
.quote__text { animation: none; clip-path: inset(0 0 0 0); }
.char { animation: none; transform: none; opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.quote {
margin: 0;
text-align: center;
width: min(360px, 90vw);
}
.quote__text {
margin: 0;
color: #17141a;
font-weight: 800;
font-size: clamp(22px, 6.5vw, 36px);
line-height: 1.3;
animation: wipe-reveal 2s ease-out infinite;
}
@keyframes wipe-reveal {
0%, 4% {
clip-path: inset(0 100% 0 0);
}
55%, 80% {
clip-path: inset(0 0 0 0);
}
96%, 100% {
clip-path: inset(0 100% 0 0);
}
}
.char {
display: inline-block;
animation: char-flow 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.char--space {
width: 0.28em;
}
@keyframes char-flow {
0%, 4% {
transform: translate(var(--nx0, 0), var(--ny0, 18px)) rotate(var(--nr0, 0deg)) scale(0.55);
opacity: 0;
}
16% {
transform: translate(var(--wx, 0), var(--wy, 5px)) rotate(var(--wr, 0deg)) scale(1.08);
opacity: 1;
}
30%, 78% {
transform: translate(0, 0) rotate(0deg) scale(1);
opacity: 1;
}
96%, 100% {
transform: translate(var(--nx0, 0), var(--ny0, 18px)) rotate(var(--nr0, 0deg)) scale(0.55);
opacity: 0;
}
}
.quote__cite {
display: block;
margin-top: 16px;
color: rgba(23, 20, 26, 0.72);
font-weight: 600;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
.quote__text {
animation: none;
clip-path: inset(0 0 0 0);
}
.char {
animation: none;
transform: none;
opacity: 1;
}
}// 인용문 글자별 노이즈 오프셋 + 벡터필드 진입 (형태2차) — 문장을 span 으로 쪼개고
// 결정론 해시로 출발점/경유점 변수만 심는다. 프레임은 style.scss 의 char-flow 키프레임이 つくる。.
// 기존 wipe-reveal(clip-path 와이프) 과 겹쳐 — 와이프가 지나가는 동안 글자가 흩어졌다 힘장을 따라 자리 잡는다.
(function () {
function hash(n) { // 정수 → 0..1 결정론 난수 (sin 해시)
var s = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return s - Math.floor(s);
}
document.querySelectorAll('.quote .quote__text').forEach(function (q) {
var text = q.textContent;
q.textContent = '';
for (var c = 0; c < text.length; c++) {
var ch = document.createElement('span');
if (text[c] === ' ') { ch.className = 'char char--space'; }
else {
ch.className = 'char';
ch.textContent = text[c];
var s = c * 13 + 5;
var h1 = hash(s), h2 = hash(s + 1), h3 = hash(s + 2), h4 = hash(s + 3);
var ang = h1 * Math.PI * 2;
var rad = 14 + h2 * 26;
var x0 = Math.cos(ang) * rad - 18, y0 = 18 + h3 * 16; // 왼쪽 위에서 시작해 오른쪽 아래로 흐른다
var rot0 = (h4 - .5) * 34;
var wx = x0 - 0.9 * y0 * 0.55, wy = y0 + 0.9 * x0 * 0.55;
var wr = rot0 * 0.4;
ch.style.setProperty('--nx0', x0.toFixed(1) + 'px');
ch.style.setProperty('--ny0', y0.toFixed(1) + 'px');
ch.style.setProperty('--nr0', rot0.toFixed(1) + 'deg');
ch.style.setProperty('--wx', (wx * .6).toFixed(1) + 'px');
ch.style.setProperty('--wy', (wy * .18 + 3).toFixed(1) + 'px');
ch.style.setProperty('--wr', wr.toFixed(1) + 'deg');
ch.style.animationDelay = (-0.035 * c).toFixed(3) + 's';
}
q.appendChild(ch);
}
});
})();import type { CSSProperties } from "react";
import styles from "./KineticTestimonial.module.scss";
export type KineticTestimonialProps = {
quote?: string;
cite?: string;
/** 왼쪽에서 오른쪽으로 한 번 걷히는 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type CharVars = CSSProperties & {
"--nx0"?: string; "--ny0"?: string; "--nr0"?: string;
"--wx"?: string; "--wy"?: string; "--wr"?: string;
};
/** 정수 → 0..1 결정론 난수 (sin 해시 — vanilla 06 과 동일 계열) */
function hash(n: number): number {
const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return s - Math.floor(s);
}
/** 한 글자 = 출발점(노이즈) + 힘장 경유점 변수 묶음. vanilla index.html 스크립트와 같은 식. */
function charVars(c: number): CharVars {
const s = c * 13 + 5;
const h1 = hash(s), h2 = hash(s + 1), h3 = hash(s + 2), h4 = hash(s + 3);
const ang = h1 * Math.PI * 2;
const rad = 14 + h2 * 26;
const x0 = Math.cos(ang) * rad - 18, y0 = 18 + h3 * 16; // 왼쪽 위에서 시작해 오른쪽 아래로 흐른다
const rot0 = (h4 - .5) * 34;
// 회전 힘장 v(p) = ω × p 를 출발점에서 dt 적분 — v = ω*(-y, x), dt=0.55, ω=0.9
const wx = x0 - 0.9 * y0 * 0.55, wy = y0 + 0.9 * x0 * 0.55;
const wr = rot0 * 0.4;
return {
"--nx0": `${x0.toFixed(1)}px`,
"--ny0": `${y0.toFixed(1)}px`,
"--nr0": `${rot0.toFixed(1)}deg`,
"--wx": `${(wx * .6).toFixed(1)}px`,
"--wy": `${(wy * .18 + 3).toFixed(1)}px`,
"--wr": `${wr.toFixed(1)}deg`,
animationDelay: `${(-0.035 * c).toFixed(3)}s`,
};
}
export default function KineticTestimonial({
quote = "“Speed changed how our team works.”",
cite = "Park Ji-hoon · Growth Lead",
duration = 2000,
easing = "ease-out",
color = "#17141a",
}: KineticTestimonialProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars}>
<figure className={styles.quote}>
<p className={styles.text}>
{quote.split("").map((ch, c) =>
ch === " " ? (
<span key={c} className={styles.charSpace} />
) : (
<span key={c} className={styles.char} style={charVars(c)}>{ch}</span>
)
)}
</p>
<figcaption className={styles.cite}>{cite}</figcaption>
</figure>
</div>
);
}06 超大型引用文 — 計測 累積面積 7.105% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · キネティックタイポグラフィをコードで分解してみた
このデモが属する記事
キネティックタイポグラフィをコードで分解してみたキネティックタイポグラフィとは、見出しの文字サイズが画面幅に合わせて変わり、単語が順番に浮かび上がり、数字がオドメーターのように上がっていくデザイン手法のことです。最近のランディングページはどれもファーストビューがこれなので、実際にコードを この記事のデモをすべて見る


