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;
};
// 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 X from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core";
import { transpiler as ne } from "@strudel.cycles/transpiler";
const ae = Z({
import l, { useCallback as M, useRef as E, useEffect as C, useMemo as B, useState as _, useLayoutEffect as W } from "react";
import Y from "@uiw/react-codemirror";
import { Decoration as y, EditorView as $ } from "@codemirror/view";
import { StateEffect as G, StateField as J } from "@codemirror/state";
import { javascript as Z } from "@codemirror/lang-javascript";
import { tags as s } from "@lezer/highlight";
import { createTheme as ee } from "@uiw/codemirror-themes";
import { repl as te, pianoroll as re } from "@strudel.cycles/core";
import { webaudioOutput as oe, getAudioContext as ne } from "@strudel.cycles/webaudio";
import { useInView as ae } from "react-hook-inview";
import { transpiler as se } from "@strudel.cycles/transpiler";
const ce = ee({
theme: "dark",
settings: {
background: "#222",
@ -22,299 +22,364 @@ const ae = Z({
gutterForeground: "#8a919966"
},
styles: [
{ tag: r.keyword, color: "#c792ea" },
{ tag: r.operator, color: "#89ddff" },
{ tag: r.special(r.variableName), color: "#eeffff" },
{ tag: r.typeName, color: "#c3e88d" },
{ tag: r.atom, color: "#f78c6c" },
{ tag: r.number, color: "#c3e88d" },
{ tag: r.definition(r.variableName), color: "#82aaff" },
{ tag: r.string, color: "#c3e88d" },
{ tag: r.special(r.string), color: "#c3e88d" },
{ tag: r.comment, color: "#7d8799" },
{ tag: r.variableName, color: "#c792ea" },
{ tag: r.tagName, color: "#c3e88d" },
{ tag: r.bracket, color: "#525154" },
{ tag: r.meta, color: "#ffcb6b" },
{ tag: r.attributeName, color: "#c792ea" },
{ tag: r.propertyName, color: "#c792ea" },
{ tag: r.className, color: "#decb6b" },
{ tag: r.invalid, color: "#ffffff" }
{ tag: s.keyword, color: "#c792ea" },
{ tag: s.operator, color: "#89ddff" },
{ tag: s.special(s.variableName), color: "#eeffff" },
{ tag: s.typeName, color: "#c3e88d" },
{ tag: s.atom, color: "#f78c6c" },
{ tag: s.number, color: "#c3e88d" },
{ tag: s.definition(s.variableName), color: "#82aaff" },
{ tag: s.string, color: "#c3e88d" },
{ tag: s.special(s.string), color: "#c3e88d" },
{ tag: s.comment, color: "#7d8799" },
{ tag: s.variableName, color: "#c792ea" },
{ tag: s.tagName, color: "#c3e88d" },
{ tag: s.bracket, color: "#525154" },
{ tag: s.meta, color: "#ffcb6b" },
{ tag: s.attributeName, color: "#c792ea" },
{ tag: s.propertyName, color: "#c792ea" },
{ tag: s.className, color: "#decb6b" },
{ tag: s.invalid, color: "#ffffff" }
]
});
const B = $.define(), se = G.define({
const O = G.define(), ie = J.define({
create() {
return E.none;
return y.none;
},
update(e, t) {
try {
for (let o of t.effects)
if (o.is(B))
if (o.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]);
for (let r of t.effects)
if (r.is(O))
if (r.value) {
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = y.set([a.range(0, t.newDoc.length)]);
} else
e = E.set([]);
e = y.set([]);
return e;
} catch (o) {
return console.warn("flash error", o), e;
} catch (r) {
return console.warn("flash error", r), e;
}
},
provide: (e) => U.decorations.from(e)
}), ce = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) });
provide: (e) => $.decorations.from(e)
}), le = (e) => {
e.dispatch({ effects: O.of(!0) }), setTimeout(() => {
e.dispatch({ effects: O.of(!1) });
}, 200);
}, z = $.define(), ie = G.define({
}, H = G.define(), ue = J.define({
create() {
return E.none;
return y.none;
},
update(e, t) {
try {
for (let o of t.effects)
if (o.is(z)) {
const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: u, end: d }) => {
const f = s.context.color || "#FFCA28";
let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(d.line).from + d.column;
for (let r of t.effects)
if (r.is(H)) {
const a = r.value.map(
(n) => (n.context.locations || []).map(({ start: c, end: i }) => {
const d = n.context.color || "#FFCA28";
let o = t.newDoc.line(c.line).from + c.column, u = t.newDoc.line(i.line).from + i.column;
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) || [];
e = E.set(a, !0);
e = y.set(a, !0);
}
return e;
} catch {
return E.set([]);
return y.set([]);
}
},
provide: (e) => U.decorations.from(e)
}), le = [Y(), ae, ie, se];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) {
const d = _(
(i) => {
t?.(i);
provide: (e) => $.decorations.from(e)
}), de = [Z(), ce, ue, ie];
function fe({ value: e, onChange: t, onViewChanged: r, onSelectionChange: a, options: n, editorDidMount: c }) {
const i = M(
(u) => {
t?.(u);
},
[t]
), f = _(
(i) => {
o?.(i);
), d = M(
(u) => {
r?.(u);
},
[o]
), c = _(
(i) => {
i.selectionSet && a && a?.(i.state.selection);
[r]
), o = M(
(u) => {
u.selectionSet && a && a?.(u.state.selection);
},
[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,
onChange: d,
onCreateEditor: f,
onUpdate: c,
extensions: le
onChange: i,
onCreateEditor: d,
onUpdate: o,
extensions: de
}));
}
function K(...e) {
function I(...e) {
return e.filter(Boolean).join(" ");
}
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), u = H();
L(() => {
function me({ view: e, pattern: t, active: r, getTime: a }) {
const n = E([]), c = E();
C(() => {
if (e)
if (t && o) {
let d = requestAnimationFrame(function f() {
if (t && r) {
let i = requestAnimationFrame(function d() {
try {
const c = a(), m = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60];
u.current = m[1], s.current = s.current.filter((g) => g.whole.end > c);
const h = t.queryArc(...m).filter((g) => g.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) });
const o = a(), m = [Math.max(c.current || o, o - 1 / 10, 0), o + 1 / 60];
c.current = m[1], n.current = n.current.filter((v) => v.whole.end > o);
const g = t.queryArc(...m).filter((v) => v.hasOnset());
n.current = n.current.concat(g), e.dispatch({ effects: H.of(n.current) });
} catch {
e.dispatch({ effects: z.of([]) });
e.dispatch({ effects: H.of([]) });
}
d = requestAnimationFrame(f);
i = requestAnimationFrame(d);
});
return () => {
cancelAnimationFrame(d);
cancelAnimationFrame(i);
};
} else
s.current = [], e.dispatch({ effects: z.of([]) });
}, [t, o, e]);
n.current = [], e.dispatch({ effects: H.of([]) });
}, [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 = {
container: fe,
header: me,
buttons: ge,
button: pe,
buttonDisabled: he,
error: be,
body: ve
};
function O({ type: e }) {
return /* @__PURE__ */ n.createElement("svg", {
function ge(e, t = !1) {
const r = E(), a = E(), n = (d) => {
if (a.current !== void 0) {
const o = d - a.current;
e(d, o);
}
a.current = d, r.current = requestAnimationFrame(n);
}, c = () => {
r.current = requestAnimationFrame(n);
}, i = () => {
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",
className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20",
fill: "currentColor"
}, {
refresh: /* @__PURE__ */ n.createElement("path", {
refresh: /* @__PURE__ */ l.createElement("path", {
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",
clipRule: "evenodd"
}),
play: /* @__PURE__ */ n.createElement("path", {
play: /* @__PURE__ */ l.createElement("path", {
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",
clipRule: "evenodd"
}),
pause: /* @__PURE__ */ n.createElement("path", {
pause: /* @__PURE__ */ l.createElement("path", {
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",
clipRule: "evenodd"
})
}[e]);
}
function Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []);
}
function we({
defaultOutput: e,
interval: t,
getTime: o,
evalOnMount: a = !1,
initialCode: s = "",
autolink: u = !1,
beforeEval: d,
afterEval: f,
onEvalError: c,
onToggle: i
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 = {
container: we,
header: ye,
buttons: Ee,
button: ke,
buttonDisabled: _e,
error: Ce,
body: Fe
}, Ne = () => ne().currentTime;
function Be({
tune: e,
hideOutsideView: t = !1,
init: r,
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 {
code: s,
setCode: u,
evaluate: d,
activateCode: f,
error: c,
isDirty: i,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
code: i,
setCode: d,
evaluate: o,
activateCode: u,
error: m,
isDirty: g,
activeCode: v,
pattern: F,
started: k,
scheduler: A,
togglePlay: b,
stop: D,
canvasId: x
} = ve({
initialCode: e,
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
threshold: 0.01
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]);
return ue({
view: y,
pattern: h,
active: g && !m?.includes("strudel disable-highlighting"),
getTime: () => C.getPhase()
}), 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);
defaultOutput: oe,
getTime: Ne
});
pe({
pattern: F,
started: k,
getTime: () => A.now(),
onDraw: (f, h) => {
const V = document.querySelector("#" + x).getContext("2d");
re({ ctx: V, time: f, haps: h, autorange: 1, fold: 1, playhead: 1 });
}
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", {
className: v.container,
ref: S
}, /* @__PURE__ */ n.createElement("div", {
className: v.header
}, /* @__PURE__ */ n.createElement("div", {
className: v.buttons
}, /* @__PURE__ */ n.createElement("button", {
className: K(v.button, g ? "sc-animate-pulse" : ""),
onClick: () => N()
}, /* @__PURE__ */ n.createElement(O, {
type: g ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", {
className: K(i ? v.button : v.buttonDisabled),
onClick: () => f()
}, /* @__PURE__ */ n.createElement(O, {
});
const [R, S] = _(), [z, N] = ae({
threshold: 0.01
}), T = E(), L = B(() => ((N || !t) && (T.current = !0), N || T.current), [N, t]);
return me({
view: R,
pattern: F,
active: k && !v?.includes("strudel disable-highlighting"),
getTime: () => A.getPhase()
}), W(() => {
if (a) {
const f = async (h) => {
(h.ctrlKey || h.altKey) && (h.code === "Enter" ? (h.preventDefault(), le(R), await u()) : h.code === "Period" && (D(), h.preventDefault()));
};
return window.addEventListener("keydown", f, !0), () => window.removeEventListener("keydown", f, !0);
}
}, [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"
}))), c && /* @__PURE__ */ n.createElement("div", {
className: v.error
}, c.message)), /* @__PURE__ */ n.createElement("div", {
className: v.body
}, F && /* @__PURE__ */ n.createElement(de, {
value: s,
onChange: u,
onViewChanged: M
})));
}))), m && /* @__PURE__ */ l.createElement("div", {
className: w.error
}, m.message)), /* @__PURE__ */ l.createElement("div", {
className: w.body
}, L && /* @__PURE__ */ l.createElement(fe, {
value: i,
onChange: d,
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 {
de as CodeMirror,
Se as MiniRepl,
K as cx,
ce as flash,
ue as useHighlighting,
Te as useKeydown,
Ee as usePostMessage,
we as useStrudel
fe as CodeMirror,
Be as MiniRepl,
I as cx,
le as flash,
me as useHighlighting,
Oe as useKeydown,
he as usePostMessage,
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 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
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 usePatternFrame from '../hooks/usePatternFrame.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;
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
const {
code,
setCode,
@ -26,11 +28,23 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
scheduler,
togglePlay,
stop,
canvasId,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
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(() => {
init && activateCode();
}, [init, activateCode]); */
@ -88,6 +102,18 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</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>
);
}

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

View File

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