diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 725dd818..7933600e 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -6,7 +6,7 @@ import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language' import { Compartment, EditorState } from '@codemirror/state'; import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view'; import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core'; -import { isAutoCompletionEnabled } from './Autocomplete'; +// import { isAutoCompletionEnabled } from './Autocomplete'; import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; @@ -17,7 +17,7 @@ const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []), theme, - isAutoCompletionEnabled, + // isAutoCompletionEnabled, isPatternHighlightingEnabled, isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []), isFlashEnabled, diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro new file mode 100644 index 00000000..5377c08f --- /dev/null +++ b/website/src/pages/vanilla/index.astro @@ -0,0 +1,98 @@ +--- +import HeadCommon from '../../components/HeadCommon.astro'; +--- + + + + + Strudel Vanilla REPL + + +
+
+
+
+ + +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ + + diff --git a/website/src/repl/vanilla/vanilla.css b/website/src/repl/vanilla/vanilla.css new file mode 100644 index 00000000..7c985be7 --- /dev/null +++ b/website/src/repl/vanilla/vanilla.css @@ -0,0 +1,38 @@ +:root { + --foreground: white; +} + +body, +input { + font-family: monospace; + background-color: black; + color: white; +} + +input, +select { + background-color: black !important; +} + +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/website/src/repl/vanilla/vanilla.mjs b/website/src/repl/vanilla/vanilla.mjs new file mode 100644 index 00000000..6e5c54a3 --- /dev/null +++ b/website/src/repl/vanilla/vanilla.mjs @@ -0,0 +1,199 @@ +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 './vanilla.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); +});