better draw cleanup + began reference tab

This commit is contained in:
Felix Roos 2022-11-13 14:50:48 +01:00
parent 8f31fbe275
commit c92cb1c096
9 changed files with 178 additions and 127 deletions

View File

@ -1,7 +1,7 @@
{ {
"source": { "source": {
"includePattern": ".+\\.(js(doc|x)?|mjs)$", "includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser" "excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
}, },
"plugins": ["plugins/markdown"], "plugins": ["plugins/markdown"],
"opts": { "opts": {

View File

@ -49,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
return this; return this;
}; };
export const cleanupDraw = () => { export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext(); const ctx = getDrawContext();
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) { if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation); cancelAnimationFrame(window.strudelAnimation);
} }

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,15 @@
import n, { useCallback as N, useRef as x, useEffect as R, useState as y, useMemo as q, useLayoutEffect as I } from "react"; import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
import J from "@uiw/react-codemirror"; import Q from "@uiw/react-codemirror";
import { Decoration as E, EditorView as K } from "@codemirror/view"; import { Decoration as E, EditorView as O } from "@codemirror/view";
import { StateEffect as O, StateField as j } from "@codemirror/state"; import { StateEffect as j, StateField as U } from "@codemirror/state";
import { javascript as Q } from "@codemirror/lang-javascript"; import { javascript as W } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight"; import { tags as r } from "@lezer/highlight";
import { createTheme as W } from "@uiw/codemirror-themes"; import { createTheme as X } from "@uiw/codemirror-themes";
import { useInView as X } from "react-hook-inview"; import { useInView as Y } from "react-hook-inview";
import { webaudioOutput as Y, getAudioContext as Z } from "@strudel.cycles/webaudio"; import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
import { repl as ee } from "@strudel.cycles/core"; import { repl as te } from "@strudel.cycles/core";
import { transpiler as te } from "@strudel.cycles/transpiler"; import { transpiler as re } from "@strudel.cycles/transpiler";
const re = W({ const oe = X({
theme: "dark", theme: "dark",
settings: { settings: {
background: "#222", background: "#222",
@ -42,14 +42,14 @@ const re = W({
{ tag: r.invalid, color: "#ffffff" } { tag: r.invalid, color: "#ffffff" }
] ]
}); });
const L = O.define(), oe = j.define({ const T = j.define(), ne = U.define({
create() { create() {
return E.none; return E.none;
}, },
update(e, t) { update(e, t) {
try { try {
for (let o of t.effects) for (let o of t.effects)
if (o.is(L)) if (o.is(T))
if (o.value) { if (o.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } }); const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]); e = E.set([a.range(0, t.newDoc.length)]);
@ -60,12 +60,12 @@ const L = O.define(), oe = j.define({
return console.warn("flash error", o), e; return console.warn("flash error", o), e;
} }
}, },
provide: (e) => K.decorations.from(e) provide: (e) => O.decorations.from(e)
}), ne = (e) => { }), ae = (e) => {
e.dispatch({ effects: L.of(!0) }), setTimeout(() => { e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
e.dispatch({ effects: L.of(!1) }); e.dispatch({ effects: T.of(!1) });
}, 200); }, 200);
}, A = O.define(), ae = j.define({ }, A = j.define(), se = U.define({
create() { create() {
return E.none; return E.none;
}, },
@ -74,11 +74,11 @@ const L = O.define(), oe = j.define({
for (let o of t.effects) for (let o of t.effects)
if (o.is(A)) { if (o.is(A)) {
const a = o.value.map( const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: u, end: l }) => { (s) => (s.context.locations || []).map(({ start: m, end: l }) => {
const d = s.context.color || "#FFCA28"; const d = s.context.color || "#FFCA28";
let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(l.line).from + l.column; let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
const f = t.newDoc.length; const g = t.newDoc.length;
return c > f || i > f ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i); return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
}) })
).flat().filter(Boolean) || []; ).flat().filter(Boolean) || [];
e = E.set(a, !0); e = E.set(a, !0);
@ -88,9 +88,9 @@ const L = O.define(), oe = j.define({
return E.set([]); return E.set([]);
} }
}, },
provide: (e) => K.decorations.from(e) provide: (e) => O.decorations.from(e)
}), se = [Q(), re, ae, oe]; }), ce = [W(), oe, se, ne];
function ce({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) { function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
const l = N( const l = N(
(i) => { (i) => {
t?.(i); t?.(i);
@ -107,27 +107,27 @@ function ce({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, opt
}, },
[a] [a]
); );
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(J, { return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
value: e, value: e,
onChange: l, onChange: l,
onCreateEditor: d, onCreateEditor: d,
onUpdate: c, onUpdate: c,
extensions: se extensions: ce
})); }));
} }
function S(...e) { function B(...e) {
return e.filter(Boolean).join(" "); return e.filter(Boolean).join(" ");
} }
function ie({ view: e, pattern: t, active: o, getTime: a }) { function le({ view: e, pattern: t, active: o, getTime: a }) {
const s = x([]), u = x(); const s = x([]), m = x();
R(() => { R(() => {
if (e) if (e)
if (t && o) { if (t && o) {
let d = function() { let d = function() {
try { try {
const c = a(), f = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60]; const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
u.current = f[1], s.current = s.current.filter((m) => m.whole.end > c); m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
const v = t.queryArc(...f).filter((m) => m.hasOnset()); const v = t.queryArc(...g).filter((p) => p.hasOnset());
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) }); s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
} catch { } catch {
e.dispatch({ effects: A.of([]) }); e.dispatch({ effects: A.of([]) });
@ -141,16 +141,16 @@ function ie({ view: e, pattern: t, active: o, getTime: a }) {
s.current = [], e.dispatch({ effects: A.of([]) }); s.current = [], e.dispatch({ effects: A.of([]) });
}, [t, o, e]); }, [t, o, e]);
} }
const le = "_container_3i85k_1", de = "_header_3i85k_5", ue = "_buttons_3i85k_9", fe = "_button_3i85k_9", me = "_buttonDisabled_3i85k_17", ge = "_error_3i85k_21", pe = "_body_3i85k_25", b = { const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = {
container: le, container: de,
header: de, header: ue,
buttons: ue, buttons: fe,
button: fe, button: me,
buttonDisabled: me, buttonDisabled: ge,
error: ge, error: pe,
body: pe body: he
}; };
function B({ type: e }) { function q({ type: e }) {
return /* @__PURE__ */ n.createElement("svg", { return /* @__PURE__ */ n.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",
@ -174,97 +174,100 @@ function B({ type: e }) {
}) })
}[e]); }[e]);
} }
function he({ function ve({
defaultOutput: e, defaultOutput: e,
interval: t, interval: t,
getTime: o, getTime: o,
evalOnMount: a = !1, evalOnMount: a = !1,
initialCode: s = "", initialCode: s = "",
autolink: u = !1, autolink: m = !1,
beforeEval: l, beforeEval: l,
afterEval: d, afterEval: d,
onEvalError: c onEvalError: c,
onToggle: i
}) { }) {
const [i, f] = y(), [v, m] = y(), [h, D] = y(s), [_, C] = y(h), [P, z] = y(), [k, F] = y(!1), H = h !== _, { scheduler: w, evaluate: g, start: U, stop: $, pause: G } = q( const [g, v] = w(), [p, D] = w(), [h, k] = w(s), [y, P] = w(h), [z, _] = w(), [C, H] = w(!1), F = h !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I(
() => ee({ () => te({
interval: t, interval: t,
defaultOutput: e, defaultOutput: e,
onSchedulerError: f, onSchedulerError: v,
onEvalError: (p) => { onEvalError: (f) => {
m(p), c?.(p); D(f), c?.(f);
}, },
getTime: o, getTime: o,
transpiler: te, transpiler: re,
beforeEval: ({ code: p }) => { beforeEval: ({ code: f }) => {
D(p), l?.(); k(f), l?.();
}, },
afterEval: ({ pattern: p, code: V }) => { afterEval: ({ pattern: f, code: S }) => {
C(V), z(p), m(), f(), u && (window.location.hash = "#" + encodeURIComponent(btoa(V))), d?.(); P(S), _(f), D(), v(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.();
}, },
onToggle: (p) => F(p) onToggle: (f) => {
H(f), i?.(f);
}
}), }),
[e, t, o] [e, t, o]
), M = N(async (p = !0) => g(h, p), [g, h]), T = x(); ), M = N(async (f = !0) => L(h, f), [L, h]), V = x();
return R(() => { return R(() => {
!T.current && a && h && (T.current = !0, M()); !V.current && a && h && (V.current = !0, M());
}, [M, a, h]), R(() => () => { }, [M, a, h]), R(() => () => {
w.stop(); u.stop();
}, [w]), { }, [u]), {
code: h, code: h,
setCode: D, setCode: k,
error: i || v, error: g || p,
schedulerError: i, schedulerError: g,
scheduler: w, scheduler: u,
evalError: v, evalError: p,
evaluate: g, evaluate: L,
activateCode: M, activateCode: M,
activeCode: _, activeCode: y,
isDirty: H, isDirty: F,
pattern: P, pattern: z,
started: k, started: C,
start: U, start: $,
stop: $, stop: G,
pause: G, pause: J,
togglePlay: async () => { togglePlay: async () => {
k ? w.pause() : await M(); C ? u.pause() : await M();
} }
}; };
} }
const ve = () => Z().currentTime; const be = () => ee().currentTime;
function Re({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) { function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const { const {
code: s, code: s,
setCode: u, setCode: m,
evaluate: l, evaluate: l,
activateCode: d, activateCode: d,
error: c, error: c,
isDirty: i, isDirty: i,
activeCode: f, activeCode: g,
pattern: v, pattern: v,
started: m, started: p,
scheduler: h, scheduler: D,
togglePlay: D, togglePlay: h,
stop: _ stop: k
} = he({ } = ve({
initialCode: e, initialCode: e,
defaultOutput: Y, defaultOutput: Z,
getTime: ve getTime: be
}), [C, P] = y(), [z, k] = X({ }), [y, P] = w(), [z, _] = Y({
threshold: 0.01 threshold: 0.01
}), F = x(), H = q(() => ((k || !t) && (F.current = !0), k || F.current), [k, t]); }), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]);
return ie({ return le({
view: C, view: y,
pattern: v, pattern: v,
active: m && !f?.includes("strudel disable-highlighting"), active: p && !g?.includes("strudel disable-highlighting"),
getTime: () => h.getPhase() getTime: () => D.getPhase()
}), I(() => { }), K(() => {
if (a) { if (a) {
const w = async (g) => { const F = async (u) => {
(g.ctrlKey || g.altKey) && (g.code === "Enter" ? (g.preventDefault(), ne(C), await d()) : g.code === "Period" && (_(), g.preventDefault())); (u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault()));
}; };
return window.addEventListener("keydown", w, !0), () => window.removeEventListener("keydown", w, !0); return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0);
} }
}, [a, v, s, l, _, C]), /* @__PURE__ */ n.createElement("div", { }, [a, v, s, l, k, y]), /* @__PURE__ */ n.createElement("div", {
className: b.container, className: b.container,
ref: z ref: z
}, /* @__PURE__ */ n.createElement("div", { }, /* @__PURE__ */ n.createElement("div", {
@ -272,36 +275,36 @@ function Re({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
}, /* @__PURE__ */ n.createElement("div", { }, /* @__PURE__ */ n.createElement("div", {
className: b.buttons className: b.buttons
}, /* @__PURE__ */ n.createElement("button", { }, /* @__PURE__ */ n.createElement("button", {
className: S(b.button, m ? "sc-animate-pulse" : ""), className: B(b.button, p ? "sc-animate-pulse" : ""),
onClick: () => D() onClick: () => h()
}, /* @__PURE__ */ n.createElement(B, { }, /* @__PURE__ */ n.createElement(q, {
type: m ? "pause" : "play" type: p ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", { })), /* @__PURE__ */ n.createElement("button", {
className: S(i ? b.button : b.buttonDisabled), className: B(i ? b.button : b.buttonDisabled),
onClick: () => d() onClick: () => d()
}, /* @__PURE__ */ n.createElement(B, { }, /* @__PURE__ */ n.createElement(q, {
type: "refresh" type: "refresh"
}))), c && /* @__PURE__ */ n.createElement("div", { }))), c && /* @__PURE__ */ n.createElement("div", {
className: b.error className: b.error
}, c.message)), /* @__PURE__ */ n.createElement("div", { }, c.message)), /* @__PURE__ */ n.createElement("div", {
className: b.body className: b.body
}, H && /* @__PURE__ */ n.createElement(ce, { }, H && /* @__PURE__ */ n.createElement(ie, {
value: s, value: s,
onChange: u, onChange: m,
onViewChanged: P onViewChanged: P
}))); })));
} }
function Pe(e) { function ze(e) {
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []); return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
} }
const ze = (e) => I(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export { export {
ce as CodeMirror, ie as CodeMirror,
Re as MiniRepl, Pe as MiniRepl,
S as cx, B as cx,
ne as flash, ae as flash,
ie as useHighlighting, le as useHighlighting,
ze as useKeydown, He as useKeydown,
Pe as usePostMessage, ze as usePostMessage,
he as useStrudel ve as useStrudel
}; };

View File

@ -12,6 +12,7 @@ function useStrudel({
beforeEval, beforeEval,
afterEval, afterEval,
onEvalError, onEvalError,
onToggle,
}) { }) {
// scheduler // scheduler
const [schedulerError, setSchedulerError] = useState(); const [schedulerError, setSchedulerError] = useState();
@ -49,7 +50,10 @@ function useStrudel({
} }
afterEval?.(); afterEval?.();
}, },
onToggle: (v) => setStarted(v), onToggle: (v) => {
setStarted(v);
onToggle?.(v);
},
}), }),
[defaultOutput, interval, getTime], [defaultOutput, interval, getTime],
); );

