export function animate(root, animations, gsap) {
const tl = gsap.timeline();
preconvertGeometry(root, animations);
for (const a of animations) {
const sel =
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.id) : `[id="${a.id}"]`;
const el = root.querySelector(sel);
if (!el) continue;
switch (a.effect) {
case 'fade':
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration, ease: 'power1.out' }, a), a);
break;
case 'pop':
enterExit(
tl,
el,
withOverrides({ scale: 0, transformOrigin: '50% 50%', duration: a.duration, ease: 'back.out(1.7)' }, a),
a
);
break;
case 'slide':
slideIn(tl, el, a);
break;
case 'draw':
drawOn(el, a, tl);
break;
case 'move':
moveAlong(root, el, a, tl);
break;
case 'morph':
morphInto(root, el, a, tl);
break;
case 'highlight':
highlightWith(el, a, tl);
break;
case 'type':
typeReveal(el, a, tl);
break;
case 'scramble':
scrambleReveal(el, a, tl);
break;
case 'wiggle':
wiggleShake(el, a, tl);
break;
default:
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration }, a), a);
}
}
return tl;
}
export function interactive(root, interactions, Draggable) {
if (!Draggable || !interactions || !interactions.length) return [];
const pick = (id) =>
root.querySelector(
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(id) : `[id="${id}"]`
);
const out = [];
for (const it of interactions) {
const el = pick(it.id);
if (!el) continue;
const vars = { type: it.axis || 'x,y', inertia: !!it.inertia };
if (it.bounds) {
const b = pick(it.bounds);
if (b) vars.bounds = b;
}
out.push(...Draggable.create(el, vars));
}
return out;
}
function enterExit(tl, el, vars, a) {
return a.out ? tl.to(el, vars, a.start) : tl.from(el, vars, a.start);
}
function slideIn(tl, el, a) {
let bb = { width: 0, height: 0 };
try {
bb = el.getBBox();
} catch {
}
const off = {
left: { x: -(bb.width * 1.5 || 40) },
right: { x: bb.width * 1.5 || 40 },
up: { y: -(bb.height * 1.5 || 40) },
down: { y: bb.height * 1.5 || 40 },
}[a.from || 'left'];
enterExit(tl, el, withOverrides({ ...off, opacity: 0, duration: a.duration, ease: 'power2.out' }, a), a);
}
function typeReveal(el, a, tl) {
const units = el.querySelectorAll('.rpic-ch');
if (!units.length) return;
const step = a.duration / units.length;
const vars = withOverrides({ opacity: 0, duration: step, stagger: step, ease: 'none' }, a);
return a.out ? tl.to(units, vars, a.start) : tl.from(units, vars, a.start);
}
function scrambleReveal(el, a, tl) {
const text = el.querySelector('text');
if (!text) return;
const chars = a.chars || 'upperCase';
const target = a.out ? '' : '{original}';
const vars = withOverrides(
{ duration: a.duration, scrambleText: { text: target, chars, speed: 1 }, ease: 'none' },
a
);
return tl.to(text, vars, a.start);
}
function wiggleShake(el, a, tl) {
const n = a.wiggles || 6;
const vars = withOverrides(
{ rotation: 8, transformOrigin: '50% 50%', duration: a.duration, ease: `wiggle(${n})` },
a
);
return tl.fromTo(el, { rotation: 0 }, vars, a.start);
}
function withOverrides(vars, a) {
if (a.repeat) vars.repeat = a.repeat;
if (a.yoyo) vars.yoyo = true;
if (a.ease) vars.ease = a.ease;
return vars;
}
const SVG_NS = 'http://www.w3.org/2000/svg';
function pathDataOf(el) {
const n = el.tagName.toLowerCase();
const f = (a) => parseFloat(el.getAttribute(a)) || 0;
if (n === 'path') return el.getAttribute('d');
if (n === 'line') return `M${f('x1')},${f('y1')} L${f('x2')},${f('y2')}`;
if (n === 'polyline' || n === 'polygon') {
const nums = (el.getAttribute('points') || '').trim().split(/[\s,]+/).map(Number);
if (nums.length < 4) return null;
let d = '';
for (let i = 0; i + 1 < nums.length; i += 2) d += `${i ? 'L' : 'M'}${nums[i]},${nums[i + 1]} `;
return n === 'polygon' ? d + 'Z' : d.trim();
}
if (n === 'rect') return `M${f('x')},${f('y')} h${f('width')} v${f('height')} h${-f('width')} Z`;
if (n === 'circle') {
const r = f('r'), cx = f('cx'), cy = f('cy');
return `M${cx - r},${cy} a${r},${r} 0 1,0 ${2 * r},0 a${r},${r} 0 1,0 ${-2 * r},0 Z`;
}
if (n === 'ellipse') {
const rx = f('rx'), ry = f('ry'), cx = f('cx'), cy = f('cy');
return `M${cx - rx},${cy} a${rx},${ry} 0 1,0 ${2 * rx},0 a${rx},${ry} 0 1,0 ${-2 * rx},0 Z`;
}
return null;
}
function convertToPath(el) {
if (!el || el.tagName.toLowerCase() === 'path') return el;
const d = pathDataOf(el);
if (!d || !el.parentNode || typeof document === 'undefined') return el;
const p = document.createElementNS(SVG_NS, 'path');
p.setAttribute('d', d);
for (const attr of el.attributes) {
if (!/^(x1|y1|x2|y2|points|x|y|width|height|cx|cy|r|rx|ry)$/.test(attr.name)) {
p.setAttribute(attr.name, attr.value);
}
}
el.parentNode.replaceChild(p, el);
return p;
}
function preconvertGeometry(root, animations) {
const ALL = 'path, polyline, line, rect, circle, ellipse, polygon';
const pick = (id, sel) => {
if (!id) return;
const g = root.querySelector(`[id="${id}"]`);
const prim = g && g.querySelector(sel);
if (prim) convertToPath(prim);
};
for (const a of animations || []) {
if (a.effect === 'move') pick(a.path, 'path, polyline, line');
if (a.effect === 'morph') {
pick(a.id, ALL);
pick(a.morph, ALL);
}
}
}
function highlightWith(el, a, tl) {
if (a.color) {
const shapes = el.querySelectorAll('path, polyline, line, rect, circle, ellipse, polygon');
tl.to(
shapes,
withOverrides({ stroke: a.color, strokeWidth: '+=2', duration: a.duration, ease: 'power1.inOut' }, a),
a.start
);
tl.to(
el,
withOverrides({ scale: 1.1, transformOrigin: '50% 50%', duration: a.duration, ease: 'power1.inOut' }, a),
a.start
);
} else {
tl.to(
el,
withOverrides({ scale: 1.12, transformOrigin: '50% 50%', duration: a.duration, ease: 'power1.inOut' }, a),
a.start
);
}
}
function morphInto(root, el, a, tl) {
if (!a.morph) return;
const tsel =
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.morph) : `[id="${a.morph}"]`;
const target = root.querySelector(tsel);
const src = el.querySelector('path');
const dst = target && target.querySelector('path');
if (!src || !dst) return;
tl.to(src, withOverrides({ morphSVG: dst, duration: a.duration, ease: 'power1.inOut' }, a), a.start);
}
function moveAlong(root, el, a, tl) {
if (!a.path) return;
const psel =
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.path) : `[id="${a.path}"]`;
const group = root.querySelector(psel);
const pathEl = group && group.querySelector('path');
if (!pathEl) return;
tl.to(
el,
withOverrides(
{
motionPath: { path: pathEl, align: pathEl, alignOrigin: [0.5, 0.5], autoRotate: false },
duration: a.duration,
ease: 'none',
},
a
),
a.start
);
}
function drawOn(group, a, tl) {
const rangeStart = a.drawFrom != null ? a.drawFrom : 0;
const rangeEnd = a.drawTo != null ? a.drawTo : 1;
const hasRange = a.drawFrom != null || a.drawTo != null;
const els = group.querySelectorAll('path, polyline, line, rect, circle, ellipse, polygon');
els.forEach((el) => {
const fill = el.getAttribute('fill');
if (el.getAttribute('stroke-width') === '0' || (fill && fill !== 'none' && !el.getAttribute('stroke'))) {
if (hasRange && rangeEnd < 1) return;
tl.from(
el,
{ opacity: 0, scale: 0, transformOrigin: '50% 50%', duration: Math.min(0.2, a.duration * 0.4), ease: 'back.out(1.7)' },
a.start + a.duration * 0.8
);
return;
}
let len = 0;
try {
len = el.getTotalLength();
} catch {
len = 0;
}
if (len > 0 && hasRange) {
const p0 = rangeStart * len;
const p1 = rangeEnd * len;
const [w0, w1] = a.out ? [p1 - p0, 0] : [0, p1 - p0];
const set = (w) => el.setAttribute('stroke-dasharray', `0 ${p0} ${Math.max(0, w)} ${len}`);
set(w0);
const proxy = { w: w0 };
tl.to(
proxy,
withOverrides({ w: w1, duration: a.duration, ease: 'none', onUpdate: () => set(proxy.w) }, a),
a.start
);
} else if (len > 0) {
const [begin, end] = a.out ? [0, len] : [len, 0];
tl.fromTo(
el,
{ strokeDasharray: len, strokeDashoffset: begin },
withOverrides({ strokeDashoffset: end, duration: a.duration, ease: 'none' }, a),
a.start
);
} else {
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration }, a), a);
}
});
const texts = group.querySelectorAll('text');
if (texts.length) {
tl.from(texts, { opacity: 0, duration: a.duration * 0.6 }, a.start + a.duration * 0.4);
}
}