begin reimplementing draw logic for parallel use

This commit is contained in:
Felix Roos 2022-12-26 20:58:57 +01:00
parent 0792d0d59d
commit 2d1b62a978
9 changed files with 550 additions and 250 deletions

View File

@ -153,3 +153,131 @@ Pattern.prototype.pianoroll = function ({
); );
return this; return this;
}; };
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
export function pianoroll({
time,
haps,
cycles = 4,
playhead = 0.5,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
ctx,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
if (!autorange && fold) {
console.warn('disabling autorange has no effect when fold is enabled');
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
// onQuery
const { min, max, values } = haps.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => a - b);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
return this;
}

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,15 @@
import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react"; import l, { useCallback as M, useRef as E, useEffect as C, useMemo as B, useState as _, useLayoutEffect as W } from "react";
import X from "@uiw/react-codemirror"; import Y from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view"; import { Decoration as y, EditorView as $ } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state"; import { StateEffect as G, StateField as J } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript"; import { javascript as Z } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight"; import { tags as s } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes"; import { createTheme as ee } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview"; import { repl as te, pianoroll as re } from "@strudel.cycles/core";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio"; import { webaudioOutput as oe, getAudioContext as ne } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core"; import { useInView as ae } from "react-hook-inview";
import { transpiler as ne } from "@strudel.cycles/transpiler"; import { transpiler as se } from "@strudel.cycles/transpiler";
const ae = Z({ const ce = ee({
theme: "dark", theme: "dark",
settings: { settings: {
background: "#222", background: "#222",
@ -22,299 +22,364 @@ const ae = Z({
gutterForeground: "#8a919966" gutterForeground: "#8a919966"
}, },
styles: [ styles: [
{ tag: r.keyword, color: "#c792ea" }, { tag: s.keyword, color: "#c792ea" },
{ tag: r.operator, color: "#89ddff" }, { tag: s.operator, color: "#89ddff" },
{ tag: r.special(r.variableName), color: "#eeffff" }, { tag: s.special(s.variableName), color: "#eeffff" },
{ tag: r.typeName, color: "#c3e88d" }, { tag: s.typeName, color: "#c3e88d" },
{ tag: r.atom, color: "#f78c6c" }, { tag: s.atom, color: "#f78c6c" },
{ tag: r.number, color: "#c3e88d" }, { tag: s.number, color: "#c3e88d" },
{ tag: r.definition(r.variableName), color: "#82aaff" }, { tag: s.definition(s.variableName), color: "#82aaff" },
{ tag: r.string, color: "#c3e88d" }, { tag: s.string, color: "#c3e88d" },
{ tag: r.special(r.string), color: "#c3e88d" }, { tag: s.special(s.string), color: "#c3e88d" },
{ tag: r.comment, color: "#7d8799" }, { tag: s.comment, color: "#7d8799" },
{ tag: r.variableName, color: "#c792ea" }, { tag: s.variableName, color: "#c792ea" },
{ tag: r.tagName, color: "#c3e88d" }, { tag: s.tagName, color: "#c3e88d" },
{ tag: r.bracket, color: "#525154" }, { tag: s.bracket, color: "#525154" },
{ tag: r.meta, color: "#ffcb6b" }, { tag: s.meta, color: "#ffcb6b" },
{ tag: r.attributeName, color: "#c792ea" }, { tag: s.attributeName, color: "#c792ea" },
{ tag: r.propertyName, color: "#c792ea" }, { tag: s.propertyName, color: "#c792ea" },
{ tag: r.className, color: "#decb6b" }, { tag: s.className, color: "#decb6b" },
{ tag: r.invalid, color: "#ffffff" } { tag: s.invalid, color: "#ffffff" }
] ]
}); });
const B = $.define(), se = G.define({ const O = G.define(), ie = J.define({
create() { create() {
return E.none; return y.none;
}, },
update(e, t) { update(e, t) {
try { try {
for (let o of t.effects) for (let r of t.effects)
if (o.is(B)) if (r.is(O))
if (o.value) { if (r.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } }); const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]); e = y.set([a.range(0, t.newDoc.length)]);
} else } else
e = E.set([]); e = y.set([]);
return e; return e;
} catch (o) { } catch (r) {
return console.warn("flash error", o), e; return console.warn("flash error", r), e;
} }
}, },
provide: (e) => U.decorations.from(e) provide: (e) => $.decorations.from(e)
}), ce = (e) => { }), le = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => { e.dispatch({ effects: O.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) }); e.dispatch({ effects: O.of(!1) });
}, 200); }, 200);
}, z = $.define(), ie = G.define({ }, H = G.define(), ue = J.define({
create() { create() {
return E.none; return y.none;
}, },
update(e, t) { update(e, t) {
try { try {
for (let o of t.effects) for (let r of t.effects)
if (o.is(z)) { if (r.is(H)) {
const a = o.value.map( const a = r.value.map(
(s) => (s.context.locations || []).map(({ start: u, end: d }) => { (n) => (n.context.locations || []).map(({ start: c, end: i }) => {
const f = s.context.color || "#FFCA28"; const d = n.context.color || "#FFCA28";
let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(d.line).from + d.column; let o = t.newDoc.line(c.line).from + c.column, u = t.newDoc.line(i.line).from + i.column;
const m = t.newDoc.length; const m = t.newDoc.length;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${f};` } }).range(c, i); return o > m || u > m ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(o, u);
}) })
).flat().filter(Boolean) || []; ).flat().filter(Boolean) || [];
e = E.set(a, !0); e = y.set(a, !0);
} }
return e; return e;
} catch { } catch {
return E.set([]); return y.set([]);
} }
}, },
provide: (e) => U.decorations.from(e) provide: (e) => $.decorations.from(e)
}), le = [Y(), ae, ie, se]; }), de = [Z(), ce, ue, ie];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) { function fe({ value: e, onChange: t, onViewChanged: r, onSelectionChange: a, options: n, editorDidMount: c }) {
const d = _( const i = M(
(i) => { (u) => {
t?.(i); t?.(u);
}, },
[t] [t]
), f = _( ), d = M(
(i) => { (u) => {
o?.(i); r?.(u);
}, },
[o] [r]
), c = _( ), o = M(
(i) => { (u) => {
i.selectionSet && a && a?.(i.state.selection); u.selectionSet && a && a?.(u.state.selection);
}, },
[a] [a]
); );
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, { return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Y, {
value: e, value: e,
onChange: d, onChange: i,
onCreateEditor: f, onCreateEditor: d,
onUpdate: c, onUpdate: o,
extensions: le extensions: de
})); }));
} }
function K(...e) { function I(...e) {
return e.filter(Boolean).join(" "); return e.filter(Boolean).join(" ");
} }
function ue({ view: e, pattern: t, active: o, getTime: a }) { function me({ view: e, pattern: t, active: r, getTime: a }) {
const s = H([]), u = H(); const n = E([]), c = E();
L(() => { C(() => {
if (e) if (e)
if (t && o) { if (t && r) {
let d = requestAnimationFrame(function f() { let i = requestAnimationFrame(function d() {
try { try {
const c = a(), m = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60]; const o = a(), m = [Math.max(c.current || o, o - 1 / 10, 0), o + 1 / 60];
u.current = m[1], s.current = s.current.filter((g) => g.whole.end > c); c.current = m[1], n.current = n.current.filter((v) => v.whole.end > o);
const h = t.queryArc(...m).filter((g) => g.hasOnset()); const g = t.queryArc(...m).filter((v) => v.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) }); n.current = n.current.concat(g), e.dispatch({ effects: H.of(n.current) });
} catch { } catch {
e.dispatch({ effects: z.of([]) }); e.dispatch({ effects: H.of([]) });
} }
d = requestAnimationFrame(f); i = requestAnimationFrame(d);
}); });
return () => { return () => {
cancelAnimationFrame(d); cancelAnimationFrame(i);
}; };
} else } else
s.current = [], e.dispatch({ effects: z.of([]) }); n.current = [], e.dispatch({ effects: H.of([]) });
}, [t, o, e]); }, [t, r, e]);
} }
const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = { function ge(e, t = !1) {
container: fe, const r = E(), a = E(), n = (d) => {
header: me, if (a.current !== void 0) {
buttons: ge, const o = d - a.current;
button: pe, e(d, o);
buttonDisabled: he, }
error: be, a.current = d, r.current = requestAnimationFrame(n);
body: ve }, c = () => {
}; r.current = requestAnimationFrame(n);
function O({ type: e }) { }, i = () => {
return /* @__PURE__ */ n.createElement("svg", { r.current && cancelAnimationFrame(r.current), delete r.current;
};
return C(() => {
r.current && (i(), c());
}, [e]), C(() => (t && c(), i), []), {
start: c,
stop: i
};
}
function pe({ pattern: e, started: t, getTime: r, onDraw: a }) {
let n = E([]), c = E(null);
const { start: i, stop: d } = ge(
M(() => {
const o = r();
if (c.current === null) {
c.current = o;
return;
}
const u = e.queryArc(Math.max(c.current, o - 1 / 10), o), m = 4;
c.current = o, n.current = (n.current || []).filter((g) => g.whole.end > o - m).concat(u.filter((g) => g.hasOnset())), a(o, n.current);
}, [e])
);
C(() => {
t ? i() : (n.current = [], d());
}, [t]);
}
function he(e) {
return C(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), M((t) => window.postMessage(t, "*"), []);
}
function ve({
defaultOutput: e,
interval: t,
getTime: r,
evalOnMount: a = !1,
initialCode: n = "",
autolink: c = !1,
beforeEval: i,
afterEval: d,
onEvalError: o,
onToggle: u,
canvasId: m
}) {
const g = B(() => be(), []);
m = m || `canvas-${g}`;
const [v, F] = _(), [k, A] = _(), [b, D] = _(n), [x, R] = _(), [S, z] = _(), [N, T] = _(!1), L = b !== x, { scheduler: f, evaluate: h, start: V, stop: K, pause: Q } = B(
() => te({
interval: t,
defaultOutput: e,
onSchedulerError: F,
onEvalError: (p) => {
A(p), o?.(p);
},
getTime: r,
transpiler: se,
beforeEval: ({ code: p }) => {
D(p), i?.();
},
afterEval: ({ pattern: p, code: q }) => {
R(q), z(p), A(), F(), c && (window.location.hash = "#" + encodeURIComponent(btoa(q))), d?.();
},
onToggle: (p) => {
T(p), u?.(p);
}
}),
[e, t, r]
), X = he(({ data: { from: p, type: q } }) => {
q === "start" && p !== g && K();
}), P = M(
async (p = !0) => {
await h(b, p), X({ type: "start", from: g });
},
[h, b]
), j = E();
return C(() => {
!j.current && a && b && (j.current = !0, P());
}, [P, a, b]), C(() => () => {
f.stop();
}, [f]), {
id: g,
canvasId: m,
code: b,
setCode: D,
error: v || k,
schedulerError: v,
scheduler: f,
evalError: k,
evaluate: h,
activateCode: P,
activeCode: x,
isDirty: L,
pattern: S,
started: N,
start: V,
stop: K,
pause: Q,
togglePlay: async () => {
N ? f.pause() : await P();
}
};
}
function be() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
function U({ type: e }) {
return /* @__PURE__ */ l.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5", className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20", viewBox: "0 0 20 20",
fill: "currentColor" fill: "currentColor"
}, { }, {
refresh: /* @__PURE__ */ n.createElement("path", { refresh: /* @__PURE__ */ l.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
play: /* @__PURE__ */ n.createElement("path", { play: /* @__PURE__ */ l.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
pause: /* @__PURE__ */ n.createElement("path", { pause: /* @__PURE__ */ l.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd" clipRule: "evenodd"
}) })
}[e]); }[e]);
} }
function Ee(e) { const we = "_container_3i85k_1", ye = "_header_3i85k_5", Ee = "_buttons_3i85k_9", ke = "_button_3i85k_9", _e = "_buttonDisabled_3i85k_17", Ce = "_error_3i85k_21", Fe = "_body_3i85k_25", w = {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []); container: we,
} header: ye,
function we({ buttons: Ee,
defaultOutput: e, button: ke,
interval: t, buttonDisabled: _e,
getTime: o, error: Ce,
evalOnMount: a = !1, body: Fe
initialCode: s = "", }, Ne = () => ne().currentTime;
autolink: u = !1, function Be({
beforeEval: d, tune: e,
afterEval: f, hideOutsideView: t = !1,
onEvalError: c, init: r,
onToggle: i enableKeyboard: a,
withCanvas: n = !1,
canvasHeight: c = 200
}) { }) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), u && (window.location.hash = "#" + encodeURIComponent(btoa(P))), f?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const { const {
code: s, code: i,
setCode: u, setCode: d,
evaluate: d, evaluate: o,
activateCode: f, activateCode: u,
error: c, error: m,
isDirty: i, isDirty: g,
activeCode: m, activeCode: v,
pattern: h, pattern: F,
started: g, started: k,
scheduler: C, scheduler: A,
togglePlay: N, togglePlay: b,
stop: p stop: D,
} = we({ canvasId: x
} = ve({
initialCode: e, initialCode: e,
defaultOutput: te, defaultOutput: oe,
getTime: ke getTime: Ne
}), [y, M] = w(), [S, k] = ee({ });
threshold: 0.01 pe({
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]); pattern: F,
return ue({ started: k,
view: y, getTime: () => A.now(),
pattern: h, onDraw: (f, h) => {
active: g && !m?.includes("strudel disable-highlighting"), const V = document.querySelector("#" + x).getContext("2d");
getTime: () => C.getPhase() re({ ctx: V, time: f, haps: h, autorange: 1, fold: 1, playhead: 1 });
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await f()) : b.code === "Period" && (p(), b.preventDefault()));
};
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
} }
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", { });
className: v.container, const [R, S] = _(), [z, N] = ae({
ref: S threshold: 0.01
}, /* @__PURE__ */ n.createElement("div", { }), T = E(), L = B(() => ((N || !t) && (T.current = !0), N || T.current), [N, t]);
className: v.header return me({
}, /* @__PURE__ */ n.createElement("div", { view: R,
className: v.buttons pattern: F,
}, /* @__PURE__ */ n.createElement("button", { active: k && !v?.includes("strudel disable-highlighting"),
className: K(v.button, g ? "sc-animate-pulse" : ""), getTime: () => A.getPhase()
onClick: () => N() }), W(() => {
}, /* @__PURE__ */ n.createElement(O, { if (a) {
type: g ? "pause" : "play" const f = async (h) => {
})), /* @__PURE__ */ n.createElement("button", { (h.ctrlKey || h.altKey) && (h.code === "Enter" ? (h.preventDefault(), le(R), await u()) : h.code === "Period" && (D(), h.preventDefault()));
className: K(i ? v.button : v.buttonDisabled), };
onClick: () => f() return window.addEventListener("keydown", f, !0), () => window.removeEventListener("keydown", f, !0);
}, /* @__PURE__ */ n.createElement(O, { }
}, [a, F, i, o, D, R]), /* @__PURE__ */ l.createElement("div", {
className: w.container,
ref: z
}, /* @__PURE__ */ l.createElement("div", {
className: w.header
}, /* @__PURE__ */ l.createElement("div", {
className: w.buttons
}, /* @__PURE__ */ l.createElement("button", {
className: I(w.button, k ? "sc-animate-pulse" : ""),
onClick: () => b()
}, /* @__PURE__ */ l.createElement(U, {
type: k ? "pause" : "play"
})), /* @__PURE__ */ l.createElement("button", {
className: I(g ? w.button : w.buttonDisabled),
onClick: () => u()
}, /* @__PURE__ */ l.createElement(U, {
type: "refresh" type: "refresh"
}))), c && /* @__PURE__ */ n.createElement("div", { }))), m && /* @__PURE__ */ l.createElement("div", {
className: v.error className: w.error
}, c.message)), /* @__PURE__ */ n.createElement("div", { }, m.message)), /* @__PURE__ */ l.createElement("div", {
className: v.body className: w.body
}, F && /* @__PURE__ */ n.createElement(de, { }, L && /* @__PURE__ */ l.createElement(fe, {
value: s, value: i,
onChange: u, onChange: d,
onViewChanged: M onViewChanged: S
}))); })), n && /* @__PURE__ */ l.createElement("canvas", {
id: x,
className: "w-full pointer-events-none",
height: c,
ref: (f) => {
f && f.width !== f.clientWidth && (f.width = f.clientWidth);
}
}));
} }
const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); const Oe = (e) => W(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export { export {
de as CodeMirror, fe as CodeMirror,
Se as MiniRepl, Be as MiniRepl,
K as cx, I as cx,
ce as flash, le as flash,
ue as useHighlighting, me as useHighlighting,
Te as useKeydown, Oe as useKeydown,
Ee as usePostMessage, he as usePostMessage,
we as useStrudel ve as useStrudel
}; };

View File

@ -1,18 +1,20 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; import { pianoroll } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useInView } from 'react-hook-inview'; import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx'; import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs'; import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6'; import usePatternFrame from '../hooks/usePatternFrame.mjs';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import useStrudel from '../hooks/useStrudel.mjs'; import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import './style.css';
const getTime = () => getAudioContext().currentTime; const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) { export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
const { const {
code, code,
setCode, setCode,
@ -26,11 +28,23 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
scheduler, scheduler,
togglePlay, togglePlay,
stop, stop,
canvasId,
} = useStrudel({ } = useStrudel({
initialCode: tune, initialCode: tune,
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
getTime, getTime,
}); });
usePatternFrame({
pattern,
started,
getTime: () => scheduler.now(),
onDraw: (time, haps) => {
const ctx = document.querySelector('#' + canvasId).getContext('2d');
pianoroll({ ctx, time, haps, autorange: 1, fold: 1, playhead: 1 });
},
});
/* useEffect(() => { /* useEffect(() => {
init && activateCode(); init && activateCode();
}, [init, activateCode]); */ }, [init, activateCode]); */
@ -88,6 +102,18 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
<div className={styles.body}> <div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />} {show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div> </div>
{withCanvas && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
function useFrame(callback, autostart = false) {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(time, deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
const start = () => {
requestRef.current = requestAnimationFrame(animate);
};
const stop = () => {
requestRef.current && cancelAnimationFrame(requestRef.current);
delete requestRef.current;
};
useEffect(() => {
if (requestRef.current) {
stop();
start();
}
}, [callback]);
useEffect(() => {
if (autostart) {
start();
}
return stop;
}, []); // Make sure the effect only runs once
return {
start,
stop,
};
}
export default useFrame;

View File

@ -0,0 +1,34 @@
import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw }) {
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime();
if (lastFrame.current === null) {
lastFrame.current = phase;
return;
}
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
const cycles = 4;
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end > phase - cycles) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(phase, visibleHaps.current);
}, [pattern]),
);
useEffect(() => {
if (started) {
startFrame();
} else {
visibleHaps.current = [];
stopFrame();
}
}, [started]);
}
export default usePatternFrame;

View File

@ -14,8 +14,10 @@ function useStrudel({
afterEval, afterEval,
onEvalError, onEvalError,
onToggle, onToggle,
canvasId,
}) { }) {
const id = useMemo(() => s4(), []); const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
// scheduler // scheduler
const [schedulerError, setSchedulerError] = useState(); const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState(); const [evalError, setEvalError] = useState();
@ -97,6 +99,8 @@ function useStrudel({
}; };
const error = schedulerError || evalError; const error = schedulerError || evalError;
return { return {
id,
canvasId,
code, code,
setCode, setCode,
error, error,

View File

@ -22,7 +22,7 @@ if (typeof window !== 'undefined') {
prebake(); prebake();
} }
export function MiniRepl({ tune }) { export function MiniRepl({ tune, withCanvas }) {
const [Repl, setRepl] = useState(); const [Repl, setRepl] = useState();
useEffect(() => { useEffect(() => {
// we have to load this package on the client // we have to load this package on the client
@ -31,5 +31,5 @@ export function MiniRepl({ tune }) {
setRepl(() => res.MiniRepl); setRepl(() => res.MiniRepl);
}); });
}, []); }, []);
return Repl ? <Repl tune={tune} hideOutsideView={true} /> : <pre>{tune}</pre>; return Repl ? <Repl tune={tune} hideOutsideView={true} withCanvas={withCanvas} /> : <pre>{tune}</pre>;
} }

View File

@ -15,7 +15,7 @@ Here's the same pattern written in three different ways:
- `note`: letter notation, good for those who are familiar with western music theory: - `note`: letter notation, good for those who are familiar with western music theory:
<MiniRepl client:idle tune={`note("a3 c#4 e4 a4")`} /> <MiniRepl client:idle tune={`note("a3 c#4 e4 a4")`} withCanvas />
- `n`: number notation, good for those who want to use recognisable pitches, but don't care about music theory: - `n`: number notation, good for those who want to use recognisable pitches, but don't care about music theory: