mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-06 09:06:16 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85ee97c9ef | |||
| 479041ebb6 |
@@ -23,4 +23,3 @@ reverbGen.mjs
|
||||
hydra.mjs
|
||||
jsdoc-synonyms.js
|
||||
packages/hs2js/src/hs2js.mjs
|
||||
samples
|
||||
@@ -14,16 +14,10 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
1. Install [Node.js](https://nodejs.org/)
|
||||
2. Install [pnpm](https://pnpm.io/installation)
|
||||
3. Install dependencies by running the following command:
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
4. Run the development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
```bash
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
|
||||
+7
-7
@@ -56,18 +56,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@vitest/ui": "^2.1.3",
|
||||
"acorn": "^8.13.0",
|
||||
"@vitest/ui": "^1.1.0",
|
||||
"acorn": "^8.11.3",
|
||||
"dependency-tree": "^10.0.9",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^4.0.3",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^8.1.8",
|
||||
"prettier": "^3.3.3",
|
||||
"lerna": "^8.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
lineNumbers,
|
||||
drawSelection,
|
||||
} from '@codemirror/view';
|
||||
import { repl, registerControl } from '@strudel/core';
|
||||
import { Pattern, repl } from '@strudel/core';
|
||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
@@ -24,7 +24,6 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { prettierPlugin } from './prettier.mjs';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
@@ -78,7 +77,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
javascript(),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
prettierPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
@@ -101,8 +99,10 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
},
|
||||
{
|
||||
key: 'Alt-.',
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
run: (_, e) => {
|
||||
e.preventDefault();
|
||||
onStop?.();
|
||||
},
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
@@ -135,7 +135,6 @@ export class StrudelMirror {
|
||||
autodraw,
|
||||
prebake,
|
||||
bgFill = true,
|
||||
solo = true,
|
||||
...replOptions
|
||||
} = options;
|
||||
this.code = initialCode;
|
||||
@@ -146,7 +145,6 @@ export class StrudelMirror {
|
||||
this.drawContext = drawContext;
|
||||
this.onDraw = onDraw || this.draw;
|
||||
this.id = id || s4();
|
||||
this.solo = solo;
|
||||
|
||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||
@@ -163,14 +161,12 @@ export class StrudelMirror {
|
||||
replOptions?.onToggle?.(started);
|
||||
if (started) {
|
||||
this.drawer.start(this.repl.scheduler);
|
||||
if (this.solo) {
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
updateMiniLocations(this.editor, []);
|
||||
@@ -225,7 +221,7 @@ export class StrudelMirror {
|
||||
|
||||
// stop this repl when another repl is started
|
||||
this.onStartRepl = (e) => {
|
||||
if (this.solo && e.detail !== this.id) {
|
||||
if (e.detail !== this.id) {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
@@ -360,12 +356,3 @@ function s4() {
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the css of highlighted events. Make sure to use single quotes!
|
||||
* @name markcss
|
||||
* @example
|
||||
* note("c a f e")
|
||||
* .markcss('text-decoration:underline')
|
||||
*/
|
||||
export const markcss = registerControl('markcss');
|
||||
|
||||
@@ -93,7 +93,6 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.value?.color ?? 'var(--foreground)';
|
||||
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
@@ -115,7 +114,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style },
|
||||
attributes: { style: `outline: solid 2px ${color}` },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.21.21",
|
||||
"@uiw/codemirror-themes-all": "^4.21.21",
|
||||
"nanostores": "^0.9.5",
|
||||
"prettier": "^3.3.3"
|
||||
"nanostores": "^0.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import typescriptPlugin from 'prettier/plugins/typescript';
|
||||
import estreePlugin from 'prettier/plugins/estree';
|
||||
import { keymap } from '@codemirror/view';
|
||||
|
||||
export async function runPrettier(editorView) {
|
||||
const currentState = editorView.state.doc.toString();
|
||||
// Prettier insists on consistent quotes, but in Strudel double quotes are interpreted
|
||||
// as patterns and single quotes are for everything else, so a consistent setting won't work.
|
||||
// It's a great formatter though, so as a workaround it works to put "// prettier-ignore" comments
|
||||
// before the single quoted stuff to preserve it, but this is a pain to make users to, so an extra
|
||||
// hack is to take the preformatted code, insert these comments behind the scenes, format, then
|
||||
// remove them before setting the editor state.
|
||||
const preFormat = currentState
|
||||
.split('\n')
|
||||
.map((line) => (line.match(/'.*'/) != null ? '// prettier-ignore\n' + line : line))
|
||||
.join('\n');
|
||||
console.log(preFormat);
|
||||
const formattedState = (
|
||||
await prettier.format(preFormat, {
|
||||
parser: 'typescript',
|
||||
plugins: [typescriptPlugin, estreePlugin],
|
||||
semi: false,
|
||||
})
|
||||
).replace(/.*\/\/ prettier-ignore.*\n/g, '');
|
||||
|
||||
editorView.dispatch({
|
||||
changes: { from: 0, to: editorView.state.doc.length, insert: formattedState },
|
||||
selection: EditorSelection.single(
|
||||
// keep cursor close to the original position, but also keep it within the bounds
|
||||
// of the formatted document
|
||||
Math.min(editorView.state.selection.main.to, formattedState.length),
|
||||
),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
}
|
||||
|
||||
export const prettierPlugin = keymap.of([
|
||||
{
|
||||
key: 'Alt-,',
|
||||
preventDefault: true,
|
||||
run: runPrettier,
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-,',
|
||||
preventDefault: true,
|
||||
run: runPrettier,
|
||||
},
|
||||
{
|
||||
key: 'Alt-Shift-f',
|
||||
preventDefault: true,
|
||||
run: runPrettier,
|
||||
},
|
||||
]);
|
||||
+20
-20
@@ -18,25 +18,25 @@ export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
{ tag: t.labelName, color: '#0f380f' },
|
||||
{ tag: t.keyword, color: '#0f380f' },
|
||||
{ tag: t.operator, color: '#0f380f' },
|
||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.typeName, color: '#0f380f' },
|
||||
{ tag: t.atom, color: '#0f380f' },
|
||||
{ tag: t.number, color: '#0f380f' },
|
||||
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
||||
{ tag: t.string, color: '#0f380f' },
|
||||
{ tag: t.special(t.string), color: '#0f380f' },
|
||||
{ tag: t.comment, color: '#0f380f' },
|
||||
{ tag: t.variableName, color: '#0f380f' },
|
||||
{ tag: t.tagName, color: '#0f380f' },
|
||||
{ tag: t.bracket, color: '#0f380f' },
|
||||
{ tag: t.meta, color: '#0f380f' },
|
||||
{ tag: t.attributeName, color: '#0f380f' },
|
||||
{ tag: t.propertyName, color: '#0f380f' },
|
||||
{ tag: t.className, color: '#0f380f' },
|
||||
{ tag: t.invalid, color: '#0f380f' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
||||
],
|
||||
});
|
||||
|
||||
+20
-20
@@ -15,25 +15,25 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
{ tag: t.labelName, color: 'white' },
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
|
||||
+20
-20
@@ -18,25 +18,25 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'inherit' },
|
||||
{ tag: t.labelName, color: 'white' },
|
||||
{ tag: t.keyword, color: 'white' },
|
||||
{ tag: t.operator, color: 'white' },
|
||||
{ tag: t.special(t.variableName), color: 'white' },
|
||||
{ tag: t.typeName, color: 'white' },
|
||||
{ tag: t.atom, color: 'white' },
|
||||
{ tag: t.number, color: 'white' },
|
||||
{ tag: t.definition(t.variableName), color: 'white' },
|
||||
{ tag: t.string, color: 'white' },
|
||||
{ tag: t.special(t.string), color: 'white' },
|
||||
{ tag: t.comment, color: 'white' },
|
||||
{ tag: t.variableName, color: 'white' },
|
||||
{ tag: t.tagName, color: 'white' },
|
||||
{ tag: t.bracket, color: 'white' },
|
||||
{ tag: t.meta, color: 'white' },
|
||||
{ tag: t.attributeName, color: 'white' },
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
|
||||
+19
-19
@@ -14,24 +14,24 @@ export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: t.labelName, color: '#41FF00' },
|
||||
{ tag: t.keyword, color: '#41FF00' },
|
||||
{ tag: t.operator, color: '#41FF00' },
|
||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||
{ tag: t.typeName, color: '#41FF00' },
|
||||
{ tag: t.atom, color: '#41FF00' },
|
||||
{ tag: t.number, color: '#41FF00' },
|
||||
{ tag: t.definition(t.variableName), color: '#41FF00' },
|
||||
{ tag: t.string, color: '#41FF00' },
|
||||
{ tag: t.special(t.string), color: '#41FF00' },
|
||||
{ tag: t.comment, color: '#41FF00' },
|
||||
{ tag: t.variableName, color: '#41FF00' },
|
||||
{ tag: t.tagName, color: '#41FF00' },
|
||||
{ tag: t.bracket, color: '#41FF00' },
|
||||
{ tag: t.meta, color: '#41FF00' },
|
||||
{ tag: t.attributeName, color: '#41FF00' },
|
||||
{ tag: t.propertyName, color: '#41FF00' },
|
||||
{ tag: t.className, color: '#41FF00' },
|
||||
{ tag: t.invalid, color: '#41FF00' },
|
||||
],
|
||||
});
|
||||
|
||||
+19
-19
@@ -16,24 +16,24 @@ export default createTheme({
|
||||
theme: 'light',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.labelName, color: 'inherit' },
|
||||
{ tag: t.keyword, color: 'inherit' },
|
||||
{ tag: t.operator, color: 'inherit' },
|
||||
{ tag: t.special(t.variableName), color: 'inherit' },
|
||||
{ tag: t.typeName, color: 'inherit' },
|
||||
{ tag: t.atom, color: 'inherit' },
|
||||
{ tag: t.number, color: 'inherit' },
|
||||
{ tag: t.definition(t.variableName), color: 'inherit' },
|
||||
{ tag: t.string, color: 'inherit' },
|
||||
{ tag: t.special(t.string), color: 'inherit' },
|
||||
{ tag: t.comment, color: 'inherit' },
|
||||
{ tag: t.variableName, color: 'inherit' },
|
||||
{ tag: t.tagName, color: 'inherit' },
|
||||
{ tag: t.bracket, color: 'inherit' },
|
||||
{ tag: t.meta, color: 'inherit' },
|
||||
{ tag: t.attributeName, color: 'inherit' },
|
||||
{ tag: t.propertyName, color: 'inherit' },
|
||||
{ tag: t.className, color: 'inherit' },
|
||||
{ tag: t.invalid, color: 'inherit' },
|
||||
{ tag: t.labelName, color: 'black' },
|
||||
{ tag: t.keyword, color: 'black' },
|
||||
{ tag: t.operator, color: 'black' },
|
||||
{ tag: t.special(t.variableName), color: 'black' },
|
||||
{ tag: t.typeName, color: 'black' },
|
||||
{ tag: t.atom, color: 'black' },
|
||||
{ tag: t.number, color: 'black' },
|
||||
{ tag: t.definition(t.variableName), color: 'black' },
|
||||
{ tag: t.string, color: 'black' },
|
||||
{ tag: t.special(t.string), color: 'black' },
|
||||
{ tag: t.comment, color: 'black' },
|
||||
{ tag: t.variableName, color: 'black' },
|
||||
{ tag: t.tagName, color: 'black' },
|
||||
{ tag: t.bracket, color: 'black' },
|
||||
{ tag: t.meta, color: 'black' },
|
||||
{ tag: t.attributeName, color: 'black' },
|
||||
{ tag: t.propertyName, color: 'black' },
|
||||
{ tag: t.className, color: 'black' },
|
||||
{ tag: t.invalid, color: 'black' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2593,7 +2593,7 @@ export const steps = register('steps', function (targetTactus, pat) {
|
||||
// avoid divide by zero..
|
||||
return nothing;
|
||||
}
|
||||
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
|
||||
return pat.fast(Fraction(targetTactus).div(pat.tactus));
|
||||
});
|
||||
|
||||
export function _polymeterListSteps(steps, ...args) {
|
||||
@@ -2801,7 +2801,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
|
||||
return pat.s_add(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
export const s_extend = stepRegister('s_extend', function (factor, pat) {
|
||||
export const s_cycles = stepRegister('s_extend', function (factor, pat) {
|
||||
return pat.fast(factor).s_expand(factor);
|
||||
});
|
||||
|
||||
@@ -2882,13 +2882,6 @@ export const s_tour = function (pat, ...many) {
|
||||
return pat.s_tour(...many);
|
||||
};
|
||||
|
||||
const s_zip = function (...pats) {
|
||||
pats = pats.filter((pat) => pat.hasTactus);
|
||||
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
|
||||
// Should maybe use lcm or gcd for tactus?
|
||||
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Control-related functions, i.e. ones that manipulate patterns of
|
||||
// objects
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
pick.mjs - methods that use one pattern to pick events from other patterns.
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.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 { Pattern, reify, silence, register } from './pattern.mjs';
|
||||
|
||||
import { _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
const _pick = function (lookup, pat, modulo = true) {
|
||||
const array = Array.isArray(lookup);
|
||||
const len = Object.keys(lookup).length;
|
||||
|
||||
lookup = objectMap(lookup, reify);
|
||||
|
||||
if (len === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.fmap((i) => {
|
||||
let key = i;
|
||||
if (array) {
|
||||
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||
}
|
||||
return lookup[key];
|
||||
});
|
||||
};
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||
* @example
|
||||
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||
* @example
|
||||
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||
* @example
|
||||
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||
*/
|
||||
|
||||
export const pick = function (lookup, pat) {
|
||||
// backward compatibility - the args used to be flipped
|
||||
if (Array.isArray(pat)) {
|
||||
[pat, lookup] = [lookup, pat];
|
||||
}
|
||||
return __pick(lookup, pat);
|
||||
};
|
||||
|
||||
const __pick = register('pick', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).innerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).innerJoin();
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||
*/
|
||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickOut = register('pickOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).outerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).outerJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).restartJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a@2 b@2 c@2 d@2>".pickRestart({
|
||||
a: n("0 1 2 0"),
|
||||
b: n("2 3 4 ~"),
|
||||
c: n("[4 5] [4 3] 2 0"),
|
||||
d: n("0 -3 0 ~")
|
||||
}).scale("C:major").s("piano")
|
||||
*/
|
||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).restartJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickReset = register('pickReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).resetJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).resetJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @name inhabit
|
||||
* @synonyms pickSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||
b: s("cp sd")
|
||||
})
|
||||
* @example
|
||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||
*/
|
||||
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).squeezeJoin();
|
||||
});
|
||||
|
||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @name inhabitmod
|
||||
* @synonyms pickmodSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).squeezeJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||
*/
|
||||
|
||||
export const squeeze = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat
|
||||
.fmap((i) => {
|
||||
const key = _mod(Math.round(i), xs.length);
|
||||
return xs[key];
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
+221
-29
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
signal.mjs - continuous patterns
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
|
||||
signal.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.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 { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id, _mod } from './util.mjs';
|
||||
import { id, _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
@@ -253,6 +253,211 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
*/
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
const _pick = function (lookup, pat, modulo = true) {
|
||||
const array = Array.isArray(lookup);
|
||||
const len = Object.keys(lookup).length;
|
||||
|
||||
lookup = objectMap(lookup, reify);
|
||||
|
||||
if (len === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.fmap((i) => {
|
||||
let key = i;
|
||||
if (array) {
|
||||
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||
}
|
||||
return lookup[key];
|
||||
});
|
||||
};
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||
* @example
|
||||
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||
* @example
|
||||
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||
* @example
|
||||
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||
*/
|
||||
|
||||
export const pick = function (lookup, pat) {
|
||||
// backward compatibility - the args used to be flipped
|
||||
if (Array.isArray(pat)) {
|
||||
[pat, lookup] = [lookup, pat];
|
||||
}
|
||||
return __pick(lookup, pat);
|
||||
};
|
||||
|
||||
const __pick = register('pick', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).innerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).innerJoin();
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||
*/
|
||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickOut = register('pickOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).outerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).outerJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).restartJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a@2 b@2 c@2 d@2>".pickRestart({
|
||||
a: n("0 1 2 0"),
|
||||
b: n("2 3 4 ~"),
|
||||
c: n("[4 5] [4 3] 2 0"),
|
||||
d: n("0 -3 0 ~")
|
||||
}).scale("C:major").s("piano")
|
||||
*/
|
||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).restartJoin();
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickReset = register('pickReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).resetJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).resetJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @name inhabit
|
||||
* @synonyms pickSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||
b: s("cp sd")
|
||||
})
|
||||
* @example
|
||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||
*/
|
||||
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).squeezeJoin();
|
||||
});
|
||||
|
||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @name inhabitmod
|
||||
* @synonyms pickmodSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).squeezeJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||
*/
|
||||
|
||||
export const squeeze = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat
|
||||
.fmap((i) => {
|
||||
const key = _mod(Math.round(i), xs.length);
|
||||
return xs[key];
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
|
||||
export const __chooseWith = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
@@ -391,11 +596,8 @@ export const perlinWith = (pat) => {
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
export const degradeByWith = register(
|
||||
'degradeByWith',
|
||||
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
true,
|
||||
true,
|
||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -412,14 +614,9 @@ export const degradeByWith = register(
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
export const degradeBy = register(
|
||||
'degradeBy',
|
||||
function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -433,7 +630,7 @@ export const degradeBy = register(
|
||||
* @example
|
||||
* s("[hh?]*8")
|
||||
*/
|
||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
|
||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
|
||||
/**
|
||||
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
|
||||
@@ -453,17 +650,12 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
|
||||
* x => x.undegradeBy(0.8).pan(1)
|
||||
* )
|
||||
*/
|
||||
export const undegradeBy = register(
|
||||
'undegradeBy',
|
||||
function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
|
||||
@@ -480,7 +672,7 @@ export const undegradeBy = register(
|
||||
* x => x.undegrade().pan(1)
|
||||
* )
|
||||
*/
|
||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
|
||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
+173
-223
@@ -1,253 +1,203 @@
|
||||
/*
|
||||
mini.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
|
||||
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.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 * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel/core';
|
||||
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
|
||||
|
||||
const randOffset = 0.0003;
|
||||
|
||||
const applyOptions = (parent, enter) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
const tactus_source = pat.__tactus_source;
|
||||
if (ops) {
|
||||
for (const op of ops) {
|
||||
switch (op.type_) {
|
||||
case 'stretch': {
|
||||
const legalTypes = ['fast', 'slow'];
|
||||
const { type, amount } = op.arguments_;
|
||||
if (!legalTypes.includes(type)) {
|
||||
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||
}
|
||||
pat = strudel.reify(pat)[type](enter(amount));
|
||||
break;
|
||||
}
|
||||
case 'replicate': {
|
||||
const { amount } = op.arguments_;
|
||||
pat = strudel.reify(pat);
|
||||
pat = pat._repeatCycles(amount)._fast(amount);
|
||||
break;
|
||||
}
|
||||
case 'bjorklund': {
|
||||
if (op.arguments_.rotation) {
|
||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
||||
} else {
|
||||
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'degradeBy': {
|
||||
pat = strudel
|
||||
.reify(pat)
|
||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
const friend = enter(op.arguments_.element);
|
||||
pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
case 'range': {
|
||||
const friend = enter(op.arguments_.element);
|
||||
pat = strudel.reify(pat);
|
||||
const arrayRange = (start, stop, step = 1) =>
|
||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
|
||||
start < stop ? start + index * step : start - index * step,
|
||||
);
|
||||
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
|
||||
pat = range(pat, friend);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn(`operator "${op.type_}" not implemented`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pat.__tactus_source = pat.__tactus_source || tactus_source;
|
||||
return pat;
|
||||
// "parser"
|
||||
let token_types = {
|
||||
open_polyrhythm: /^\[/,
|
||||
close_polyrhythm: /^\]/,
|
||||
open_polymeter: /^\{/,
|
||||
close_polymeter: /^\}/,
|
||||
open_slowpolymeter: /^\</,
|
||||
close_slowpolymeter: /^\>/,
|
||||
subpat_delimiter: /^,/,
|
||||
fast: /^\*/,
|
||||
slow: /^\//,
|
||||
// is there a better way without back-tracking for whole numbers?
|
||||
// float: /^(\d*\.)?\d+/,
|
||||
plain: /^[a-zA-Z0-9\.\#]+/,
|
||||
};
|
||||
|
||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
onEnter?.(ast);
|
||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||
switch (ast.type_) {
|
||||
case 'pattern': {
|
||||
// resolveReplications(ast);
|
||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||
const alignment = ast.arguments_.alignment;
|
||||
const with_tactus = children.filter((child) => child.__tactus_source);
|
||||
let pat;
|
||||
switch (alignment) {
|
||||
case 'stack': {
|
||||
pat = strudel.stack(...children);
|
||||
if (with_tactus.length) {
|
||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'polymeter_slowcat': {
|
||||
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
|
||||
if (with_tactus.length) {
|
||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'polymeter': {
|
||||
// polymeter
|
||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||
let token_offset;
|
||||
function next_token(code, global_offset) {
|
||||
for (let type in token_types) {
|
||||
const match = code.match(token_types[type]);
|
||||
if (match) {
|
||||
const leftpad = match[0].length - match[0].trimLeft().length;
|
||||
const rightpad = match[0].length - match[0].trimRight().length;
|
||||
const result = {
|
||||
type,
|
||||
value: match[0],
|
||||
location: [token_offset + leftpad + global_offset, token_offset + match[0].length + global_offset - rightpad],
|
||||
};
|
||||
token_offset += match[0].length;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw new Error('could not match "' + code + '"');
|
||||
}
|
||||
|
||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
|
||||
pat = strudel.stack(...aligned);
|
||||
break;
|
||||
}
|
||||
case 'rand': {
|
||||
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||
if (with_tactus.length) {
|
||||
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'feet': {
|
||||
pat = strudel.fastcat(...children);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (weightedChildren) {
|
||||
const weightSum = ast.source_.reduce(
|
||||
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
|
||||
strudel.Fraction(0),
|
||||
);
|
||||
pat = strudel.timeCat(
|
||||
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
|
||||
);
|
||||
pat.__weight = weightSum; // for polymeter
|
||||
pat.tactus = weightSum;
|
||||
if (with_tactus.length) {
|
||||
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
|
||||
}
|
||||
} else {
|
||||
pat = strudel.sequence(...children);
|
||||
pat.tactus = children.length;
|
||||
}
|
||||
if (ast.arguments_.tactus) {
|
||||
pat.__tactus_source = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (with_tactus.length) {
|
||||
pat.__tactus_source = true;
|
||||
}
|
||||
return pat;
|
||||
function tokenize(code, global_offset) {
|
||||
let tokens = [];
|
||||
token_offset = 0;
|
||||
while (code.length > 0) {
|
||||
const trimmed = code.trim();
|
||||
token_offset += code.length - trimmed.length;
|
||||
code = trimmed;
|
||||
const token = next_token(code, global_offset);
|
||||
code = code.slice(token.value.length);
|
||||
tokens.push(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
class Parser {
|
||||
parse(code, global_offset) {
|
||||
this.tokens = tokenize(code, global_offset);
|
||||
const subpats = this.parse_subpats();
|
||||
return { type: 'polymeter', subpats };
|
||||
}
|
||||
consume(type) {
|
||||
const token = this.tokens.shift();
|
||||
this.offset += token.length;
|
||||
if (token.type !== type) {
|
||||
throw new Error('expected token type ' + type + ' got ' + token.type);
|
||||
}
|
||||
case 'element': {
|
||||
1;
|
||||
return enter(ast.source_);
|
||||
return token;
|
||||
}
|
||||
parse_step() {
|
||||
let next = this.tokens[0]?.type;
|
||||
if (next === 'open_polyrhythm') {
|
||||
return this.parse_polyrhythm();
|
||||
}
|
||||
case 'atom': {
|
||||
if (ast.source_ === '~' || ast.source_ === '-') {
|
||||
return strudel.silence;
|
||||
}
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
if (offset === -1) {
|
||||
// skip location handling (used when getting leaves to avoid confusion)
|
||||
return strudel.pure(value);
|
||||
}
|
||||
const [from, to] = getLeafLocation(code, ast, offset);
|
||||
return strudel.pure(value).withLoc(from, to);
|
||||
if (next === 'open_polymeter') {
|
||||
return this.parse_polymeter();
|
||||
}
|
||||
case 'stretch':
|
||||
return enter(ast.source_).slow(enter(ast.arguments_.amount));
|
||||
default:
|
||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||
return strudel.silence;
|
||||
if (next === 'open_slowpolymeter') {
|
||||
return this.parse_slowpolymeter();
|
||||
}
|
||||
if (next === 'plain') {
|
||||
return this.consume('plain');
|
||||
}
|
||||
throw new Error('unexpected token ' + this.tokens[0]?.value + ' of type ' + this.tokens[0]?.type);
|
||||
}
|
||||
parse_expr() {
|
||||
let result = this.parse_step();
|
||||
let looking = true;
|
||||
while (looking) {
|
||||
let next = this.tokens[0]?.type;
|
||||
if (!next) {
|
||||
looking = false;
|
||||
} else {
|
||||
switch (next) {
|
||||
case 'fast':
|
||||
case 'slow':
|
||||
this.consume(next);
|
||||
const factor = this.parse_step();
|
||||
result = { type: next, factor, step: result };
|
||||
break;
|
||||
default:
|
||||
looking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
parse_seq(...close_types) {
|
||||
const steps = [];
|
||||
while (!close_types.includes(this.tokens[0]?.type)) {
|
||||
steps.push(this.parse_expr());
|
||||
}
|
||||
return { type: 'seq', steps };
|
||||
}
|
||||
parse_subpats(close_type) {
|
||||
const subpats = [];
|
||||
while (true) {
|
||||
subpats.push(this.parse_seq(close_type, 'subpat_delimiter'));
|
||||
const next = this.tokens[0]?.type;
|
||||
if (next) {
|
||||
this.consume(next);
|
||||
}
|
||||
if (next === close_type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return subpats;
|
||||
}
|
||||
parse_polymeter() {
|
||||
this.consume('open_polymeter');
|
||||
const subpats = this.parse_subpats('close_polymeter');
|
||||
return { type: 'polymeter', subpats };
|
||||
}
|
||||
parse_polyrhythm() {
|
||||
this.consume('open_polyrhythm');
|
||||
const subpats = this.parse_subpats('close_polyrhythm');
|
||||
return { type: 'polyrhythm', subpats };
|
||||
}
|
||||
parse_slowpolymeter() {
|
||||
this.consume('open_slowpolymeter');
|
||||
const subpats = this.parse_subpats('close_slowpolymeter');
|
||||
return { type: 'slowpolymeter', subpats };
|
||||
}
|
||||
}
|
||||
|
||||
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
|
||||
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
|
||||
// value is expected without quotes!
|
||||
const { start, end } = leaf.location_;
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
// make sure whitespaces are not part of the highlight
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns ast
|
||||
export const mini2ast = (code, start = 0, userCode = code) => {
|
||||
try {
|
||||
return krill.parse(code);
|
||||
} catch (error) {
|
||||
const region = [error.location.start.offset + start, error.location.end.offset + start];
|
||||
const line = userCode.slice(0, region[0]).split('\n').length;
|
||||
throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
|
||||
function patternifyTree(tree) {
|
||||
if (tree.type === 'polyrhythm') {
|
||||
const args = tree.subpats.map((arg) => patternifyTree(arg));
|
||||
return strudel.polyrhythm(...args);
|
||||
}
|
||||
if (tree.type === 'slowpolymeter') {
|
||||
const args = tree.subpats.map((arg) => patternifyTree(arg));
|
||||
return strudel.s_polymeterSteps(1, ...args);
|
||||
}
|
||||
if (tree.type === 'polymeter') {
|
||||
const args = tree.subpats.map((subpat) => patternifyTree(subpat));
|
||||
return strudel.s_polymeter(...args);
|
||||
}
|
||||
if (tree.type === 'fast') {
|
||||
const step = patternifyTree(tree.step);
|
||||
const factor = patternifyTree(tree.factor);
|
||||
return strudel.fast(factor, step);
|
||||
}
|
||||
if (tree.type === 'slow') {
|
||||
const step = patternifyTree(tree.step);
|
||||
const factor = patternifyTree(tree.factor);
|
||||
return strudel.slow(factor, step);
|
||||
}
|
||||
if (tree.type === 'plain') {
|
||||
return strudel.pure(tree.value).withLoc(tree.location[0], tree.location[1]);
|
||||
}
|
||||
if (tree.type === 'seq') {
|
||||
const steps = tree.steps.map((step) => patternifyTree(step));
|
||||
return strudel.fastcat(...steps);
|
||||
}
|
||||
}
|
||||
|
||||
const parser = new Parser();
|
||||
export const m = (code, global_offset) => {
|
||||
const tree = parser.parse(code, global_offset + 1);
|
||||
const pat = patternifyTree(tree);
|
||||
return strudel.reify(pat);
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns all nodes that are leaves
|
||||
export const getLeaves = (code, start, userCode) => {
|
||||
const ast = mini2ast(code, start, userCode);
|
||||
let leaves = [];
|
||||
patternifyAST(
|
||||
ast,
|
||||
code,
|
||||
(node) => {
|
||||
if (node.type_ === 'atom') {
|
||||
leaves.push(node);
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return leaves;
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
|
||||
export const getLeafLocations = (code, start = 0, userCode) => {
|
||||
return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start));
|
||||
export const getLeafLocations = (code, start) => {
|
||||
code = code.replace(/^"/, '');
|
||||
code = code.replace(/"$/, '');
|
||||
const leaves = tokenize(code, start).filter((x) => x.type == 'plain');
|
||||
return leaves.map((x) => x.location.map((y) => y + 1));
|
||||
};
|
||||
|
||||
// mini notation only (wraps in "")
|
||||
export const mini = (...strings) => {
|
||||
const pats = strings.map((str) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code);
|
||||
});
|
||||
const pats = strings.map(m);
|
||||
return strudel.sequence(...pats);
|
||||
};
|
||||
|
||||
// turns str mini string (without quotes) into pattern
|
||||
// offset is the position of the mini string in the JS code
|
||||
// each leaf node will get .withLoc added
|
||||
// this function is used by the transpiler for double quoted strings
|
||||
export const m = (str, offset) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code, null, offset);
|
||||
};
|
||||
|
||||
// includes haskell style (raw krill parsing)
|
||||
export const h = (string) => {
|
||||
const ast = mini2ast(string);
|
||||
return patternifyAST(ast, string);
|
||||
};
|
||||
|
||||
export function minify(thing) {
|
||||
if (typeof thing === 'string') {
|
||||
return mini(thing);
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
"devDependencies": {
|
||||
"peggy": "^3.0.2",
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,19 +221,6 @@ describe('mini', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocation', () => {
|
||||
it('gets location of leaf nodes', () => {
|
||||
const code = '"bd sd"';
|
||||
const ast = mini2ast(code);
|
||||
|
||||
const bd = ast.source_[0].source_;
|
||||
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
|
||||
|
||||
const sd = ast.source_[1].source_;
|
||||
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocations', () => {
|
||||
it('gets locations of leaf nodes', () => {
|
||||
expect(getLeafLocations('"bd sd"')).toEqual([
|
||||
@@ -242,9 +229,9 @@ describe('getLeafLocations', () => {
|
||||
]);
|
||||
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[4, 5], // "2" columns
|
||||
[7, 9], // sd columns
|
||||
[10, 12], // cp columns
|
||||
[4, 5], // "2" columns
|
||||
]);
|
||||
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
|
||||
@@ -10,8 +10,6 @@ if (typeof HTMLElement !== 'undefined') {
|
||||
static observedAttributes = ['code'];
|
||||
settings = codemirrorSettings.get();
|
||||
editor = null;
|
||||
sync = false;
|
||||
solo = true;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -51,8 +49,6 @@ if (typeof HTMLElement !== 'undefined') {
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
},
|
||||
solo: this.solo,
|
||||
sync: this.sync,
|
||||
});
|
||||
// init settings
|
||||
this.editor.updateSettings(this.settings);
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^2.1.3"
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9265
-11483
File diff suppressed because it is too large
Load Diff
@@ -4551,27 +4551,6 @@ exports[`runs examples > example "lsize" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "markcss" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
"[ 1/4 → 1/2 | note:a ]",
|
||||
"[ 1/2 → 3/4 | note:f ]",
|
||||
"[ 3/4 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 5/4 | note:c ]",
|
||||
"[ 5/4 → 3/2 | note:a ]",
|
||||
"[ 3/2 → 7/4 | note:f ]",
|
||||
"[ 7/4 → 2/1 | note:e ]",
|
||||
"[ 2/1 → 9/4 | note:c ]",
|
||||
"[ 9/4 → 5/2 | note:a ]",
|
||||
"[ 5/2 → 11/4 | note:f ]",
|
||||
"[ 11/4 → 3/1 | note:e ]",
|
||||
"[ 3/1 → 13/4 | note:c ]",
|
||||
"[ 13/4 → 7/2 | note:a ]",
|
||||
"[ 7/2 → 15/4 | note:f ]",
|
||||
"[ 15/4 → 4/1 | note:e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "mask" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
|
||||
@@ -134,9 +134,6 @@ strudel.Pattern.prototype._pitchwheel = function () {
|
||||
strudel.Pattern.prototype._pianoroll = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.markcss = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
const uiHelpersMocked = {
|
||||
backgroundImage: id,
|
||||
|
||||
@@ -3,6 +3,11 @@ import { configDefaults, defineConfig } from 'vitest/config';
|
||||
/// <reference types="vitest" />
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// ...
|
||||
deps: {
|
||||
registerNodeLoader: true,
|
||||
},
|
||||
threads: false,
|
||||
reporters: 'verbose',
|
||||
isolate: false,
|
||||
silent: true,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
Vendored
+290
@@ -0,0 +1,290 @@
|
||||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.mdx': Promise<{
|
||||
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.md': Promise<{
|
||||
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
export { z } from 'astro/zod';
|
||||
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof AnyEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||
|
||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||
export type DataCollectionKey = keyof DataEntryMap;
|
||||
|
||||
// This needs to be in sync with ImageMetadata
|
||||
export type ImageFunction = () => import('astro/zod').ZodObject<{
|
||||
src: import('astro/zod').ZodString;
|
||||
width: import('astro/zod').ZodNumber;
|
||||
height: import('astro/zod').ZodNumber;
|
||||
format: import('astro/zod').ZodUnion<
|
||||
[
|
||||
import('astro/zod').ZodLiteral<'png'>,
|
||||
import('astro/zod').ZodLiteral<'jpg'>,
|
||||
import('astro/zod').ZodLiteral<'jpeg'>,
|
||||
import('astro/zod').ZodLiteral<'tiff'>,
|
||||
import('astro/zod').ZodLiteral<'webp'>,
|
||||
import('astro/zod').ZodLiteral<'gif'>,
|
||||
import('astro/zod').ZodLiteral<'svg'>,
|
||||
import('astro/zod').ZodLiteral<'avif'>,
|
||||
]
|
||||
>;
|
||||
}>;
|
||||
|
||||
type BaseSchemaWithoutEffects =
|
||||
| import('astro/zod').AnyZodObject
|
||||
| import('astro/zod').ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
|
||||
| import('astro/zod').ZodDiscriminatedUnion<string, import('astro/zod').AnyZodObject[]>
|
||||
| import('astro/zod').ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
|
||||
|
||||
type BaseSchema =
|
||||
| BaseSchemaWithoutEffects
|
||||
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
|
||||
|
||||
export type SchemaContext = { image: ImageFunction };
|
||||
|
||||
type DataCollectionConfig<S extends BaseSchema> = {
|
||||
type: 'data';
|
||||
schema?: S | ((context: SchemaContext) => S);
|
||||
};
|
||||
|
||||
type ContentCollectionConfig<S extends BaseSchema> = {
|
||||
type?: 'content';
|
||||
schema?: S | ((context: SchemaContext) => S);
|
||||
};
|
||||
|
||||
type CollectionConfig<S> = ContentCollectionConfig<S> | DataCollectionConfig<S>;
|
||||
|
||||
export function defineCollection<S extends BaseSchema>(
|
||||
input: CollectionConfig<S>
|
||||
): CollectionConfig<S>;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||
ContentEntryMap[C]
|
||||
>['slug'];
|
||||
|
||||
export function getEntryBySlug<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
// Note that this has to accept a regular string too, for SSR
|
||||
entrySlug: E
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||
collection: C,
|
||||
entryId: E
|
||||
): Promise<CollectionEntry<C>>;
|
||||
|
||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(entry: {
|
||||
collection: C;
|
||||
slug: E;
|
||||
}): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(entry: {
|
||||
collection: C;
|
||||
id: E;
|
||||
}): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
slug: E
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof ContentEntryMap>(
|
||||
entries: {
|
||||
collection: C;
|
||||
slug: ValidContentEntrySlug<C>;
|
||||
}[]
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: {
|
||||
collection: C;
|
||||
id: keyof DataEntryMap[C];
|
||||
}[]
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function reference<C extends keyof AnyEntryMap>(
|
||||
collection: C
|
||||
): import('astro/zod').ZodEffects<
|
||||
import('astro/zod').ZodString,
|
||||
C extends keyof ContentEntryMap
|
||||
? {
|
||||
collection: C;
|
||||
slug: ValidContentEntrySlug<C>;
|
||||
}
|
||||
: {
|
||||
collection: C;
|
||||
id: keyof DataEntryMap[C];
|
||||
}
|
||||
>;
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
export function reference<C extends string>(
|
||||
collection: C
|
||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
|
||||
type ContentEntryMap = {
|
||||
"blog": {
|
||||
"release-0.0.2-schwindlig.mdx": {
|
||||
id: "release-0.0.2-schwindlig.mdx";
|
||||
slug: "release-002-schwindlig";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.2.1-stuermisch.mdx": {
|
||||
id: "release-0.0.2.1-stuermisch.mdx";
|
||||
slug: "release-0021-stuermisch";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.3-maelstrom.mdx": {
|
||||
id: "release-0.0.3-maelstrom.mdx";
|
||||
slug: "release-003-maelstrom";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.0.4-gischt.mdx": {
|
||||
id: "release-0.0.4-gischt.mdx";
|
||||
slug: "release-004-gischt";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.3.0-donauwelle.mdx": {
|
||||
id: "release-0.3.0-donauwelle.mdx";
|
||||
slug: "release-030-donauwelle";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.4.0-brandung.mdx": {
|
||||
id: "release-0.4.0-brandung.mdx";
|
||||
slug: "release-040-brandung";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.5.0-wirbel.mdx": {
|
||||
id: "release-0.5.0-wirbel.mdx";
|
||||
slug: "release-050-wirbel";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.6.0-zimtschnecke.mdx": {
|
||||
id: "release-0.6.0-zimtschnecke.mdx";
|
||||
slug: "release-060-zimtschnecke";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.7.0-zuckerguss.mdx": {
|
||||
id: "release-0.7.0-zuckerguss.mdx";
|
||||
slug: "release-070-zuckerguss";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.8.0-himbeermuffin.mdx": {
|
||||
id: "release-0.8.0-himbeermuffin.mdx";
|
||||
slug: "release-080-himbeermuffin";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-0.9.0-bananenbrot.mdx": {
|
||||
id: "release-0.9.0-bananenbrot.mdx";
|
||||
slug: "release-090-bananenbrot";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"release-1.0.0-geburtstagskuchen.mdx": {
|
||||
id: "release-1.0.0-geburtstagskuchen.mdx";
|
||||
slug: "release-100-geburtstagskuchen";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
"year-2.mdx": {
|
||||
id: "year-2.mdx";
|
||||
slug: "year-2";
|
||||
body: string;
|
||||
collection: "blog";
|
||||
data: InferEntrySchema<"blog">
|
||||
} & { render(): Render[".mdx"] };
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
type DataEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||
|
||||
type ContentConfig = typeof import("../src/content/config");
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
.astro
|
||||
# build output
|
||||
dist/
|
||||
|
||||
|
||||
@@ -26,15 +26,15 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
title="Top Navigation"
|
||||
>
|
||||
<div class="flex overflow-visible items-center grow" style="overflow:visible">
|
||||
<div class="flex items-center text-2xl space-x-2">
|
||||
<a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
|
||||
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
|
||||
<span class="block rotate-90 text-blue-500">꩜</span>
|
||||
<div class="flex space-x-2 items-baseline">
|
||||
<span>🌀</span>
|
||||
<div class="flex space-x-1 items-baseline">
|
||||
<span class="">strudel</span>
|
||||
<span class="text-sm font-medium">DOCS</span>
|
||||
<span class="text-sm">DOCS</span>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
|
||||
<div class="search-item h-10">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Panel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<HorizontalPanel context={context} />
|
||||
<Panel context={context} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { silence, noteToMidi, _mod } from '@strudel/core';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
@@ -36,7 +37,7 @@ export function MiniRepl({
|
||||
const shouldShowCanvas = !!punchcard;
|
||||
const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
|
||||
autodraw = !!punchcard || !!claviature || !!autodraw;
|
||||
drawTime = (drawTime ?? punchcard) ? [0, 4] : [-2, 2];
|
||||
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
|
||||
if (claviature) {
|
||||
drawTime = [0, 0];
|
||||
}
|
||||
|
||||
@@ -98,7 +98,3 @@ What follows is the API doc of all the options you can pass:
|
||||
## Pitchwheel
|
||||
|
||||
<JsDoc client:idle name="pitchwheel" h={0} />
|
||||
|
||||
## markcss
|
||||
|
||||
<JsDoc client:idle name="markcss" h={0} />
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
|
||||
import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon';
|
||||
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
|
||||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { useSettings, setIsZen } from '../../settings.mjs';
|
||||
@@ -11,19 +15,24 @@ export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
|
||||
const { isZen } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
|
||||
'flex-none text-black z-[100] text-lg select-none',
|
||||
!isZen && !isEmbedded && 'bg-lineHighlight',
|
||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
||||
isEmbedded ? 'flex' : 'md:flex',
|
||||
)}
|
||||
>
|
||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||
{/* <img
|
||||
src={logo}
|
||||
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
|
||||
alt="logo"
|
||||
/> */}
|
||||
<h1
|
||||
onClick={() => {
|
||||
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
|
||||
@@ -34,46 +43,31 @@ export function Header({ context, embedded = false }) {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'mt-[1px]',
|
||||
started && !isCSSAnimationDisabled && 'animate-spin',
|
||||
'cursor-pointer text-blue-500',
|
||||
isZen && 'fixed top-2 right-4',
|
||||
)}
|
||||
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer', isZen && 'fixed top-2 right-4')}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block rotate-90">꩜</span>
|
||||
🌀
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="space-x-2">
|
||||
<span className="">strudel</span>
|
||||
<span className="text-sm font-medium">REPL</span>
|
||||
{!isEmbedded && isButtonRowHidden && (
|
||||
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
|
||||
DOCS
|
||||
</a>
|
||||
)}
|
||||
<div className={cx(started && 'animate-pulse')}>
|
||||
<span className="">strudel</span> <span className="text-sm">REPL</span>
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
{!isZen && !isButtonRowHidden && (
|
||||
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
|
||||
{!isZen && (
|
||||
<div className="flex max-w-full overflow-auto text-foreground">
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
title={started ? 'stop' : 'play'}
|
||||
className={cx(
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
'hover:opacity-50',
|
||||
!started && !isCSSAnimationDisabled && 'animate-pulse',
|
||||
)}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-2')}>
|
||||
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
|
||||
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
|
||||
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
|
||||
</span>
|
||||
@@ -90,6 +84,8 @@ export function Header({ context, embedded = false }) {
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
|
||||
)}
|
||||
>
|
||||
{/* <CommandLineIcon className="w-6 h-6" /> */}
|
||||
<ArrowPathIcon className="w-6 h-6" />
|
||||
{!isEmbedded && <span>update</span>}
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
@@ -98,6 +94,7 @@ export function Header({ context, embedded = false }) {
|
||||
className="hover:opacity-50 p-2 flex items-center space-x-1"
|
||||
onClick={handleShuffle}
|
||||
>
|
||||
<SparklesIcon className="w-6 h-6" />
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -110,6 +107,7 @@ export function Header({ context, embedded = false }) {
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<LinkIcon className="w-6 h-6" />
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -119,6 +117,7 @@ export function Header({ context, embedded = false }) {
|
||||
href={`${baseNoTrailing}/workshop/getting-started/`}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<AcademicCapIcon className="w-6 h-6" />
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Panel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
@@ -13,7 +13,7 @@ export default function ReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition, isZen } = settings;
|
||||
const { panelPosition } = settings;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
@@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
|
||||
{panelPosition === 'right' && <Panel context={context} />}
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
|
||||
{panelPosition === 'bottom' && <Panel context={context} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,18 @@
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
|
||||
export function ConsoleTab() {
|
||||
const [log, setLog] = useState([]);
|
||||
const { fontFamily, fontSize } = useSettings();
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
export function ConsoleTab({ log }) {
|
||||
return (
|
||||
<div
|
||||
id="console-tab"
|
||||
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
|
||||
style={{ fontFamily, fontSize }}
|
||||
>
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
|
||||
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
|
||||
███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
|
||||
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}</pre>
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
@@ -72,7 +42,3 @@ function linkify(inputText) {
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export function ButtonGroup({ value, onChange, items }) {
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
id={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'px-2 border-b h-8 whitespace-nowrap',
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
|
||||
import { logger } from '@strudel/core';
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
|
||||
import { setActiveFooter, useSettings } from '../../../settings.mjs';
|
||||
import { ConsoleTab } from './ConsoleTab';
|
||||
import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
@@ -7,206 +12,117 @@ import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
import useClient from '@src/useClient.mjs';
|
||||
|
||||
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
|
||||
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
|
||||
|
||||
export function HorizontalPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { isPanelOpen, activeFooter: tab } = settings;
|
||||
export function Panel({ context }) {
|
||||
const footerContent = useRef();
|
||||
const [log, setLog] = useState([]);
|
||||
const { activeFooter, isZen, panelPosition } = useSettings();
|
||||
|
||||
return (
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')}
|
||||
>
|
||||
{isPanelOpen && (
|
||||
<div className="flex h-full overflow-auto pr-10 ">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
)}
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (footerContent.current && activeFooter === 'console') {
|
||||
// scroll log box to bottom when log changes
|
||||
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||
}
|
||||
}, [log, activeFooter]);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!footerContent.current) {
|
||||
} else if (activeFooter === 'console') {
|
||||
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||
} else {
|
||||
footerContent.current.scrollTop = 0;
|
||||
}
|
||||
}, [activeFooter]);
|
||||
|
||||
<div className="absolute right-4 pt-4">
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
</div>
|
||||
</PanelNav>
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
}
|
||||
|
||||
export function VerticalPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { activeFooter: tab, isPanelOpen } = settings;
|
||||
const PanelTab = ({ children, name, label }) => (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setActiveFooter(name)}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
activeFooter === name ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label || name}
|
||||
</div>
|
||||
{activeFooter === name && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
const client = useClient();
|
||||
if (isZen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isActive = activeFooter !== '';
|
||||
|
||||
let positions = {
|
||||
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
|
||||
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
|
||||
};
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')}
|
||||
>
|
||||
{isPanelOpen ? (
|
||||
<div className={cx('flex flex-col h-full')}>
|
||||
<div className="flex justify-between w-full ">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
|
||||
<div className="flex justify-between px-2">
|
||||
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
|
||||
<PanelTab name="intro" label="welcome" />
|
||||
<PanelTab name="patterns" />
|
||||
<PanelTab name="sounds" />
|
||||
<PanelTab name="console" />
|
||||
<PanelTab name="reference" />
|
||||
<PanelTab name="settings" />
|
||||
{TAURI && <PanelTab name="files" />}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
|
||||
{activeFooter === 'intro' && <WelcomeTab context={context} />}
|
||||
{activeFooter === 'patterns' && <PatternsTab context={context} />}
|
||||
{activeFooter === 'console' && <ConsoleTab log={log} />}
|
||||
{activeFooter === 'sounds' && <SoundsTab />}
|
||||
{activeFooter === 'reference' && <Reference />}
|
||||
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
|
||||
{activeFooter === 'files' && <FilesTab />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setIsPanelOpened(true);
|
||||
}}
|
||||
aria-label="open menu panel"
|
||||
className={cx(
|
||||
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
|
||||
)}
|
||||
>
|
||||
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
</PanelNav>
|
||||
);
|
||||
}
|
||||
|
||||
const tabNames = {
|
||||
welcome: 'intro',
|
||||
patterns: 'patterns',
|
||||
sounds: 'sounds',
|
||||
reference: 'reference',
|
||||
console: 'console',
|
||||
settings: 'settings',
|
||||
};
|
||||
if (TAURI) {
|
||||
tabNames.files = 'files';
|
||||
}
|
||||
|
||||
function PanelNav({ children, className, settings, ...props }) {
|
||||
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
|
||||
return (
|
||||
<nav
|
||||
onClick={() => {
|
||||
if (!settings.isPanelOpen) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (isHoverBehavior && !settings.isPanelOpen) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isHoverBehavior && !settings.isPanelPinned) {
|
||||
setIsPanelOpened(false);
|
||||
}
|
||||
}}
|
||||
aria-label="Menu Panel"
|
||||
className={cx('bg-lineHighlight group overflow-x-auto', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelContent({ context, tab }) {
|
||||
switch (tab) {
|
||||
case tabNames.patterns:
|
||||
return <PatternsTab context={context} />;
|
||||
case tabNames.console:
|
||||
return <ConsoleTab />;
|
||||
case tabNames.sounds:
|
||||
return <SoundsTab />;
|
||||
case tabNames.reference:
|
||||
return <Reference />;
|
||||
case tabNames.settings:
|
||||
return <SettingsTab started={context.started} />;
|
||||
case tabNames.files:
|
||||
return <FilesTab />;
|
||||
default:
|
||||
return <WelcomeTab context={context} />;
|
||||
}
|
||||
}
|
||||
|
||||
function PanelTab({ label, isSelected, onClick }) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
isSelected ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
function Tabs({ setTab, tab, className }) {
|
||||
return (
|
||||
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
|
||||
{Object.keys(tabNames).map((key) => {
|
||||
const val = tabNames[key];
|
||||
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelActionButton({ settings }) {
|
||||
const { togglePanelTrigger, isPanelPinned, isPanelOpen } = settings;
|
||||
const isHoverBehavior = togglePanelTrigger === 'hover';
|
||||
if (!isPanelOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHoverBehavior) {
|
||||
return <PinButton pinned={isPanelPinned} />;
|
||||
}
|
||||
return <CloseButton onClick={() => setIsPanelOpened(false)} />;
|
||||
}
|
||||
|
||||
function PinButton({ pinned }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setPanelPinned(!pinned)}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
pinned ? 'flex' : 'hidden',
|
||||
)}
|
||||
aria-label="Pin Menu Panel"
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill={'currentColor'}
|
||||
strokeWidth="0"
|
||||
className="w-full h-full"
|
||||
opacity={pinned ? 1 : '.3'}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
)}
|
||||
aria-label="Close Menu"
|
||||
>
|
||||
<XMarkIcon />
|
||||
</button>
|
||||
);
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
||||
<button className="hover:opacity-50" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
@@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
||||
<section className="flex overflow-y-scroll max-h-full flex-col">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
|
||||
@@ -25,37 +25,32 @@ export function Reference() {
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
|
||||
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
<div class="w-full ml-2 mb-2 top-0 sticky">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md border-none"
|
||||
className="w-full p-1 bg-background rounded-md"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onInput={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
|
||||
id="reference-container"
|
||||
>
|
||||
<div className="prose dark:prose-invert min-w-full px-1 ">
|
||||
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container">
|
||||
<div className="prose dark:prose-invert max-w-full pr-4">
|
||||
<h2>API Reference</h2>
|
||||
<p>
|
||||
This is the long list functions you can use! Remember that you don't need to remember all of those and that
|
||||
|
||||
@@ -94,8 +94,6 @@ export function SettingsTab({ started }) {
|
||||
isAutoCompletionEnabled,
|
||||
isTooltipEnabled,
|
||||
isFlashEnabled,
|
||||
isButtonRowHidden,
|
||||
isCSSAnimationDisabled,
|
||||
isSyncEnabled,
|
||||
isLineWrappingEnabled,
|
||||
fontSize,
|
||||
@@ -103,12 +101,11 @@ export function SettingsTab({ started }) {
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
togglePanelTrigger,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4 w-full">
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -173,32 +170,7 @@ export function SettingsTab({ started }) {
|
||||
items={{ bottom: 'Bottom', right: 'Right' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
<FormItem label="Open Panel on: ">
|
||||
<ButtonGroup
|
||||
value={togglePanelTrigger}
|
||||
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
|
||||
items={{ click: 'Click', hover: 'Hover' }}
|
||||
></ButtonGroup>
|
||||
{/* <Checkbox
|
||||
label="Click"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'click');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger != 'hover'}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hover"
|
||||
onChange={(cbEvent) => {
|
||||
if (cbEvent.target.checked) {
|
||||
settingsMap.setKey('togglePanelTrigger', 'hover');
|
||||
}
|
||||
}}
|
||||
value={togglePanelTrigger == 'hover'}
|
||||
/> */}
|
||||
</FormItem>
|
||||
<FormItem label="More Settings">
|
||||
<FormItem label="Code Settings">
|
||||
<Checkbox
|
||||
label="Enable bracket matching"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)}
|
||||
@@ -258,16 +230,6 @@ export function SettingsTab({ started }) {
|
||||
disabled={shouldAlwaysSync}
|
||||
value={isSyncEnabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hide top buttons"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
|
||||
value={isButtonRowHidden}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable CSS Animations"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
|
||||
value={isCSSAnimationDisabled}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||
<FormItem label="Reset Settings">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
@@ -52,14 +52,15 @@ export function SoundsTab() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md my-2"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<div className="w-full ml-2 mb-2 top-0 sticky">
|
||||
<input
|
||||
className="w-full p-1 bg-background rounded-md"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
@@ -73,8 +74,7 @@ export function SoundsTab() {
|
||||
></ButtonGroup>
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -5,8 +5,10 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
|
||||
export function WelcomeTab({ context }) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
||||
<h3>꩜ welcome</h3>
|
||||
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
|
||||
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
|
||||
@@ -28,7 +30,7 @@ export function WelcomeTab({ context }) {
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>꩜ about</h3>
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="https://tidalcycles.org/" target="_blank">
|
||||
|
||||
@@ -30,16 +30,10 @@ export const defaultSettings = {
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
patternFilter: 'community',
|
||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||
panelPosition: 'right',
|
||||
isPanelPinned: false,
|
||||
isPanelOpen: true,
|
||||
togglePanelTrigger: 'click', //click | hover
|
||||
userPatterns: '{}',
|
||||
audioDeviceName: defaultAudioDeviceName,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
isButtonRowHidden: false,
|
||||
isCSSAnimationDisabled: false,
|
||||
};
|
||||
|
||||
let search = null;
|
||||
@@ -72,23 +66,17 @@ export function useSettings() {
|
||||
isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted),
|
||||
isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled),
|
||||
isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled),
|
||||
isButtonRowHidden: parseBoolean(state.isButtonRowHidden),
|
||||
isCSSAnimationDisabled: parseBoolean(state.isCSSAnimationDisabled),
|
||||
isTooltipEnabled: parseBoolean(state.isTooltipEnabled),
|
||||
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
|
||||
isFlashEnabled: parseBoolean(state.isFlashEnabled),
|
||||
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
||||
fontSize: Number(state.fontSize),
|
||||
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
isPanelPinned: parseBoolean(state.isPanelPinned),
|
||||
isPanelOpen: parseBoolean(state.isPanelOpen),
|
||||
userPatterns: userPatterns,
|
||||
};
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
}
|
||||
:root {
|
||||
--app-height: 100vh;
|
||||
--app-width: 100vw;
|
||||
}
|
||||
|
||||
#console-tab {
|
||||
|
||||
@@ -34,7 +34,6 @@ module.exports = {
|
||||
},
|
||||
spacing: {
|
||||
'app-height': 'var(--app-height)',
|
||||
'app-width': 'var(--app-width)',
|
||||
},
|
||||
typography(theme) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user