diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 5ceb9619c..6ad942094 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -10,8 +10,9 @@ import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
-import { theme } from './themes.mjs';
+import { initTheme, activateTheme, theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
+import { persistentAtom } from '@nanostores/persistent';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -25,11 +26,32 @@ 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 = persistentAtom('codemirror-settings', defaultSettings, {
+ encode: JSON.stringify,
+ decode: JSON.parse,
+});
+
// 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]))),
);
+ initTheme(settings.theme);
let state = EditorState.create({
doc: initialCode,
extensions: [
@@ -86,7 +108,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, id, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, ...replOptions } = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
@@ -94,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);
@@ -101,14 +124,15 @@ 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;
};
this.prebaked = prebake();
- // this.drawFirstFrame();
+ autodraw && this.drawFirstFrame();
this.repl = repl({
...replOptions,
@@ -116,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, []);
@@ -140,13 +170,11 @@ export class StrudelMirror {
});
this.editor = initEditor({
root,
- settings,
initialCode,
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(),
@@ -154,9 +182,17 @@ 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';
}
+ // 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) {
@@ -166,8 +202,9 @@ export class StrudelMirror {
await this.prebaked;
try {
await this.repl.evaluate(this.code, false);
- this.drawer.invalidate(this.repl.scheduler);
- this.onDraw?.(this.drawer.visibleHaps, 0, []);
+ 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) {
console.warn('first frame could not be painted');
}
@@ -181,7 +218,7 @@ export class StrudelMirror {
}
async toggle() {
if (this.repl.scheduler.started) {
- this.repl.scheduler.stop();
+ this.repl.stop();
} else {
this.evaluate();
}
@@ -212,6 +249,9 @@ export class StrudelMirror {
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
+ if (key === 'theme') {
+ activateTheme(value);
+ }
}
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
@@ -231,6 +271,8 @@ export class StrudelMirror {
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
+ const updated = { ...codemirrorSettings.get(), ...settings };
+ codemirrorSettings.set(updated);
}
changeSetting(key, value) {
if (extensions[key]) {
@@ -246,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/packages/codemirror/examples/strudelmirror/.gitignore b/packages/codemirror/examples/strudelmirror/.gitignore
deleted file mode 100644
index a547bf36d..000000000
--- 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 0e1d43cef..000000000
--- 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 676f4b97b..000000000
--- 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 5c946bff8..000000000
--- 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 fabc795c4..000000000
--- 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/packages/codemirror/package.json b/packages/codemirror/package.json
index a309efa2d..0c57db8b9 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/codemirror/themes.mjs b/packages/codemirror/themes.mjs
index 71fb76423..ee3e05bfa 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/packages/core/draw.mjs b/packages/core/draw.mjs
index c57baa63d..ff5359e14 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
diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs
index 254dd94a0..4a8fd8db4 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/packages/core/repl.mjs b/packages/core/repl.mjs
index 0bd5229df..b3c7a5435 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/.gitignore b/packages/repl/.gitignore
new file mode 100644
index 000000000..10dcd67fb
--- /dev/null
+++ b/packages/repl/.gitignore
@@ -0,0 +1 @@
+stats.html
\ No newline at end of file
diff --git a/packages/repl/README.md b/packages/repl/README.md
new file mode 100644
index 000000000..ff3109487
--- /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/examples/simple.html b/packages/repl/examples/simple.html
new file mode 100644
index 000000000..849a63689
--- /dev/null
+++ b/packages/repl/examples/simple.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/packages/repl/index.mjs b/packages/repl/index.mjs
new file mode 100644
index 000000000..4119059d6
--- /dev/null
+++ b/packages/repl/index.mjs
@@ -0,0 +1,2 @@
+export * from './repl-component.mjs';
+export * from './prebake.mjs';
diff --git a/packages/repl/package.json b/packages/repl/package.json
new file mode 100644
index 000000000..aaa959837
--- /dev/null
+++ b/packages/repl/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@strudel/repl",
+ "version": "0.9.4",
+ "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": {
+ "@rollup/plugin-replace": "^5.0.5",
+ "@strudel.cycles/core": "workspace:*",
+ "@strudel.cycles/midi": "workspace:*",
+ "@strudel.cycles/mini": "workspace:*",
+ "@strudel.cycles/soundfonts": "workspace:*",
+ "@strudel.cycles/tonal": "workspace:*",
+ "@strudel.cycles/transpiler": "workspace:*",
+ "@strudel.cycles/webaudio": "workspace:*",
+ "@strudel/codemirror": "workspace:*",
+ "@strudel/hydra": "workspace:*",
+ "rollup-plugin-visualizer": "^5.8.1"
+ },
+ "devDependencies": {
+ "vite": "^4.3.3"
+ }
+}
diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs
new file mode 100644
index 000000000..80d2c3bd4
--- /dev/null
+++ b/packages/repl/prebake.mjs
@@ -0,0 +1,54 @@
+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';
+
+export async function prebake() {
+ const modulesLoading = evalScope(
+ // import('@strudel.cycles/core'),
+ 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(),
+ // 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`),
+ samples(`${ds}/EmuSP12.json`),
+ 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 };
+ });
+};
diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs
new file mode 100644
index 000000000..1aece8a47
--- /dev/null
+++ b/packages/repl/repl-component.mjs
@@ -0,0 +1,69 @@
+import { getDrawContext, silence } from '@strudel.cycles/core';
+import { transpiler } from '@strudel.cycles/transpiler';
+import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
+import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
+import { prebake } from './prebake.mjs';
+
+if (typeof HTMLElement !== 'undefined') {
+ class StrudelRepl extends HTMLElement {
+ static observedAttributes = ['code'];
+ settings = codemirrorSettings.get();
+ editor = null;
+ constructor() {
+ super();
+ }
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name === 'code') {
+ this.code = newValue;
+ this.editor?.setCode(newValue);
+ }
+ }
+ 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,
+ 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);
+ }
+ // Element functionality written in here
+ }
+
+ customElements.define('strudel-editor', StrudelRepl);
+}
diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js
new file mode 100644
index 000000000..49391bf3f
--- /dev/null
+++ b/packages/repl/vite.config.js
@@ -0,0 +1,28 @@
+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({
+ plugins: [],
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'index.mjs'),
+ name: 'strudel',
+ formats: ['es', 'iife'],
+ fileName: (ext) => ({ es: 'index.mjs', iife: 'index.js' }[ext]),
+ },
+ 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 a2416a62e..6fecabffe 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:
@@ -468,6 +428,46 @@ importers:
specifier: ^4.3.3
version: 4.3.3
+ packages/repl:
+ dependencies:
+ '@rollup/plugin-replace':
+ specifier: ^5.0.5
+ version: 5.0.5
+ '@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
+ rollup-plugin-visualizer:
+ specifier: ^5.8.1
+ version: 5.9.0
+ devDependencies:
+ vite:
+ specifier: ^4.3.3
+ version: 4.5.0
+
packages/serial:
dependencies:
'@strudel.cycles/core':
@@ -702,6 +702,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
@@ -750,6 +753,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
@@ -4115,6 +4121,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'}
@@ -4127,109 +4146,19 @@ packages:
rollup: 2.79.1
dev: true
- /@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
+ /@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
/@sigstore/protobuf-specs@0.1.0:
resolution: {integrity: sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==}
@@ -6234,7 +6163,6 @@ packages:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
- dev: true
/clone-buffer@1.0.0:
resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==}
@@ -6739,7 +6667,6 @@ packages:
/define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
- dev: true
/define-properties@1.1.4:
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
@@ -7547,6 +7474,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:
@@ -8003,7 +7934,6 @@ packages:
/get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- dev: true
/get-func-name@2.0.0:
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
@@ -8898,7 +8828,6 @@ packages:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
- dev: true
/is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
@@ -9125,7 +9054,6 @@ packages:
engines: {node: '>=8'}
dependencies:
is-docker: 2.2.1
- dev: true
/is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
@@ -11259,7 +11187,6 @@ packages:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
- dev: true
/optionator@0.8.3:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
@@ -12525,7 +12452,6 @@ packages:
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
- dev: true
/require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
@@ -12697,7 +12623,6 @@ packages:
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.6.2
- dev: true
/rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
@@ -12722,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'}
@@ -14331,7 +14235,7 @@ packages:
fsevents: 2.3.3
dev: true
- /vite@4.5.0(@types/node@18.16.3):
+ /vite@4.5.0:
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -14359,19 +14263,19 @@ packages:
terser:
optional: true
dependencies:
- '@types/node': 18.16.3
esbuild: 0.18.20
- postcss: 8.4.31
+ postcss: 8.4.32
rollup: 3.28.0
optionalDependencies:
fsevents: 2.3.3
+ dev: true
- /vite@5.0.8:
- resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==}
- engines: {node: ^18.0.0 || >=20.0.0}
+ /vite@4.5.0(@types/node@18.16.3):
+ resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
- '@types/node': ^18.0.0 || >=20.0.0
+ '@types/node': '>= 14'
less: '*'
lightningcss: ^1.21.0
sass: '*'
@@ -14394,12 +14298,12 @@ packages:
terser:
optional: true
dependencies:
- esbuild: 0.19.5
- postcss: 8.4.32
- rollup: 4.9.0
+ '@types/node': 18.16.3
+ esbuild: 0.18.20
+ postcss: 8.4.31
+ rollup: 3.28.0
optionalDependencies:
fsevents: 2.3.3
- dev: true
/vitefu@0.2.4(vite@4.5.0):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
@@ -14815,7 +14719,6 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
/wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
@@ -14898,7 +14801,6 @@ packages:
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
- dev: true
/yaeti@0.0.6:
resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==}
@@ -14958,7 +14860,6 @@ packages:
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
- dev: true
/yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index baafa3c35..615475e46 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"
diff --git a/website/package.json b/website/package.json
index f53548427..b2c5f39ab 100644
--- a/website/package.json
+++ b/website/package.json
@@ -34,9 +34,10 @@
"@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",
"@tailwindcss/typography": "^0.5.8",
@@ -53,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/components/HeadCommonNew.astro b/website/src/components/HeadCommonNew.astro
deleted file mode 100644
index 9f323a7a3..000000000
--- 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/docs/Icon.jsx b/website/src/docs/Icon.jsx
new file mode 100644
index 000000000..64d5f88ac
--- /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 000000000..725616698
--- /dev/null
+++ b/website/src/docs/MicroRepl.jsx
@@ -0,0 +1,151 @@
+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';
+
+export function MicroRepl({
+ code,
+ hideHeader = false,
+ canvasHeight = 100,
+ 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 = [0, 4];
+ 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({
+ id,
+ defaultOutput: webaudioOutput,
+ getTime: () => getAudioContext().currentTime,
+ transpiler,
+ autodraw: !!shouldDraw,
+ root: containerRef.current,
+ initialCode: '// LOADING',
+ pattern: silence,
+ drawTime,
+ 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) => {
+ setReplState({ ...state });
+ },
+ });
+ // init settings
+ editor.setCode(code);
+ editorRef.current = editor;
+ }, []);
+
+ 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 });
+ });
+ }
+ return () => {
+ editor.clear();
+ };
+ }, []);
+
+ if (!client) {
+ return {code} ;
+ }
+
+ return (
+
+ {!hideHeader && (
+
+
+ editorRef.current?.toggle()}
+ >
+
+
+ editorRef.current?.evaluate()}
+ >
+
+
+
+
+ )}
+
+
+ {error &&
{error.message}
}
+
+ {shouldDraw && (
+
{
+ 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(' ');
+}
+
+function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+}
diff --git a/website/src/pages/recipes/recipes-next.mdx b/website/src/pages/recipes/recipes-next.mdx
new file mode 100644
index 000000000..4230063f8
--- /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:
+
+
diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro
index 44144bb64..d4ea40b84 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
@@ -90,7 +85,8 @@ import HeadCommonNew from '../../components/HeadCommonNew.astro';
-
+
+
diff --git a/website/src/repl/vanilla/vanilla.css b/website/src/repl/vanilla/vanilla.css
index 584dd924d..5387fb04b 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 488fcf8c4..6f0cc0585 100644
--- a/website/src/repl/vanilla/vanilla.mjs
+++ b/website/src/repl/vanilla/vanilla.mjs
@@ -1,94 +1,12 @@
-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 { codemirrorSettings, defaultSettings } from '@strudel/codemirror';
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,
-};
-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
- editor.updateSettings(initialSettings);
-
+ const repl = document.getElementById('editor');
+ editor = repl.editor;
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] || '';
@@ -192,11 +110,8 @@ function setFormValues(form, values) {
}
const form = document.querySelector('form[name=settings]');
-setFormValues(form, initialSettings);
+setFormValues(form, codemirrorSettings.get());
form.addEventListener('change', () => {
- const values = getFormValues(form, initialSettings);
- // console.log('values', values);
- editor.updateSettings(values);
- // TODO: only activateTheme when it changes
- activateTheme(values.theme);
+ const values = getFormValues(form, defaultSettings);
+ editor?.updateSettings(values);
});