diff --git a/package.json b/package.json index 734affc654..88b9a26bfe 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "c8": "^7.1.1", "code-red": "0.1.1", "codecov": "^3.6.5", - "css-tree": "1.0.0-alpha39", + "css-tree": "1.0.0-alpha22", "eslint": "^6.8.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-svelte3": "^2.7.3", diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 88074683ec..48e9ef0236 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -116,7 +116,7 @@ export function init( if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = val))) { if (i in $$.bound) $$.bound[i](val); if (ready) { - if (!~$$.dirty) { + if (!~$$.dirty[0]) { schedule_update(component); $$.dirty.fill(0); } diff --git a/src/runtime/internal/environment.ts b/src/runtime/internal/environment.ts index aa1c62b830..0ab2575798 100644 --- a/src/runtime/internal/environment.ts +++ b/src/runtime/internal/environment.ts @@ -1,9 +1,9 @@ import { noop } from './utils'; - +export const resolved_promise = Promise.resolve(); export const is_client = typeof window !== 'undefined'; export const is_iframe = is_client && window.location !== window.parent.location; -export let now = is_client ?()=> performance.now() :()=> Date.now(); +export let now = is_client ? performance.now.bind(performance) : Date.now.bind(Date); export let raf = is_client ? window.requestAnimationFrame : noop; diff --git a/src/runtime/internal/loop.ts b/src/runtime/internal/loop.ts index 3e25d8ba96..e1bf02033a 100644 --- a/src/runtime/internal/loop.ts +++ b/src/runtime/internal/loop.ts @@ -6,103 +6,106 @@ type TaskCanceller = () => void; let next_frame: Array = []; let running_frame: Array = []; -function run(t: number) { +let next_frame_length = 0; +const run = (t: number) => { + t = now(); [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; + for (let i = (next_frame_length = 0), j = running_frame.length, v; i < j; i++) { + if ((v = running_frame[i])(t)) { + next_frame[next_frame_length++] = v; + } } running_frame.length = 0; - if (next_frame_tasks) raf(run); -} + if (next_frame_length) raf(run); + else console.log('ended loop'); +}; -type TimeoutTask = { t: number; c: (now: number) => void }; +type TimeoutTask = { timestamp: number; callback: (now: number) => void }; const timed_tasks: Array = []; const pending_insert_timed: Array = []; let running_timed = false; let pending_inserts = false; -function run_timed(now: number) { +const 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); - } + while (last_index >= 0 && now >= timed_tasks[last_index].timestamp) timed_tasks[last_index--].callback(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 */ + ) + if (now >= (this_task = pending_insert_timed[i]).timestamp) this_task.callback(now); + else { + /* moves each task up until this_task.timestamp > task.timestamp */ + for (j = last_index; j > 0 && this_task.timestamp > (that_task = timed_tasks[j]).timestamp; j--) 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)); -} -export function setAnimationTimeout(callback: () => void, timestamp: number): TaskCanceller { - const task: TimeoutTask = { c: callback, t: timestamp }; + return (running_timed = !!(timed_tasks.length = last_index)); +}; +const unsafe_loop = (fn) => { + if (!next_frame_length) raf(run); + next_frame[next_frame_length++] = fn; +}; +export const loop = (fn) => { + let running = true; + if (!next_frame_length) raf(run); + next_frame[next_frame_length++] = (t) => !running || fn(t); + return () => void (running = false); +}; +export const setAnimationTimeout = (callback: () => void, timestamp: number): TaskCanceller => { + const task: TimeoutTask = { callback, timestamp }; if (running_timed) { pending_inserts = !!pending_insert_timed.push(task); } else { + unsafe_loop(run_timed); + running_timed = true; timed_tasks.push(task); - if (1 === next_frame.push(run_timed)) raf(run); } - return () => void (task.c = noop); -} + return () => void (task.callback = noop); +}; /** * Calls function every frame with a value going from 0 to 1 */ -export function useTween( +export const useTween = ( run: (now: number) => void, stop: () => void, end_time: number, duration = end_time - now() -): TaskCanceller { +): 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); + unsafe_loop((t) => { + if (!running) return false; + t = 1 - (end_time - t) / duration; + if (t >= 1) return run(1), stop(), false; + if (t >= 0) run(t); + return running; + }); return () => void (running = false); -} +}; /** * Calls function every frame with the amount of elapsed frames */ -export function onEachFrame( +export const onEachFrame = ( each_frame: (seconds_elapsed: number) => boolean, on_stop?, max_skipped_frames = 4 -): TaskCanceller { +): 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); + const cancel = (t) => (on_stop && on_stop(t), false); + unsafe_loop((t: number) => { + if (!running) return cancel(t); + if (t > lastTime + max_skipped_frames) t = lastTime + max_skipped_frames; + return each_frame((-lastTime + (lastTime = t)) / 1000) ? true : cancel(t); + }); return () => void (running = false); -} +}; // tests export const clear_loops = () => - void (next_frame.length = running_frame.length = timed_tasks.length = pending_insert_timed.length = 0); + void (next_frame.length = running_frame.length = timed_tasks.length = pending_insert_timed.length = next_frame_length = +(running_timed = pending_inserts = false)); diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index 38855d8602..d62cc2ea4d 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -1,9 +1,8 @@ import { set_current_component } from './lifecycle'; +import { resolved_promise } from './environment'; const dirty_components = []; -const resolved_promise = Promise.resolve(); - let update_scheduled = false; export function schedule_update(component) { dirty_components.push(component); diff --git a/src/runtime/internal/stores.ts b/src/runtime/internal/stores.ts index bb4b5e2eeb..f1981baa3c 100644 --- a/src/runtime/internal/stores.ts +++ b/src/runtime/internal/stores.ts @@ -1,22 +1,33 @@ -import { safe_not_equal, noop } from './utils'; +import { safe_not_equal, noop, subscribe } from './utils'; +import { onEachFrame, useTween, loop } from './loop'; +import { now } from './environment'; type Setter = (value: T) => void; type StopCallback = () => void; export type StartStopNotifier = (set: Setter) => StopCallback | void; type Subscriber = (value: T) => void; type Unsubscriber = () => void; +interface Observable { + subscribe(callback: Subscriber): Unsubscriber; +} +type Observable_s = Observable[] | Observable; +type Deriver = (values: any, setter?: Setter) => void | (() => void) | T; + +/** + * Internal Svelte Observable + */ export class Store { - static update_queue: Store[] = []; + static value_queue: Store['value'][] = []; + static update_queue: Store['subscribers'][] = []; static is_flushing = false; static flush(store: Store) { - this.update_queue.push(store); + this.value_queue.push(store.value); + this.update_queue.push([...store.subscribers]); 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); + for (let i = 0, j = 0, subscribers, value; i < this.update_queue.length; i++) + for (j = 0, subscribers = this.update_queue[i], value = this.value_queue[i]; j < subscribers.length; j++) + subscribers[j].run(value); + this.update_queue.length = this.value_queue.length = +(this.is_flushing = false); } value: T; has_subscribers = false; @@ -27,21 +38,32 @@ export class Store { 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(); + for (let i = 0; i < this.subscribers.length; i++) this.subscribers[i].invalidate(); Store.flush(this); } subscribe(run: Subscriber, invalidate = noop) { - const subscriber = { r: run, i: invalidate }; + const subscriber = { run, 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; + const index = this.subscribers.indexOf(subscriber); + if (~index) { + if (Store.is_flushing) subscriber.run = subscriber.invalidate = noop; + this.subscribers.splice(index, 1); + this.has_subscribers = !!this.subscribers.length; + return true; + } + return false; } } -export class Writable extends Store { +/** + * like Store but + * + StartStopNotifier + * + update function + */ +class StartStopWritable extends Store { start: StartStopNotifier; stop = noop; constructor(initial: T, startStopNotifier: StartStopNotifier) { @@ -49,17 +71,178 @@ export class Writable extends Store { this.start = startStopNotifier || noop; } subscribe(run, invalidate) { + // *must* run *after* first subscription ? 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); + if (this.stop) 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); + if (super.unsubscribe(subscriber)) { + if (!this.has_subscribers) this.stop(); + return true; + } + return false; + } +} +/** + * StartStopWritable but + * + safe_not_equal + */ +export class Writable extends StartStopWritable { + set(next_value: T) { + if (safe_not_equal(this.value, next_value)) super.set(next_value); + } +} +export class Derived, T> extends StartStopWritable { + cleanup = noop; + target; + deriver; + set: (value_s: unknown | unknown[]) => void; + constructor(stores: S, deriver: D, initial_value?: T) { + super( + initial_value, + Array.isArray(stores) + ? (_set) => { + let l = stores.length; + let pending = 1 << l; + const values = new Array(l); + const unsubs = stores.map((store, i) => + subscribe( + store, + (v) => void ((values[i] = v), !(pending &= ~(1 << i)) && this.set(values)), + () => void (pending |= 1 << i) + ) + ); + if (!(pending &= ~(1 << l))) this.set(values); + return () => { + while (l--) unsubs[l](); + this.cleanup(); + }; + } + : (_set) => ((unsub) => void (unsub(), this.cleanup())).bind(this, subscribe(stores, this.set)) + ); + this.target = stores; + this.set = + // deriver defines < 2 arguments ? + deriver.length < 2 + ? // return value is store value + (v) => void super.set(deriver(v) as T) + : // return value is cleanup | void, store value is set manually + (v) => + void (this.cleanup(), + typeof (this.cleanup = deriver(v, super.set.bind(this)) as () => void) !== 'function' && + (this.cleanup = noop)); + } +} +export type initCreateMotionTick = (set: (value: T) => void) => createMotionTick; +export type createMotionTick = (prev_value: T, next_value: T) => SpringTick; +export type SpringTick = (current_value: T, elapsed: number, dt: number) => boolean; +export type TweenTick = (t: number) => boolean; +/** applies motion fn to every leaf of any object */ +function parseStructure(obj: T, schema: initCreateMotionTick): initCreateMotionTick { + const isArray = Array.isArray(obj); + if (typeof obj === 'object' && obj !== null && (isArray || Object.prototype === Object.getPrototypeOf(obj))) { + const keys = Object.keys(obj); + let i = 0, + l = keys.length, + k = '', + createTickers = keys.map((key) => parseStructure(obj[key], schema)((next_value) => (obj[key] = next_value))), + tickers = new Array(l), + pending = 0; + const target = { ...obj }; + //@ts-ignore + 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]; + for (i = 0; i < l; i++) (pending |= 1 << i), (tickers[i] = createTickers[i](obj[keys[i]], target[keys[i]])); + return (_current, elapsed, dt) => { + for (i = 0; i < l; i++) if (pending & (1 << i) && !tickers[i](obj[keys[i]], elapsed, dt)) pending &= ~(1 << i); + //@ts-ignore + set(isArray ? [...obj] : { ...obj }); + return !!pending; + }; + }; + } + return schema; +} +abstract class MotionStore extends Store { + running = false; + cancel = noop; + initCreateTicker: initCreateMotionTick; + createTicker: createMotionTick; + tick; + constructor(value: T, startSetTick: initCreateMotionTick) { + super(value); + this.createTicker = parseStructure(value, (this.initCreateTicker = startSetTick))(super.set.bind(this)); + } + set(next_value: T) { + const this_id = ++this.uidRunning; + this.clearStateSubscribers(false); + //@ts-ignore + if (!this.value && this.value !== 0) { + this.setImmediate(next_value); + } else { + this.tick = this.createTicker(this.value, next_value); + this.loop(() => this.clearStateSubscribers(true)); + this.running = true; + } + return { + then: (resolve, reject) => { + const stop = (has_ended) => (this.uidRunning === this_id ? resolve : reject)(has_ended); + if (!this.running || this_id !== this.uidRunning) stop(true); + else this.onCompletionSubscribers.push(stop); + }, + }; + } + abstract loop(stop): void; + setImmediate(value) { + this.createTicker = parseStructure(value, this.initCreateTicker)(super.set.bind(this)); + super.set((this.value = value)); + if (this.running) this.cancel(); + this.running = false; + } + onCompletionSubscribers = []; + onRestSubscribers = []; + uidRunning = 0; + onRest(callback: Subscriber) { + this.onRestSubscribers.push(callback); + return () => { + const index = this.onRestSubscribers.indexOf(callback); + if (~index) this.onRestSubscribers[index] = noop; + }; + } + clearStateSubscribers(has_ended: boolean) { + let i = 0, + l = this.onRestSubscribers.length; + if (has_ended) { + this.running = false; + if (l) + for (; i < this.onRestSubscribers.length; i++) { + this.onRestSubscribers[i](); + if (this.onRestSubscribers[i] === noop) this.onRestSubscribers.splice(i--, 1); + } + } + for (i = 0, l = this.onCompletionSubscribers.length; i < l; i++) this.onCompletionSubscribers[i](has_ended); + this.onCompletionSubscribers.length = 0; + } +} +export class SpringMotion extends MotionStore { + elapsed = 0.0; + tick: SpringTick; + loop(stop) { + this.elapsed = 0.0; + if (!this.running) this.cancel = onEachFrame((dt) => this.tick(this.value, (this.elapsed += dt), dt), stop); + } +} +export class TweenMotion extends MotionStore { + tick: TweenTick; + loop(stop) { + if (this.running) this.cancel(); + this.cancel = loop((t) => this.tick(t) || (stop(), false)); } } diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index a0dd6379b5..0051ba6618 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -26,7 +26,7 @@ interface ExtendedDoc extends Document { const active_documents = new Set(); let running_animations = 0; -function add_rule(node, name, rule): [CSSStyleSheet, Set] { +function add_rule(node): [CSSStyleSheet, Set] { const { ownerDocument } = node; if (!active_documents.has(ownerDocument)) { active_documents.add(ownerDocument); @@ -52,7 +52,7 @@ const gen = (t, step, css) => { }; export function animate_css(css: (t: number) => string, node: HTMLElement, duration: number, t = 0) { const [name, rule] = gen(t, duration / (FRAME_RATE || calc_framerate()), css); - const [stylesheet, rules] = add_rule(node, rule); + const [stylesheet, rules] = add_rule(node); if (!rules.has(name)) { rules.add(name); stylesheet.insertRule(rule, stylesheet.cssRules.length); diff --git a/src/runtime/motion/_tweened.ts b/src/runtime/motion/_tweened.ts deleted file mode 100644 index 3c98cd1dde..0000000000 --- a/src/runtime/motion/_tweened.ts +++ /dev/null @@ -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 { -// delay?: number; -// duration?: number | ((from: T, to: T) => number); -// easing?: (t: number) => number; -// interpolate?: (a: T, b: T) => (t: number) => T; -// } - -// type Updater = (target_value: T, value: T) => T; - -// interface Tweened extends Readable { -// set(value: T, opts: Options): Promise; - -// update(updater: Updater, opts: Options): Promise; -// } - -// export function tweened(value?: T, defaults: Options = {}): Tweened { -// const store = writable(value); - -// let task: Task; -// let target_value = value; - -// function set(new_value: T, opts: Options) { -// 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) => set(fn(target_value, value), opts), -// subscribe: store.subscribe -// }; -// } diff --git a/src/runtime/motion/spring.ts b/src/runtime/motion/spring.ts index 81d7f41c8b..366bcba2ef 100644 --- a/src/runtime/motion/spring.ts +++ b/src/runtime/motion/spring.ts @@ -1,4 +1,5 @@ -import { Store, onEachFrame } from 'svelte/internal'; +import { SpringMotion, TweenMotion, now } from 'svelte/internal'; +import { is_date } from './utils'; function solve_spring( prev_value: number, @@ -20,62 +21,91 @@ function solve_spring( return (t: number) => target_value - (Math.cos((f = t * dfm)) * delta + Math.sin(f) * leftover) * Math.exp(t * dm); } } -class MotionStore extends Store { - 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))); - } +export function spring(value, { mass = 1.0, damping = 10.0, stiffness = 100.0, precision = 0.001, soft = false } = {}) { + const store = new SpringMotion(value, (set) => { + let velocity = 0.0, + calc; + return (from_value, to_value) => { + calc = solve_spring(from_value, velocity, to_value, obj); + 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); + }; + }); + const obj = { + mass, + damping, + stiffness, + precision, + soft, + set(next_value, params) { + if (params) { + if ('mass' in params) obj.mass = params.mass; + if ('damping' in params) obj.damping = params.damping; + if ('stiffness' in params) obj.stiffness = params.stiffness; + if ('precision' in params) obj.precision = params.precision; + if ('soft' in params) obj.soft = params.soft; + } + return store.set(next_value); + }, + setImmediate: store.setImmediate.bind(store), + subscribe: store.subscribe.bind(store), + onRest: store.onRest.bind(store), + }; + return obj; } -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); - }; - }) - ); +function tween_between(a, b) { + if (a === b || a !== a) return () => a; + else if (typeof a === 'number') { + return (t) => a + t * (b-a); + } else if (is_date(a) && is_date(b)) { + a = a.getTime(); + b = b.getTime(); + const delta = b - a; + return (t) => new Date(a + t * delta); + } else throw new Error(`Cannot interpolate ${typeof a} values`); } -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; +export function tween( + value, + { + delay: default_delay = 0, + duration: default_duration = 400, + easing: default_easing = (v) => v, + interpolate: default_interpolate = tween_between, + } +) { + let delay = default_delay, + duration = default_duration, + easing = default_easing, + interpolate = default_interpolate; + const store = new TweenMotion(value, (set) => { + let end_time = 0, + calc; + return (from_value, to_value) => { + end_time = now() + delay + duration; + calc = interpolate(from_value, to_value); + return (t) => { + t = 1 - (end_time - t) / duration; + if (t >= 1) return set(calc(easing(1))), false; + if (t >= 0) set(calc(easing(t))); + return true; }; }; + }); + function set(next_value, params) { + delay = (params && params.delay) || default_delay; + duration = (params && 'duration' in params && params.duration) || default_duration; + easing = (params && params.easing) || default_easing; + interpolate = (params && params.interpolate) || default_interpolate; + return store.set(next_value); } - return schema; + return { + set, + // update: (fn, params) => set(fn(target_value, value), params), + setImmediate: store.setImmediate.bind(store), + subscribe: store.subscribe.bind(store), + onRest: store.onRest.bind(store), + }; } diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts index 163f16a6b5..98381ebcd5 100644 --- a/src/runtime/store/index.ts +++ b/src/runtime/store/index.ts @@ -1,4 +1,4 @@ -import { subscribe, noop, get_store_value, Writable, StartStopNotifier } from 'svelte/internal'; +import { get_store_value, Writable, StartStopNotifier, Derived } from 'svelte/internal'; export { get_store_value as get }; export function readable(value: T, start: StartStopNotifier) { const store = new Writable(value, start); @@ -8,38 +8,7 @@ export function writable(value: T, start: StartStopNotifier) { const store = new Writable(value, start); return { set: store.set.bind(store), update: store.update.bind(store), subscribe: store.subscribe.bind(store) }; } -export function derived(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()); - } - ); +export function derived(stores, deriver, initial_value?) { + const store = new Derived(stores, deriver, initial_value); + return { subscribe: store.subscribe.bind(store) }; } diff --git a/src/runtime/transition/index.ts b/src/runtime/transition/index.ts index e39de6b7d9..837ae16e51 100644 --- a/src/runtime/transition/index.ts +++ b/src/runtime/transition/index.ts @@ -1,19 +1,20 @@ import { cubicOut, cubicInOut, linear } from 'svelte/easing'; type EasingFunction = (t: number) => number; - -export interface TransitionConfig { +interface BasicConfig { delay?: number; duration?: number; easing?: EasingFunction; +} +interface TimeableConfig extends Omit { + duration?: number | ((len: number) => number); +} +export interface TransitionConfig extends BasicConfig { css?: (t: number, u?: number) => string; tick?: (t: number, u?: number) => void; } -interface BlurParams { - delay: number; - duration: number; - easing?: EasingFunction; +interface BlurParams extends BasicConfig { amount: number; opacity: number; } @@ -34,13 +35,7 @@ export function blur( }; } -interface FadeParams { - delay: number; - duration: number; - easing: EasingFunction; -} - -export function fade(node: Element, { delay = 0, duration = 400, easing = linear }: FadeParams): TransitionConfig { +export function fade(node: Element, { delay = 0, duration = 400, easing = linear }: BasicConfig): TransitionConfig { const o = +getComputedStyle(node).opacity; return { delay, @@ -50,10 +45,7 @@ export function fade(node: Element, { delay = 0, duration = 400, easing = linear }; } -interface FlyParams { - delay: number; - duration: number; - easing: EasingFunction; +interface FlyParams extends BasicConfig { x: number; y: number; opacity: number; @@ -79,13 +71,7 @@ export function fly( }; } -interface SlideParams { - delay: number; - duration: number; - easing: EasingFunction; -} - -export function slide(node: Element, { delay = 0, duration = 400, easing = cubicOut }: SlideParams): TransitionConfig { +export function slide(node: Element, { delay = 0, duration = 400, easing = cubicOut }: BasicConfig): TransitionConfig { const style = getComputedStyle(node); const opacity = +style.opacity; const height = parseFloat(style.height); @@ -112,10 +98,7 @@ export function slide(node: Element, { delay = 0, duration = 400, easing = cubic }; } -interface ScaleParams { - delay: number; - duration: number; - easing: EasingFunction; +interface ScaleParams extends BasicConfig { start: number; opacity: number; } @@ -140,11 +123,8 @@ export function scale( }; } -interface DrawParams { - delay: number; +interface DrawParams extends TimeableConfig { speed: number; - duration: number | ((len: number) => number); - easing: EasingFunction; } export function draw( @@ -156,18 +136,10 @@ export function draw( else if (typeof duration === 'function') duration = duration(len); return { delay, duration, easing, css: (t, u) => `stroke-dasharray: ${t * len} ${u * len};` }; } - -interface CrossfadeParams { - delay: number; - duration: number | ((len: number) => number); - easing: EasingFunction; -} -interface CrossFadeConfig extends CrossfadeParams { - fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig; -} -interface MarkedCrossFadeConfig extends CrossfadeParams { - key: any; +interface CrossFadeConfig extends TimeableConfig { + fallback: (node: Element, params: TimeableConfig, intro: boolean) => TransitionConfig; } +type MarkedCrossFadeConfig = TimeableConfig & { key: any }; type ElementMap = Map; export function crossfade({ @@ -182,7 +154,7 @@ export function crossfade({ function crossfade( from_node: Element, to_node: Element, - { delay = default_delay, easing = default_easing, duration = default_duration }: CrossfadeParams + { delay = default_delay, easing = default_easing, duration = default_duration }: TimeableConfig ) { const from = from_node.getBoundingClientRect(); const to = to_node.getBoundingClientRect();