Compare commits

...

57 Commits

Author SHA1 Message Date
Felix Roos 4007e952ac move prettier plugin to separate file + add alt shift f 2024-12-16 22:36:49 +01:00
jack-arms 1873377f60 pnpm run codeformat 2024-12-03 16:19:13 -08:00
Jack Armstrong 3747c5b390 add formatting shortcut to use prettier 2024-11-23 18:48:20 -08:00
Felix Roos 726abf76ce Merge pull request #1214 from bthj/main
REPL: solo and sync configuration
2024-11-20 16:32:51 +01:00
Björn Þór Jónsson a7d74ed6d6 REPL: solo and sync configuration; codeformat 2024-11-20 16:28:07 +01:00
Björn Þór Jónsson b913e1c50d REPL: solo and sync configuration 2024-11-09 15:13:24 +01:00
Alex McLean ae335ac40c Add s_zip for 'cat'-ing patterns together step-by-step (#1208)
Fix tactus returned by 'step'
Fix name of toplevel s_extend (was 's_cycles')
2024-10-30 21:29:43 +01:00
Alex McLean 9ecad810b1 Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends (#1205)
* separate pick methods into their own module
* preserve tactus in degrade and friends
2024-10-30 21:28:31 +01:00
Felix Roos f18cf76dde Merge pull request #1206 from ChinoUkaegbu/chore/edit-setup-instructions
chore: Edit run locally instructions in README.md
2024-10-30 17:11:52 +01:00
ChinoUkaegbu 08c918f13e edit run locally instructions in README.md 2024-10-29 11:20:05 +01:00
Felix Roos 2e72c7f4ed Merge pull request #1203 from tidalcycles/console-colors
colorize console + tweak header
2024-10-23 23:08:14 +02:00
Felix Roos 614f393a5d more cleanup 2024-10-23 23:05:17 +02:00
Felix Roos 75f7f4deac cleanup 2024-10-23 23:03:48 +02:00
Felix Roos 2a67698616 - remove header icons (except play/stop button)
- use unicode spiral instead of emoji spiral
- fix header height on mobile
2024-10-23 02:29:23 +02:00
Felix Roos 801879bddf use fontFamily + fontSize from settings in console 2024-10-23 01:17:18 +02:00
Felix Roos 7d8984b706 fix: lint 2024-10-23 01:11:03 +02:00
Felix Roos 1a2847d953 - move console logic to console tab
- colorize logs when color is set
2024-10-23 01:08:32 +02:00
Felix Roos e71db24073 Merge pull request #1202 from tidalcycles/markcss
markcss
2024-10-22 22:54:59 +02:00
Felix Roos f61de0c47c use color inherit in monochrome themes
(to be able to set color in markcss)
2024-10-22 22:48:36 +02:00
Felix Roos 53b8bbb732 snapshot + add markcss in test runtime 2024-10-22 22:27:57 +02:00
Felix Roos aeaf447142 add doc 2024-10-22 22:27:14 +02:00
Felix Roos 1e6e0a61b3 feat: markcss 2024-10-22 22:23:48 +02:00
Felix Roos dd696ba663 Merge pull request #1200 from tidalcycles/more-settings
add 2 new ui settings
2024-10-21 22:56:37 +02:00
Felix Roos be4c8cbdce hide docs link when embedded 2024-10-21 22:47:05 +02:00
Felix Roos b6b54a8553 add 2 new settings
- disable css animations
- hide buttons
- add docs link to left (to use when buttons are hidden)
2024-10-21 21:50:40 +02:00
Jade (Rose) Rowland 1d05051fd6 Merge pull request #1199 from tidalcycles/origin/daslyfe/panel_fixes
Make panel hover behavior optional
2024-10-21 11:22:52 -07:00
Jade (Rose) Rowland 6bca8e2a2f Merge branch 'origin/daslyfe/panel_fixes' of github.com:tidalcycles/strudel into origin/daslyfe/panel_fixes 2024-10-21 11:18:35 -07:00
Jade (Rose) Rowland 630ba62cc3 prettier 2024-10-21 11:18:08 -07:00
Jade (Rose) Rowland 8ac98262bc Delete website/.astro/types.d.ts 2024-10-21 11:17:00 -07:00
Jade (Rose) Rowland 6057e9ada1 Delete website/.astro/astro/content.d.ts 2024-10-21 11:16:40 -07:00
Jade (Rose) Rowland 94e8b18ffa remove astro 2024-10-21 11:15:37 -07:00
Jade (Rose) Rowland 2ec20c4ed4 gitignore 2024-10-21 08:39:11 -07:00
Jade (Rose) Rowland c4fe2c7497 fix horizontal button 2024-10-20 23:33:02 -07:00
Jade (Rose) Rowland df2d28f0e5 panel options 2024-10-20 21:54:33 -07:00
Felix Roos d9bd6f9e7d hotfix: alt+dot breakage 2024-10-20 21:04:18 +02:00
Felix Roos 1e0cc2ab17 hotfix: ignore .astro folder + delete files from git 2024-10-20 20:45:49 +02:00
Jade (Rose) Rowland 835c7b6879 Merge pull request #1193 from daslyfe/orgin/daslyfe/feat/pinmenu
Menu Panel Improvements!
2024-10-18 22:28:35 -04:00
Felix Roos 721e7f06b8 Merge pull request #1198 from tidalcycles/patchday2
update lockfile + minor versions
2024-10-19 01:10:06 +02:00
Felix Roos a46bbeaa10 fix: format + ignore samples 2024-10-19 01:05:23 +02:00
Felix Roos e3a3c0c5f9 Revert "failing to upgrade eslint"
This reverts commit 67964c7e23.
2024-10-19 00:56:55 +02:00
Felix Roos 67964c7e23 failing to upgrade eslint 2024-10-19 00:31:52 +02:00
Felix Roos 9e5f9d37ef update vitest from 1 to 2 2024-10-18 23:45:54 +02:00
Felix Roos 0c1234a35f update minor versions 2024-10-18 23:38:35 +02:00
Felix Roos c6cc487fc6 update lockfile version from 6 to 9 2024-10-18 23:35:19 +02:00
Jade (Rose) Rowland dd171f8320 panel position height dependent 2024-10-15 13:33:37 -04:00
Jade (Rose) Rowland c22c786cf2 welcome width 2024-10-14 15:19:16 -04:00
Jade (Rose) Rowland c191471e1e better width calc 2024-10-14 15:12:32 -04:00
Jade (Rose) Rowland 0d0cb3fd93 fix: tiny vertical menu 2024-10-14 15:05:10 -04:00
Jade (Rose) Rowland bb4b7dc533 fix: vertical breakpoints on mobile 2024-10-14 15:01:05 -04:00
Jade (Rose) Rowland 8440fa42ea remove unessecary hover logic 2024-10-14 13:51:14 -04:00
Jade (Rose) Rowland 61f070744a fix: lint 2024-10-14 13:09:24 -04:00
Jade (Rose) Rowland 2e6a561184 remove spillover 2024-10-14 13:08:39 -04:00
Jade (Rose) Rowland 8703d684c4 fix: lint 2024-10-14 13:01:02 -04:00
Jade (Rose) Rowland b0d2115f43 feat: mobile responsiveness 2024-10-14 12:45:55 -04:00
Jade (Rose) Rowland 96802a271c feat: menu pinning 2024-10-13 23:24:54 -04:00
Jade (Rose) Rowland a91f4576f3 Merge branch 'main' of github.com:daslyfe/strudel 2024-10-13 00:01:12 -04:00
Jade (Rose) Rowland 07bcaa65a9 create meta params 2024-09-06 00:16:30 -04:00
46 changed files with 12307 additions and 10075 deletions
+1
View File
@@ -23,3 +23,4 @@ reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/hs2js/src/hs2js.mjs
samples
+10 -4
View File
@@ -14,10 +14,16 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
pnpm i
pnpm dev
```
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
```
## Using Strudel In Your Project
+7 -7
View File
@@ -56,18 +56,18 @@
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"@vitest/ui": "^2.1.3",
"acorn": "^8.13.0",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.31.0",
"events": "^3.3.0",
"jsdoc": "^4.0.2",
"jsdoc": "^4.0.3",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.0.1",
"prettier": "^3.1.1",
"lerna": "^8.1.8",
"prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+25 -12
View File
@@ -13,7 +13,7 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, repl } from '@strudel/core';
import { repl, registerControl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
@@ -24,6 +24,7 @@ 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 : []),
@@ -77,6 +78,7 @@ 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),
@@ -99,10 +101,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
preventDefault: true,
run: () => onStop?.(),
},
/* {
key: 'Ctrl-Shift-.',
@@ -135,6 +135,7 @@ export class StrudelMirror {
autodraw,
prebake,
bgFill = true,
solo = true,
...replOptions
} = options;
this.code = initialCode;
@@ -145,6 +146,7 @@ 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));
@@ -161,12 +163,14 @@ export class StrudelMirror {
replOptions?.onToggle?.(started);
if (started) {
this.drawer.start(this.repl.scheduler);
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
if (this.solo) {
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
}
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
@@ -221,7 +225,7 @@ export class StrudelMirror {
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (e.detail !== this.id) {
if (this.solo && e.detail !== this.id) {
this.stop();
}
};
@@ -356,3 +360,12 @@ 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');
+2 -1
View File
@@ -93,6 +93,7 @@ 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');
@@ -114,7 +115,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style: `outline: solid 2px ${color}` },
attributes: { style },
}),
);
}
+2 -1
View File
@@ -49,7 +49,8 @@
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
"nanostores": "^0.9.5",
"prettier": "^3.3.3"
},
"devDependencies": {
"vite": "^5.0.10"
+55
View File
@@ -0,0 +1,55 @@
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
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ 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' },
{ 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' },
],
});
+20 -20
View File
@@ -15,25 +15,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
{ 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' },
],
});
+20 -20
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
{ 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' },
],
});
+19 -19
View File
@@ -14,24 +14,24 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
{ 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' },
],
});
+19 -19
View File
@@ -16,24 +16,24 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ 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' },
{ 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' },
],
});
+1
View File
@@ -14,6 +14,7 @@ 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';
+1 -1
View File
@@ -36,6 +36,6 @@
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+9 -2
View File
@@ -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));
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
});
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_cycles = stepRegister('s_extend', function (factor, pat) {
export const s_extend = stepRegister('s_extend', function (factor, pat) {
return pat.fast(factor).s_expand(factor);
});
@@ -2882,6 +2882,13 @@ 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
+214
View File
@@ -0,0 +1,214 @@
/*
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();
};
+29 -221
View File
@@ -1,13 +1,13 @@
/*
signal.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
signal.mjs - continuous 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 { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs';
import { id, _mod } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -253,211 +253,6 @@ 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) {
@@ -596,8 +391,11 @@ 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)),
export const degradeByWith = register(
'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
true,
true,
);
/**
@@ -614,9 +412,14 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
* @example
* s("[hh?0.2]*8")
*/
export const degradeBy = register('degradeBy', function (x, pat) {
return pat._degradeByWith(rand, x);
});
export const degradeBy = register(
'degradeBy',
function (x, pat) {
return pat._degradeByWith(rand, x);
},
true,
true,
);
/**
*
@@ -630,7 +433,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
* @example
* s("[hh?]*8")
*/
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
/**
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
@@ -650,12 +453,17 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* x => x.undegradeBy(0.8).pan(1)
* )
*/
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
});
export const undegradeBy = register(
'undegradeBy',
function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
},
true,
true,
);
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
@@ -672,7 +480,7 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
/**
*
+1 -1
View File
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+4
View File
@@ -10,6 +10,8 @@ if (typeof HTMLElement !== 'undefined') {
static observedAttributes = ['code'];
settings = codemirrorSettings.get();
editor = null;
sync = false;
solo = true;
constructor() {
super();
}
@@ -49,6 +51,8 @@ if (typeof HTMLElement !== 'undefined') {
});
this.dispatchEvent(event);
},
solo: this.solo,
sync: this.sync,
});
// init settings
this.editor.updateSettings(this.settings);
+1 -1
View File
@@ -37,6 +37,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+1 -1
View File
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+1 -1
View File
@@ -34,6 +34,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+11448 -9230
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -4551,6 +4551,27 @@ 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 ]",
+3
View File
@@ -134,6 +134,9 @@ strudel.Pattern.prototype._pitchwheel = function () {
strudel.Pattern.prototype._pianoroll = function () {
return this;
};
strudel.Pattern.prototype.markcss = function () {
return this;
};
const uiHelpersMocked = {
backgroundImage: id,
-5
View File
@@ -3,11 +3,6 @@ import { configDefaults, defineConfig } from 'vitest/config';
/// <reference types="vitest" />
export default defineConfig({
test: {
// ...
deps: {
registerNodeLoader: true,
},
threads: false,
reporters: 'verbose',
isolate: false,
silent: true,
-5
View File
@@ -1,5 +0,0 @@
{
"devToolbar": {
"enabled": false
}
}
-290
View File
@@ -1,290 +0,0 @@
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
View File
@@ -1,3 +1,4 @@
.astro
# build output
dist/
+5 -5
View File
@@ -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">
<a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
<div class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span>🌀</span>
<div class="flex space-x-1 items-baseline">
<span class="block rotate-90 text-blue-500">꩜</span>
<div class="flex space-x-2 items-baseline">
<span class="">strudel</span>
<span class="text-sm">DOCS</span>
<span class="text-sm font-medium">DOCS</span>
</div>
</h1>
</a>
</div>
</div>
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
<div class="search-item h-10">
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel } 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} />
<Panel context={context} />
<HorizontalPanel context={context} />
</div>
);
}
+1 -2
View File
@@ -1,7 +1,6 @@
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';
@@ -37,7 +36,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,3 +98,7 @@ 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} />
+25 -24
View File
@@ -1,8 +1,4 @@
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';
@@ -15,24 +11,19 @@ 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 } = useSettings();
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
return (
<header
id="header"
className={cx(
'flex-none text-black z-[100] text-lg select-none',
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
!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', ''));
@@ -43,31 +34,46 @@ export function Header({ context, embedded = false }) {
)}
>
<div
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer', isZen && 'fixed top-2 right-4')}
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
🌀
<span className="block rotate-90">꩜</span>
</div>
{!isZen && (
<div className={cx(started && 'animate-pulse')}>
<span className="">strudel</span> <span className="text-sm">REPL</span>
<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>
)}
</h1>
</div>
{!isZen && (
<div className="flex max-w-full overflow-auto text-foreground">
{!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
className={cx(
!isEmbedded ? 'p-2' : 'px-2',
'hover:opacity-50',
!started && !isCSSAnimationDisabled && 'animate-pulse',
)}
>
{!pending ? (
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
<span className={cx('flex items-center space-x-2')}>
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
@@ -84,8 +90,6 @@ 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 && (
@@ -94,7 +98,6 @@ 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>
)}
@@ -107,7 +110,6 @@ export function Header({ context, embedded = false }) {
)}
onClick={handleShare}
>
<LinkIcon className="w-6 h-6" />
<span>share</span>
</button>
)}
@@ -117,7 +119,6 @@ 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>
)}
+4 -4
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel, VerticalPanel } 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 } = settings;
const { panelPosition, isZen } = 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} />
{panelPosition === 'right' && <Panel context={context} />}
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{panelPosition === 'bottom' && <Panel context={context} />}
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
</div>
);
}
@@ -1,18 +1,48 @@
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({ log }) {
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);
});
}, []),
);
return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}</pre>
<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 }}
>
{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')}>
<div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
@@ -42,3 +72,7 @@ function linkify(inputText) {
return replacedText;
}
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -6,6 +6,7 @@ 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',
+192 -108
View File
@@ -1,10 +1,5 @@
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 { nanoid } from 'nanoid';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
import { setActiveFooter, useSettings } from '../../../settings.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -12,117 +7,206 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import useClient from '@src/useClient.mjs';
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function Panel({ context }) {
const footerContent = useRef();
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelOpen, activeFooter: tab } = settings;
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]);
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);
});
}, []),
);
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 (
<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>
<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>
)}
<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>
);
}
export function VerticalPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
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} />
</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 useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
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>
);
}
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50" onClick={onClick} title={label}>
<button className="hover:opacity-50 text-nowrap" 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 pb-4 flex flex-col overflow-hidden max-h-full">
<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">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
</div>
)}
<section className="flex overflow-y-scroll max-h-full flex-col">
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
+24 -19
View File
@@ -25,32 +25,37 @@ export function Reference() {
}, [search]);
return (
<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">
<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">
<input
className="w-full p-1 bg-background rounded-md"
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</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 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>
</div>
<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">
<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 ">
<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,6 +94,8 @@ export function SettingsTab({ started }) {
isAutoCompletionEnabled,
isTooltipEnabled,
isFlashEnabled,
isButtonRowHidden,
isCSSAnimationDisabled,
isSyncEnabled,
isLineWrappingEnabled,
fontSize,
@@ -101,11 +103,12 @@ 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">
<div className="text-foreground p-4 space-y-4 w-full">
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -170,7 +173,32 @@ export function SettingsTab({ started }) {
items={{ bottom: 'Bottom', right: 'Right' }}
></ButtonGroup>
</FormItem>
<FormItem label="Code Settings">
<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">
<Checkbox
label="Enable bracket matching"
onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)}
@@ -230,6 +258,16 @@ 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">
+11 -11
View File
@@ -1,7 +1,7 @@
import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
import React, { useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
@@ -52,15 +52,14 @@ export function SoundsTab() {
});
return (
<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 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 className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
@@ -74,7 +73,8 @@ 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">
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -5,10 +5,8 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
return (
<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>
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3>꩜ 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:
@@ -30,7 +28,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">
+12
View File
@@ -30,10 +30,16 @@ 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;
@@ -66,17 +72,23 @@ 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);
+1
View File
@@ -52,6 +52,7 @@
}
:root {
--app-height: 100vh;
--app-width: 100vw;
}
#console-tab {
+1
View File
@@ -34,6 +34,7 @@ module.exports = {
},
spacing: {
'app-height': 'var(--app-height)',
'app-width': 'var(--app-width)',
},
typography(theme) {
return {