mirror of https://github.com/sveltejs/svelte
parent
a26a75eba6
commit
0d9d3c53d1
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* __DEV__
|
||||
*
|
||||
* Used in src/runtime
|
||||
*
|
||||
* Bundles dev runtime to svelte/dev/[library].[m]js
|
||||
* the compiler rewrites its own 'svelte' imports to 'svelte/dev' automatically
|
||||
*
|
||||
*/
|
||||
declare var __DEV__: boolean;
|
||||
/**
|
||||
* __VERSION__
|
||||
*
|
||||
* Svelte's version in package.json
|
||||
* Used in src/compiler and src/runtime
|
||||
*
|
||||
*/
|
||||
declare var __VERSION__: string;
|
||||
/**
|
||||
* __COMPILER_API_VERSION__
|
||||
*
|
||||
* Unique ID passed to the compiler
|
||||
* Used to mitigate breaking changes with bundler plugins
|
||||
*
|
||||
* VERSIONS ( default is "3.0.0" )
|
||||
*
|
||||
* >3.22.0 : {
|
||||
*
|
||||
* The change :
|
||||
* A different runtime is now used in dev mode,
|
||||
* every import has to go through 'svelte/dev' instead of 'svelte'
|
||||
*
|
||||
* Requirement :
|
||||
* In dev mode, bundler plugins must make sure that every 'svelte' import is replaced by 'svelte/dev'
|
||||
*
|
||||
* }
|
||||
*
|
||||
*/
|
||||
//declare var __COMPILER_API_VERSION__: boolean;
|
||||
/**
|
||||
* Unique ID devtools must pass to the compiler
|
||||
* Used to
|
||||
*
|
||||
* VERSIONS ( default is "^3.21.0" )
|
||||
* - 0 (default) : compiler imports from prod runtime
|
||||
* - 1 : in dev mode, compiler imports from dev runtime
|
||||
*/
|
||||
//declare var __DEVTOOLS_API_VERSION__: boolean;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Bundle different runtime for tests
|
||||
* instead of relying on hacks
|
||||
*/
|
||||
declare var __TEST__: boolean;
|
@ -1,6 +1,8 @@
|
||||
import '../ambient';
|
||||
|
||||
export { default as compile } from './compile/index';
|
||||
export { default as parse } from './parse/index';
|
||||
export { default as preprocess } from './preprocess/index';
|
||||
export { walk } from 'estree-walker';
|
||||
|
||||
export const VERSION = '__VERSION__';
|
||||
export const version = __VERSION__;
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { custom_event, append, insert, detach, listen, attr } from './dom';
|
||||
let inited
|
||||
export function dispatch_dev$legacy<T = any>(type: string, detail?: T) {
|
||||
if(!inited && `__SVELTE_DEVTOOLS_GLOBAL_HOOK__` in window) {
|
||||
inited = true
|
||||
throw new Error(`You must specify the version`)
|
||||
}
|
||||
document.dispatchEvent(custom_event(type, { version: __VERSION__, ...detail }));
|
||||
}
|
||||
|
||||
export function append_dev$legacy(target: Node, node: Node) {
|
||||
dispatch_dev$legacy('SvelteDOMInsert', { target, node });
|
||||
append(target, node);
|
||||
}
|
||||
|
||||
export function insert_dev$legacy(target: Node, node: Node, anchor?: Node) {
|
||||
dispatch_dev$legacy('SvelteDOMInsert', { target, node, anchor });
|
||||
insert(target, node, anchor);
|
||||
}
|
||||
|
||||
export function detach_dev$legacy(node: Node) {
|
||||
dispatch_dev$legacy('SvelteDOMRemove', { node });
|
||||
detach(node);
|
||||
}
|
||||
|
||||
export function detach_between_dev$legacy(before: Node, after: Node) {
|
||||
while (before.nextSibling && before.nextSibling !== after) {
|
||||
detach_dev$legacy(before.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
export function detach_before_dev$legacy(after: Node) {
|
||||
while (after.previousSibling) {
|
||||
detach_dev$legacy(after.previousSibling);
|
||||
}
|
||||
}
|
||||
|
||||
export function detach_after_dev$legacy(before: Node) {
|
||||
while (before.nextSibling) {
|
||||
detach_dev$legacy(before.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
export function listen_dev$legacy(
|
||||
node: Node,
|
||||
event: string,
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions | EventListenerOptions,
|
||||
has_prevent_default?: boolean,
|
||||
has_stop_propagation?: boolean
|
||||
) {
|
||||
const modifiers = options === true ? ['capture'] : options ? Array.from(Object.keys(options)) : [];
|
||||
if (has_prevent_default) modifiers.push('preventDefault');
|
||||
if (has_stop_propagation) modifiers.push('stopPropagation');
|
||||
|
||||
dispatch_dev$legacy('SvelteDOMAddEventListener', { node, event, handler, modifiers });
|
||||
|
||||
const dispose = listen(node, event, handler, options);
|
||||
return () => {
|
||||
dispatch_dev$legacy('SvelteDOMRemoveEventListener', { node, event, handler, modifiers });
|
||||
dispose();
|
||||
};
|
||||
}
|
||||
|
||||
export function attr_dev$legacy(node: Element, attribute: string, value?: string) {
|
||||
attr(node, attribute, value);
|
||||
|
||||
if (value == null) dispatch_dev$legacy('SvelteDOMRemoveAttribute', { node, attribute });
|
||||
else dispatch_dev$legacy('SvelteDOMSetAttribute', { node, attribute, value });
|
||||
}
|
||||
|
||||
export function prop_dev$legacy(node: Element, property: string, value?: any) {
|
||||
node[property] = value;
|
||||
|
||||
dispatch_dev$legacy('SvelteDOMSetProperty', { node, property, value });
|
||||
}
|
||||
|
||||
export function dataset_dev$legacy(node: HTMLElement, property: string, value?: any) {
|
||||
node.dataset[property] = value;
|
||||
|
||||
dispatch_dev$legacy('SvelteDOMSetDataset', { node, property, value });
|
||||
}
|
||||
|
||||
export function set_data_dev$legacy(text, data) {
|
||||
data = '' + data;
|
||||
if (text.data === data) return;
|
||||
|
||||
dispatch_dev$legacy('SvelteDOMSetData', { node: text, data });
|
||||
text.data = data;
|
||||
}
|
@ -1,88 +1,108 @@
|
||||
import { raf, now } from './environment';
|
||||
import { now, raf } from './environment';
|
||||
import { calc_framerate, FRAME_RATE } from './style_manager';
|
||||
import { noop } from './utils';
|
||||
type TaskCallback = (t: number) => boolean;
|
||||
type TaskCanceller = () => void;
|
||||
|
||||
export interface Task {
|
||||
abort(): void;
|
||||
promise: Promise<void>;
|
||||
let next_frame: Array<TaskCallback> = [];
|
||||
let running_frame: Array<TaskCallback> = [];
|
||||
function run(t: number) {
|
||||
[running_frame, next_frame] = [next_frame, running_frame];
|
||||
let next_frame_tasks = 0;
|
||||
for (let i = 0, j = running_frame.length, v; i < j; i++) {
|
||||
if ((v = running_frame[i])(t)) next_frame[next_frame_tasks++] = v;
|
||||
}
|
||||
running_frame.length = 0;
|
||||
if (next_frame_tasks) raf(run);
|
||||
}
|
||||
type TaskCallback = (now: number) => boolean | void;
|
||||
type TaskEntry = { c: TaskCallback; f: () => void };
|
||||
|
||||
const tasks = new Set<TaskEntry>();
|
||||
|
||||
function run_tasks(now: number) {
|
||||
tasks.forEach((task) => {
|
||||
if (task.c(now)) return;
|
||||
tasks.delete(task);
|
||||
task.f();
|
||||
});
|
||||
if (tasks.size) raf(run_tasks);
|
||||
type TimeoutTask = { t: number; c: (now: number) => void };
|
||||
const timed_tasks: Array<TimeoutTask> = [];
|
||||
const pending_insert_timed: Array<TimeoutTask> = [];
|
||||
let running_timed = false;
|
||||
let pending_inserts = false;
|
||||
function run_timed(now: number) {
|
||||
/* Runs every timed out task */
|
||||
let last_index = timed_tasks.length - 1;
|
||||
while (last_index >= 0 && now >= timed_tasks[last_index].t) {
|
||||
timed_tasks[last_index--].c(now);
|
||||
}
|
||||
if (pending_inserts) {
|
||||
for (
|
||||
let i = 0, j = last_index, this_task: TimeoutTask, that_task: TimeoutTask;
|
||||
i < pending_insert_timed.length;
|
||||
i++
|
||||
) {
|
||||
if (now >= (this_task = pending_insert_timed[i]).t) {
|
||||
this_task.c(now);
|
||||
} else {
|
||||
for (j = last_index; j > 0 && this_task.t > (that_task = timed_tasks[j]).t; j--) {
|
||||
/* move each task up until this_task.timestamp > task.timestamp */
|
||||
timed_tasks[j + 1] = that_task;
|
||||
}
|
||||
timed_tasks[j] = this_task;
|
||||
last_index++;
|
||||
}
|
||||
}
|
||||
pending_inserts = !!(pending_insert_timed.length = 0);
|
||||
}
|
||||
return (running_timed = !!(timed_tasks.length = last_index + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing purposes only!
|
||||
*/
|
||||
export function clear_loops() {
|
||||
tasks.clear();
|
||||
export function setAnimationTimeout(callback: () => void, timestamp: number): TaskCanceller {
|
||||
const task: TimeoutTask = { c: callback, t: timestamp };
|
||||
if (running_timed) {
|
||||
pending_inserts = !!pending_insert_timed.push(task);
|
||||
} else {
|
||||
timed_tasks.push(task);
|
||||
if (1 === next_frame.push(run_timed)) raf(run);
|
||||
}
|
||||
return () => void (task.c = noop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new task that runs on each raf frame
|
||||
* until it returns a falsy value or is aborted
|
||||
* Calls function every frame with a value going from 0 to 1
|
||||
*/
|
||||
export function loop(callback: TaskCallback): Task {
|
||||
let task: TaskEntry;
|
||||
if (!tasks.size) raf(run_tasks);
|
||||
return {
|
||||
promise: new Promise((fulfill) => {
|
||||
tasks.add((task = { c: callback, f: fulfill }));
|
||||
}),
|
||||
abort() {
|
||||
tasks.delete(task);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function add(c: TaskCallback) {
|
||||
const task = { c, f: noop };
|
||||
if (!tasks.size) raf(run_tasks);
|
||||
tasks.add(task);
|
||||
return () => tasks.delete(task);
|
||||
export function useTween(
|
||||
run: (now: number) => void,
|
||||
stop: () => void,
|
||||
end_time: number,
|
||||
duration = end_time - now()
|
||||
): TaskCanceller {
|
||||
let running = true;
|
||||
if (
|
||||
1 ===
|
||||
next_frame.push((t) => {
|
||||
if (!running) return false;
|
||||
t = (end_time - t) / duration;
|
||||
if (t >= 1) return run(1), stop(), (running = false);
|
||||
if (t >= 0) run(t);
|
||||
return running;
|
||||
})
|
||||
)
|
||||
raf(run);
|
||||
return () => void (running = false);
|
||||
}
|
||||
|
||||
type TimeoutTask = { t: number; c: (now: number) => void };
|
||||
|
||||
// sorted descending
|
||||
const timed_tasks: Array<TimeoutTask> = [];
|
||||
|
||||
/**
|
||||
* basically
|
||||
* (fn, t) => setTimeout( () => raf(fn), t )
|
||||
* Calls function every frame with the amount of elapsed frames
|
||||
*/
|
||||
export function setAnimationTimeout(callback: () => void, timestamp: number) {
|
||||
let i = timed_tasks.length;
|
||||
let v;
|
||||
const task = { c: callback, t: timestamp };
|
||||
if (i) {
|
||||
while (i > 0 && timestamp > (v = timed_tasks[i - 1]).t) timed_tasks[i--] = v;
|
||||
timed_tasks[i] = task;
|
||||
} else {
|
||||
timed_tasks.push(task);
|
||||
add((now) => {
|
||||
let i = timed_tasks.length;
|
||||
while (i > 0 && now >= timed_tasks[--i].t) timed_tasks.pop().c(now);
|
||||
return !!timed_tasks.length;
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
const index = timed_tasks.indexOf(task);
|
||||
if (~index) timed_tasks.splice(index, 1);
|
||||
};
|
||||
export function onEachFrame(
|
||||
each_frame: (seconds_elapsed: number) => boolean,
|
||||
on_stop?,
|
||||
max_skipped_frames = 4
|
||||
): TaskCanceller {
|
||||
max_skipped_frames *= FRAME_RATE || calc_framerate();
|
||||
let lastTime = now();
|
||||
let running = true;
|
||||
if (
|
||||
1 ===
|
||||
next_frame.push((t: number) => {
|
||||
if (!running) return on_stop && on_stop(t), false;
|
||||
if (t > lastTime + max_skipped_frames) t = lastTime + max_skipped_frames;
|
||||
return each_frame((-lastTime + (lastTime = t)) / 1000);
|
||||
})
|
||||
)
|
||||
raf(run);
|
||||
return () => void (running = false);
|
||||
}
|
||||
export const loopThen = (run: (now: number) => void, stop: () => void, duration: number, end_time: number) =>
|
||||
add((t) => {
|
||||
t = (end_time - t) / duration;
|
||||
if (t >= 1) return void run(1), stop();
|
||||
else if (t >= 0) run(t);
|
||||
return true;
|
||||
});
|
||||
// tests
|
||||
export const clear_loops = () =>
|
||||
void (next_frame.length = running_frame.length = timed_tasks.length = pending_insert_timed.length = 0);
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { safe_not_equal, noop } from './utils';
|
||||
type Setter<T> = (value: T) => void;
|
||||
type StopCallback = () => void;
|
||||
export type StartStopNotifier<T> = (set: Setter<T>) => StopCallback | void;
|
||||
type Subscriber<T> = (value: T) => void;
|
||||
type Unsubscriber = () => void;
|
||||
export class Store<T> {
|
||||
static update_queue: Store<any>[] = [];
|
||||
static is_flushing = false;
|
||||
static flush(store: Store<any>) {
|
||||
this.update_queue.push(store);
|
||||
if (this.is_flushing) return;
|
||||
this.is_flushing = true;
|
||||
for (let i = 0, j = 0, subscribers, value; i < this.update_queue.length; i++) {
|
||||
for (j = 0, { subscribers, value } = this.update_queue[i]; j < subscribers.length; j++) {
|
||||
subscribers[j].r(value);
|
||||
}
|
||||
}
|
||||
this.update_queue.length = +(this.is_flushing = false);
|
||||
}
|
||||
value: T;
|
||||
has_subscribers = false;
|
||||
subscribers = [];
|
||||
constructor(initial: T) {
|
||||
this.value = initial;
|
||||
}
|
||||
set(next_value: T) {
|
||||
this.value = next_value;
|
||||
if (!this.has_subscribers) return;
|
||||
for (let i = 0; i < this.subscribers.length; i++) this.subscribers[i].i();
|
||||
Store.flush(this);
|
||||
}
|
||||
subscribe(run: Subscriber<T>, invalidate = noop) {
|
||||
const subscriber = { r: run, i: invalidate };
|
||||
this.subscribers.push(subscriber);
|
||||
run(this.value), (this.has_subscribers = true);
|
||||
return this.unsubscribe.bind(this, subscriber) as Unsubscriber;
|
||||
}
|
||||
unsubscribe(subscriber) {
|
||||
this.subscribers.splice(this.subscribers.indexOf(subscriber), 1);
|
||||
this.has_subscribers = !!this.subscribers.length;
|
||||
}
|
||||
}
|
||||
export class Writable<T> extends Store<T> {
|
||||
start: StartStopNotifier<T>;
|
||||
stop = noop;
|
||||
constructor(initial: T, startStopNotifier: StartStopNotifier<T>) {
|
||||
super(initial);
|
||||
this.start = startStopNotifier || noop;
|
||||
}
|
||||
subscribe(run, invalidate) {
|
||||
if (!super.has_subscribers) this.stop = this.start(this.set.bind(this)) || noop;
|
||||
return super.subscribe(run, invalidate);
|
||||
}
|
||||
set(next_value: T) {
|
||||
if (this.stop && safe_not_equal(super.value, next_value)) super.set(next_value);
|
||||
}
|
||||
update(fn) {
|
||||
this.set(fn(this.value));
|
||||
}
|
||||
unsubscribe(subscriber) {
|
||||
super.unsubscribe(subscriber);
|
||||
if (!super.has_subscribers && this.stop) this.stop(), (this.stop = null);
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
// import { Readable, writable } from 'svelte/store';
|
||||
// import { assign, loop, now, Task } from 'svelte/internal';
|
||||
// import { linear } from 'svelte/easing';
|
||||
// import { is_date } from './utils';
|
||||
|
||||
// function get_interpolator(a, b) {
|
||||
// if (a === b || a !== a) return () => a;
|
||||
|
||||
// const type = typeof a;
|
||||
|
||||
// if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) {
|
||||
// throw new Error('Cannot interpolate values of different type');
|
||||
// }
|
||||
|
||||
// if (Array.isArray(a)) {
|
||||
// const arr = b.map((bi, i) => {
|
||||
// return get_interpolator(a[i], bi);
|
||||
// });
|
||||
|
||||
// return t => arr.map(fn => fn(t));
|
||||
// }
|
||||
|
||||
// if (type === 'object') {
|
||||
// if (!a || !b) throw new Error('Object cannot be null');
|
||||
|
||||
// if (is_date(a) && is_date(b)) {
|
||||
// a = a.getTime();
|
||||
// b = b.getTime();
|
||||
// const delta = b - a;
|
||||
// return t => new Date(a + t * delta);
|
||||
// }
|
||||
|
||||
// const keys = Object.keys(b);
|
||||
// const interpolators = {};
|
||||
|
||||
// keys.forEach(key => {
|
||||
// interpolators[key] = get_interpolator(a[key], b[key]);
|
||||
// });
|
||||
|
||||
// return t => {
|
||||
// const result = {};
|
||||
// keys.forEach(key => {
|
||||
// result[key] = interpolators[key](t);
|
||||
// });
|
||||
// return result;
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (type === 'number') {
|
||||
// const delta = b - a;
|
||||
// return t => a + t * delta;
|
||||
// }
|
||||
|
||||
// throw new Error(`Cannot interpolate ${type} values`);
|
||||
// }
|
||||
|
||||
// interface Options<T> {
|
||||
// delay?: number;
|
||||
// duration?: number | ((from: T, to: T) => number);
|
||||
// easing?: (t: number) => number;
|
||||
// interpolate?: (a: T, b: T) => (t: number) => T;
|
||||
// }
|
||||
|
||||
// type Updater<T> = (target_value: T, value: T) => T;
|
||||
|
||||
// interface Tweened<T> extends Readable<T> {
|
||||
// set(value: T, opts: Options<T>): Promise<void>;
|
||||
|
||||
// update(updater: Updater<T>, opts: Options<T>): Promise<void>;
|
||||
// }
|
||||
|
||||
// export function tweened<T>(value?: T, defaults: Options<T> = {}): Tweened<T> {
|
||||
// const store = writable(value);
|
||||
|
||||
// let task: Task;
|
||||
// let target_value = value;
|
||||
|
||||
// function set(new_value: T, opts: Options<T>) {
|
||||
// if (value == null) {
|
||||
// store.set(value = new_value);
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
|
||||
// target_value = new_value;
|
||||
|
||||
// let previous_task = task;
|
||||
// let started = false;
|
||||
|
||||
// let {
|
||||
// delay = 0,
|
||||
// duration = 400,
|
||||
// easing = linear,
|
||||
// interpolate = get_interpolator
|
||||
// } = assign(assign({}, defaults), opts);
|
||||
|
||||
// const start = now() + delay;
|
||||
// let fn;
|
||||
|
||||
// task = loop(now => {
|
||||
// if (now < start) return true;
|
||||
|
||||
// if (!started) {
|
||||
// fn = interpolate(value, new_value);
|
||||
// if (typeof duration === 'function') duration = duration(value, new_value);
|
||||
// started = true;
|
||||
// }
|
||||
|
||||
// if (previous_task) {
|
||||
// previous_task.abort();
|
||||
// previous_task = null;
|
||||
// }
|
||||
|
||||
// const elapsed = now - start;
|
||||
|
||||
// if (elapsed > duration) {
|
||||
// store.set(value = new_value);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // @ts-ignore
|
||||
// store.set(value = fn(easing(elapsed / duration)));
|
||||
// return true;
|
||||
// });
|
||||
|
||||
// return task.promise;
|
||||
// }
|
||||
|
||||
// return {
|
||||
// set,
|
||||
// update: (fn, opts: Options<T>) => set(fn(target_value, value), opts),
|
||||
// subscribe: store.subscribe
|
||||
// };
|
||||
// }
|
@ -1,2 +1,2 @@
|
||||
export * from './spring';
|
||||
export * from './tweened';
|
||||
// export * from './_tweened';
|
||||
|
@ -1,147 +1,81 @@
|
||||
import { Readable, writable } from 'svelte/store';
|
||||
import { loop, now, Task } from 'svelte/internal';
|
||||
import { is_date } from './utils';
|
||||
|
||||
interface TickContext<T> {
|
||||
inv_mass: number;
|
||||
dt: number;
|
||||
opts: Spring<T>;
|
||||
settled: boolean;
|
||||
}
|
||||
|
||||
function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, target_value: T): T {
|
||||
if (typeof current_value === 'number' || is_date(current_value)) {
|
||||
// @ts-ignore
|
||||
const delta = target_value - current_value;
|
||||
// @ts-ignore
|
||||
const velocity = (current_value - last_value) / (ctx.dt||1/60); // guard div by 0
|
||||
const spring = ctx.opts.stiffness * delta;
|
||||
const damper = ctx.opts.damping * velocity;
|
||||
const acceleration = (spring - damper) * ctx.inv_mass;
|
||||
const d = (velocity + acceleration) * ctx.dt;
|
||||
|
||||
if (Math.abs(d) < ctx.opts.precision && Math.abs(delta) < ctx.opts.precision) {
|
||||
return target_value; // settled
|
||||
} else {
|
||||
ctx.settled = false; // signal loop to keep ticking
|
||||
// @ts-ignore
|
||||
return is_date(current_value) ?
|
||||
new Date(current_value.getTime() + d) : current_value + d;
|
||||
}
|
||||
} else if (Array.isArray(current_value)) {
|
||||
// @ts-ignore
|
||||
return current_value.map((_, i) =>
|
||||
tick_spring(ctx, last_value[i], current_value[i], target_value[i]));
|
||||
} else if (typeof current_value === 'object') {
|
||||
const next_value = {};
|
||||
for (const k in current_value)
|
||||
// @ts-ignore
|
||||
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
|
||||
// @ts-ignore
|
||||
return next_value;
|
||||
import { Store, onEachFrame } from 'svelte/internal';
|
||||
|
||||
function solve_spring(
|
||||
prev_value: number,
|
||||
prev_velocity: number,
|
||||
target_value: number,
|
||||
{ stiffness, mass, damping, soft }
|
||||
) {
|
||||
const delta = target_value - prev_value;
|
||||
if (soft || 1 <= damping / (2.0 * Math.sqrt(stiffness * mass))) {
|
||||
const angular_frequency = -Math.sqrt(stiffness / mass);
|
||||
return (t: number) =>
|
||||
target_value - (delta + t * (-angular_frequency * delta - prev_velocity)) * Math.exp(t * angular_frequency);
|
||||
} else {
|
||||
throw new Error(`Cannot spring ${typeof current_value} values`);
|
||||
const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2);
|
||||
const leftover = (damping * delta - 2.0 * mass * prev_velocity) / damping_frequency;
|
||||
const dfm = (0.5 * damping_frequency) / mass;
|
||||
const dm = -(0.5 * damping) / mass;
|
||||
let f = 0.0;
|
||||
return (t: number) => target_value - (Math.cos((f = t * dfm)) * delta + Math.sin(f) * leftover) * Math.exp(t * dm);
|
||||
}
|
||||
}
|
||||
|
||||
interface SpringOpts {
|
||||
stiffness?: number;
|
||||
damping?: number;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
interface SpringUpdateOpts {
|
||||
hard?: any;
|
||||
soft?: string | number | boolean;
|
||||
class MotionStore<T> extends Store<T> {
|
||||
elapsed = 0.0;
|
||||
running = false;
|
||||
setTick: (prev_value: T, next_value: T) => (current_value: T, elapsed: number, dt: number) => boolean;
|
||||
tick: (current_value: T, elapsed: number, dt: number) => boolean;
|
||||
constructor(value, startSetTick) {
|
||||
super(value);
|
||||
this.setTick = startSetTick(super.set.bind(this));
|
||||
}
|
||||
set(next_value: T) {
|
||||
this.elapsed = 0.0;
|
||||
this.tick = this.setTick(this.value, next_value);
|
||||
if (this.running) return;
|
||||
else this.running = true;
|
||||
onEachFrame((dt) => (this.running = this.tick(this.value, (this.elapsed += dt), dt)));
|
||||
}
|
||||
}
|
||||
|
||||
type Updater<T> = (target_value: T, value: T) => T;
|
||||
|
||||
interface Spring<T> extends Readable<T>{
|
||||
set: (new_value: T, opts?: SpringUpdateOpts) => Promise<void>;
|
||||
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
|
||||
precision: number;
|
||||
damping: number;
|
||||
stiffness: number;
|
||||
export function spring(value, params) {
|
||||
const { mass = 1.0, damping = 10.0, stiffness = 100.0, precision = 0.001, soft = false } = params || {};
|
||||
return new MotionStore(
|
||||
value,
|
||||
parseStructure(value, (set) => {
|
||||
let velocity = 0.0,
|
||||
calc;
|
||||
return (from_value, to_value) => {
|
||||
calc = solve_spring(from_value, velocity, to_value, { mass, damping, stiffness, soft });
|
||||
return (current, elapsed, dt) =>
|
||||
precision > Math.abs((velocity = (-current + (current = calc(elapsed))) / dt)) &&
|
||||
precision > Math.abs(to_value - current)
|
||||
? (set(to_value), !!(velocity = 0.0))
|
||||
: (set(current), true);
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function spring<T=any>(value?: T, opts: SpringOpts = {}): Spring<T> {
|
||||
const store = writable(value);
|
||||
const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;
|
||||
|
||||
let last_time: number;
|
||||
let task: Task;
|
||||
let current_token: object;
|
||||
let last_value: T = value;
|
||||
let target_value: T = value;
|
||||
|
||||
let inv_mass = 1;
|
||||
let inv_mass_recovery_rate = 0;
|
||||
let cancel_task = false;
|
||||
|
||||
function set(new_value: T, opts: SpringUpdateOpts={}): Promise<void> {
|
||||
target_value = new_value;
|
||||
const token = current_token = {};
|
||||
|
||||
if (value == null || opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
|
||||
cancel_task = true; // cancel any running animation
|
||||
last_time = now();
|
||||
last_value = new_value;
|
||||
store.set(value = target_value);
|
||||
return Promise.resolve();
|
||||
} else if (opts.soft) {
|
||||
const rate = opts.soft === true ? .5 : +opts.soft;
|
||||
inv_mass_recovery_rate = 1 / (rate * 60);
|
||||
inv_mass = 0; // infinite mass, unaffected by spring forces
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
last_time = now();
|
||||
cancel_task = false;
|
||||
|
||||
task = loop(now => {
|
||||
|
||||
if (cancel_task) {
|
||||
cancel_task = false;
|
||||
task = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1);
|
||||
|
||||
const ctx: TickContext<T> = {
|
||||
inv_mass,
|
||||
opts: spring,
|
||||
settled: true, // tick_spring may signal false
|
||||
dt: (now - last_time) * 60 / 1000
|
||||
};
|
||||
const next_value = tick_spring(ctx, last_value, value, target_value);
|
||||
|
||||
last_time = now;
|
||||
last_value = value;
|
||||
store.set(value = next_value);
|
||||
|
||||
if (ctx.settled)
|
||||
task = null;
|
||||
return !ctx.settled;
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(fulfil => {
|
||||
task.promise.then(() => {
|
||||
if (token === current_token) fulfil();
|
||||
});
|
||||
});
|
||||
function parseStructure(obj, schema) {
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
const keys = Object.keys(obj);
|
||||
let i = 0,
|
||||
k = '',
|
||||
setTickers = keys.map((key) => parseStructure(obj[key], schema)((next_value) => (obj[key] = next_value))),
|
||||
tickers,
|
||||
pending = 0;
|
||||
const target = { ...obj };
|
||||
const isArray = Array.isArray(obj);
|
||||
obj = isArray ? [...obj] : { ...obj };
|
||||
return (set) => (_from_value, to_value) => {
|
||||
for (k in to_value) if (to_value[k] !== obj[k]) target[k] = to_value[k];
|
||||
tickers = setTickers.map((setTicker, i) => ((pending |= 1 << i), setTicker(obj[keys[i]], target[keys[i]])));
|
||||
return (_current, elapsed, dt) => {
|
||||
for (i = 0; i < tickers.length; i++)
|
||||
if (pending & (1 << i) && !tickers[i](obj[keys[i]], elapsed, dt)) pending &= ~(1 << i);
|
||||
set(isArray ? [...obj] : { ...obj });
|
||||
return !!pending;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const spring: Spring<T> = {
|
||||
set,
|
||||
update: (fn, opts: SpringUpdateOpts) => set(fn(target_value, value), opts),
|
||||
subscribe: store.subscribe,
|
||||
stiffness,
|
||||
damping,
|
||||
precision
|
||||
};
|
||||
|
||||
return spring;
|
||||
return schema;
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
import { Readable, writable } from 'svelte/store';
|
||||
import { assign, loop, now, Task } from 'svelte/internal';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { is_date } from './utils';
|
||||
|
||||
function get_interpolator(a, b) {
|
||||
if (a === b || a !== a) return () => a;
|
||||
|
||||
const type = typeof a;
|
||||
|
||||
if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) {
|
||||
throw new Error('Cannot interpolate values of different type');
|
||||
}
|
||||
|
||||
if (Array.isArray(a)) {
|
||||
const arr = b.map((bi, i) => {
|
||||
return get_interpolator(a[i], bi);
|
||||
});
|
||||
|
||||
return t => arr.map(fn => fn(t));
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
if (!a || !b) throw new Error('Object cannot be null');
|
||||
|
||||
if (is_date(a) && is_date(b)) {
|
||||
a = a.getTime();
|
||||
b = b.getTime();
|
||||
const delta = b - a;
|
||||
return t => new Date(a + t * delta);
|
||||
}
|
||||
|
||||
const keys = Object.keys(b);
|
||||
const interpolators = {};
|
||||
|
||||
keys.forEach(key => {
|
||||
interpolators[key] = get_interpolator(a[key], b[key]);
|
||||
});
|
||||
|
||||
return t => {
|
||||
const result = {};
|
||||
keys.forEach(key => {
|
||||
result[key] = interpolators[key](t);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const delta = b - a;
|
||||
return t => a + t * delta;
|
||||
}
|
||||
|
||||
throw new Error(`Cannot interpolate ${type} values`);
|
||||
}
|
||||
|
||||
interface Options<T> {
|
||||
delay?: number;
|
||||
duration?: number | ((from: T, to: T) => number);
|
||||
easing?: (t: number) => number;
|
||||
interpolate?: (a: T, b: T) => (t: number) => T;
|
||||
}
|
||||
|
||||
type Updater<T> = (target_value: T, value: T) => T;
|
||||
|
||||
interface Tweened<T> extends Readable<T> {
|
||||
set(value: T, opts: Options<T>): Promise<void>;
|
||||
|
||||
update(updater: Updater<T>, opts: Options<T>): Promise<void>;
|
||||
}
|
||||
|
||||
export function tweened<T>(value?: T, defaults: Options<T> = {}): Tweened<T> {
|
||||
const store = writable(value);
|
||||
|
||||
let task: Task;
|
||||
let target_value = value;
|
||||
|
||||
function set(new_value: T, opts: Options<T>) {
|
||||
if (value == null) {
|
||||
store.set(value = new_value);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
target_value = new_value;
|
||||
|
||||
let previous_task = task;
|
||||
let started = false;
|
||||
|
||||
let {
|
||||
delay = 0,
|
||||
duration = 400,
|
||||
easing = linear,
|
||||
interpolate = get_interpolator
|
||||
} = assign(assign({}, defaults), opts);
|
||||
|
||||
const start = now() + delay;
|
||||
let fn;
|
||||
|
||||
task = loop(now => {
|
||||
if (now < start) return true;
|
||||
|
||||
if (!started) {
|
||||
fn = interpolate(value, new_value);
|
||||
if (typeof duration === 'function') duration = duration(value, new_value);
|
||||
started = true;
|
||||
}
|
||||
|
||||
if (previous_task) {
|
||||
previous_task.abort();
|
||||
previous_task = null;
|
||||
}
|
||||
|
||||
const elapsed = now - start;
|
||||
|
||||
if (elapsed > duration) {
|
||||
store.set(value = new_value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
store.set(value = fn(easing(elapsed / duration)));
|
||||
return true;
|
||||
});
|
||||
|
||||
return task.promise;
|
||||
}
|
||||
|
||||
return {
|
||||
set,
|
||||
update: (fn, opts: Options<T>) => set(fn(target_value, value), opts),
|
||||
subscribe: store.subscribe
|
||||
};
|
||||
}
|
@ -1,204 +1,45 @@
|
||||
import { run_all, subscribe, noop, safe_not_equal, is_function, get_store_value } from 'svelte/internal';
|
||||
|
||||
/** Callback to inform of a value updates. */
|
||||
type Subscriber<T> = (value: T) => void;
|
||||
|
||||
/** Unsubscribes from value updates. */
|
||||
type Unsubscriber = () => void;
|
||||
|
||||
/** Callback to update a value. */
|
||||
type Updater<T> = (value: T) => T;
|
||||
|
||||
/** Cleanup logic callback. */
|
||||
type Invalidator<T> = (value?: T) => void;
|
||||
|
||||
/** Start and stop notification callbacks. */
|
||||
type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
|
||||
|
||||
/** Readable interface for subscribing. */
|
||||
export interface Readable<T> {
|
||||
/**
|
||||
* Subscribe on value changes.
|
||||
* @param run subscription callback
|
||||
* @param invalidate cleanup callback
|
||||
*/
|
||||
subscribe(run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber;
|
||||
import { subscribe, noop, get_store_value, Writable, StartStopNotifier } from 'svelte/internal';
|
||||
export { get_store_value as get };
|
||||
export function readable<T>(value: T, start: StartStopNotifier<T>) {
|
||||
const store = new Writable(value, start);
|
||||
return { subscribe: store.subscribe.bind(store) };
|
||||
}
|
||||
|
||||
/** Writable interface for both updating and subscribing. */
|
||||
export interface Writable<T> extends Readable<T> {
|
||||
/**
|
||||
* Set value and inform subscribers.
|
||||
* @param value to set
|
||||
*/
|
||||
set(value: T): void;
|
||||
|
||||
/**
|
||||
* Update value using callback and inform subscribers.
|
||||
* @param updater callback
|
||||
*/
|
||||
update(updater: Updater<T>): void;
|
||||
export function writable<T>(value: T, start: StartStopNotifier<T>) {
|
||||
const store = new Writable(value, start);
|
||||
return { set: store.set.bind(store), update: store.update.bind(store), subscribe: store.subscribe.bind(store) };
|
||||
}
|
||||
|
||||
/** Pair of subscriber and invalidator. */
|
||||
type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidator<T>];
|
||||
|
||||
const subscriber_queue = [];
|
||||
|
||||
/**
|
||||
* Creates a `Readable` store that allows reading by subscription.
|
||||
* @param value initial value
|
||||
* @param {StartStopNotifier}start start and stop notifications for subscriptions
|
||||
*/
|
||||
export function readable<T>(value: T, start: StartStopNotifier<T>): Readable<T> {
|
||||
return {
|
||||
subscribe: writable(value, start).subscribe,
|
||||
};
|
||||
export function derived<T, S>(stores: S, deriver, initial_value?: T) {
|
||||
let cleanup = noop;
|
||||
const dispatcher =
|
||||
deriver.length < 2
|
||||
? (set, v) => void set(deriver(v))
|
||||
: (set, v) => void (cleanup(), typeof (cleanup = deriver(v, set)) !== 'function' && (cleanup = noop));
|
||||
return readable(
|
||||
initial_value,
|
||||
Array.isArray(stores)
|
||||
? (set) => {
|
||||
set = dispatcher.bind(null, set);
|
||||
let l = stores.length;
|
||||
let pending = 1 << l;
|
||||
const values = new Array(l);
|
||||
const unsubs = stores.map((store, i) =>
|
||||
subscribe(
|
||||
store,
|
||||
// @ts-ignore
|
||||
(v) => void ((values[i] = v), !(pending &= ~(1 << i)) && set(values)),
|
||||
() => void (pending |= 1 << i)
|
||||
)
|
||||
);
|
||||
// @ts-ignore
|
||||
if (!(pending &= ~(1 << l))) set(values);
|
||||
return () => {
|
||||
while (l--) unsubs[l]();
|
||||
cleanup();
|
||||
};
|
||||
}
|
||||
: (set) => {
|
||||
const unsub = subscribe(stores, dispatcher.bind(null, set));
|
||||
return () => void (unsub(), cleanup());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `Writable` store that allows both updating and reading by subscription.
|
||||
* @param {*=}value initial value
|
||||
* @param {StartStopNotifier=}start start and stop notifications for subscriptions
|
||||
*/
|
||||
export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writable<T> {
|
||||
let stop: Unsubscriber;
|
||||
const subscribers: Array<SubscribeInvalidateTuple<T>> = [];
|
||||
|
||||
function set(new_value: T): void {
|
||||
if (safe_not_equal(value, new_value)) {
|
||||
value = new_value;
|
||||
if (stop) { // store is ready
|
||||
const run_queue = !subscriber_queue.length;
|
||||
for (let i = 0; i < subscribers.length; i += 1) {
|
||||
const s = subscribers[i];
|
||||
s[1]();
|
||||
subscriber_queue.push(s, value);
|
||||
}
|
||||
if (run_queue) {
|
||||
for (let i = 0; i < subscriber_queue.length; i += 2) {
|
||||
subscriber_queue[i][0](subscriber_queue[i + 1]);
|
||||
}
|
||||
subscriber_queue.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(fn: Updater<T>): void {
|
||||
set(fn(value));
|
||||
}
|
||||
|
||||
function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber {
|
||||
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
|
||||
subscribers.push(subscriber);
|
||||
if (subscribers.length === 1) {
|
||||
stop = start(set) || noop;
|
||||
}
|
||||
run(value);
|
||||
|
||||
return () => {
|
||||
const index = subscribers.indexOf(subscriber);
|
||||
if (index !== -1) {
|
||||
subscribers.splice(index, 1);
|
||||
}
|
||||
if (subscribers.length === 0) {
|
||||
stop();
|
||||
stop = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { set, update, subscribe };
|
||||
}
|
||||
|
||||
/** One or more `Readable`s. */
|
||||
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>];
|
||||
|
||||
/** One or more values from `Readable` stores. */
|
||||
type StoresValues<T> = T extends Readable<infer U> ? U :
|
||||
{ [K in keyof T]: T[K] extends Readable<infer U> ? U : never };
|
||||
|
||||
/**
|
||||
* Derived value store by synchronizing one or more readable stores and
|
||||
* applying an aggregation function over its input values.
|
||||
*
|
||||
* @param stores - input stores
|
||||
* @param fn - function callback that aggregates the values
|
||||
*/
|
||||
export function derived<S extends Stores, T>(
|
||||
stores: S,
|
||||
fn: (values: StoresValues<S>) => T
|
||||
): Readable<T>;
|
||||
|
||||
/**
|
||||
* Derived value store by synchronizing one or more readable stores and
|
||||
* applying an aggregation function over its input values.
|
||||
*
|
||||
* @param stores - input stores
|
||||
* @param fn - function callback that aggregates the values
|
||||
* @param initial_value - when used asynchronously
|
||||
*/
|
||||
export function derived<S extends Stores, T>(
|
||||
stores: S,
|
||||
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
|
||||
initial_value?: T
|
||||
): Readable<T>;
|
||||
|
||||
export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Readable<T> {
|
||||
const single = !Array.isArray(stores);
|
||||
const stores_array: Array<Readable<any>> = single
|
||||
? [stores as Readable<any>]
|
||||
: stores as Array<Readable<any>>;
|
||||
|
||||
const auto = fn.length < 2;
|
||||
|
||||
return readable(initial_value, (set) => {
|
||||
let inited = false;
|
||||
const values = [];
|
||||
|
||||
let pending = 0;
|
||||
let cleanup = noop;
|
||||
|
||||
const sync = () => {
|
||||
if (pending) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
const result = fn(single ? values[0] : values, set);
|
||||
if (auto) {
|
||||
set(result as T);
|
||||
} else {
|
||||
cleanup = is_function(result) ? result as Unsubscriber : noop;
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribers = stores_array.map((store, i) => subscribe(
|
||||
store,
|
||||
(value) => {
|
||||
values[i] = value;
|
||||
pending &= ~(1 << i);
|
||||
if (inited) {
|
||||
sync();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
pending |= (1 << i);
|
||||
}),
|
||||
);
|
||||
|
||||
inited = true;
|
||||
sync();
|
||||
|
||||
return function stop() {
|
||||
run_all(unsubscribers);
|
||||
cleanup();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value from a store by subscribing and immediately unsubscribing.
|
||||
* @param store readable
|
||||
*/
|
||||
export { get_store_value as get };
|
Loading…
Reference in new issue