mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-12 06:08:34 +00:00
148 lines
6.1 KiB
JavaScript
148 lines
6.1 KiB
JavaScript
import React, {useCallback, useLayoutEffect, useRef, useState} from "../_snowpack/pkg/react.js";
|
|
import CodeMirror, {markEvent, markParens} from "./CodeMirror.js";
|
|
import cx from "./cx.js";
|
|
import {evaluate} from "./evaluate.js";
|
|
import logo from "./logo.svg.proxy.js";
|
|
import {useWebMidi} from "./midi.js";
|
|
import playStatic from "./static.js";
|
|
import {defaultSynth} from "./tone.js";
|
|
import * as tunes from "./tunes.js";
|
|
import useRepl from "./useRepl.js";
|
|
const [_, codeParam] = window.location.href.split("#");
|
|
let decoded;
|
|
try {
|
|
decoded = atob(decodeURIComponent(codeParam || ""));
|
|
} catch (err) {
|
|
console.warn("failed to decode", err);
|
|
}
|
|
function getRandomTune() {
|
|
const allTunes = Object.values(tunes);
|
|
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
return randomItem(allTunes);
|
|
}
|
|
const randomTune = getRandomTune();
|
|
function App() {
|
|
const [editor, setEditor] = useState();
|
|
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending} = useRepl({
|
|
tune: decoded || randomTune,
|
|
defaultSynth,
|
|
onDraw: useCallback(markEvent(editor), [editor])
|
|
});
|
|
const [uiHidden, setUiHidden] = useState(false);
|
|
const logBox = useRef();
|
|
useLayoutEffect(() => {
|
|
logBox.current.scrollTop = logBox.current?.scrollHeight;
|
|
}, [log]);
|
|
useLayoutEffect(() => {
|
|
const handleKeyPress = async (e) => {
|
|
if (e.ctrlKey || e.altKey) {
|
|
switch (e.code) {
|
|
case "Enter":
|
|
await activateCode();
|
|
break;
|
|
case "Period":
|
|
cycle.stop();
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener("keypress", handleKeyPress);
|
|
return () => document.removeEventListener("keypress", handleKeyPress);
|
|
}, [pattern, code]);
|
|
useWebMidi({
|
|
ready: useCallback(({outputs}) => {
|
|
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(" | ")}) to the pattern. `);
|
|
}, []),
|
|
connected: useCallback(({outputs}) => {
|
|
pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(", ")}`);
|
|
}, []),
|
|
disconnected: useCallback(({outputs}) => {
|
|
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(", ")}`);
|
|
}, [])
|
|
});
|
|
return /* @__PURE__ */ React.createElement("div", {
|
|
className: "min-h-screen flex flex-col"
|
|
}, /* @__PURE__ */ React.createElement("header", {
|
|
id: "header",
|
|
className: cx("flex-none w-full h-14 px-2 flex border-b border-gray-200 justify-between z-[10]", uiHidden ? "bg-transparent text-white" : "bg-white")
|
|
}, /* @__PURE__ */ React.createElement("div", {
|
|
className: "flex items-center space-x-2"
|
|
}, /* @__PURE__ */ React.createElement("img", {
|
|
src: logo,
|
|
className: "Tidal-logo w-12 h-12",
|
|
alt: "logo"
|
|
}), /* @__PURE__ */ React.createElement("h1", {
|
|
className: "text-2xl"
|
|
}, "Strudel REPL")), /* @__PURE__ */ React.createElement("div", {
|
|
className: "flex space-x-4"
|
|
}, /* @__PURE__ */ React.createElement("button", {
|
|
onClick: () => togglePlay()
|
|
}, !pending ? /* @__PURE__ */ React.createElement("span", {
|
|
className: "flex items-center w-16"
|
|
}, cycle.started ? /* @__PURE__ */ React.createElement("svg", {
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
className: "h-5 w-5",
|
|
viewBox: "0 0 20 20",
|
|
fill: "currentColor"
|
|
}, /* @__PURE__ */ React.createElement("path", {
|
|
fillRule: "evenodd",
|
|
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
|
clipRule: "evenodd"
|
|
})) : /* @__PURE__ */ React.createElement("svg", {
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
className: "h-5 w-5",
|
|
viewBox: "0 0 20 20",
|
|
fill: "currentColor"
|
|
}, /* @__PURE__ */ React.createElement("path", {
|
|
fillRule: "evenodd",
|
|
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
|
clipRule: "evenodd"
|
|
})), cycle.started ? "pause" : "play") : /* @__PURE__ */ React.createElement(React.Fragment, null, "loading...")), /* @__PURE__ */ React.createElement("button", {
|
|
onClick: async () => {
|
|
const _code = getRandomTune();
|
|
console.log("tune", _code);
|
|
setCode(_code);
|
|
const parsed = await evaluate(_code);
|
|
setPattern(parsed.pattern);
|
|
}
|
|
}, "🎲 random"), /* @__PURE__ */ React.createElement("button", null, /* @__PURE__ */ React.createElement("a", {
|
|
href: "./tutorial"
|
|
}, "📚 tutorial")), /* @__PURE__ */ React.createElement("button", {
|
|
onClick: () => setUiHidden((c) => !c)
|
|
}, "👀 ", uiHidden ? "show ui" : "hide ui"))), /* @__PURE__ */ React.createElement("section", {
|
|
className: "grow flex flex-col text-gray-100"
|
|
}, /* @__PURE__ */ React.createElement("div", {
|
|
className: "grow relative",
|
|
id: "code"
|
|
}, /* @__PURE__ */ React.createElement("div", {
|
|
className: cx("h-full transition-opacity", error ? "focus:ring-red-500" : "focus:ring-slate-800", uiHidden ? "opacity-0" : "opacity-100")
|
|
}, /* @__PURE__ */ React.createElement(CodeMirror, {
|
|
value: code,
|
|
editorDidMount: setEditor,
|
|
options: {
|
|
mode: "javascript",
|
|
theme: "material",
|
|
lineNumbers: false,
|
|
styleSelectedText: true,
|
|
cursorBlinkRate: 0
|
|
},
|
|
onCursor: markParens,
|
|
onChange: (_2, __, value) => setCode(value)
|
|
}), /* @__PURE__ */ React.createElement("span", {
|
|
className: "p-4 absolute top-0 right-0 text-xs whitespace-pre text-right pointer-events-none"
|
|
}, !cycle.started ? `press ctrl+enter to play
|
|
` : dirty ? `ctrl+enter to update
|
|
` : "no changes\n")), error && /* @__PURE__ */ React.createElement("div", {
|
|
className: cx("absolute right-2 bottom-2 px-2", "text-red-500")
|
|
}, error?.message || "unknown error")), /* @__PURE__ */ React.createElement("textarea", {
|
|
className: "z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none",
|
|
value: log,
|
|
readOnly: true,
|
|
ref: logBox,
|
|
style: {fontFamily: "monospace"}
|
|
})), /* @__PURE__ */ React.createElement("button", {
|
|
className: "fixed right-4 bottom-2 z-[11]",
|
|
onClick: () => playStatic(code)
|
|
}, "static"));
|
|
}
|
|
export default App;
|