View File

@ -3,6 +3,7 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"predev": "cd ${PWD}/../tutorial/ && npm run render",
"dev": "vite --host", "dev": "vite --host",
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",

View File

@ -107,7 +107,7 @@ export const AppContext = createContext();
function App() { function App() {
const [view, setView] = useState(); // codemirror view const [view, setView] = useState(); // codemirror view
const [lastShared, setLastShared] = useState(); const [lastShared, setLastShared] = useState();
const [activeFooter, setActiveFooter] = useState('console'); const [activeFooter, setActiveFooter] = useState('');
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop } = useStrudel({ const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop } = useStrudel({
initialCode: '// LOADING', initialCode: '// LOADING',
@ -118,6 +118,7 @@ function App() {
cleanupUi(); cleanupUi();
cleanupDraw(); cleanupDraw();
}, },
onToggle: (play) => !play && cleanupDraw(false),
}); });
// init code // init code

View File

@ -4,6 +4,7 @@ import { cx } from '@strudel.cycles/react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import React, { useContext, useCallback, useLayoutEffect, useRef, useState } from 'react'; import React, { useContext, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useEvent, loadedSamples, AppContext } from './App'; import { useEvent, loadedSamples, AppContext } from './App';
import { Reference } from './Reference';
export function Footer() { export function Footer() {
// const [activeFooter, setActiveFooter] = useState('console'); // const [activeFooter, setActiveFooter] = useState('console');
@ -67,10 +68,11 @@ export function Footer() {
return ( return (
<footer className="bg-footer z-[20]"> <footer className="bg-footer z-[20]">
<div className="flex justify-between px-2"> <div className="flex justify-between px-2">
<div className="flex pb-2 select-none"> <div className={cx('flex select-none', activeFooter && 'pb-2')}>
<FooterTab name="intro" /> <FooterTab name="intro" label="welcome" />
<FooterTab name="samples" /> <FooterTab name="samples" />
<FooterTab name="console" label={`console (${log.length})`} /> <FooterTab name="console" />
<FooterTab name="reference" />
</div> </div>
{activeFooter !== '' && ( {activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-white"> <button onClick={() => setActiveFooter('')} className="text-white">
@ -80,11 +82,11 @@ export function Footer() {
</div> </div>
{activeFooter !== '' && ( {activeFooter !== '' && (
<div <div
className="text-white font-mono text-sm h-72 max-h-[33vh] flex-none overflow-auto max-w-full px-4" className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
ref={footerContent} ref={footerContent}
> >
{activeFooter === 'intro' && ( {activeFooter === 'intro' && (
<div className="prose prose-invert max-w-[600px] pt-2 font-sans pb-8"> <div className="prose prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
<h3> <h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome <span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3> </h3>
@ -129,7 +131,7 @@ export function Footer() {
</div> </div>
)} )}
{activeFooter === 'console' && ( {activeFooter === 'console' && (
<div className="break-all"> <div className="break-all px-4">
{log.map((l, i) => { {log.map((l, i) => {
const message = linkify(l.message); const message = linkify(l.message);
return ( return (
@ -145,7 +147,7 @@ export function Footer() {
</div> </div>
)} )}
{activeFooter === 'samples' && ( {activeFooter === 'samples' && (
<div className="break-normal w-full"> <div className="break-normal w-full px-4">
<span className="text-white">{loadedSamples.length} banks loaded:</span> <span className="text-white">{loadedSamples.length} banks loaded:</span>
{loadedSamples.map(([name, samples]) => ( {loadedSamples.map(([name, samples]) => (
<span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}> <span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}>
@ -161,6 +163,7 @@ export function Footer() {
))} ))}
</div> </div>
)} )}
{activeFooter === 'reference' && <Reference />}
</div> </div>
)} )}
</footer> </footer>

39
repl/src/Reference.jsx Normal file
View File

@ -0,0 +1,39 @@
import jsdocJson from '../../doc.json';
console.log('jsdocJson', jsdocJson);
const visibleFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => a.meta.filename.localeCompare(b.meta.filename) + a.name.localeCompare(b.name));
export function Reference() {
return (
<div className="flex h-full w-full pt-2">
<div className="w-64 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
{visibleFunctions.map((entry, i) => (
<a key={i} className="cursor-pointer block hover:bg-linegray py-1 px-4" href={`#doc-${i}`}>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative">
<div className="prose prose-invert">
<h2>API Reference</h2>
<p>
This is the long list functions you can use! Remember that you don't need to remember all of those and that
you can already make music with a small set of functions!
</p>
{visibleFunctions.map((entry, i) => (
<section key={i}>
<h3 id={`doc-${i}`}>{entry.name}</h3>
<small>{entry.meta.filename}</small>
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
{entry.examples?.map((example, j) => (
<pre key={j}>{example}</pre>
))}
</section>
))}
</div>
</div>
</div>
);
}