mirror of
https://codeberg.org/vlw/monkeydo.git
synced 2025-09-13 23:53:41 +02:00
Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6b18c90228 | ||
![]() |
46f3a07c85 | ||
![]() |
a6e51c1e48 |
4 changed files with 148 additions and 114 deletions
|
@ -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
121
README.md
|
@ -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>
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue