Merge branch 'tidalcycles:main' into envelope_improvements

This commit is contained in:
Jade (Rose) Rowland 2023-12-29 22:27:52 -05:00 committed by GitHub
commit 12451ba5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 1183 additions and 6412 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 150 KiB

3
examples/README.md Normal file
View File

@ -0,0 +1,3 @@
# examples
This folder contains usage examples for different scenarios.

View File

@ -0,0 +1,5 @@
# buildless examples
These examples show you how strudel can be used in a regular html file, without the need for a build tool.
Most examples are using [skypack](https://www.skypack.dev/)

View File

@ -0,0 +1,31 @@
<script src="https://unpkg.com/@strudel/repl@0.9.4"></script>
<strudel-editor>
<!--
// @date 23-08-15
// "golf rolf" @by froos @license CC BY-NC-SA 4.0
setcps(1)
stack(
s("bd*2, ~ rim*<1!3 2>, hh*4").bank('RolandTR909')
.off(-1/8, set(speed("1.5").gain(.25)))
.mask("<0!16 1!64>")
,
note("g1(3,8)")
.s("gm_synth_bass_2:<0 2>")
.delay(".8:.25:.25")
.clip("<.5!16 2!32>")
.off(1/8, add(note("12?0.7")))
.lpf(sine.range(500,2000).slow(32)).lpq(8)
.add(note("0,.05"))
.mask("<0!8 1!32>")
,
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
).reset("<x@15 x(5,8)>")
-->
</strudel-editor>

View File

@ -0,0 +1,8 @@
# codemirror-repl example
This folder demonstrates how to set up a full strudel repl with the `@strudel/codemirror` package. Run it using:
```sh
pnpm i
pnpm dev
```

View File

@ -0,0 +1,13 @@
# headless-repl demo
This demo shows how to use strudel in "headless mode".
Buttons A / B / C will switch between different patterns.
It showcases the usage of the `@strudel/web` package, using [vite](https://vitejs.dev/) as the dev server.
## Running
```sh
pnpm i && cd examples/headless-repl
pnpm dev
# open http://localhost:5173/
```

1
examples/minimal-repl/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,10 @@
# minimal repl
This folder demonstrates how to set up a minimal strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```
If you're looking for a more feature rich alternative, have a look at the [../codemirror-repl](codemirror-repl example)

View File

@ -0,0 +1,11 @@
# superdough demo
This demo shows how to use [superdough](https://www.npmjs.com/package/superdough) with [vite](https://vitejs.dev/).
## Running
```sh
pnpm i && cd examples/headless-repl
pnpm dev
# open http://localhost:5173/
```

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<title>Superdough Example</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="play">PLAAAAAAAY</button>
<script type="module">
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
</script>
</body>
</html>

View File

@ -3,13 +3,3 @@
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
![dependencies](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
Generated with
```sh
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
```

View File

@ -1,7 +1,7 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
@ -10,36 +10,32 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc }) {
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
export function Autocomplete({ doc, label }) {
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
${doc.description}
<ul>
${doc.params?.map(
({ name, type, description }) =>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
)}
</ul>
<div>
${doc.examples?.map((example) => `<div><pre>${example}</pre></div>`)}
</div>
</div>`[0];
/*
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
*/
}
const jsdocCompletions = jsdoc.docs
@ -54,13 +50,7 @@ const jsdocCompletions = jsdoc.docs
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} />);
return node;
},
info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));

View File

