├── .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 [](https://www.npmjs.com/package/ola) [](https://github.com/franciscop/ola/blob/master/ola.test.js) [](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 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | Smooth interpolation with Ola()
224 |
225 |
226 | Harsh interpolation with Tweenmax
227 |
228 |
229 |
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 |
--------------------------------------------------------------------------------