The 12 principles of animation

애니메이션 12원칙

Twelve rules for making a drawing feel alive, codified by Disney animators Ollie Johnston and Frank Thomas in their 1981 book The Illusion of Life. Much of today's UI motion vocabulary traces back to this list.

Also known as: Disney의 12원칙Ollie Johnston & Frank Thomas12 Basic Principles of Animation
···
html
<div class="hud"><span id="pname">Squash &amp; Stretch</span><span id="pcount">1 / 12</span></div>
<canvas id="pc"></canvas>
css
.hud{position:absolute;top:7%;left:6%;right:6%;z-index:2;display:flex;justify-content:space-between;align-items:baseline;
  font:700 clamp(12px,3.4vmin,17px)/1 sans-serif;color:var(--fg)}
#pcount{font:11px/1 ui-monospace,monospace;color:var(--muted);font-weight:400}
#pc{position:absolute;inset:0;display:block;width:100%;height:100%}
js
const canvas = document.getElementById('pc');
const ctx = canvas.getContext('2d');
const nameEl = document.getElementById('pname');
const countEl = document.getElementById('pcount');
const cs = getComputedStyle(document.documentElement);
function col(name, fb) { const v = cs.getPropertyValue(name).trim(); return v || fb; }
const FG = col('--fg', '#15151a');
const MUTED = col('--muted', '#6b6b76');
const LINE = col('--line', '#e2e0d8');
const ACCENT = col('--accent', '#5b5bf7');
const ACCENT2 = col('--accent-2', '#f25c8a');
const ACCENT3 = col('--accent-3', '#18c29c');
let w = 0, h = 0;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  canvas.width = w * dpr; canvas.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();

function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
function R() { return h * 0.065; }
function ball(x, y, rx, ry, color, alpha) {
  ctx.globalAlpha = alpha === undefined ? 1 : alpha;
  ctx.beginPath();
  ctx.ellipse(x, y, Math.max(rx, 1), Math.max(ry, 1), 0, 0, Math.PI * 2);
  ctx.fillStyle = color;
  ctx.fill();
  ctx.globalAlpha = 1;
}
function seg(x1, y1, x2, y2, color, width, dash) {
  ctx.strokeStyle = color; ctx.lineWidth = width || 1;
  ctx.setLineDash(dash || []);
  ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
  ctx.setLineDash([]);
}

const PHASES = [
  { name: 'Squash & Stretch', draw: function (t) {
    const gy = h * 0.72;
    seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
    const b = Math.abs(Math.sin(t * Math.PI));
    const y = gy - b * h * 0.4;
    const sq = 1 - b;
    ball(w * 0.5, y - R() * (1 - sq * 0.3), R() * (1 + sq * 0.5), R() * (1 - sq * 0.35), ACCENT);
  } },
  { name: 'Anticipation', draw: function (t) {
    const gy = h * 0.55;
    seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
    const x = t < 0.3
      ? w * 0.2 - easeOut(t / 0.3) * w * 0.06
      : w * 0.14 + easeOut(Math.min((t - 0.3) / 0.55, 1)) * w * 0.62;
    ball(x, gy, R(), R(), ACCENT);
  } },
  { name: 'Staging', draw: function (t) {
    const gy = h * 0.55;
    ball(w * 0.22, gy, R() * 0.8, R() * 0.8, MUTED, 0.25);
    ball(w * 0.78, gy, R() * 0.8, R() * 0.8, MUTED, 0.25);
    const pulse = 1 + Math.sin(t * Math.PI * 2) * 0.1;
    ball(w * 0.5, gy, R() * 1.3 * pulse, R() * 1.3 * pulse, ACCENT2);
  } },
  { name: 'Straight Ahead vs Pose to Pose', draw: function (t) {
    const y1 = h * 0.32, y2 = h * 0.68;
    seg(w * 0.1, y1, w * 0.9, y1, LINE, 1, [2, 4]);
    seg(w * 0.1, y2, w * 0.9, y2, LINE, 1, [2, 4]);
    const x1 = w * 0.15 + t * w * 0.7;
    ball(x1, y1 + Math.sin(t * 22) * h * 0.05, R() * 0.7, R() * 0.7, ACCENT);
    const poses = [0.15, 0.4, 0.65, 0.85];
    const idx = Math.min(Math.floor(t * poses.length), poses.length - 1);
    ball(w * poses[idx], y2, R() * 0.85, R() * 0.85, ACCENT2);
  } },
  { name: 'Follow Through & Overlap', draw: function (t) {
    const gy = h * 0.55;
    const x = w * 0.5 + Math.sin(t * Math.PI * 2) * w * 0.32;
    ball(w * 0.5 + Math.sin((t - 0.09) * Math.PI * 2) * w * 0.32, gy, R() * 0.65, R() * 0.65, ACCENT, 0.35);
    ball(w * 0.5 + Math.sin((t - 0.045) * Math.PI * 2) * w * 0.32, gy, R() * 0.82, R() * 0.82, ACCENT, 0.6);
    ball(x, gy, R(), R(), ACCENT);
  } },
  { name: 'Slow In & Slow Out', draw: function (t) {
    const gy = h * 0.55;
    for (let i = 0; i <= 8; i++) {
      const px = w * 0.15 + easeInOut(i / 8) * w * 0.7;
      ball(px, gy, R() * 0.16, R() * 0.16, MUTED, 0.5);
    }
    ball(w * 0.15 + easeInOut(t) * w * 0.7, gy, R() * 0.85, R() * 0.85, ACCENT3);
  } },
  { name: 'Arcs', draw: function (t) {
    const x0 = w * 0.15, x1 = w * 0.85, gy = h * 0.68, apex = h * 0.22;
    seg(x0, gy, x1, gy, LINE, 1, [2, 4]);
    ctx.beginPath();
    for (let i = 0; i <= 30; i++) {
      const tt = i / 30;
      const px = x0 + (x1 - x0) * tt;
      const py = gy - Math.sin(tt * Math.PI) * (gy - apex);
      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    ctx.strokeStyle = LINE; ctx.lineWidth = 1; ctx.stroke();
    const px = x0 + (x1 - x0) * t;
    const py = gy - Math.sin(t * Math.PI) * (gy - apex);
    ball(px, py, R() * 0.75, R() * 0.75, ACCENT);
  } },
  { name: 'Secondary Action', draw: function (t) {
    const gy = h * 0.55;
    const x = w * 0.15 + t * w * 0.7;
    ball(x, gy, R(), R(), ACCENT);
    const ang = t * Math.PI * 10;
    ball(x + Math.cos(ang) * R() * 1.7, gy + Math.sin(ang) * R() * 1.7, R() * 0.3, R() * 0.3, ACCENT2);
  } },
  { name: 'Timing', draw: function (t) {
    const y1 = h * 0.32, y2 = h * 0.68;
    const x = w * 0.15 + t * w * 0.7;
    ball(x, y1 - Math.abs(Math.sin(t * 3 * Math.PI)) * h * 0.15, R() * 0.5, R() * 0.5, ACCENT);
    ball(x, y2 - Math.abs(Math.sin(t * Math.PI)) * h * 0.28, R() * 1.15, R() * 1.15, ACCENT2);
  } },
  { name: 'Exaggeration', draw: function (t) {
    const gy = h * 0.72;
    seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
    const b = Math.abs(Math.sin(t * Math.PI));
    const sq = 1 - b;
    const y = gy - b * h * 0.46;
    ball(w * 0.35, gy - Math.abs(Math.sin(t * Math.PI)) * h * 0.25 - R() * 0.5, R() * 0.5, R() * 0.5, MUTED, 0.4);
    ball(w * 0.65, y - R() * 1.15 * (1 - sq * 0.9), R() * 1.15 * (1 + sq * 1.5), R() * 1.15 * (1 - sq * 0.8), ACCENT2);
  } },
  { name: 'Solid Drawing', draw: function (t) {
    const cx = w * 0.5, cy = h * 0.55;
    const rot = t * Math.PI * 2;
    const rx = Math.max(Math.abs(Math.cos(rot)) * R() * 1.3, R() * 0.35);
    ball(cx, cy, rx, R() * 1.3, ACCENT);
    ball(cx + Math.sin(rot) * rx * 0.4, cy - R() * 0.5, R() * 0.22, R() * 0.32, '#ffffff', 0.5);
  } },
  { name: 'Appeal', draw: function (t) {
    const cx = w * 0.5, cy = h * 0.58;
    const breathe = 1 + Math.sin(t * Math.PI * 2) * 0.04;
    const rx = R() * 1.5 * breathe, ry = R() * 1.3 * breathe;
    ball(cx, cy, rx, ry, ACCENT3);
    const blinkFrac = t % 1;
    const blink = blinkFrac > 0.92 ? 0.15 : 1;
    ball(cx - rx * 0.32, cy - ry * 0.15, R() * 0.14, R() * 0.14 * blink, FG);
    ball(cx + rx * 0.32, cy - ry * 0.15, R() * 0.14, R() * 0.14 * blink, FG);
    ctx.strokeStyle = FG; ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(cx, cy + ry * 0.28, rx * 0.28, 0.15 * Math.PI, 0.85 * Math.PI);
    ctx.stroke();
  } },
];

let phaseIdx = -1;
const DUR = 1900;
const startT = performance.now();
function loop(now) {
  const elapsed = Math.max(0, now - startT);
  const idx = Math.floor(elapsed / DUR) % PHASES.length;
  const t = (elapsed % DUR) / DUR;
  if (idx !== phaseIdx) {
    phaseIdx = idx;
    nameEl.textContent = PHASES[idx].name;
    countEl.textContent = (idx + 1) + ' / ' + PHASES.length;
  }
  ctx.clearRect(0, 0, w, h);
  PHASES[idx].draw(t);
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

Ollie Johnston and Frank Thomas distilled decades of trial and error from Disney's animators (1930s–80s) into twelve rules: squash and stretch, anticipation, staging, straight ahead action and pose to pose, follow through and overlapping action, slow in and slow out, arcs, secondary action, timing, exaggeration, solid drawing, and appeal.

They were written for hand-drawn cel animation, but most of them answer a more general question — what does it take to make an object seem to obey real physics and emotion? — so they carry over cleanly into 3D, games, and today's UI motion. A button dipping before it "fires" (anticipation), a modal that dims everything but the one thing it wants noticed (staging), a drag that keeps drifting a beat after release (follow through) — these are all UI translations of the same twelve rules.

The demo below cycles through all twelve, one at a time, as a quick overview. Each principle gets its own, more detailed entry linked below.

When to use

Don't cram all twelve into one piece of UI motion. Most interactions only need two or three at once — save the rest for other moments.