@ -6,18 +6,21 @@ import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { Compartment, EditorState, Prec } 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.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
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 : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
// isAutoCompletionEnabled,
isAutoCompletionEnabled,
isTooltipEnabled,
isPatternHighlightingEnabled,
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
@ -25,11 +28,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,14 +110,26 @@ 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 = [0, 0],
autodraw,
prebake,
bgFill = true,
...replOptions
} = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.painters = [];
this.drawTime = drawTime;
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,21 +137,29 @@ 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,
onToggle: (started) => {
replOptions?.onToggle?.(started);
if (started) {
this.adjustDrawTime();
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, []);
@ -135,18 +179,17 @@ export class StrudelMirror {
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.adjustDrawTime();
this.drawer.invalidate();
},
});
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 +197,28 @@ export class StrudelMirror {
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.backgroundColor = 'var(--background)';
this.root.style.display = 'block';
if (bgFill) {
this.root.style.backgroundColor = 'var(--background)';
}
cmEditor.style.backgroundColor = 'transparent';
}
const settings = codemirrorSettings.get();
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (e.detail !== this.id) {
this.stop();
}
};
document.addEventListener('start-repl', this.onStartRepl);
}
// adjusts draw time depending on if there are painters
adjustDrawTime() {
// when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
}
async drawFirstFrame() {
if (!this.onDraw) {
@ -166,8 +228,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 +244,7 @@ export class StrudelMirror {
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.scheduler.stop();
this.repl.stop();
} else {
this.evaluate();
}
@ -212,6 +275,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 +297,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 +314,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);
}

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StrudelMirror Example</title>
</head>
<body>
<div class="settings">
<form name="settings">
<label
>theme
<select name="theme">
<option>strudelTheme</option>
<option>bluescreen</option>
<option>blackscreen</option>
<option>whitescreen</option>
<option>teletext</option>
<option>algoboy</option>
<option>terminal</option>
<option>abcdef</option>
<option>androidstudio</option>
<option>atomone</option>
<option>aura</option>
<option>bespin</option>
<option>darcula</option>
<option>dracula</option>
<option>duotoneDark</option>
<option>eclipse</option>
<option>githubDark</option>
<option>gruvboxDark</option>
<option>materialDark</option>
<option>nord</option>
<option>okaidia</option>
<option>solarizedDark</option>
<option>sublime</option>
<option>tokyoNight</option>
<option>tokyoNightStorm</option>
<option>vscodeDark</option>
<option>xcodeDark</option>
<option>bbedit</option>
<option>duotoneLight</option>
<option>githubLight</option>
<option>gruvboxLight</option>
<option>materialLight</option>
<option>noctisLilac</option>
<option>solarizedLight</option>
<option>tokyoNightDay</option>
<option>xcodeLight</option>
</select> </label
><br />
<!-- <label
>keybindings
<select name="keybindings">
<option>codemirror</option>
<option>vim</option>
<option>emacs</option>
<option>vscode</option>
</select> </label
><br />
<label
>fontFamily
<select name="fontFamily">
<option>monospace</option>
<option>helvetica</option>
</select> </label
><br /> -->
<label>fontSize <input type="number" name="fontSize" /></label>
<br />
<label><input type="checkbox" name="isLineNumbersDisplayed" />isLineNumbersDisplayed</label>
<br />
<label><input type="checkbox" name="isActiveLineHighlighted" />isActiveLineHighlighted</label>
<br />
<label><input type="checkbox" name="isPatternHighlightingEnabled" />isPatternHighlightingEnabled</label>
<br />
<label><input type="checkbox" name="isFlashEnabled" />isFlashEnabled</label>
<br />
<label><input type="checkbox" name="isLineWrappingEnabled" />isLineWrappingEnabled</label>
<!-- <label><input type="checkbox" name="isAutoCompletionEnabled" />isAutoCompletionEnabled</label> -->
<!-- <label><input type="checkbox" name="isTooltipEnabled" />isTooltipEnabled</label> -->
</form>
</div>
<div id="code"></div>
<script type="module" src="/main.js"></script>
</body>
</html>

View File

@ -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("<g1(<3 4>,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("<Gm9 Gm11>/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);
});

View File

@ -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:*"
}
}

View File

@ -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;
}

View File

@ -0,0 +1,17 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
if (chunk === undefined) return '';
return chunk;
};
export let h = (strings, ...vars) => {
let string = '';
for (let i in strings) {
string += parseChunk(strings[i]);
string += parseChunk(vars[i]);
}
return html(string);
};

