├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── docs ├── ball.gif ├── compare │ ├── tweenmax.html │ └── tweenmax.js ├── dots.gif ├── index.html ├── index.js ├── line.gif ├── line.mp4 ├── smooth_ola.png ├── smooth_tweenmax.png └── style.css ├── ola.js ├── ola.min.js ├── ola.test.js ├── package.json └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/franciscopresencia/19 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 1 1 *' # Runs every January 1st at 00:00 UTC 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [8.x, 10.x, 12.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: install dependencies 23 | run: npm install 24 | - name: npm test 25 | run: npm test 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | .DS_Store 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Francisco Presencia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/ball.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/ball.gif -------------------------------------------------------------------------------- /docs/compare/tweenmax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ola 8 | 9 | 10 | 11 |
12 | Real value 13 | Interpolated 14 |
15 | 16 |
17 | 18 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/compare/tweenmax.js: -------------------------------------------------------------------------------- 1 | // Create a tween that modifies properties of obj 2 | const obj = { x: 0 }; 3 | const angle = new TweenMax(obj, 2, { x: 100 }); 4 | //then later, update the destination x and y values, restarting the angle 5 | angle.updateTo({ x: Math.random() }, true); 6 | //or to update the values mid-tween without restarting, do this: 7 | // angle.updateTo({x:300, y:0}, false); 8 | 9 | // Double size and scale for better resolution 10 | const canvas = document.querySelector("#graph"); 11 | canvas.width = window.innerWidth * 2; 12 | 13 | const graph = new SmoothieChart({ 14 | grid: { strokeStyle: "#fff", fillStyle: "#fff" }, 15 | maxValue: 1, 16 | minValue: 0, 17 | // This makes it render a bit worse, but otherwise it's cheating 18 | interpolation: "step" 19 | }); 20 | graph.streamTo(canvas, 1200); 21 | 22 | const control = new TimeSeries(); 23 | const output = new TimeSeries(); 24 | 25 | // Add to SmoothieChart 26 | graph.addTimeSeries(control, { strokeStyle: "rgba(0, 0, 255, 0.3)" }); 27 | graph.addTimeSeries(output, { 28 | strokeStyle: "rgba(255, 0, 0, 0.6)", 29 | lineWidth: 2 30 | }); 31 | control.append(new Date(), 0); 32 | 33 | (function tick() { 34 | output.append(new Date(), obj.x); 35 | requestAnimationFrame(tick); 36 | })(); 37 | 38 | // Add a random value to each line every 1-2 seconds 39 | (function update() { 40 | const to = Math.random(); 41 | angle.updateTo({ x: to }, true); 42 | control.append(new Date(), to); 43 | setTimeout(update, 1300); 44 | })(); 45 | -------------------------------------------------------------------------------- /docs/dots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/dots.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ola 8 | 9 | 10 | 11 |
12 | Real value 13 | Interpolated 14 |
15 | 16 |
17 | 18 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | // Initial value for the position; center of the container 2 | const angle = Ola(0, 1000); 3 | 4 | // Double size and scale for better resolution 5 | const canvas = document.querySelector("#graph"); 6 | canvas.width = window.innerWidth * 2; 7 | 8 | const graph = new SmoothieChart({ 9 | grid: { strokeStyle: "#fff", fillStyle: "#fff" }, 10 | maxValue: 1, 11 | minValue: 0, 12 | // This makes it render a bit worse, but otherwise it's cheating 13 | interpolation: "step" 14 | }); 15 | graph.streamTo(canvas, 1200); 16 | 17 | const control = new TimeSeries(); 18 | const output = new TimeSeries(); 19 | 20 | // Add to SmoothieChart 21 | graph.addTimeSeries(control, { strokeStyle: "rgba(0, 0, 255, 0.3)" }); 22 | graph.addTimeSeries(output, { 23 | strokeStyle: "rgba(255, 0, 0, 0.6)", 24 | lineWidth: 2 25 | }); 26 | control.append(new Date(), 0); 27 | 28 | (function tick() { 29 | output.append(new Date(), angle.value); 30 | requestAnimationFrame(tick); 31 | })(); 32 | 33 | // Add a random value to each line every 1-2 seconds 34 | (function update() { 35 | const to = Math.random(); 36 | angle.value = to; 37 | control.append(new Date(), to); 38 | setTimeout(update, 1300); 39 | })(); 40 | -------------------------------------------------------------------------------- /docs/line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/line.gif -------------------------------------------------------------------------------- /docs/line.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/line.mp4 -------------------------------------------------------------------------------- /docs/smooth_ola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/smooth_ola.png -------------------------------------------------------------------------------- /docs/smooth_tweenmax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/ola/d8176c563c3a1cb09aebcd5a7d8dcf88493af3f4/docs/smooth_tweenmax.png -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Sans-Serif; 5 | } 6 | 7 | .legend { 8 | margin: 10px; 9 | } 10 | 11 | .dot { 12 | margin-right: 10px; 13 | } 14 | 15 | .dot::before { 16 | content: ""; 17 | width: 15px; 18 | height: 15px; 19 | margin-bottom: -2px; 20 | margin-right: 5px; 21 | border-radius: 10px; 22 | display: inline-block; 23 | background: red; 24 | } 25 | 26 | .dot.control::before { 27 | background: rgba(0, 0, 255, 0.4); 28 | } 29 | 30 | .dot.interpolated::before { 31 | background: rgba(255, 0, 0, 0.6); 32 | } 33 | 34 | .container { 35 | position: relative; 36 | margin: 10px; 37 | box-shadow: 0 0 0 1px #ccc; 38 | border-radius: 8px; 39 | height: 200px; 40 | overflow: hidden; 41 | } 42 | 43 | canvas { 44 | transform: scale(0.5); 45 | transform-origin: top left; 46 | } 47 | -------------------------------------------------------------------------------- /ola.js: -------------------------------------------------------------------------------- 1 | // Calculate the position based on the current position, speed and time: 2 | // https://www.wolframalpha.com/input/?i=x+%3D+2+*+t+%5E+3+-+3+*+t+%5E+2+%2B+1+from+t+%3D+0+to+t+%3D+1 3 | const position = (x0, v0, t1, t) => { 4 | const a = (v0 * t1 + 2 * x0) / t1 ** 3; 5 | const b = -(2 * v0 * t1 + 3 * x0) / t1 ** 2; 6 | const c = v0; 7 | const d = x0; 8 | return a * t ** 3 + b * t ** 2 + c * t + d; 9 | }; 10 | 11 | // Calculate the speed based on the current position, speed and time: 12 | const speed = (x0, v0, t1, t) => { 13 | const a = (v0 * t1 + 2 * x0) / t1 ** 3; 14 | const b = -(2 * v0 * t1 + 3 * x0) / t1 ** 2; 15 | const c = v0; 16 | const d = x0; 17 | return 3 * a * t ** 2 + 2 * b * t + c; 18 | }; 19 | 20 | // Loop it in different ways 21 | const each = function(values, cb) { 22 | const multi = typeof values === "number" ? { value: values } : values; 23 | Object.entries(multi).map(([key, value]) => cb(value, key)); 24 | }; 25 | 26 | // Handle a single dimension 27 | function Single(init, time) { 28 | this.start = new Date() / 1000; 29 | this.time = time; 30 | this.from = init; 31 | this.current = init; 32 | this.to = init; 33 | this.speed = 0; 34 | } 35 | 36 | Single.prototype.get = function(now) { 37 | const t = now / 1000 - this.start; 38 | if (t < 0) { 39 | throw new Error("Cannot read in the past"); 40 | } 41 | if (t >= this.time) { 42 | return this.to; 43 | } 44 | return this.to - position(this.to - this.from, this.speed, this.time, t); 45 | }; 46 | 47 | Single.prototype.getSpeed = function(now) { 48 | const t = now / 1000 - this.start; 49 | if (t >= this.time) { 50 | return 0; 51 | } 52 | return speed(this.to - this.from, this.speed, this.time, t); 53 | }; 54 | 55 | Single.prototype.set = function(value, time) { 56 | const now = new Date(); 57 | const current = this.get(now); 58 | this.speed = this.getSpeed(now); 59 | this.start = now / 1000; 60 | this.from = current; 61 | this.to = value; 62 | if (time) { 63 | this.time = time; 64 | } 65 | return current; 66 | }; 67 | 68 | // The multidimensional constructor, makes a { value: init } if it's 1D 69 | function Ola(values, time = 300) { 70 | // This is just an alias 71 | if (typeof values === "number") { 72 | values = { value: values }; 73 | } 74 | 75 | // Loop over the first argument 76 | each(values, (init, key) => { 77 | const value = new Single(init, time / 1000); 78 | // But we are not interested in it; instead, set it as a ghost 79 | Object.defineProperty(values, "_" + key, { value }); // pos._x.to, pos._x.speed, ... 80 | Object.defineProperty(values, "$" + key, { get: () => value.to }); // pos.$x 81 | Object.defineProperty(values, key, { 82 | get: () => value.get(new Date()), // pos.x 83 | set: val => value.set(val), // pos.x = 10 84 | enumerable: true 85 | }); 86 | }); 87 | 88 | // pos.get('x') 89 | Object.defineProperty(values, "get", { 90 | get: () => 91 | function(name = "value", now = new Date()) { 92 | return this["_" + name].get(now); 93 | } 94 | }); 95 | 96 | // pos.set(10) 97 | // pos.set({ x: 10 }) 98 | // pos.set({ x: 10 }, time) 99 | Object.defineProperty(values, "set", { 100 | get: () => 101 | function(values, time = 0) { 102 | each(values, (value, key) => { 103 | this["_" + key].set(value, time / 1000); 104 | }); 105 | } 106 | }); 107 | 108 | // So that you can use the original methods 109 | return values; 110 | } 111 | 112 | export default Ola; 113 | -------------------------------------------------------------------------------- /ola.min.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Ola=factory())})(this,function(){"use strict";const position=(x0,v0,t1,t)=>{const a=(v0*t1+2*x0)/t1**3;const b=-(2*v0*t1+3*x0)/t1**2;const c=v0;const d=x0;return a*t**3+b*t**2+c*t+d};const speed=(x0,v0,t1,t)=>{const a=(v0*t1+2*x0)/t1**3;const b=-(2*v0*t1+3*x0)/t1**2;const c=v0;return 3*a*t**2+2*b*t+c};const each=function(values,cb){const multi=typeof values==="number"?{value:values}:values;Object.entries(multi).map(([key,value])=>cb(value,key))};function Single(init,time){this.start=new Date/1e3;this.time=time;this.from=init;this.current=init;this.to=init;this.speed=0}Single.prototype.get=function(now){const t=now/1e3-this.start;if(t<0){throw new Error("Cannot read in the past")}if(t>=this.time){return this.to}return this.to-position(this.to-this.from,this.speed,this.time,t)};Single.prototype.getSpeed=function(now){const t=now/1e3-this.start;if(t>=this.time){return 0}return speed(this.to-this.from,this.speed,this.time,t)};Single.prototype.set=function(value,time){const now=new Date;const current=this.get(now);this.speed=this.getSpeed(now);this.start=now/1e3;this.from=current;this.to=value;if(time){this.time=time}return current};function Ola(values,time=300){if(typeof values==="number"){values={value:values}}each(values,(init,key)=>{const value=new Single(init,time/1e3);Object.defineProperty(values,"_"+key,{value:value});Object.defineProperty(values,"$"+key,{get:()=>value.to});Object.defineProperty(values,key,{get:()=>value.get(new Date),set:val=>value.set(val),enumerable:true})});Object.defineProperty(values,"get",{get:()=>(function(name="value",now=new Date){return this["_"+name].get(now)})});Object.defineProperty(values,"set",{get:()=>(function(values,time=0){each(values,(value,key)=>{this["_"+key].set(value,time/1e3)})})});return values}return Ola}); -------------------------------------------------------------------------------- /ola.test.js: -------------------------------------------------------------------------------- 1 | import Ola from "./ola"; 2 | 3 | const delay = (time = 400) => new Promise(done => setTimeout(done, time)); 4 | 5 | // 10% of approximation by default 6 | expect.extend({ 7 | toApproximate: (a, b, perc = 0.1) => { 8 | const pass = a === 0 || b === 0 ? a === b : Math.abs((a - b) / b) <= perc; 9 | return { 10 | pass, 11 | message: () => `"${a}" ${pass ? "should" : "does not"} approximate "${b}"` 12 | }; 13 | } 14 | }); 15 | 16 | describe("Ola", () => { 17 | it("is defined", () => { 18 | expect(Ola).toBeDefined(); 19 | }); 20 | 21 | it("can read the future", () => { 22 | const pos = Ola(0); 23 | pos.value = 100; 24 | const now = new Date().getTime(); 25 | expect(Math.round(pos.get("value", now))).toBe(0); 26 | expect(pos.get("value", now + 150)).toApproximate(50); 27 | expect(pos.get("value", now + 400)).toApproximate(100); 28 | }); 29 | 30 | it("cannot read the past", () => { 31 | const pos = Ola(0); 32 | const now = new Date().getTime(); 33 | expect(() => pos.get("value", now - 100)).toThrowError(/past/); 34 | }); 35 | 36 | it("unfortunately has inconsistencies", async () => { 37 | const pos = Ola(0); 38 | const now = new Date().getTime(); 39 | await delay(200); 40 | expect(() => pos.get("value", now)).not.toThrowError(/past/); 41 | }); 42 | }); 43 | 44 | describe("Ola(0)", () => { 45 | it("can be initialized", () => { 46 | expect(Ola(0)).toBeDefined(); 47 | }); 48 | 49 | it("can read the value", async () => { 50 | const pos = Ola(0); 51 | expect(pos.value).toBe(0); 52 | expect(pos.get()).toBe(0); 53 | expect(pos.get("value")).toBe(0); 54 | expect(pos).toEqual({ value: 0 }); 55 | expect(JSON.stringify(pos)).toBe('{"value":0}'); 56 | }); 57 | 58 | it("can update the value", async () => { 59 | const pos = Ola(0); 60 | pos.value = 100; 61 | await delay(); 62 | 63 | expect(pos.value).toBe(100); 64 | expect(pos.get("value")).toBe(100); 65 | expect(pos).toEqual({ value: 100 }); 66 | expect(JSON.stringify(pos)).toBe('{"value":100}'); 67 | }); 68 | 69 | it("can update the value after long time with speed 0", async () => { 70 | const pos = Ola(0); 71 | pos.value = 100; 72 | await delay(1000); 73 | pos.value = 0; 74 | expect(pos.value).toApproximate(100); 75 | await delay(150); 76 | expect(pos.value).toApproximate(50); 77 | }); 78 | 79 | it("can update the value in several spots", async () => { 80 | const pos = Ola(0); 81 | pos.value = 100; 82 | await delay(150); // value ~= 50 83 | pos.value = 0; 84 | expect(pos.value).toApproximate(50); 85 | await delay(150); // value SHOULD NOT ~= 25 because of initial speed 86 | expect(pos.value).not.toApproximate(25); 87 | }); 88 | 89 | it("will have speed when approximated midway", async () => { 90 | const pos = Ola(0); 91 | pos.value = 100; 92 | await delay(150); // value ~= 50 93 | pos.value = 0; 94 | expect(pos.value).toApproximate(50); 95 | await delay(150); // value SHOULD NOT ~= 25 because of initial speed 96 | expect(pos.value).not.toApproximate(25); 97 | }); 98 | 99 | it("can update the time it takes", async () => { 100 | const pos = Ola({ value: 0 }); 101 | pos.value = 100; 102 | await delay(); 103 | expect(pos.value).toApproximate(100); 104 | pos.set({ value: 200 }, 100); 105 | await delay(100); 106 | expect(pos.value).toApproximate(200); 107 | }); 108 | 109 | it("can use negative numbers", async () => { 110 | const pos = Ola(-10); 111 | pos.value = -100; 112 | await delay(); 113 | 114 | expect(pos.value).toBe(-100); 115 | expect(pos.get("value")).toBe(-100); 116 | expect(pos).toEqual({ value: -100 }); 117 | expect(JSON.stringify(pos)).toBe('{"value":-100}'); 118 | }); 119 | 120 | it("can update the value with .set()", async () => { 121 | const pos = Ola(0); 122 | pos.set(100); 123 | await delay(); 124 | 125 | expect(pos.value).toBe(100); 126 | expect(pos.get("value")).toBe(100); 127 | expect(pos).toEqual({ value: 100 }); 128 | expect(JSON.stringify(pos)).toBe('{"value":100}'); 129 | }); 130 | }); 131 | 132 | describe("Ola({ x: 0 })", () => { 133 | it("can be initialized", () => { 134 | expect(Ola({ x: 0 })).toEqual({ x: 0 }); 135 | }); 136 | 137 | it("can read the value", () => { 138 | const pos = Ola({ x: 0 }); 139 | expect(pos.x).toBe(0); 140 | expect(pos.get("x")).toBe(0); 141 | expect(pos).toEqual({ x: 0 }); 142 | expect(JSON.stringify(pos)).toBe('{"x":0}'); 143 | }); 144 | 145 | it("can update the value", async () => { 146 | const pos = Ola({ x: 0 }); 147 | pos.x = 100; 148 | await delay(); 149 | 150 | expect(pos.x).toBe(100); 151 | expect(pos.get("x")).toBe(100); 152 | expect(pos).toEqual({ x: 100 }); 153 | expect(JSON.stringify(pos)).toBe('{"x":100}'); 154 | }); 155 | 156 | it("can use negative numbers", async () => { 157 | const pos = Ola({ x: -10 }); 158 | pos.x = -100; 159 | await delay(); 160 | 161 | expect(pos.x).toBe(-100); 162 | expect(pos.get("x")).toBe(-100); 163 | expect(pos).toEqual({ x: -100 }); 164 | expect(JSON.stringify(pos)).toBe('{"x":-100}'); 165 | }); 166 | 167 | it("can update the value with .set()", async () => { 168 | const pos = Ola({ x: 0 }); 169 | pos.set({ x: 100 }); 170 | await delay(); 171 | 172 | expect(pos.x).toBe(100); 173 | expect(pos.get("x")).toBe(100); 174 | expect(pos).toEqual({ x: 100 }); 175 | expect(JSON.stringify(pos)).toBe('{"x":100}'); 176 | }); 177 | 178 | it("can update multiple values", async () => { 179 | const pos = Ola({ x: 0, y: 0, z: 0 }); 180 | pos.set({ x: 0, y: 10, z: 100 }); 181 | await delay(); 182 | 183 | expect(pos).toEqual({ x: 0, y: 10, z: 100 }); 184 | expect(JSON.stringify(pos)).toBe(`{"x":0,"y":10,"z":100}`); 185 | }); 186 | }); 187 | 188 | describe("Ola([0])", () => { 189 | it("can be initialized", () => { 190 | expect(Ola([0])).toEqual([0]); 191 | }); 192 | 193 | it("can read the value", () => { 194 | const pos = Ola([0]); 195 | expect(pos[0]).toBe(0); 196 | expect(pos.get(0)).toBe(0); 197 | expect(pos.get("0")).toBe(0); 198 | expect(pos).toEqual([0]); 199 | expect(JSON.stringify(pos)).toBe("[0]"); 200 | }); 201 | 202 | it("can loop it", async () => { 203 | const pos = Ola([0, 0, 0]); 204 | pos.forEach((val, i) => { 205 | expect(val).toBe(0); 206 | pos[i] = 100; 207 | }); 208 | 209 | await delay(); 210 | pos.forEach(val => { 211 | expect(val).toBe(100); 212 | }); 213 | }); 214 | 215 | it("can create it from Array(N).fill(0)", async () => { 216 | // Generates 1000 instances seamlessly 217 | const dots = Ola(Array(1000).fill(0)); 218 | 219 | // Everything updates every 600ms 220 | dots.forEach((dot, i) => { 221 | expect(dot).toBe(0); 222 | dots[i] = 100; 223 | }); 224 | 225 | await delay(); 226 | dots.forEach(dot => { 227 | expect(dot).toBe(100); 228 | }); 229 | }); 230 | 231 | it("can update the value", async () => { 232 | const pos = Ola([0]); 233 | pos[0] = 100; 234 | await delay(); 235 | 236 | expect(pos[0]).toBe(100); 237 | expect(pos.get(0)).toBe(100); 238 | expect(pos).toEqual([100]); 239 | expect(JSON.stringify(pos)).toBe("[100]"); 240 | }); 241 | 242 | it("can use negative numbers", async () => { 243 | const pos = Ola([-10]); 244 | pos[0] = -100; 245 | await delay(); 246 | 247 | expect(pos[0]).toBe(-100); 248 | expect(pos.get(0)).toBe(-100); 249 | expect(pos.get("0")).toBe(-100); 250 | expect(pos).toEqual([-100]); 251 | expect(JSON.stringify(pos)).toBe("[-100]"); 252 | }); 253 | 254 | it("can update the value with .set()", async () => { 255 | const pos = Ola([0]); 256 | pos.set([100]); 257 | await delay(); 258 | 259 | expect(pos[0]).toBe(100); 260 | expect(pos.get(0)).toBe(100); 261 | expect(pos.get("0")).toBe(100); 262 | expect(pos).toEqual([100]); 263 | expect(JSON.stringify(pos)).toBe("[100]"); 264 | }); 265 | 266 | it("can update multiple values", async () => { 267 | const pos = Ola([0, 0, 0]); 268 | pos.set([0, 10, 100]); 269 | await delay(); 270 | 271 | expect(pos).toEqual([0, 10, 100]); 272 | expect(JSON.stringify(pos)).toBe("[0,10,100]"); 273 | }); 274 | 275 | it("can update multiple values", async () => { 276 | const pos = Ola(Array(3).fill(0)); 277 | pos.set([0, 10, 100]); 278 | await delay(); 279 | 280 | expect(pos).toEqual([0, 10, 100]); 281 | expect(JSON.stringify(pos)).toBe("[0,10,100]"); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ola", 3 | "version": "1.2.1", 4 | "description": "Smooth animation library for interpolating numbers in real time", 5 | "homepage": "https://github.com/franciscop/ola#readme", 6 | "repository": "https://github.com/franciscop/ola.git", 7 | "bugs": "https://github.com/franciscop/ola/issues", 8 | "funding": { 9 | "url": "https://www.paypal.me/franciscopresencia/19" 10 | }, 11 | "author": "Francisco Presencia (https://francisco.io/)", 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "rollup ola.js --name Ola --output.format umd | uglifyjs -o ola.min.js", 15 | "ugly": "uglifyjs --version", 16 | "test": "jest --coverage", 17 | "size": "echo \"$(gzip -c ola.min.js | wc -c) bytes\" # Only for Unix", 18 | "watch": "jest --watch" 19 | }, 20 | "keywords": [ 21 | "value", 22 | "interpolation", 23 | "inbetweening", 24 | "animation", 25 | "transition", 26 | "smooth", 27 | "tween", 28 | "js" 29 | ], 30 | "main": "ola.min.js", 31 | "files": [], 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "@babel/core": "^7.4.4", 35 | "@babel/preset-env": "^7.4.4", 36 | "babel-jest": "^24.8.0", 37 | "jest": "^24.8.0", 38 | "rollup": "^1.11.3", 39 | "uglify-es": "^3.3.9" 40 | }, 41 | "babel": { 42 | "presets": [ 43 | [ 44 | "@babel/preset-env", 45 | { 46 | "targets": { 47 | "node": "current" 48 | } 49 | } 50 | ] 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ola [![npm install ola](https://img.shields.io/badge/npm%20install-ola-blue.svg)](https://www.npmjs.com/package/ola) [![test badge](https://github.com/franciscop/ola/workflows/tests/badge.svg)](https://github.com/franciscop/ola/blob/master/ola.test.js) [![gzip size](https://img.badgesize.io/franciscop/ola/master/ola.min.js.svg?compression=gzip&label=size)](https://github.com/franciscop/ola/blob/master/ola.min.js) 2 | 3 | Smooth animation library for [inbetweening](https://en.wikipedia.org/wiki/Inbetweening) / [interpolating](https://en.wikipedia.org/wiki/Interpolation_(computer_graphics)) numbers in realtime: 4 | 5 | 6 | 7 | 8 | 9 | ```js 10 | // Start tracking the value 11 | const pos = Ola({ y: 0 }); 12 | 13 | // Set the value to update async 14 | pos.set({ y: 100 }); 15 | 16 | // Read the evolution over time 17 | setInterval(() => graph(pos.y), 5); 18 | ``` 19 | 20 | It works with multiple values/dimensions: 21 | 22 | 23 | 24 | 25 | 26 | ```js 27 | const pos = Ola({ x: 0, y: 0 }); 28 | 29 | window.addEventListener('click', e => { 30 | pos.set({ x: e.pageX, y: e.pageY }); 31 | }); 32 | 33 | setInterval(() => { 34 | ball.style.left = `${pos.x}px`; 35 | ball.style.top = `${pos.y}px`; 36 | }, 10); 37 | ``` 38 | 39 | Also works great with many instances since they are independent: 40 | 41 | 42 | 43 | 44 | 45 | ```js 46 | // Generates 1000 instances seamlessly 47 | const dots = Ola(Array(1000).fill(0)); 48 | 49 | // Everything updates every 600ms 50 | setInterval(() => dots.forEach((dot, i) => { 51 | dots[i] = Math.random(); 52 | }), 600); 53 | 54 | // ... read + paint screen here 55 | ``` 56 | 57 | > Tip: click on the GIFs for a live demo with the code :) 58 | 59 | ## Getting started 60 | 61 | Install it with npm: 62 | 63 | ``` 64 | npm install ola 65 | ``` 66 | 67 | Then import it and use it: 68 | 69 | ```js 70 | import Ola from "ola"; 71 | const pos = Ola({ x: 0 }); 72 | console.log(pos.x); // 0 73 | ``` 74 | 75 | If you prefer to use a CDN: 76 | 77 | ```html 78 | 79 | 83 | ``` 84 | 85 | ## Documentation 86 | 87 | There are three distinct operations that can be run: creating an instance, setting it to update and reading it. 88 | 89 | ### Create an instance 90 | 91 | ```js 92 | Ola(initial, time = 300); 93 | ``` 94 | 95 | The first parameter is the initial value. It can be either a single number, or an object of `key:numbers` or an array of numbers: 96 | 97 | ```js 98 | const heater = Ola(20); // Alias of `{ value: 20 }` 99 | const motor = Ola({ angle: 180 }); // A named parameter for clarity 100 | const position = Ola({ x: 0, y: 0 }); // Any number of properties 101 | const heights = Ola([0, 0, 0, 0]); // A group of heights 102 | ``` 103 | 104 | The second parameter is how long the transition will last. It should be a number that represents the time in milliseconds: 105 | 106 | ```js 107 | const heater = Ola(20); // Default = 300 ms 108 | const motor = Ola({ angle: 180 }, 1000); // Turn the motor slowly 109 | const position = Ola({ x: 0, y: 0 }, 100); // Quick movements for the position 110 | const heights = Ola([0, 0, 0, 0], 300); // 300, same as the default 111 | ``` 112 | 113 | Passing a single number as a parameter is the same as passing `{ value: num }`, we are just helping by setting a shortname. It is offered for convenience, but recommend not mixing both styles in the same project. 114 | 115 | It works with Javascript numbers, but please keep things reasonable (under `Number.MAX_VALUE / 10`): 116 | 117 | ```js 118 | console.log(Ola(100)); 119 | console.log(Ola(-100)); 120 | console.log(Ola(0.001)); 121 | console.log(Ola(1 / 100)); 122 | ``` 123 | 124 | The time it takes to update can also be updated while setting the value, which will update it for any subsequent transition: 125 | 126 | ```js 127 | // All `pos.set()` will take 1 full second 128 | const pos = Ola({ x: 0 }, 1000); 129 | pos.set({ x: 100 }, 3000); 130 | ``` 131 | 132 | ### Update the value 133 | 134 | ```js 135 | heater.value = 25; // Since the constructor used a number, use `.value` 136 | motor.angle = 90; // Turn -90 degrees from before 137 | position.set({ x: 100, y: 100 }); // Move 0,0 => 100,100 138 | heights[1] = 120; // Move the second (0-index) item to 120 139 | ``` 140 | 141 | When we update a property **it is not updated instantaneously** (that's the whole point of this library), but instead it's set to update asynchronously: 142 | 143 | ```js 144 | const pos = Ola({ x: 0 }); 145 | pos.set({ x: 100 }); 146 | 147 | // 0 - still hasn't updated 148 | console.log(pos.x); 149 | 150 | // 100 - after 300ms it's fully updated 151 | setTimeout(() => console.log(pos.x), 1000); 152 | ``` 153 | 154 | Remember that if you set the value as `Ola(10)`, this is really an alias for `Ola({ value: 10 })`, so use the property `.value` to update it: 155 | 156 | ```js 157 | heater.value = 25; 158 | heater.set({ value: 25 }); 159 | ``` 160 | 161 | You can see in this graph, the blue line is the value that is set though `.set()`, while the red line is the value that reading it returns: 162 | 163 | 164 | 165 | 166 | 167 | ### Read the value 168 | 169 | ```js 170 | log(heater.value); // Since the constructor used a number, use `.value` 171 | log(motor.angle); // Read as an object property 172 | log(position.get("x")); // Find the X value 173 | log(heights[1]); // Move the first item to 120 174 | ``` 175 | 176 | You can read the value at any time, and the value will be calculated at that moment in time: 177 | 178 | ```js 179 | const pos = Ola({ x: 0 }); 180 | pos.set({ x: 100 }); 181 | 182 | setInterval(() => { 183 | // It will update every time it's read 184 | console.log(pos.x); 185 | }, 10); 186 | ``` 187 | 188 | In contrast to other libraries, there's no need to tick/update the function every N ms or before reading the value, since `Ola()` uses math functions you should just read it when needed. 189 | 190 | ## Advanced usage 191 | 192 | If you need to access more advanced features, you can read these two properties: 193 | 194 | ```js 195 | // All the details about the current transition, please see the source for more info 196 | log(heater._value); // { to: 25, from: 20, ... } 197 | log(motor._angle); // { to: 90, from: 180, ... } 198 | 199 | // The value that will be set when the transition is finished 200 | log(heater.$value); // 25 201 | log(motor.$angle); // 90 202 | ``` 203 | 204 | ## Features 205 | 206 | While there are some other great libraries like Tween, this one has some improvements: 207 | 208 | ### Smooth in realtime 209 | 210 | Other libraries don't move smoothly when there's an update **while the previous transition is still ongoing**. Ola makes sure there are no harsh corners: 211 | 212 | 213 | 214 | 217 | 220 | 221 | 222 | 225 | 228 | 229 |
215 | 216 | 218 | 219 |
223 | Smooth interpolation with Ola() 224 | 226 | Harsh interpolation with Tweenmax 227 |
230 | 231 | Status of libraries updating animation mid-way: 232 | 233 | - **Ola.js** - working smoothly, see screenshot above. 234 | - **TweenMax** - harsh transition. See screenshot above. 235 | - **Tween.js** - no transitions at all, feature request made in 2016: https://github.com/tweenjs/tween.js/issues/257 236 | - [**Open an Issue**](https://github.com/franciscop/ola/issues/new) with other libraries that you know. 237 | 238 | ### Lazy loading 239 | 240 | Since this is driven by mathematical equations, the library doesn't calculate any value until it needs to be read/updated. It will also _only_ change the one we need instead of all of the values: 241 | 242 | ```js 243 | const position = Ola({ x: 0, y: 0 }); 244 | position.x = 10; // Only updates X 245 | console.log(position.x); // Calculates only X position, not y 246 | ``` 247 | 248 | Not only this is great for performance, but it also makes for a clean self-contained API where each instance is independent and portable. 249 | 250 | ## Others from Author 251 | 252 | Like this project? Francisco has many more! Check them out: 253 | 254 | - **[server.js](https://serverjs.io/)** - a batteries-included Node.js server 255 | - **[translate.js](https://github.com/franciscop/translate)** - to easily translate text on the browser and Node.js 256 | --------------------------------------------------------------------------------