Compare commits

...

3 commits

Author SHA1 Message Date
Victor Westerlund
6b18c90228 Remove promise anipattern 2022-01-21 13:07:16 +01:00
Victor Westerlund
46f3a07c85
Replace setTimeout with requestAnimationFrame (#3)
* dev21w45-a

* dev21w45-b

* Implement requestAnimationFrame

This commit replaces setTimeout with a working example of requestAnimationFrame. Some further testing will have to be done

* Add support for loop
2021-12-26 14:07:37 +01:00
Victor Westerlund
a6e51c1e48
Update README.md 2021-12-26 12:50:52 +01:00
4 changed files with 148 additions and 114 deletions

View file

@ -37,11 +37,7 @@ export default class Monkeydo extends MonkeyMaster {
async play(manifest = null) { async play(manifest = null) {
if(!this.ready && !manifest) throw new Error("Can not start playback without a manifest"); if(!this.ready && !manifest) throw new Error("Can not start playback without a manifest");
if(manifest) { if(manifest) await this.load(manifest);
const load = this.load(manifest)
load.then(() => this.start());
return;
}
return await this.start(); return await this.start();
} }
} }

121
README.md
View file

@ -1,9 +1,83 @@
<p align="center"> <p align="center">
<img width="400" src="https://storage.googleapis.com/public.victorwesterlund.com/github/VictorWesterlund/monkeydo/monkeydo_.svg"/> <img width="400" src="https://storage.googleapis.com/public.victorwesterlund.com/github/VictorWesterlund/monkeydo/monkeydo_.svg"/>
</p> </p>
<h3 align="center">Threaded task chaining for JavaScript</h3> <h3 align="center">Multithreaded web animations and task chaining</h3>
<hr> <hr>
<p align="center">Monkeydo uses the portable data format JSON to read tasks, making it easy to read by primates and machines alike.</p> <p align="center">Execute general purpose JavaScript on cue with greater performance. Monkeydo is great, and designed for, complex DOM animations.</p>
<p align="center"><img src="https://storage.googleapis.com/public.victorwesterlund.com/github/VictorWesterlund/monkeydo/simple_demo.gif"/></a>
<table>
<td>
<p align="center">Monkeydo JSON manifest<br><a href="#manifest-semantics">View semantics</a></p>
<pre lang="json">
{
"tasks": [
[2000,"moveTo",100,0],
[1500,"setColor","red"],
[2650,"setColor","blue"],
[550,"moveTo",350,0]
]
}
</pre>
</td>
<td>
<p align="center">Normal JavaScript</p>
<pre lang="js">
const methods = {
element: document.getElementById("element"),
moveTo: (x,y) => {
methods.element.style.setProperty("transform",`translate(${x}%,${y}%)`);
},
setColor: (color) => {
methods.element.style.setProperty("background-color",color);
}
};
</pre>
</td>
</table>
<a href="https://victorwesterlund.github.io/monkeydo-demo/demo/simple_shapes">Open live demo</a>
<h1 align="center">Use Monkeydo</h1>
<p>Monkeydo comes as an ES6 module. In this guide we'll import this directly from a <i>./modules/</i> folder, but any location accessible by the importing script will work.</p>
<ol>
<li><strong>Import <code>Monkeydo</code> as an ESM</strong>
<pre lang="js">
import { default as Monkeydo } from "./modules/Monkeydo/Monkeydo.mjs";
</pre>
</li>
<li><strong>Define your JS methods in an object</strong>
<pre lang="js">
const methods = {
singForMe: (foo,bar) => {
console.log(foo,bar);
}
}
</pre>
</li>
<li><strong>Define your tasks in a JSON manifest (file or JSON-compatible JavaScript)</strong>
<pre lang="json">
{
"tasks": [
[0,"singForMe","Just like a","monkey"],
[1200,"singForMe","I've been","dancing"],
[160,"singForMe","my whole","life"]
]
}
</pre>
</li>
<li><strong>Initialize and run <code>Monkeydo</code> with your methods and manifest</strong>
<pre lang="js">
const monkey = new Monkeydo(methods);
monkey.play(manifest);
</pre>
</li>
</ol>
<p>The example above would be the same as running:</p>
<pre lang="js">
console.log("Just like a","monkey"); // Right away
console.log("I've been","dancing"); // 1.2 seconds after the first
console.log("my whole","life"); // and then 160 milliseconds after the second
</pre>
<h1>Manifest Semantics</h1>
<p>The JS passed to the Monkeydo constructor is executed by the initiator thread (ususally the main thread) when time is up. Which method and when is defined in a JSON file or string with the following semantics:</p>
<table> <table>
<td> <td>
<pre lang="json"> <pre lang="json">
@ -22,7 +96,7 @@
</tr> </tr>
<tr> <tr>
<td align="center">0</td> <td align="center">0</td>
<td><strong>Delay</strong><br>Wait this many milliseconds before running this task</td> <td><strong>Delay</strong><br>Wait this many milliseconds before running this method</td>
</tr> </tr>
<tr> <tr>
<td align="center">1</td> <td align="center">1</td>
@ -35,44 +109,3 @@
</table> </table>
</td> </td>
</table> </table>
<h1 align="center">Use Monkeydo</h1>
<p>Monkeydo comes as an importable ECMAScript 6 module. In this guide we'll import this directly from a <i>./modules/</i> folder, but any web-accesible location will work.</p>
<ol>
<li><strong>Import <code>Monkeydo</code> as an ES6 module</strong>
<pre lang="js">
import { default as Monkeydo } from "./modules/Monkeydo/Monkeydo.mjs";
</pre>
</li>
<li><strong>Define your JS methods in an object</strong>
<pre lang="js">
const methods = {
myJavaScriptMethod: (foo,bar) => {
console.log(foo,bar);
}
}
</pre>
</li>
<li><strong>Define your tasks in a JSON manifest (file or JSON-compatible JavaScript)</strong>
<pre lang="json">
{
"tasks": [
[0,"myJavaSriptMethod","Just like a","monkey"],
[1200,"myJavaSriptMethod","I've been","dancing"],
[160,"myJavaSriptMethod","my whole","life"]
]
}
</pre>
</li>
<li><strong>Initialize and run <code>Monkeydo</code> with your methods and manifest</strong>
<pre lang="js">
const monkey = new Monkeydo(methods);
monkey.play(manifest);
</pre>
</li>
</ol>
<p>The example above would be the same as running:</p>
<pre lang="js">
console.log("Just like a","monkey"); // Right away
console.log("I've been","dancing"); // 1.2 seconds after the first
console.log("my whole","life"); // and then 160 milliseconds after the second
</pre>

View file

@ -5,44 +5,55 @@ importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
class Monkey { class Monkey {
constructor() { constructor() {
this.flags = new Uint8ClampedArray(3); this.flags = new Uint8ClampedArray(3);
this.tasks = [];
this.tasksLength = 0; this.tasks = {
this.i = 0; tasks: [],
// Runtime task queue length: 0,
this.queue = { _target: 0,
thisTask: null, _i: 0,
nextTask: null set manifest(manifest) {
this.tasks = manifest;
this.length = this.tasks.length - 1;
},
get task() {
return this.tasks[this._i];
},
get target() {
return this._target;
}
} }
} }
// Task scheduler // Advance to the next task or loop
next() { next() {
if(this.flags[0] === 0 || this.flags[2] === 0) return this.abort(); // Reset index and loop if out of tasks
const task = this.tasks[this.i]; if(this.tasks._i >= this.tasks.length) {
this.tasks._i = -1;
// Run task after delay if(this.flags[1] === 255) return; // Loop forever
this.queue.thisTask = setTimeout(() => { this.flags[2] -= 1;
// Dispatch task to main thread
postMessage(["TASK",task]);
this.i++;
},task[0]);
// Loop until flag is 0 or infinite if 255
if(this.i === this.tasksLength) {
this.i = -1;
if(this.flags[1] < 255) this.flags[2]--;
} }
this.tasks._i++;
const nextTask = this.tasks.task;
this.tasks._target = performance.now() + nextTask[0];
}
// Queue the next task // Main event loop, runs on every frame
this.queue.nextTask = setTimeout(() => this.next(),task[0]); tick() {
if(this === undefined) return;
if(this.flags[0] === 0 || this.flags[2] === 0) return this.abort();
const frame = Math.min(performance.now(),this.tasks.target);
if(frame == this.tasks.target) {
postMessage(["TASK",this.tasks.task]);
this.next();
}
requestAnimationFrame(this.tick.bind(this));
} }
abort() { abort() {
this.flags[2] = 0; // Playing: false this.flags[2] = 0; // Playing: false
clearTimeout(this.queue.thisTask);
clearTimeout(this.queue.nextTask);
this.queue.thisTask = null;
this.queue.nextTask = null;
} }
// Set or get a runtime flag // Set or get a runtime flag
@ -64,21 +75,17 @@ class Monkey {
// Install a Monkeydo manifest // Install a Monkeydo manifest
async loadManifest(manifest) { async loadManifest(manifest) {
return await new Promise((resolve,reject) => { if(typeof manifest !== "object") {
if(typeof manifest !== "object") { try {
try { manifest = JSON.parse(manifest);
manifest = JSON.parse(manifest);
}
catch {
reject("Failed to load manifest");
}
} }
this.tasks = manifest.tasks; catch {
// Store length as property so we don't have to calculate the offset each iteration of next() Promise.reject("Failed to load manifest");
this.tasksLength = manifest.tasks.length - 1; }
this.flags[0] = 1; // Manifest loaded: true }
resolve(); this.tasks.manifest = manifest.tasks;
}); this.flags[0] = 1; // Manifest loaded: true
return true;
} }
} }

View file

@ -47,14 +47,13 @@ export default class MonkeyMaster {
const Monkey = Comlink.wrap(worker); const Monkey = Comlink.wrap(worker);
this.comlink = await new Monkey(); this.comlink = await new Monkey();
// Wait for comlink to initialize proxy and send queued flags // Wait for comlink to spin up
return await new Promise((resolve,reject) => { if(!this.comlink) Promise.reject("Failed to establish Comlink with worker");
if(!this.comlink) reject("Failed to open proxy to worker");
this.ready = true; this.ready = true;
this.queue.sendAllFlags(); // Send queued flags when worker is ready
resolve(); this.queue.sendAllFlags();
}); return true;
} }
// Return a flag array index by name // Return a flag array index by name
@ -103,21 +102,20 @@ export default class MonkeyMaster {
// Load a Monkeydo manifest by URL or JSON string // Load a Monkeydo manifest by URL or JSON string
async loadManifest(manifest) { async loadManifest(manifest) {
if(!this.ready) await this.init(); if(!this.ready) await this.init();
return await new Promise((resolve,reject) => { let load = null;
let load = null; // Attempt load string as URL and fetch manifest
// Attempt load string as URL and fetch manifest try {
try { const url = new URL(manifest);
const url = new URL(manifest); // If the URL parsed but fetch failed, this promise will reject
// If the URL parsed but fetch failed, this promise will reject load = this.comlink.fetchManifest(url.toString());
load = this.comlink.fetchManifest(url.toString()); }
} // Or attempt to load string as JSON if it's not a URL
// Or attempt to load string as JSON if it's not a URL catch {
catch { load = this.comlink.loadManifest(manifest);
load = this.comlink.loadManifest(manifest); }
}
load.then(() => resolve()) load.then(() => Promise.resolve())
.catch(() => reject("Failed to load manifest")); .catch(() => Promise.reject("Failed to load manifest"));
});
} }
async stop() { async stop() {
@ -132,6 +130,6 @@ export default class MonkeyMaster {
if(playing > 0) return; if(playing > 0) return;
await this.setFlag("playing",loop); await this.setFlag("playing",loop);
return await this.comlink.next(); return await this.comlink.tick();
} }
} }