diff --git a/interpolate/index.d.ts b/interpolate/index.d.ts new file mode 100644 index 0000000000..e42f0252dd --- /dev/null +++ b/interpolate/index.d.ts @@ -0,0 +1 @@ +export * from '../types/runtime/interpolate/index'; \ No newline at end of file diff --git a/interpolate/index.js b/interpolate/index.js new file mode 100644 index 0000000000..2a24d5d478 --- /dev/null +++ b/interpolate/index.js @@ -0,0 +1,16 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function dates(a, b) { + const difference = (b = new Date(b).getTime()) - (a = new Date(a).getTime()); + const d = new Date(a); + return (t) => (d.setTime(a + difference * t), d); +} +function numbers(a, b) { + const d = (b = +b) - (a = +a); + return (t) => a + d * t; +} + +exports.dates = dates; +exports.numbers = numbers; diff --git a/interpolate/index.mjs b/interpolate/index.mjs new file mode 100644 index 0000000000..66327570c6 --- /dev/null +++ b/interpolate/index.mjs @@ -0,0 +1,11 @@ +function dates(a, b) { + const difference = (b = new Date(b).getTime()) - (a = new Date(a).getTime()); + const d = new Date(a); + return (t) => (d.setTime(a + difference * t), d); +} +function numbers(a, b) { + const d = (b = +b) - (a = +a); + return (t) => a + d * t; +} + +export { dates, numbers }; diff --git a/interpolate/package.json b/interpolate/package.json new file mode 100644 index 0000000000..e106ca54ab --- /dev/null +++ b/interpolate/package.json @@ -0,0 +1,5 @@ +{ + "main": "./index", + "module": "./index.mjs", + "types": "./index.d.ts" + } \ No newline at end of file diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 43a0f754f9..898f595b17 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -29,7 +29,7 @@ export class ElseBlockWrapper extends Wrapper { this.block = block.child({ comment: create_debugging_comment(node, this.renderer.component), name: this.renderer.component.get_unique_name(`create_else_block`), - type: 'else' + type: 'else', }); this.fragment = new FragmentWrapper( @@ -58,7 +58,7 @@ export default class EachBlockWrapper extends Wrapper { fixed_length: number; data_length: string; view_length: string; - } + }; context_props: Array; index_name: Identifier; @@ -82,7 +82,7 @@ export default class EachBlockWrapper extends Wrapper { const { dependencies } = node.expression; block.add_dependencies(dependencies); - this.node.contexts.forEach(context => { + this.node.contexts.forEach((context) => { renderer.add_to_context(context.key.name, true); }); @@ -93,7 +93,7 @@ export default class EachBlockWrapper extends Wrapper { // @ts-ignore todo: probably error key: node.key as string, - bindings: new Map(block.bindings) + bindings: new Map(block.bindings), }); // TODO this seems messy @@ -105,7 +105,7 @@ export default class EachBlockWrapper extends Wrapper { const fixed_length = node.expression.node.type === 'ArrayExpression' && - node.expression.node.elements.every(element => element.type !== 'SpreadElement') + node.expression.node.elements.every((element) => element.type !== 'SpreadElement') ? node.expression.node.elements.length : null; @@ -118,7 +118,7 @@ export default class EachBlockWrapper extends Wrapper { const length = { type: 'Identifier', name: 'length', - loc: { start, end } + loc: { start, end }, }; const each_block_value = renderer.component.get_unique_name(`${this.var.name}_value`); @@ -136,23 +136,22 @@ export default class EachBlockWrapper extends Wrapper { // optimisation for array literal fixed_length, data_length: fixed_length === null ? x`${each_block_value}.${length}` : fixed_length, - view_length: fixed_length === null ? x`${iterations}.length` : fixed_length + view_length: fixed_length === null ? x`${iterations}.length` : fixed_length, }; const store = - node.expression.node.type === 'Identifier' && - node.expression.node.name[0] === '$' + node.expression.node.type === 'Identifier' && node.expression.node.name[0] === '$' ? node.expression.node.name.slice(1) : null; - node.contexts.forEach(prop => { + node.contexts.forEach((prop) => { this.block.bindings.set(prop.key.name, { object: this.vars.each_block_value, property: this.index_name, modifier: prop.modifier, snippet: prop.modifier(x`${this.vars.each_block_value}[${this.index_name}]` as Node), store, - tail: prop.modifier(x`[${this.index_name}]` as Node) + tail: prop.modifier(x`[${this.index_name}]` as Node), }); }); @@ -165,14 +164,7 @@ export default class EachBlockWrapper extends Wrapper { this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, strip_whitespace, next_sibling); if (this.node.else) { - this.else = new ElseBlockWrapper( - renderer, - block, - this, - this.node.else, - strip_whitespace, - next_sibling - ); + this.else = new ElseBlockWrapper(renderer, block, this, this.node.else, strip_whitespace, next_sibling); renderer.blocks.push(this.else.block); @@ -194,14 +186,18 @@ export default class EachBlockWrapper extends Wrapper { const { renderer } = this; const { component } = renderer; - const needs_anchor = this.next - ? !this.next.is_dom_node() : - !parent_node || !this.parent.is_dom_node(); + const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node(); - this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`); + this.context_props = this.node.contexts.map( + (prop) => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};` + ); - if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`); - if (this.node.has_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`); + if (this.node.has_binding) + this.context_props.push( + b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;` + ); + if (this.node.has_binding || this.node.index) + this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`); const snippet = this.node.expression.manipulate(block); @@ -224,7 +220,7 @@ export default class EachBlockWrapper extends Wrapper { const update_anchor_node = needs_anchor ? block.get_unique_name(`${this.var.name}_anchor`) : (this.next && this.next.var) || { type: 'Identifier', name: 'null' }; - const update_mount_node: Identifier = this.get_update_mount_node((update_anchor_node as Identifier)); + const update_mount_node: Identifier = this.get_update_mount_node(update_anchor_node as Identifier); const args = { block, @@ -234,7 +230,7 @@ export default class EachBlockWrapper extends Wrapper { initial_anchor_node, initial_mount_node, update_anchor_node, - update_mount_node + update_mount_node, }; const all_dependencies = new Set(this.block.dependencies); // TODO should be dynamic deps only @@ -258,12 +254,7 @@ export default class EachBlockWrapper extends Wrapper { } if (needs_anchor) { - block.add_element( - update_anchor_node as Identifier, - x`@empty()`, - parent_nodes && x`@empty()`, - parent_node - ); + block.add_element(update_anchor_node as Identifier, x`@empty()`, parent_nodes && x`@empty()`, parent_node); } if (this.else) { @@ -354,7 +345,7 @@ export default class EachBlockWrapper extends Wrapper { initial_anchor_node, initial_mount_node, update_anchor_node, - update_mount_node + update_mount_node, }: { block: Block; parent_node: Identifier; @@ -365,12 +356,7 @@ export default class EachBlockWrapper extends Wrapper { update_anchor_node: Identifier; update_mount_node: Identifier; }) { - const { - create_each_block, - iterations, - data_length, - view_length - } = this.vars; + const { create_each_block, iterations, data_length, view_length } = this.vars; const get_key = block.get_unique_name('get_key'); const lookup = block.get_unique_name(`${this.var.name}_lookup`); @@ -382,18 +368,16 @@ export default class EachBlockWrapper extends Wrapper { this.block.first = this.fragment.nodes[0].var; } else { this.block.first = this.block.get_unique_name('first'); - this.block.add_element( - this.block.first, - x`@empty()`, - parent_nodes && x`@empty()`, - null - ); + this.block.add_element(this.block.first, x`@empty()`, parent_nodes && x`@empty()`, null); } block.chunks.init.push(b` const ${get_key} = #ctx => ${this.node.key.manipulate(block)}; - ${this.renderer.options.dev && b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`} + ${ + this.renderer.options.dev && + b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});` + } for (let #i = 0; #i < ${data_length}; #i += 1) { let child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i); let key = ${get_key}(child_ctx); @@ -424,12 +408,12 @@ export default class EachBlockWrapper extends Wrapper { const dynamic = this.block.has_update_method; const destroy = this.node.has_animation - ? (this.block.has_outros + ? this.block.has_outros ? `@fix_and_outro_and_destroy_block` - : `@fix_and_destroy_block`) + : `@fix_and_destroy_block` : this.block.has_outros - ? `@outro_and_destroy_block` - : `@destroy_block`; + ? `@outro_and_destroy_block` + : `@destroy_block`; if (this.dependencies.size) { this.updates.push(b` @@ -437,10 +421,17 @@ export default class EachBlockWrapper extends Wrapper { ${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`} ${this.block.has_outros && b`@group_outros();`} - ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`} - ${this.renderer.options.dev && b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`} - ${iterations} = @update_keyed_each(${iterations}, #dirty, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context}); - ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`} + ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1){ ${iterations}[#i].r();}`} + ${ + this.renderer.options.dev && + b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});` + } + ${iterations} = @update_keyed_each(${iterations}, #dirty, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${ + this.vars.each_block_value + }, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${ + this.vars.get_each_context + }); + ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1){ ${iterations}[#i].a();}`} ${this.block.has_outros && b`@check_outros();`} `); } @@ -467,7 +458,7 @@ export default class EachBlockWrapper extends Wrapper { initial_anchor_node, initial_mount_node, update_anchor_node, - update_mount_node + update_mount_node, }: { block: Block; parent_nodes: Identifier; @@ -477,17 +468,10 @@ export default class EachBlockWrapper extends Wrapper { update_anchor_node: Identifier; update_mount_node: Identifier; }) { - const { - create_each_block, - iterations, - fixed_length, - data_length, - view_length - } = this.vars; + const { create_each_block, iterations, fixed_length, data_length, view_length } = this.vars; block.chunks.init.push(b` let ${iterations} = []; - for (let #i = 0; #i < ${data_length}; #i += 1) { ${iterations}[#i] = ${create_each_block}(${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i)); } @@ -529,7 +513,7 @@ export default class EachBlockWrapper extends Wrapper { } ` : has_transitions - ? b` + ? b` if (${iterations}[#i]) { @transition_in(${this.vars.iterations}[#i], 1); } else { @@ -539,7 +523,7 @@ export default class EachBlockWrapper extends Wrapper { ${iterations}[#i].m(${update_mount_node}, ${update_anchor_node}); } ` - : b` + : b` if (!${iterations}[#i]) { ${iterations}[#i] = ${create_each_block}(child_ctx); ${iterations}[#i].c(); @@ -568,7 +552,9 @@ export default class EachBlockWrapper extends Wrapper { `; } else { remove_old_blocks = b` - for (${this.block.has_update_method ? null : x`#i = ${data_length}`}; #i < ${this.block.has_update_method ? view_length : '#old_length'}; #i += 1) { + for (${this.block.has_update_method ? null : x`#i = ${data_length}`}; #i < ${ + this.block.has_update_method ? view_length : '#old_length' + }; #i += 1) { ${iterations}[#i].d(1); } ${!fixed_length && b`${view_length} = ${data_length};`} diff --git a/src/runtime/animate/index.ts b/src/runtime/animate/index.ts index 17ac9e9458..73f2c4ea8e 100644 --- a/src/runtime/animate/index.ts +++ b/src/runtime/animate/index.ts @@ -1,6 +1,5 @@ import { cubicOut } from 'svelte/easing'; -import { AnimationConfig } from 'svelte/internal'; - +import { AnimationConfig, run_duration } from 'svelte/internal'; interface FlipParams { delay: number; @@ -23,7 +22,7 @@ export function flip( return { delay, - duration: typeof duration === 'function' ? duration(Math.sqrt(dx * dx + dy * dy)) : duration, + duration: run_duration(duration, Math.sqrt(dx * dx + dy * dy)), easing, css: (_t, u) => `transform: ${transform} translate(${u * dx}px, ${u * dy}px);`, }; diff --git a/src/runtime/easing/index.ts b/src/runtime/easing/index.ts index 0da302152c..a550fe3bfd 100644 --- a/src/runtime/easing/index.ts +++ b/src/runtime/easing/index.ts @@ -1,3 +1,5 @@ +import { dev$assert } from 'svelte/internal'; + export { identity as linear } from 'svelte/internal'; export const quadIn = (t: number) => t ** 2; export const quadOut = (t: number) => 1.0 - (1.0 - t) ** 2; @@ -47,9 +49,10 @@ export const circOut = (t: number) => Math.sin(Math.acos(1 - t)); export const circInOut = (t: number) => 0.5 * (t >= 0.5 ? 2.0 - Math.sin(Math.acos(1.0 - 2.0 * (1.0 - t))) : Math.sin(Math.acos(1.0 - 2.0 * t))); export const cubicBezier = (x1: number, y1: number, x2: number, y2: number) => { - if (__DEV__) - if (0 > x1 || x1 < 1 || 0 > x2 || x2 > 1) - throw new Error(`CubicBezier x1 & x2 values must be { 0 < x < 1 }, got { x1 : ${x1}, x2: ${x2} }`); + dev$assert( + x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1, + `CubicBezier x1 & x2 values must be { 0 < x < 1 }, got { x1 : ${x1}, x2: ${x2} }` + ); const ax = 1.0 - (x2 = 3.0 * (x2 - x1) - (x1 = 3.0 * x1)) - x1, ay = 1.0 - (y2 = 3.0 * (y2 - y1) - (y1 = 3.0 * y1)) - y1; let r = 0.0, diff --git a/src/runtime/internal/index.ts b/src/runtime/internal/index.ts index a167420665..309a6e7052 100644 --- a/src/runtime/internal/index.ts +++ b/src/runtime/internal/index.ts @@ -1,5 +1,8 @@ export * from './animations'; export * from './await_block'; +export * from './Component'; +export * from './dev.legacy'; +export * from './dev'; export * from './dom'; export * from './environment'; export * from './globals'; @@ -9,8 +12,7 @@ export * from './loop'; export * from './scheduler'; export * from './spread'; export * from './ssr'; +export * from './stores'; +// export * from './style_manager' export * from './transitions'; export * from './utils'; -export * from './Component'; -export * from './dev'; -export * from './stores' \ No newline at end of file diff --git a/src/runtime/internal/loop.ts b/src/runtime/internal/loop.ts index 9335fcaa68..8650e3720e 100644 --- a/src/runtime/internal/loop.ts +++ b/src/runtime/internal/loop.ts @@ -53,7 +53,7 @@ export const loop = (fn) => { next_frame[next_frame_length++] = (t) => !running || fn(t); return () => void (running = false); }; -export const setAnimationTimeout = (callback: () => void, timestamp: number): TaskCanceller => { +export const setFrameTimeout = (callback: () => void, timestamp: number): TaskCanceller => { const task: TimeoutTask = { callback, timestamp }; if (running_timed) { pending_inserts = !!pending_insert_timed.push(task); @@ -67,16 +67,16 @@ export const setAnimationTimeout = (callback: () => void, timestamp: number): Ta /** * Calls function every frame with a value going from 0 to 1 */ -export const useTween = ( - run: (now: number) => void, +export const setTweenTimeout = ( stop: (now: number) => void, end_time: number, + run: (now: number) => void, duration = end_time - now() ): TaskCanceller => { let running = true; unsafe_loop((t) => { if (!running) return false; - t = (end_time - t) / duration; + t = 1 - (end_time - t) / duration; if (t >= 1) return run(1), stop(t), false; if (t >= 0) run(t); return running; @@ -102,6 +102,7 @@ export const onEachFrame = ( }); return () => void (running = false); }; -// tests + +/** tests only */ export const clear_loops = () => 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/stores.ts b/src/runtime/internal/stores.ts index f1981baa3c..4bb41dc5c5 100644 --- a/src/runtime/internal/stores.ts +++ b/src/runtime/internal/stores.ts @@ -1,6 +1,5 @@ import { safe_not_equal, noop, subscribe } from './utils'; -import { onEachFrame, useTween, loop } from './loop'; -import { now } from './environment'; +import { onEachFrame, loop } from './loop'; type Setter = (value: T) => void; type StopCallback = () => void; export type StartStopNotifier = (set: Setter) => StopCallback | void; @@ -140,11 +139,16 @@ export class Derived, T> extends St } } export type initCreateMotionTick = (set: (value: T) => void) => createMotionTick; +export type initCreateTweenTick = (set: (value: T) => void) => createTweenTick; export type createMotionTick = (prev_value: T, next_value: T) => SpringTick; +export type createTweenTick = (prev_value: T, next_value: T) => TweenTick; 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 { +function parseStructure( + obj: unknown, + schema: initCreateMotionTick | initCreateTweenTick +): initCreateMotionTick | initCreateTweenTick { const isArray = Array.isArray(obj); if (typeof obj === 'object' && obj !== null && (isArray || Object.prototype === Object.getPrototypeOf(obj))) { const keys = Object.keys(obj); @@ -155,15 +159,13 @@ function parseStructure(obj: T, schema: initCreateMotionTick): initCreateM tickers = new Array(l), pending = 0; const target = { ...obj }; - //@ts-ignore - obj = isArray ? [...obj] : { ...obj }; + obj = isArray ? [...(obj as T[])] : { ...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 }); + set(isArray ? [...(obj as T[])] : { ...(obj as any) }); return !!pending; }; }; @@ -173,18 +175,17 @@ function parseStructure(obj: T, schema: initCreateMotionTick): initCreateM abstract class MotionStore extends Store { running = false; cancel = noop; - initCreateTicker: initCreateMotionTick; - createTicker: createMotionTick; + initCreateTicker; + createTicker; tick; - constructor(value: T, startSetTick: initCreateMotionTick) { + constructor(value: T, startSetTick: initCreateMotionTick | initCreateTweenTick) { 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) { + if (!this.value && (this.value as unknown) !== 0) { this.setImmediate(next_value); } else { this.tick = this.createTicker(this.value, next_value); @@ -232,14 +233,18 @@ abstract class MotionStore extends Store { } } export class SpringMotion extends MotionStore { - elapsed = 0.0; + initCreateTicker: initCreateMotionTick; + createTicker: createMotionTick; tick: SpringTick; + elapsed = 0.0; 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 { + initCreateTicker: initCreateTweenTick; + createTicker: createTweenTick; tick: TweenTick; loop(stop) { if (this.running) this.cancel(); diff --git a/src/runtime/internal/transitions.ts b/src/runtime/internal/transitions.ts index 8f476be7d6..2a4502187e 100644 --- a/src/runtime/internal/transitions.ts +++ b/src/runtime/internal/transitions.ts @@ -1,11 +1,13 @@ -import { run_all } from './utils'; -import { now } from './environment'; -import { setAnimationTimeout, useTween } from './loop'; -import { animate_css } from './style_manager'; -import { custom_event } from './dom'; import { TransitionConfig } from '../transition'; -import { add_measure_callback, add_render_callback } from './scheduler'; import { Fragment } from './Component'; +import { custom_event } from './dom'; +import { now } from './environment'; +import { setFrameTimeout, setTweenTimeout } from './loop'; +import { add_measure_callback } from './scheduler'; +import { animate_css } from './style_manager'; + +type TransitionFn = (node: HTMLElement, params: any) => TransitionConfig; +type StopResetReverse = (reset_reverse?: 1 | -1) => StopResetReverse; function startStopDispatcher(node: Element, is_intro: boolean) { node.dispatchEvent(custom_event(`${is_intro ? 'intro' : 'outro'}start`)); @@ -14,92 +16,132 @@ function startStopDispatcher(node: Element, is_intro: boolean) { const outroing = new Set(); -let outros; -export function group_outros() { - outros = { - /* parent group */ p: outros, - /* callbacks */ c: [], - /* remaining outros */ r: 0, - }; -} +let transition_group; +export const group_outros = () => void (transition_group = { p: transition_group, c: [], r: 0 }); -export function check_outros() { - if (!outros.r) run_all(outros.c); - outros = outros.p; -} +export const check_outros = () => { + if (!transition_group.r) for (let i = 0; i < transition_group.c.length; i++) transition_group.c[i](); + transition_group = transition_group.p; +}; -export function transition_in(block: Fragment, local?: 0 | 1) { +export const transition_in = (block: Fragment, local?: 0 | 1) => { if (!block || !block.i) return; outroing.delete(block); block.i(local); -} +}; -export function transition_out(block: Fragment, local?: 0 | 1, detach?: 0 | 1, callback?: () => void) { +export const transition_out = (block: Fragment, local?: 0 | 1, detach?: 0 | 1, callback?: () => void) => { if (!block || !block.o || outroing.has(block)) return; outroing.add(block); - outros.c.push(() => { + transition_group.c.push(() => { if (!outroing.has(block)) return; - outroing.delete(block); + else outroing.delete(block); if (!callback) return; if (detach) block.d(1); callback(); }); block.o(local); -} +}; -const eased = (fn: (t: number) => any, easing: (t: number) => number, start, end) => (t: number) => - fn(start + (end - start) * easing(t)); -//easing ? (!is_intro ? (t: number) => fn(easing(t)) : (t: number) => fn(1 - easing(1 - t))) : fn; -const runner = (fn: (t0: number, t1: number) => any, is_intro: boolean) => - is_intro ? (t: number) => fn(t, 1 - t) : (t: number) => fn(1 - t, t); -const mirror = (fn, easing, is_intro) => { - const run = is_intro ? (t) => fn(1 - t, t) : (t) => fn(t, 1 - t); - return easing ? (is_intro ? (t) => run(1 - easing(1 - t)) : (t) => run(easing(t))) : run; +/* todo: deprecate */ +const swap = (fn, is_intro) => (is_intro ? (t) => fn(t, 1 - t) : (t) => fn(1 - t, t)); + +const mirrored = (fn, is_intro, easing) => { + const run = swap(fn, is_intro); + return easing ? (!is_intro ? (t) => run(1 - easing(1 - t)) : (t) => run(easing(t))) : run; }; -type TransitionFn = (node: HTMLElement, params: any) => TransitionConfig; -export function run_transition( + +const reversed = (fn, is_intro, easing, start = 0, end = 1) => { + const run = swap(fn, is_intro); + const difference = end - start; + return easing ? (t) => run(start + difference * easing(t)) : (t) => run(start + difference * t); +}; + +export const run_transition = ( node: HTMLElement, fn: TransitionFn, is_intro = true, params = {}, - left_duration = 0, - prev_left = 0 -): StopResetReverse { + /* internal to this file */ + is_bidirectional = false, + elapsed_duration = 0, + delay_left = -1, + elapsed_ratio = 0 +) => { let config; let running = true; let cancel_css; let cancel_raf; let dispatch_end; - let end_time; - let t; - let start_ratio; - const group = outros; - if (!is_intro) group.r++; + let start_time = 0; + let end_time = 0; - const start = ({ delay = 0, duration = 300, easing, tick, css }: TransitionConfig) => { + const group = transition_group; + if (!is_intro) transition_group.r++; + + const start = ({ delay = 0, duration = 300, easing, tick, css, strategy = 'reverse' }: TransitionConfig) => { if (!running) return; - t = duration - (left_duration > 0 ? left_duration : 0); - end_time = now() + t; - start_ratio = 1 - easing((t - prev_left) / duration); - if (css) cancel_css = animate_css(eased(runner(css, is_intro), easing, start_ratio, 1), node, t, 0); + + if (~delay_left) delay = delay_left; + + const solver = strategy === 'reverse' ? reversed : mirrored; + const runner = (fn) => solver(fn, is_intro, easing, elapsed_ratio, 1); + + if (solver === mirrored) { + delay -= elapsed_duration; + } else if (solver === reversed) { + duration -= elapsed_duration; + } + + end_time = (start_time = now() + delay) + duration; + dispatch_end = startStopDispatcher(node, is_intro); - cancel_raf = tick - ? useTween(eased(runner(tick, is_intro), easing, start_ratio, 1), stop, end_time, t) - : setAnimationTimeout(stop, end_time); + + if (css) cancel_css = animate_css(runner(css), node, duration, delay); + cancel_raf = tick ? setTweenTimeout(stop, end_time, runner(tick), duration) : setFrameTimeout(stop, end_time); }; - const stop = (end_reset_reverse?: number | 1 | -1) => { + const stop: StopResetReverse = (t?: number | 1 | -1) => { if (!running) return; else running = false; + if (cancel_css) cancel_css(); if (cancel_raf) cancel_raf(); - if (t > end_time && dispatch_end) dispatch_end(); - if (!is_intro && !--group.r) for (let i = 0; i < group.c.length; i++) group.c[i](); - if (!~end_reset_reverse) - return run_transition(node, () => config, !is_intro, params, end_time - now(), left_duration); - else if (left_duration) running_bidi.delete(node); + if (t > end_time) { + if (dispatch_end) { + dispatch_end(); + } + } + + if (!is_intro) { + if (!--group.r) { + for (let i = 0; i < group.c.length; i++) { + group.c[i](); + } + } + } + + if (is_bidirectional) { + if (-1 === t) { + return ( + (t = now()) < end_time && + run_transition( + node, + () => config, + !is_intro, + params, + true, + end_time - t, + start_time > t ? start_time - t : 0, + (1 - elapsed_ratio) * (1 - config.easing(1 - (end_time - t) / (end_time - start_time))) + ) + ); + } else { + running_bidi.delete(node); + } + } }; add_measure_callback(() => { @@ -108,12 +150,14 @@ export function run_transition( }); return stop; -} -export type StopResetReverse = (reset_reverse?: 1 | -1) => StopResetReverse; +}; + const running_bidi: Map = new Map(); -export function run_bidirectional_transition(node: HTMLElement, fn: TransitionFn, is_intro: boolean, params: any) { +export const run_bidirectional_transition = (node: HTMLElement, fn: TransitionFn, is_intro: boolean, params: any) => { let cancel; if (running_bidi.has(node) && (cancel = running_bidi.get(node)(-1))) running_bidi.set(node, cancel); - else running_bidi.set(node, (cancel = run_transition(node, fn, is_intro, params, -1))); + else running_bidi.set(node, (cancel = run_transition(node, fn, is_intro, params, true))); return cancel; -} +}; +export const run_duration = (duration, ...args): number => + typeof duration === 'function' ? duration.apply(null, args) : duration; diff --git a/src/runtime/interpolate/index.ts b/src/runtime/interpolate/index.ts new file mode 100644 index 0000000000..668c88d99a --- /dev/null +++ b/src/runtime/interpolate/index.ts @@ -0,0 +1,9 @@ +export function dates(a, b) { + const difference = (b = new Date(b).getTime()) - (a = new Date(a).getTime()); + const d = new Date(a); + return (t) => (d.setTime(a + difference * t), d); +} +export function numbers(a, b) { + const d = (b = +b) - (a = +a); + return (t) => a + d * t; +} diff --git a/src/runtime/motion/index.ts b/src/runtime/motion/index.ts index 8e1cf4aa6f..bf42a8339c 100644 --- a/src/runtime/motion/index.ts +++ b/src/runtime/motion/index.ts @@ -1,2 +1,119 @@ -export * from './spring'; -// export * from './_tweened'; +import { SpringMotion, TweenMotion, now, run_duration } from 'svelte/internal'; +import { numbers } from 'svelte/interpolate'; +interface TweenParams { + delay?: number; + duration?: number | ((from: T, to: T) => number); + easing?: (t: number) => number; + interpolate?: (a: T, b: T) => (t: number) => T; +} +interface SpringParams { + stiffness?: number /* { 10 < 100 < 200 } */; + damping?: number /* { 1 < 10 < 20 } */; + mass?: number /* { 0.1 < 1 < 20 } */; + precision?: number /* = 0.001 */; + soft?: boolean /* disables damping */; +} + +function solve_spring( + prev_value: number, + prev_velocity: number, + target_value: number, + { stiffness, mass, damping, soft }: SpringParams +) { + 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 { + 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); + } +} + +export function spring( + value, + { mass = 1.0, damping = 10.0, stiffness = 100.0, precision = 0.001, soft = false }: SpringParams = {} +) { + 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?: SpringParams) { + 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 tween( + value: T, + { + delay: default_delay = 0, + duration: default_duration = 400, + easing: default_easing = (v) => v, + interpolate: default_interpolate = numbers, + }: TweenParams +) { + let delay = default_delay, + duration = default_duration, + easing = default_easing, + interpolate = default_interpolate; + const store = new TweenMotion(value, (set) => { + let end_time = 0, + this_duration = 0, + calc; + return (from_value: T, to_value: T) => { + end_time = now() + delay + (this_duration = run_duration(duration, from_value, to_value)); + calc = interpolate(from_value, to_value); + return (t) => { + t = 1 - (end_time - t) / this_duration; + if (t >= 1) return set(calc(easing(1))), false; + if (t >= 0) set(calc(easing(t))); + return true; + }; + }; + }); + const set = (next_value, params) => { + delay = (params && params.delay) || default_delay; + duration = +((params && 'duration' in params) || default_duration); + easing = (params && params.easing) || default_easing; + interpolate = (params && params.interpolate) || default_interpolate; + return store.set(next_value); + }; + return { + set, + /* todo: test update() */ + update: (fn, params) => set(fn(store.value, value), params), + setImmediate: store.setImmediate.bind(store), + subscribe: store.subscribe.bind(store), + onRest: store.onRest.bind(store), + }; +} diff --git a/src/runtime/motion/spring.ts b/src/runtime/motion/spring.ts deleted file mode 100644 index 366bcba2ef..0000000000 --- a/src/runtime/motion/spring.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SpringMotion, TweenMotion, now } from 'svelte/internal'; -import { is_date } from './utils'; - -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 { - 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); - } -} -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; -} -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`); -} -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 { - 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/motion/utils.ts b/src/runtime/motion/utils.ts deleted file mode 100644 index 2eab8baa2f..0000000000 --- a/src/runtime/motion/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function is_date(obj: any): obj is Date { - return Object.prototype.toString.call(obj) === '[object Date]'; -} diff --git a/src/runtime/transition/index.ts b/src/runtime/transition/index.ts index 800e1288b2..6d8836bc20 100644 --- a/src/runtime/transition/index.ts +++ b/src/runtime/transition/index.ts @@ -1,10 +1,12 @@ -import { cubicOut, cubicInOut, linear } from 'svelte/easing'; +import { cubicOut, cubicInOut } from 'svelte/easing'; +import { run_duration } from 'svelte/internal'; type EasingFunction = (t: number) => number; interface BasicConfig { delay?: number; duration?: number; easing?: EasingFunction; + strategy?: 'reverse' | 'mirror'; } interface TimeableConfig extends Omit { duration?: number | ((len: number) => number); @@ -31,18 +33,13 @@ export function blur( delay, duration, easing, - css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * amount}px);`, + css: (_t, u) => `opacity: ${target_opacity - od * u}; filter:${f} blur(${u * amount}px);`, }; } -export function fade(node: Element, { delay = 0, duration = 400, easing = linear }: BasicConfig): TransitionConfig { +export function fade(node: Element, { delay = 0, duration = 400, easing }: BasicConfig): TransitionConfig { const o = +getComputedStyle(node).opacity; - return { - delay, - duration, - easing, - css: (t) => `opacity: ${t * o};`, - }; + return { delay, duration, easing, css: (t) => `opacity: ${t * o};` }; } interface FlyParams extends BasicConfig { @@ -57,15 +54,13 @@ export function fly( ): TransitionConfig { const style = getComputedStyle(node); const target_opacity = +style.opacity; - const transform = style.transform === 'none' ? '' : style.transform; + const prev = style.transform === 'none' ? '' : style.transform; const od = target_opacity * (1 - opacity); return { delay, duration, easing, - css: (t, u) => ` - transform: ${transform} translate(${u * x}px, ${u * y}px); - opacity: ${target_opacity - od * u}`, + css: (_t, u) => `transform: ${prev} translate(${u * x}px, ${u * y}px); opacity: ${target_opacity - od * u};`, }; } @@ -114,10 +109,7 @@ export function scale( delay, duration, easing, - css: (_t, u) => ` - transform: ${transform} scale(${1 - sd * u}); - opacity: ${target_opacity - od * u}; - `, + css: (_t, u) => `transform: ${transform} scale(${1 - sd * u}); opacity: ${target_opacity - od * u};`, }; } @@ -131,7 +123,7 @@ export function draw( ): TransitionConfig { const len = node.getTotalLength(); if (duration === undefined) duration = speed ? len / speed : 800; - else if (typeof duration === 'function') duration = duration(len); + else duration = run_duration(duration, len); return { delay, duration, easing, css: (t, u) => `stroke-dasharray: ${t * len} ${u * len};` }; } interface CrossFadeConfig extends TimeableConfig { @@ -165,7 +157,7 @@ export function crossfade({ return { delay, easing, - duration: typeof duration === 'function' ? duration(Math.sqrt(dx * dx + dy * dy)) : duration, + duration: run_duration(duration, Math.sqrt(dx * dx + dy * dy)), css: (t, u) => ` opacity: ${t * +opacity}; transform-origin: top left;