Skip to content

Commit

Permalink
Clanup, fix and test untracking setter
Browse files Browse the repository at this point in the history
  • Loading branch information
thetarnav committed Sep 19, 2024
1 parent 98fed63 commit 31bf2ce
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 78 deletions.
124 changes: 47 additions & 77 deletions packages/spring/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ type Task = {
promise: Promise<void>;
};

type TickContext<T extends SpringTarget> = {
type TickContext = {
inv_mass: number;
dt: number;
opts: SpringOptions & { set: SpringSetter<T> };
settled: boolean;
};

Expand Down Expand Up @@ -111,11 +110,11 @@ export type SpringOptions = {
precision?: number;
};

export type SpringTargetPrimitive = number | Date;
export type SpringTarget =
| SpringTargetPrimitive
| { [key: string]: SpringTargetPrimitive | SpringTarget }
| SpringTargetPrimitive[]
| number
| Date
| { [key: string]: number | Date | SpringTarget }
| (number | Date)[]
| SpringTarget[];

/**
Expand Down Expand Up @@ -152,75 +151,57 @@ export function createSpring<T extends SpringTarget>(
initialValue: T,
options: SpringOptions = {},
): [Accessor<WidenSpringTarget<T>>, SpringSetter<WidenSpringTarget<T>>] {
const [springValue, setSpringValue] = createSignal<T>(initialValue);
const [springValue, setSpringValue] = createSignal(initialValue);
const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = options;

let lastTime = 0;
let last_time = 0;
let task: Task | null = null;
let current_token: object | undefined = undefined;
let lastValue: T = initialValue;
let targetValue: T | undefined;
let current_value = initialValue
let last_value = initialValue;
let target_value = initialValue;
let inv_mass = 1;
let inv_mass_recovery_rate = 0;
let cancelTask = false;
let cancel_task = false;

/**
* Gets `newValue` from the SpringSetter's first argument.
*/
function getNewValue(newValue: T | ((prev: T) => T)) {
if (typeof newValue === "function") {
return newValue(lastValue);
}

return newValue;
}

const set: SpringSetter<T> = untrack(() => (newValue, opts = {}) => {
targetValue = getNewValue(newValue);
const set: SpringSetter<T> = (param, opts = {}) => {
target_value = typeof param === "function" ? param(current_value) : param;

const token = current_token ?? {};
current_token = token;

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (springValue() == null || opts.hard || (stiffness >= 1 && damping >= 1)) {
cancelTask = true;
lastTime = raf.now();
lastValue = getNewValue(newValue);
setSpringValue(_ => getNewValue(newValue));
if (current_value == null || opts.hard || (stiffness >= 1 && damping >= 1)) {
cancel_task = true;
last_time = raf.now();
setSpringValue(_ => current_value = last_value = target_value);
return Promise.resolve();
} else if (opts.soft) {
const rate = opts.soft === true ? 0.5 : +opts.soft;
inv_mass_recovery_rate = 1 / (rate * 60);
inv_mass = 0; // Infinite mass, unaffected by spring forces.
}
if (!task) {
lastTime = raf.now();
cancelTask = false;
last_time = raf.now();
cancel_task = false;

const _loop = loop(now => {
if (cancelTask) {
cancelTask = false;
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> = {
const ctx: TickContext = {
inv_mass: inv_mass,
opts: {
set: set,
damping: damping,
precision: precision,
stiffness: stiffness,
},
settled: true,
dt: ((now - lastTime) * 60) / 1000,
dt: ((now - last_time) * 60) / 1000,
};
const next_value = tick_spring(ctx, lastValue, springValue(), targetValue!);
lastTime = now;
lastValue = springValue();
setSpringValue(_ => next_value);
let new_value = tick_spring(ctx, last_value, current_value, target_value)
last_time = now;
last_value = current_value;
setSpringValue(current_value = new_value);
if (ctx.settled) {
task = null;
}
Expand All @@ -235,53 +216,42 @@ export function createSpring<T extends SpringTarget>(
if (token === current_token) fulfil();
});
});
});
};

const tick_spring = <T extends SpringTarget>(
ctx: TickContext<T>,
const tick_spring = (
ctx: TickContext,
last_value: T,
current_value: T,
target_value: T,
): T => {
): any => {
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 delta = +target_value - +current_value;
const velocity = (+current_value - +last_value) / (ctx.dt || 1 / 60); // guard div by 0
const spring = stiffness * delta;
const damper = 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!) {
if (Math.abs(d) < precision && Math.abs(delta) < 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
ctx.settled = false; // signal loop to keep ticking
return typeof current_value === "number" ? current_value + d : new Date(+current_value + d);
}
if (Array.isArray(current_value)) {
return current_value.map((_, i) =>
// @ts-ignore
// @ts-expect-error
tick_spring(ctx, last_value[i], current_value[i], target_value[i]),
);
} else if (typeof current_value === "object") {
const next_value = {};
}
if (typeof current_value === "object") {
const next_value = {...current_value};
for (const k in current_value) {
// @ts-ignore
next_value[k] = tick_spring(
ctx,
// @ts-ignore
last_value[k],
current_value[k],
target_value[k],
);
// @ts-expect-error
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
}
// @ts-ignore
return next_value;
} else {
throw new Error(`Cannot spring ${typeof current_value} values`);
}
throw new Error(`Cannot spring ${typeof current_value} values`);
};

return [
Expand Down
30 changes: 29 additions & 1 deletion packages/spring/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRoot, createSignal } from "solid-js";
import { createEffect, createRoot, createSignal } from "solid-js";
import { describe, expect, it, vi, afterAll, beforeAll, beforeEach } from "vitest";
import { createDerivedSpring, createSpring } from "../src/index.js";

Expand Down Expand Up @@ -119,6 +119,34 @@ describe("createSpring", () => {
dispose();
});

it("Setter does not subscribe to self", () => {
let runs = 0
const [signal, setSignal] = createSignal(0)

const [setSpring, dispose] = createRoot(dispose => {
const [, setSpring] = createSpring(0)

createEffect(() => {
runs++
setSpring(p => {
signal() // this one should be tracked
return p+1
}, { hard: true })
})

return [setSpring, dispose]
});
expect(runs).toBe(1)

setSpring(p => p+1, { hard: true })
expect(runs).toBe(1)

setSignal(1)
expect(runs).toBe(2)

dispose();
});

it("instantly updates `number` when set with hard using a function as an argument.", () => {
const start = 0;
const end = 50;
Expand Down

0 comments on commit 31bf2ce

Please sign in to comment.