mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
draw scheduler in vanilla js
This commit is contained in:
parent
95719654f3
commit
1f4c2f8c5a
91
packages/core/examples/vite-vanilla-repl-cm6/drawer.js
Normal file
91
packages/core/examples/vite-vanilla-repl-cm6/drawer.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@
|
||||
<div id="editor"></div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<canvas id="roll"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -22,3 +22,7 @@ main {
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#roll {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
@ -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 }) }),
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user