From 9c215230cffbb08bdcd47c861f88cfa6065f6752 Mon Sep 17 00:00:00 2001 From: Jonathan Dumaine Date: Thu, 13 Sep 2018 00:54:16 -0700 Subject: [PATCH 1/5] Avoid object literals in updateConfig Inner function should be free of memory allocation --- src/index.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3b6cb86..cc0b992 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export interface SpringConfig { overshootClamping: boolean; // False when overshooting is allowed, true when it is not. Defaults to false. restVelocityThreshold: number; // When spring's velocity is below `restVelocityThreshold`, it is at rest. Defaults to .001. restDisplacementThreshold: number; // When the spring's displacement (current value) is below `restDisplacementThreshold`, it is at rest. Defaults to .001. + [index: string]: any; } export type PartialSpringConfig = Partial; @@ -158,16 +159,14 @@ export class Spring { this._advanceSpringToTime(Date.now()); - const baseConfig = { - fromValue: this._currentValue, - initialVelocity: this._currentVelocity - }; + this._config.fromValue = this._currentValue; + this._config.initialVelocity = this._currentVelocity; - this._config = { - ...this._config, - ...baseConfig, - ...updatedConfig - }; + for (const key in updatedConfig) { + if (this._config.hasOwnProperty(key)) { + this._config[key] = updatedConfig[key as keyof SpringConfig]; + } + } this._reset(); From f6a34f0a908c6b93b94f55f04f36349d3a01efd4 Mon Sep 17 00:00:00 2001 From: Jonathan Dumaine Date: Thu, 13 Sep 2018 00:55:53 -0700 Subject: [PATCH 2/5] Add `setValue` method, `raf` config option Also mangles _step and `advancedToTime` pretty bad. --- demos/squares/index.html | 15 ++++++++ demos/squares/index.tsx | 79 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 34 ++++++++++++----- 3 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 demos/squares/index.html create mode 100644 demos/squares/index.tsx diff --git a/demos/squares/index.html b/demos/squares/index.html new file mode 100644 index 0000000..71011bc --- /dev/null +++ b/demos/squares/index.html @@ -0,0 +1,15 @@ + + + Squares + + + + +
+ + + diff --git a/demos/squares/index.tsx b/demos/squares/index.tsx new file mode 100644 index 0000000..e4e17b2 --- /dev/null +++ b/demos/squares/index.tsx @@ -0,0 +1,79 @@ +import { Spring } from "../../dist/module" + + +const COUNT = 50 +const squares = [] + +const canvas = document.createElement('canvas') +const ctx = canvas.getContext('2d') + +canvas.style.width = canvas.style.height = '100%' +document.body.appendChild(canvas) +resizeCanvasToDisplaySize(canvas) + + +class Square { + constructor(i) { + this.i = i + this.x = Math.random() * canvas.width + this.y = Math.random() * canvas.height + this.color = '#'+Math.random().toString(16).substr(2,6) + this.springs = { + x: new Spring({fromValue: this.x, raf: false}), // x + y: new Spring({fromValue: this.y, raf: false}) // y + } + this.springs.x.onUpdate(s => this.x = s.currentValue) + this.springs.y.onUpdate(s => this.y = s.currentValue) + } + setPosition(x, y) { + this.springs.x.setValue(x); + this.springs.y.setValue(y); + } + tick(now) { + this.springs.x._advanceSpringToTime(now, true) + this.springs.y._advanceSpringToTime(now, true) + //this.springs.x._step() + //this.springs.x._step() + } +} + +function resizeCanvasToDisplaySize(canvas) { + let w = (canvas.clientWidth*devicePixelRatio) | 0 + let h = (canvas.clientHeight*devicePixelRatio) | 0 + if (canvas.width != w || canvas.height != h) { + canvas.width = w + canvas.height = h + } +} + +function draw() { + const now = Date.now() + resizeCanvasToDisplaySize(canvas) + const width = canvas.width + const height = canvas.height + ctx.clearRect(0, 0, width, height) + for(var i = 0; i < squares.length; i++) { + const square = squares[i] + square.tick(now) + ctx.fillStyle = square.color + ctx.fillRect(square.x, square.y, 50, 50) + } + requestAnimationFrame(draw) +} + +setInterval(() => { + randomize() +}, 100) + +function randomize() { + squares[Math.floor(squares.length*Math.random())] + .setPosition(Math.random() * canvas.width, Math.random() * canvas.height) +} + +for(var i = 0; i < COUNT; i++) { + squares.push(new Square(i)) +} + +draw() + +window.randomize = randomize diff --git a/src/index.ts b/src/index.ts index cc0b992..8962d06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export interface SpringConfig { overshootClamping: boolean; // False when overshooting is allowed, true when it is not. Defaults to false. restVelocityThreshold: number; // When spring's velocity is below `restVelocityThreshold`, it is at rest. Defaults to .001. restDisplacementThreshold: number; // When the spring's displacement (current value) is below `restDisplacementThreshold`, it is at rest. Defaults to .001. + raf: boolean; [index: string]: any; } @@ -62,6 +63,7 @@ export class Spring { initialVelocity: withDefault(config.initialVelocity, 0), overshootClamping: withDefault(config.overshootClamping, false), allowsOverdamping: withDefault(config.allowsOverdamping, false), + raf: withDefault(config.raf, true), restVelocityThreshold: withDefault(config.restVelocityThreshold, 0.001), restDisplacementThreshold: withDefault( config.restDisplacementThreshold, @@ -85,9 +87,11 @@ export class Spring { if (!this._currentAnimationStep) { this._notifyListeners("onStart"); - this._currentAnimationStep = requestAnimationFrame((t: number) => { - this._step(Date.now()); - }); + if (this._config.raf) { + this._currentAnimationStep = requestAnimationFrame((t: number) => { + this._step(); + }); + } } } @@ -157,7 +161,7 @@ export class Spring { // being changed in `updatedConfig`, we run the simulation with `_step()` // and default `fromValue` and `initialVelocity` to their current values. - this._advanceSpringToTime(Date.now()); + this._advanceSpringToTime(); this._config.fromValue = this._currentValue; this._config.initialVelocity = this._currentVelocity; @@ -173,6 +177,18 @@ export class Spring { return this; } + /** + * + */ + setValue(value: number): this { + this._config.fromValue = this._currentValue; + this._config.toValue = value; + this._config.initialVelocity = this._currentVelocity; + this.start(); + this._advanceSpringToTime(undefined, true); + return this; + } + /** * The provided callback will be invoked when the simulation begins. */ @@ -245,20 +261,20 @@ export class Spring { * current state once per frame, and schedules the next frame if the spring is * not yet at rest. */ - private _step(timestamp: number) { - this._advanceSpringToTime(timestamp, true); + private _step() { + this._advanceSpringToTime(undefined, true); // check `_isAnimating`, in case `stop()` got called during // `_advanceSpringToTime()` - if (this._isAnimating) { + if (this._config.raf && this._isAnimating) { this._currentAnimationStep = requestAnimationFrame((t: number) => - this._step(Date.now()) + this._step() ); } } private _advanceSpringToTime( - timestamp: number, + timestamp: number = Date.now(), shouldNotifyListeners: boolean = false ) { // `_advanceSpringToTime` updates `_currentTime` and triggers the listeners. From b7decbb64439d3ef1de3e2ca70dbadda9b804211 Mon Sep 17 00:00:00 2001 From: Jonathan Dumaine Date: Thu, 13 Sep 2018 00:56:00 -0700 Subject: [PATCH 3/5] Add squares demo --- demos/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/index.html b/demos/index.html index 8ab7358..dc61a5a 100644 --- a/demos/index.html +++ b/demos/index.html @@ -6,6 +6,7 @@

Demos

From d86a3ad7bf32c7e687f44bbc016c99d44c5110d4 Mon Sep 17 00:00:00 2001 From: Jonathan Dumaine Date: Mon, 17 Sep 2018 12:12:16 -0700 Subject: [PATCH 4/5] Improve squares demo Use class references to avoid unnecessary closures on draw calls --- demos/squares/index.tsx | 98 ++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/demos/squares/index.tsx b/demos/squares/index.tsx index e4e17b2..9f72087 100644 --- a/demos/squares/index.tsx +++ b/demos/squares/index.tsx @@ -1,22 +1,13 @@ -import { Spring } from "../../dist/module" - - -const COUNT = 50 -const squares = [] +'use strict' -const canvas = document.createElement('canvas') -const ctx = canvas.getContext('2d') - -canvas.style.width = canvas.style.height = '100%' -document.body.appendChild(canvas) -resizeCanvasToDisplaySize(canvas) +import { Spring } from "../../dist/module" class Square { - constructor(i) { + constructor(i, x, y) { this.i = i - this.x = Math.random() * canvas.width - this.y = Math.random() * canvas.height + this.x = x + this.y = y this.color = '#'+Math.random().toString(16).substr(2,6) this.springs = { x: new Spring({fromValue: this.x, raf: false}), // x @@ -37,6 +28,52 @@ class Square { } } +class Renderer { + constructor() { + + const COUNT = 1000 + const canvas = document.createElement('canvas') + const squares = [] + + canvas.style.width = canvas.style.height = '100%' + document.body.appendChild(canvas) + resizeCanvasToDisplaySize(canvas) + + for(var i = 0; i < COUNT; i++) { + squares.push(new Square(i, Math.random() * canvas.width, Math.random() * canvas.height)) + } + + this.squares = squares + this.canvas = canvas + this.ctx = canvas.getContext('2d') + + } + + draw = () => { + const now = Date.now() + const {canvas, squares, ctx} = this + + resizeCanvasToDisplaySize(canvas) + + const {width, height} = canvas + + ctx.clearRect(0, 0, width, height) + + const index = Math.floor(squares.length*Math.random()) + squares[index].setPosition(Math.random() * canvas.width, Math.random() * canvas.height) + + for(var i = 0; i < squares.length; i++) { + const square = squares[i] + square.tick(now) + ctx.fillStyle = square.color + ctx.fillRect(square.x, square.y, 10, 10) + } + + requestAnimationFrame(this.draw) + } + +} + function resizeCanvasToDisplaySize(canvas) { let w = (canvas.clientWidth*devicePixelRatio) | 0 let h = (canvas.clientHeight*devicePixelRatio) | 0 @@ -46,34 +83,5 @@ function resizeCanvasToDisplaySize(canvas) { } } -function draw() { - const now = Date.now() - resizeCanvasToDisplaySize(canvas) - const width = canvas.width - const height = canvas.height - ctx.clearRect(0, 0, width, height) - for(var i = 0; i < squares.length; i++) { - const square = squares[i] - square.tick(now) - ctx.fillStyle = square.color - ctx.fillRect(square.x, square.y, 50, 50) - } - requestAnimationFrame(draw) -} - -setInterval(() => { - randomize() -}, 100) - -function randomize() { - squares[Math.floor(squares.length*Math.random())] - .setPosition(Math.random() * canvas.width, Math.random() * canvas.height) -} - -for(var i = 0; i < COUNT; i++) { - squares.push(new Square(i)) -} - -draw() - -window.randomize = randomize +const renderer = new Renderer() +renderer.draw() From 101be221877cc75af91cd4e3de286ce0be70747b Mon Sep 17 00:00:00 2001 From: Jonathan Dumaine Date: Mon, 17 Sep 2018 12:25:30 -0700 Subject: [PATCH 5/5] Add commented raf, no-raf versions of squares demo --- demos/squares/index.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/demos/squares/index.tsx b/demos/squares/index.tsx index 9f72087..1e0e825 100644 --- a/demos/squares/index.tsx +++ b/demos/squares/index.tsx @@ -9,14 +9,28 @@ class Square { this.x = x this.y = y this.color = '#'+Math.random().toString(16).substr(2,6) + + // raf + //this.springs = { + //x: new Spring({fromValue: this.x}), // x + //y: new Spring({fromValue: this.y}) // y + //} + + // no raf this.springs = { x: new Spring({fromValue: this.x, raf: false}), // x y: new Spring({fromValue: this.y, raf: false}) // y } + this.springs.x.onUpdate(s => this.x = s.currentValue) this.springs.y.onUpdate(s => this.y = s.currentValue) } setPosition(x, y) { + // raf + //this.springs.x.updateConfig({toValue: x}).start() + //this.springs.y.updateConfig({toValue: y}).start() + + // no raf this.springs.x.setValue(x); this.springs.y.setValue(y); } @@ -64,7 +78,10 @@ class Renderer { for(var i = 0; i < squares.length; i++) { const square = squares[i] + + // with no raf, have to manually advance simulation. comment out if using raf square.tick(now) + ctx.fillStyle = square.color ctx.fillRect(square.x, square.y, 10, 10) }