UNPKG

@qyu/anim-core

Version:

Animation definition and implementation

576 lines (560 loc) 18.3 kB
import * as spring from '@qyu/spring'; const anim_new_chain = function (src) { return { finished(point) { return src.length === 0 || (point.ptr === src.length - 1 && src[point.ptr].finished(point.child)); }, emit(point) { if (src.length === 0) { return; } src[point.ptr].emit(point.child); }, emitdiff(from, to) { if (src.length === 0) { return; } src[to.ptr].emitdiff(from.child, to.child); }, step(point, timeskip) { if (src.length === 0) { return point; } const activeanim = src[point.ptr]; const afterstep = activeanim.step(point.child, timeskip); if (activeanim.finished(afterstep)) { return { child: afterstep, ptr: Math.min(src.length - 1, point.ptr + 1) }; } return { ...point, child: afterstep }; }, }; }; const anim_new_chainmap = function (src) { const activeanim = (index, key) => { for (let i = index; i >= 0; --i) { if (src[i][key]) { return src[i][key]; } } }; return { finished(point) { if (src.length === 0) { return true; } if (point.ptr !== src.length - 1) { return false; } for (const key in point.children) { const key_point = point.children[key]; const key_activeanim = activeanim(point.ptr, key); if (!key_activeanim.finished(key_point)) { return false; } } return true; }, emit(point) { if (src.length === 0) { return; } for (const key in point.children) { const key_point = point.children[key]; const key_activeanim = activeanim(point.ptr, key); // emit activeanim for key if present, else - emit first inactive anim if (key_activeanim) { key_activeanim.emit(key_point); } else { for (let i = point.ptr + 1; i < src.length - 1; ++i) { const i_animmap = src[i]; const key_srcanim = i_animmap[key]; if (key_srcanim) { key_srcanim.emit(key_point); } } } } }, emitdiff(from, to) { if (src.length === 0) { return; } for (const key in to.children) { const key_activeanim = activeanim(to.ptr, key); const to_key_point = to.children[key]; const from_key_point = from.children[key]; key_activeanim?.emitdiff(from_key_point, to_key_point); } }, step(point, timeskip) { if (src.length === 0) { return point; } const afterstep = {}; for (const key in point.children) { const key_point = point.children[key]; const key_activeanim = activeanim(point.ptr, key); if (key_activeanim) { afterstep[key] = key_activeanim.step(key_point, timeskip); } else { afterstep[key] = point.children[key]; } } for (const key in afterstep) { const key_point = afterstep[key]; const key_activeanim = activeanim(point.ptr, key); if (key_activeanim && !key_activeanim.finished(key_point)) { return { ...point, children: afterstep }; } } return { ptr: Math.min(point.ptr + 1, src.length - 1), children: afterstep }; } }; }; const anim_new_line = (config) => { return { emit(point) { config.effect(point.state); }, emitdiff(from, to) { if (from.state !== to.state) { config.effect(to.state); } }, finished(point) { return point.state === config.target; }, step(point, timeskip) { const displacement = config.target - point.state; const direction = Math.sign(displacement); const range = Math.abs(displacement); const movement = config.velocity * timeskip; if (movement >= range) { return { state: config.target }; } return { state: point.state + movement * direction }; } }; }; const anim_new_loop = function (config) { return { finished(point) { return point.remaining <= 0 && config.src.finished(point.child); }, emit(point) { config.src.emit(point.child); }, emitdiff(from, to) { config.src.emitdiff(from.child, to.child); }, step(point, timeskip) { const afterstep = config.src.step(point.child, timeskip); if (point.remaining > 0 && config.src.finished(afterstep)) { return { child: config.point, remaining: point.remaining - 1 }; } return { child: afterstep, remaining: point.remaining }; } }; }; const anim_new_merge = function (src) { return { finished(point) { return point.every((point_item, index) => { return src[index].finished(point_item); }); }, emit(point) { point.forEach((point_item, index) => { src[index].emit(point_item); }); }, emitdiff(from, to) { to.forEach((to_item, index) => { src[index].emitdiff(from[index], to_item); }); }, step(point, timeskip) { return point.map((point_item, index) => src[index].step(point_item, timeskip)); } }; }; const anim_new_mergemap = function (src) { return { finished(point) { return Object.keys(src).every(key => { return src[key].finished(point[key]); }); }, emit(point) { Object.keys(src).forEach(key => { src[key].emit(point[key]); }); }, emitdiff(from, to) { Object.keys(src).forEach(key => { src[key].emitdiff(from[key], to[key]); }); }, step(point, timeskip) { return Object.fromEntries(Object.entries(src).map(([key, src_anim]) => { return [key, src_anim.step(point[key], timeskip)]; })); } }; }; const anim_new_pipe = function (params) { const { src, config } = params; return { finished: point => { return src.finished(config.pipei(point)); }, emit: point => { return src.emit(config.pipei(point)); }, emitdiff: (point_from, point_to) => { return src.emitdiff(config.pipei(point_from), config.pipei(point_to)); }, step: (point, timepassed) => { return config.pipeo(src.step(config.pipei(point), timepassed)); }, }; }; const anim_new_playback = function (params) { return { finished: point => { return params.src.finished(point); }, emit: point => { return params.src.emit(point); }, emitdiff: (point_from, point_to) => { return params.src.emitdiff(point_from, point_to); }, step: (point, timepassed) => { return params.src.step(point, timepassed * params.config.multiplier); } }; }; const anim_new_sequence = function (src) { return { finished(point) { return point.children.every((childpoint, index) => src[index].finished(childpoint)); }, emit(point) { for (let i = src.length - 1; i > point.mergeptr; --i) { src[i].emit(point.children[i]); } for (let i = 0; i <= point.mergeptr; ++i) { src[i].emit(point.children[i]); } }, emitdiff(from, to) { for (let i = src.length - 1; i > to.mergeptr; --i) { src[i].emitdiff(from.children[i], to.children[i]); } for (let i = 0; i <= to.mergeptr; ++i) { src[i].emitdiff(from.children[i], to.children[i]); } }, step(point, timeskip) { let stepdone = true; const afterstep_childpoints = []; for (let i = 0; i <= point.mergeptr; ++i) { const i_anim = src[i]; const i_point = point.children[i]; const i_point_next = i_anim.step(i_point, timeskip); afterstep_childpoints.push(i_point_next); if (!i_anim.finished(i_point_next)) { stepdone = false; } } for (let i = point.mergeptr + 1; i < src.length; ++i) { afterstep_childpoints.push(point.children[i]); } return { children: afterstep_childpoints, mergeptr: Math.min(point.mergeptr + Number(stepdone), src.length - 1) }; }, }; }; const anim_new_spring = (config) => { const { precision: { velocity: precision_velocity = 1e-2, displacement: precision_displacement = 1e-1 } = {} } = config; return { emit(point) { config.effect(point.state, point.velocity); }, emitdiff(from, to) { if (from.state !== to.state || from.velocity !== to.velocity) { config.effect(to.state, to.velocity); } }, finished(point) { return (point.state === config.target && point.velocity === 0); }, step(point, timeskip) { const springconfig = { drive: point.velocity, dampratio: config.dampratio, natfreq: config.natfreq, displacement: point.state - config.target }; const velocity = spring.velocity(springconfig, timeskip); const displacement = spring.displacement(springconfig, timeskip); if (Math.abs(velocity) < precision_velocity && Math.abs(displacement) < precision_displacement) { return { velocity: 0, state: config.target, }; } return { velocity, state: config.target + displacement }; } }; }; const anim_new_sequence_strict = function (src) { return { finished(point) { return point.children.every((childpoint, index) => src[index].finished(childpoint)); }, emit(point) { // find active index let i = 0; for (; i < src.length; ++i) { const anim = src[i]; const child = point.children[i]; if (anim.finished(child)) { continue; } break; } // i is first not done element // all animations are done if (i >= src.length) { for (let j = src.length - 1; j >= 0; --j) { const anim = src[j]; const child = point.children[j]; anim.emit(child); } return; } // undone animation in the middle { // emit latter first so they will be overriden for (let j = src.length - 1; j > i; --j) { const anim = src[j]; const child = point.children[j]; anim.emit(child); } // emit in order so latter overrides earlier for (let j = 0; j <= i; ++j) { const anim = src[j]; const child = point.children[j]; anim.emit(child); } } }, emitdiff(from, to) { // find active index let i = 0; for (; i < src.length; ++i) { const anim = src[i]; const child_from = from.children[i]; if (anim.finished(child_from)) { continue; } break; } // i is first not done element // all animations are done if (i >= src.length) { for (let j = src.length - 1; j >= 0; --j) { const anim = src[j]; const child_to = to.children[j]; const child_from = from.children[j]; anim.emitdiff(child_from, child_to); } return; } // undone animation in the middle { // emit latter first so they will be overriden for (let j = src.length - 1; j > i; --j) { const anim = src[j]; const child_to = to.children[j]; const child_from = from.children[j]; anim.emitdiff(child_from, child_to); } // emit in order so latter overrides earlier for (let j = 0; j <= i; ++j) { const anim = src[j]; const child_to = to.children[j]; const child_from = from.children[j]; anim.emitdiff(child_from, child_to); } } }, step(point, timeskip) { let i = 0; const next_children = []; for (; i < src.length; ++i) { const anim = src[i]; const child = point.children[i]; if (anim.finished(child)) { next_children.push(child); continue; } { next_children.push(anim.step(child, timeskip)); break; } } for (i += 1; i < src.length; ++i) { next_children.push(point.children[i]); } return { children: next_children }; }, }; }; const emitter_new_manual = function (params) { let last_point = params.point; const { anim, batch } = params; if (batch) { batch(() => { anim.emit(last_point); }); } else { anim.emit(last_point); } return { point: () => { return last_point; }, finished: () => { return anim.finished(last_point); }, step: (timepassed) => { const now_point = anim.step(last_point, timepassed); if (batch) { batch(() => { anim.emitdiff(last_point, now_point); }); } else { anim.emitdiff(last_point, now_point); } { last_point = now_point; } } }; }; const emitter_new_interval = function (params) { const { anim, scheduler, batch } = params; let last_point = params.point; let last_time = scheduler.time_init(); let cleanup; if (batch) { batch(() => { anim.emit(last_point); }); } else { anim.emit(last_point); } const update = (now_time) => { const timepassed = now_time - last_time; const now_point = anim.step(last_point, timepassed); if (batch) { batch(() => { anim.emitdiff(last_point, now_point); }); } else { anim.emitdiff(last_point, now_point); } { last_time = now_time; last_point = now_point; } }; const framecb = timenow => { update(timenow); { if (anim.finished(last_point)) { cleanup = undefined; return; } { cleanup = scheduler.request(framecb); } } }; cleanup = scheduler.request(framecb); return { point: () => { return last_point; }, active: () => { return typeof cleanup === "function"; }, softstop: () => { if (cleanup) { cleanup(); cleanup = undefined; update(scheduler.time_init()); } }, hardstop: () => { if (cleanup) { cleanup(); cleanup = undefined; } } }; }; const fscheduler_new_frame = function (performance, request, cancel) { return { time_init: () => performance.now(), request: framecb => { const id = request(() => { framecb(performance.now()); }); return () => { cancel(id); }; } }; }; export { anim_new_chain, anim_new_chainmap, anim_new_line, anim_new_loop, anim_new_merge, anim_new_mergemap, anim_new_pipe, anim_new_playback, anim_new_sequence, anim_new_sequence_strict, anim_new_spring, emitter_new_interval, emitter_new_manual, fscheduler_new_frame };