Skip to content

Commit

Permalink
Refacto SpringSequence and add stable
Browse files Browse the repository at this point in the history
  • Loading branch information
etienne-dldc committed Jan 25, 2023
1 parent e013776 commit 28e0752
Show file tree
Hide file tree
Showing 11 changed files with 714 additions and 630 deletions.
142 changes: 76 additions & 66 deletions deno_dist/Spring.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { invariant, normalizeT, makeSpringFn, toPrecision } from './utils.ts';
import { SpringConfig } from './SpringConfig.ts';
import { invariant, normalizeT, makeSpringFn, toPrecision, isStable } from './utils.ts';
import { type ISpringConfig, SpringConfig } from './SpringConfig.ts';

export interface SpringResult {
position: number;
Expand All @@ -10,10 +10,13 @@ export interface SpringFn {
(t: number): SpringResult;
readonly position: (t: number) => number;
readonly velocity: (t: number) => number;
readonly config: Readonly<Partial<SpringConfig>>;
// returns true if the spring is stable at time t
// i.e. position === equilibrium && velocity === 0
readonly stable: (t: number) => boolean;
readonly config: Readonly<Partial<ISpringConfig>>;
}

export function Spring(config: Partial<SpringConfig> = {}): SpringFn {
export function Spring(config: Partial<ISpringConfig> = {}): SpringFn {
const conf = SpringConfig.defaults(config);

invariant(conf.dampingRatio >= 0, 'Damping Ration must be >= 0');
Expand All @@ -25,7 +28,7 @@ export function Spring(config: Partial<SpringConfig> = {}): SpringFn {
if (conf.angularFrequency <= conf.dampingRatioPrecision) {
return springIdentity(conf.position, conf.velocity, config);
}
if (isStable(conf.position, conf.velocity, conf)) {
if (isStable(conf, conf)) {
return springIdentity(conf.position, conf.velocity, config);
}
if (conf.dampingRatio > 1 + conf.dampingRatioPrecision) {
Expand All @@ -41,47 +44,46 @@ export function Spring(config: Partial<SpringConfig> = {}): SpringFn {
return springCriticallyDamped(conf, config);
}

function isStable(position: number, velocity: number, conf: SpringConfig): boolean {
return Math.abs(position - conf.equilibrium) <= conf.positionPrecision && Math.abs(velocity) <= conf.velocityPrecision;
}

function springIdentity(position: number, velocity: number, originalConf: Partial<SpringConfig>): SpringFn {
function springIdentity(position: number, velocity: number, originalConf: Partial<ISpringConfig>): SpringFn {
const identity = { position, velocity };
return makeSpringFn(
originalConf,
() => identity,
() => position,
() => velocity
() => velocity,
() => true
);
}

function springOverDamped(conf: SpringConfig, originalConf: Partial<SpringConfig>): SpringFn {
function springOverDamped(conf: ISpringConfig, originalConf: Partial<ISpringConfig>): SpringFn {
const za = -conf.angularFrequency * conf.dampingRatio;
const zb = conf.angularFrequency * Math.sqrt(conf.dampingRatio * conf.dampingRatio - 1);
const z1 = za - zb;
const z2 = za + zb;
const invTwoZb = 1 / (2 * zb);
const posDiff = conf.position - conf.equilibrium;

const main = (t: number) => {
const [e2, e1_Over_TwoZb, e2_Over_TwoZb, z2e2_Over_TwoZb] = springOverDampedCommon(t, conf.timeScale, conf.timeStart, z1, z2, invTwoZb);
return {
position: springOverDampedPosition(
conf.positionPrecision,
conf.equilibrium,
posDiff,
e1_Over_TwoZb,
z2,
z2e2_Over_TwoZb,
e2,
conf.velocity,
e2_Over_TwoZb
),
velocity: springOverDampedVelocity(conf.velocityPrecision, posDiff, z1, e1_Over_TwoZb, z2e2_Over_TwoZb, e2, z2, conf.velocity),
};
};

return makeSpringFn(
originalConf,
(t: number) => {
const [e2, e1_Over_TwoZb, e2_Over_TwoZb, z2e2_Over_TwoZb] = springOverDampedCommon(t, conf.timeScale, conf.timeStart, z1, z2, invTwoZb);
return {
position: springOverDampedPosition(
conf.positionPrecision,
conf.equilibrium,
posDiff,
e1_Over_TwoZb,
z2,
z2e2_Over_TwoZb,
e2,
conf.velocity,
e2_Over_TwoZb
),
velocity: springOverDampedVelocity(conf.velocityPrecision, posDiff, z1, e1_Over_TwoZb, z2e2_Over_TwoZb, e2, z2, conf.velocity),
};
},
main,
(t) => {
const [e2, e1_Over_TwoZb, e2_Over_TwoZb, z2e2_Over_TwoZb] = springOverDampedCommon(t, conf.timeScale, conf.timeStart, z1, z2, invTwoZb);
return springOverDampedPosition(
Expand All @@ -99,7 +101,8 @@ function springOverDamped(conf: SpringConfig, originalConf: Partial<SpringConfig
(t) => {
const [e2, e1_Over_TwoZb, _e2_Over_TwoZb, z2e2_Over_TwoZb] = springOverDampedCommon(t, conf.timeScale, conf.timeStart, z1, z2, invTwoZb);
return springOverDampedVelocity(conf.velocityPrecision, posDiff, z1, e1_Over_TwoZb, z2e2_Over_TwoZb, e2, z2, conf.velocity);
}
},
(t) => isStable(main(t), conf)
);
}

Expand Down Expand Up @@ -143,38 +146,40 @@ function springOverDampedVelocity(
return toPrecision(posDiff * ((z1e1_Over_TwoZb - z2e2_Over_TwoZb + e2) * z2) + vel * (-z1e1_Over_TwoZb + z2e2_Over_TwoZb), precision);
}

function springUnderDamped(conf: SpringConfig, originalConf: Partial<SpringConfig>): SpringFn {
function springUnderDamped(conf: ISpringConfig, originalConf: Partial<ISpringConfig>): SpringFn {
const omegaZeta = conf.angularFrequency * conf.dampingRatio;
const alpha = conf.angularFrequency * Math.sqrt(1 - conf.dampingRatio * conf.dampingRatio);
const posDiff = conf.position - conf.equilibrium;

const main = (t: number) => {
const [invAlpha, expSin, expCos, expOmegaZetaSin_Over_Alpha] = springUnderDampedCommon(t, conf.timeScale, conf.timeStart, omegaZeta, alpha);
return {
position: springUnderDampedPosition(
conf.positionPrecision,
conf.equilibrium,
posDiff,
expCos,
expOmegaZetaSin_Over_Alpha,
conf.velocity,
expSin,
invAlpha
),
velocity: springUnderDampedVelocity(
conf.velocityPrecision,
posDiff,
expSin,
alpha,
omegaZeta,
expOmegaZetaSin_Over_Alpha,
conf.velocity,
expCos
),
};
};

return makeSpringFn(
originalConf,
(t: number) => {
const [invAlpha, expSin, expCos, expOmegaZetaSin_Over_Alpha] = springUnderDampedCommon(t, conf.timeScale, conf.timeStart, omegaZeta, alpha);
return {
position: springUnderDampedPosition(
conf.positionPrecision,
conf.equilibrium,
posDiff,
expCos,
expOmegaZetaSin_Over_Alpha,
conf.velocity,
expSin,
invAlpha
),
velocity: springUnderDampedVelocity(
conf.velocityPrecision,
posDiff,
expSin,
alpha,
omegaZeta,
expOmegaZetaSin_Over_Alpha,
conf.velocity,
expCos
),
};
},
main,
(t) => {
const [invAlpha, expSin, expCos, expOmegaZetaSin_Over_Alpha] = springUnderDampedCommon(t, conf.timeScale, conf.timeStart, omegaZeta, alpha);
return springUnderDampedPosition(
Expand All @@ -191,7 +196,8 @@ function springUnderDamped(conf: SpringConfig, originalConf: Partial<SpringConfi
(t) => {
const [_invAlpha, expSin, expCos, expOmegaZetaSin_Over_Alpha] = springUnderDampedCommon(t, conf.timeScale, conf.timeStart, omegaZeta, alpha);
return springUnderDampedVelocity(conf.velocityPrecision, posDiff, expSin, alpha, omegaZeta, expOmegaZetaSin_Over_Alpha, conf.velocity, expCos);
}
},
(t) => isStable(main(t), conf)
);
}

Expand Down Expand Up @@ -238,25 +244,29 @@ function springUnderDampedVelocity(
);
}

function springCriticallyDamped(conf: SpringConfig, originalConf: Partial<SpringConfig>): SpringFn {
function springCriticallyDamped(conf: ISpringConfig, originalConf: Partial<ISpringConfig>): SpringFn {
const oldPos = conf.position - conf.equilibrium; // update in equilibrium relative space

const main = (t: number) => {
const [expTerm, timeExp, timeExpFreq] = springCriticallyDampedCommon(t, conf.timeScale, conf.timeStart, conf.angularFrequency);
return {
position: springCriticallyDampedPosition(conf.positionPrecision, oldPos, timeExpFreq, expTerm, conf.velocity, timeExp, conf.equilibrium),
velocity: springCriticallyDampedVelocity(conf.velocityPrecision, oldPos, conf.angularFrequency, timeExpFreq, conf.velocity, expTerm),
};
};

return makeSpringFn(
originalConf,
(t: number) => {
const [expTerm, timeExp, timeExpFreq] = springCriticallyDampedCommon(t, conf.timeScale, conf.timeStart, conf.angularFrequency);
return {
position: springCriticallyDampedPosition(conf.positionPrecision, oldPos, timeExpFreq, expTerm, conf.velocity, timeExp, conf.equilibrium),
velocity: springCriticallyDampedVelocity(conf.velocityPrecision, oldPos, conf.angularFrequency, timeExpFreq, conf.velocity, expTerm),
};
},
main,
(t) => {
const [expTerm, timeExp, timeExpFreq] = springCriticallyDampedCommon(t, conf.timeScale, conf.timeStart, conf.angularFrequency);
return springCriticallyDampedPosition(conf.positionPrecision, oldPos, timeExpFreq, expTerm, conf.velocity, timeExp, conf.equilibrium);
},
(t) => {
const [expTerm, _timeExp, timeExpFreq] = springCriticallyDampedCommon(t, conf.timeScale, conf.timeStart, conf.angularFrequency);
return springCriticallyDampedVelocity(conf.velocityPrecision, oldPos, conf.angularFrequency, timeExpFreq, conf.velocity, expTerm);
}
},
(t) => isStable(main(t), conf)
);
}

Expand Down
32 changes: 16 additions & 16 deletions deno_dist/SpringConfig.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
// Default to 1000 so time is in milliseconds
export const DEFAULT_TIME_SCALE = 1000;

// This value ensure a valid binary rounding
export const DEFAULT_PRECISION = 1 / (1 << 14);

export interface SpringConfig {
export interface ISpringConfig {
// initial position
position: number;
// initial velocity
Expand All @@ -25,7 +19,13 @@ export interface SpringConfig {
dampingRatioPrecision: number;
}

const DEFAULT_CONFIG: SpringConfig = {
// Default to 1000 so time is in milliseconds
export const DEFAULT_TIME_SCALE = 1000;

// This value ensure a valid binary rounding
export const DEFAULT_PRECISION = 1 / (1 << 14);

export const DEFAULT_CONFIG: ISpringConfig = {
position: 0,
velocity: 0,
equilibrium: 1,
Expand Down Expand Up @@ -55,54 +55,54 @@ export const SpringConfig = {
angularFrequencyFromSpringConstant,
};

function defaults(config: Partial<SpringConfig> = {}): SpringConfig {
function defaults(config: Partial<ISpringConfig> = {}): ISpringConfig {
return {
...DEFAULT_CONFIG,
...config,
};
}

function basic(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function basic(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
angularFrequency: 1,
dampingRatio: 1,
...config,
};
}

function gentle(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function gentle(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
angularFrequency: 0.6,
dampingRatio: 0.6,
...config,
};
}

function wobbly(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function wobbly(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
angularFrequency: 0.8,
dampingRatio: 0.4,
...config,
};
}

function stiff(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function stiff(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
angularFrequency: 1.1,
dampingRatio: 0.7,
...config,
};
}

function slow(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function slow(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
angularFrequency: 0.5,
dampingRatio: 1,
...config,
};
}

function decay(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function decay(config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
const resolved = {
...DEFAULT_CONFIG,
...config,
Expand All @@ -117,7 +117,7 @@ function decay(config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
};
}

function stable(equilibrium: number, config: Partial<SpringConfig> = {}): Partial<SpringConfig> {
function stable(equilibrium: number, config: Partial<ISpringConfig> = {}): Partial<ISpringConfig> {
return {
...config,
velocity: 0,
Expand Down
Loading

0 comments on commit 28e0752

Please sign in to comment.