UNPKG

animbase

Version:
446 lines (412 loc) 14.3 kB
const splitColor = color => color .match(/^([\w]{2})([\w]{2})([\w]{2})([\w]{2})$/) .slice(1) .map(v => parseInt(v, 16)); const toHexRGBA = s => { const found = s.match(/^#(?:([0-9a-fA-F]{3})|([0-9a-fA-F]{4})|([0-9a-fA-F]{6})|([0-9a-fA-F]{8}))$/); if (!found) throw Error('Unsupported color format'); const [, c3, c4, c6, c8] = found; return c3 ? c3 .split('') .map(v => v + v) .join('') + 'ff' : c4 ? c4 .split('') .map(v => v + v) .join('') : c6 ? c6 + 'ff' : c8; }; const interpolateColor = (t, color1, color2) => { const cInt1 = splitColor(toHexRGBA(color1)); const cInt2 = splitColor(toHexRGBA(color2)); return ( '#' + cInt1 .map((v, i) => Math.round(Math.max(0, Math.min(255, v + (cInt2[i] - v) * t))) .toString(16) .padStart(2, '0') ) .join('') ); }; const easingFunctions = { linear: t => t, ease: t => t * t * (3 - 2 * t), in: t => t * t * t, out: t => 1 - Math.pow(1 - t, 3), inOut: t => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2), // fungsi easing lainnya: inQuad: t => t * t, outQuad: t => t * (2 - t), inOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), inCubic: t => t * t * t, outCubic: t => --t * t * t + 1, inOutCubic: t => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1), inQuart: t => t * t * t * t, outQuart: t => 1 - --t * t * t * t, inOutQuart: t => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t), inQuint: t => t * t * t * t * t, outQuint: t => 1 + --t * t * t * t * t, inOutQuint: t => (t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t), inSine: t => 1 - Math.cos((t * Math.PI) / 2), outSine: t => Math.sin((t * Math.PI) / 2), inOutSine: t => -(Math.cos(Math.PI * t) - 1) / 2, inExpo: t => (0 === t ? 0 : Math.pow(2, 10 * (t - 1))), outExpo: t => (1 === t ? 1 : 1 - Math.pow(2, -10 * t)), inOutExpo: t => 0 === t ? 0 : 1 === t ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, 10 - 20 * t)) / 2, inCirc: t => 1 - Math.sqrt(1 - Math.pow(t, 2)), outCirc: t => Math.sqrt(1 - Math.pow(t - 1, 2)), inOutCirc: t => t < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2, inBack: t => 2.70158 * t * t * t - 1.70158 * t * t, outBack: t => 1 + 2.70158 * --t * t * t + 1.70158 * t * t, inOutBack: t => t < 0.5 ? (Math.pow(2 * t, 2) * ((2.5949095 + 1) * 2 * t - 2.5949095)) / 2 : (Math.pow(2 * t - 2, 2) * ((2.5949095 + 1) * (t * 2 - 2) + 2.5949095) + 2) / 2, inElastic: t => (0 === t ? 0 : 1 === t ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin(((t * 10 - 10.75) * 2 * Math.PI) / 3)), outElastic: t => (0 === t ? 0 : 1 === t ? 1 : Math.pow(2, -10 * t) * Math.sin(((t * 10 - 0.75) * 2 * Math.PI) / 3) + 1), inOutElastic: t => 0 === t ? 0 : 1 === t ? 1 : t < 0.5 ? -(Math.pow(2, 20 * t - 10) * Math.sin(((20 * t - 11.125) * 2 * Math.PI) / 4.5)) / 2 : (Math.pow(2, -20 * t + 10) * Math.sin(((20 * t - 11.125) * 2 * Math.PI) / 4.5)) / 2 + 1, inBounce: t => 1 - easingFunctions.outBounce(1 - t), outBounce: t => t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + 0.75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375 : 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375, inOutBounce: t => t < 0.5 ? (1 - easingFunctions.outBounce(1 - 2 * t)) / 2 : (1 + easingFunctions.outBounce(2 * t - 1)) / 2, spring: (t, stiffness = 4.5, damping = 6) => 1 - Math.cos(t * stiffness * Math.PI) * Math.exp(-t * damping), }; const linearInterpolation = (a, aMin, aMax, bMin, bMax) => bMin + ((a - aMin) * (bMax - bMin)) / (aMax - aMin); const warnedKeys = new Set(); const warnOnce = (key, ...args) => { if (warnedKeys.has(key)) return; warnedKeys.add(key); console.warn(...args); }; let re = { color: /#[0-9a-fA-F]{3,}/, number: /-?\d+(?:\.\d+)?/, unit: /[%a-zA-Z]*/, func: /[a-zA-Z]+/, }; re = { ...re, numberWithOptionalUnit: new RegExp(`(?:(${re.color.source})|(?:(${re.number.source})(${re.unit.source})?))`), }; re = { ...re, numberWithOptionalUnitAndFunc: new RegExp(`${re.numberWithOptionalUnit.source}(?:\\.(${re.func.source}))?`), }; const parseExpression = (s, options = {}) => { const {strict = false, warnOnce: warnOnceFn = warnOnce, context = ''} = options; const rege = RegExp(re.numberWithOptionalUnitAndFunc.source, 'g'); let arr; const r = []; while ((arr = rege.exec(s)) !== null) { const [text, color, sValue, unit, func] = arr; if (func && !easingFunctions[func]) { if (strict) throw Error(`Invalid easing function: ${func}`); warnOnceFn(`animbase:ease:${func}:${context}`, '[AnimBase] Invalid easing function:', func, context); } r.push({ type: color ? 'color' : 'number', text, value: color ? color : parseFloat(sValue), unit, func: func && easingFunctions[func] ? func : undefined, lastIndex: rege.lastIndex, }); } return r; }; function replaceString(str, _ref, _replacer) { const ref = [..._ref].reverse(); const replacer = [..._replacer].reverse(); for (let i = 0; i < ref.length; i++) { const fromIndex = ref[i].lastIndex - ref[i].text.length; const toIndex = ref[i].lastIndex; str = str.substring(0, fromIndex) + replacer[i] + str.substring(toIndex); } return str; } const getValue = ({ startValue, endValue, startFrame = 0, endFrame, currentFrame, func = 'linear', type, strict = false, warnOnce: warnOnceFn = warnOnce, context = '', } = {}) => { if (startValue === endValue) return startValue; const normalValue = linearInterpolation(currentFrame, startFrame, endFrame, 0, 1); const easeValue = easingFunctions[func](normalValue); if (type === 'color') { try { return interpolateColor(easeValue, startValue, endValue); } catch (err) { if (strict) throw err; warnOnceFn(`animbase:color:${context}`, '[AnimBase] Invalid color format:', startValue, endValue, context); return easeValue >= 1 ? endValue : startValue; } } return linearInterpolation(easeValue, 0, 1, startValue, endValue); }; const handlerMapping = { style: (el, key, val) => { el.style[key] = val; }, setAttribute: (el, key, val) => { el.setAttribute(key, val); }, textContent: (el, key, val) => { el.textContent = val; }, scrollProp: (el, key, val) => { el[key] = parseFloat(val); // for scrollTop, scrollLeft }, prop: (el, key, val) => { el[key] = val; // generic fallback for special properties like currentTime }, cssVar: (el, key, val) => { el.style.setProperty(key, val); }, }; const keyHandlerRegistry = {}; const keyHandlerResolver = key => { if (keyHandlerRegistry[key]) return keyHandlerRegistry[key]; if (key === 'd' || key === 'value' || key === 'viewBox' || key === 'x' || key === 'y' || key === 'cx' || key === 'cy') return 'setAttribute'; if (key === 'text') return 'textContent'; if (key === 'scrollTop' || key === 'scrollLeft') return 'scrollProp'; if (key === 'currentTime' || key === 'volume' || key === 'playbackRate') return 'prop'; if (key.startsWith('--')) return 'cssVar'; return 'style'; }; class AnimatedElement { constructor(element, {init = null, config = null} = {}) { this.element = element; this.setConfig({init, config}); } setConfig({init = null, config = null} = {}) { const strict = this.element.dataset.animStrict === 'true'; this.strict = strict; const animInit = this.element.dataset.animInit; const animConfig = this.element.dataset.animConfig; if (!init || !config) { // fallback dari DOM dataset try { init = animInit ? JSON.parse(animInit) : {}; } catch (e) { if (strict) throw e; warnOnce(`animbase:json:init:${this.element}`, '[AnimBase] Invalid data-anim-init JSON', animInit); } try { config = animConfig ? JSON.parse(animConfig) : {}; } catch (e) { if (strict) throw e; warnOnce(`animbase:json:config:${this.element}`, '[AnimBase] Invalid data-anim-config JSON', animConfig); } } init = init || {}; config = config || {}; this.rawInit = init; this.rawConfig = config; const styleConfig = {}; let startFrame = 0; let ref = {}; const initialRef = {}; for (const [prop, str] of Object.entries(init)) { initialRef[prop] = parseExpression(str, { strict, context: `prop:${prop}`, }); ref[prop] = [...initialRef[prop]]; } for (const [k, props] of Object.entries(config)) { if (k === '0') continue; const endFrame = parseInt(k); styleConfig[k] = {startFrame, endFrame, ref, props: {}}; for (const [prop, str] of Object.entries(props)) { const values = parseExpression(str, { strict, context: `prop:${prop}`, }); ref = {...ref, [prop]: values}; styleConfig[k].props[prop] = values; } startFrame = endFrame + 1; } this.initial = init; this.initialRef = initialRef; this.styleConfig = styleConfig; this.styleEntries = Object.entries(styleConfig); this.lastFrame = null; } // update(currentFrame, debug = false) { // const result = {}; // const entries = Object.entries(this.styleConfig); // const found = entries.find(([, d]) => currentFrame >= d.startFrame && currentFrame <= d.endFrame); // if (found) { // const detail = found[1]; // for (const [prop, v] of Object.entries(detail.ref)) { // const replacer = detail.props[prop] // ? detail.props[prop].map( // (vi, i) => // `${getValue({ // startValue: v[i].value, // endValue: vi.value, // startFrame: detail.startFrame, // endFrame: detail.endFrame, // currentFrame, // func: vi.func, // type: vi.type, // })}${vi.unit || ''}` // ) // : v.map(vi => `${vi.value}${vi.unit || ''}`); // const r = replaceString(this.initial[prop], this.initialRef[prop], replacer); // if (debug) result[prop] = r; // else this.element.style[prop] = r; // } // } else { // const detail = entries.at(-1)[1]; // for (const [prop, v] of Object.entries(detail.ref)) { // const replacer = detail.props[prop] // ? detail.props[prop].map(vi => `${vi.value}${vi.unit || ''}`) // : v.map(vi => `${vi.value}${vi.unit || ''}`); // const r = replaceString(this.initial[prop], this.initialRef[prop], replacer); // if (debug) result[prop] = r; // else this.element.style[prop] = r; // } // } // return debug ? result : undefined; // } update(currentFrame, debug = false) { if (this.lastFrame === currentFrame) return debug ? {} : undefined; this.lastFrame = currentFrame; const result = {}; const entries = this.styleEntries; if (!entries.length) return debug ? result : undefined; const found = entries.find(([, d]) => currentFrame >= d.startFrame && currentFrame <= d.endFrame); const detail = found?.[1] ?? entries.at(-1)[1]; const isActive = Boolean(found); for (const [prop, v] of Object.entries(detail.ref)) { const replacer = isActive && detail.props[prop] ? detail.props[prop].map( (vi, i) => `${getValue({ startValue: v[i].value, endValue: vi.value, startFrame: detail.startFrame, endFrame: detail.endFrame, currentFrame, func: vi.func, type: vi.type, strict: this.strict, context: `prop:${prop}`, })}${vi.unit || ''}` ) : detail.props[prop] ? detail.props[prop].map(vi => `${vi.value}${vi.unit || ''}`) : v.map(vi => `${vi.value}${vi.unit || ''}`); const r = replaceString(this.initial[prop], this.initialRef[prop], replacer); if (debug) result[prop] = r; else { // this.element.style[prop] = r; const handlerType = keyHandlerResolver(prop); const handlerFn = typeof handlerType === 'function' ? handlerType : handlerMapping[handlerType]; if (handlerFn) handlerFn(this.element, prop, r); } } return debug ? result : undefined; } } class TimelineAnimator { constructor(getTimelineValue, options = {}) { this.getTimelineValue = getTimelineValue; this.animatedElements = new Map(); this.timelineValue = NaN; this.elementSelector = options.elementSelector || '[data-anim-config]'; this.autoCollect = options.autoCollect ?? true; this.autoUpdate = options.autoUpdate ?? true; if (this.autoCollect) this.collectAnimatedElements(); if (this.autoUpdate) this.updateTimeline(); // Optional init render } collectAnimatedElements() { const elements = document.querySelectorAll(this.elementSelector); elements.forEach(el => this.addElement(el)); } addElement(element, config = {}) { const animated = element instanceof AnimatedElement ? element : new AnimatedElement(element, config); this.animatedElements.set(animated.element, animated); } removeElement(element) { this.animatedElements.delete(element); } updateTimeline() { const nextValue = this.getTimelineValue(); if (nextValue === this.timelineValue) return; this.timelineValue = nextValue; this.updateAllAnimatedElements(); } updateAllAnimatedElements() { this.animatedElements.forEach(ae => ae.update(this.timelineValue)); } } function registerKeyHandler(key, handlerTypeOrFn) { if (typeof handlerTypeOrFn === 'function') { keyHandlerRegistry[key] = handlerTypeOrFn; handlerMapping[key] = handlerTypeOrFn; return; } keyHandlerRegistry[key] = handlerTypeOrFn; if (typeof handlerTypeOrFn === 'string' && !handlerMapping[handlerTypeOrFn]) { warnOnce( `animbase:handler:${handlerTypeOrFn}`, '[AnimBase] Unknown handler type:', handlerTypeOrFn, `for key "${key}"` ); } } function registerHandlerType(type, handlerFn) { handlerMapping[type] = handlerFn; } var Core = /*#__PURE__*/Object.freeze({ __proto__: null, AnimatedElement: AnimatedElement, TimelineAnimator: TimelineAnimator, easingFunctions: easingFunctions, linearInterpolation: linearInterpolation, parseExpression: parseExpression, registerHandlerType: registerHandlerType, registerKeyHandler: registerKeyHandler }); // animbase-core-only.js if (typeof window !== "undefined") { window.AnimBase = window.AnimBase || {}; Object.assign(window.AnimBase, Core); } export { AnimatedElement, TimelineAnimator, easingFunctions, linearInterpolation, parseExpression, registerHandlerType, registerKeyHandler };