// 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; } } // Sync delays with media element this.media = { _timeStamp: null, get time() { return this._timeStamp; }, set time(timeStamp) { timeStamp = Math.floor(timeStamp); this._timeStamp = timeStamp; } }; } // 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; const currentTime = this.media.time ? this.media.time : performance.now(); this.tasks._target = currentTime + 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]; } // Use HTMLMediaElement.currentTime instead of performance.now() setMedia(value = null) { if(value instanceof HTMLMediaElement !== true) value = null; this._media = value; } // 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) { return await new Promise((resolve,reject) => { if(typeof manifest !== "object") { try { manifest = JSON.parse(manifest); } catch { reject("Failed to load manifest"); } } this.tasks.manifest = manifest.tasks; this.flags[0] = 1; // Manifest loaded: true resolve(); }); } } Comlink.expose(Monkey);