View File

@ -47,7 +47,8 @@
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
"react-dom": "^18.2.0"
"nanostores": "^0.8.1",
"@nanostores/persistent": "^0.8.0"
},
"devDependencies": {
"vite": "^4.3.3"

View File

@ -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');
}

View File

@ -1,32 +1,33 @@
import { createRoot } from 'react-dom/client';
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../../../doc.json';
import { Autocomplete } from './Autocomplete';
import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
let ctrlDown = false;
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
if (typeof window !== 'undefined') {
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
}
export const strudelTooltip = hoverTooltip(
(view, pos, side) => {
@ -65,10 +66,13 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
createRoot(dom).render(<Autocomplete doc={entry} label={word} />);
const ac = Autocomplete({ doc: entry, label: word });
dom.appendChild(ac);
return { dom };
},
};
},
{ hoverTime: 10 },
);
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);

View File

@ -11,13 +11,14 @@ export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_since_last_cps_change = 0;
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
// called slightly before each cycle
@ -25,14 +26,24 @@ export class Cyclist {
if (tick === 0) {
this.origin = phase;
}
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_since_last_cps_change = this.lastEnd;
}
this.num_ticks_since_cps_change++;
try {
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
const end = round(begin + duration * this.cps);
//convert ticks to cycles, so you can query the pattern for events
const eventLength = duration * this.cps;
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left till phase begins
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline;
haps.forEach((hap) => {
@ -59,6 +70,8 @@ export class Cyclist {
this.onToggle?.(v);
}
start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
@ -84,7 +97,11 @@ export class Cyclist {
}
}
setCps(cps = 1) {
if (this.cps === cps) {
return;
}
this.cps = cps;
this.num_ticks_since_cps_change = 0;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());

View File

@ -111,8 +111,6 @@ export class Framer {
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
@ -122,6 +120,8 @@ export class Drawer {
console.warn('Drawer: no scheduler');
return;
}
const lookbehind = Math.abs(this.drawTime[0]);
const lookahead = this.drawTime[1];
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
@ -145,12 +145,16 @@ export class Drawer {
},
);
}
invalidate(scheduler = this.scheduler) {
setDrawTime(drawTime) {
this.drawTime = drawTime;
}
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

View File

@ -1,8 +0,0 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
pnpm i
pnpm dev
```

View File

@ -1 +0,0 @@
!dist

View File

@ -1,8 +0,0 @@
# vite-vanilla-repl
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
```sh
npm i
npm run dev
```

View File

@ -1 +0,0 @@
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
</head>
<body style="margin: 0; background: #222">
<div style="display: grid; height: 100vh">
<textarea
id="text"
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
spellcheck="false"
></textarea>
</div>
<button
id="start"
style="
position: absolute;
border-radius: 10px;
top: 20px;
right: 20px;
padding: 20px;
border: 2px solid white;
background: transparent;
color: white;
cursor: pointer;
"
>
evaluate
</button>
<div id="output"></div>
</body>
</html>

View File

@ -1,12 +0,0 @@
/*
gist.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/gist.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// this is a shortcut to eval code from a gist
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
export default (route, cache = true) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
.then((res) => res.text())
.then((code) => eval(code));

View File

@ -27,7 +27,6 @@ export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
export const version = p.version; */

View File

@ -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));
};
/**

View File

@ -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: false,
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 =

View File

@ -1,23 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1 +0,0 @@
examples

View File

@ -1,55 +0,0 @@
# @strudel.cycles/react
This package contains react hooks and components for strudel. It is used internally by the Strudel REPL.
## Install
```js
npm i @strudel.cycles/react
```
## Usage
Here is a minimal example of how to set up a MiniRepl:
```jsx
import * as React from 'react';
import '@strudel.cycles/react/dist/style.css';
import { MiniRepl } from '@strudel.cycles/react';
import { evalScope, controls } from '@strudel.cycles/core';
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
async function prebake() {
await samples(
'https://strudel.cc/tidal-drum-machines.json',
'github:ritchse/tidal-drum-machines/main/machines/'
);
await samples(
'https://strudel.cc/EmuSP12.json',
'https://strudel.cc/EmuSP12/'
);
}
async function init() {
await evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal')
);
await prebake();
initAudioOnFirstClick();
}
if (typeof window !== 'undefined') {
init();
}
export default function App() {
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
}
```
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example

View File

@ -1,15 +0,0 @@
# nano-repl
this is an example of how to create a repl with strudel and react.
## Usage
after cloning the strudel repo:
```sh
pnpm i
cd packages/react/examples/nano-repl
pnpm dev
```
you should now have a repl running at `http://localhost:5173/`

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel Nano REPL</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,32 +0,0 @@
{
"name": "@strudel.cycles/nano-repl",
"private": true,
"version": "0.6.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}

View File

@ -1,144 +0,0 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import {
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick();
async function init() {
// TODO: only import stuff when play is pressed?
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
stack(
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8") // drums
.speed(perlin.range(.7,.9)) // random sample speed variation
//.hush()
,"<a1 b1*2 a1(3,8) e2>" // bassline
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
.attack(1) // slowly fade in
//.hush()
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.fast(2/3)`;
// await prebake();
const ctx = getAudioContext();
const getTime = () => ctx.currentTime;
function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
const error = evalError || schedulerError;
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await evaluate(code);
if (e.shiftKey) {
panic();
scheduler.stop();
scheduler.start();
}
if (!scheduler.started) {
scheduler.start();
}
} else if (e.code === 'Period') {
scheduler.stop();
panic();
e.preventDefault();
}
}
},
[scheduler, evaluate, view, code],
),
);
return (
<div>
<nav className="z-[12] w-full flex justify-center fixed bottom-0">
<div className="bg-slate-500 space-x-2 px-2 rounded-t-md">
<button
onClick={async () => {
await evaluate(code);
scheduler.start();
}}
>
start
</button>
<button onClick={() => scheduler.stop()}>stop</button>
{isDirty && <button onClick={() => evaluate(code)}>eval</button>}
</div>
{error && <p>error {error.message}</p>}
</nav>
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
</div>
);
}
export default App;

View File

@ -1,9 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -1,19 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
body {
background: #123;
}

View File

@ -1,28 +0,0 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
};

View File

@ -1,7 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Strudel React Components</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,70 +0,0 @@
{
"name": "@strudel.cycles/react",
"version": "0.9.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}

View File

@ -1,12 +0,0 @@
/*
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,22 +0,0 @@
import React from 'react';
import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css';
import { controls, evalScope } from '@strudel.cycles/core';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
);
function App() {
return (
<div>
<MiniRepl tune={`note("c3")`} />
</div>
);
}
export default App;

View File

@ -1,89 +0,0 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../../../doc.json';
const getDocLabel = (doc) => doc.name || doc.longname;
const getDocSynonyms = (doc) => [getDocLabel(doc), ...(doc.synonyms || [])];
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc, label = getDocLabel(doc) }) {
const synonyms = getDocSynonyms(doc).filter((a) => a !== label);
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{label}</h3>{' '}
{!!synonyms.length && (
<span>
Synonyms: <code>{synonyms.join(', ')}</code>
</span>
)}
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
}
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.reduce(
(acc, doc) /*: Completion */ =>
acc.concat(
[getDocLabel(doc), ...(doc.synonyms || [])].map((label) => ({
label,
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} label={label} />);
return node;
},
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
})),
),
[],
);
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};

View File

@ -1,134 +0,0 @@
import { autocompletion } from '@codemirror/autocomplete';
import { Prec } from '@codemirror/state';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { ViewPlugin, EditorView, keymap } from '@codemirror/view';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import _CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo } from 'react';
import strudelTheme from '../themes/strudel-theme';
import { strudelAutocomplete } from './Autocomplete';
import { strudelTooltip } from './Tooltip';
import {
highlightExtension,
flashField,
flash,
highlightMiniLocations,
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
export { flash, highlightMiniLocations, updateMiniLocations };
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
export default function CodeMirror({
value,
onChange,
onViewChanged,
onSelectionChange,
onDocChange,
theme,
keybindings,
isLineNumbersDisplayed,
isActiveLineHighlighted,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
fontSize = 18,
fontFamily = 'monospace',
}) {
const handleOnChange = useCallback(
(value) => {
onChange?.(value);
},
[onChange],
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
},
[onViewChanged],
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.docChanged && onDocChange) {
onDocChange?.(viewUpdate);
}
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
},
[onSelectionChange],
);
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor(view) {}
},
{
provide: (plugin) => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = {
vim,
emacs,
vscode: vscodeExtension,
};
if (bindings[keybindings]) {
_extensions.push(bindings[keybindings]());
}
if (isAutoCompletionEnabled) {
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isTooltipEnabled) {
_extensions.push(strudelTooltip);
}
_extensions.push([keymap.of({})]);
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
return _extensions;
}, [keybindings, isAutoCompletionEnabled, isTooltipEnabled, isLineWrappingEnabled]);
const basicSetup = useMemo(
() => ({
lineNumbers: isLineNumbersDisplayed,
highlightActiveLine: isActiveLineHighlighted,
}),
[isLineNumbersDisplayed, isActiveLineHighlighted],
);
return (
<div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror
value={value}
theme={theme || strudelTheme}
onChange={handleOnChange}
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={basicSetup}
/>
</div>
);
}

View File

@ -1,199 +0,0 @@
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import './style.css';
import { logger } from '@strudel.cycles/core';
import useEvent from '../hooks/useEvent.mjs';
import useKeydown from '../hooks/useKeydown.mjs';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
onTrigger,
drawTime,
punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme,
keybindings,
isLineNumbersDisplayed,
isActiveLineHighlighted,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard],
);
const {
code,
setCode,
evaluate,
activateCode,
error,
isDirty,
activeCode,
pattern,
started,
scheduler,
togglePlay,
stop,
canvasId,
id: replId,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat, id) => {
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime,
evalOnMount,
drawContext,
drawTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
});
const wasVisible = useRef();
const show = useMemo(() => {
if (isVisible || !hideOutsideView) {
wasVisible.current = true;
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
}
}
},
[activateCode, stop, view],
),
);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
const { data } = e.detail;
const logId = data?.hap?.context?.id;
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-8);
});
}
}, []),
);
return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'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()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'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()}
>
<Icon type="refresh" />
</button>
</div>
</div>
)}
<div className="overflow-auto relative">
{show && (
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
isActiveLineHighlighted={isActiveLineHighlighted}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{punchcard && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
{!!log.length && (
<div className="bg-gray-800 rounded-md p-2">
{log.map(({ message }, i) => (
<div key={i}>{message}</div>
))}
</div>
)}
</div>
);
}
// TODO: dedupe
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}

View File

@ -1,34 +0,0 @@
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.cm-editor {
background-color: transparent !important;
height: 100%;
z-index: 11;
}
.cm-theme {
width: 100%;
height: 100%;
}
.cm-theme-light {
width: 100%;
}
footer {
z-index: 0 !important;
}
.strudel-tooltip {
padding: 5px;
}

View File

@ -1,50 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef(0);
const [miniLocations, setMiniLocations] = useState([]);
useEffect(() => {
if (view) {
updateMiniLocations(view, miniLocations);
}
}, [view, miniLocations]);
useEffect(() => {
if (view) {
if (pattern && active) {
lastEnd.current = 0;
let frame = requestAnimationFrame(function updateHighlights() {
try {
const audioTime = getTime();
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [round(begin), round(audioTime + 1 / 60)];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets
highlightMiniLocations(view, begin, highlights.current);
} catch (err) {
highlightMiniLocations(view, 0, []);
}
frame = requestAnimationFrame(updateHighlights);
});
return () => {
cancelAnimationFrame(frame);
};
} else {
highlights.current = [];
highlightMiniLocations(view, 0, highlights.current);
}
}
}, [pattern, active, view]);
return { setMiniLocations };
}
export default useHighlighting;

View File

@ -1,10 +0,0 @@
import { useLayoutEffect } from 'react';
// set active pattern on ctrl+enter
const useKeydown = (callback) =>
useLayoutEffect(() => {
window.addEventListener('keydown', callback, true);
return () => window.removeEventListener('keydown', callback, true);
}, [callback]);
export default useKeydown;

View File

@ -1,48 +0,0 @@
import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2] }) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
useEffect(() => {
if (pattern && started) {
const t = getTime();
const futureHaps = pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query..
visibleHaps.current = visibleHaps.current.filter((h) => h.whole.begin < t);
visibleHaps.current = visibleHaps.current.concat(futureHaps);
}
}, [pattern, started]);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime() + lookahead;
if (lastFrame.current === null) {
lastFrame.current = phase;
return;
}
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern, onDraw]),
);
useEffect(() => {
if (started) {
startFrame();
} else {
visibleHaps.current = [];
stopFrame();
}
}, [started]);
return {
clear: () => {
visibleHaps.current = [];
},
};
}
export default usePatternFrame;

View File

@ -1,17 +0,0 @@
/*
usePostMessage.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useEffect, useCallback } from 'react';
function usePostMessage(listener) {
useEffect(() => {
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}, [listener]);
return useCallback((data) => window.postMessage(data, '*'), []);
}
export default usePostMessage;

View File

@ -1,171 +0,0 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import usePatternFrame from './usePatternFrame';
import usePostMessage from './usePostMessage.mjs';
function useStrudel({
defaultOutput,
interval,
getTime,
evalOnMount = false,
initialCode = '',
beforeEval,
afterEval,
editPattern,
onEvalError,
onToggle,
canvasId,
drawContext,
drawTime = [-2, 2],
paintOptions = {},
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
const [code, setCode] = useState(initialCode);
const [activeCode, setActiveCode] = useState();
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
() =>
repl({
interval,
defaultOutput,
onSchedulerError: setSchedulerError,
onEvalError: (err) => {
setEvalError(err);
onEvalError?.(err);
},
getTime,
drawContext,
transpiler,
editPattern,
beforeEval: async ({ code }) => {
setCode(code);
await beforeEval?.();
},
afterEval: (res) => {
const { pattern: _pattern, code } = res;
setActiveCode(code);
setPattern(_pattern);
setEvalError();
setSchedulerError();
afterEval?.(res);
},
onToggle: (v) => {
setStarted(v);
onToggle?.(v);
},
}),
[defaultOutput, interval, getTime],
);
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
stop();
}
});
const activateCode = useCallback(
async (newCode, autostart = true) => {
if (newCode) {
setCode(code);
}
const res = await evaluate(newCode || code, autostart);
broadcast({ type: 'start', from: id });
return res;
},
[evaluate, code],
);
const onDraw = useCallback(
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime, paintOptions);
},
[drawContext, canvasId, paintOptions],
);
const drawFirstFrame = useCallback(
(pat) => {
if (shouldPaint(pat)) {
const [_, lookahead] = drawTime;
const haps = pat.queryArc(0, lookahead);
// draw at -0.001 to avoid activating haps at 0
onDraw(pat, -0.001, haps, drawTime);
}
},
[drawTime, onDraw, shouldPaint],
);
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
inited.current = true;
evaluate(code, false).then((pat) => drawFirstFrame(pat));
}
}, [evalOnMount, code, evaluate, drawFirstFrame]);
// this will stop the scheduler when hot reloading in development
useEffect(() => {
return () => {
scheduler.stop();
};
}, [scheduler]);
const togglePlay = async () => {
if (started) {
scheduler.stop();
drawFirstFrame(pattern);
} else {
await activateCode();
}
};
const error = schedulerError || evalError;
usePatternFrame({
pattern,
started: shouldPaint(pattern) && started,
getTime: () => scheduler.now(),
drawTime,
onDraw,
});
return {
id,
canvasId,
code,
setCode,
error,
schedulerError,
scheduler,
evalError,
evaluate,
activateCode,
activeCode,
isDirty,
pattern,
started,
start,
stop,
pause,
togglePlay,
setCps,
};
}
export default useStrudel;
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}

View File

@ -1,13 +0,0 @@
import { useEffect, useState } from 'react';
import { updateWidgets } from '@strudel/codemirror';
// i know this is ugly.. in the future, repl needs to run without react
export function useWidgets(view) {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
if (view) {
updateWidgets(view, widgets);
}
}, [view, widgets]);
return { widgets, setWidgets };
}

View File

@ -1,12 +0,0 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
export * from './components/MiniRepl'; // !SSR
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useKeydown } from './hooks/useKeydown';
export { default as useEvent } from './hooks/useEvent';
export { default as strudelTheme } from './themes/strudel-theme';
export { default as teletext } from './themes/teletext';
export { default as cx } from './cx';

View File

@ -1,9 +0,0 @@
import React from 'react';
import App from './App';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -1,41 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#9bbc0f',
foreground: '#0f380f', // whats that?
caret: '#0f380f',
selection: '#306230',
selectionMatch: '#ffffff26',
lineHighlight: '#8bac0f',
lineBackground: '#9bbc0f50',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});

View File

@ -1,38 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});

View File

@ -1,41 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});

View File

@ -1,45 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export default createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
// { tag: t.typeName, color: '#f07178' }, // original
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original
{ tag: t.variableName, color: '#c792ea' },
// { tag: t.tagName, color: '#ff5370' }, // original
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});

View File

@ -1,50 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});

View File

@ -1,36 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});

View File

@ -1,38 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});

View File

@ -1,24 +0,0 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
corePlugins: {
preflight: false,
},
};

View File

@ -1,47 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { peerDependencies, dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic',
}),
],
build: {
lib: {
entry: resolve(__dirname, 'src', 'index.js'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
// for UMD name: 'GlobalName'
},
rollupOptions: {
external: [
...Object.keys(peerDependencies),
...Object.keys(dependencies),
// TODO: find out which of below names are obsolete now
'@strudel.cycles/transpiler',
'acorn',
'@strudel.cycles/core',
'@strudel.cycles/mini',
'@strudel.cycles/tonal',
'@strudel.cycles/midi',
'@strudel.cycles/xen',
'@strudel.cycles/serial',
'@strudel.cycles/webaudio',
'@codemirror/view',
'@codemirror/lang-javascript',
'@codemirror/state',
'@codemirror/commands',
'@lezer/highlight',
'@codemirror/language',
'@uiw/codemirror-themes',
'@uiw/react-codemirror',
'@lezer/highlight',
],
},
target: 'esnext',
},
});

1
packages/repl/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
stats.html

3
packages/repl/README.md Normal file
View File

@ -0,0 +1,3 @@
# @strudel/repl
The Strudel REPL as a web component.

2
packages/repl/index.mjs Normal file
View File

@ -0,0 +1,2 @@
export * from './repl-component.mjs';
export * from './prebake.mjs';

View File

@ -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 <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"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"
}
}

54
packages/repl/prebake.mjs Normal file
View File

@ -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 };
});
};

View File

@ -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('<!--', '').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);
}

View File

@ -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',
},
});

View File

@ -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?

Some files were not shown because too many files have changed in this diff Show More