localize draw logic

This commit is contained in:
Felix Roos 2023-01-13 12:03:34 +01:00
parent d980c9ca4c
commit 1ac784dc7a
15 changed files with 397 additions and 308 deletions

View File

@ -59,3 +59,9 @@ export const cleanupDraw = (clearScreen = true) => {
clearInterval(window.strudelScheduler); clearInterval(window.strudelScheduler);
} }
}; };
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
return this;
};

View File

@ -2060,6 +2060,7 @@ export const velocity = register('velocity', function (velocity, pat) {
*/ */
// TODO - fix // TODO - fix
export const legato = register('legato', function (value, pat) { export const legato = register('legato', function (value, pat) {
value = Fraction(value);
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value)))); return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
}); });

View File

@ -283,3 +283,13 @@ export function pianoroll({
ctx.stroke(); ctx.stroke();
return this; return this;
} }
Pattern.prototype.noteroll = function (options = { fold: 1 }) {
return this.onPaint((ctx, time, haps, drawTime) => {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
const playhead = lookbehind / cycles;
pianoroll({ ctx, time, haps, ...options, cycles, playhead });
});
};

View File

@ -12,8 +12,8 @@ export function repl({
afterEval, afterEval,
getTime, getTime,
transpiler, transpiler,
editPattern,
onToggle, onToggle,
drawContext,
}) { }) {
const scheduler = new Cyclist({ const scheduler = new Cyclist({
interval, interval,
@ -35,7 +35,7 @@ export function repl({
getTime, getTime,
onToggle, onToggle,
}); });
setTime(() => scheduler.getPhase()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => { const evaluate = async (code, autostart = true) => {
if (!code) { if (!code) {
throw new Error('no code to evaluate'); throw new Error('no code to evaluate');
@ -45,7 +45,6 @@ export function repl({
let { pattern } = await _evaluate(code, transpiler); let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`); logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart); scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern }); afterEval?.({ code, pattern });
return pattern; return pattern;

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,15 @@
import l, { useCallback as N, useRef as k, useEffect as _, useMemo as K, useState as w, useLayoutEffect as G } from "react"; import l, { useCallback as w, useRef as A, useEffect as k, useMemo as $, useState as _, useLayoutEffect as Y } from "react";
import Z from "@uiw/react-codemirror"; import ie from "@uiw/react-codemirror";
import { Decoration as y, EditorView as J } from "@codemirror/view"; import { Decoration as M, EditorView as Z } from "@codemirror/view";
import { StateEffect as Q, StateField as X } from "@codemirror/state"; import { StateEffect as ee, StateField as te } from "@codemirror/state";
import { javascript as ee } from "@codemirror/lang-javascript"; import { javascript as le } from "@codemirror/lang-javascript";
import { tags as s } from "@lezer/highlight"; import { tags as i } from "@lezer/highlight";
import { createTheme as te } from "@uiw/codemirror-themes"; import { createTheme as ue } from "@uiw/codemirror-themes";
import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core"; import { webaudioOutput as de, getAudioContext as fe } from "@strudel.cycles/webaudio";
import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio"; import { useInView as me } from "react-hook-inview";
import { useInView as se } from "react-hook-inview"; import { repl as he, logger as ge } from "@strudel.cycles/core";
import { transpiler as ie } from "@strudel.cycles/transpiler"; import { transpiler as pe } from "@strudel.cycles/transpiler";
const le = te({ const ve = ue({
theme: "dark", theme: "dark",
settings: { settings: {
background: "#222", background: "#222",
@ -22,241 +22,273 @@ const le = te({
gutterForeground: "#8a919966" gutterForeground: "#8a919966"
}, },
styles: [ styles: [
{ tag: s.keyword, color: "#c792ea" }, { tag: i.keyword, color: "#c792ea" },
{ tag: s.operator, color: "#89ddff" }, { tag: i.operator, color: "#89ddff" },
{ tag: s.special(s.variableName), color: "#eeffff" }, { tag: i.special(i.variableName), color: "#eeffff" },
{ tag: s.typeName, color: "#c3e88d" }, { tag: i.typeName, color: "#c3e88d" },
{ tag: s.atom, color: "#f78c6c" }, { tag: i.atom, color: "#f78c6c" },
{ tag: s.number, color: "#c3e88d" }, { tag: i.number, color: "#c3e88d" },
{ tag: s.definition(s.variableName), color: "#82aaff" }, { tag: i.definition(i.variableName), color: "#82aaff" },
{ tag: s.string, color: "#c3e88d" }, { tag: i.string, color: "#c3e88d" },
{ tag: s.special(s.string), color: "#c3e88d" }, { tag: i.special(i.string), color: "#c3e88d" },
{ tag: s.comment, color: "#7d8799" }, { tag: i.comment, color: "#7d8799" },
{ tag: s.variableName, color: "#c792ea" }, { tag: i.variableName, color: "#c792ea" },
{ tag: s.tagName, color: "#c3e88d" }, { tag: i.tagName, color: "#c3e88d" },
{ tag: s.bracket, color: "#525154" }, { tag: i.bracket, color: "#525154" },
{ tag: s.meta, color: "#ffcb6b" }, { tag: i.meta, color: "#ffcb6b" },
{ tag: s.attributeName, color: "#c792ea" }, { tag: i.attributeName, color: "#c792ea" },
{ tag: s.propertyName, color: "#c792ea" }, { tag: i.propertyName, color: "#c792ea" },
{ tag: s.className, color: "#decb6b" }, { tag: i.className, color: "#decb6b" },
{ tag: s.invalid, color: "#ffffff" } { tag: i.invalid, color: "#ffffff" }
] ]
}); });
const j = Q.define(), ue = X.define({ const G = ee.define(), be = te.define({
create() { create() {
return y.none; return M.none;
}, },
update(e, r) { update(e, r) {
try { try {
for (let t of r.effects) for (let t of r.effects)
if (t.is(j)) if (t.is(G))
if (t.value) { if (t.value) {
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } }); const o = M.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = y.set([a.range(0, r.newDoc.length)]); e = M.set([o.range(0, r.newDoc.length)]);
} else } else
e = y.set([]); e = M.set([]);
return e; return e;
} catch (t) { } catch (t) {
return console.warn("flash error", t), e; return console.warn("flash error", t), e;
} }
}, },
provide: (e) => J.decorations.from(e) provide: (e) => Z.decorations.from(e)
}), de = (e) => { }), Ee = (e) => {
e.dispatch({ effects: j.of(!0) }), setTimeout(() => { e.dispatch({ effects: G.of(!0) }), setTimeout(() => {
e.dispatch({ effects: j.of(!1) }); e.dispatch({ effects: G.of(!1) });
}, 200); }, 200);
}, z = Q.define(), fe = X.define({ }, O = ee.define(), ye = te.define({
create() { create() {
return y.none; return M.none;
}, },
update(e, r) { update(e, r) {
try { try {
for (let t of r.effects) for (let t of r.effects)
if (t.is(z)) { if (t.is(O)) {
const a = t.value.map( const o = t.value.map(
(o) => (o.context.locations || []).map(({ start: i, end: u }) => { (u) => (u.context.locations || []).map(({ start: m, end: d }) => {
const m = o.context.color || "#FFCA28"; const a = u.context.color || "#FFCA28";
let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column; let c = r.newDoc.line(m.line).from + m.column, h = r.newDoc.line(d.line).from + d.column;
const v = r.newDoc.length; const g = r.newDoc.length;
return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d); return c > g || h > g ? void 0 : M.mark({ attributes: { style: `outline: 1.5px solid ${a};` } }).range(c, h);
}) })
).flat().filter(Boolean) || []; ).flat().filter(Boolean) || [];
e = y.set(a, !0); e = M.set(o, !0);
} }
return e; return e;
} catch { } catch {
return y.set([]); return M.set([]);
} }
}, },
provide: (e) => J.decorations.from(e) provide: (e) => Z.decorations.from(e)
}), me = [ee(), le, fe, ue]; }), we = [le(), ve, ye, be];
function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) { function ke({ value: e, onChange: r, onViewChanged: t, onSelectionChange: o, options: u, editorDidMount: m }) {
const u = N( const d = w(
(d) => { (h) => {
r?.(d); r?.(h);
}, },
[r] [r]
), m = N( ), a = w(
(d) => { (h) => {
t?.(d); t?.(h);
}, },
[t] [t]
), n = N( ), c = w(
(d) => { (h) => {
d.selectionSet && a && a?.(d.state.selection); h.selectionSet && o && o?.(h.state.selection);
}, },
[a] [o]
); );
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, { return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(ie, {
value: e, value: e,
onChange: u, onChange: d,
onCreateEditor: m, onCreateEditor: a,
onUpdate: n, onUpdate: c,
extensions: me extensions: we
})); }));
} }
function W(...e) { function T(...e) {
return e.filter(Boolean).join(" "); return e.filter(Boolean).join(" ");
} }
function pe({ view: e, pattern: r, active: t, getTime: a }) { function Fe({ view: e, pattern: r, active: t, getTime: o }) {
const o = k([]), i = k(); const u = A([]), m = A();
_(() => { k(() => {
if (e) if (e)
if (r && t) { if (r && t) {
let u = requestAnimationFrame(function m() { let d = requestAnimationFrame(function a() {
try { try {
const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60]; const c = o(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n); m.current = g[1], u.current = u.current.filter((p) => p.whole.end > c);
const h = r.queryArc(...v).filter((p) => p.hasOnset()); const n = r.queryArc(...g).filter((p) => p.hasOnset());
o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) }); u.current = u.current.concat(n), e.dispatch({ effects: O.of(u.current) });
} catch { } catch {
e.dispatch({ effects: z.of([]) }); e.dispatch({ effects: O.of([]) });
} }
u = requestAnimationFrame(m); d = requestAnimationFrame(a);
}); });
return () => { return () => {
cancelAnimationFrame(u); cancelAnimationFrame(d);
}; };
} else } else
o.current = [], e.dispatch({ effects: z.of([]) }); u.current = [], e.dispatch({ effects: O.of([]) });
}, [r, t, e]); }, [r, t, e]);
} }
function he(e, r = !1) { function _e(e, r = !1) {
const t = k(), a = k(), o = (m) => { const t = A(), o = A(), u = (a) => {
if (a.current !== void 0) { if (o.current !== void 0) {
const n = m - a.current; const c = a - o.current;
e(m, n); e(a, c);
} }
a.current = m, t.current = requestAnimationFrame(o); o.current = a, t.current = requestAnimationFrame(u);
}, i = () => { }, m = () => {
t.current = requestAnimationFrame(o); t.current = requestAnimationFrame(u);
}, u = () => { }, d = () => {
t.current && cancelAnimationFrame(t.current), delete t.current; t.current && cancelAnimationFrame(t.current), delete t.current;
}; };
return _(() => { return k(() => {
t.current && (u(), i()); t.current && (d(), m());
}, [e]), _(() => (r && i(), u), []), { }, [e]), k(() => (r && m(), d), []), {
start: i, start: m,
stop: u stop: d
}; };
} }
function ve({ pattern: e, started: r, getTime: t, onDraw: a }) { function Me({ pattern: e, started: r, getTime: t, onDraw: o, drawTime: u = [-2, 2] }) {
let o = k([]), i = k(null); let [m, d] = u;
const { start: u, stop: m } = he( m = Math.abs(m);
N(() => { let a = A([]), c = A(null);
const n = t(); k(() => {
if (i.current === null) { if (e) {
i.current = n; const n = t(), p = e.queryArc(n, n + d);
a.current = a.current.filter((b) => b.whole.begin < n), a.current = a.current.concat(p);
}
}, [e]);
const { start: h, stop: g } = _e(
w(() => {
const n = t() + d;
if (c.current === null) {
c.current = n;
return; return;
} }
const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4; const p = e.queryArc(Math.max(c.current, n - 1 / 10), n);
i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current); c.current = n, a.current = (a.current || []).filter((b) => b.whole.end > n - m - d).concat(p.filter((b) => b.hasOnset())), o(e, n - d, a.current, u);
}, [e]) }, [e])
); );
_(() => { k(() => {
r ? u() : (o.current = [], m()); r ? h() : (a.current = [], g());
}, [r]); }, [r]);
} }
function be(e) { function Ae(e) {
return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []); return k(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), w((r) => window.postMessage(r, "*"), []);
} }
function Ee({ function Ne({
defaultOutput: e, defaultOutput: e,
interval: r, interval: r,
getTime: t, getTime: t,
evalOnMount: a = !1, evalOnMount: o = !1,
initialCode: o = "", initialCode: u = "",
autolink: i = !1, autolink: m = !1,
beforeEval: u, beforeEval: d,
afterEval: m, afterEval: a,
editPattern: n, onEvalError: c,
onEvalError: d, onToggle: h,
onToggle: v, canvasId: g,
canvasId: h drawContext: n,
drawTime: p = [-2, 2]
}) { }) {
const p = K(() => we(), []); const b = $(() => De(), []);
h = h || `canvas-${p}`; g = g || `canvas-${b}`;
const [F, A] = w(), [P, D] = w(), [b, q] = w(o), [x, V] = w(), [I, R] = w(), [L, B] = w(!1), H = b !== x, { scheduler: M, evaluate: c, start: f, stop: C, pause: O } = K( const [q, x] = _(), [C, z] = _(), [E, D] = _(u), [H, B] = _(), [N, P] = _(), [R, S] = _(!1), I = E !== H, { scheduler: s, evaluate: v, start: J, stop: V, pause: re } = $(
() => re({ () => he({
interval: r, interval: r,
defaultOutput: e, defaultOutput: e,
onSchedulerError: A, onSchedulerError: x,
onEvalError: (g) => { onEvalError: (f) => {
D(g), d?.(g); z(f), c?.(f);
}, },
getTime: t, getTime: t,
transpiler: ie, drawContext: n,
beforeEval: ({ code: g }) => { transpiler: pe,
q(g), u?.(); beforeEval: ({ code: f }) => {
D(f), d?.();
}, },
editPattern: n ? (g) => n(g, p) : void 0, afterEval: ({ pattern: f, code: y }) => {
afterEval: ({ pattern: g, code: T }) => { B(y), P(f), z(), x(), m && (window.location.hash = "#" + encodeURIComponent(btoa(y))), a?.();
V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.();
}, },
onToggle: (g) => { onToggle: (f) => {
B(g), v?.(g); S(f), h?.(f);
} }
}), }),
[e, r, t] [e, r, t]
), Y = be(({ data: { from: g, type: T } }) => { ), ne = Ae(({ data: { from: f, type: y } }) => {
T === "start" && g !== p && C(); y === "start" && f !== b && V();
}), S = N( }), K = w(
async (g = !0) => { async (f = !0) => {
await c(b, g), Y({ type: "start", from: p }); const y = await v(E, f);
return ne({ type: "start", from: b }), y;
}, },
[c, b] [v, E]
), U = k(); ), L = w(
return _(() => { (f, y, U, W) => {
!U.current && a && b && (U.current = !0, S()); const { onPaint: ce } = f.context || {}, se = typeof n == "function" ? n(g) : n;
}, [S, a, b]), _(() => () => { ce?.(se, y, U, W);
M.stop(); },
}, [M]), { [n, g]
id: p, ), j = w(
canvasId: h, (f) => {
code: b, if (n && L) {
setCode: q, const [y, U] = p, W = f.queryArc(0, U);
error: F || P, L(f, 0, W, p);
schedulerError: F, }
scheduler: M, },
evalError: P, [p, L]
evaluate: c, ), Q = A();
activateCode: S, k(() => {
activeCode: x, !Q.current && n && L && o && E && (Q.current = !0, v(E, !1).then((f) => j(f)));
isDirty: H, }, [K, o, E, j]), k(() => () => {
pattern: I, s.stop();
started: L, }, [s]);
start: f, const oe = async () => {
stop: C, R ? (s.stop(), j(N)) : await K();
pause: O, }, ae = q || C;
togglePlay: async () => { return Me({
L ? M.pause() : await S(); pattern: N,
} started: n && R,
getTime: () => s.now(),
drawTime: p,
onDraw: L
}), {
id: b,
canvasId: g,
code: E,
setCode: D,
error: ae,
schedulerError: q,
scheduler: s,
evalError: C,
evaluate: v,
activateCode: K,
activeCode: H,
isDirty: I,
pattern: N,
started: R,
start: J,
stop: V,
pause: re,
togglePlay: oe
}; };
} }
function we() { function De() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
} }
function $({ type: e }) { function X({ type: e }) {
return /* @__PURE__ */ l.createElement("svg", { 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",
@ -277,125 +309,122 @@ function $({ type: e }) {
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"
}),
stop: /* @__PURE__ */ l.createElement("path", {
fillRule: "evenodd",
d: "M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z",
clipRule: "evenodd"
}) })
}[e]); }[e]);
} }
const ye = "_container_3i85k_1", ke = "_header_3i85k_5", _e = "_buttons_3i85k_9", Fe = "_button_3i85k_9", Ce = "_buttonDisabled_3i85k_17", Ne = "_error_3i85k_21", xe = "_body_3i85k_25", E = { const Ce = "_container_3i85k_1", Re = "_header_3i85k_5", Le = "_buttons_3i85k_9", qe = "_button_3i85k_9", xe = "_buttonDisabled_3i85k_17", ze = "_error_3i85k_21", He = "_body_3i85k_25", F = {
container: ye, container: Ce,
header: ke, header: Re,
buttons: _e, buttons: Le,
button: Fe, button: qe,
buttonDisabled: Ce, buttonDisabled: xe,
error: Ne, error: ze,
body: xe body: He
}, Me = () => ce().currentTime; }, Pe = () => fe().currentTime;
function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) { function Te({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, drawTime: o, canvasHeight: u = 200 }) {
const { const {
code: i, code: m,
setCode: u, setCode: d,
evaluate: m, evaluate: a,
activateCode: n, activateCode: c,
error: d, error: h,
isDirty: v, isDirty: g,
activeCode: h, activeCode: n,
pattern: p, pattern: p,
started: F, started: b,
scheduler: A, scheduler: q,
togglePlay: P, togglePlay: x,
stop: D, stop: C,
canvasId: b, canvasId: z,
id: q id: E
} = Ee({ } = Ne({
initialCode: e, initialCode: e,
defaultOutput: ae, defaultOutput: de,
getTime: Me, getTime: Pe,
editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f })) evalOnMount: !!o,
}); drawContext: o ? (s) => document.querySelector("#" + s)?.getContext("2d") : null,
ve({ drawTime: o
pattern: p, }), [D, H] = _(), [B, N] = me({
started: a && F,
getTime: () => A.now(),
onDraw: (c, f) => {
const C = document.querySelector("#" + b).getContext("2d");
oe({ ctx: C, time: c, haps: f, autorange: 1, fold: 1, playhead: 1 });
}
});
const [x, V] = w(), [I, R] = se({
threshold: 0.01 threshold: 0.01
}), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]); }), P = A(), R = $(() => ((N || !r) && (P.current = !0), N || P.current), [N, r]);
pe({ Fe({
view: x, view: D,
pattern: p, pattern: p,
active: F && !h?.includes("strudel disable-highlighting"), active: b && !n?.includes("strudel disable-highlighting"),
getTime: () => A.getPhase() getTime: () => q.now()
}), G(() => { }), Y(() => {
if (t) { if (t) {
const c = async (f) => { const s = async (v) => {
(f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault())); (v.ctrlKey || v.altKey) && (v.code === "Enter" ? (v.preventDefault(), Ee(D), await c()) : v.code === "Period" && (C(), v.preventDefault()));
}; };
return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0); return window.addEventListener("keydown", s, !0), () => window.removeEventListener("keydown", s, !0);
} }
}, [t, p, i, m, D, x]); }, [t, p, m, a, C, D]);
const [H, M] = w([]); const [S, I] = _([]);
return Ae( return Se(
N((c) => { w((s) => {
const { data: f } = c.detail; const { data: v } = s.detail;
f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10)); v?.hap?.context?.id === E && I((V) => V.concat([s.detail]).slice(-10));
}, []) }, [])
), /* @__PURE__ */ l.createElement("div", { ), /* @__PURE__ */ l.createElement("div", {
className: E.container, className: F.container,
ref: I ref: B
}, /* @__PURE__ */ l.createElement("div", { }, /* @__PURE__ */ l.createElement("div", {
className: E.header className: F.header
}, /* @__PURE__ */ l.createElement("div", { }, /* @__PURE__ */ l.createElement("div", {
className: E.buttons className: F.buttons
}, /* @__PURE__ */ l.createElement("button", { }, /* @__PURE__ */ l.createElement("button", {
className: W(E.button, F ? "sc-animate-pulse" : ""), className: T(F.button, b ? "sc-animate-pulse" : ""),
onClick: () => P() onClick: () => x()
}, /* @__PURE__ */ l.createElement($, { }, /* @__PURE__ */ l.createElement(X, {
type: F ? "pause" : "play" type: b ? "stop" : "play"
})), /* @__PURE__ */ l.createElement("button", { })), /* @__PURE__ */ l.createElement("button", {
className: W(v ? E.button : E.buttonDisabled), className: T(g ? F.button : F.buttonDisabled),
onClick: () => n() onClick: () => c()
}, /* @__PURE__ */ l.createElement($, { }, /* @__PURE__ */ l.createElement(X, {
type: "refresh" type: "refresh"
}))), d && /* @__PURE__ */ l.createElement("div", { }))), h && /* @__PURE__ */ l.createElement("div", {
className: E.error className: F.error
}, d.message)), /* @__PURE__ */ l.createElement("div", { }, h.message)), /* @__PURE__ */ l.createElement("div", {
className: E.body className: F.body
}, B && /* @__PURE__ */ l.createElement(ge, { }, R && /* @__PURE__ */ l.createElement(ke, {
value: i, value: m,
onChange: u, onChange: d,
onViewChanged: V onViewChanged: H
})), a && /* @__PURE__ */ l.createElement("canvas", { })), o && /* @__PURE__ */ l.createElement("canvas", {
id: b, id: z,
className: "w-full pointer-events-none", className: "w-full pointer-events-none",
height: o, height: u,
ref: (c) => { ref: (s) => {
c && c.width !== c.clientWidth && (c.width = c.clientWidth); s && s.width !== s.clientWidth && (s.width = s.clientWidth);
} }
}), !!H.length && /* @__PURE__ */ l.createElement("div", { }), !!S.length && /* @__PURE__ */ l.createElement("div", {
className: "sc-bg-gray-800 sc-rounded-md sc-p-2" className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
}, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", { }, S.map(({ message: s }, v) => /* @__PURE__ */ l.createElement("div", {
key: f key: v
}, c)))); }, s))));
} }
function Ae(e) { function Se(e) {
De(ne.key, e); Ve(ge.key, e);
} }
function De(e, r, t = !1) { function Ve(e, r, t = !1) {
_(() => (document.addEventListener(e, r, t), () => { k(() => (document.addEventListener(e, r, t), () => {
document.removeEventListener(e, r, t); document.removeEventListener(e, r, t);
}), [r]); }), [r]);
} }
const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); const Xe = (e) => Y(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export { export {
ge as CodeMirror, ke as CodeMirror,
je as MiniRepl, Te as MiniRepl,
W as cx, T as cx,
de as flash, Ee as flash,
pe as useHighlighting, Fe as useHighlighting,
Ue as useKeydown, Xe as useKeydown,
be as usePostMessage, Ae as usePostMessage,
Ee as useStrudel Ne as useStrudel
}; };

View File

@ -26,6 +26,13 @@ export function Icon({ type }) {
clipRule="evenodd" clipRule="evenodd"
/> />
), ),
stop: (
<path
fillRule="evenodd"
d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z"
clipRule="evenodd"
/>
),
}[type] }[type]
} }
</svg> </svg>

View File

@ -1,11 +1,9 @@
import { pianoroll } from '@strudel.cycles/core';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react'; import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { useInView } from 'react-hook-inview'; import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css'; 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 usePatternFrame from '../hooks/usePatternFrame.mjs';
import useStrudel from '../hooks/useStrudel.mjs'; import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6'; import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon'; import { Icon } from './Icon';
@ -15,7 +13,7 @@ import { logger } from '@strudel.cycles/core';
const getTime = () => getAudioContext().currentTime; const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) { export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, canvasHeight = 200 }) {
const { const {
code, code,
setCode, setCode,
@ -35,24 +33,11 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
initialCode: tune, initialCode: tune,
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
getTime, getTime,
editPattern: (pat, id) => { evalOnMount: !!drawTime,
return pat.withContext((ctx) => ({ ...ctx, id })); drawContext: !!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
}, drawTime,
}); });
usePatternFrame({
pattern,
started: withCanvas && 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]); */
const [view, setView] = useState(); const [view, setView] = useState();
const [ref, isVisible] = useInView({ const [ref, isVisible] = useInView({
threshold: 0.01, threshold: 0.01,
@ -68,7 +53,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
view, view,
pattern, pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'), active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(), getTime: () => scheduler.now(),
}); });
// set active pattern on ctrl+enter // set active pattern on ctrl+enter
@ -110,7 +95,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
<div className={styles.header}> <div className={styles.header}>
<div className={styles.buttons}> <div className={styles.buttons}>
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}> <button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={started ? 'pause' : 'play'} /> <Icon type={started ? 'stop' : 'play'} />
</button> </button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}> <button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" /> <Icon type="refresh" />
@ -121,7 +106,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
<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 && ( {drawTime && (
<canvas <canvas
id={canvasId} id={canvasId}
className="w-full pointer-events-none" className="w-full pointer-events-none"

View File

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

View File

@ -1,6 +1,7 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react'; import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core'; import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler'; import { transpiler } from '@strudel.cycles/transpiler';
import usePatternFrame from './usePatternFrame';
import usePostMessage from './usePostMessage.mjs'; import usePostMessage from './usePostMessage.mjs';
function useStrudel({ function useStrudel({
@ -12,10 +13,11 @@ function useStrudel({
autolink = false, autolink = false,
beforeEval, beforeEval,
afterEval, afterEval,
editPattern,
onEvalError, onEvalError,
onToggle, onToggle,
canvasId, canvasId,
drawContext,
drawTime = [-2, 2],
}) { }) {
const id = useMemo(() => s4(), []); const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`; canvasId = canvasId || `canvas-${id}`;
@ -40,12 +42,12 @@ function useStrudel({
onEvalError?.(err); onEvalError?.(err);
}, },
getTime, getTime,
drawContext,
transpiler, transpiler,
beforeEval: ({ code }) => { beforeEval: ({ code }) => {
setCode(code); setCode(code);
beforeEval?.(); beforeEval?.();
}, },
editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined,
afterEval: ({ pattern: _pattern, code }) => { afterEval: ({ pattern: _pattern, code }) => {
setActiveCode(code); setActiveCode(code);
setPattern(_pattern); setPattern(_pattern);
@ -71,19 +73,40 @@ function useStrudel({
}); });
const activateCode = useCallback( const activateCode = useCallback(
async (autostart = true) => { async (autostart = true) => {
await evaluate(code, autostart); const res = await evaluate(code, autostart);
broadcast({ type: 'start', from: id }); broadcast({ type: 'start', from: id });
return res;
}, },
[evaluate, code], [evaluate, code],
); );
const onDraw = useCallback(
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime);
},
[drawContext, canvasId],
);
const drawFirstFrame = useCallback(
(pat) => {
if (drawContext && onDraw) {
const [_, lookahead] = drawTime;
const haps = pat.queryArc(0, lookahead);
onDraw(pat, 0, haps, drawTime);
}
},
[drawTime, onDraw],
);
const inited = useRef(); const inited = useRef();
useEffect(() => { useEffect(() => {
if (!inited.current && evalOnMount && code) { if (!inited.current && drawContext && onDraw && evalOnMount && code) {
inited.current = true; inited.current = true;
activateCode(); evaluate(code, false).then((pat) => drawFirstFrame(pat));
} }
}, [activateCode, evalOnMount, code]); }, [activateCode, evalOnMount, code, drawFirstFrame]);
// this will stop the scheduler when hot reloading in development // this will stop the scheduler when hot reloading in development
useEffect(() => { useEffect(() => {
@ -94,12 +117,22 @@ function useStrudel({
const togglePlay = async () => { const togglePlay = async () => {
if (started) { if (started) {
scheduler.pause(); scheduler.stop();
drawFirstFrame(pattern);
} else { } else {
await activateCode(); await activateCode();
} }
}; };
const error = schedulerError || evalError; const error = schedulerError || evalError;
usePatternFrame({
pattern,
started: drawContext && started,
getTime: () => scheduler.now(),
drawTime,
onDraw,
});
return { return {
id, id,
canvasId, canvasId,

View File

@ -22,7 +22,7 @@ if (typeof window !== 'undefined') {
prebake(); prebake();
} }
export function MiniRepl({ tune, withCanvas }) { export function MiniRepl({ tune, drawTime }) {
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, withCanvas }) {
setRepl(() => res.MiniRepl); setRepl(() => res.MiniRepl);
}); });
}, []); }, []);
return Repl ? <Repl tune={tune} hideOutsideView={true} withCanvas={withCanvas} /> : <pre>{tune}</pre>; return Repl ? <Repl tune={tune} hideOutsideView={true} drawTime={drawTime} /> : <pre>{tune}</pre>;
} }

View File

@ -28,7 +28,7 @@ Strudel however runs directly in your web browser, does not require any custom s
The main place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/) ([what is a REPL?](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)), but in these pages you will also encounter interactive "MiniREPLs" where you can listen to and edit Strudel patterns. The main place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/) ([what is a REPL?](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)), but in these pages you will also encounter interactive "MiniREPLs" where you can listen to and edit Strudel patterns.
Try clicking the play icon below: Try clicking the play icon below:
<MiniRepl client:idle tune={`s("bd sd")`} /> <MiniRepl client:idle tune={`s("bd sd").noteroll()`} drawTime={[0, 2]} />
Then edit the text so it reads `s("bd sd cp hh")` and click the refresh icon. Then edit the text so it reads `s("bd sd cp hh")` and click the refresh icon.
Congratulations, you have now live coded your first Strudel pattern! Congratulations, you have now live coded your first Strudel pattern!

View File

@ -51,12 +51,12 @@ If you do just want to get a regular string that is _not_ parsed as mini-notatio
We can play more notes by separating them with spaces: We can play more notes by separating them with spaces:
<MiniRepl client:idle tune={`note("e5 b4 d5 c5")`} withCanvas /> <MiniRepl client:idle tune={`note("c e g").noteroll()`} drawTime={[0, 4]} />
Here, those four notes are squashed into one cycle, so each note is a quarter second long. Here, those four notes are squashed into one cycle, so each note is a quarter second long.
Try adding or removing notes and notice how the tempo changes! Try adding or removing notes and notice how the tempo changes!
<MiniRepl client:idle tune={`note("e5 b4 d5 c5 e5 b4 d5 c5")`} withCanvas /> <MiniRepl client:idle tune={`note("c d e f g a b").noteroll()`} drawTime={[0, 4]} />
Note that the overall duration of time does not change, and instead each note length descreases. Note that the overall duration of time does not change, and instead each note length descreases.
This is a key idea, as it illustrates the 'Cycle' in TidalCycles! This is a key idea, as it illustrates the 'Cycle' in TidalCycles!

View File

@ -29,7 +29,11 @@ add a mini repl with
- `client:idle` is required to tell astro that the repl should be interactive, see [Client Directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) - `client:idle` is required to tell astro that the repl should be interactive, see [Client Directive](https://docs.astro.build/en/reference/directives-reference/#client-directives)
- `tune`: be any valid pattern code - `tune`: be any valid pattern code
- `withCanvas`: If set, a canvas will be rendered below that shows a pianoroll (WIP) - `drawTime`: time window for drawing. Use together with `.noteroll()`. Example:
```jsx
<MiniRepl client:idle tune={`note("a3 c#4 e4 a4").noteroll()`} drawTime={[0, 2]} />
```
## In-Source Documentation ## In-Source Documentation

View File

@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { cleanupDraw, cleanupUi, controls, evalScope, logger } from '@strudel.cycles/core'; import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react'; import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react';
import { import {
getAudioContext, getAudioContext,
@ -54,6 +54,11 @@ evalScope(
export let loadedSamples = []; export let loadedSamples = [];
const presets = prebake(); const presets = prebake();
let drawContext;
if (typeof window !== 'undefined') {
drawContext = getDrawContext();
}
Promise.all([...modules, presets]).then((data) => { Promise.all([...modules, presets]).then((data) => {
// console.log('modules and sample registry loade', data); // console.log('modules and sample registry loade', data);
loadedSamples = Object.entries(getLoadedSamples() || {}); loadedSamples = Object.entries(getLoadedSamples() || {});
@ -125,6 +130,7 @@ export function Repl({ embedded = false }) {
setPending(false); setPending(false);
}, },
onToggle: (play) => !play && cleanupDraw(false), onToggle: (play) => !play && cleanupDraw(false),
drawContext,
}); });
// init code // init code
@ -167,7 +173,7 @@ export function Repl({ embedded = false }) {
view, view,
pattern, pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'), active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(), getTime: () => scheduler.now(),
}); });
// //