mirror of
https://codeberg.org/vlw/monkeydo.git
synced 2025-09-14 16:13:41 +02:00
Compare commits
No commits in common. "master" and "0.2.2" have entirely different histories.
6 changed files with 354 additions and 334 deletions
113
Monkeydo.mjs
113
Monkeydo.mjs
|
@ -1,43 +1,96 @@
|
||||||
import { default as MonkeyMaster } from "./monkey/MonkeyMaster.mjs";
|
import { default as MonkeyWorker } from "./do/MonkeyManager.mjs";
|
||||||
|
|
||||||
export default class Monkeydo extends MonkeyMaster {
|
export default class Monkeydo extends MonkeyWorker {
|
||||||
constructor(methods) {
|
constructor(methods = {}) {
|
||||||
if(typeof methods !== "object") {
|
super(methods);
|
||||||
throw new TypeError(`Expected type 'object' but got '${typeof methods}' when initializing Monkeydo`);
|
this.monkeydo = {
|
||||||
|
version: "0.2.1",
|
||||||
|
debugLevel: 0,
|
||||||
|
// Flag if debugging is enabled, regardless of level
|
||||||
|
get debug() {
|
||||||
|
return this.debugLevel > 0 ? true : false;
|
||||||
|
},
|
||||||
|
// Set debug level. Non-verbose debugging if called without an argument
|
||||||
|
set debug(level = 1) {
|
||||||
|
this.debugLevel = level;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.seal(this.monkeydo);
|
||||||
|
|
||||||
|
// Monkeydo manifest parsed with load()
|
||||||
|
this.manifest = {
|
||||||
|
tasks: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!window.Worker) {
|
||||||
|
throw new Error("JavaScript Workers aren't supported by your browser");
|
||||||
}
|
}
|
||||||
super();
|
|
||||||
this.methods = {};
|
|
||||||
Object.assign(this.methods,methods);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute a task
|
debug(...attachments) {
|
||||||
do(task) {
|
if(this.monkeydo.debug) {
|
||||||
if(!task[1] in this.methods) return;
|
console.warn("-- Monkeydo debug -->",attachments);
|
||||||
const args = task.splice(2);
|
return;
|
||||||
this.methods[task[1]]?.(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop playback X times or negative number for infinite
|
|
||||||
async loop(times = 255) {
|
|
||||||
if(typeof times !== "number") {
|
|
||||||
times = parseInt(times);
|
|
||||||
}
|
}
|
||||||
times = Math.floor(times);
|
|
||||||
times = Math.min(Math.max(times,0),255); // Clamp number to 8 bits
|
|
||||||
return await this.setFlag("loop",times);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Monkeydo manifest
|
// Loop playback; -1 or false = infinite
|
||||||
|
loop(times = -1) {
|
||||||
|
// Typecast boolean to left shifted integer;
|
||||||
|
if(typeof times === "boolean") {
|
||||||
|
times = times ? -1 : 0;
|
||||||
|
}
|
||||||
|
times = times < 0 ? -1 : times;
|
||||||
|
this.setFlag("loop",times);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a Monkeydo manifest from JSON via string or URL
|
||||||
async load(manifest) {
|
async load(manifest) {
|
||||||
if(typeof manifest === "object") {
|
const errorPrefix = "MANIFEST_IMPORT_FAILED: ";
|
||||||
manifest = JSON.stringify(manifest);
|
let data;
|
||||||
|
if(typeof manifest !== "string") {
|
||||||
|
this.debug(manifest);
|
||||||
|
throw new TypeError(errorPrefix + "Expected JSON or URL");
|
||||||
}
|
}
|
||||||
return await this.loadManifest(manifest);
|
|
||||||
|
// Attempt to parse the argument as JSON
|
||||||
|
try {
|
||||||
|
data = JSON.parse(manifest);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// If that fails, attempt to parse it as a URL
|
||||||
|
try {
|
||||||
|
manifest = new URL(manifest);
|
||||||
|
const fetchManifest = await fetch(manifest);
|
||||||
|
|
||||||
|
// If the URL parsed but the fetch response is invalid, give up and throw an error
|
||||||
|
if(!fetchManifest.ok || fetchManifest.headers.get("Content-Type") !== "application/json") {
|
||||||
|
throw new TypeError(errorPrefix + "Invalid response Content-Type or HTTP status");
|
||||||
|
}
|
||||||
|
data = await fetchManifest.json();
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
this.debug(manifest);
|
||||||
|
if(!error instanceof TypeError) {
|
||||||
|
throw new TypeError(errorPrefix + "Invalid JSON or URL");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the parsed JSON is a valid Monkeydo manifest
|
||||||
|
if(!data.hasOwnProperty("tasks")) {
|
||||||
|
this.debug(data);
|
||||||
|
throw new Error(errorPrefix + "Expected 'header' and 'body' properties in object");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manifest.tasks = data.tasks;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async play(manifest = null) {
|
// Execute tasks from Monkeydo manifest
|
||||||
if(!this.ready && !manifest) throw new Error("Can not start playback without a manifest");
|
do() {
|
||||||
if(manifest) await this.load(manifest);
|
// Hand over the loaded manifest to the MonkeyWorker task manager
|
||||||
return await this.start();
|
this.giveManifest().then(() => this.play());
|
||||||
}
|
}
|
||||||
}
|
}
|
123
README.md
123
README.md
|
@ -1,83 +1,9 @@
|
||||||
<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">Multithreaded web animations and task chaining</h3>
|
<h3 align="center">Threaded task chaining for JavaScript</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<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">Monkeydo uses the portable data format JSON to read tasks, making it easy to read by primates and machines alike.</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">
|
||||||
|
@ -96,7 +22,7 @@ console.log("my whole","life"); // and then 160 milliseconds after the second
|
||||||
</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 method</td>
|
<td><strong>Delay</strong><br>Wait this many milliseconds before running this task</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">1</td>
|
<td align="center">1</td>
|
||||||
|
@ -109,3 +35,46 @@ console.log("my whole","life"); // and then 160 milliseconds after the second
|
||||||
</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>Import <code>Monkeydo</code> from your repo clone or download
|
||||||
|
<pre lang="js">
|
||||||
|
import { default } from "./modules/Monkeydo/Monkeydo.mjs";
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
<li>Define your JS methods
|
||||||
|
<pre lang="js">
|
||||||
|
const methods = {
|
||||||
|
myJavaScriptMethod: (foo,bar) => {
|
||||||
|
console.log(foo,bar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
<li>Define your tasks in a JSON file (or directly in JavaScript)
|
||||||
|
<pre lang="json">
|
||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
[0,"myJavaSriptMethod","I see trees of","green"],
|
||||||
|
[300,"myJavaSriptMethod","red","roses too"],
|
||||||
|
[160,"myJavaSriptMethod","I see them","bloom"],
|
||||||
|
[1200,"myJavaSriptMethod","for","me and you"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
<li>Initialize and run <code>Monkeydo</code> with your methods and manifest
|
||||||
|
<pre lang="js">
|
||||||
|
const monkey = new Monkeydo(methods,manifest);
|
||||||
|
monkey.do();
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p>The example above would be the same as running:</p>
|
||||||
|
<pre lang="js">
|
||||||
|
console.log("I see trees of","green"); // Right away
|
||||||
|
console.log("red","roses too"); // 300 milliseconds after the first
|
||||||
|
console.log("I see them","bloom"); // 160 milliseconds after that one
|
||||||
|
console.log("for","me and you"); // and lastly, 1200 after that
|
||||||
|
</pre>
|
||||||
|
|
131
do/Monkey.js
Normal file
131
do/Monkey.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// Dedicated worker which executes tasks from a Monkeydo manifest
|
||||||
|
|
||||||
|
class Monkey {
|
||||||
|
constructor(manifest) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
this.tasks = manifest.tasks;
|
||||||
|
this.tasksLength = this.tasks.length - 1;
|
||||||
|
|
||||||
|
this.flags = {
|
||||||
|
playing: 0,
|
||||||
|
stacking: 0, // Subsequent calls to play() will build a queue (jQuery-style)
|
||||||
|
loop: 0, // Loop n times; <0 = infinite
|
||||||
|
_forwards: 1, // Playback direction
|
||||||
|
set forwards(forwards = true) {
|
||||||
|
if(forwards == this._forwards) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Toggle playback direction
|
||||||
|
self.tasks = self.tasks.reverse();
|
||||||
|
this._forwards = 1 - this._forwards;
|
||||||
|
},
|
||||||
|
get forwards() {
|
||||||
|
return this._forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.i = 0; // Manifest iterator index
|
||||||
|
this.queue = {
|
||||||
|
task: null,
|
||||||
|
next: null
|
||||||
|
}
|
||||||
|
Object.seal(this.queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass task to main thread for execution
|
||||||
|
run(task) {
|
||||||
|
postMessage(["TASK",task]);
|
||||||
|
this.i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt timeout and put monkey to sleep
|
||||||
|
interrupt() {
|
||||||
|
clearTimeout(this.queue.task);
|
||||||
|
clearTimeout(this.queue.next);
|
||||||
|
this.queue.task = null;
|
||||||
|
this.queue.next = null;
|
||||||
|
this.flags.playing = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
// Stack playback as loops if flag is set
|
||||||
|
if(this.flags.playing) {
|
||||||
|
if(this.flags.stacking && this.flags.loop >= 0) {
|
||||||
|
this.flags.loop++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queueNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule task for execution by index
|
||||||
|
queueNext() {
|
||||||
|
this.flags.playing = 1;
|
||||||
|
const data = this.tasks[this.i];
|
||||||
|
const task = {
|
||||||
|
wait: data[0],
|
||||||
|
func: data[1],
|
||||||
|
args: data.slice(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schedule the current task to run after the specified wait time
|
||||||
|
this.queue.task = setTimeout(() => this.run(task),task.wait);
|
||||||
|
|
||||||
|
// We're out of tasks to schedule..
|
||||||
|
if(this.i >= this.tasksLength) {
|
||||||
|
this.i = -1;
|
||||||
|
// Exit if we're out of loops
|
||||||
|
if(this.flags.loop === 0) {
|
||||||
|
this.flags.playing = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement loop iterations if not infinite (negative int)
|
||||||
|
if(this.flags.loop > 0) {
|
||||||
|
this.flags.loop--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run this function again when the scheduled task will fire
|
||||||
|
this.queue.next = setTimeout(() => this.queueNext(),task.wait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handler for messages received from initiator
|
||||||
|
onmessage = (message) => {
|
||||||
|
const type = message.data[0] ? message.data[0] : message.data;
|
||||||
|
const data = message.data[1];
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
// Attempt to load manfiest provided by initiator thread
|
||||||
|
case "GIVE_MANIFEST":
|
||||||
|
try {
|
||||||
|
this.monkey = new Monkey(data);
|
||||||
|
postMessage(["RECEIVED_MANIFEST","OK"]);
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
postMessage(["RECEIVED_MANIFEST",error]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SET_PLAYING":
|
||||||
|
if(data === true) {
|
||||||
|
this.monkey.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.monkey.interrupt();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GET_FLAG":
|
||||||
|
const flag = this.monkey.flags[data];
|
||||||
|
postMessage(parseInt(flag));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SET_FLAG":
|
||||||
|
this.monkey.flags[data[0]] = data[1];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: return; // No op
|
||||||
|
}
|
||||||
|
}
|
94
do/MonkeyManager.mjs
Normal file
94
do/MonkeyManager.mjs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// Task manager for Monkeydo dedicated workers
|
||||||
|
|
||||||
|
export default class MonkeyManager {
|
||||||
|
constructor(methods) {
|
||||||
|
// Object of scoped methods for this manifest
|
||||||
|
this.methods = {};
|
||||||
|
Object.assign(this.methods,methods);
|
||||||
|
|
||||||
|
// Get path of this file
|
||||||
|
let location = new URL(import.meta.url);
|
||||||
|
location = location.pathname.replace("MonkeyManager.mjs",""); // Get parent directory
|
||||||
|
|
||||||
|
// Spawn a dedicated worker for scheduling events from manifest
|
||||||
|
this.worker = new Worker(location + "Monkey.js");
|
||||||
|
this.worker.addEventListener("message",message => this.message(message));
|
||||||
|
|
||||||
|
this.reversed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a status flag from the worker
|
||||||
|
async getFlag(flag) {
|
||||||
|
this.worker.postMessage(["GET_FLAG",flag]);
|
||||||
|
const response = await new Promise((resolve) => {
|
||||||
|
this.worker.addEventListener("message",message => resolve(message.data));
|
||||||
|
});
|
||||||
|
this.debug("GET_FLAG",flag,response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a status flag for the worker
|
||||||
|
async setFlag(flag,value = 0) {
|
||||||
|
const flagExists = await this.getFlag(flag);
|
||||||
|
if(flagExists === null) {
|
||||||
|
this.debug(flagExists);
|
||||||
|
throw new Error("Flag does not not exist");
|
||||||
|
}
|
||||||
|
this.worker.postMessage(["SET_FLAG",[flag,value]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get acknowledgement from worker for a transactional operation
|
||||||
|
async ack(name) {
|
||||||
|
const status = await new Promise((resolve,reject) => {
|
||||||
|
const ack = this.worker.addEventListener("message",message => {
|
||||||
|
if(message.data[0] !== name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(message.data[1] !== "OK") {
|
||||||
|
reject(message.data);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.worker.removeEventListener("message",ack);
|
||||||
|
});
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass manifest to worker and await response from worker
|
||||||
|
async giveManifest() {
|
||||||
|
this.worker.postMessage(["GIVE_MANIFEST",this.manifest]);
|
||||||
|
const status = await this.ack("RECEIVED_MANIFEST");
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call method from object and pass arguments
|
||||||
|
run(task) {
|
||||||
|
this.methods[task.func](...task.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
this.worker.postMessage(["SET_PLAYING",true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.worker.postMessage(["SET_PLAYING",false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handler for messages received from worker
|
||||||
|
message(message) {
|
||||||
|
const type = message.data[0] ? message.data[0] : message.data;
|
||||||
|
const data = message.data[1];
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case "TASK":
|
||||||
|
this.run(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "DEBUG":
|
||||||
|
default:
|
||||||
|
this.debug("MESSAGE_FROM_WORKER",message.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,92 +0,0 @@
|
||||||
// Dedicated worker (monkey) that executes tasks from a Monkeydo manifest
|
|
||||||
|
|
||||||
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
|
|
||||||
|
|
||||||
class Monkey {
|
|
||||||
constructor() {
|
|
||||||
this.flags = new Uint8ClampedArray(3);
|
|
||||||
|
|
||||||
this.tasks = {
|
|
||||||
tasks: [],
|
|
||||||
length: 0,
|
|
||||||
_target: 0,
|
|
||||||
_i: 0,
|
|
||||||
set manifest(manifest) {
|
|
||||||
this.tasks = manifest;
|
|
||||||
this.length = this.tasks.length - 1;
|
|
||||||
},
|
|
||||||
get task() {
|
|
||||||
return this.tasks[this._i];
|
|
||||||
},
|
|
||||||
get target() {
|
|
||||||
return this._target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance to the next task or loop
|
|
||||||
next() {
|
|
||||||
// Reset index and loop if out of tasks
|
|
||||||
if(this.tasks._i >= this.tasks.length) {
|
|
||||||
this.tasks._i = -1;
|
|
||||||
if(this.flags[1] === 255) return; // Loop forever
|
|
||||||
this.flags[2] -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tasks._i++;
|
|
||||||
const nextTask = this.tasks.task;
|
|
||||||
this.tasks._target = performance.now() + nextTask[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main event loop, runs on every frame
|
|
||||||
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() {
|
|
||||||
this.flags[2] = 0; // Playing: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set or get a runtime flag
|
|
||||||
flag(index,value = null) {
|
|
||||||
return value ? this.flags[index] = value : this.flags[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch and install manifest from URL
|
|
||||||
async fetchManifest(url) {
|
|
||||||
const manifest = await fetch(url);
|
|
||||||
if(!manifest.ok) {
|
|
||||||
console.error("Monkeydo fetch error:",manifest);
|
|
||||||
throw new Error("Server responded with an error");
|
|
||||||
};
|
|
||||||
|
|
||||||
const json = await manifest.json();
|
|
||||||
return await this.loadManifest(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install a Monkeydo manifest
|
|
||||||
async loadManifest(manifest) {
|
|
||||||
if(typeof manifest !== "object") {
|
|
||||||
try {
|
|
||||||
manifest = JSON.parse(manifest);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Promise.reject("Failed to load manifest");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.tasks.manifest = manifest.tasks;
|
|
||||||
this.flags[0] = 1; // Manifest loaded: true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Comlink.expose(Monkey);
|
|
|
@ -1,135 +0,0 @@
|
||||||
// Task manager for Monkeydo dedicated workers (monkeys)
|
|
||||||
|
|
||||||
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
|
|
||||||
|
|
||||||
export default class MonkeyMaster {
|
|
||||||
constructor() {
|
|
||||||
this.comlink = null;
|
|
||||||
|
|
||||||
this.ready = false;
|
|
||||||
// Tasks will be queued here on runtime if the worker isn't ready
|
|
||||||
this.queue = {
|
|
||||||
_flags: [],
|
|
||||||
set flag(flag) {
|
|
||||||
this._flags.push(flag);
|
|
||||||
},
|
|
||||||
// Attempt to send all queued flags
|
|
||||||
sendAllFlags: () => {
|
|
||||||
// Copy flags and clear queue
|
|
||||||
const flags = [...this.queue._flags];
|
|
||||||
this.queue._flags = [];
|
|
||||||
flags.forEach(flag => this.setFlag(...flag));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import worker relative to this module
|
|
||||||
getWorkerPath() {
|
|
||||||
const name = "Monkey.js";
|
|
||||||
const url = new URL(import.meta.url);
|
|
||||||
|
|
||||||
// Replace pathname of this file with worker
|
|
||||||
const path = url.pathname.split("/");
|
|
||||||
path[path.length - 1] = name;
|
|
||||||
|
|
||||||
url.pathname = path.join("/");
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
// Spawn and wrap dedicated worker with Comlink
|
|
||||||
const worker = new Worker(this.getWorkerPath());
|
|
||||||
worker.addEventListener("message",event => {
|
|
||||||
if(event.data[0] !== "TASK") return;
|
|
||||||
this.do(event.data[1]); // Send inner array (task)
|
|
||||||
});
|
|
||||||
|
|
||||||
const Monkey = Comlink.wrap(worker);
|
|
||||||
this.comlink = await new Monkey();
|
|
||||||
|
|
||||||
// Wait for comlink to spin up
|
|
||||||
if(!this.comlink) Promise.reject("Failed to establish Comlink with worker");
|
|
||||||
|
|
||||||
this.ready = true;
|
|
||||||
// Send queued flags when worker is ready
|
|
||||||
this.queue.sendAllFlags();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a flag array index by name
|
|
||||||
flagStringToIndex(flag) {
|
|
||||||
const flags = [
|
|
||||||
"MANIFEST_LOADED",
|
|
||||||
"LOOP",
|
|
||||||
"PLAYING"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Translate string to index
|
|
||||||
if(typeof flag === "string" || flag < 0) {
|
|
||||||
flag = flags.indexOf(flag.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that key is in bounds
|
|
||||||
if(flag < 0 || flags > flags.length - 1) return false;
|
|
||||||
return flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFlag(flag) {
|
|
||||||
const key = this.flagStringToIndex(flag);
|
|
||||||
if(!key) Promise.reject("Invalid flag");
|
|
||||||
return await this.comlink.flag(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set or queue worker runtime flag
|
|
||||||
async setFlag(flag,value) {
|
|
||||||
const key = this.flagStringToIndex(flag);
|
|
||||||
if(!key) Promise.reject("Invalid flag");
|
|
||||||
|
|
||||||
// Set the flag when the worker is ready
|
|
||||||
if(!this.ready) {
|
|
||||||
this.queue.flag = [key,value];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell worker to update flag by key
|
|
||||||
const update = await this.comlink.flag(key,value);
|
|
||||||
if(!update) {
|
|
||||||
this.queue.flag = [key,value];
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load a Monkeydo manifest by URL or JSON string
|
|
||||||
async loadManifest(manifest) {
|
|
||||||
if(!this.ready) await this.init();
|
|
||||||
let load = null;
|
|
||||||
// Attempt load string as URL and fetch manifest
|
|
||||||
try {
|
|
||||||
const url = new URL(manifest);
|
|
||||||
// If the URL parsed but fetch failed, this promise will reject
|
|
||||||
load = this.comlink.fetchManifest(url.toString());
|
|
||||||
}
|
|
||||||
// Or attempt to load string as JSON if it's not a URL
|
|
||||||
catch {
|
|
||||||
load = this.comlink.loadManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
load.then(() => Promise.resolve())
|
|
||||||
.catch(() => Promise.reject("Failed to load manifest"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
return await this.comlink.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start playback of a loaded manifest
|
|
||||||
async start() {
|
|
||||||
const playing = await this.getFlag("playing");
|
|
||||||
let loop = await this.getFlag("loop");
|
|
||||||
loop = loop > 0 ? loop : 1; // Play once if loop has no value
|
|
||||||
|
|
||||||
if(playing > 0) return;
|
|
||||||
await this.setFlag("playing",loop);
|
|
||||||
return await this.comlink.tick();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue