126가지 모션 효과를 자체 모션 엔진(Luna Motion v1.0)으로 구현한 라이브 데모 · 코드 샘플 모음. 이징·스태거·타임라인·스프링·SVG·드래그·파티클·로더·게임·물리·트랜지션·글리치·차트·마스코트·UI 알림·오디오·페이퍼까지 — 외부 라이브러리 없이 순수 JS 한 파일로 동작. 각 카드의 "코드 보기" 버튼으로 바로 복사해 다른 도구에 적용할 수 있습니다.
// 격자가 중심에서 바깥으로 차례로 등장 const grid = document.querySelectorAll('#grid1 > div'); lm.motion({ targets: grid, scale: [0, 1], opacity: [0, 1], duration: 700, delay: lm.stagger(40, { grid: [8, 4], from: 'center' }), easing: 'outBack', });
// 막대들이 사인파처럼 위아래 출렁 lm.motion({ targets: '#wave2 > div', translateY: [0, -40], duration: 800, delay: lm.stagger(60, { from: 'center' }), loop: true, direction: 'alternate', easing: 'ioSine', });
// 원 궤도 회전 — translateX 후 rotate lm.motion({ targets: '#planet3a', rotate: [0, 360], translateX: 60, duration: 3000, loop: true, easing: 'linear', });
// 마우스 따라 부드럽게 끌려옴 const el = document.querySelector('#magnet4'); wrap.addEventListener('pointermove', e => { const r = wrap.getBoundingClientRect(); const dx = e.clientX - r.left - r.width/2; const dy = e.clientY - r.top - r.height/2; lm.motion({ targets: el, translateX: dx*0.3, translateY: dy*0.3, duration: 400, easing: 'outQuart', }); });
// 위에서 떨어져 통통 튀며 등장 lm.motion({ targets: '#bounce5 > div', translateY: [-200, 0], opacity: [0, 1], duration: 1200, delay: lm.stagger(100), easing: 'outBounce', });
// 글자별로 분할 후 위에서 떨어짐 const spans = lm.splitText($('#letter6')); lm.motion({ targets: spans, translateY: [-30, 0], opacity: [0, 1], rotate: [-15, 0], duration: 800, delay: lm.stagger(60), easing: 'outBack', });
// 단어 단위로 분할 const words = lm.splitText($('#word7'), { by: 'word' }); lm.motion({ targets: words, translateY: [14, 0], opacity: [0, 1], duration: 600, delay: lm.stagger(90), });
// 한 글자씩 타자치는 효과 const text = "> init luna.whale.tech"; let i = 0; const timer = setInterval(() => { el.textContent = text.slice(0, ++i); if (i >= text.length) clearInterval(timer); }, 80);
// 무작위 글자가 차례로 정답으로 복원 function scramble(el, target) { const chars = "!@#$%^&*()_+ABC0123"; let step = 0; const id = setInterval(() => { el.textContent = target.split('').map((c, i) => i < step ? c : chars[Math.floor(Math.random() * chars.length)] ).join(''); step++; if (step > target.length) clearInterval(id); }, 60); }
// 글자가 위아래로 영구 출렁 const spans = lm.splitText($('#textwave10')); lm.motion({ targets: spans, translateY: [0, -12], duration: 800, delay: lm.stagger(70, { from: 'center' }), loop: true, direction: 'alternate', easing: 'ioSine', });
// 패스를 한 번에 그리는 효과 const path = $('#path11'); const len = lm.pathLength(path); path.setAttribute('stroke-dasharray', len); lm.motion({ targets: path, strokeDashoffset: [len, 0], duration: 1500, easing: 'ioCubic', });
// 점좌표 보간으로 다각형 모핑 const A = [[100,30],[170,80],[145,160],[55,160],[30,80]]; const B = [[100,100],[100,100],[100,100],[100,100],[100,100]]; tween(0, 1, t => { poly.setAttribute('points', A.map(([x,y],i) => (x+(B[i][0]-x)*t)+','+(y+(B[i][1]-y)*t)) .join(' ')); });
// 여러 path를 timeline 으로 순서 있게 그림 const tl = lm.timeline(); parts.forEach((p, i) => { const len = lm.pathLength(p) || 120; p.setAttribute('stroke-dasharray', len); tl.add({ targets: p, strokeDashoffset: [len, 0], duration: 700, }, i*200); }); tl.play();
// 사인파 d 속성 보간 lm.motion({ targets: { phase: 0 }, phase: 2*Math.PI, duration: 2000, loop: true, easing: 'linear', update: t => { const d = buildSineD(t * 2*Math.PI); path.setAttribute('d', d); }, });
// 점이 패스를 따라 움직임 (getPointAtLength) const path = $('#path15'); const len = path.getTotalLength(); lm.motion({ targets: { p: 0 }, p: 1, duration: 2500, loop: true, easing: 'ioSine', update: t => { const pt = path.getPointAtLength(len*t); follower.style.transform = `translate(${pt.x}px, ${pt.y}px)`; }, });
// hue-rotate 필터로 색상환 회전 lm.motion({ targets: el, filter: ['hue-rotate(0deg)', 'hue-rotate(360deg)'], duration: 3000, loop: true, easing: 'linear', });
// box-shadow + scale 무한 펄스 lm.motion({ targets: el, scale: [1, 1.3], boxShadow: ['0 0 0px #06b6d4', '0 0 60px #ec4899'], duration: 800, loop: true, direction: 'alternate', easing: 'ioSine', });
// 회색 → 무지개로 차례차례 색칠 const palette = ['#ef4444','#f97316','#fbbf24','#10b981','#06b6d4','#8b5cf6','#ec4899']; dots.forEach((d, i) => { lm.motion({ targets: d, backgroundColor: ['#64748b', palette[i]], scale: [0.7, 1.1, 1], duration: 600, delay: i*100, easing: 'outBack', }); });
// background-position 으로 그라디언트 이동 lm.motion({ targets: { p: 0 }, p: 100, duration: 3000, loop: true, easing: 'linear', update: t => el.style.backgroundPosition = (t*400) + '% 0', });
// blur 12px → 0 풀림 lm.motion({ targets: { b: 12 }, b: 0, duration: 1200, easing: 'outQuart', update: t => { const b = 12 - t*12; el.style.filter = `blur(${b}px)`; el.style.opacity = t; }, });
// rotateY 0 → 180 (preserve-3d 필요) lm.motion({ targets: card, rotateY: [0, 180], duration: 1000, loop: true, direction: 'alternate', easing: 'ioCubic', });
// rotateX + rotateY 무한 회전 lm.motion({ targets: cube, rotateX: [0, 360], rotateY: [0, 360], duration: 5000, loop: true, easing: 'linear', });
// 마우스 위치에 따라 perspective 기울이기 wrap.addEventListener('pointermove', e => { const r = wrap.getBoundingClientRect(); const rx = ((e.clientY-r.top)/r.height-.5) * -30; const ry = ((e.clientX-r.left)/r.width-.5) * 30; card.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`; });
// 적층 카드가 펼쳐짐 cards.forEach((c, i) => { lm.motion({ targets: c, translateX: [0, (i-1.5)*60], translateY: [0, 0], rotate: [0, (i-1.5)*8], duration: 800, delay: i*80, easing: 'outBack', }); });
// 깊이에 따라 다른 속도로 이동 (data-depth 활용) wrap.addEventListener('pointermove', e => { layers.forEach(L => { const d = +L.dataset.depth; L.style.transform = `translate(${dx*d}px, ${dy*d}px)`; }); });
// 드래그 후 가장 가까운 zone으로 spring snap let dragging = false, sx, sy, x = 0, y = 0; handle.addEventListener('pointerdown', e => { dragging = true; sx = e.clientX - x; sy = e.clientY - y; }); addEventListener('pointerup', () => { if (!dragging) return; dragging = false; const snap = findNearestZone(x, y); lm.motion({ targets: handle, translateX: snap.x, translateY: snap.y, duration: 700, easing: lm.spring({ stiffness: 120, damping: 10 }), }); });
// 클릭 위치에서 원이 퍼져나감 stage.addEventListener('click', e => { const r = stage.getBoundingClientRect(); const rip = document.createElement('div'); rip.className = 'ripple'; rip.style.left = (e.clientX - r.left) + 'px'; rip.style.top = (e.clientY - r.top) + 'px'; stage.appendChild(rip); lm.motion({ targets: rip, width: ['0px', '200px'], height: ['0px', '200px'], opacity: [1, 0], duration: 800, complete: () => rip.remove(), }); });
// IntersectionObserver + lm.motion const io = new IntersectionObserver(es => { es.forEach(e => { if (e.isIntersecting) { lm.motion({ targets: e.target, translateY: [20, 0], opacity: [0, 1], duration: 500, }); io.unobserve(e.target); } }); }, { root: stage }); items.forEach(it => io.observe(it));
// 버튼 → spring physics 로 thumb 이동 btns.forEach(b => b.addEventListener('click', () => { const p = +b.dataset.pos; lm.motion({ targets: thumb, left: [thumb.style.left || '0%', (p*100)+'%'], duration: 800, easing: lm.spring({ stiffness: 160, damping: 12 }), }); }));
// 0 → 9999 부드럽게 카운트업 lm.motion({ targets: { v: 0 }, v: 9999, duration: 2000, easing: 'outQuart', update: t => el.textContent = Math.round(t*9999).toLocaleString(), });
// 클릭 위치에서 파티클이 사방으로 폭발 function burst(x, y) { for (let i = 0; i < 30; i++) { const p = make('particle'); p.style.background = randomColor(); const ang = (i/30)*Math.PI*2; const dist = lm.random(40, 100); lm.motion({ targets: p, translateX: [0, Math.cos(ang)*dist], translateY: [0, Math.sin(ang)*dist], opacity: [1, 0], scale: [1, 0.3], duration: 800, easing: 'outQuart', complete: () => p.remove(), }); } }
// 위에서 색종이가 회전하며 떨어짐 for (let i = 0; i < 40; i++) { const c = make('confetti'); c.style.left = lm.random(0, 100) + '%'; c.style.background = randomColor(); lm.motion({ targets: c, translateY: [0, 240], rotate: [0, lm.random(-360, 720)], duration: lm.random(1500, 3000), delay: i*30, easing: 'inQuad', }); }
// 마우스 이동마다 작은 점이 사라짐 stage.addEventListener('pointermove', e => { const r = stage.getBoundingClientRect(); const dot = make('trail-dot'); dot.style.left = (e.clientX - r.left) + 'px'; dot.style.top = (e.clientY - r.top) + 'px'; lm.motion({ targets: dot, scale: [1, 0], opacity: [1, 0], duration: 600, complete: () => dot.remove(), }); });
// 격자 점이 마우스에서 멀어지는 방향으로 휨 field.addEventListener('pointermove', e => { dots.forEach(d => { const dx = d.x - mx, dy = d.y - my; const dist = Math.hypot(dx, dy); const push = Math.max(0, 60 - dist); d.el.style.transform = `translate(${dx/dist*push}px,${dy/dist*push}px)`; }); });
// 여러 통계 동시 카운트업 nums.forEach(n => { const target = +n.dataset.target; lm.motion({ targets: { v: 0 }, v: target, duration: 1500, easing: 'outQuart', update: t => n.textContent = Math.round(t*target).toLocaleString(), }); });
// 타임라인으로 순차 합성 (overlap 가능) const tl = lm.timeline({ easing: 'outBack', duration: 800 }); tl.add({ targets: '#seq36a', scale: [0,1.4,1], rotate: [0,180] }) .add({ targets: '#seq36b', translateY: [-30,0] }, '-=400') .add({ targets: '#seq36c', scale: [0,1], opacity: [0,1] }, '-=400') .play();
// 컬러 그라디언트 보더 회전 lm.motion({ targets: el, rotate: [0, 360], duration: 900, loop: true, easing: 'linear', });
// 점 3개가 차례로 커지고 작아지기 lm.motion({ targets: '#pulse38 > div', scale: [0.6, 1.4], opacity: [0.4, 1], duration: 600, delay: lm.stagger(150), loop: true, direction: 'alternate', easing: 'ioSine', });
// 0% → 100% 채워지면서 그라디언트 흐르기 lm.motion({ targets: { p: 0 }, p: 100, duration: 2000, easing: 'outQuart', update: t => { fill.style.width = (t*100) + '%'; fill.style.backgroundPosition = (t*200) + '% 0'; }, });
// pathLength=100 으로 dashoffset이 곧 % 진행률 lm.motion({ targets: { p: 0 }, p: 87, duration: 2000, easing: 'outQuart', update: t => { const pct = t*87; ring.setAttribute('stroke-dashoffset', 100-pct); text.textContent = Math.round(pct) + '%'; }, });
// gradient background-position 으로 빛 흐르기 lm.motion({ targets: { p: 0 }, p: 1, duration: 1400, loop: true, easing: 'linear', update: t => lines.forEach(L => L.style.backgroundPosition = (-100 + t*200) + '% 0'), });
// 16개 도트가 사인파처럼 위아래 lm.motion({ targets: '#dotwave42 > div', height: ['8px', '40px'], duration: 700, delay: lm.stagger(50, { from: 'center' }), loop: true, direction: 'alternate', easing: 'ioSine', });
// rotateY 회전 + translateY 포물선 const spins = 5 + lm.random(0, 2); lm.motion({ targets: coin, rotateY: [0, 360*spins], duration: 1500, easing: 'outQuart', update: t => coin.style.translate = `0 ${-Math.sin(t*Math.PI)*60}px`, });
// 5장이 섞이며 펼쳐졌다가 다시 모이기 const tl = lm.timeline({ duration: 600, easing: 'outBack' }); cards.forEach((c, i) => tl.add({ targets: c, translateX: (i-2) * 50, rotate: (i-2) * 8, }, i*60)); tl.play();
// rotateX/Y 무작위 spin 후 정해진 면 정지 const rx = 720 + lm.random(0, 3)*90; const ry = 720 + lm.random(0, 3)*90; lm.motion({ targets: dice, rotateX: [0, rx], rotateY: [0, ry], duration: 1400, easing: 'outCubic', });
// 점수 카운트 + 작은 +N 라벨이 위로 떠올라 사라짐 lm.motion({ targets: { v: prevScore }, v: newScore, duration: 800, easing: 'outQuart', update: t => el.textContent = Math.round(prevScore + (newScore-prevScore)*t), }); spawnPopLabel('+250');
// 콤보 증가 시마다 통통 + scale 펄스 function addCombo() { combo++; el.textContent = combo; lm.motion({ targets: el, scale: [1.5, 1], rotate: [lm.random(-12,12), 0], duration: 400, easing: 'outBack', }); }
// 텍스트 등장 + 빛줄기 12개 회전 폭발 lm.motion({ targets: text, scale: [0, 1.3, 1], opacity: [0, 1], duration: 800, easing: 'outBack' }); rays.forEach((r, i) => lm.motion({ targets: r, rotate: [(i*30), (i*30) + 90], scale: [0, 1], opacity: [1, 0], duration: 1000, easing: 'outQuart', }));
// 감쇠 진동: angle = A·cos(ωt)·e^(-bt) lm.motion({ targets: { t: 0 }, t: 1, duration: 5000, loop: true, easing: 'linear', update: t => { const a = 45 * Math.cos(t*12) * Math.exp(-t*0.4); arm.style.transform = `rotate(${a}deg)`; }, });
// 8개 노드가 마우스를 따라 지연된 chain 운동 nodes.forEach((n, i) => { n.x += (target.x - n.x) * (0.15 + i*0.05); n.y += (target.y - n.y) * (0.15 + i*0.05); target = n; });
// outBounce 이징 + scale로 충돌 압축 lm.motion({ targets: ball, translateY: [0, 150], scaleY: [1, 0.7, 1], duration: 1500, loop: true, direction: 'alternate', easing: 'outBounce', });
// 잎이 좌우 흔들리며 회전+낙하 leaves.forEach(L => lm.motion({ targets: { t: 0 }, t: 1, duration: lm.random(3000,5000), loop: true, easing: 'linear', update: t => { L.style.transform = `translate(${Math.sin(t*8)*30}px, ${t*220}px) rotate(${t*720}deg)`; }, }));
// 사인파 두 개 합성으로 출렁이는 액체 표면 function buildPath(ph) { let d = 'M 0 180'; for (let x = 0; x <= 220; x += 5) { const y = 90 + Math.sin(x/40+ph)*10 + Math.sin(x/15+ph*2)*5; d += ` L ${x} ${y}`; } return d + ' L 220 180 Z'; }
// 연기 puff: 위로 떠오르며 커지고 사라짐 lm.motion({ targets: { t: 0 }, t: 1, duration: 2000, easing: 'outQuart', update: t => { puff.style.transform = `translate(-50%, ${-t*120}px) scale(${1 + t*2})`; puff.style.opacity = 1 - t; }, });
// 좌우 커튼이 양옆으로 슬라이드 lm.motion({ targets: '.curtain.left', translateX: [0, '-100%'], duration: 1000, easing: 'ioCubic' }); lm.motion({ targets: '.curtain.right', translateX: [0, '100%'], duration: 1000, easing: 'ioCubic' });
// clip-path circle 반지름이 0 → max lm.motion({ targets: { r: 0 }, r: 160, duration: 1200, easing: 'ioCubic', update: t => mask.style.clipPath = `circle(${(1-t)*160}px at 50% 50%)`, });
// translateX -100% 단위로 자동 회전 let idx = 0; setInterval(() => { idx = (idx + 1) % 3; lm.motion({ targets: track, translateX: `${-idx*100}%`, duration: 600, easing: 'ioCubic', }); }, 2000);
// 카드들이 도미노처럼 차례로 뒤집힘 lm.motion({ targets: cards, rotateX: [90, 0], opacity: [0, 1], duration: 700, delay: lm.stagger(80), easing: 'outBack', });
// 7줄 줄무늬가 좌/우 번갈아 슬라이드 stripes.forEach((s, i) => lm.motion({ targets: s, translateX: [0, i % 2 ? '100%' : '-100%'], duration: 800, delay: i*80, easing: 'ioCubic', }));
// 스케일 0 → 6 + opacity 1 → 0 으로 화면 밖으로 폭발 lm.motion({ targets: el, scale: [0, 6], opacity: [1, 0], duration: 1200, easing: 'outQuart', });
// ::before / ::after 두 레이어 상하 흔들림 lm.motion({ targets: { t: 0 }, t: 1, duration: 200, loop: true, easing: 'linear', update: () => { el.style.setProperty('--gx1', lm.random(-3,3) + 'px'); el.style.setProperty('--gx2', lm.random(-3,3) + 'px'); }, });
// 3개 채널이 미세하게 다른 위치로 이동 lm.motion({ targets: layers, translateX: [0, (el, i) => (i-1)*6], duration: 300, loop: true, direction: 'alternate', });
// 가로 빛줄기가 위에서 아래로 무한 통과 lm.motion({ targets: scan, translateY: ['-10px', '140px'], duration: 2200, loop: true, easing: 'linear', });
// feTurbulence baseFrequency 변화로 왜곡 강도 조절 lm.motion({ targets: { f: 0.02 }, f: 0.08, duration: 2000, loop: true, direction: 'alternate', update: t => turb.setAttribute('baseFrequency', 0.02 + t*0.06), });
// CSS image-rendering + filter blur로 픽셀화 풀어짐 lm.motion({ targets: { p: 20 }, p: 0, duration: 1500, easing: 'outQuart', update: t => el.style.filter = `blur(${(1-t)*16}px) contrast(${1+t*0.3})`, });
// Canvas ImageData에 무작위 픽셀 매 프레임 그리기 function drawNoise() { const img = ctx.createImageData(w, h); for (let i = 0; i < img.data.length; i += 4) { const v = Math.random() * 255; img.data[i] = img.data[i+1] = img.data[i+2] = v; img.data[i+3] = 255; } ctx.putImageData(img, 0, 0); }
// 막대들이 0에서 데이터 높이로 차례로 자람 lm.motion({ targets: bars, height: (el, i) => data[i] + 'px', duration: 900, delay: lm.stagger(60), easing: 'outBack', });
// pathLength=100 이라 dasharray가 곧 % 비율 const percentages = [35, 25, 40]; let offset = 0; slices.forEach((s, i) => { s.setAttribute('stroke-dashoffset', -offset); lm.motion({ targets: s, strokeDasharray: ['0 100', `${percentages[i]} 100`], duration: 1000, delay: i*200, }); offset += percentages[i]; });
// 데이터 포인트가 차례로 그려지며 polyline 연장 lm.motion({ targets: { i: 0 }, i: data.length, duration: 1500, easing: 'outCubic', update: t => { const n = Math.floor(t * data.length); line.setAttribute('points', data.slice(0, n+1).map((d,i) => `${i*30},${140-d}`).join(' ')); }, });
// 셀 색상이 stagger로 어두움 → 강도 색 cells.forEach((c, i) => { const v = data[i]; lm.motion({ targets: c, backgroundColor: ['rgba(6,182,212,0.05)', `rgba(6,182,212,${v})`], duration: 400, delay: i*15, }); });
// progress ring과 동일하지만 두께가 도넛 비율 lm.motion({ targets: { p: 0 }, p: 73, duration: 1800, easing: 'outQuart', update: t => { ring.setAttribute('stroke-dashoffset', 100 - t*73); text.textContent = Math.round(t*73) + '%'; }, });
// 미니 라인차트 + 채워진 영역 lm.motion({ targets: { i: 0 }, i: data.length, duration: 1500, update: t => redrawSpark(Math.floor(t*data.length)), });
// 위아래 출렁 + 좌우 미세 흔들 + 호흡 scale lm.motion({ targets: el, translateY: [0, -12], rotate: [-3, 3], scale: [1, 1.05], duration: 2000, loop: true, direction: 'alternate', easing: 'ioSine', });
// 거품이 아래에서 위로 떠오르며 좌우 흔들 lm.motion({ targets: { t: 0 }, t: 1, duration: 3000, easing: 'linear', update: t => { bubble.style.transform = `translate(${Math.sin(t*6)*15}px, ${-t*200}px) scale(${1+t*0.3})`; bubble.style.opacity = 1-t; }, });
// 마우스 따라 ✦ 별이 회전하며 사라짐 stage.addEventListener('pointermove', e => { const sp = make('sparkle'); sp.style.left = e.x + 'px'; sp.style.top = e.y + 'px'; lm.motion({ targets: sp, rotate: [0, 360], scale: [1.5, 0], opacity: [1, 0], duration: 800, complete: () => sp.remove(), }); });
// 별들이 반짝반짝 — 무작위 opacity stars.forEach(s => lm.motion({ targets: s, opacity: [0.2, 1], scale: [0.6, 1.2], duration: lm.random(800,1800), delay: lm.random(0,2000), loop: true, direction: 'alternate', easing: 'ioSine', }));
// 클릭하면 ❤️ 가 위로 떠오름 (좋아요 효과) stage.addEventListener('click', e => { const h = make('heart'); h.textContent = '❤️'; h.style.left = e.x + 'px'; h.style.top = e.y + 'px'; lm.motion({ targets: h, translateY: [0, -100], translateX: [0, lm.random(-30,30)], scale: [0.5, 1.5], opacity: [1, 0], duration: 1200, }); });
// 구름들이 다른 속도로 좌→우 영구 흐름 clouds.forEach(c => lm.motion({ targets: c, translateX: ['-60px', '400px'], duration: lm.random(8000,15000), loop: true, easing: 'linear', }));
// 오른쪽에서 들어오고 3초 후 사라짐 lm.motion({ targets: toast, translateX: ['120%', '0%'], opacity: [0, 1], duration: 500, easing: 'outBack', }); setTimeout(() => lm.motion({ targets: toast, translateX: ['0%', '120%'], duration: 400, complete: () => toast.remove(), }), 2500);
// 백드롭 페이드 + 박스 scale spring lm.motion({ targets: backdrop, opacity: [0, 1], duration: 300 }); lm.motion({ targets: box, scale: [0.7, 1], opacity: [0, 1], duration: 700, easing: lm.spring({ stiffness: 160, damping: 12 }), });
// rotateX로 위에서 펼쳐지듯 등장 lm.motion({ targets: bubble, translateY: ['10px', '0px'], rotateX: [-90, 0], opacity: [0, 1], duration: 350, easing: 'outBack', });
// 빨간 점이 펄스 + 숫자 등장 시 통통 lm.motion({ targets: dot, scale: [1, 1.3, 1], duration: 600, loop: true, easing: 'ioSine', });
// 아래에서 슬라이드 업, UNDO 버튼 포함 lm.motion({ targets: snack, translateY: ['80px', '0px'], opacity: [0, 1], duration: 450, easing: 'outBack', });
// 위에서 바운스로 떨어짐 (공지 배너) lm.motion({ targets: banner, translateY: ['-100%', '0%'], duration: 700, easing: 'outBounce', });
// 12개 막대가 각자 다른 주파수로 출렁 bars.forEach((b, i) => lm.motion({ targets: b, height: ['8px', lm.random(40,120) + 'px'], duration: lm.random(300,600), loop: true, direction: 'alternate', easing: 'ioSine', }));
// 사인파 + 무작위 변조로 음성 파형 흉내 lm.motion({ targets: { t: 0 }, t: 1, duration: 2000, loop: true, easing: 'linear', update: t => bars.forEach((b, i) => { const h = Math.abs(Math.sin(i*.4 + t*10)) * 40 + 4; b.style.height = h + 'px'; }), });
// 무작위 음량으로 게이지 출렁 setInterval(() => { lm.motion({ targets: fill, width: [fill.style.width, lm.random(30,95) + '%'], duration: 200, easing: 'outQuart', }); }, 300);
// 주파수 스펙트럼 곡선이 출렁 lm.motion({ targets: { t: 0 }, t: 1, duration: 3000, loop: true, easing: 'linear', update: t => { let d = 'M 0 100'; for (let x = 0; x <= 240; x += 8) { const y = 100 - (Math.sin(x/20+t*10) + Math.cos(x/7+t*8))*25 - 30; d += ` L ${x} ${y}`; } path.setAttribute('d', d); }, });
// BPM 박자에 맞춰 통통 (140 BPM = 428ms 주기) lm.motion({ targets: el, scale: [1, 1.25], duration: 214, loop: true, direction: 'alternate', easing: 'outQuad', });
// 턴테이블 LP 회전 (33 RPM = 1800ms/회전) lm.motion({ targets: disc, rotate: [0, 360], duration: 1800, loop: true, easing: 'linear', });
// 책 페이지가 왼쪽 모서리 기준으로 넘어감 lm.motion({ targets: book, rotateY: [0, -180], duration: 1500, loop: true, direction: 'alternate', easing: 'ioCubic', });
// 큐브 6면이 펼쳐져 십자가 모양 → 다시 모임 lm.motion({ targets: cube, rotateX: [-20, 10], rotateY: [20, 45], duration: 3000, loop: true, direction: 'alternate', });
// 삼각형 점좌표 보간으로 종이접기 흉내 const A = "100,30 150,100 50,100"; const B = "100,100 150,30 50,30"; lm.motion({ targets: { t: 0 }, t: 1, duration: 2000, loop: true, direction: 'alternate', update: t => poly.setAttribute('points', interp(A, B, t)), });
// 12개 도미노가 차례로 90도 쓰러짐 lm.motion({ targets: dominos, rotate: [0, 90], duration: 300, delay: lm.stagger(120), easing: 'inQuad', });
// height auto 보간 (실제 높이 측정 후 px로) heads.forEach(h => h.addEventListener('click', () => { const body = h.nextElementSibling; const isOpen = body.classList.contains('open'); lm.motion({ targets: body, height: isOpen ? [body.scrollHeight, 0] : [0, body.scrollHeight], duration: 350, easing: 'ioCubic', }); body.classList.toggle('open'); }));
// 18개 세로 막대가 사인파처럼 키 변화 lm.motion({ targets: bars, height: ['30%', '95%'], duration: 800, delay: lm.stagger(40, { from: 'center' }), loop: true, direction: 'alternate', easing: 'ioSine', });
// 점이 -2 → +2 사이를 spring 으로 이동, 라벨 동기화 lm.motion({ targets: { v: -2 }, v: 2, duration: 2500, loop: true, direction: 'alternate', easing: 'ioCubic', update: t => { const v = -2 + t*4; const x = 120 + v*40; dot.setAttribute('cx', x); label.setAttribute('x', x); label.textContent = v.toFixed(1); }, });
// 좌표 (x,y) 점이 차례로 plot — 좌표→픽셀 변환 const pts = [[-2,3],[1,2],[2,-1],[-1,-2]]; pts.forEach((p, i) => { const [x, y] = [100 + p[0]*20, 100 - p[1]*20]; const c = circle(x, y, 0); lm.motion({ targets: c, r: [0, 5], duration: 400, delay: i*200, easing: 'outBack', }); });
// y=sin(x) 곡선이 좌→우로 그려짐 + 진행점 lm.motion({ targets: { p: 0 }, p: 1, duration: 2500, loop: true, easing: 'linear', update: t => { const end = t * 220; let d = 'M 0 ' + (90 - Math.sin(0)*40); for (let x = 0; x <= end; x += 2) { const mathX = (x - 110) / 25; const y = 90 - Math.sin(mathX) * 40; d += ` L ${x} ${y}`; } path.setAttribute('d', d); }, });
// 8조각 파이가 0/8 → 5/8 까지 차례로 채워짐 function slicePath(i) { const a0 = (i/8)*Math.PI*2 - Math.PI/2; const a1 = ((i+1)/8)*Math.PI*2 - Math.PI/2; const p0 = [80+Math.cos(a0)*60, 80+Math.sin(a0)*60]; const p1 = [80+Math.cos(a1)*60, 80+Math.sin(a1)*60]; return `M 80 80 L ${p0} A 60 60 0 0 1 ${p1} Z`; }
// 컴퍼스가 펼쳐졌다가 회전하며 원 그리기 const tl = lm.timeline(); // 1. 다리 펼치기 tl.add({ targets: armA, attr: { rotate: '-30 100 20' }, duration: 600 }) .add({ targets: armB, attr: { rotate: '30 100 20' }, duration: 600 }, '-=600') // 2. 펜이 원호 그리기 .add({ targets: { ang: 0 }, ang: 2*Math.PI, duration: 2000, update: t => drawArc(t), });
// 방정식 풀이 단계가 차례로 등장 + 활성 줄 강조 lines.forEach((L, i) => lm.motion({ targets: L, translateX: [-12, 0], opacity: [0, 1], duration: 500, delay: i*600, easing: 'outBack', begin: () => L.classList.add('active'), complete: () => { L.classList.remove('active'); L.classList.add('done'); }, }));
// 직각삼각형 → 세 변에 정사각형 → 식 등장 const tl = lm.timeline({ duration: 600, easing: 'outBack' }); tl.add({ targets: sqA, opacity: [0,1] }) .add({ targets: sqB, opacity: [0,1] }, '-=300') .add({ targets: sqC, opacity: [0,1] }, '-=300') .add({ targets: eq, opacity: [0,1], scale: [0.7,1] }); tl.play();
// 벡터 A 그리기 → 끝에서 B 시작 → 합 결과 강조 tl.add({ targets: { p: 0 }, p: 1, update: t => vecA.setAttribute('x2', 50+t*80) }) .add({ targets: { p: 0 }, p: 1, update: t => { vecB.setAttribute('x2', 130+t*40); vecB.setAttribute('y2', 110-t*60); } }) .add({ targets: { p: 0 }, p: 1, update: t => { vecC.setAttribute('x2', 50+t*120); vecC.setAttribute('y2', 140-t*90); } });
// SSS 작도: 밑변 → 두 호 → 교점 → 변 연결 // C = (Ax + (Bx-Ax)·k - h·(By-Ay)/d, Ay + ...) 정확한 교점 계산 const d = Math.hypot(Bx-Ax, By-Ay); const k = (b*b - a*a + d*d) / (2*d); const h = Math.sqrt(b*b - k*k); const Cx = Ax + k*(Bx-Ax)/d - h*(By-Ay)/d; const Cy = Ay + k*(By-Ay)/d + h*(Bx-Ax)/d;
// 동전 2회 던지기 트리 — 가지 + 노드 + 확률 라벨 const outcomes = [ { path: ['H'], prob: 0.5 }, { path: ['T'], prob: 0.5 }, { path: ['H','H'], prob: 0.25 }, ... ]; outcomes.forEach((o, i) => drawBranch(o, i*200));
// 막대그래프 → 정규분포 곡선 fitting function gauss(x, mu, sig) { return Math.exp(-0.5*((x-mu)/sig)**2) / (sig*Math.sqrt(2*Math.PI)); } // y축 스케일 후 path build
// 회전 행렬 R(θ) 적용 — 사각형이 원점 기준 회전 function rotate(p, θ) { const c = Math.cos(θ), s = Math.sin(θ); return [p[0]*c - p[1]*s, p[0]*s + p[1]*c]; }
// 3개 전자가 다른 각도의 타원 궤도를 돌기 lm.motion({ targets: { θ: 0 }, θ: 2*Math.PI, duration: 3000, loop: true, update: t => { // 타원 매개변수 (a*cosθ, b*sinθ) 후 회전 e1.setAttribute('cx', ...); }, });
// 두 사인파 합성 = 보강/상쇄 간섭 const y1 = Math.sin(x/15 + ph) * 25; const y2 = Math.sin(x/15 - ph) * 25; const sum = y1 + y2; // → 2·sin(x/15)·cos(ph) 형태
// 진자: KE = ½mv², PE = mgh — 합 일정 (에너지 보존) const θ = θ_max * Math.cos(ω*t); const h = L*(1 - Math.cos(θ)); const v = L*ω*Math.sin(θ_max*Math.sin(ω*t)); // 근사 // 막대 높이 = 에너지 비율 × 120px
// 스프링이 늘어났다 줄어듦 (단순조화운동) function springPath(L) { let d = 'M 20 100 L 30 100'; const coils = 8, w = (L - 20) / coils; for (let i = 0; i < coils; i++) { d += ` L ${30 + i*w + w/4} 88 L ${30 + i*w + 3*w/4} 112`; } return d + ` L ${L} 100`; }
// 볼록렌즈 3대 광선 작도 → 상 위치 결정 // 1) 광축 평행 광선 → 초점 통과 // 2) 렌즈 중심 통과 → 직진 // 3) 초점 통과 광선 → 광축 평행 // 교점 = 상의 위치 (1/v − 1/u = 1/f 검증)
// 두 사인파 가닥 + 사이 염기쌍 — 회전하는 이중나선 for (let y = 0; y <= 180; y += 8) { const x1 = 110 + Math.sin(y/20 + ph) * 35; const x2 = 110 - Math.sin(y/20 + ph) * 35; // 가닥 점 + 염기쌍 라인 }
// 26개 알파벳이 stagger로 통통 등장 lm.motion({ targets: letters, scale: [0, 1.2, 1], rotate: [-30, 0], opacity: [0, 1], duration: 600, delay: lm.stagger(50), easing: 'outBack', });
// "STUDY" 글자가 위로 솟아 분리되었다 다시 합침 lm.motion({ targets: letters, translateY: [0, -40], rotate: [0, (el, i) => (i-2)*10], duration: 700, delay: lm.stagger(100), loop: true, direction: 'alternate', easing: 'outBack', });
// "education" 음절별 등장 + 강세 음절 강조 lm.motion({ targets: syllables, scale: [0, 1], opacity: [0, 1], translateY: [10, 0], duration: 500, delay: lm.stagger(200), easing: 'outBack', }); // 강세 음절은 한번 더 통통 lm.motion({ targets: stressedSyl, scale: [1, 1.2, 1], delay: 800, duration: 500, });
// 단어 카드 뒤집어서 한국어 의미 보여주기 lm.motion({ targets: card, rotateY: [0, 180], duration: 1000, loop: true, direction: 'alternate', easing: 'ioCubic', });
// "The fox jumps" 구문 트리 — S → NP + VP 분기 const tl = lm.timeline(); tl.add({ targets: '#S', opacity: [0,1], scale: [0,1] }) .add({ targets: branches, x2: ... }) .add({ targets: [NP, VP], opacity: [0,1] }) .add({ targets: leaves, opacity: [0,1] }); tl.play();
// 과거 → 현재 → 미래 시제 마커가 차례로 나타남 lm.motion({ targets: markers, scale: [0, 1], opacity: [0, 1], duration: 500, delay: lm.stagger(400), easing: 'outBack', });
// ㄱ + ㅏ + ㅁ → "감" 결합 const tl = lm.timeline(); tl.add({ targets: pieces, scale: [0,1], delay: lm.stagger(300) }) .add({ targets: arrows, opacity: [0,1] }, '-=300') .add({ targets: result, scale: [0,1.3,1], opacity: [0,1] }); tl.play();
// 학교 → [학꾜] 된소리되기 — 받침 ㄱ이 다음 ㄱ을 ㄲ으로 lm.motion({ targets: [a, b], scale: [0.7, 1], opacity: [0, 1], duration: 600, delay: lm.stagger(200), easing: 'outBack', }); // 결과 [학꾜] 강조 — 통통 펄스 lm.motion({ targets: result, scale: [1, 1.3, 1], delay: 800, duration: 600, });
// 山 한자 4획이 stroke-draw로 차례로 그려짐 strokes.forEach((s, i) => { const len = lm.pathLength(s); lm.motion({ targets: s, strokeDashoffset: [len, 0], duration: 500, delay: i*500, easing: 'ioCubic', }); });
// 정몽주 단심가 — 3장이 차례로 등장 + 핵심어 강조 lm.motion({ targets: lines, translateX: [-20, 0], opacity: [0, 1], duration: 800, delay: lm.stagger(1500), easing: 'outQuart', });
// "나는학교에간다" → 띄어쓰기 마커가 차례로 들어감 markers.forEach((m, i) => lm.motion({ targets: m, width: ['0px', '8px'], duration: 400, delay: 800 + i*600, easing: 'outBack', }));
// 14자음 × 7모음 = 98칸 매트릭스가 stagger로 등장 const consonants = 'ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎ'; const vowels = 'ㅏㅑㅓㅕㅗㅛㅜ'; lm.motion({ targets: cells, scale: [0, 1], opacity: [0, 1], duration: 400, delay: lm.stagger(15, { grid: [7, 14], from: 'center' }), easing: 'outBack', });