From d4afbc63e276eaeaadce94ba7015e48710190e28 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 09:22:50 +0100 Subject: [PATCH 01/35] strudel web component --- website/src/pages/vanilla/mini.astro | 65 ++++++++++ website/src/repl/vanilla/strudel-editor.mjs | 132 ++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 website/src/pages/vanilla/mini.astro create mode 100644 website/src/repl/vanilla/strudel-editor.mjs diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro new file mode 100644 index 00000000..77dd58b6 --- /dev/null +++ b/website/src/pages/vanilla/mini.astro @@ -0,0 +1,65 @@ +--- +import HeadCommonNew from '../../components/HeadCommonNew.astro'; +--- + + + + + Strudel Vanilla REPL + + +

vanilli repl

+ ,6)/2").bank('RolandTR707') + , + s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>") + .lastOf(8, x=>x.segment("12").end(.2).gain(isaw)) + , + s("[tb ~ tb]").bank('RolandTR707') + .clip(0).release(.08).room(.2) + ).off(-1/6, x=>x.speed(.7).gain(.2).degrade()) + , + stack( + note(",6) ~!2 [f1?]*2>") + .s("sawtooth").lpf(perlin.range(400,1000)) + .lpa(.1).lpenv(-3).room(.2) + .lpq(8).noise(.2) + .add(note("0,.1")) + , + chord("<~ Gm9 ~!2>") + .dict('ireal').voicing() + .s("sawtooth").vib("2:.1") + .lpf(1000).lpa(.1).lpenv(-4) + .room(.5) + , + n(run(3)).chord("/8") + .dict('ireal-ext') + .off(1/2, add(n(4))) + .voicing() + .clip(.1).release(.05) + .s("sine").jux(rev) + .sometimesBy(sine.slow(16), add(note(12))) + .room(.75) + .lpf(sine.range(200,2000).slow(16)) + .gain(saw.slow(4).div(2)) + ).add(note(perlin.range(0,.5))) + )`}> + + + diff --git a/website/src/repl/vanilla/strudel-editor.mjs b/website/src/repl/vanilla/strudel-editor.mjs new file mode 100644 index 00000000..85acc72e --- /dev/null +++ b/website/src/repl/vanilla/strudel-editor.mjs @@ -0,0 +1,132 @@ +import { getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core'; +import { StrudelMirror } from '@strudel/codemirror'; +import { transpiler } from '@strudel.cycles/transpiler'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; +import { + getAudioContext, + webaudioOutput, + registerSynthSounds, + registerZZFXSounds, + samples, +} from '@strudel.cycles/webaudio'; + +function camelToKebab(camelCaseString) { + return camelCaseString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} +function kebabToCamel(kebabCaseString) { + return kebabCaseString.replace(/-([a-z])/g, function (match, group) { + return group.toUpperCase(); + }); +} + +const initialSettings = { + keybindings: 'strudelTheme', + isLineNumbersDisplayed: true, + isActiveLineHighlighted: true, + isAutoCompletionEnabled: false, + isPatternHighlightingEnabled: true, + isFlashEnabled: true, + isTooltipEnabled: false, + isLineWrappingEnabled: false, + theme: 'teletext', + fontFamily: 'monospace', + fontSize: 18, +}; +const settingAttributes = Object.keys(initialSettings).map(camelToKebab); +const parseAttribute = (name, value) => { + const camel = kebabToCamel(name); + const type = typeof initialSettings[camel]; + // console.log('type', type, name); + if (type === 'boolean') { + return ['1', 'true'].includes(value); + } + if (type === 'number') { + return Number(value); + } + return value; +}; +// console.log('attributes', settingAttributes); + +class StrudelEditor extends HTMLElement { + static observedAttributes = ['code', ...settingAttributes]; + settings = initialSettings; + constructor() { + super(); + } + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'code') { + this.code = newValue; + this.editor?.setCode(initialCode); + } else if (settingAttributes.includes(name)) { + const camel = kebabToCamel(name); + this.settings[camel] = parseAttribute(name, newValue); + // console.log('name', name, newValue, camel, this.settings[camel]); + this.editor?.updateSettings(this.settings); + } + } + connectedCallback() { + const drawContext = getDrawContext(); + const drawTime = [-2, 2]; + this.container = document.createElement('div'); + this.appendChild(this.container); + this.editor = new StrudelMirror({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + root: this.container, + initialCode: '// LOADING', + pattern: silence, + settings: this.settings, + drawTime, + onDraw: (haps, time, frame, painters) => { + painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); + painters?.forEach((painter) => { + // ctx time haps drawTime paintOptions + painter(drawContext, time, haps, drawTime, { clear: false }); + }); + }, + prebake: async () => { + // populate scope / lazy load modules + const modulesLoading = evalScope( + import('@strudel.cycles/core'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/webaudio'), + import('@strudel/codemirror'), + import('@strudel/hydra'), + import('@strudel.cycles/soundfonts'), + // import('@strudel.cycles/xen'), + // import('@strudel.cycles/serial'), + // import('@strudel.cycles/csound'), + /* import('@strudel.cycles/midi'), */ + // import('@strudel.cycles/osc'), + controls, // sadly, this cannot be exported from core directly (yet) + ); + // load samples + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + await Promise.all([ + modulesLoading, + registerSynthSounds(), + registerZZFXSounds(), + registerSoundfonts(), + samples(`${ds}/tidal-drum-machines.json`), + samples(`${ds}/piano.json`), + samples(`${ds}/Dirt-Samples.json`), + samples(`${ds}/EmuSP12.json`), + samples(`${ds}/vcsl.json`), + ]); + }, + afterEval: ({ code }) => { + window.location.hash = '#' + code2hash(code); + }, + }); + // init settings + this.editor.updateSettings(this.settings); + this.editor.setCode(this.code); + // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); + // onEvent('strudel-toggle-play', () => this.editor.toggle()); + } + // Element functionality written in here +} + +customElements.define('strudel-editor', StrudelEditor); From f0c3d38ea7e089d3f6ba6c8a54cb4bef0cba1157 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 10:12:47 +0100 Subject: [PATCH 02/35] move repl web component to new repl package + use it in /vanilla + /vanilla/mini --- packages/codemirror/codemirror.mjs | 1 + packages/repl/README.md | 3 + packages/repl/index.mjs | 1 + packages/repl/package.json | 49 ++++++++++++ packages/repl/prebake.mjs | 34 ++++++++ .../repl/repl-component.mjs | 62 ++++----------- pnpm-lock.yaml | 72 +++++++++++++++++ website/package.json | 1 + website/src/pages/vanilla/index.astro | 3 +- website/src/pages/vanilla/mini.astro | 62 +++++---------- website/src/repl/vanilla/vanilla.css | 2 +- website/src/repl/vanilla/vanilla.mjs | 78 ++----------------- 12 files changed, 204 insertions(+), 164 deletions(-) create mode 100644 packages/repl/README.md create mode 100644 packages/repl/index.mjs create mode 100644 packages/repl/package.json create mode 100644 packages/repl/prebake.mjs rename website/src/repl/vanilla/strudel-editor.mjs => packages/repl/repl-component.mjs (60%) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 5ceb9619..43b74ef8 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -154,6 +154,7 @@ export class StrudelMirror { }); const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { + this.root.style.display = 'block'; this.root.style.backgroundColor = 'var(--background)'; cmEditor.style.backgroundColor = 'transparent'; } diff --git a/packages/repl/README.md b/packages/repl/README.md new file mode 100644 index 00000000..ff310948 --- /dev/null +++ b/packages/repl/README.md @@ -0,0 +1,3 @@ +# @strudel/repl + +The Strudel REPL as a web component. diff --git a/packages/repl/index.mjs b/packages/repl/index.mjs new file mode 100644 index 00000000..330cd77d --- /dev/null +++ b/packages/repl/index.mjs @@ -0,0 +1 @@ +export * from './repl-component.mjs'; diff --git a/packages/repl/package.json b/packages/repl/package.json new file mode 100644 index 00000000..08e3fbcf --- /dev/null +++ b/packages/repl/package.json @@ -0,0 +1,49 @@ +{ + "name": "@strudel/repl", + "version": "0.9.0", + "description": "Strudel REPL as a Web Component", + "main": "index.mjs", + "publishConfig": { + "main": "dist/index.js", + "module": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "contributors": [ + "Alex McLean " + ], + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel.cycles/core": "workspace:*", + "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/tonal": "workspace:*", + "@strudel.cycles/transpiler": "workspace:*", + "@strudel.cycles/webaudio": "workspace:*", + "@strudel/codemirror": "workspace:*", + "@strudel/hydra": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*", + "@strudel.cycles/midi": "workspace:*" + }, + "devDependencies": { + "vite": "^4.3.3" + } +} diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs new file mode 100644 index 00000000..65e2d96f --- /dev/null +++ b/packages/repl/prebake.mjs @@ -0,0 +1,34 @@ +import { controls, evalScope } from '@strudel.cycles/core'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; +import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio'; + +export async function prebake() { + const modulesLoading = evalScope( + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + import('@strudel/codemirror'), + import('@strudel/hydra'), + import('@strudel.cycles/soundfonts'), + import('@strudel.cycles/midi'), + // import('@strudel.cycles/xen'), + // import('@strudel.cycles/serial'), + // import('@strudel.cycles/csound'), + // import('@strudel.cycles/osc'), + controls, // sadly, this cannot be exported from core directly (yet) + ); + // load samples + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + await Promise.all([ + modulesLoading, + registerSynthSounds(), + registerZZFXSounds(), + registerSoundfonts(), + samples(`${ds}/tidal-drum-machines.json`), + samples(`${ds}/piano.json`), + samples(`${ds}/Dirt-Samples.json`), + samples(`${ds}/EmuSP12.json`), + samples(`${ds}/vcsl.json`), + ]); +} diff --git a/website/src/repl/vanilla/strudel-editor.mjs b/packages/repl/repl-component.mjs similarity index 60% rename from website/src/repl/vanilla/strudel-editor.mjs rename to packages/repl/repl-component.mjs index 85acc72e..44220766 100644 --- a/website/src/repl/vanilla/strudel-editor.mjs +++ b/packages/repl/repl-component.mjs @@ -1,14 +1,8 @@ -import { getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core'; -import { StrudelMirror } from '@strudel/codemirror'; +import { getDrawContext, silence } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; -import { registerSoundfonts } from '@strudel.cycles/soundfonts'; -import { - getAudioContext, - webaudioOutput, - registerSynthSounds, - registerZZFXSounds, - samples, -} from '@strudel.cycles/webaudio'; +import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; +import { StrudelMirror } from '@strudel/codemirror'; +import { prebake } from './prebake.mjs'; function camelToKebab(camelCaseString) { return camelCaseString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); @@ -46,13 +40,16 @@ const parseAttribute = (name, value) => { return value; }; // console.log('attributes', settingAttributes); - -class StrudelEditor extends HTMLElement { +class StrudelRepl extends HTMLElement { static observedAttributes = ['code', ...settingAttributes]; settings = initialSettings; + editor = null; constructor() { super(); } + onReady(listener) { + this.readyListener = listener; + } attributeChangedCallback(name, oldValue, newValue) { if (name === 'code') { this.code = newValue; @@ -67,13 +64,11 @@ class StrudelEditor extends HTMLElement { connectedCallback() { const drawContext = getDrawContext(); const drawTime = [-2, 2]; - this.container = document.createElement('div'); - this.appendChild(this.container); this.editor = new StrudelMirror({ defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, - root: this.container, + root: this, initialCode: '// LOADING', pattern: silence, settings: this.settings, @@ -85,48 +80,19 @@ class StrudelEditor extends HTMLElement { painter(drawContext, time, haps, drawTime, { clear: false }); }); }, - prebake: async () => { - // populate scope / lazy load modules - const modulesLoading = evalScope( - import('@strudel.cycles/core'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/mini'), - import('@strudel.cycles/webaudio'), - import('@strudel/codemirror'), - import('@strudel/hydra'), - import('@strudel.cycles/soundfonts'), - // import('@strudel.cycles/xen'), - // import('@strudel.cycles/serial'), - // import('@strudel.cycles/csound'), - /* import('@strudel.cycles/midi'), */ - // import('@strudel.cycles/osc'), - controls, // sadly, this cannot be exported from core directly (yet) - ); - // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; - await Promise.all([ - modulesLoading, - registerSynthSounds(), - registerZZFXSounds(), - registerSoundfonts(), - samples(`${ds}/tidal-drum-machines.json`), - samples(`${ds}/piano.json`), - samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), - samples(`${ds}/vcsl.json`), - ]); - }, + prebake, afterEval: ({ code }) => { - window.location.hash = '#' + code2hash(code); + // window.location.hash = '#' + code2hash(code); }, }); // init settings this.editor.updateSettings(this.settings); this.editor.setCode(this.code); + this.readyListener?.(this); // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); // onEvent('strudel-toggle-play', () => this.editor.toggle()); } // Element functionality written in here } -customElements.define('strudel-editor', StrudelEditor); +customElements.define('strudel-editor', StrudelRepl); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2416a62..3fa26a48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -468,6 +468,40 @@ importers: specifier: ^4.3.3 version: 4.3.3 + packages/repl: + dependencies: + '@strudel.cycles/core': + specifier: workspace:* + version: link:../core + '@strudel.cycles/midi': + specifier: workspace:* + version: link:../midi + '@strudel.cycles/mini': + specifier: workspace:* + version: link:../mini + '@strudel.cycles/soundfonts': + specifier: workspace:* + version: link:../soundfonts + '@strudel.cycles/tonal': + specifier: workspace:* + version: link:../tonal + '@strudel.cycles/transpiler': + specifier: workspace:* + version: link:../transpiler + '@strudel.cycles/webaudio': + specifier: workspace:* + version: link:../webaudio + '@strudel/codemirror': + specifier: workspace:* + version: link:../codemirror + '@strudel/hydra': + specifier: workspace:* + version: link:../hydra + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.5.0 + packages/serial: dependencies: '@strudel.cycles/core': @@ -702,6 +736,9 @@ importers: '@strudel/hydra': specifier: workspace:* version: link:../packages/hydra + '@strudel/repl': + specifier: workspace:* + version: link:../packages/repl '@supabase/supabase-js': specifier: ^2.21.0 version: 2.21.0 @@ -14331,6 +14368,41 @@ packages: fsevents: 2.3.3 dev: true + /vite@4.5.0: + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.18.20 + postcss: 8.4.32 + rollup: 3.28.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@4.5.0(@types/node@18.16.3): resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/website/package.json b/website/package.json index f5354842..b2267a69 100644 --- a/website/package.json +++ b/website/package.json @@ -37,6 +37,7 @@ "@strudel/hydra": "workspace:*", "@strudel/codemirror": "workspace:*", "@strudel/desktopbridge": "workspace:*", + "@strudel/repl": "workspace:*", "@supabase/supabase-js": "^2.21.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro index 44144bb6..d5c41d6d 100644 --- a/website/src/pages/vanilla/index.astro +++ b/website/src/pages/vanilla/index.astro @@ -90,7 +90,8 @@ import HeadCommonNew from '../../components/HeadCommonNew.astro'; -
+ + diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro index 77dd58b6..68dccb8a 100644 --- a/website/src/pages/vanilla/mini.astro +++ b/website/src/pages/vanilla/mini.astro @@ -2,13 +2,29 @@ import HeadCommonNew from '../../components/HeadCommonNew.astro'; --- + - + + Strudel Vanilla REPL +

vanilli repl

+

This is a REPL:

,6)/2").bank('RolandTR707') - , - s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>") - .lastOf(8, x=>x.segment("12").end(.2).gain(isaw)) - , - s("[tb ~ tb]").bank('RolandTR707') - .clip(0).release(.08).room(.2) - ).off(-1/6, x=>x.speed(.7).gain(.2).degrade()) - , - stack( - note(",6) ~!2 [f1?]*2>") - .s("sawtooth").lpf(perlin.range(400,1000)) - .lpa(.1).lpenv(-3).room(.2) - .lpq(8).noise(.2) - .add(note("0,.1")) - , - chord("<~ Gm9 ~!2>") - .dict('ireal').voicing() - .s("sawtooth").vib("2:.1") - .lpf(1000).lpa(.1).lpenv(-4) - .room(.5) - , - n(run(3)).chord("/8") - .dict('ireal-ext') - .off(1/2, add(n(4))) - .voicing() - .clip(.1).release(.05) - .s("sine").jux(rev) - .sometimesBy(sine.slow(16), add(note(12))) - .room(.75) - .lpf(sine.range(200,2000).slow(16)) - .gain(saw.slow(4).div(2)) - ).add(note(perlin.range(0,.5))) - )`}> - + code={`s("bd")`}> + +

