mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge pull request #372 from tidalcycles/local-draw
improve new draw logic
This commit is contained in:
commit
362679954a
@ -59,3 +59,9 @@ export const cleanupDraw = (clearScreen = true) => {
|
||||
clearInterval(window.strudelScheduler);
|
||||
}
|
||||
};
|
||||
|
||||
Pattern.prototype.onPaint = function (onPaint) {
|
||||
// this is evil! TODO: add pattern.context
|
||||
this.context = { onPaint };
|
||||
return this;
|
||||
};
|
||||
|
||||
@ -2060,6 +2060,7 @@ export const velocity = register('velocity', function (velocity, pat) {
|
||||
*/
|
||||
// TODO - fix
|
||||
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))));
|
||||
});
|
||||
|
||||
|
||||
@ -283,3 +283,15 @@ export function pianoroll({
|
||||
ctx.stroke();
|
||||
return this;
|
||||
}
|
||||
|
||||
function getOptions(drawTime, options = {}) {
|
||||
let [lookbehind, lookahead] = drawTime;
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
const cycles = lookahead + lookbehind;
|
||||
const playhead = lookbehind / cycles;
|
||||
return { fold: 1, ...options, cycles, playhead };
|
||||
}
|
||||
|
||||
Pattern.prototype.punchcard = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||
};
|
||||
|
||||
@ -12,8 +12,8 @@ export function repl({
|
||||
afterEval,
|
||||
getTime,
|
||||
transpiler,
|
||||
editPattern,
|
||||
onToggle,
|
||||
editPattern,
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
@ -35,7 +35,7 @@ export function repl({
|
||||
getTime,
|
||||
onToggle,
|
||||
});
|
||||
setTime(() => scheduler.getPhase()); // TODO: refactor?
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
|
||||
2
packages/react/dist/index.cjs.js
vendored
2
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
589
packages/react/dist/index.es.js
vendored
589
packages/react/dist/index.es.js
vendored
@ -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 Z from "@uiw/react-codemirror";
|
||||
import { Decoration as y, EditorView as J } from "@codemirror/view";
|
||||
import { StateEffect as Q, StateField as X } from "@codemirror/state";
|
||||
import { javascript as ee } from "@codemirror/lang-javascript";
|
||||
import { tags as s } from "@lezer/highlight";
|
||||
import { createTheme as te } from "@uiw/codemirror-themes";
|
||||
import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core";
|
||||
import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio";
|
||||
import { useInView as se } from "react-hook-inview";
|
||||
import { transpiler as ie } from "@strudel.cycles/transpiler";
|
||||
const le = te({
|
||||
import l, { useCallback as y, useRef as N, useEffect as k, useMemo as J, useState as M, useLayoutEffect as te } from "react";
|
||||
import le from "@uiw/react-codemirror";
|
||||
import { Decoration as A, EditorView as re } from "@codemirror/view";
|
||||
import { StateEffect as ne, StateField as oe } from "@codemirror/state";
|
||||
import { javascript as ue } from "@codemirror/lang-javascript";
|
||||
import { tags as i } from "@lezer/highlight";
|
||||
import { createTheme as de } from "@uiw/codemirror-themes";
|
||||
import { webaudioOutput as fe, getAudioContext as me } from "@strudel.cycles/webaudio";
|
||||
import { useInView as he } from "react-hook-inview";
|
||||
import { repl as ge, logger as pe } from "@strudel.cycles/core";
|
||||
import { transpiler as ve } from "@strudel.cycles/transpiler";
|
||||
const be = de({
|
||||
theme: "dark",
|
||||
settings: {
|
||||
background: "#222",
|
||||
@ -22,241 +22,280 @@ const le = te({
|
||||
gutterForeground: "#8a919966"
|
||||
},
|
||||
styles: [
|
||||
{ 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" }
|
||||
{ tag: i.keyword, color: "#c792ea" },
|
||||
{ tag: i.operator, color: "#89ddff" },
|
||||
{ tag: i.special(i.variableName), color: "#eeffff" },
|
||||
{ tag: i.typeName, color: "#c3e88d" },
|
||||
{ tag: i.atom, color: "#f78c6c" },
|
||||
{ tag: i.number, color: "#c3e88d" },
|
||||
{ tag: i.definition(i.variableName), color: "#82aaff" },
|
||||
{ tag: i.string, color: "#c3e88d" },
|
||||
{ tag: i.special(i.string), color: "#c3e88d" },
|
||||
{ tag: i.comment, color: "#7d8799" },
|
||||
{ tag: i.variableName, color: "#c792ea" },
|
||||
{ tag: i.tagName, color: "#c3e88d" },
|
||||
{ tag: i.bracket, color: "#525154" },
|
||||
{ tag: i.meta, color: "#ffcb6b" },
|
||||
{ tag: i.attributeName, color: "#c792ea" },
|
||||
{ tag: i.propertyName, color: "#c792ea" },
|
||||
{ tag: i.className, color: "#decb6b" },
|
||||
{ tag: i.invalid, color: "#ffffff" }
|
||||
]
|
||||
});
|
||||
const j = Q.define(), ue = X.define({
|
||||
const Q = ne.define(), Ee = oe.define({
|
||||
create() {
|
||||
return y.none;
|
||||
return A.none;
|
||||
},
|
||||
update(e, r) {
|
||||
try {
|
||||
for (let t of r.effects)
|
||||
if (t.is(j))
|
||||
if (t.is(Q))
|
||||
if (t.value) {
|
||||
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||
e = y.set([a.range(0, r.newDoc.length)]);
|
||||
const n = A.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||
e = A.set([n.range(0, r.newDoc.length)]);
|
||||
} else
|
||||
e = y.set([]);
|
||||
e = A.set([]);
|
||||
return e;
|
||||
} catch (t) {
|
||||
return console.warn("flash error", t), e;
|
||||
}
|
||||
},
|
||||
provide: (e) => J.decorations.from(e)
|
||||
}), de = (e) => {
|
||||
e.dispatch({ effects: j.of(!0) }), setTimeout(() => {
|
||||
e.dispatch({ effects: j.of(!1) });
|
||||
provide: (e) => re.decorations.from(e)
|
||||
}), ye = (e) => {
|
||||
e.dispatch({ effects: Q.of(!0) }), setTimeout(() => {
|
||||
e.dispatch({ effects: Q.of(!1) });
|
||||
}, 200);
|
||||
}, z = Q.define(), fe = X.define({
|
||||
}, I = ne.define(), we = oe.define({
|
||||
create() {
|
||||
return y.none;
|
||||
return A.none;
|
||||
},
|
||||
update(e, r) {
|
||||
try {
|
||||
for (let t of r.effects)
|
||||
if (t.is(z)) {
|
||||
const a = t.value.map(
|
||||
(o) => (o.context.locations || []).map(({ start: i, end: u }) => {
|
||||
const m = o.context.color || "#FFCA28";
|
||||
let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column;
|
||||
const v = r.newDoc.length;
|
||||
return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d);
|
||||
if (t.is(I)) {
|
||||
const n = t.value.map(
|
||||
(s) => (s.context.locations || []).map(({ start: m, end: u }) => {
|
||||
const o = s.context.color || "#FFCA28";
|
||||
let c = r.newDoc.line(m.line).from + m.column, g = r.newDoc.line(u.line).from + u.column;
|
||||
const b = r.newDoc.length;
|
||||
return c > b || g > b ? void 0 : A.mark({ attributes: { style: `outline: 1.5px solid ${o};` } }).range(c, g);
|
||||
})
|
||||
).flat().filter(Boolean) || [];
|
||||
e = y.set(a, !0);
|
||||
e = A.set(n, !0);
|
||||
}
|
||||
return e;
|
||||
} catch {
|
||||
return y.set([]);
|
||||
return A.set([]);
|
||||
}
|
||||
},
|
||||
provide: (e) => J.decorations.from(e)
|
||||
}), me = [ee(), le, fe, ue];
|
||||
function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) {
|
||||
const u = N(
|
||||
(d) => {
|
||||
r?.(d);
|
||||
provide: (e) => re.decorations.from(e)
|
||||
}), ke = [ue(), be, we, Ee];
|
||||
function Fe({ value: e, onChange: r, onViewChanged: t, onSelectionChange: n, options: s, editorDidMount: m }) {
|
||||
const u = y(
|
||||
(g) => {
|
||||
r?.(g);
|
||||
},
|
||||
[r]
|
||||
), m = N(
|
||||
(d) => {
|
||||
t?.(d);
|
||||
), o = y(
|
||||
(g) => {
|
||||
t?.(g);
|
||||
},
|
||||
[t]
|
||||
), n = N(
|
||||
(d) => {
|
||||
d.selectionSet && a && a?.(d.state.selection);
|
||||
), c = y(
|
||||
(g) => {
|
||||
g.selectionSet && n && n?.(g.state.selection);
|
||||
},
|
||||
[a]
|
||||
[n]
|
||||
);
|
||||
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, {
|
||||
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(le, {
|
||||
value: e,
|
||||
onChange: u,
|
||||
onCreateEditor: m,
|
||||
onUpdate: n,
|
||||
extensions: me
|
||||
onCreateEditor: o,
|
||||
onUpdate: c,
|
||||
extensions: ke
|
||||
}));
|
||||
}
|
||||
function W(...e) {
|
||||
function T(...e) {
|
||||
return e.filter(Boolean).join(" ");
|
||||
}
|
||||
function pe({ view: e, pattern: r, active: t, getTime: a }) {
|
||||
const o = k([]), i = k();
|
||||
_(() => {
|
||||
function _e({ view: e, pattern: r, active: t, getTime: n }) {
|
||||
const s = N([]), m = N(0);
|
||||
k(() => {
|
||||
if (e)
|
||||
if (r && t) {
|
||||
let u = requestAnimationFrame(function m() {
|
||||
m.current = 0;
|
||||
let u = requestAnimationFrame(function o() {
|
||||
try {
|
||||
const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60];
|
||||
i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n);
|
||||
const h = r.queryArc(...v).filter((p) => p.hasOnset());
|
||||
o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) });
|
||||
const c = n(), b = [Math.max(m.current ?? c, c - 1 / 10, -0.01), c + 1 / 60];
|
||||
m.current = b[1], s.current = s.current.filter((h) => h.whole.end > c);
|
||||
const a = r.queryArc(...b).filter((h) => h.hasOnset());
|
||||
s.current = s.current.concat(a), e.dispatch({ effects: I.of(s.current) });
|
||||
} catch {
|
||||
e.dispatch({ effects: z.of([]) });
|
||||
e.dispatch({ effects: I.of([]) });
|
||||
}
|
||||
u = requestAnimationFrame(m);
|
||||
u = requestAnimationFrame(o);
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(u);
|
||||
};
|
||||
} else
|
||||
o.current = [], e.dispatch({ effects: z.of([]) });
|
||||
s.current = [], e.dispatch({ effects: I.of([]) });
|
||||
}, [r, t, e]);
|
||||
}
|
||||
function he(e, r = !1) {
|
||||
const t = k(), a = k(), o = (m) => {
|
||||
if (a.current !== void 0) {
|
||||
const n = m - a.current;
|
||||
e(m, n);
|
||||
function Me(e, r = !1) {
|
||||
const t = N(), n = N(), s = (o) => {
|
||||
if (n.current !== void 0) {
|
||||
const c = o - n.current;
|
||||
e(o, c);
|
||||
}
|
||||
a.current = m, t.current = requestAnimationFrame(o);
|
||||
}, i = () => {
|
||||
t.current = requestAnimationFrame(o);
|
||||
n.current = o, t.current = requestAnimationFrame(s);
|
||||
}, m = () => {
|
||||
t.current = requestAnimationFrame(s);
|
||||
}, u = () => {
|
||||
t.current && cancelAnimationFrame(t.current), delete t.current;
|
||||
};
|
||||
return _(() => {
|
||||
t.current && (u(), i());
|
||||
}, [e]), _(() => (r && i(), u), []), {
|
||||
start: i,
|
||||
return k(() => {
|
||||
t.current && (u(), m());
|
||||
}, [e]), k(() => (r && m(), u), []), {
|
||||
start: m,
|
||||
stop: u
|
||||
};
|
||||
}
|
||||
function ve({ pattern: e, started: r, getTime: t, onDraw: a }) {
|
||||
let o = k([]), i = k(null);
|
||||
const { start: u, stop: m } = he(
|
||||
N(() => {
|
||||
const n = t();
|
||||
if (i.current === null) {
|
||||
i.current = n;
|
||||
function Ae({ pattern: e, started: r, getTime: t, onDraw: n, drawTime: s = [-2, 2] }) {
|
||||
let [m, u] = s;
|
||||
m = Math.abs(m);
|
||||
let o = N([]), c = N(null);
|
||||
k(() => {
|
||||
if (e) {
|
||||
const a = t(), h = e.queryArc(Math.max(a, 0), a + u + 0.1);
|
||||
o.current = o.current.filter((v) => v.whole.begin < a), o.current = o.current.concat(h);
|
||||
}
|
||||
}, [e]);
|
||||
const { start: g, stop: b } = Me(
|
||||
y(() => {
|
||||
const a = t() + u;
|
||||
if (c.current === null) {
|
||||
c.current = a;
|
||||
return;
|
||||
}
|
||||
const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4;
|
||||
i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current);
|
||||
const h = e.queryArc(Math.max(c.current, a - 1 / 10), a);
|
||||
c.current = a, o.current = (o.current || []).filter((v) => v.whole.end >= a - m - u).concat(h.filter((v) => v.hasOnset())), n(e, a - u, o.current, s);
|
||||
}, [e])
|
||||
);
|
||||
_(() => {
|
||||
r ? u() : (o.current = [], m());
|
||||
}, [r]);
|
||||
}
|
||||
function be(e) {
|
||||
return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []);
|
||||
}
|
||||
function Ee({
|
||||
defaultOutput: e,
|
||||
interval: r,
|
||||
getTime: t,
|
||||
evalOnMount: a = !1,
|
||||
initialCode: o = "",
|
||||
autolink: i = !1,
|
||||
beforeEval: u,
|
||||
afterEval: m,
|
||||
editPattern: n,
|
||||
onEvalError: d,
|
||||
onToggle: v,
|
||||
canvasId: h
|
||||
}) {
|
||||
const p = K(() => we(), []);
|
||||
h = h || `canvas-${p}`;
|
||||
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(
|
||||
() => re({
|
||||
interval: r,
|
||||
defaultOutput: e,
|
||||
onSchedulerError: A,
|
||||
onEvalError: (g) => {
|
||||
D(g), d?.(g);
|
||||
},
|
||||
getTime: t,
|
||||
transpiler: ie,
|
||||
beforeEval: ({ code: g }) => {
|
||||
q(g), u?.();
|
||||
},
|
||||
editPattern: n ? (g) => n(g, p) : void 0,
|
||||
afterEval: ({ pattern: g, code: T }) => {
|
||||
V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.();
|
||||
},
|
||||
onToggle: (g) => {
|
||||
B(g), v?.(g);
|
||||
}
|
||||
}),
|
||||
[e, r, t]
|
||||
), Y = be(({ data: { from: g, type: T } }) => {
|
||||
T === "start" && g !== p && C();
|
||||
}), S = N(
|
||||
async (g = !0) => {
|
||||
await c(b, g), Y({ type: "start", from: p });
|
||||
},
|
||||
[c, b]
|
||||
), U = k();
|
||||
return _(() => {
|
||||
!U.current && a && b && (U.current = !0, S());
|
||||
}, [S, a, b]), _(() => () => {
|
||||
M.stop();
|
||||
}, [M]), {
|
||||
id: p,
|
||||
canvasId: h,
|
||||
code: b,
|
||||
setCode: q,
|
||||
error: F || P,
|
||||
schedulerError: F,
|
||||
scheduler: M,
|
||||
evalError: P,
|
||||
evaluate: c,
|
||||
activateCode: S,
|
||||
activeCode: x,
|
||||
isDirty: H,
|
||||
pattern: I,
|
||||
started: L,
|
||||
start: f,
|
||||
stop: C,
|
||||
pause: O,
|
||||
togglePlay: async () => {
|
||||
L ? M.pause() : await S();
|
||||
return k(() => {
|
||||
r ? g() : (o.current = [], b());
|
||||
}, [r]), {
|
||||
clear: () => {
|
||||
o.current = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
function we() {
|
||||
function Ne(e) {
|
||||
return k(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), y((r) => window.postMessage(r, "*"), []);
|
||||
}
|
||||
function De({
|
||||
defaultOutput: e,
|
||||
interval: r,
|
||||
getTime: t,
|
||||
evalOnMount: n = !1,
|
||||
initialCode: s = "",
|
||||
autolink: m = !1,
|
||||
beforeEval: u,
|
||||
afterEval: o,
|
||||
editPattern: c,
|
||||
onEvalError: g,
|
||||
onToggle: b,
|
||||
canvasId: a,
|
||||
drawContext: h,
|
||||
drawTime: v = [-2, 2]
|
||||
}) {
|
||||
const R = J(() => Ce(), []);
|
||||
a = a || `canvas-${R}`;
|
||||
const [L, x] = M(), [H, P] = M(), [E, S] = M(s), [V, q] = M(), [z, K] = M(), [D, O] = M(!1), j = E !== V, { scheduler: F, evaluate: C, start: d, stop: p, pause: X } = J(
|
||||
() => ge({
|
||||
interval: r,
|
||||
defaultOutput: e,
|
||||
onSchedulerError: x,
|
||||
onEvalError: (f) => {
|
||||
P(f), g?.(f);
|
||||
},
|
||||
getTime: t,
|
||||
drawContext: h,
|
||||
transpiler: ve,
|
||||
editPattern: c,
|
||||
beforeEval: ({ code: f }) => {
|
||||
S(f), u?.();
|
||||
},
|
||||
afterEval: ({ pattern: f, code: w }) => {
|
||||
q(w), K(f), P(), x(), m && (window.location.hash = "#" + encodeURIComponent(btoa(w))), o?.();
|
||||
},
|
||||
onToggle: (f) => {
|
||||
O(f), b?.(f);
|
||||
}
|
||||
}),
|
||||
[e, r, t]
|
||||
), U = Ne(({ data: { from: f, type: w } }) => {
|
||||
w === "start" && f !== R && p();
|
||||
}), Y = y(
|
||||
async (f = !0) => {
|
||||
const w = await C(E, f);
|
||||
return U({ type: "start", from: R }), w;
|
||||
},
|
||||
[C, E]
|
||||
), B = y(
|
||||
(f, w, $, G) => {
|
||||
const { onPaint: se } = f.context || {}, ie = typeof h == "function" ? h(a) : h;
|
||||
se?.(ie, w, $, G);
|
||||
},
|
||||
[h, a]
|
||||
), W = y(
|
||||
(f) => {
|
||||
if (h && B) {
|
||||
const [w, $] = v, G = f.queryArc(0, $);
|
||||
B(f, -1e-3, G, v);
|
||||
}
|
||||
},
|
||||
[h, v, B]
|
||||
), Z = N();
|
||||
k(() => {
|
||||
!Z.current && n && E && (Z.current = !0, C(E, !1).then((f) => W(f)));
|
||||
}, [n, E, C, W]), k(() => () => {
|
||||
F.stop();
|
||||
}, [F]);
|
||||
const ce = async () => {
|
||||
D ? (F.stop(), W(z)) : await Y();
|
||||
}, ae = L || H;
|
||||
return Ae({
|
||||
pattern: z,
|
||||
started: h && D,
|
||||
getTime: () => F.now(),
|
||||
drawTime: v,
|
||||
onDraw: B
|
||||
}), {
|
||||
id: R,
|
||||
canvasId: a,
|
||||
code: E,
|
||||
setCode: S,
|
||||
error: ae,
|
||||
schedulerError: L,
|
||||
scheduler: F,
|
||||
evalError: H,
|
||||
evaluate: C,
|
||||
activateCode: Y,
|
||||
activeCode: V,
|
||||
isDirty: j,
|
||||
pattern: z,
|
||||
started: D,
|
||||
start: d,
|
||||
stop: p,
|
||||
pause: X,
|
||||
togglePlay: ce
|
||||
};
|
||||
}
|
||||
function Ce() {
|
||||
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
|
||||
}
|
||||
function $({ type: e }) {
|
||||
function ee({ type: e }) {
|
||||
return /* @__PURE__ */ l.createElement("svg", {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: "sc-h-5 sc-w-5",
|
||||
@ -277,125 +316,127 @@ function $({ type: e }) {
|
||||
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"
|
||||
}),
|
||||
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]);
|
||||
}
|
||||
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 = {
|
||||
container: ye,
|
||||
header: ke,
|
||||
buttons: _e,
|
||||
button: Fe,
|
||||
buttonDisabled: Ce,
|
||||
error: Ne,
|
||||
body: xe
|
||||
}, Me = () => ce().currentTime;
|
||||
function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) {
|
||||
const {
|
||||
code: i,
|
||||
setCode: u,
|
||||
evaluate: m,
|
||||
activateCode: n,
|
||||
error: d,
|
||||
const Re = "_container_3i85k_1", xe = "_header_3i85k_5", Le = "_buttons_3i85k_9", qe = "_button_3i85k_9", ze = "_buttonDisabled_3i85k_17", He = "_error_3i85k_21", Pe = "_body_3i85k_25", _ = {
|
||||
container: Re,
|
||||
header: xe,
|
||||
buttons: Le,
|
||||
button: qe,
|
||||
buttonDisabled: ze,
|
||||
error: He,
|
||||
body: Pe
|
||||
}, Se = () => me().currentTime;
|
||||
function Ye({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, drawTime: n, punchcard: s, canvasHeight: m = 200 }) {
|
||||
n = n || (s ? [0, 4] : void 0);
|
||||
const u = !!n, o = y(
|
||||
n ? (d) => document.querySelector("#" + d)?.getContext("2d") : null,
|
||||
[n]
|
||||
), {
|
||||
code: c,
|
||||
setCode: g,
|
||||
evaluate: b,
|
||||
activateCode: a,
|
||||
error: h,
|
||||
isDirty: v,
|
||||
activeCode: h,
|
||||
pattern: p,
|
||||
started: F,
|
||||
scheduler: A,
|
||||
activeCode: R,
|
||||
pattern: L,
|
||||
started: x,
|
||||
scheduler: H,
|
||||
togglePlay: P,
|
||||
stop: D,
|
||||
canvasId: b,
|
||||
id: q
|
||||
} = Ee({
|
||||
stop: E,
|
||||
canvasId: S,
|
||||
id: V
|
||||
} = De({
|
||||
initialCode: e,
|
||||
defaultOutput: ae,
|
||||
getTime: Me,
|
||||
editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f }))
|
||||
});
|
||||
ve({
|
||||
pattern: p,
|
||||
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({
|
||||
defaultOutput: fe,
|
||||
editPattern: (d) => s ? d.punchcard() : d,
|
||||
getTime: Se,
|
||||
evalOnMount: u,
|
||||
drawContext: o,
|
||||
drawTime: n
|
||||
}), [q, z] = M(), [K, D] = he({
|
||||
threshold: 0.01
|
||||
}), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]);
|
||||
pe({
|
||||
view: x,
|
||||
pattern: p,
|
||||
active: F && !h?.includes("strudel disable-highlighting"),
|
||||
getTime: () => A.getPhase()
|
||||
}), G(() => {
|
||||
}), O = N(), j = J(() => ((D || !r) && (O.current = !0), D || O.current), [D, r]);
|
||||
_e({
|
||||
view: q,
|
||||
pattern: L,
|
||||
active: x && !R?.includes("strudel disable-highlighting"),
|
||||
getTime: () => H.now()
|
||||
}), te(() => {
|
||||
if (t) {
|
||||
const c = async (f) => {
|
||||
(f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault()));
|
||||
const d = async (p) => {
|
||||
(p.ctrlKey || p.altKey) && (p.code === "Enter" ? (p.preventDefault(), ye(q), await a()) : p.code === "Period" && (E(), p.preventDefault()));
|
||||
};
|
||||
return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0);
|
||||
return window.addEventListener("keydown", d, !0), () => window.removeEventListener("keydown", d, !0);
|
||||
}
|
||||
}, [t, p, i, m, D, x]);
|
||||
const [H, M] = w([]);
|
||||
return Ae(
|
||||
N((c) => {
|
||||
const { data: f } = c.detail;
|
||||
f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10));
|
||||
}, [t, L, c, b, E, q]);
|
||||
const [F, C] = M([]);
|
||||
return Ve(
|
||||
y((d) => {
|
||||
const { data: p } = d.detail;
|
||||
p?.hap?.context?.id === V && C((U) => U.concat([d.detail]).slice(-10));
|
||||
}, [])
|
||||
), /* @__PURE__ */ l.createElement("div", {
|
||||
className: E.container,
|
||||
ref: I
|
||||
className: _.container,
|
||||
ref: K
|
||||
}, /* @__PURE__ */ l.createElement("div", {
|
||||
className: E.header
|
||||
className: _.header
|
||||
}, /* @__PURE__ */ l.createElement("div", {
|
||||
className: E.buttons
|
||||
className: _.buttons
|
||||
}, /* @__PURE__ */ l.createElement("button", {
|
||||
className: W(E.button, F ? "sc-animate-pulse" : ""),
|
||||
className: T(_.button, x ? "sc-animate-pulse" : ""),
|
||||
onClick: () => P()
|
||||
}, /* @__PURE__ */ l.createElement($, {
|
||||
type: F ? "pause" : "play"
|
||||
}, /* @__PURE__ */ l.createElement(ee, {
|
||||
type: x ? "stop" : "play"
|
||||
})), /* @__PURE__ */ l.createElement("button", {
|
||||
className: W(v ? E.button : E.buttonDisabled),
|
||||
onClick: () => n()
|
||||
}, /* @__PURE__ */ l.createElement($, {
|
||||
className: T(v ? _.button : _.buttonDisabled),
|
||||
onClick: () => a()
|
||||
}, /* @__PURE__ */ l.createElement(ee, {
|
||||
type: "refresh"
|
||||
}))), d && /* @__PURE__ */ l.createElement("div", {
|
||||
className: E.error
|
||||
}, d.message)), /* @__PURE__ */ l.createElement("div", {
|
||||
className: E.body
|
||||
}, B && /* @__PURE__ */ l.createElement(ge, {
|
||||
value: i,
|
||||
onChange: u,
|
||||
onViewChanged: V
|
||||
})), a && /* @__PURE__ */ l.createElement("canvas", {
|
||||
id: b,
|
||||
}))), h && /* @__PURE__ */ l.createElement("div", {
|
||||
className: _.error
|
||||
}, h.message)), /* @__PURE__ */ l.createElement("div", {
|
||||
className: _.body
|
||||
}, j && /* @__PURE__ */ l.createElement(Fe, {
|
||||
value: c,
|
||||
onChange: g,
|
||||
onViewChanged: z
|
||||
})), n && /* @__PURE__ */ l.createElement("canvas", {
|
||||
id: S,
|
||||
className: "w-full pointer-events-none",
|
||||
height: o,
|
||||
ref: (c) => {
|
||||
c && c.width !== c.clientWidth && (c.width = c.clientWidth);
|
||||
height: m,
|
||||
ref: (d) => {
|
||||
d && d.width !== d.clientWidth && (d.width = d.clientWidth);
|
||||
}
|
||||
}), !!H.length && /* @__PURE__ */ l.createElement("div", {
|
||||
}), !!F.length && /* @__PURE__ */ l.createElement("div", {
|
||||
className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
|
||||
}, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", {
|
||||
key: f
|
||||
}, c))));
|
||||
}, F.map(({ message: d }, p) => /* @__PURE__ */ l.createElement("div", {
|
||||
key: p
|
||||
}, d))));
|
||||
}
|
||||
function Ae(e) {
|
||||
De(ne.key, e);
|
||||
function Ve(e) {
|
||||
Oe(pe.key, e);
|
||||
}
|
||||
function De(e, r, t = !1) {
|
||||
_(() => (document.addEventListener(e, r, t), () => {
|
||||
function Oe(e, r, t = !1) {
|
||||
k(() => (document.addEventListener(e, r, t), () => {
|
||||
document.removeEventListener(e, r, t);
|
||||
}), [r]);
|
||||
}
|
||||
const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||
const Ze = (e) => te(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||
export {
|
||||
ge as CodeMirror,
|
||||
je as MiniRepl,
|
||||
W as cx,
|
||||
de as flash,
|
||||
pe as useHighlighting,
|
||||
Ue as useKeydown,
|
||||
be as usePostMessage,
|
||||
Ee as useStrudel
|
||||
Fe as CodeMirror,
|
||||
Ye as MiniRepl,
|
||||
T as cx,
|
||||
ye as flash,
|
||||
_e as useHighlighting,
|
||||
Ze as useKeydown,
|
||||
Ne as usePostMessage,
|
||||
De as useStrudel
|
||||
};
|
||||
|
||||
@ -26,6 +26,13 @@ export function Icon({ type }) {
|
||||
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]
|
||||
}
|
||||
</svg>
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { pianoroll } from '@strudel.cycles/core';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import cx from '../cx';
|
||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import usePatternFrame from '../hooks/usePatternFrame.mjs';
|
||||
import useStrudel from '../hooks/useStrudel.mjs';
|
||||
import CodeMirror6, { flash } from './CodeMirror6';
|
||||
import { Icon } from './Icon';
|
||||
@ -15,7 +13,13 @@ import { logger } from '@strudel.cycles/core';
|
||||
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
|
||||
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
|
||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||
const evalOnMount = !!drawTime;
|
||||
const drawContext = useCallback(
|
||||
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||
[drawTime],
|
||||
);
|
||||
const {
|
||||
code,
|
||||
setCode,
|
||||
@ -34,25 +38,13 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
|
||||
} = useStrudel({
|
||||
initialCode: tune,
|
||||
defaultOutput: webaudioOutput,
|
||||
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
|
||||
getTime,
|
||||
editPattern: (pat, id) => {
|
||||
return pat.withContext((ctx) => ({ ...ctx, id }));
|
||||
},
|
||||
evalOnMount,
|
||||
drawContext,
|
||||
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 [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
@ -68,7 +60,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
@ -110,7 +102,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={started ? 'pause' : 'play'} />
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<Icon type="refresh" />
|
||||
@ -121,7 +113,7 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa
|
||||
<div className={styles.body}>
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||
</div>
|
||||
{withCanvas && (
|
||||
{drawTime && (
|
||||
<canvas
|
||||
id={canvasId}
|
||||
className="w-full pointer-events-none"
|
||||
|
||||
@ -3,16 +3,17 @@ import { setHighlights } from '../components/CodeMirror6';
|
||||
|
||||
function useHighlighting({ view, pattern, active, getTime }) {
|
||||
const highlights = useRef([]);
|
||||
const lastEnd = useRef();
|
||||
const lastEnd = useRef(0);
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
lastEnd.current = 0;
|
||||
let frame = requestAnimationFrame(function updateHighlights() {
|
||||
try {
|
||||
const audioTime = getTime();
|
||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||
// see https://github.com/tidalcycles/strudel/issues/108
|
||||
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy
|
||||
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
|
||||
const span = [begin, audioTime + 1 / 60];
|
||||
lastEnd.current = span[1];
|
||||
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
|
||||
@ -2,23 +2,32 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
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 lastFrame = useRef(null);
|
||||
useEffect(() => {
|
||||
if (pattern) {
|
||||
const t = getTime();
|
||||
const futureHaps = pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query..
|
||||
visibleHaps.current = visibleHaps.current.filter((h) => h.whole.begin < t);
|
||||
visibleHaps.current = visibleHaps.current.concat(futureHaps);
|
||||
}
|
||||
}, [pattern]);
|
||||
const { start: startFrame, stop: stopFrame } = useFrame(
|
||||
useCallback(() => {
|
||||
const phase = getTime();
|
||||
const phase = getTime() + lookahead;
|
||||
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
|
||||
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
onDraw(phase, visibleHaps.current);
|
||||
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
|
||||
}, [pattern]),
|
||||
);
|
||||
useEffect(() => {
|
||||
@ -29,6 +38,11 @@ function usePatternFrame({ pattern, started, getTime, onDraw }) {
|
||||
stopFrame();
|
||||
}
|
||||
}, [started]);
|
||||
return {
|
||||
clear: () => {
|
||||
visibleHaps.current = [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default usePatternFrame;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { repl } from '@strudel.cycles/core';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import usePatternFrame from './usePatternFrame';
|
||||
import usePostMessage from './usePostMessage.mjs';
|
||||
|
||||
function useStrudel({
|
||||
@ -16,6 +17,8 @@ function useStrudel({
|
||||
onEvalError,
|
||||
onToggle,
|
||||
canvasId,
|
||||
drawContext,
|
||||
drawTime = [-2, 2],
|
||||
}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
canvasId = canvasId || `canvas-${id}`;
|
||||
@ -40,12 +43,13 @@ function useStrudel({
|
||||
onEvalError?.(err);
|
||||
},
|
||||
getTime,
|
||||
drawContext,
|
||||
transpiler,
|
||||
editPattern,
|
||||
beforeEval: ({ code }) => {
|
||||
setCode(code);
|
||||
beforeEval?.();
|
||||
},
|
||||
editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined,
|
||||
afterEval: ({ pattern: _pattern, code }) => {
|
||||
setActiveCode(code);
|
||||
setPattern(_pattern);
|
||||
@ -71,19 +75,41 @@ function useStrudel({
|
||||
});
|
||||
const activateCode = useCallback(
|
||||
async (autostart = true) => {
|
||||
await evaluate(code, autostart);
|
||||
const res = await evaluate(code, autostart);
|
||||
broadcast({ type: 'start', from: id });
|
||||
return res;
|
||||
},
|
||||
[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);
|
||||
// draw at -0.001 to avoid activating haps at 0
|
||||
onDraw(pat, -0.001, haps, drawTime);
|
||||
}
|
||||
},
|
||||
[drawContext, drawTime, onDraw],
|
||||
);
|
||||
|
||||
const inited = useRef();
|
||||
useEffect(() => {
|
||||
if (!inited.current && evalOnMount && code) {
|
||||
inited.current = true;
|
||||
activateCode();
|
||||
evaluate(code, false).then((pat) => drawFirstFrame(pat));
|
||||
}
|
||||
}, [activateCode, evalOnMount, code]);
|
||||
}, [evalOnMount, code, evaluate, drawFirstFrame]);
|
||||
|
||||
// this will stop the scheduler when hot reloading in development
|
||||
useEffect(() => {
|
||||
@ -94,12 +120,22 @@ function useStrudel({
|
||||
|
||||
const togglePlay = async () => {
|
||||
if (started) {
|
||||
scheduler.pause();
|
||||
scheduler.stop();
|
||||
drawFirstFrame(pattern);
|
||||
} else {
|
||||
await activateCode();
|
||||
}
|
||||
};
|
||||
const error = schedulerError || evalError;
|
||||
|
||||
usePatternFrame({
|
||||
pattern,
|
||||
started: drawContext && started,
|
||||
getTime: () => scheduler.now(),
|
||||
drawTime,
|
||||
onDraw,
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
canvasId,
|
||||
|
||||
@ -3,8 +3,9 @@ import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { prebake } from '../repl/prebake';
|
||||
|
||||
let modules;
|
||||
if (typeof window !== 'undefined') {
|
||||
evalScope(
|
||||
modules = evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
@ -22,14 +23,20 @@ if (typeof window !== 'undefined') {
|
||||
prebake();
|
||||
}
|
||||
|
||||
export function MiniRepl({ tune, withCanvas }) {
|
||||
export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
|
||||
const [Repl, setRepl] = useState();
|
||||
useEffect(() => {
|
||||
// we have to load this package on the client
|
||||
// because codemirror throws an error on the server
|
||||
import('@strudel.cycles/react').then((res) => {
|
||||
setRepl(() => res.MiniRepl);
|
||||
});
|
||||
Promise.all([import('@strudel.cycles/react'), modules])
|
||||
.then(([res]) => setRepl(() => res.MiniRepl))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
return Repl ? <Repl tune={tune} hideOutsideView={true} withCanvas={withCanvas} /> : <pre>{tune}</pre>;
|
||||
return Repl ? (
|
||||
<div className="mb-4">
|
||||
<Repl tune={tune} hideOutsideView={true} drawTime={drawTime} punchcard={punchcard} canvasHeight={canvasHeight} />
|
||||
</div>
|
||||
) : (
|
||||
<pre>{tune}</pre>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
Try clicking the play icon below:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd sd")`} />
|
||||
<MiniRepl client:idle tune={`s("bd sd")`} punchcard />
|
||||
|
||||
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!
|
||||
@ -54,7 +54,7 @@ Alternatively, you can get a taste of what Strudel can do by clicking play on th
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
stack(
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8") // drums
|
||||
.speed(perlin.range(.7,.9)) // random sample speed variation
|
||||
,"<a1 b1\*2 a1(3,8) e2>" // bassline
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
|
||||
@ -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:
|
||||
|
||||
<MiniRepl client:idle tune={`note("e5 b4 d5 c5")`} withCanvas />
|
||||
<MiniRepl client:idle tune={`note("c e g b")`} punchcard />
|
||||
|
||||
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!
|
||||
|
||||
<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")`} punchcard />
|
||||
|
||||
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!
|
||||
@ -72,28 +72,28 @@ But, it will begin to make sense as we go through more elements of mini-notation
|
||||
|
||||
We can slow the sequence down by enclosing it in brackets and dividing it by a number (`/2`):
|
||||
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2")`} />
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2")`} punchcard />
|
||||
|
||||
The division by two means that the sequence will be played over the course of two cycles.
|
||||
You can also use decimal numbers for any tempo you like (`/2.75`).
|
||||
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2.75")`} />
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/2.75")`} punchcard />
|
||||
|
||||
## Angle Brackets
|
||||
|
||||
Using angle brackets `<>`, we can define the sequence length based on the number of events:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5>")`} />
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5>")`} punchcard />
|
||||
|
||||
The above snippet is the same as:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/4")`} />
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]/4")`} punchcard />
|
||||
|
||||
The advantage of the angle brackets, is that we can add more events without needing to change the number at the end.
|
||||
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5>")`} />
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5>")`} punchcard />
|
||||
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5 b4>")`} />
|
||||
<MiniRepl client:idle tune={`note("<e5 b4 d5 c5 e5 b4>")`} punchcard />
|
||||
|
||||
This is more similar to traditional music sequencers and piano rolls, where adding a note increases the perceived overall duration.
|
||||
|
||||
@ -101,15 +101,13 @@ This is more similar to traditional music sequencers and piano rolls, where addi
|
||||
|
||||
Contrary to division, a sequence can be sped up by multiplying it by a number using the asterisk symbol (`*`):
|
||||
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} />
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} punchcard />
|
||||
|
||||
The multiplication by two here means that the sequence will play twice a cycle.
|
||||
|
||||
As with divisions, multiplications can be decimal (`*2.75`):
|
||||
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} />
|
||||
|
||||
Actually, this is not true, but this will be [fixed](https://github.com/tidalcycles/strudel/issues/314) :)
|
||||
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2.75")`} punchcard />
|
||||
|
||||
## Subdividing time with bracket nesting
|
||||
|
||||
@ -133,7 +131,7 @@ Well, what this means is that in TidalCycles, not only can you divide time any w
|
||||
|
||||
The "~" represents a rest, and will create silence between other events:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} />
|
||||
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} punchcard />
|
||||
|
||||
## Parallel / polyphony
|
||||
|
||||
@ -141,17 +139,17 @@ Using commas, we can play chords.
|
||||
The following are the same:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4]")`} />
|
||||
<MiniRepl client:idle tune={`note("g3,b3,e4")`} />
|
||||
<MiniRepl client:idle tune={`note("g3,b3,e4")`} punchcard canvasHeight={80} />
|
||||
|
||||
But to play multiple chords in a sequence, we have to wrap them in brackets:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} punchcard />
|
||||
|
||||
## Elongation
|
||||
|
||||
With the "@" symbol, we can specify temporal "weight" of a sequence child:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
|
||||
|
||||
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
|
||||
|
||||
@ -159,7 +157,7 @@ Here, the first chord has a weight of 2, making it twice the length of the other
|
||||
|
||||
Using "!" we can repeat without speeding up:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} punchcard />
|
||||
|
||||
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
|
||||
|
||||
@ -181,24 +179,24 @@ Using round brackets after an event, we can create rhythmical sub-divisions base
|
||||
This algorithm can be found in many different types of music software, and is often referred to as a [Euclidean rhythm](https://en.wikipedia.org/wiki/Euclidean_rhythm) sequencer, after computer scientist Godfriend Toussaint.
|
||||
Why is it interesting? Well, consider the following simple example:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,0)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,0)")`} punchcard canvasHeight={50} />
|
||||
|
||||
Sound familiar?
|
||||
This is a popular Euclidian rhythm going by various names, such as "Pop Clave".
|
||||
These rhythms can be found in all musical cultures, and the Euclidian rhythm algorithm allows us to express them extremely easily.
|
||||
Writing this rhythm out in full require describing:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd ~ ~ bd ~ ~ bd ~")`} />
|
||||
<MiniRepl client:idle tune={`s("bd ~ ~ bd ~ ~ bd ~")`} punchcard canvasHeight={50} />
|
||||
|
||||
But using the Euclidian rhythm notation, we only need to express "3 beats over 8 segments, starting on position 1".
|
||||
|
||||
This makes it easy to write patterns with interesting rhythmic structures and variations that still sound familiar:
|
||||
|
||||
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} />
|
||||
<MiniRepl client:idle tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} punchcard canvasHeight={50} />
|
||||
|
||||
Note that since the example above does not use the third `offset` parameter, it can be written simply as `"(3,8)"`.
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd(3,8)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8)")`} punchcard canvasHeight={50} />
|
||||
|
||||
Let's look at those three parameters in detail.
|
||||
|
||||
@ -207,26 +205,26 @@ Let's look at those three parameters in detail.
|
||||
`beats`: the first parameter controls how may beats will be played.
|
||||
Compare these:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd(2,8)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(5,8)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(7,8)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(2,8)")`} punchcard canvasHeight={50} />
|
||||
<MiniRepl client:idle tune={`s("bd(5,8)")`} punchcard canvasHeight={50} />
|
||||
<MiniRepl client:idle tune={`s("bd(7,8)")`} punchcard canvasHeight={50} />
|
||||
|
||||
### Segments
|
||||
|
||||
`segments`: the second parameter controls the total amount of segments the beats will be distributed over:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd(3,4)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,13)")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,4)")`} punchcard canvasHeight={50} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8)")`} punchcard canvasHeight={50} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,13)")`} punchcard canvasHeight={50} />
|
||||
|
||||
### Offsets
|
||||
|
||||
`offset`: the third (optional) parameter controls the starting position for distributing the beats.
|
||||
We need a secondary rhythm to hear the difference:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,0), hh cp")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,3), hh cp")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,5), hh cp")`} />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,0), hh cp")`} punchcard />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,3), hh cp")`} punchcard />
|
||||
<MiniRepl client:idle tune={`s("bd(3,8,5), hh cp")`} punchcard />
|
||||
|
||||
## Mini-notation exercise
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ Here's the same pattern written in three different ways:
|
||||
|
||||
<MiniRepl client:idle tune={`freq("220 275 330 440")`} />
|
||||
|
||||
Let's look at `note`, `n` and `freq` in more detail...
|
||||
Let's look at those in more detail...
|
||||
|
||||
## `note` names
|
||||
|
||||
|
||||
@ -18,13 +18,13 @@ Strudel is written in JavaScript, while Tidal is written in Haskell.
|
||||
|
||||
This difference is most obvious when looking at the syntax:
|
||||
|
||||
```hs
|
||||
```haskell
|
||||
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
|
||||
```
|
||||
|
||||
One _could_ express that pattern to Strudel like so:
|
||||
|
||||
```txt
|
||||
```
|
||||
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
|
||||
```
|
||||
|
||||
@ -37,7 +37,7 @@ operators, or change the meaning of existing ones.
|
||||
|
||||
Before you discard Strudel as an unwieldy paren monster, look at this alternative way to write the above:
|
||||
|
||||
```txt
|
||||
```
|
||||
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
|
||||
```
|
||||
|
||||
@ -114,7 +114,7 @@ Also, samples are always loaded from a URL rather than from the disk, although [
|
||||
The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet.
|
||||
You can use the following "workaround" to create multiple patterns that can be turned on and off:
|
||||
|
||||
```txt
|
||||
```
|
||||
let a = note("c a f e")
|
||||
|
||||
let b = s("bd sd")
|
||||
@ -127,7 +127,7 @@ stack(
|
||||
|
||||
Alternatively, you could write everything as one `stack` and use `.hush()` to silence a pattern:
|
||||
|
||||
```txt
|
||||
```
|
||||
stack(
|
||||
note("c a f e"),
|
||||
s("bd sd").hush()
|
||||
@ -141,6 +141,6 @@ Note that strudel will always use the last statement in your code as the pattern
|
||||
Strudels tempo is 1 cycle per second, while tidal defaults to `0.5625`.
|
||||
You can get the same tempo as tidal with:
|
||||
|
||||
```txt
|
||||
```
|
||||
note("c a f e").fast(.5625);
|
||||
```
|
||||
|
||||
@ -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)
|
||||
- `tune`: be any valid pattern code
|
||||
- `withCanvas`: If set, a canvas will be rendered below that shows a pianoroll (WIP)
|
||||
- `punchcard`: if added, a punchcard / pianoroll visualization is renderd
|
||||
- `drawTime`: time window for drawing, defaults to `[0, 4]`
|
||||
- `canvasHeight`: height of the canvas, defaults to 100px
|
||||
|
||||
See `mini-notation.mdx` for usage examples
|
||||
|
||||
## In-Source Documentation
|
||||
|
||||
|
||||
@ -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/>.
|
||||
*/
|
||||
|
||||
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 {
|
||||
getAudioContext,
|
||||
@ -54,6 +54,12 @@ evalScope(
|
||||
export let loadedSamples = [];
|
||||
const presets = prebake();
|
||||
|
||||
let drawContext, clearCanvas;
|
||||
if (typeof window !== 'undefined') {
|
||||
drawContext = getDrawContext();
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
}
|
||||
|
||||
Promise.all([...modules, presets]).then((data) => {
|
||||
// console.log('modules and sample registry loade', data);
|
||||
loadedSamples = Object.entries(getLoadedSamples() || {});
|
||||
@ -125,6 +131,7 @@ export function Repl({ embedded = false }) {
|
||||
setPending(false);
|
||||
},
|
||||
onToggle: (play) => !play && cleanupDraw(false),
|
||||
drawContext,
|
||||
});
|
||||
|
||||
// init code
|
||||
@ -167,7 +174,7 @@ export function Repl({ embedded = false }) {
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
//
|
||||
@ -203,6 +210,7 @@ export function Repl({ embedded = false }) {
|
||||
const handleShuffle = async () => {
|
||||
const { code, name } = getRandomTune();
|
||||
logger(`[repl] ✨ loading random tune "${name}"`);
|
||||
clearCanvas();
|
||||
resetLoadedSamples();
|
||||
await prebake(); // declare default samples
|
||||
await evaluate(code, false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user