diff --git a/package-lock.json b/package-lock.json index ff7539db..a40ec855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,12 @@ "version": "1.0.2", "license": "GPL-3.0-or-later", "dependencies": { - "fraction.js": "^4.1.2" + "automation-events": "^4.0.12", + "fraction.js": "^4.1.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "standardized-audio-context": "^25.3.20", + "tone": "^14.7.77" }, "devDependencies": { "mocha": "^9.1.4", @@ -122,6 +127,17 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz", + "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", @@ -689,6 +705,18 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "node_modules/automation-events": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.13.tgz", + "integrity": "sha512-SYHkG0A0x+JwLBHSexZhlv9mcYlvFpen9S2zTNjchfp5EDSbrs3Fm9Teje0PWpUNTV5W1C/kEPdZ4RTrGOm7Hg==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12.20.1" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -2513,8 +2541,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2688,6 +2715,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -3248,7 +3286,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3860,6 +3897,31 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, "node_modules/read-cmd-shim": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz", @@ -3924,6 +3986,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -4095,6 +4162,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -4352,6 +4428,16 @@ "node": ">= 8" } }, + "node_modules/standardized-audio-context": { + "version": "25.3.20", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.20.tgz", + "integrity": "sha512-c6eMQXmN7iDS7ROuSqOrHQhxpazerJSnRHEJiKD8YkruZBTt/a5E7zmk+KkStoi0dohFAod8wvwWxc7S1gmdig==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "automation-events": "^4.0.12", + "tslib": "^2.3.1" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4506,6 +4592,15 @@ "node": ">=8.0" } }, + "node_modules/tone": { + "version": "14.7.77", + "resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz", + "integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==", + "dependencies": { + "standardized-audio-context": "^25.1.8", + "tslib": "^2.0.1" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -4528,8 +4623,7 @@ "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -4984,6 +5078,14 @@ } } }, + "@babel/runtime": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz", + "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@gar/promisify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", @@ -5459,6 +5561,15 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "automation-events": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.13.tgz", + "integrity": "sha512-SYHkG0A0x+JwLBHSexZhlv9mcYlvFpen9S2zTNjchfp5EDSbrs3Fm9Teje0PWpUNTV5W1C/kEPdZ4RTrGOm7Hg==", + "requires": { + "@babel/runtime": "^7.17.0", + "tslib": "^2.3.1" + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -6861,8 +6972,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "4.1.0", @@ -7003,6 +7113,14 @@ "is-unicode-supported": "^0.1.0" } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7423,8 +7541,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", @@ -7873,6 +7990,25 @@ "safe-buffer": "^5.1.0" } }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, "read-cmd-shim": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz", @@ -7933,6 +8069,11 @@ "picomatch": "^2.2.1" } }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -8053,6 +8194,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -8255,6 +8405,16 @@ "minipass": "^3.1.1" } }, + "standardized-audio-context": { + "version": "25.3.20", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.20.tgz", + "integrity": "sha512-c6eMQXmN7iDS7ROuSqOrHQhxpazerJSnRHEJiKD8YkruZBTt/a5E7zmk+KkStoi0dohFAod8wvwWxc7S1gmdig==", + "requires": { + "@babel/runtime": "^7.17.0", + "automation-events": "^4.0.12", + "tslib": "^2.3.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8371,6 +8531,15 @@ "is-number": "^7.0.0" } }, + "tone": { + "version": "14.7.77", + "resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz", + "integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==", + "requires": { + "standardized-audio-context": "^25.1.8", + "tslib": "^2.0.1" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -8390,8 +8559,7 @@ "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tunnel-agent": { "version": "0.6.0", diff --git a/repl/public/hot.js b/repl/public/hot.js new file mode 100644 index 00000000..eeaed770 --- /dev/null +++ b/repl/public/hot.js @@ -0,0 +1,45 @@ +// this file can be used to livecode from the comfort of your editor. +// just export a pattern from export default +// enable hot mode by pressing "toggle hot mode" on the top right of the repl + +import { mini, h } from '../src/parse'; +import { sequence, pure, reify, slowcat, fastcat, cat, stack, silence } from '../../strudel.mjs'; +import { gain, filter } from '../src/tone'; + +export default stack( + sequence( + mini( + 'e5 [b4 c5] d5 [c5 b4]', + 'a4 [a4 c5] e5 [d5 c5]', + 'b4 [~ c5] d5 e5', + 'c5 a4 a4 ~', + '[~ d5] [~ f5] a5 [g5 f5]', + 'e5 [~ c5] e5 [d5 c5]', + 'b4 [b4 c5] d5 e5', + 'c5 a4 a4 ~' + ) + .synth({ + oscillator: { type: 'sine' }, + envelope: { attack: 0.1 }, + }) + .rev() + ), + sequence( + mini( + 'e2 e3 e2 e3 e2 e3 e2 e3', + 'a2 a3 a2 a3 a2 a3 a2 a3', + 'g#2 g#3 g#2 g#3 e2 e3 e2 e3', + 'a2 a3 a2 a3 a2 a3 b1 c2', + 'd2 d3 d2 d3 d2 d3 d2 d3', + 'c2 c3 c2 c3 c2 c3 c2 c3', + 'b1 b2 b1 b2 e2 e3 e2 e3', + 'a1 a2 a1 a2 a1 a2 a1 a2' + ) + .synth({ + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.1 }, + }) + .chain(gain(0.7), filter(2000)) + .rev() + ) +).slow(16); diff --git a/repl/snowpack.config.mjs b/repl/snowpack.config.mjs index cf4a7a53..9fa4cf7a 100644 --- a/repl/snowpack.config.mjs +++ b/repl/snowpack.config.mjs @@ -27,7 +27,7 @@ export default { }, packageOptions: { /* ... */ - knownEntrypoints: ['fraction.js'], + knownEntrypoints: ['fraction.js', 'codemirror'], }, devOptions: { tailwindConfig: './tailwind.config.js', diff --git a/repl/src/App.tsx b/repl/src/App.tsx index faad502a..abb3e3f2 100644 --- a/repl/src/App.tsx +++ b/repl/src/App.tsx @@ -1,22 +1,27 @@ -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import logo from './logo.svg'; -import * as strudel from '../../strudel.mjs'; import cx from './cx'; import * as Tone from 'tone'; import useCycle from './useCycle'; -import type { Hap, Pattern } from './types'; +import type { Pattern } from './types'; import * as tunes from './tunes'; -import * as krill from './parse'; +import * as parser from './parse'; import CodeMirror from './CodeMirror'; +import hot from '../public/hot'; -const { tetris, tetrisMini, tetrisHaskell } = tunes; +const { tetris, tetrisRev } = tunes; +const { parse } = parser; -const { sequence, pure, reify, slowcat, fastcat, cat, stack, silence } = strudel; // make available to eval -const { mini, h } = krill; // for eval (direct import wont work somehow) -const parse = (code: string): Pattern => eval(code); +const getHotCode = async () => { + return fetch('/hot.js') + .then((res) => res.text()) + .then((src) => { + return src.split('export default').slice(-1)[0].trim(); + }); +}; -const synth = new Tone.PolySynth().toDestination(); -synth.set({ +const defaultSynth = new Tone.PolySynth().toDestination(); +defaultSynth.set({ oscillator: { type: 'triangle' }, envelope: { release: 0.01, @@ -25,11 +30,13 @@ synth.set({ function App() { const [mode, setMode] = useState('javascript'); - const [code, setCode] = useState(tetrisHaskell); + const [code, setCode] = useState(tetrisRev); const [log, setLog] = useState(''); const logBox = useRef(); const [error, setError] = useState(); const [pattern, setPattern] = useState(); + const [activePattern, setActivePattern] = useState(); + const [isHot, setIsHot] = useState(false); // set to true to enable live coding in hot.js, using dev server // logs events of cycle const logCycle = (_events: any, cycle: any) => { if (_events.length) { @@ -39,81 +46,137 @@ function App() { // cycle hook to control scheduling const cycle = useCycle({ onEvent: useCallback((time, event) => { - // console.log('event', event, time); - synth.triggerAttackRelease(event.value, event.duration, time); + try { + if (typeof event.value === 'string') { + defaultSynth.triggerAttackRelease(event.value, event.duration, time); + /* console.warn('no instrument chosen', event); + throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */ + } else { + const { onTrigger } = event.value; + onTrigger(time, event); + } + setError(undefined); + } catch (err: any) { + console.warn(err); + err.message = 'unplayable event: ' + err?.message; + setError(err); + } }, []), onQuery: useCallback( (span) => { try { - return pattern?.query(span) || []; + return activePattern?.query(span) || []; } catch (err: any) { setError(err); return []; } }, - [pattern] + [activePattern] ), - onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), [pattern]), - ready: !!pattern, + onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), [activePattern]), + ready: !!activePattern, }); + + // set active pattern on ctrl+enter + useLayoutEffect(() => { + const handleKeyPress = (e: any) => { + if (e.ctrlKey && e.code === 'Enter') { + setActivePattern(() => pattern); + !cycle.started && cycle.start(); + } + }; + document.addEventListener('keypress', handleKeyPress); + return () => document.removeEventListener('keypress', handleKeyPress); + }, [pattern]); + // parse pattern when code changes useEffect(() => { - try { - let _pattern: Pattern; - try { - _pattern = h(code); - setMode('pegjs'); // haskell mode does not recognize quotes, pegjs looks ok by accident.. - } catch (err) { - setMode('javascript'); - // code is not haskell like - _pattern = parse(code); - if (_pattern?.constructor?.name !== 'Pattern') { - const message = `got "${typeof _pattern}" instead of pattern`; - throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.')); - } + let _code = code; + // handle hot mode + if (isHot) { + if (typeof hot !== 'string') { + getHotCode().then((_code) => { + setCode(_code); + setMode('javascript'); + }); // if using HMR, just use changed file + setActivePattern(hot); + return; + } else { + _code = hot; + setCode(_code); } - setPattern(() => _pattern); // need arrow function here! otherwise if user returns a function, react will think it's a state reducer + } + // normal mode + try { + const parsed = parse(_code); + // need arrow function here! otherwise if user returns a function, react will think it's a state reducer + // only first time, then need ctrl+enter + pattern; + setPattern(() => parsed.pattern); + if (!activePattern || isHot) { + setActivePattern(() => parsed.pattern); + } + setMode(parsed.mode); setError(undefined); } catch (err: any) { console.warn(err); setError(err); } - }, [code]); + }, [code, isHot]); + // scroll log box to bottom when log changes useLayoutEffect(() => { logBox.current.scrollTop = logBox.current?.scrollHeight; }, [log]); + return (
-
- logo -

Strudel REPL

+
+
+ logo +

Strudel REPL

+
+ {window.location.href.includes('http://localhost:8080') && ( + + )}
-
+
{ - setLog((log) => log + `${log ? '\n\n' : ''}✏️ edit\n${code}\n${value}`); - setCode(value); + if (!isHot) { + // setLog((log) => log + `${log ? '\n\n' : ''}✏️ edit\n${code}\n${value}`); + setCode(value); + } }} /> + + {!cycle.started + ? `press ctrl+enter to play\n` + : !isHot && activePattern !== pattern + ? `ctrl+enter to update\n` + : 'no changes\n'} + {!isHot && <>{{ pegjs: 'mini' }[mode] || mode} mode} + {isHot && '🔥 hot mode: go to hot.js to edit pattern, then save'} +
{error &&
{error?.message || 'unknown error'}
} - {/*