@qyu/anim-core
Version:
Animation definition and implementation
576 lines (560 loc) • 18.3 kB
JavaScript
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 };