From 1f4c2f8c5a0d21599da4fc0cb258281ebda77ae9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 10:52:21 +0200 Subject: [PATCH] draw scheduler in vanilla js --- .../examples/vite-vanilla-repl-cm6/drawer.js | 91 +++++++++++++++++++ .../vite-vanilla-repl-cm6/highlighter.js | 44 --------- .../examples/vite-vanilla-repl-cm6/index.html | 1 + .../examples/vite-vanilla-repl-cm6/main.js | 22 +++-- .../examples/vite-vanilla-repl-cm6/style.css | 4 + packages/core/pianoroll.mjs | 8 +- 6 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/drawer.js delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/highlighter.js diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js new file mode 100644 index 00000000..3bd6fbc9 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js @@ -0,0 +1,91 @@ +const round = (x) => Math.round(x * 1000) / 1000; + +export class Framer { + constructor(onFrame, onError) { + this.onFrame = onFrame; + this.onError = onError; + } + start() { + const self = this; + let frame = requestAnimationFrame(function updateHighlights(time) { + try { + self.onFrame(time); + } catch (err) { + self.onError(err); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + } + } +} + +export class Drawer { + constructor(onDraw, drawTime) { + let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] + lookbehind = Math.abs(lookbehind); + this.visibleHaps = []; + this.lastFrame = null; + this.drawTime = drawTime; + this.framer = new Framer( + () => { + if (!this.scheduler) { + console.warn('Drawer: no scheduler'); + return; + } + // calculate current frame time (think right side of screen for pianoroll) + const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase + if (this.lastFrame === null) { + this.lastFrame = phase; + return; + } + // query haps from last frame till now. take last 100ms max + const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); + this.lastFrame = phase; + this.visibleHaps = (this.visibleHaps || []) + // filter out haps that are too far in the past (think left edge of screen for pianoroll) + .filter((h) => h.whole.end >= phase - lookbehind - lookahead) + // add new haps with onset (think right edge bars scrolling in) + .concat(haps.filter((h) => h.hasOnset())); + const time = phase - lookahead; + onDraw(this.visibleHaps, time, this); + }, + (err) => { + console.warn('draw error', err); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + // append future haps + this.visibleHaps = this.visibleHaps.concat(futureHaps); + } + start(scheduler) { + this.scheduler = scheduler; + this.invalidate(); + this.framer.start(); + } + stop() { + if (this.framer) { + this.framer.stop(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js deleted file mode 100644 index 0a306e70..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js +++ /dev/null @@ -1,44 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -// this class can be used to create a code highlighter -// it is encapsulated from the editor via the onUpdate callback -// the scheduler is expected to be an instance of Cyclist -export class Highlighter { - constructor(onUpdate) { - this.onUpdate = onUpdate; - } - start(scheduler) { - let highlights = []; - let lastEnd = 0; - this.stop(); - const self = this; - let frame = requestAnimationFrame(function updateHighlights() { - try { - const time = scheduler.now(); - // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away - // see https://github.com/tidalcycles/strudel/issues/108 - const begin = Math.max(lastEnd ?? time, time - 1 / 10, -0.01); // negative time seems buggy - const span = [round(begin), round(time + 1 / 60)]; - lastEnd = span[1]; - highlights = highlights.filter((hap) => hap.whole.end > time); // keep only highlights that are still active - const haps = scheduler.pattern - .queryArc(...span) - .filter((hap) => hap.hasOnset()); - highlights = highlights.concat(haps); // add potential new onsets - self.onUpdate(highlights); // highlight all still active + new active haps - } catch (err) { - self.onUpdate([]); - } - frame = requestAnimationFrame(updateHighlights); - }); - self.cancel = () => { - cancelAnimationFrame(frame); - }; - } - stop() { - if (this.cancel) { - this.cancel(); - this.onUpdate([]); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/index.html b/packages/core/examples/vite-vanilla-repl-cm6/index.html index 32381f03..1a214021 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/index.html +++ b/packages/core/examples/vite-vanilla-repl-cm6/index.html @@ -15,6 +15,7 @@
+ diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index ff10ad64..6e0ed677 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,10 +2,13 @@ import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; -import { Highlighter } from './highlighter'; +import { Drawer } from './drawer'; import { bumpStreet } from './tunes'; +import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; + let code = bumpStreet; const repl = initStrudel(); +const roll = document.getElementById('roll'); const view = initEditor({ initialCode: code, @@ -22,20 +25,27 @@ async function onEvaluate() { if (!scheduler.started) { scheduler.stop(); await evaluate(code); - highlighter.start(scheduler); + drawer.start(scheduler); } else { await evaluate(code); + drawer.invalidate(); // this is a bit mystic } } async function onStop() { const { scheduler } = await repl; scheduler.stop(); - highlighter.stop(); + drawer.stop(); } - -let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); +const ctx = roll.getContext('2d'); +let drawer = new Drawer( + (haps, time, { drawTime }) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + highlightHaps(view, currentFrame); + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 1 }) }); + }, + [-2, 2], +); document.getElementById('play').addEventListener('click', () => onEvaluate()); - document.getElementById('stop').addEventListener('click', async () => onStop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 20970778..fed3f86a 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -22,3 +22,7 @@ main { .container { flex-grow: 1; } + +#roll { + height: 300px; +} diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 7f5f2fec..b0f4c074 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -156,7 +156,7 @@ export function pianoroll({ return this; } -function getOptions(drawTime, options = {}) { +export function getDrawOptions(drawTime, options = {}) { let [lookbehind, lookahead] = drawTime; lookbehind = Math.abs(lookbehind); const cycles = lookahead + lookbehind; @@ -165,11 +165,13 @@ function getOptions(drawTime, options = {}) { } Pattern.prototype.punchcard = function (options) { - return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) })); + return this.onPaint((ctx, time, haps, drawTime) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }), + ); }; Pattern.prototype.pianoroll = function (options) { return this.onPaint((ctx, time, haps, drawTime) => - pianoroll({ ctx, time, haps, ...getOptions(drawTime, { fold: 0, ...options }) }), + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); };