motion-dom
Version:
113 lines (110 loc) • 4 kB
JavaScript
import { frame } from '../frameloop/frame.mjs';
class MotionValueState {
/**
* @param step - The frameloop step renders are scheduled in. Defaults
* to `frame.render`. Effects that feed a render loop running in
* `frame.render` (GPU scenes) should write in `frame.preRender`.
*/
constructor(step = frame.render) {
this.step = step;
this.values = new Map();
/**
* Renders scheduled for the next flush, usually one or two (values
* bound to the same computed render, e.g. `x` and `y` to `transform`,
* share an entry). Tracked with a count rather than truncating the
* array so no backing store is reallocated every frame. Kept per
* state so pending writes can be flushed synchronously before the
* subject is measured.
*/
this.pending = [];
this.numPending = 0;
/**
* Run the pending renders. Resetting the count first means a render
* scheduled while flushing lands in the next frame, as with the
* frameloop itself.
*/
this.flush = () => {
const { pending, numPending } = this;
this.numPending = 0;
for (let i = 0; i < numPending; i++)
pending[i]();
};
}
/**
* @param render - Writes the value to the subject. Renders read the
* motion value directly rather than a cached copy, so there is one
* place a value lives.
* @param computed - A value already in this state (e.g. `transform`)
* whose render should run whenever `value` changes.
*/
set(name, value, render, computed) {
this.values.get(name)?.onRemove();
if (computed) {
for (const entry of this.values.values()) {
if (entry.value === computed)
render = entry.render;
}
}
const onChange = () => render && this.schedule(render);
/**
* Values created ahead of a DOM read start out undefined and
* have nothing to render until they're set.
*/
value.get() !== undefined && onChange();
const cancelOnChange = value.on("change", onChange);
const onRemove = () => {
cancelOnChange();
render && !computed && this.cancel(render);
/**
* A later set() of the same name has already removed this
* entry, so leave the one that replaced it.
*/
this.values.get(name)?.onRemove === onRemove &&
this.values.delete(name);
};
this.values.set(name, {
value,
render: computed ? undefined : render,
onRemove,
});
return onRemove;
}
get(name) {
return this.values.get(name)?.value;
}
/**
* Detach every value from this state and return them, so another
* renderer can take them over. The state stays cached by its effect,
* so the bound transforms are reset too rather than leaking into
* values bound later.
*/
release() {
const values = new Map();
this.values.forEach((entry, name) => {
values.set(name, entry.value);
entry.onRemove();
});
this.transformKeys = this.transformValues = undefined;
return values;
}
schedule(render) {
const { pending, numPending } = this;
for (let i = 0; i < numPending; i++) {
if (pending[i] === render)
return;
}
numPending || this.step(this.flush);
pending[this.numPending++] = render;
}
cancel(render) {
const { pending } = this;
for (let i = 0; i < this.numPending; i++) {
if (pending[i] === render) {
pending[i] = pending[--this.numPending];
return;
}
}
}
}
export { MotionValueState };
//# sourceMappingURL=MotionValueState.mjs.map