This is another REPL:

+ diff --git a/website/src/repl/vanilla/vanilla.css b/website/src/repl/vanilla/vanilla.css index 584dd924..5387fb04 100644 --- a/website/src/repl/vanilla/vanilla.css +++ b/website/src/repl/vanilla/vanilla.css @@ -12,7 +12,7 @@ select { html, body, -#code, +#editor, .cm-editor, .cm-scroller { padding: 0; diff --git a/website/src/repl/vanilla/vanilla.mjs b/website/src/repl/vanilla/vanilla.mjs index 488fcf8c..721b745c 100644 --- a/website/src/repl/vanilla/vanilla.mjs +++ b/website/src/repl/vanilla/vanilla.mjs @@ -1,13 +1,5 @@ -import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core'; -import { StrudelMirror, initTheme, activateTheme } from '@strudel/codemirror'; -import { transpiler } from '@strudel.cycles/transpiler'; -import { - getAudioContext, - webaudioOutput, - registerSynthSounds, - registerZZFXSounds, - samples, -} from '@strudel.cycles/webaudio'; +import { hash2code, logger } from '@strudel.cycles/core'; +import { activateTheme, initTheme } from '@strudel/codemirror'; import './vanilla.css'; let editor; @@ -27,68 +19,9 @@ const initialSettings = { initTheme(initialSettings.theme); async function run() { - const container = document.getElementById('code'); - if (!container) { - console.warn('could not init: no container found'); - return; - } - - const drawContext = getDrawContext(); - const drawTime = [-2, 2]; - editor = new StrudelMirror({ - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - transpiler, - root: container, - initialCode: '// LOADING', - pattern: silence, - settings: initialSettings, - drawTime, - onDraw: (haps, time, frame, painters) => { - painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); - painters?.forEach((painter) => { - // ctx time haps drawTime paintOptions - painter(drawContext, time, haps, drawTime, { clear: false }); - }); - }, - prebake: async () => { - // populate scope / lazy load modules - const modulesLoading = evalScope( - import('@strudel.cycles/core'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/mini'), - // import('@strudel.cycles/xen'), - import('@strudel.cycles/webaudio'), - import('@strudel/codemirror'), - /* import('@strudel/hydra'), */ - // import('@strudel.cycles/serial'), - /* import('@strudel.cycles/soundfonts'), */ - // import('@strudel.cycles/csound'), - /* import('@strudel.cycles/midi'), */ - // import('@strudel.cycles/osc'), - controls, // sadly, this cannot be exported from core directly (yet) - ); - // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; - await Promise.all([ - modulesLoading, - registerSynthSounds(), - registerZZFXSounds(), - samples(`${ds}/tidal-drum-machines.json`), - samples(`${ds}/piano.json`), - samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), - samples(`${ds}/vcsl.json`), - ]); - }, - afterEval: ({ code }) => { - window.location.hash = '#' + code2hash(code); - }, - }); - - // init settings + const repl = document.getElementById('editor'); + editor = repl.editor; editor.updateSettings(initialSettings); - logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight'); const codeParam = window.location.href.split('#')[1] || ''; @@ -195,8 +128,7 @@ const form = document.querySelector('form[name=settings]'); setFormValues(form, initialSettings); form.addEventListener('change', () => { const values = getFormValues(form, initialSettings); - // console.log('values', values); - editor.updateSettings(values); + editor?.updateSettings(values); // TODO: only activateTheme when it changes activateTheme(values.theme); }); From f6b1e13113c4d8ad774c0e9b509310c703f9cd57 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 10:14:20 +0100 Subject: [PATCH 03/35] fix: lint errors + wrong color --- packages/repl/repl-component.mjs | 2 +- website/src/pages/vanilla/mini.astro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index 44220766..a4b14a0d 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -53,7 +53,7 @@ class StrudelRepl extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { if (name === 'code') { this.code = newValue; - this.editor?.setCode(initialCode); + this.editor?.setCode(newValue); } else if (settingAttributes.includes(name)) { const camel = kebabToCamel(name); this.settings[camel] = parseAttribute(name, newValue); diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro index 68dccb8a..2f8e8599 100644 --- a/website/src/pages/vanilla/mini.astro +++ b/website/src/pages/vanilla/mini.astro @@ -8,7 +8,7 @@ import HeadCommonNew from '../../components/HeadCommonNew.astro'; Strudel Vanilla REPL diff --git a/website/src/repl/vanilla/vanilla.mjs b/website/src/repl/vanilla/vanilla.mjs index 721b745c..3c2a54a5 100644 --- a/website/src/repl/vanilla/vanilla.mjs +++ b/website/src/repl/vanilla/vanilla.mjs @@ -1,5 +1,4 @@ import { hash2code, logger } from '@strudel.cycles/core'; -import { activateTheme, initTheme } from '@strudel/codemirror'; import './vanilla.css'; let editor; @@ -16,7 +15,6 @@ const initialSettings = { fontFamily: 'monospace', fontSize: 18, }; -initTheme(initialSettings.theme); async function run() { const repl = document.getElementById('editor'); @@ -129,6 +127,4 @@ setFormValues(form, initialSettings); form.addEventListener('change', () => { const values = getFormValues(form, initialSettings); editor?.updateSettings(values); - // TODO: only activateTheme when it changes - activateTheme(values.theme); }); From 129327077a1338f78731f0a2e0d52d3a9f0b796e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 11:24:38 +0100 Subject: [PATCH 10/35] bump repl to 0.9.2 --- packages/repl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/repl/package.json b/packages/repl/package.json index f3c60059..f79d94f3 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "0.9.1", + "version": "0.9.2", "description": "Strudel REPL as a Web Component", "main": "index.mjs", "publishConfig": { From 505ad17447276585e89177acce11772bdb0476a0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 11:25:36 +0100 Subject: [PATCH 11/35] fix: process is not defined --- packages/repl/index.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/repl/index.mjs b/packages/repl/index.mjs index 330cd77d..f771208d 100644 --- a/packages/repl/index.mjs +++ b/packages/repl/index.mjs @@ -1 +1,8 @@ +// nanostores use process.env which kills the browser build +window.process = { + env: { + NODE_ENV: 'development', + }, +}; + export * from './repl-component.mjs'; From 9fac2c3c339f9047b88a3612d2fe6ee928270c5f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 11:33:35 +0100 Subject: [PATCH 12/35] band aid fix for process is not defined --- packages/repl/examples/simple.html | 6 ++++-- packages/repl/package.json | 1 + packages/repl/vite.config.js | 7 ++++++ pnpm-lock.yaml | 34 ++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/repl/examples/simple.html b/packages/repl/examples/simple.html index 17c4bebf..da47d089 100644 --- a/packages/repl/examples/simple.html +++ b/packages/repl/examples/simple.html @@ -1,2 +1,4 @@ - - + + + + diff --git a/packages/repl/package.json b/packages/repl/package.json index f79d94f3..87e8b9c0 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -33,6 +33,7 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { + "@rollup/plugin-replace": "^5.0.5", "@strudel.cycles/core": "workspace:*", "@strudel.cycles/midi": "workspace:*", "@strudel.cycles/mini": "workspace:*", diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js index d94cd4d7..49391bf3 100644 --- a/packages/repl/vite.config.js +++ b/packages/repl/vite.config.js @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; // import { visualizer } from 'rollup-plugin-visualizer'; +import replace from '@rollup/plugin-replace'; // https://vitejs.dev/config/ export default defineConfig({ @@ -15,6 +16,12 @@ export default defineConfig({ }, rollupOptions: { // external: [...Object.keys(dependencies)], + plugins: [ + replace({ + 'process.env.NODE_ENV': JSON.stringify('production'), + preventAssignment: true, + }), + ], }, target: 'esnext', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3161184..344a24bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -470,6 +470,9 @@ importers: packages/repl: dependencies: + '@rollup/plugin-replace': + specifier: ^5.0.5 + version: 5.0.5 '@strudel.cycles/core': specifier: workspace:* version: link:../core @@ -4155,6 +4158,19 @@ packages: rollup: 2.79.1 dev: true + /@rollup/plugin-replace@5.0.5: + resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0 + magic-string: 0.30.5 + dev: false + /@rollup/pluginutils@3.1.0(rollup@2.79.1): resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} @@ -4167,6 +4183,20 @@ packages: rollup: 2.79.1 dev: true + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: false + /@rollup/rollup-android-arm-eabi@4.9.0: resolution: {integrity: sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==} cpu: [arm] @@ -7585,6 +7615,10 @@ packages: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} dev: true + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: false + /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: From 9b58cf9890488d9ab5a0bebd7f288585a857ac0f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 11:34:14 +0100 Subject: [PATCH 13/35] bump repl to 0.9.3 --- packages/repl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/repl/package.json b/packages/repl/package.json index 87e8b9c0..9269d706 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "0.9.2", + "version": "0.9.3", "description": "Strudel REPL as a Web Component", "main": "index.mjs", "publishConfig": { From 372e93e8f8155ffb0ce851ae57a7f59bdd0a2658 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 11:50:49 +0100 Subject: [PATCH 14/35] repl now accepts code in innerHTML + bump --- packages/repl/examples/repl.html | 2 ++ packages/repl/package.json | 2 +- packages/repl/repl-component.mjs | 13 +++++++++++- website/src/pages/vanilla/mini.astro | 31 +++++++++++++++++++--------- 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 packages/repl/examples/repl.html diff --git a/packages/repl/examples/repl.html b/packages/repl/examples/repl.html new file mode 100644 index 00000000..925b00f1 --- /dev/null +++ b/packages/repl/examples/repl.html @@ -0,0 +1,2 @@ + + diff --git a/packages/repl/package.json b/packages/repl/package.json index 9269d706..aaa95983 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "0.9.3", + "version": "0.9.4", "description": "Strudel REPL as a Web Component", "main": "index.mjs", "publishConfig": { diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index d0a2bdf2..ebcca532 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -59,13 +59,24 @@ class StrudelRepl extends HTMLElement { } } connectedCallback() { + // setTimeout makes sure the dom is ready + setTimeout(() => { + const code = (this.innerHTML + '').replace('', '').trim(); + if (code) { + // use comment code in element body if present + this.setAttribute('code', code); + } + }); + // use a separate container for the editor, to make sure the innerHTML stays as is + const container = document.createElement('div'); + this.parentElement.insertBefore(container, this.nextSibling); const drawContext = getDrawContext(); const drawTime = [-2, 2]; this.editor = new StrudelMirror({ defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, - root: this, + root: container, initialCode: '// LOADING', pattern: silence, settings: this.settings, diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro index b68e141b..f2a25703 100644 --- a/website/src/pages/vanilla/mini.astro +++ b/website/src/pages/vanilla/mini.astro @@ -1,16 +1,9 @@ ---- -import HeadCommonNew from '../../components/HeadCommonNew.astro'; ---- - - - Strudel Vanilla REPL - -

vanilli repl

+

This is a REPL:

This is another REPL:

- + From 8e35079fd1a85244324e25aaf3c4e05bc59d7f6c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 21:01:45 +0100 Subject: [PATCH 15/35] more code in example --- packages/repl/examples/repl.html | 2 -- packages/repl/examples/simple.html | 33 ++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) delete mode 100644 packages/repl/examples/repl.html diff --git a/packages/repl/examples/repl.html b/packages/repl/examples/repl.html deleted file mode 100644 index 925b00f1..00000000 --- a/packages/repl/examples/repl.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/packages/repl/examples/simple.html b/packages/repl/examples/simple.html index da47d089..849a6368 100644 --- a/packages/repl/examples/simple.html +++ b/packages/repl/examples/simple.html @@ -1,4 +1,33 @@ - + - + + + From 93e8186388cb4344dee35a28102bcce1fb39b82b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 22:23:20 +0100 Subject: [PATCH 16/35] MicroRepl poc --- packages/codemirror/codemirror.mjs | 3 +- packages/core/repl.mjs | 41 +++++++++++- packages/repl/repl-component.mjs | 6 ++ website/src/docs/Icon.jsx | 38 +++++++++++ website/src/docs/MicroRepl.jsx | 91 +++++++++++++++++++++++++++ website/src/pages/vanilla/micro.astro | 32 ++++++++++ 6 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 website/src/docs/Icon.jsx create mode 100644 website/src/docs/MicroRepl.jsx create mode 100644 website/src/pages/vanilla/micro.astro diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 92dd6214..d7d67607 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -146,8 +146,7 @@ export class StrudelMirror { onChange: (v) => { if (v.docChanged) { this.code = v.state.doc.toString(); - // TODO: repl is still untouched to make sure the old Repl.jsx stays untouched.. - // this.repl.setCode(this.code); + this.repl.setCode?.(this.code); } }, onEvaluate: () => this.evaluate(), diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 0bd5229d..b3c7a543 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -16,13 +16,36 @@ export function repl({ transpiler, onToggle, editPattern, + onUpdateState, }) { + const state = { + schedulerError: undefined, + evalError: undefined, + code: '// LOADING', + activeCode: '// LOADING', + pattern: undefined, + miniLocations: [], + widgets: [], + pending: true, + started: false, + }; + + const updateState = (update) => { + Object.assign(state, update); + state.isDirty = state.code !== state.activeCode; + state.error = state.evalError || state.schedulerError; + onUpdateState?.(state); + }; + const scheduler = new Cyclist({ interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, getTime, - onToggle, + onToggle: (started) => { + updateState({ started }); + onToggle?.(started); + }, }); let pPatterns = {}; let allTransform; @@ -43,6 +66,7 @@ export function repl({ throw new Error('no code to evaluate'); } try { + updateState({ code, pending: true }); await beforeEval?.({ code }); shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler); @@ -58,17 +82,28 @@ export function repl({ } logger(`[eval] code updated`); setPattern(pattern, autostart); + updateState({ + miniLocations: meta?.miniLocations || [], + widgets: meta?.widgets || [], + activeCode: code, + pattern, + evalError: undefined, + schedulerError: undefined, + pending: false, + }); afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { // console.warn(`[repl] eval error: ${err.message}`); logger(`[eval] error: ${err.message}`, 'error'); + updateState({ evalError: err, pending: false }); onEvalError?.(err); } }; const stop = () => scheduler.stop(); const start = () => scheduler.start(); const pause = () => scheduler.pause(); + const toggle = () => scheduler.toggle(); const setCps = (cps) => scheduler.setCps(cps); const setCpm = (cpm) => scheduler.setCps(cpm / 60); @@ -127,8 +162,8 @@ export function repl({ setCpm, setcpm: setCpm, }); - - return { scheduler, evaluate, start, stop, pause, setCps, setPattern }; + const setCode = (code) => updateState({ code }); + return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index ebcca532..554be3bc 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -92,6 +92,12 @@ class StrudelRepl extends HTMLElement { afterEval: ({ code }) => { // window.location.hash = '#' + code2hash(code); }, + onUpdateState: (state) => { + const event = new CustomEvent('update', { + detail: state, + }); + this.dispatchEvent(event); + }, }); // init settings this.editor.updateSettings(this.settings); diff --git a/website/src/docs/Icon.jsx b/website/src/docs/Icon.jsx new file mode 100644 index 00000000..64d5f88a --- /dev/null +++ b/website/src/docs/Icon.jsx @@ -0,0 +1,38 @@ +export function Icon({ type }) { + return ( + + { + { + refresh: ( + + ), + play: ( + + ), + pause: ( + + ), + stop: ( + + ), + }[type] + } + + ); +} diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx new file mode 100644 index 00000000..b3e2637e --- /dev/null +++ b/website/src/docs/MicroRepl.jsx @@ -0,0 +1,91 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import { Icon } from './Icon'; + +export function MicroRepl({ code, hideHeader = false }) { + /* const [ref, isVisible] = useInView({ + threshold: 0.01, + }); */ + const [replState, setReplState] = useState({}); + const { started, isDirty, error } = replState; + const wc = useRef(); + function togglePlay() { + if (wc.current) { + wc.current?.editor.evaluate(); + } + } + const listener = useCallback((e) => setReplState({ ...e.detail }), []); + useEffect(() => { + return () => { + wc.current.removeEventListener('update', listener); + }; + }, []); + return ( +
+ {!hideHeader && ( +
+
+ + +
+
+ )} +
+ { + if (wc.current) { + return; + } + wc.current = el; + el.addEventListener('update', listener); + }} + > + {error &&
{error.message}
} +
+ {/* punchcard && ( + { + if (el && el.width !== el.clientWidth) { + el.width = el.clientWidth; + } + }} + > + ) */} + {/* !!log.length && ( +
+ {log.map(({ message }, i) => ( +
{message}
+ ))} +
+ ) */} +
+ ); +} + +function cx(...classes) { + // : Array + return classes.filter(Boolean).join(' '); +} diff --git a/website/src/pages/vanilla/micro.astro b/website/src/pages/vanilla/micro.astro new file mode 100644 index 00000000..e8414eed --- /dev/null +++ b/website/src/pages/vanilla/micro.astro @@ -0,0 +1,32 @@ +--- +import { MicroRepl } from '../../docs/MicroRepl' +--- + + + + Strudel Micro REPL + + + +
+

MicroRepl:

+ /16") + .dict('ireal') + .voicing() + .when("<1 0> 0@3", sub(note(12))) + .add(note("[12 | 0]*4")) + .attack(slider(0.511)) + .decay(slider(0.466)) + .sustain(slider(0.398)) + .release(slider(0.443)) + .s('sawtooth') + .lpa(sine.range(.1,.25).slow(12)) + .lpenv(sine.range(0,4).slow(16)) + .lpf(perlin.range(400,1000)) + .add(note(perlin.range(0,.25))) + .vib(4).vibmod(.1) + .room(.75).size(8)`} /> +
+ + From b5edcde638c4a44a9ee20803bf267a5a6c31c872 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 23:18:58 +0100 Subject: [PATCH 17/35] cleanup --- website/src/components/HeadCommonNew.astro | 58 ---------------------- website/src/pages/vanilla/index.astro | 5 -- 2 files changed, 63 deletions(-) delete mode 100644 website/src/components/HeadCommonNew.astro diff --git a/website/src/components/HeadCommonNew.astro b/website/src/components/HeadCommonNew.astro deleted file mode 100644 index 9f323a7a..00000000 --- a/website/src/components/HeadCommonNew.astro +++ /dev/null @@ -1,58 +0,0 @@ ---- -import { pwaInfo } from 'virtual:pwa-info'; -import '../styles/index.css'; - -const { BASE_URL } = import.meta.env; -const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; ---- - - - - - - - - - - - - - - - - - - - - - - - -{pwaInfo && } - - diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro index d5c41d6d..d4ea40b8 100644 --- a/website/src/pages/vanilla/index.astro +++ b/website/src/pages/vanilla/index.astro @@ -1,10 +1,5 @@ ---- -import HeadCommonNew from '../../components/HeadCommonNew.astro'; ---- - - Strudel Vanilla REPL From 9c13f6bb53df9ad4d7b0bc0d1369d7edb4d779e0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 23:20:21 +0100 Subject: [PATCH 18/35] fix: mini repl play toggle button --- packages/codemirror/codemirror.mjs | 2 +- website/src/docs/MicroRepl.jsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index d7d67607..4b64692c 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -182,7 +182,7 @@ export class StrudelMirror { } async toggle() { if (this.repl.scheduler.started) { - this.repl.scheduler.stop(); + this.repl.stop(); } else { this.evaluate(); } diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index b3e2637e..534db708 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { Icon } from './Icon'; +// import { useInView } from 'react-hook-inview'; export function MicroRepl({ code, hideHeader = false }) { /* const [ref, isVisible] = useInView({ @@ -10,7 +11,7 @@ export function MicroRepl({ code, hideHeader = false }) { const wc = useRef(); function togglePlay() { if (wc.current) { - wc.current?.editor.evaluate(); + wc.current?.editor.toggle(); } } const listener = useCallback((e) => setReplState({ ...e.detail }), []); From 89c06046b525100a3c28832a4460eda9b05a7950 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 23:27:38 +0100 Subject: [PATCH 19/35] fix: repl package import on server --- packages/repl/index.mjs | 7 -- packages/repl/prebake.mjs | 7 +- packages/repl/repl-component.mjs | 132 ++++++++++++++++--------------- 3 files changed, 72 insertions(+), 74 deletions(-) diff --git a/packages/repl/index.mjs b/packages/repl/index.mjs index f771208d..330cd77d 100644 --- a/packages/repl/index.mjs +++ b/packages/repl/index.mjs @@ -1,8 +1 @@ -// nanostores use process.env which kills the browser build -window.process = { - env: { - NODE_ENV: 'development', - }, -}; - export * from './repl-component.mjs'; diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 1038bab3..02638d1a 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -1,5 +1,4 @@ import { controls, evalScope } from '@strudel.cycles/core'; -import { registerSoundfonts } from '@strudel.cycles/soundfonts'; import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio'; import * as core from '@strudel.cycles/core'; @@ -26,7 +25,11 @@ export async function prebake() { modulesLoading, registerSynthSounds(), registerZZFXSounds(), - registerSoundfonts(), + //registerSoundfonts(), + // need dynamic import here, because importing @strudel.cycles/soundfonts fails on server: + // => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically + // seems to be a problem with soundfont2 + import('@strudel.cycles/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index 554be3bc..2eda358d 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -40,72 +40,74 @@ const parseAttribute = (name, value) => { return value; }; // console.log('attributes', settingAttributes); -class StrudelRepl extends HTMLElement { - static observedAttributes = ['code', ...settingAttributes]; - settings = initialSettings; - editor = null; - constructor() { - super(); - } - attributeChangedCallback(name, oldValue, newValue) { - if (name === 'code') { - this.code = newValue; - this.editor?.setCode(newValue); - } else if (settingAttributes.includes(name)) { - const camel = kebabToCamel(name); - this.settings[camel] = parseAttribute(name, newValue); - // console.log('name', name, newValue, camel, this.settings[camel]); - this.editor?.updateSettings(this.settings); +if (typeof HTMLElement !== 'undefined') { + class StrudelRepl extends HTMLElement { + static observedAttributes = ['code', ...settingAttributes]; + settings = initialSettings; + editor = null; + constructor() { + super(); } - } - connectedCallback() { - // setTimeout makes sure the dom is ready - setTimeout(() => { - const code = (this.innerHTML + '').replace('', '').trim(); - if (code) { - // use comment code in element body if present - this.setAttribute('code', code); + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'code') { + this.code = newValue; + this.editor?.setCode(newValue); + } else if (settingAttributes.includes(name)) { + const camel = kebabToCamel(name); + this.settings[camel] = parseAttribute(name, newValue); + // console.log('name', name, newValue, camel, this.settings[camel]); + this.editor?.updateSettings(this.settings); } - }); - // use a separate container for the editor, to make sure the innerHTML stays as is - const container = document.createElement('div'); - this.parentElement.insertBefore(container, this.nextSibling); - const drawContext = getDrawContext(); - const drawTime = [-2, 2]; - this.editor = new StrudelMirror({ - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - transpiler, - root: container, - initialCode: '// LOADING', - pattern: silence, - settings: this.settings, - drawTime, - onDraw: (haps, time, frame, painters) => { - painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); - painters?.forEach((painter) => { - // ctx time haps drawTime paintOptions - painter(drawContext, time, haps, drawTime, { clear: false }); - }); - }, - prebake, - afterEval: ({ code }) => { - // window.location.hash = '#' + code2hash(code); - }, - onUpdateState: (state) => { - const event = new CustomEvent('update', { - detail: state, - }); - this.dispatchEvent(event); - }, - }); - // init settings - this.editor.updateSettings(this.settings); - this.editor.setCode(this.code); - // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); - // onEvent('strudel-toggle-play', () => this.editor.toggle()); + } + connectedCallback() { + // setTimeout makes sure the dom is ready + setTimeout(() => { + const code = (this.innerHTML + '').replace('', '').trim(); + if (code) { + // use comment code in element body if present + this.setAttribute('code', code); + } + }); + // use a separate container for the editor, to make sure the innerHTML stays as is + const container = document.createElement('div'); + this.parentElement.insertBefore(container, this.nextSibling); + const drawContext = getDrawContext(); + const drawTime = [-2, 2]; + this.editor = new StrudelMirror({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + root: container, + initialCode: '// LOADING', + pattern: silence, + settings: this.settings, + drawTime, + onDraw: (haps, time, frame, painters) => { + painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); + painters?.forEach((painter) => { + // ctx time haps drawTime paintOptions + painter(drawContext, time, haps, drawTime, { clear: false }); + }); + }, + prebake, + afterEval: ({ code }) => { + // window.location.hash = '#' + code2hash(code); + }, + onUpdateState: (state) => { + const event = new CustomEvent('update', { + detail: state, + }); + this.dispatchEvent(event); + }, + }); + // init settings + this.editor.updateSettings(this.settings); + this.editor.setCode(this.code); + // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); + // onEvent('strudel-toggle-play', () => this.editor.toggle()); + } + // Element functionality written in here } - // Element functionality written in here -} -customElements.define('strudel-editor', StrudelRepl); + customElements.define('strudel-editor', StrudelRepl); +} From 20e8a305836e4f43b68b1d40b831983d4c84a139 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Dec 2023 23:30:27 +0100 Subject: [PATCH 20/35] copy recipes page to test new MicroRepl --- website/src/docs/MicroRepl.jsx | 2 + website/src/pages/recipes/recipes-next.mdx | 312 +++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 website/src/pages/recipes/recipes-next.mdx diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 534db708..a9673142 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -1,5 +1,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { Icon } from './Icon'; +import '@strudel/repl'; + // import { useInView } from 'react-hook-inview'; export function MicroRepl({ code, hideHeader = false }) { diff --git a/website/src/pages/recipes/recipes-next.mdx b/website/src/pages/recipes/recipes-next.mdx new file mode 100644 index 00000000..62a1a5e9 --- /dev/null +++ b/website/src/pages/recipes/recipes-next.mdx @@ -0,0 +1,312 @@ +--- +title: Recipes +layout: ../../layouts/MainLayout.astro +--- + +import { MicroRepl } from '../../docs/MicroRepl'; + +# Recipes + +This page shows possible ways to achieve common (or not so common) musical goals. +There are often many ways to do a thing and there is no right or wrong. +The fun part is that each representation will give you different impulses when improvising. + +## Arpeggios + +An arpeggio is when the notes of a chord are played in sequence. +We can either write the notes by hand: + + + +...or use scales: + + + +...or chord symbols: + + + +...using off: + + + +## Chopping Breaks + +A sample can be looped and chopped like this: + + + +This fits the break into 8 cycles + chops it in 16 pieces. +The chops are not audible yet, because we're not doing any manipulation. +Let's add randmized doubling + reversing: + + + +If we want to specify the order of samples, we can replace `chop` with `slice`: + +") + .cut(1).rarely(ply(2))`} + punchcard +/> + +If we use `splice` instead of `slice`, the speed adjusts to the duration of the event: + +") + .cut(1).rarely(ply(2))`} + punchcard +/> + +Note that we don't need `fit`, because `splice` will do that by itself. + +## Filter Envelopes + +A minimal filter envelope looks like this: + + d2") + .s("sawtooth") + .lpf(400).lpa(.2).lpenv(4) + .scope()`} +/> + +We can flip the envelope by setting `lpenv` negative + add some resonance `lpq`: + + d2") + .s("sawtooth").lpq(8) + .lpf(400).lpa(.2).lpenv(-4) + .scope()`} +/> + +## Layering Sounds + +We can layer sounds by separating them with ",": + +") +.s("sawtooth, square") // <------ +.scope()`} +/> + +We can control the gain of individual sounds like this: + +") +.s("sawtooth, square:0:.5") // <--- "name:number:gain" +.scope()`} +/> + +For more control over each voice, we can use `layer`: + +").layer( + x=>x.s("sawtooth").vib(4), + x=>x.s("square").add(note(12)) +).scope()`} +/> + +Here, we give the sawtooth a vibrato and the square is moved an octave up. +With `layer`, you can use any pattern method available on each voice, so sky is the limit.. + +## Oscillator Detune + +We can fatten a sound by adding a detuned version to itself: + +") +.add(note("0,.1")) // <------ chorus +.s("sawtooth").scope()`} + punchcard +/> + +Try out different values, or add another voice! + +## Polyrhythms + +Here is a simple example of a polyrhythm: + + + +A polyrhythm is when 2 different tempos happen at the same time. + +## Polymeter + +This is a polymeter: + +,").fast(2)`} punchcard /> + +A polymeter is when 2 different bar lengths play at the same tempo. + +## Phasing + +This is a phasing: + +*[6,6.1]").piano()`} punchcard /> + +Phasing happens when the same sequence plays at slightly different tempos. + +## Running through samples + +Using `run` with `n`, we can rush through a sample bank: + + + +This works great with sample banks that contain similar sounds, like in this case different recordings of a tabla. +Often times, you'll hear the beginning of the phrase not where the pattern begins. +In this case, I hear the beginning at the third sample, which can be accounted for with `early`. + + + +Let's add some randomness: + + + +## Tape Warble + +We can emulate a pitch warbling effect like this: + + + +## Sound Duration + +There are a number of ways to change the sound duration. Using clip: + +/2")`} +/> + +The value of clip is relative to the duration of each event. +We can also create overlaps using release: + +/2")`} +/> + +This will smoothly fade out each sound for the given number of seconds. +We could also make the notes shorter with decay / sustain: + +/2").sustain(0)`} +/> + +For now, there is a limitation where decay values that exceed the event duration may cause little cracks, so use higher numbers with caution.. + +When using samples, we also have `.end` to cut relative to the sample length: + +")`} /> + +Compare that to clip: + +")`} /> + +or decay / sustain + +").sustain(0)`} /> + +## Wavetable Synthesis + +You can loop a sample with `loop` / `loopEnd`: + +").s("bd").loop(1).loopEnd(.05).gain(.2)`} /> + +This allows us to play the first 5% of the bass drum as a synth! +To simplify loading wavetables, any sample that starts with `wt_` will be looped automatically: + + + +Running through different wavetables can also give interesting variations: + + + +...adding a filter envelope + reverb: + + From 48e06bd213dace518d3068ad3eb3f0d68b6f74a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 12:37:16 +0100 Subject: [PATCH 21/35] fix: redundant style injections for multiple repls --- packages/codemirror/themes.mjs | 14 ++++++++++---- website/src/docs/MicroRepl.jsx | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 71fb7642..ee3e05bf 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -484,11 +484,16 @@ export function injectStyle(rule) { return () => styleSheet.deleteRule(ruleIndex); } -let currentTheme, resetThemeStyle, themeStyle; +let currentTheme, + resetThemeStyle, + themeStyle, + styleID = 'strudel-theme-vars'; export function initTheme(theme) { - themeStyle = document.createElement('style'); - themeStyle.id = 'strudel-theme'; - document.head.append(themeStyle); + if (!document.getElementById(styleID)) { + themeStyle = document.createElement('style'); + themeStyle.id = styleID; + document.head.append(themeStyle); + } activateTheme(theme); } @@ -496,6 +501,7 @@ export function activateTheme(name) { if (currentTheme === name) { return; } + currentTheme = name; if (!settings[name]) { console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings'); } diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index a9673142..c0e34637 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -54,6 +54,7 @@ export function MicroRepl({ code, hideHeader = false }) {
{ if (wc.current) { From fc034830d081ecafa8f8580f4eaf1ea70d35b33f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 13:08:09 +0100 Subject: [PATCH 22/35] use StrudelMirror directly in MicroRepl --- packages/repl/index.mjs | 1 + packages/repl/repl-component.mjs | 2 - pnpm-lock.yaml | 3 + website/package.json | 3 +- website/src/docs/MicroRepl.jsx | 134 ++++++++++++++++++++----------- 5 files changed, 93 insertions(+), 50 deletions(-) diff --git a/packages/repl/index.mjs b/packages/repl/index.mjs index 330cd77d..4119059d 100644 --- a/packages/repl/index.mjs +++ b/packages/repl/index.mjs @@ -1 +1,2 @@ export * from './repl-component.mjs'; +export * from './prebake.mjs'; diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index 2eda358d..b8947640 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -103,8 +103,6 @@ if (typeof HTMLElement !== 'undefined') { // init settings this.editor.updateSettings(this.settings); this.editor.setCode(this.code); - // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); - // onEvent('strudel-toggle-play', () => this.editor.toggle()); } // Element functionality written in here } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 344a24bc..67599cd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,6 +793,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-hook-inview: + specifier: ^4.5.0 + version: 4.5.0(react-dom@18.2.0)(react@18.2.0) rehype-autolink-headings: specifier: ^6.1.1 version: 6.1.1 diff --git a/website/package.json b/website/package.json index b2267a69..b2c5f39a 100644 --- a/website/package.json +++ b/website/package.json @@ -34,9 +34,9 @@ "@strudel.cycles/transpiler": "workspace:*", "@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/xen": "workspace:*", - "@strudel/hydra": "workspace:*", "@strudel/codemirror": "workspace:*", "@strudel/desktopbridge": "workspace:*", + "@strudel/hydra": "workspace:*", "@strudel/repl": "workspace:*", "@supabase/supabase-js": "^2.21.0", "@tailwindcss/forms": "^0.5.3", @@ -54,6 +54,7 @@ "nanostores": "^0.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-inview": "^4.5.0", "rehype-autolink-headings": "^6.1.1", "rehype-slug": "^5.0.1", "rehype-urls": "^1.1.1", diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index c0e34637..cc401cd6 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -1,32 +1,78 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { Icon } from './Icon'; -import '@strudel/repl'; +import { getDrawContext, silence } from '@strudel.cycles/core'; +import { transpiler } from '@strudel.cycles/transpiler'; +import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; +import { StrudelMirror } from '@strudel/codemirror'; +import { prebake } from '@strudel/repl'; +import { useInView } from 'react-hook-inview'; -// import { useInView } from 'react-hook-inview'; +const initialSettings = { + keybindings: 'strudelTheme', + isLineNumbersDisplayed: false, + isActiveLineHighlighted: true, + isAutoCompletionEnabled: false, + isPatternHighlightingEnabled: true, + isFlashEnabled: true, + isTooltipEnabled: false, + isLineWrappingEnabled: false, + theme: 'strudelTheme', + fontFamily: 'monospace', + fontSize: 18, +}; -export function MicroRepl({ code, hideHeader = false }) { - /* const [ref, isVisible] = useInView({ +export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchcard, punchcardLabels }) { + const init = useCallback(({ code }) => { + const drawContext = getDrawContext(); + const drawTime = [-2, 2]; + const editor = new StrudelMirror({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + root: containerRef.current, + initialCode: '// LOADING', + pattern: silence, + settings: initialSettings, + drawTime, + onDraw: (haps, time, frame, painters) => { + painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); + painters?.forEach((painter) => { + // ctx time haps drawTime paintOptions + painter(drawContext, time, haps, drawTime, { clear: false }); + }); + }, + prebake, + onUpdateState: (state) => { + setReplState({ ...state }); + }, + }); + // init settings + editor.updateSettings(initialSettings); + editor.setCode(code); + editorRef.current = editor; + }, []); + + const [ref, isVisible] = useInView({ threshold: 0.01, - }); */ + onEnter: () => { + if (!editorRef.current) { + init({ code }); + } + }, + }); const [replState, setReplState] = useState({}); const { started, isDirty, error } = replState; - const wc = useRef(); - function togglePlay() { - if (wc.current) { - wc.current?.editor.toggle(); - } - } - const listener = useCallback((e) => setReplState({ ...e.detail }), []); - useEffect(() => { - return () => { - wc.current.removeEventListener('update', listener); - }; - }, []); + const editorRef = useRef(); + const containerRef = useRef(); + + const [canvasId] = useState(Date.now()); + const drawContext = useCallback( + punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, + [punchcard], + ); + return ( -
+
{!hideHeader && (
@@ -35,7 +81,7 @@ export function MicroRepl({ code, hideHeader = false }) { 'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background', started ? 'animate-pulse' : '', )} - onClick={() => togglePlay()} + onClick={() => editorRef.current?.toggle()} > @@ -44,40 +90,34 @@ export function MicroRepl({ code, hideHeader = false }) { 'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight', isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed', )} - onClick={() => activateCode()} + onClick={() => editorRef.current?.evaluate()} >
)} -
- { - if (wc.current) { - return; - } - wc.current = el; - el.addEventListener('update', listener); - }} - > +
+
{error &&
{error.message}
}
{/* punchcard && ( - { - if (el && el.width !== el.clientWidth) { - el.width = el.clientWidth; - } - }} - > - ) */} + { + if (el && el.width !== el.clientWidth) { + el.width = el.clientWidth; + } + //const ratio = el.clientWidth / canvasHeight; + //const targetWidth = Math.round(el.width * ratio); + //if (el.width !== targetWidth) { + // el.width = targetWidth; + //} + }} + > + ) */} {/* !!log.length && (
{log.map(({ message }, i) => ( From 7fcd9d8a83c4bd61af6e4064711b069e8c1db470 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 16:14:40 +0100 Subject: [PATCH 23/35] approaching proper draw logic in microrepl --- packages/codemirror/codemirror.mjs | 3 +- packages/core/pianoroll.mjs | 9 ++-- website/src/docs/MicroRepl.jsx | 72 +++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4b64692c..7cac0888 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -102,7 +102,8 @@ export class StrudelMirror { this.onDraw?.(haps, time, currentFrame, this.painters); }, drawTime); - // this approach might not work with multiple repls on screen.. + // this approach does not work with multiple repls on screen + // TODO: refactor onPaint usages + find fix, maybe remove painters here? Pattern.prototype.onPaint = function (onPaint) { self.painters.push(onPaint); return this; diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 254dd94a..4a8fd8db 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -256,10 +256,13 @@ export function getDrawOptions(drawTime, options = {}) { return { fold: 1, ...options, cycles, playhead }; } +export const getPunchcardPainter = + (options = {}) => + (ctx, time, haps, drawTime, paintOptions = {}) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }); + Pattern.prototype.punchcard = function (options) { - return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) => - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }), - ); + return this.onPaint(getPunchcardPainter(options)); }; /** diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index cc401cd6..54b5e27e 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -1,6 +1,6 @@ -import { useState, useRef, useCallback, useEffect } from 'react'; +import { useState, useRef, useCallback, useMemo } from 'react'; import { Icon } from './Icon'; -import { getDrawContext, silence } from '@strudel.cycles/core'; +import { silence, getPunchcardPainter } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; import { StrudelMirror } from '@strudel/codemirror'; @@ -9,8 +9,8 @@ import { useInView } from 'react-hook-inview'; const initialSettings = { keybindings: 'strudelTheme', - isLineNumbersDisplayed: false, - isActiveLineHighlighted: true, + isLineNumbersDisplayed: true, + isActiveLineHighlighted: false, isAutoCompletionEnabled: false, isPatternHighlightingEnabled: true, isFlashEnabled: true, @@ -21,10 +21,33 @@ const initialSettings = { fontSize: 18, }; -export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchcard, punchcardLabels }) { - const init = useCallback(({ code }) => { - const drawContext = getDrawContext(); +export function MicroRepl({ + code, + hideHeader = false, + canvasHeight = 200, + onTrigger, + onPaint, + punchcard, + punchcardLabels = true, +}) { + const id = useMemo(() => s4(), []); + const canvasId = useMemo(() => `canvas-${id}`, [id]); + const shouldDraw = !!punchcard; + + const init = useCallback(({ code, shouldDraw }) => { const drawTime = [-2, 2]; + const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null; + let onDraw; + if (shouldDraw) { + onDraw = (haps, time, frame, painters) => { + painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); + painters?.forEach((painter) => { + // ctx time haps drawTime paintOptions + painter(drawContext, time, haps, drawTime, { clear: false }); + }); + }; + } + const editor = new StrudelMirror({ defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, @@ -34,12 +57,17 @@ export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchc pattern: silence, settings: initialSettings, drawTime, - onDraw: (haps, time, frame, painters) => { - painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); - painters?.forEach((painter) => { - // ctx time haps drawTime paintOptions - painter(drawContext, time, haps, drawTime, { clear: false }); - }); + onDraw, + editPattern: (pat, id) => { + if (onTrigger) { + pat = pat.onTrigger(onTrigger, false); + } + if (onPaint) { + editor.painters.push(onPaint); + } else if (punchcard) { + editor.painters.push(getPunchcardPainter({ labels: !!punchcardLabels })); + } + return pat; }, prebake, onUpdateState: (state) => { @@ -56,7 +84,7 @@ export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchc threshold: 0.01, onEnter: () => { if (!editorRef.current) { - init({ code }); + init({ code, shouldDraw }); } }, }); @@ -65,12 +93,6 @@ export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchc const editorRef = useRef(); const containerRef = useRef(); - const [canvasId] = useState(Date.now()); - const drawContext = useCallback( - punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, - [punchcard], - ); - return (
{!hideHeader && ( @@ -101,7 +123,7 @@ export function MicroRepl({ code, hideHeader = false, canvasHeight = 200, punchc
{error &&
{error.message}
}
- {/* punchcard && ( + {shouldDraw && ( - ) */} + )} {/* !!log.length && (
{log.map(({ message }, i) => ( @@ -133,3 +155,9 @@ function cx(...classes) { // : Array return classes.filter(Boolean).join(' '); } + +function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} From c29d032027e8f336fe2d6271db685ce9f99a2d3d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 16:19:18 +0100 Subject: [PATCH 24/35] add autodraw flag --- packages/codemirror/codemirror.mjs | 6 +++--- website/src/docs/MicroRepl.jsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 7cac0888..58a3e261 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -87,7 +87,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, set export class StrudelMirror { constructor(options) { - const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options; + const { root, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, settings, ...replOptions } = options; this.code = initialCode; this.root = root; this.miniLocations = []; @@ -110,7 +110,7 @@ export class StrudelMirror { }; this.prebaked = prebake(); - // this.drawFirstFrame(); + autodraw && this.drawFirstFrame(); this.repl = repl({ ...replOptions, @@ -169,7 +169,7 @@ export class StrudelMirror { try { await this.repl.evaluate(this.code, false); this.drawer.invalidate(this.repl.scheduler); - this.onDraw?.(this.drawer.visibleHaps, 0, []); + this.onDraw?.(this.drawer.visibleHaps, 0, [], this.painters); } catch (err) { console.warn('first frame could not be painted'); } diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 54b5e27e..1b97d4e6 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -52,6 +52,7 @@ export function MicroRepl({ defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, + autodraw: !!shouldDraw, root: containerRef.current, initialCode: '// LOADING', pattern: silence, From 53d6ef04b870d52ede0ef420ba0aa97e7a96a120 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 16:22:28 +0100 Subject: [PATCH 25/35] fix: drawTime + canvasHeight --- website/src/docs/MicroRepl.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 1b97d4e6..8a2280e3 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -24,7 +24,7 @@ const initialSettings = { export function MicroRepl({ code, hideHeader = false, - canvasHeight = 200, + canvasHeight = 100, onTrigger, onPaint, punchcard, @@ -35,7 +35,7 @@ export function MicroRepl({ const shouldDraw = !!punchcard; const init = useCallback(({ code, shouldDraw }) => { - const drawTime = [-2, 2]; + const drawTime = [0, 4]; const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null; let onDraw; if (shouldDraw) { From 40f3212efa30056320ba15087b01686974ede52d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Dec 2023 17:04:08 +0100 Subject: [PATCH 26/35] fix: first frame active state --- packages/codemirror/codemirror.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 58a3e261..cf0a3f62 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -169,7 +169,8 @@ export class StrudelMirror { try { await this.repl.evaluate(this.code, false); this.drawer.invalidate(this.repl.scheduler); - this.onDraw?.(this.drawer.visibleHaps, 0, [], this.painters); + // draw at -0.001 to avoid haps at 0 to be visualized as active + this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters); } catch (err) { console.warn('first frame could not be painted'); } From 592e54a53f63f6840a104e2ead7316d2fae3a941 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Dec 2023 21:13:48 +0100 Subject: [PATCH 27/35] delete strudelmirror example --- .../examples/strudelmirror/.gitignore | 24 -- .../examples/strudelmirror/index.html | 87 ------- .../codemirror/examples/strudelmirror/main.js | 199 ---------------- .../examples/strudelmirror/package.json | 29 --- .../examples/strudelmirror/style.css | 33 --- pnpm-lock.yaml | 212 +----------------- pnpm-workspace.yaml | 1 - 7 files changed, 6 insertions(+), 579 deletions(-) delete mode 100644 packages/codemirror/examples/strudelmirror/.gitignore delete mode 100644 packages/codemirror/examples/strudelmirror/index.html delete mode 100644 packages/codemirror/examples/strudelmirror/main.js delete mode 100644 packages/codemirror/examples/strudelmirror/package.json delete mode 100644 packages/codemirror/examples/strudelmirror/style.css diff --git a/packages/codemirror/examples/strudelmirror/.gitignore b/packages/codemirror/examples/strudelmirror/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/packages/codemirror/examples/strudelmirror/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/codemirror/examples/strudelmirror/index.html b/packages/codemirror/examples/strudelmirror/index.html deleted file mode 100644 index 0e1d43ce..00000000 --- a/packages/codemirror/examples/strudelmirror/index.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - StrudelMirror Example - - -
-
-
- - -
- -
- -
- -
- -
- - - -
-
-
- - - diff --git a/packages/codemirror/examples/strudelmirror/main.js b/packages/codemirror/examples/strudelmirror/main.js deleted file mode 100644 index 676f4b97..00000000 --- a/packages/codemirror/examples/strudelmirror/main.js +++ /dev/null @@ -1,199 +0,0 @@ -import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core'; -import { StrudelMirror } from '@strudel/codemirror'; -import { transpiler } from '@strudel.cycles/transpiler'; -import { - getAudioContext, - webaudioOutput, - registerSynthSounds, - registerZZFXSounds, - samples, -} from '@strudel.cycles/webaudio'; -import './style.css'; - -let editor; -const initialSettings = { - keybindings: 'codemirror', - isLineNumbersDisplayed: true, - isActiveLineHighlighted: true, - isAutoCompletionEnabled: false, - isPatternHighlightingEnabled: true, - isFlashEnabled: true, - isTooltipEnabled: false, - isLineWrappingEnabled: false, - theme: 'teletext', - fontFamily: 'monospace', - fontSize: 18, -}; - -async function run() { - const container = document.getElementById('code'); - if (!container) { - console.warn('could not init: no container found'); - return; - } - - const drawContext = getDrawContext(); - const drawTime = [-2, 2]; - editor = new StrudelMirror({ - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - transpiler, - root: container, - initialCode: '// LOADING', - pattern: silence, - settings: initialSettings, - drawTime, - onDraw: (haps, time, frame, painters) => { - painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2); - painters?.forEach((painter) => { - // ctx time haps drawTime paintOptions - painter(drawContext, time, haps, drawTime, { clear: false }); - }); - }, - prebake: async () => { - // populate scope / lazy load modules - const modulesLoading = evalScope( - import('@strudel.cycles/core'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/mini'), - // import('@strudel.cycles/xen'), - import('@strudel.cycles/webaudio'), - import('@strudel/codemirror'), - /* import('@strudel/hydra'), */ - // import('@strudel.cycles/serial'), - /* import('@strudel.cycles/soundfonts'), */ - // import('@strudel.cycles/csound'), - /* import('@strudel.cycles/midi'), */ - // import('@strudel.cycles/osc'), - controls, // sadly, this cannot be exported from core directly (yet) - ); - // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; - await Promise.all([ - modulesLoading, - registerSynthSounds(), - registerZZFXSounds(), - samples(`${ds}/tidal-drum-machines.json`), - samples(`${ds}/piano.json`), - samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), - samples(`${ds}/vcsl.json`), - ]); - }, - afterEval: ({ code }) => { - window.location.hash = '#' + code2hash(code); - }, - }); - - // init settings - editor.updateSettings(initialSettings); - - logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight'); - const codeParam = window.location.href.split('#')[1] || ''; - - const initialCode = codeParam - ? hash2code(codeParam) - : `// @date 23-11-30 -// "teigrührgerät" @by froos - -stack( - stack( - s("bd(<3!3 5>,6)/2").bank('RolandTR707') - , - s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>") - .lastOf(8, x=>x.segment("12").end(.2).gain(isaw)) - , - s("[tb ~ tb]").bank('RolandTR707') - .clip(0).release(.08).room(.2) - ).off(-1/6, x=>x.speed(.7).gain(.2).degrade()) - , - stack( - note(",6) ~!2 [f1?]*2>") - .s("sawtooth").lpf(perlin.range(400,1000)) - .lpa(.1).lpenv(-3).room(.2) - .lpq(8).noise(.2) - .add(note("0,.1")) - , - chord("<~ Gm9 ~!2>") - .dict('ireal').voicing() - .s("sawtooth").vib("2:.1") - .lpf(1000).lpa(.1).lpenv(-4) - .room(.5) - , - n(run(3)).chord("/8") - .dict('ireal-ext') - .off(1/2, add(n(4))) - .voicing() - .clip(.1).release(.05) - .s("sine").jux(rev) - .sometimesBy(sine.slow(16), add(note(12))) - .room(.75) - .lpf(sine.range(200,2000).slow(16)) - .gain(saw.slow(4).div(2)) - ).add(note(perlin.range(0,.5))) -)`; - - editor.setCode(initialCode); // simpler alternative to above init - - // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key])); - onEvent('strudel-toggle-play', () => editor.toggle()); -} - -run(); - -function onEvent(key, callback) { - const listener = (e) => { - if (e.data === key) { - callback(); - } - }; - window.addEventListener('message', listener); - return () => window.removeEventListener('message', listener); -} - -// settings form -function getInput(form, name) { - return form.querySelector(`input[name=${name}]`) || form.querySelector(`select[name=${name}]`); -} -function getFormValues(form, initial) { - const entries = Object.entries(initial).map(([key, initialValue]) => { - const input = getInput(form, key); - if (!input) { - return [key, initialValue]; // fallback - } - if (input.type === 'checkbox') { - return [key, input.checked]; - } - if (input.type === 'number') { - return [key, Number(input.value)]; - } - if (input.tagName === 'SELECT') { - return [key, input.value]; - } - return [key, input.value]; - }); - return Object.fromEntries(entries); -} -function setFormValues(form, values) { - Object.entries(values).forEach(([key, value]) => { - const input = getInput(form, key); - if (!input) { - return; - } - if (input.type === 'checkbox') { - input.checked = !!value; - } else if (input.type === 'number') { - input.value = value; - } else if (input.tagName) { - input.value = value; - } - }); -} - -const form = document.querySelector('form[name=settings]'); -setFormValues(form, initialSettings); -form.addEventListener('change', () => { - const values = getFormValues(form, initialSettings); - // console.log('values', values); - editor.updateSettings(values); -}); diff --git a/packages/codemirror/examples/strudelmirror/package.json b/packages/codemirror/examples/strudelmirror/package.json deleted file mode 100644 index 5c946bff..00000000 --- a/packages/codemirror/examples/strudelmirror/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "strudelmirror", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "devDependencies": { - "vite": "^5.0.8" - }, - "dependencies": { - "@strudel/codemirror": "workspace:*", - "@strudel.cycles/core":"workspace:*", - "@strudel.cycles/transpiler":"workspace:*", - "@strudel.cycles/tonal":"workspace:*", - "@strudel.cycles/mini":"workspace:*", - "@strudel.cycles/xen":"workspace:*", - "@strudel.cycles/webaudio":"workspace:*", - "@strudel/hydra":"workspace:*", - "@strudel.cycles/serial":"workspace:*", - "@strudel.cycles/soundfonts":"workspace:*", - "@strudel.cycles/csound":"workspace:*", - "@strudel.cycles/midi":"workspace:*", - "@strudel.cycles/osc":"workspace:*" - } -} diff --git a/packages/codemirror/examples/strudelmirror/style.css b/packages/codemirror/examples/strudelmirror/style.css deleted file mode 100644 index fabc795c..00000000 --- a/packages/codemirror/examples/strudelmirror/style.css +++ /dev/null @@ -1,33 +0,0 @@ -:root { - --foreground: white; -} - -body, -input { - font-family: monospace; - background: black; - color: white; -} - -html, -body, -#code, -.cm-editor, -.cm-scroller { - padding: 0; - margin: 0; - height: 100%; -} - -.settings { - position: fixed; - right: 0; - top: 0; - z-index: 1000; - display: flex-col; - padding: 10px; -} - -.settings > form > * + * { - margin-top: 10px; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67599cd9..6fecabff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: '@lezer/highlight': specifier: ^1.1.4 version: 1.1.4 + '@nanostores/persistent': + specifier: ^0.8.0 + version: 0.8.0(nanostores@0.8.1) '@replit/codemirror-emacs': specifier: ^6.0.1 version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) @@ -117,6 +120,9 @@ importers: '@uiw/codemirror-themes-all': specifier: ^4.19.16 version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + nanostores: + specifier: ^0.8.1 + version: 0.8.1 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -125,52 +131,6 @@ importers: specifier: ^4.3.3 version: 4.3.3 - packages/codemirror/examples/strudelmirror: - dependencies: - '@strudel.cycles/core': - specifier: workspace:* - version: link:../../../core - '@strudel.cycles/csound': - specifier: workspace:* - version: link:../../../csound - '@strudel.cycles/midi': - specifier: workspace:* - version: link:../../../midi - '@strudel.cycles/mini': - specifier: workspace:* - version: link:../../../mini - '@strudel.cycles/osc': - specifier: workspace:* - version: link:../../../osc - '@strudel.cycles/serial': - specifier: workspace:* - version: link:../../../serial - '@strudel.cycles/soundfonts': - specifier: workspace:* - version: link:../../../soundfonts - '@strudel.cycles/tonal': - specifier: workspace:* - version: link:../../../tonal - '@strudel.cycles/transpiler': - specifier: workspace:* - version: link:../../../transpiler - '@strudel.cycles/webaudio': - specifier: workspace:* - version: link:../../../webaudio - '@strudel.cycles/xen': - specifier: workspace:* - version: link:../../../xen - '@strudel/codemirror': - specifier: workspace:* - version: link:../.. - '@strudel/hydra': - specifier: workspace:* - version: link:../../../hydra - devDependencies: - vite: - specifier: ^5.0.8 - version: 5.0.8 - packages/core: dependencies: fraction.js: @@ -4200,110 +4160,6 @@ packages: picomatch: 2.3.1 dev: false - /@rollup/rollup-android-arm-eabi@4.9.0: - resolution: {integrity: sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.9.0: - resolution: {integrity: sha512-im6hUEyQ7ZfoZdNvtwgEJvBWZYauC9KVKq1w58LG2Zfz6zMd8gRrbN+xCVoqA2hv/v6fm9lp5LFGJ3za8EQH3A==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.9.0: - resolution: {integrity: sha512-u7aTMskN6Dmg1lCT0QJ+tINRt+ntUrvVkhbPfFz4bCwRZvjItx2nJtwJnJRlKMMaQCHRjrNqHRDYvE4mBm3DlQ==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.9.0: - resolution: {integrity: sha512-8FvEl3w2ExmpcOmX5RJD0yqXcVSOqAJJUJ29Lca29Ik+3zPS1yFimr2fr5JSZ4Z5gt8/d7WqycpgkX9nocijSw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.9.0: - resolution: {integrity: sha512-lHoKYaRwd4gge+IpqJHCY+8Vc3hhdJfU6ukFnnrJasEBUvVlydP8PuwndbWfGkdgSvZhHfSEw6urrlBj0TSSfg==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.9.0: - resolution: {integrity: sha512-JbEPfhndYeWHfOSeh4DOFvNXrj7ls9S/2omijVsao+LBPTPayT1uKcK3dHW3MwDJ7KO11t9m2cVTqXnTKpeaiw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.9.0: - resolution: {integrity: sha512-ahqcSXLlcV2XUBM3/f/C6cRoh7NxYA/W7Yzuv4bDU1YscTFw7ay4LmD7l6OS8EMhTNvcrWGkEettL1Bhjf+B+w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.9.0: - resolution: {integrity: sha512-uwvOYNtLw8gVtrExKhdFsYHA/kotURUmZYlinH2VcQxNCQJeJXnkmWgw2hI9Xgzhgu7J9QvWiq9TtTVwWMDa+w==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.9.0: - resolution: {integrity: sha512-m6pkSwcZZD2LCFHZX/zW2aLIISyzWLU3hrLLzQKMI12+OLEzgruTovAxY5sCZJkipklaZqPy/2bEEBNjp+Y7xg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.9.0: - resolution: {integrity: sha512-VFAC1RDRSbU3iOF98X42KaVicAfKf0m0OvIu8dbnqhTe26Kh6Ym9JrDulz7Hbk7/9zGc41JkV02g+p3BivOdAg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.9.0: - resolution: {integrity: sha512-9jPgMvTKXARz4inw6jezMLA2ihDBvgIU9Ml01hjdVpOcMKyxFBJrn83KVQINnbeqDv0+HdO1c09hgZ8N0s820Q==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.9.0: - resolution: {integrity: sha512-WE4pT2kTXQN2bAv40Uog0AsV7/s9nT9HBWXAou8+++MBCnY51QS02KYtm6dQxxosKi1VIz/wZIrTQO5UP2EW+Q==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.9.0: - resolution: {integrity: sha512-aPP5Q5AqNGuT0tnuEkK/g4mnt3ZhheiXrDIiSVIHN9mcN21OyXDVbEMqmXPE7e2OplNLDkcvV+ZoGJa2ZImFgw==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@sigstore/protobuf-specs@0.1.0: resolution: {integrity: sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -12791,27 +12647,6 @@ packages: optionalDependencies: fsevents: 2.3.3 - /rollup@4.9.0: - resolution: {integrity: sha512-bUHW/9N21z64gw8s6tP4c88P382Bq/L5uZDowHlHx6s/QWpjJXivIAbEw6LZthgSvlEizZBfLC4OAvWe7aoF7A==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.9.0 - '@rollup/rollup-android-arm64': 4.9.0 - '@rollup/rollup-darwin-arm64': 4.9.0 - '@rollup/rollup-darwin-x64': 4.9.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.9.0 - '@rollup/rollup-linux-arm64-gnu': 4.9.0 - '@rollup/rollup-linux-arm64-musl': 4.9.0 - '@rollup/rollup-linux-riscv64-gnu': 4.9.0 - '@rollup/rollup-linux-x64-gnu': 4.9.0 - '@rollup/rollup-linux-x64-musl': 4.9.0 - '@rollup/rollup-win32-arm64-msvc': 4.9.0 - '@rollup/rollup-win32-ia32-msvc': 4.9.0 - '@rollup/rollup-win32-x64-msvc': 4.9.0 - fsevents: 2.3.3 - dev: true - /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -14470,41 +14305,6 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vite@5.0.8: - resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.19.5 - postcss: 8.4.32 - rollup: 4.9.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vitefu@0.2.4(vite@4.5.0): resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} peerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index baafa3c3..615475e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,3 @@ packages: - "packages/react/examples/nano-repl" - "packages/web/examples/repl-example" - "packages/superdough/example" - - "packages/codemirror/examples/strudelmirror" From 1cdb5964c62eb3e383a5ca02e46800253ac3d52d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Dec 2023 21:16:29 +0100 Subject: [PATCH 28/35] settings sync --- packages/codemirror/codemirror.mjs | 24 +++++++++++++++++++++--- packages/codemirror/package.json | 4 +++- packages/repl/repl-component.mjs | 24 ++++-------------------- website/src/docs/MicroRepl.jsx | 16 ---------------- website/src/pages/vanilla/mini.astro | 5 +++-- website/src/repl/vanilla/vanilla.mjs | 19 +++---------------- 6 files changed, 34 insertions(+), 58 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index cf0a3f62..25639a72 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -12,6 +12,7 @@ import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocatio import { keybindings } from './keybindings.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs'; import { updateWidgets, sliderPlugin } from './slider.mjs'; +import { persistentMap } from '@nanostores/persistent'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -25,8 +26,25 @@ const extensions = { }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); +export const defaultSettings = { + keybindings: 'codemirror', + isLineNumbersDisplayed: true, + isActiveLineHighlighted: false, + isAutoCompletionEnabled: false, + isPatternHighlightingEnabled: true, + isFlashEnabled: true, + isTooltipEnabled: false, + isLineWrappingEnabled: false, + theme: 'strudelTheme', + fontFamily: 'monospace', + fontSize: 18, +}; + +export const codemirrorSettings = persistentMap('codemirror-settings', defaultSettings); + // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) { + const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), ); @@ -87,7 +105,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, set export class StrudelMirror { constructor(options) { - const { root, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, settings, ...replOptions } = options; + const { root, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, ...replOptions } = options; this.code = initialCode; this.root = root; this.miniLocations = []; @@ -142,7 +160,6 @@ export class StrudelMirror { }); this.editor = initEditor({ root, - settings, initialCode, onChange: (v) => { if (v.docChanged) { @@ -237,6 +254,7 @@ export class StrudelMirror { for (let key in extensions) { this.reconfigureExtension(key, settings[key]); } + codemirrorSettings.set({ ...codemirrorSettings.get(), ...settings }); } changeSetting(key, value) { if (extensions[key]) { diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index a309efa2..0c57db8b 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -47,7 +47,9 @@ "@strudel.cycles/core": "workspace:*", "@uiw/codemirror-themes": "^4.19.16", "@uiw/codemirror-themes-all": "^4.19.16", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "nanostores": "^0.8.1", + "@nanostores/persistent": "^0.8.0" }, "devDependencies": { "vite": "^4.3.3" diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index b8947640..b8dc4cb9 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -1,7 +1,7 @@ import { getDrawContext, silence } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; -import { StrudelMirror } from '@strudel/codemirror'; +import { StrudelMirror, defaultSettings, codemirrorSettings } from '@strudel/codemirror'; import { prebake } from './prebake.mjs'; function camelToKebab(camelCaseString) { @@ -13,24 +13,10 @@ function kebabToCamel(kebabCaseString) { }); } -const initialSettings = { - keybindings: 'strudelTheme', - isLineNumbersDisplayed: true, - isActiveLineHighlighted: true, - isAutoCompletionEnabled: false, - isPatternHighlightingEnabled: true, - isFlashEnabled: true, - isTooltipEnabled: false, - isLineWrappingEnabled: false, - theme: 'strudelTheme', - fontFamily: 'monospace', - fontSize: 18, -}; -const settingAttributes = Object.keys(initialSettings).map(camelToKebab); +const settingAttributes = Object.keys(defaultSettings).map(camelToKebab); const parseAttribute = (name, value) => { const camel = kebabToCamel(name); - const type = typeof initialSettings[camel]; - // console.log('type', type, name); + const type = typeof defaultSettings[camel]; if (type === 'boolean') { return ['1', 'true'].includes(value); } @@ -39,11 +25,10 @@ const parseAttribute = (name, value) => { } return value; }; -// console.log('attributes', settingAttributes); if (typeof HTMLElement !== 'undefined') { class StrudelRepl extends HTMLElement { static observedAttributes = ['code', ...settingAttributes]; - settings = initialSettings; + settings = codemirrorSettings.get(); editor = null; constructor() { super(); @@ -55,7 +40,6 @@ if (typeof HTMLElement !== 'undefined') { } else if (settingAttributes.includes(name)) { const camel = kebabToCamel(name); this.settings[camel] = parseAttribute(name, newValue); - // console.log('name', name, newValue, camel, this.settings[camel]); this.editor?.updateSettings(this.settings); } } diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 8a2280e3..7e97a9bb 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -7,20 +7,6 @@ import { StrudelMirror } from '@strudel/codemirror'; import { prebake } from '@strudel/repl'; import { useInView } from 'react-hook-inview'; -const initialSettings = { - keybindings: 'strudelTheme', - isLineNumbersDisplayed: true, - isActiveLineHighlighted: false, - isAutoCompletionEnabled: false, - isPatternHighlightingEnabled: true, - isFlashEnabled: true, - isTooltipEnabled: false, - isLineWrappingEnabled: false, - theme: 'strudelTheme', - fontFamily: 'monospace', - fontSize: 18, -}; - export function MicroRepl({ code, hideHeader = false, @@ -56,7 +42,6 @@ export function MicroRepl({ root: containerRef.current, initialCode: '// LOADING', pattern: silence, - settings: initialSettings, drawTime, onDraw, editPattern: (pat, id) => { @@ -76,7 +61,6 @@ export function MicroRepl({ }, }); // init settings - editor.updateSettings(initialSettings); editor.setCode(code); editorRef.current = editor; }, []); diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro index f2a25703..989582aa 100644 --- a/website/src/pages/vanilla/mini.astro +++ b/website/src/pages/vanilla/mini.astro @@ -5,7 +5,7 @@

This is a REPL:

- + code={`s("bd")`}> --> +

This is another REPL:

This is another REPL:

From e17df79133d4809eb0eff4441b5495873530556c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Dec 2023 22:42:51 +0100 Subject: [PATCH 31/35] microrepl: ssr static code --- website/src/docs/MicroRepl.jsx | 31 ++++++----- website/src/pages/recipes/recipes-next.mdx | 62 +++++++++++----------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 7e97a9bb..778e209d 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -1,11 +1,10 @@ -import { useState, useRef, useCallback, useMemo } from 'react'; +import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { Icon } from './Icon'; import { silence, getPunchcardPainter } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; import { StrudelMirror } from '@strudel/codemirror'; import { prebake } from '@strudel/repl'; -import { useInView } from 'react-hook-inview'; export function MicroRepl({ code, @@ -65,21 +64,26 @@ export function MicroRepl({ editorRef.current = editor; }, []); - const [ref, isVisible] = useInView({ - threshold: 0.01, - onEnter: () => { - if (!editorRef.current) { - init({ code, shouldDraw }); - } - }, - }); const [replState, setReplState] = useState({}); const { started, isDirty, error } = replState; const editorRef = useRef(); const containerRef = useRef(); + const [client, setClient] = useState(false); + useEffect(() => { + setClient(true); + if (!editorRef.current) { + setTimeout(() => { + init({ code, shouldDraw }); + }); + } + }, []); + + if (!client) { + return
{code}
; + } return ( -
+
{!hideHeader && (
@@ -117,11 +121,6 @@ export function MicroRepl({ if (el && el.width !== el.clientWidth) { el.width = el.clientWidth; } - //const ratio = el.clientWidth / canvasHeight; - //const targetWidth = Math.round(el.width * ratio); - //if (el.width !== targetWidth) { - // el.width = targetWidth; - //} }} > )} diff --git a/website/src/pages/recipes/recipes-next.mdx b/website/src/pages/recipes/recipes-next.mdx index 62a1a5e9..4230063f 100644 --- a/website/src/pages/recipes/recipes-next.mdx +++ b/website/src/pages/recipes/recipes-next.mdx @@ -17,7 +17,7 @@ An arpeggio is when the notes of a chord are played in sequence. We can either write the notes by hand: ") @@ -92,7 +92,7 @@ s("amen/8").fit() If we use `splice` instead of `slice`, the speed adjusts to the duration of the event: ") @@ -107,7 +107,7 @@ Note that we don't need `fit`, because `splice` will do that by itself. A minimal filter envelope looks like this: d2") .s("sawtooth") .lpf(400).lpa(.2).lpenv(4) @@ -117,7 +117,7 @@ A minimal filter envelope looks like this: We can flip the envelope by setting `lpenv` negative + add some resonance `lpq`: d2") .s("sawtooth").lpq(8) .lpf(400).lpa(.2).lpenv(-4) @@ -129,7 +129,7 @@ We can flip the envelope by setting `lpenv` negative + add some resonance `lpq`: We can layer sounds by separating them with ",": ") .s("sawtooth, square") // <------ .scope()`} @@ -138,7 +138,7 @@ We can layer sounds by separating them with ",": We can control the gain of individual sounds like this: ") .s("sawtooth, square:0:.5") // <--- "name:number:gain" .scope()`} @@ -147,7 +147,7 @@ We can control the gain of individual sounds like this: For more control over each voice, we can use `layer`: ").layer( x=>x.s("sawtooth").vib(4), x=>x.s("square").add(note(12)) @@ -162,7 +162,7 @@ With `layer`, you can use any pattern method available on each voice, so sky is We can fatten a sound by adding a detuned version to itself: ") .add(note("0,.1")) // <------ chorus .s("sawtooth").scope()`} @@ -175,7 +175,7 @@ Try out different values, or add another voice! Here is a simple example of a polyrhythm: - + A polyrhythm is when 2 different tempos happen at the same time. @@ -183,7 +183,7 @@ A polyrhythm is when 2 different tempos happen at the same time. This is a polymeter: -,").fast(2)`} punchcard /> +,").fast(2)`} punchcard /> A polymeter is when 2 different bar lengths play at the same tempo. @@ -191,7 +191,7 @@ A polymeter is when 2 different bar lengths play at the same tempo. This is a phasing: -*[6,6.1]").piano()`} punchcard /> +*[6,6.1]").piano()`} punchcard /> Phasing happens when the same sequence plays at slightly different tempos. @@ -200,7 +200,7 @@ Phasing happens when the same sequence plays at slightly different tempos. Using `run` with `n`, we can rush through a sample bank: @@ -219,7 +219,7 @@ In this case, I hear the beginning at the third sample, which can be accounted f Let's add some randomness: /2")`} /> @@ -250,7 +250,7 @@ The value of clip is relative to the duration of each event. We can also create overlaps using release: /2")`} /> @@ -259,7 +259,7 @@ This will smoothly fade out each sound for the given number of seconds. We could also make the notes shorter with decay / sustain: /2").sustain(0)`} /> @@ -268,27 +268,27 @@ For now, there is a limitation where decay values that exceed the event duration When using samples, we also have `.end` to cut relative to the sample length: -")`} /> +")`} /> Compare that to clip: -")`} /> +")`} /> or decay / sustain -").sustain(0)`} /> +").sustain(0)`} /> ## Wavetable Synthesis You can loop a sample with `loop` / `loopEnd`: -").s("bd").loop(1).loopEnd(.05).gain(.2)`} /> +").s("bd").loop(1).loopEnd(.05).gain(.2)`} /> This allows us to play the first 5% of the bass drum as a synth! To simplify loading wavetables, any sample that starts with `wt_` will be looped automatically: @@ -296,7 +296,7 @@ note("c eb g bb").s("wt_dbass").clip(2)`} Running through different wavetables can also give interesting variations: @@ -304,7 +304,7 @@ note("c2*8").s("wt_dbass").n(run(8))`} ...adding a filter envelope + reverb: Date: Sun, 17 Dec 2023 22:51:43 +0100 Subject: [PATCH 32/35] fix: first frame --- packages/codemirror/codemirror.mjs | 2 +- packages/core/draw.mjs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index bbcf0792..02ee37d9 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -188,7 +188,7 @@ export class StrudelMirror { await this.prebaked; try { await this.repl.evaluate(this.code, false); - this.drawer.invalidate(this.repl.scheduler); + this.drawer.invalidate(this.repl.scheduler, -0.001); // draw at -0.001 to avoid haps at 0 to be visualized as active this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters); } catch (err) { diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index c57baa63..ff5359e1 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -145,12 +145,13 @@ export class Drawer { }, ); } - invalidate(scheduler = this.scheduler) { + invalidate(scheduler = this.scheduler, t) { if (!scheduler) { return; } + // TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug... + t = t ?? scheduler.now(); this.scheduler = scheduler; - const t = scheduler.now(); let [_, lookahead] = this.drawTime; const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps From e8e8f888dd6d47fdb7701b7c15794db5164717c3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Dec 2023 15:21:54 +0100 Subject: [PATCH 33/35] fix: stop other repls on start --- packages/codemirror/codemirror.mjs | 26 +++++++++++++++++++++++++- website/src/docs/MicroRepl.jsx | 4 ++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 02ee37d9..6ad94209 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -108,7 +108,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo export class StrudelMirror { constructor(options) { - const { root, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, ...replOptions } = options; + const { root, id, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, ...replOptions } = options; this.code = initialCode; this.root = root; this.miniLocations = []; @@ -116,6 +116,7 @@ export class StrudelMirror { this.painters = []; this.onDraw = onDraw; const self = this; + this.id = id || s4(); this.drawer = new Drawer((haps, time) => { const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped); @@ -139,6 +140,12 @@ export class StrudelMirror { replOptions?.onToggle?.(started); if (started) { this.drawer.start(this.repl.scheduler); + // stop other repls when this one is started + document.dispatchEvent( + new CustomEvent('start-repl', { + detail: this.id, + }), + ); } else { this.drawer.stop(); updateMiniLocations(this.editor, []); @@ -179,6 +186,13 @@ export class StrudelMirror { this.root.style.backgroundColor = 'var(--background)'; cmEditor.style.backgroundColor = 'transparent'; } + // stop this repl when another repl is started + this.onStartRepl = (e) => { + if (e.detail !== this.id) { + this.stop(); + } + }; + document.addEventListener('start-repl', this.onStartRepl); } async drawFirstFrame() { if (!this.onDraw) { @@ -274,8 +288,18 @@ export class StrudelMirror { const changes = { from: 0, to: this.editor.state.doc.length, insert: code }; this.editor.dispatch({ changes }); } + clear() { + this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); + } } function parseBooleans(value) { return { true: true, false: false }[value] ?? value; } + +// helper function to generate repl ids +function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} diff --git a/website/src/docs/MicroRepl.jsx b/website/src/docs/MicroRepl.jsx index 778e209d..72561669 100644 --- a/website/src/docs/MicroRepl.jsx +++ b/website/src/docs/MicroRepl.jsx @@ -34,6 +34,7 @@ export function MicroRepl({ } const editor = new StrudelMirror({ + id, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, @@ -76,6 +77,9 @@ export function MicroRepl({ init({ code, shouldDraw }); }); } + return () => { + editor.clear(); + }; }, []); if (!client) { From 0ed615a312738aad7031ef79cedd108a1abf612d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Dec 2023 15:32:12 +0100 Subject: [PATCH 34/35] fix: add .piano function --- packages/repl/prebake.mjs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 02638d1a..80d2c3bd 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -1,4 +1,4 @@ -import { controls, evalScope } from '@strudel.cycles/core'; +import { controls, noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel.cycles/core'; import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio'; import * as core from '@strudel.cycles/core'; @@ -37,3 +37,18 @@ export async function prebake() { samples(`${ds}/vcsl.json`), ]); } + +const maxPan = noteToMidi('C8'); +const panwidth = (pan, width) => pan * width + (1 - width) / 2; + +Pattern.prototype.piano = function () { + return this.fmap((v) => ({ ...v, clip: v.clip ?? 1 })) // set clip if not already set.. + .s('piano') + .release(0.1) + .fmap((value) => { + const midi = valueToMidi(value); + // pan by pitch + const pan = panwidth(Math.min(Math.round(midi) / maxPan, 1), 0.5); + return { ...value, pan: (value.pan || 1) * pan }; + }); +}; From de4460a0470d572d309ae69fd99251d49847703e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Dec 2023 15:45:49 +0100 Subject: [PATCH 35/35] remove redundant test pages --- website/src/pages/vanilla/micro.astro | 32 --------------------------- website/src/pages/vanilla/mini.astro | 31 -------------------------- 2 files changed, 63 deletions(-) delete mode 100644 website/src/pages/vanilla/micro.astro delete mode 100644 website/src/pages/vanilla/mini.astro diff --git a/website/src/pages/vanilla/micro.astro b/website/src/pages/vanilla/micro.astro deleted file mode 100644 index e8414eed..00000000 --- a/website/src/pages/vanilla/micro.astro +++ /dev/null @@ -1,32 +0,0 @@ ---- -import { MicroRepl } from '../../docs/MicroRepl' ---- - - - - Strudel Micro REPL - - - -
-

MicroRepl:

- /16") - .dict('ireal') - .voicing() - .when("<1 0> 0@3", sub(note(12))) - .add(note("[12 | 0]*4")) - .attack(slider(0.511)) - .decay(slider(0.466)) - .sustain(slider(0.398)) - .release(slider(0.443)) - .s('sawtooth') - .lpa(sine.range(.1,.25).slow(12)) - .lpenv(sine.range(0,4).slow(16)) - .lpf(perlin.range(400,1000)) - .add(note(perlin.range(0,.25))) - .vib(4).vibmod(.1) - .room(.75).size(8)`} /> -
- - diff --git a/website/src/pages/vanilla/mini.astro b/website/src/pages/vanilla/mini.astro deleted file mode 100644 index 34438309..00000000 --- a/website/src/pages/vanilla/mini.astro +++ /dev/null @@ -1,31 +0,0 @@ - - - Strudel Vanilla REPL - - - -

This is a REPL:

- - -

This is another REPL:

- - -