mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-24 12:08:28 +00:00
Merge branch 'main' into haskell-parser
This commit is contained in:
commit
4d332cd544
1806
dependencies.svg
1806
dependencies.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 150 KiB |
3
examples/README.md
Normal file
3
examples/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# examples
|
||||||
|
|
||||||
|
This folder contains usage examples for different scenarios.
|
||||||
5
examples/buildless/README.md
Normal file
5
examples/buildless/README.md
Normal 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/)
|
||||||
31
examples/buildless/web-component-no-iframe.html
Normal file
31
examples/buildless/web-component-no-iframe.html
Normal 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>
|
||||||
8
examples/codemirror-repl/README.md
Normal file
8
examples/codemirror-repl/README.md
Normal 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
|
||||||
|
```
|
||||||
13
examples/headless-repl/README.md
Normal file
13
examples/headless-repl/README.md
Normal 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
1
examples/minimal-repl/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
10
examples/minimal-repl/README.md
Normal file
10
examples/minimal-repl/README.md
Normal 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)
|
||||||
11
examples/superdough/README.md
Normal file
11
examples/superdough/README.md
Normal 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/
|
||||||
|
```
|
||||||
38
examples/superdough/index.html
Normal file
38
examples/superdough/index.html
Normal 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>
|
||||||
@ -3,13 +3,3 @@
|
|||||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
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.
|
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)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Generated with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
|
||||||
```
|
|
||||||
|
|||||||
78
packages/codemirror/autocomplete.mjs
Normal file
78
packages/codemirror/autocomplete.mjs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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) => {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div.textContent || div.innerText || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
.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
|
||||||
|
.map((doc) /*: Completion */ => ({
|
||||||
|
label: getDocLabel(doc),
|
||||||
|
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||||
|
info: () => Autocomplete({ doc }),
|
||||||
|
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' },
|
||||||
|
], */
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isAutoCompletionEnabled(on) {
|
||||||
|
return on
|
||||||
|
? [
|
||||||
|
autocompletion({ override: [strudelAutocomplete] }),
|
||||||
|
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||||
|
]
|
||||||
|
: []; // autocompletion({ override: [] })
|
||||||
|
}
|
||||||
@ -1,37 +1,104 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
|
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { history } from '@codemirror/commands';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
|
||||||
import { Drawer, repl } from '@strudel.cycles/core';
|
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
|
||||||
import { flashField, flash } from './flash.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { oneDark } from './themes/one-dark';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
|
import { keybindings } from './keybindings.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,
|
||||||
|
isTooltipEnabled,
|
||||||
|
isPatternHighlightingEnabled,
|
||||||
|
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||||
|
isFlashEnabled,
|
||||||
|
keybindings,
|
||||||
|
};
|
||||||
|
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/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, 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({
|
let state = EditorState.create({
|
||||||
doc: initialCode,
|
doc: initialCode,
|
||||||
extensions: [
|
extensions: [
|
||||||
theme,
|
/* search(),
|
||||||
|
highlightSelectionMatches(), */
|
||||||
|
...initialSettings,
|
||||||
javascript(),
|
javascript(),
|
||||||
lineNumbers(),
|
sliderPlugin,
|
||||||
highlightExtension,
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
highlightActiveLineGutter(),
|
// bracketMatching(), // does not do anything
|
||||||
|
closeBrackets(),
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
keymap.of(defaultKeymap),
|
history(),
|
||||||
flashField,
|
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
keymap.of([
|
Prec.highest(
|
||||||
{
|
keymap.of([
|
||||||
key: 'Ctrl-Enter',
|
{
|
||||||
run: () => onEvaluate(),
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => onEvaluate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-Enter',
|
||||||
|
run: () => onEvaluate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-.',
|
||||||
|
run: () => onStop?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-.',
|
||||||
|
run: (_, e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onStop?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
key: 'Ctrl-Shift-.',
|
||||||
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Ctrl-.',
|
key: 'Ctrl-Shift-Enter',
|
||||||
run: () => onStop(),
|
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
||||||
},
|
}, */
|
||||||
]),
|
]),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,45 +110,76 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
|||||||
|
|
||||||
export class StrudelMirror {
|
export class StrudelMirror {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
const {
|
||||||
|
root,
|
||||||
|
id,
|
||||||
|
initialCode = '',
|
||||||
|
onDraw,
|
||||||
|
drawTime = [0, 0],
|
||||||
|
autodraw,
|
||||||
|
prebake,
|
||||||
|
bgFill = true,
|
||||||
|
...replOptions
|
||||||
|
} = options;
|
||||||
this.code = initialCode;
|
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) => {
|
this.drawer = new Drawer((haps, time) => {
|
||||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||||
this.highlight(currentFrame, time);
|
this.highlight(currentFrame, time);
|
||||||
onDraw?.(haps, time, currentFrame);
|
this.onDraw?.(haps, time, currentFrame, this.painters);
|
||||||
}, drawTime);
|
}, drawTime);
|
||||||
|
|
||||||
const prebaked = prebake();
|
// this approach does not work with multiple repls on screen
|
||||||
prebaked.then(async () => {
|
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
|
||||||
if (!onDraw) {
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
return;
|
self.painters.push(onPaint);
|
||||||
}
|
return this;
|
||||||
const { scheduler, evaluate } = await this.repl;
|
};
|
||||||
// draw first frame instantly
|
|
||||||
prebaked.then(async () => {
|
this.prebaked = prebake();
|
||||||
await evaluate(this.code, false);
|
autodraw && this.drawFirstFrame();
|
||||||
this.drawer.invalidate(scheduler);
|
|
||||||
onDraw?.(this.drawer.visibleHaps, 0, []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.repl = repl({
|
this.repl = repl({
|
||||||
...replOptions,
|
...replOptions,
|
||||||
onToggle: async (started) => {
|
onToggle: (started) => {
|
||||||
replOptions?.onToggle?.(started);
|
replOptions?.onToggle?.(started);
|
||||||
const { scheduler } = await this.repl;
|
|
||||||
if (started) {
|
if (started) {
|
||||||
this.drawer.start(scheduler);
|
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 {
|
} else {
|
||||||
this.drawer.stop();
|
this.drawer.stop();
|
||||||
|
updateMiniLocations(this.editor, []);
|
||||||
|
cleanupDraw(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeEval: async () => {
|
beforeEval: async () => {
|
||||||
await prebaked;
|
cleanupDraw();
|
||||||
|
this.painters = [];
|
||||||
|
await this.prebaked;
|
||||||
|
await replOptions?.beforeEval?.();
|
||||||
},
|
},
|
||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
|
// remember for when highlighting is toggled on
|
||||||
|
this.miniLocations = options.meta?.miniLocations;
|
||||||
|
this.widgets = options.meta?.widgets;
|
||||||
|
updateWidgets(this.editor, this.widgets);
|
||||||
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
|
this.adjustDrawTime();
|
||||||
this.drawer.invalidate();
|
this.drawer.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -89,25 +187,145 @@ export class StrudelMirror {
|
|||||||
root,
|
root,
|
||||||
initialCode,
|
initialCode,
|
||||||
onChange: (v) => {
|
onChange: (v) => {
|
||||||
this.code = v.state.doc.toString();
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
this.repl.setCode?.(this.code);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onEvaluate: () => this.evaluate(),
|
onEvaluate: () => this.evaluate(),
|
||||||
onStop: () => this.stop(),
|
onStop: () => this.stop(),
|
||||||
});
|
});
|
||||||
|
const cmEditor = this.root.querySelector('.cm-editor');
|
||||||
|
if (cmEditor) {
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// draw first frame instantly
|
||||||
|
await this.prebaked;
|
||||||
|
try {
|
||||||
|
await this.repl.evaluate(this.code, false);
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async evaluate() {
|
async evaluate() {
|
||||||
const { evaluate } = await this.repl;
|
|
||||||
this.flash();
|
this.flash();
|
||||||
await evaluate(this.code);
|
await this.repl.evaluate(this.code);
|
||||||
}
|
}
|
||||||
async stop() {
|
async stop() {
|
||||||
const { scheduler } = await this.repl;
|
this.repl.scheduler.stop();
|
||||||
scheduler.stop();
|
}
|
||||||
|
async toggle() {
|
||||||
|
if (this.repl.scheduler.started) {
|
||||||
|
this.repl.stop();
|
||||||
|
} else {
|
||||||
|
this.evaluate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flash(ms) {
|
flash(ms) {
|
||||||
flash(this.editor, ms);
|
flash(this.editor, ms);
|
||||||
}
|
}
|
||||||
highlight(haps, time) {
|
highlight(haps, time) {
|
||||||
highlightMiniLocations(this.editor.view, time, haps);
|
highlightMiniLocations(this.editor, time, haps);
|
||||||
|
}
|
||||||
|
setFontSize(size) {
|
||||||
|
this.root.style.fontSize = size + 'px';
|
||||||
|
}
|
||||||
|
setFontFamily(family) {
|
||||||
|
this.root.style.fontFamily = family;
|
||||||
|
const scroller = this.root.querySelector('.cm-scroller');
|
||||||
|
if (scroller) {
|
||||||
|
scroller.style.fontFamily = family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reconfigureExtension(key, value) {
|
||||||
|
if (!extensions[key]) {
|
||||||
|
console.warn(`extension ${key} is not known`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value = parseBooleans(value);
|
||||||
|
const newValue = extensions[key](value, this);
|
||||||
|
this.editor.dispatch({
|
||||||
|
effects: compartments[key].reconfigure(newValue),
|
||||||
|
});
|
||||||
|
if (key === 'theme') {
|
||||||
|
activateTheme(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLineWrappingEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||||
|
}
|
||||||
|
setLineNumbersDisplayed(enabled) {
|
||||||
|
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
||||||
|
}
|
||||||
|
setTheme(theme) {
|
||||||
|
this.reconfigureExtension('theme', theme);
|
||||||
|
}
|
||||||
|
setAutocompletionEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
|
||||||
|
}
|
||||||
|
updateSettings(settings) {
|
||||||
|
this.setFontSize(settings.fontSize);
|
||||||
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
for (let key in extensions) {
|
||||||
|
this.reconfigureExtension(key, settings[key]);
|
||||||
|
}
|
||||||
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
|
codemirrorSettings.set(updated);
|
||||||
|
}
|
||||||
|
changeSetting(key, value) {
|
||||||
|
if (extensions[key]) {
|
||||||
|
this.reconfigureExtension(key, value);
|
||||||
|
return;
|
||||||
|
} else if (key === 'fontFamily') {
|
||||||
|
this.setFontFamily(value);
|
||||||
|
} else if (key === 'fontSize') {
|
||||||
|
this.setFontSize(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCode(code) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -33,3 +33,5 @@ export const flash = (view, ms = 200) => {
|
|||||||
view.dispatch({ effects: setFlash.of(false) });
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
}, ms);
|
}, ms);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFlashEnabled = (on) => (on ? flashField : []);
|
||||||
|
|||||||
@ -124,3 +124,12 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||||
|
|
||||||
|
export const isPatternHighlightingEnabled = (on, config) => {
|
||||||
|
on &&
|
||||||
|
config &&
|
||||||
|
setTimeout(() => {
|
||||||
|
updateMiniLocations(config.editor, config.miniLocations);
|
||||||
|
}, 100);
|
||||||
|
return on ? highlightExtension : [];
|
||||||
|
};
|
||||||
|
|||||||
17
packages/codemirror/html.mjs
Normal file
17
packages/codemirror/html.mjs
Normal 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);
|
||||||
|
};
|
||||||
@ -2,3 +2,4 @@ export * from './codemirror.mjs';
|
|||||||
export * from './highlight.mjs';
|
export * from './highlight.mjs';
|
||||||
export * from './flash.mjs';
|
export * from './flash.mjs';
|
||||||
export * from './slider.mjs';
|
export * from './slider.mjs';
|
||||||
|
export * from './themes.mjs';
|
||||||
|
|||||||
31
packages/codemirror/keybindings.mjs
Normal file
31
packages/codemirror/keybindings.mjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Prec } from '@codemirror/state';
|
||||||
|
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||||
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
|
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor() {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: () => {
|
||||||
|
return Prec.highest(keymap.of([...vscodeKeymap]));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
|
const keymaps = {
|
||||||
|
vim,
|
||||||
|
emacs,
|
||||||
|
vscode: vscodeExtension,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function keybindings(name) {
|
||||||
|
const active = keymaps[name];
|
||||||
|
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||||
|
// keymap.of(searchKeymap),
|
||||||
|
}
|
||||||
@ -33,13 +33,22 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.6.0",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.2.4",
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
"@codemirror/lang-javascript": "^6.1.7",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
"@codemirror/state": "^6.2.0",
|
"@codemirror/state": "^6.2.0",
|
||||||
"@codemirror/view": "^6.10.0",
|
"@codemirror/view": "^6.10.0",
|
||||||
"@lezer/highlight": "^1.1.4",
|
"@lezer/highlight": "^1.1.4",
|
||||||
"@strudel.cycles/core": "workspace:*"
|
"@replit/codemirror-emacs": "^6.0.1",
|
||||||
|
"@replit/codemirror-vim": "^6.0.14",
|
||||||
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@uiw/codemirror-themes": "^4.19.16",
|
||||||
|
"@uiw/codemirror-themes-all": "^4.19.16",
|
||||||
|
"nanostores": "^0.8.1",
|
||||||
|
"@nanostores/persistent": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^4.3.3"
|
||||||
|
|||||||
@ -30,13 +30,13 @@ import {
|
|||||||
xcodeLight,
|
xcodeLight,
|
||||||
} from '@uiw/codemirror-themes-all';
|
} from '@uiw/codemirror-themes-all';
|
||||||
|
|
||||||
import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
|
import strudelTheme from './themes/strudel-theme';
|
||||||
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
|
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
|
||||||
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
|
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
|
||||||
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
|
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
|
||||||
import teletext, { settings as teletextSettings } from '@strudel.cycles/react/src/themes/teletext';
|
import teletext, { settings as teletextSettings } from './themes/teletext';
|
||||||
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
|
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
||||||
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal';
|
import terminal, { settings as terminalSettings } from './themes/terminal';
|
||||||
|
|
||||||
export const themes = {
|
export const themes = {
|
||||||
strudelTheme,
|
strudelTheme,
|
||||||
@ -473,6 +473,9 @@ function stringifySafe(json) {
|
|||||||
return JSON.stringify(json, getCircularReplacer());
|
return JSON.stringify(json, getCircularReplacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const theme = (theme) => themes[theme] || themes.strudelTheme;
|
||||||
|
|
||||||
|
// css style injection helpers
|
||||||
export function injectStyle(rule) {
|
export function injectStyle(rule) {
|
||||||
const newStyle = document.createElement('style');
|
const newStyle = document.createElement('style');
|
||||||
document.head.appendChild(newStyle);
|
document.head.appendChild(newStyle);
|
||||||
@ -480,3 +483,45 @@ export function injectStyle(rule) {
|
|||||||
const ruleIndex = styleSheet.insertRule(rule, 0);
|
const ruleIndex = styleSheet.insertRule(rule, 0);
|
||||||
return () => styleSheet.deleteRule(ruleIndex);
|
return () => styleSheet.deleteRule(ruleIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentTheme,
|
||||||
|
resetThemeStyle,
|
||||||
|
themeStyle,
|
||||||
|
styleID = 'strudel-theme-vars';
|
||||||
|
export function initTheme(theme) {
|
||||||
|
if (!document.getElementById(styleID)) {
|
||||||
|
themeStyle = document.createElement('style');
|
||||||
|
themeStyle.id = styleID;
|
||||||
|
document.head.append(themeStyle);
|
||||||
|
}
|
||||||
|
activateTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateTheme(name) {
|
||||||
|
if (currentTheme === name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentTheme = name;
|
||||||
|
if (!settings[name]) {
|
||||||
|
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||||
|
}
|
||||||
|
const themeSettings = settings[name] || settings.strudelTheme;
|
||||||
|
// set css variables
|
||||||
|
themeStyle.innerHTML = `:root {
|
||||||
|
${Object.entries(themeSettings)
|
||||||
|
// important to override fallback
|
||||||
|
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||||
|
.join('\n')}
|
||||||
|
}`;
|
||||||
|
// tailwind dark mode
|
||||||
|
if (themeSettings.light) {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
resetThemeStyle?.();
|
||||||
|
resetThemeStyle = undefined;
|
||||||
|
if (themeSettings.customStyle) {
|
||||||
|
resetThemeStyle = injectStyle(themeSettings.customStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
packages/codemirror/themes/one-dark.mjs
vendored
139
packages/codemirror/themes/one-dark.mjs
vendored
@ -1,139 +0,0 @@
|
|||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
|
|
||||||
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
|
||||||
|
|
||||||
const chalky = '#e5c07b',
|
|
||||||
coral = '#e06c75',
|
|
||||||
cyan = '#56b6c2',
|
|
||||||
invalid = '#ffffff',
|
|
||||||
ivory = '#abb2bf',
|
|
||||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
|
||||||
malibu = '#61afef',
|
|
||||||
sage = '#98c379',
|
|
||||||
whiskey = '#d19a66',
|
|
||||||
violet = '#c678dd',
|
|
||||||
darkBackground = '#21252b',
|
|
||||||
highlightBackground = '#2c313a',
|
|
||||||
background = '#282c34',
|
|
||||||
tooltipBackground = '#353a42',
|
|
||||||
selection = '#3E4451',
|
|
||||||
cursor = '#528bff';
|
|
||||||
|
|
||||||
/// The colors used in the theme, as CSS color strings.
|
|
||||||
export const color = {
|
|
||||||
chalky,
|
|
||||||
coral,
|
|
||||||
cyan,
|
|
||||||
invalid,
|
|
||||||
ivory,
|
|
||||||
stone,
|
|
||||||
malibu,
|
|
||||||
sage,
|
|
||||||
whiskey,
|
|
||||||
violet,
|
|
||||||
darkBackground,
|
|
||||||
highlightBackground,
|
|
||||||
background,
|
|
||||||
tooltipBackground,
|
|
||||||
selection,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The editor theme styles for One Dark.
|
|
||||||
export const oneDarkTheme = EditorView.theme(
|
|
||||||
{
|
|
||||||
'&': {
|
|
||||||
color: ivory,
|
|
||||||
backgroundColor: background,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-content': {
|
|
||||||
caretColor: cursor,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
|
||||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
|
||||||
{ backgroundColor: selection },
|
|
||||||
|
|
||||||
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
|
||||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
|
||||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
|
||||||
|
|
||||||
'.cm-searchMatch': {
|
|
||||||
backgroundColor: '#72a1ff59',
|
|
||||||
outline: '1px solid #457dff',
|
|
||||||
},
|
|
||||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
|
||||||
backgroundColor: '#6199ff2f',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
|
||||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
|
||||||
|
|
||||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
|
||||||
backgroundColor: '#bad0f847',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-gutters': {
|
|
||||||
backgroundColor: background,
|
|
||||||
color: stone,
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLineGutter': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-foldPlaceholder': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
color: '#ddd',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-tooltip': {
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
|
||||||
borderTopColor: 'transparent',
|
|
||||||
borderBottomColor: 'transparent',
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
|
||||||
borderTopColor: tooltipBackground,
|
|
||||||
borderBottomColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip-autocomplete': {
|
|
||||||
'& > ul > li[aria-selected]': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
color: ivory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ dark: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The highlighting style for code in the One Dark theme.
|
|
||||||
export const oneDarkHighlightStyle = HighlightStyle.define([
|
|
||||||
{ tag: t.keyword, color: violet },
|
|
||||||
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
|
|
||||||
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
|
||||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
|
||||||
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
|
||||||
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
|
|
||||||
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
|
|
||||||
{ tag: [t.meta, t.comment], color: stone },
|
|
||||||
{ tag: t.strong, fontWeight: 'bold' },
|
|
||||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
||||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
|
||||||
{ tag: t.link, color: stone, textDecoration: 'underline' },
|
|
||||||
{ tag: t.heading, fontWeight: 'bold', color: coral },
|
|
||||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
|
||||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
|
||||||
{ tag: t.invalid, color: invalid },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// Extension to enable the One Dark theme (both the editor theme and
|
|
||||||
/// the highlight style).
|
|
||||||
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
|
|
||||||
@ -1,32 +1,33 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import jsdoc from '../../../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { Autocomplete } from './Autocomplete';
|
import { Autocomplete } from './autocomplete.mjs';
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
|
||||||
let ctrlDown = false;
|
let ctrlDown = false;
|
||||||
|
|
||||||
// Record Control key event to trigger or block the tooltip depending on the state
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener(
|
// Record Control key event to trigger or block the tooltip depending on the state
|
||||||
'keyup',
|
window.addEventListener(
|
||||||
function (e) {
|
'keyup',
|
||||||
if (e.key == 'Control') {
|
function (e) {
|
||||||
ctrlDown = false;
|
if (e.key == 'Control') {
|
||||||
}
|
ctrlDown = false;
|
||||||
},
|
}
|
||||||
true,
|
},
|
||||||
);
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
'keydown',
|
'keydown',
|
||||||
function (e) {
|
function (e) {
|
||||||
if (e.key == 'Control') {
|
if (e.key == 'Control') {
|
||||||
ctrlDown = true;
|
ctrlDown = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const strudelTooltip = hoverTooltip(
|
export const strudelTooltip = hoverTooltip(
|
||||||
(view, pos, side) => {
|
(view, pos, side) => {
|
||||||
@ -65,10 +66,13 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
create(view) {
|
create(view) {
|
||||||
let dom = document.createElement('div');
|
let dom = document.createElement('div');
|
||||||
dom.className = 'strudel-tooltip';
|
dom.className = 'strudel-tooltip';
|
||||||
createRoot(dom).render(<Autocomplete doc={entry} label={word} />);
|
const ac = Autocomplete({ doc: entry, label: word });
|
||||||
|
dom.appendChild(ac);
|
||||||
return { dom };
|
return { dom };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ hoverTime: 10 },
|
{ hoverTime: 10 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
|
||||||
@ -11,13 +11,14 @@ export class Cyclist {
|
|||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 1;
|
this.cps = 1;
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.lastBegin = 0; // query begin of last tick
|
this.lastBegin = 0; // query begin of last tick
|
||||||
this.lastEnd = 0; // query end of last tick
|
this.lastEnd = 0; // query end of last tick
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
|
this.num_cycles_since_last_cps_change = 0;
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency; // fixed trigger time offset
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
// called slightly before each cycle
|
||||||
@ -25,14 +26,24 @@ export class Cyclist {
|
|||||||
if (tick === 0) {
|
if (tick === 0) {
|
||||||
this.origin = phase;
|
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 {
|
try {
|
||||||
const time = getTime();
|
const time = getTime();
|
||||||
const begin = this.lastEnd;
|
const begin = this.lastEnd;
|
||||||
this.lastBegin = begin;
|
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;
|
this.lastEnd = end;
|
||||||
|
|
||||||
|
// query the pattern for events
|
||||||
const haps = this.pattern.queryArc(begin, end);
|
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;
|
this.lastTick = time + tickdeadline;
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
@ -59,6 +70,8 @@ export class Cyclist {
|
|||||||
this.onToggle?.(v);
|
this.onToggle?.(v);
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
this.num_cycles_since_last_cps_change = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
@ -84,7 +97,11 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCps(cps = 1) {
|
setCps(cps = 1) {
|
||||||
|
if (this.cps === cps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
}
|
}
|
||||||
log(begin, end, haps) {
|
log(begin, end, haps) {
|
||||||
const onsets = haps.filter((h) => h.hasOnset());
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
|||||||
@ -111,8 +111,6 @@ export class Framer {
|
|||||||
// see vite-vanilla-repl-cm6 for an example
|
// see vite-vanilla-repl-cm6 for an example
|
||||||
export class Drawer {
|
export class Drawer {
|
||||||
constructor(onDraw, drawTime) {
|
constructor(onDraw, drawTime) {
|
||||||
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
|
||||||
lookbehind = Math.abs(lookbehind);
|
|
||||||
this.visibleHaps = [];
|
this.visibleHaps = [];
|
||||||
this.lastFrame = null;
|
this.lastFrame = null;
|
||||||
this.drawTime = drawTime;
|
this.drawTime = drawTime;
|
||||||
@ -122,6 +120,8 @@ export class Drawer {
|
|||||||
console.warn('Drawer: no scheduler');
|
console.warn('Drawer: no scheduler');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const lookbehind = Math.abs(this.drawTime[0]);
|
||||||
|
const lookahead = this.drawTime[1];
|
||||||
// calculate current frame time (think right side of screen for pianoroll)
|
// calculate current frame time (think right side of screen for pianoroll)
|
||||||
const phase = this.scheduler.now() + lookahead;
|
const phase = this.scheduler.now() + lookahead;
|
||||||
// first frame just captures the phase
|
// 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) {
|
if (!scheduler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug...
|
||||||
|
t = t ?? scheduler.now();
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
const t = scheduler.now();
|
|
||||||
let [_, lookahead] = this.drawTime;
|
let [_, lookahead] = this.drawTime;
|
||||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||||
// remove all future haps
|
// remove all future haps
|
||||||
|
|||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -1 +0,0 @@
|
|||||||
!dist
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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
@ -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};
|
|
||||||
@ -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>
|
|
||||||
@ -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));
|
|
||||||
@ -27,7 +27,6 @@ export * from './pianoroll.mjs';
|
|||||||
export * from './spiral.mjs';
|
export * from './spiral.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.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)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
|
|||||||
@ -256,10 +256,13 @@ export function getDrawOptions(drawTime, options = {}) {
|
|||||||
return { fold: 1, ...options, cycles, playhead };
|
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) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
|
return this.onPaint(getPunchcardPainter(options));
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -16,13 +16,36 @@ export function repl({
|
|||||||
transpiler,
|
transpiler,
|
||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
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({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
onError: onSchedulerError,
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle,
|
onToggle: (started) => {
|
||||||
|
updateState({ started });
|
||||||
|
onToggle?.(started);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let allTransform;
|
let allTransform;
|
||||||
@ -43,6 +66,7 @@ export function repl({
|
|||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
updateState({ code, pending: true });
|
||||||
await beforeEval?.({ code });
|
await beforeEval?.({ code });
|
||||||
shouldHush && hush();
|
shouldHush && hush();
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||||
@ -58,17 +82,28 @@ export function repl({
|
|||||||
}
|
}
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
setPattern(pattern, autostart);
|
setPattern(pattern, autostart);
|
||||||
|
updateState({
|
||||||
|
miniLocations: meta?.miniLocations || [],
|
||||||
|
widgets: meta?.widgets || [],
|
||||||
|
activeCode: code,
|
||||||
|
pattern,
|
||||||
|
evalError: undefined,
|
||||||
|
schedulerError: undefined,
|
||||||
|
pending: false,
|
||||||
|
});
|
||||||
afterEval?.({ code, pattern, meta });
|
afterEval?.({ code, pattern, meta });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.warn(`[repl] eval error: ${err.message}`);
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
updateState({ evalError: err, pending: false });
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const stop = () => scheduler.stop();
|
const stop = () => scheduler.stop();
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
|
const toggle = () => scheduler.toggle();
|
||||||
const setCps = (cps) => scheduler.setCps(cps);
|
const setCps = (cps) => scheduler.setCps(cps);
|
||||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||||
|
|
||||||
@ -127,8 +162,8 @@ export function repl({
|
|||||||
setCpm,
|
setCpm,
|
||||||
setcpm: setCpm,
|
setcpm: setCpm,
|
||||||
});
|
});
|
||||||
|
const setCode = (code) => updateState({ code });
|
||||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
export const getTrigger =
|
||||||
|
|||||||
@ -274,3 +274,31 @@ export const sol2note = (n, notation = 'letters') => {
|
|||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
return note + oct;
|
return note + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// code hashing helpers
|
||||||
|
|
||||||
|
export function unicodeToBase64(text) {
|
||||||
|
const utf8Bytes = new TextEncoder().encode(text);
|
||||||
|
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
||||||
|
return base64String;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToUnicode(base64String) {
|
||||||
|
const utf8Bytes = new Uint8Array(
|
||||||
|
atob(base64String)
|
||||||
|
.split('')
|
||||||
|
.map((char) => char.charCodeAt(0)),
|
||||||
|
);
|
||||||
|
const decodedText = new TextDecoder().decode(utf8Bytes);
|
||||||
|
return decodedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function code2hash(code) {
|
||||||
|
return encodeURIComponent(unicodeToBase64(code));
|
||||||
|
//return '#' + encodeURIComponent(btoa(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hash2code(hash) {
|
||||||
|
return base64ToUnicode(decodeURIComponent(hash));
|
||||||
|
//return atob(decodeURIComponent(codeParam || ''));
|
||||||
|
}
|
||||||
|
|||||||
23
packages/react/.gitignore
vendored
23
packages/react/.gitignore
vendored
@ -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?
|
|
||||||
@ -1 +0,0 @@
|
|||||||
examples
|
|
||||||
@ -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
|
|
||||||
@ -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/`
|
|
||||||
@ -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>
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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>,
|
|
||||||
);
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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: [],
|
|
||||||
};
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
});
|
|
||||||
@ -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>
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
@ -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' },
|
|
||||||
], */
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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 };
|
|
||||||
}
|
|
||||||
@ -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';
|
|
||||||
@ -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>,
|
|
||||||
);
|
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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
1
packages/repl/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
stats.html
|
||||||
3
packages/repl/README.md
Normal file
3
packages/repl/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# @strudel/repl
|
||||||
|
|
||||||
|
The Strudel REPL as a web component.
|
||||||
2
packages/repl/index.mjs
Normal file
2
packages/repl/index.mjs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './repl-component.mjs';
|
||||||
|
export * from './prebake.mjs';
|
||||||
51
packages/repl/package.json
Normal file
51
packages/repl/package.json
Normal 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
54
packages/repl/prebake.mjs
Normal 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 };
|
||||||
|
});
|
||||||
|
};
|
||||||
69
packages/repl/repl-component.mjs
Normal file
69
packages/repl/repl-component.mjs
Normal 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);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user