Compare commits

...

86 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 90c05ec38a fix edge cases 2026-02-08 14:12:21 -05:00
Jade (Rose) Rowland 7e0eed87cb normalize shortcuts 2026-01-27 17:21:36 -05:00
Jade (Rose) Rowland 1a1197f67a Merge branch 'main' into soloshortcut 2026-01-22 21:11:31 -05:00
froos 28db6d1fd4 Merge pull request 'clean up crumbs of crunchy design' (#1942) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1942
2026-01-22 22:15:01 +01:00
Aria dc9cadc979 Merge pull request 'Feat: Make default alignment configurable' (#1941) from glossing/alignment into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1941
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2026-01-22 21:50:28 +01:00
Felix Roos f99b13efc2 fix: zen mode lost border 2026-01-22 21:46:48 +01:00
Aria 0001d9c27b Change name to setDefaultJoin 2026-01-22 14:24:45 -06:00
Aria d79d122f68 Add docstring and example 2026-01-22 14:08:26 -06:00
Aria 6e61f3cdb6 Make default alignment configurable 2026-01-22 13:52:57 -06:00
Felix Roos 4993ec4cbf fix: console scroll to bottom on change + more logs + dont wrap 2026-01-22 20:40:35 +01:00
Felix Roos 8e5ca57edd cleanup + pending state 2026-01-22 20:29:03 +01:00
Felix Roos 107a3acd3a rename panel components + fix borders 2026-01-22 20:24:04 +01:00
Felix Roos 1d887e81ef fix: h-full 2026-01-22 20:18:59 +01:00
froos 7f2860eb14 Merge pull request 'crunch up layout' (#1938) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1938
2026-01-22 10:28:08 +01:00
Felix Roos 3364b9f05f keep it simple stupid 2026-01-22 10:12:16 +01:00
Jade (Rose) Rowland 3c5afb32f3 scroll to center 2026-01-22 02:04:27 -05:00
Jade (Rose) Rowland 76cbd23859 Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut 2026-01-22 00:25:41 -05:00
Jade (Rose) Rowland b436ae789c rm dead code 2026-01-22 00:25:28 -05:00
Switch Angel AKA Jade Rose 275731afc7 Merge branch 'main' into soloshortcut 2026-01-22 06:22:17 +01:00
Jade (Rose) Rowland 5cb2214d88 working 2026-01-22 00:20:44 -05:00
Felix Roos f9ed71fbf8 fix: import sounds font size 2026-01-22 01:35:19 +01:00
Felix Roos e9ea0bc7cc fix: typo 2026-01-22 01:32:04 +01:00
Felix Roos a87daf3d21 fix: patterns tab performance 2026-01-22 01:31:23 +01:00
Felix Roos 784b9c08ec copmuted settings 2026-01-22 01:16:57 +01:00
Felix Roos 139b5376fc rebrand hide button row to hide footer 2026-01-22 01:16:22 +01:00
Felix Roos a052def314 piping hot (main menu in footer) + border around panel toggle 2026-01-22 00:35:04 +01:00
Felix Roos bcce1fa68a make move sidepanel one layer up, so tabs are at the top and the toggle doesn't float 2026-01-21 23:38:25 +01:00
Felix Roos 9a10cac8e9 fix bottom panel 2026-01-21 23:07:49 +01:00
Felix Roos cf999462b7 fix: export tab placeholder color 2026-01-21 23:02:14 +01:00
Felix Roos 9fc084b291 tweaks: consistent font sizes and row heights, persist selected reference tag, add wrap prop to ButtonGroup 2026-01-21 22:47:15 +01:00
Felix Roos cb46dd7840 make it crunchier: border borders, less padding 2026-01-21 01:31:25 +01:00
froos b684e183a0 Merge pull request 'add muted theme color + tweak pane styles' (#1936) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1936
2026-01-21 00:27:43 +01:00
Felix Roos 84e4dde3d7 mini tweaks 2026-01-21 00:15:00 +01:00
Felix Roos 5f8d028d4f cleanup 2026-01-21 00:04:18 +01:00
Felix Roos e3abdd41c0 make panel toggle floating 2026-01-21 00:03:19 +01:00
Felix Roos bce6ffde08 cleanup + tweak bottom panel style 2026-01-20 23:36:09 +01:00
Felix Roos 35a3e32238 no bg on button group 2026-01-20 23:08:31 +01:00
Felix Roos 60b5024f63 remove panel hover + optimize bottom panel 2026-01-20 23:06:04 +01:00
Felix Roos 4f14ff00d5 responsiveness 2026-01-20 22:29:16 +01:00
Felix Roos 3d3130e880 fine tune 2026-01-20 22:19:32 +01:00
Felix Roos 83d542d06a proper strudel icon + toggle menu from top bar 2026-01-20 22:13:03 +01:00
Aria 6105d5027d Merge pull request 'Feat: Add retrig option for LFOs' (#1934) from glossing/lfo-retrig into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1934
2026-01-20 04:37:19 +01:00
Felix Roos 7e34069021 fix more font sizes 2026-01-20 01:06:07 +01:00
Felix Roos b75ca193a2 fix: font sizes 2026-01-20 01:01:20 +01:00
Felix Roos afac1f62ca fix: adapt color-theme attribute on root based on theme 2026-01-20 00:59:43 +01:00
Felix Roos de83805fd9 add muted theme color + use it for inputs and tags 2026-01-20 00:06:44 +01:00
Felix Roos 38b7bcb3d2 style tweaks 2026-01-19 23:00:14 +01:00
Aria 62bf9abed3 Add retrig option for LFOs 2026-01-19 09:52:49 -08:00
froos 8ec1d4d034 Merge pull request 'fix: move kabelsalat web to superdough' (#1932) from fix-kabelsalat-dependency into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1932
2026-01-18 22:27:45 +01:00
Felix Roos 6981638f70 fix: move kabelsalat web to superdough 2026-01-18 22:04:49 +01:00
froos f5823ce981 Merge pull request 'Add tags to reference' (#1645) from vvolhejn/strudel:radical-new-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1645
2026-01-18 12:36:49 +01:00
Václav Volhejn f8750901f0 Tag the remaining functions 2026-01-18 12:16:50 +01:00
Václav Volhejn c1fd8c82c6 Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-18 12:08:17 +01:00
Václav Volhejn 6e36bdfff7 Add tags for some of the new functions 2026-01-18 12:08:12 +01:00
Felix Roos 8dbdd0a9b8 Merge branch 'main' into radical-new-docs 2026-01-18 10:39:53 +01:00
froos f4c0281613 Merge pull request 'fix: performance issues when reference is open' (#1926) from robojumper/strudel:fix/reference-performance-issue into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1926
2026-01-18 10:36:00 +01:00
Václav Volhejn 99cf256ee2 Clean up 2 2026-01-17 20:11:26 +01:00
Václav Volhejn 42d0bfadce Clean up 2026-01-17 20:08:23 +01:00
Václav Volhejn 22f78fbb2f Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-17 19:24:41 +01:00
Václav Volhejn 77076a1c3e Improve reference search/filtering semantics 2026-01-17 19:12:23 +01:00
Václav Volhejn a1d31df556 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-17 18:22:56 +01:00
robojumper f9d0e154e8 fix: performance issues when reference is open 2026-01-17 17:51:32 +01:00
Felix Roos 9ffd0e496f fix: all doc 2026-01-11 12:33:03 +01:00
Felix Roos 550d7ff6cb fix: react error 2026-01-11 12:32:54 +01:00
Václav Volhejn 726e5ef14a Fix formatting 2026-01-10 07:18:29 -08:00
Václav Volhejn cc15e3cd36 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-10 06:58:39 -08:00
Václav Volhejn 2fa8ed1424 Revert seq synonym refactor 2025-11-09 20:21:19 +01:00
Václav Volhejn b7827cb89f Tag new functions 2025-11-01 21:11:57 +01:00
Václav Volhejn 29fd541f1f Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2025-11-01 20:20:07 +01:00
Václav Volhejn 0830a9df69 Add superdirt tags 2025-11-01 20:19:57 +01:00
froos 4159c8394d Merge branch 'main' into radical-new-docs 2025-10-28 22:02:10 +01:00
Václav Volhejn 466bf819d8 Fix formatting (trailing space) 2025-10-19 21:38:46 +02:00
Václav Volhejn 7e4cbefc8f Tag crossfade as internals 2025-10-19 21:22:57 +02:00
Václav Volhejn 2483c673a4 Effects -> fx, untag "crossfade" 2025-10-19 21:19:42 +02:00
Václav Volhejn 69360b3ec8 Also tag "noise" 2025-10-19 21:17:25 +02:00
Václav Volhejn 96d0f2053e Add supradough controls 2025-10-19 21:13:14 +02:00
Václav Volhejn b952e4c735 Add superdough tags 2025-10-19 20:54:22 +02:00
Václav Volhejn a4212589e5 Recategorize 2025-10-12 16:33:07 +02:00
Václav Volhejn ee1a894f3b Show tags in reference, more compact functions list 2025-10-12 16:28:05 +02:00
Václav Volhejn c62428ca27 Document remaining functions 2025-10-12 16:07:11 +02:00
Václav Volhejn d516d765d5 Redo frontend for tags 2025-10-11 14:12:45 +02:00
Václav Volhejn 5032b4e966 Rename group -> tags 2025-10-11 14:11:50 +02:00
Václav Volhejn a4d7803d43 Annotate controls.mjs 2025-10-02 21:59:45 +02:00
Václav Volhejn 4a395f372d Annote pattern.mjs and signal.mjs 2025-10-01 23:45:48 +02:00
Václav Volhejn c754dab1ef Order groups 2025-10-01 22:31:03 +02:00
Václav Volhejn df54404733 Group by category 2025-10-01 22:21:03 +02:00
106 changed files with 1607 additions and 648 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}
+7
View File
@@ -12,6 +12,13 @@ function defineTags(dictionary) {
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
},
});
dictionary.defineTag('tags', {
mustHaveValue: true,
onTagged: function (doclet, tag) {
doclet.tags = tag.value.split(/[ ,]+/);
},
});
}
module.exports = { defineTags: defineTags };
+76 -3
View File
@@ -24,7 +24,12 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import {
deleteAllInlineBeforeCharacter,
InsertCharBeforeChar,
jumpToCharacter,
jumpToNextCharacter,
} from './labelJump.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -74,6 +79,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
decode: JSON.parse,
});
const ANON_LABEL = '$';
const SOLO_LABEL = 'S';
const MUTE_LABEL = '_';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
const settings = codemirrorSettings.get();
@@ -122,12 +131,75 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1),
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
},
{
key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1),
run: (view) => {
return jumpToNextCharacter(view, ANON_LABEL, -1);
},
},
// clear all muted
{
key: `Alt-Ctrl-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
},
},
// clear all solod
{
key: `Alt-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
},
},
// clear all solo and mute
{
key: `Ctrl-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
},
},
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-${num}`,
run: (view) => {
return jumpToCharacter(view, ANON_LABEL, i);
},
};
}),
// handle solo toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
},
};
}),
// handle mute toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Ctrl-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
},
};
}),
// Handle clearing mutes and solos 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Ctrl-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL,'', i);
},
};
}),
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
@@ -434,6 +506,7 @@ function s4() {
/**
* Overrides the css of highlighted events. Make sure to use single quotes!
* @name markcss
* @tag visualization
* @example
* note("c a f e")
* .markcss('text-decoration:underline')
+133 -11
View File
@@ -1,18 +1,54 @@
import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
import { EditorView } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
export function jumpToCharacter(view, character, direction = 1) {
/**
* gets all of the positions of a character in a document, excluding commented out lines
* @param { EditorState} state
* @param {String} character
* @returns {number[]}
*/
function getCharacterPositions(state, character) {
const cursor = new SearchCursor(state.doc, character);
const characterPositions = [];
while (!cursor.next().done) {
const linestartpos = state.doc.lineAt(cursor.value.to).from
if (!isLineCommentedOut(state, linestartpos)) {
characterPositions.push(cursor.value.to);
}
}
return characterPositions;
}
function isLineCommentedOut(state, pos) {
const line = state.doc.lineAt(pos);
// remove white space
pos = line.from + line.text.search(/\S/)
const tree = syntaxTree(state);
const node = tree.resolveInner(pos, 1)
return node.name.includes("Comment")
}
/**
* jump to the next character in a document
* @param {EditorView} view
* @param {String} character
* @param {number} direction 0 or 1
* @returns {boolean}
*/
export function jumpToNextCharacter(view, character, direction = 1) {
const { state, dispatch } = view;
const pos = state.selection.main.head;
const cursor = new SearchCursor(state.doc, character);
let characterPositions = [];
let jumpPos;
while (!cursor.next().done) {
characterPositions.push(cursor.value.to);
}
const characterPositions = getCharacterPositions(state, character);
if (!characterPositions.length) {
return false;
return true;
}
if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
}
if (jumpPos == null) {
return false;
return true;
}
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({
selection: EditorSelection.cursor(jumpPos - 1),
scrollIntoView: true,
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {number} index the instance of the character
* @returns
*/
export function jumpToCharacter(view, character, index) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
if (pos == null) {
return true;
}
const selection = EditorSelection.cursor(pos - 1);
dispatch({
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @returns {true}
*/
export function deleteAllInlineBeforeCharacter(view, character) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const changes = [];
characterPositions.forEach((pos) => {
const line = state.doc.lineAt(pos);
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
return;
}
changes.push({
from: line.from,
to: pos - 1,
insert: '',
});
});
dispatch({ changes });
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {String} character2
* @param {number} index
* @returns
*/
export function InsertCharBeforeChar(view, character, character2, index) {
const { state, dispatch } = view;
const changes = [];
const characterPositions = getCharacterPositions(state, character);
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
const line = state.doc.lineAt(labelpos);
//delete preceeding characters
changes.push({
from: line.from,
to: labelpos - 1,
insert: '',
});
changes.push({
insert: character2,
from: line.from,
});
dispatch({ changes });
return true;
}
+1
View File
@@ -117,6 +117,7 @@ export const sliderPlugin = ViewPlugin.fromClass(
* Displays a slider widget to allow the user manipulate a value
*
* @name slider
* @tags external_io, visualization
* @param {number} value Initial value
* @param {number} min Minimum value - optional, defaults to 0
* @param {number} max Maximum value - optional, defaults to 1
+1
View File
@@ -198,6 +198,7 @@ export function activateTheme(name) {
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
color-scheme: ${themeSettings.light ? 'light' : 'dark'};
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
+3 -1
View File
@@ -17,13 +17,15 @@ export const settings = {
background: 'white',
lineBackground: 'transparent',
foreground: deepPurple,
muted: deepPurple + '50',
caret: '#797977',
selection: yellowPink,
selectionMatch: '#2B323D',
gutterBackground: grey,
gutterBackground: pinkAccent,
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
light: true,
};
export default createTheme({
+1
View File
@@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B'];
export const settings = {
background: palette[3],
foreground: palette[1],
muted: palette[1] + '50',
caret: palette[0],
selection: palette[0],
selectionMatch: palette[1],
+2
View File
@@ -8,6 +8,7 @@ export const settings = {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#282b2e',
foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00',
selection: '#4e5254',
selectionMatch: '#4e5254',
+1
View File
@@ -12,6 +12,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[1],
muted: hex[2],
selection: hex[2],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#272C35',
lineBackground: '#272C3599',
foreground: 'hsl(220, 14%, 71%)',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#272C35',
foreground: '#9d9b97',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#3d4c64',
selectionMatch: '#3d4c64',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#21202e',
foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff',
selection: '#5a51898f',
selectionMatch: '#5a51898f',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#FFFFFF',
foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
+2 -1
View File
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
foreground: 'white',
muted: '#ffffff50',
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
+2 -1
View File
@@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
foreground: 'white',
muted: '#ffffff50',
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
+1
View File
@@ -11,6 +11,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[3],
selection: hex[3],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -9,6 +9,7 @@ export const settings = {
background: '#242424',
lineBackground: '#24242499',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#242424',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -27,6 +28,7 @@ export default createTheme({
settings: {
background: '#282a36',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
+2
View File
@@ -10,6 +10,7 @@ export const settings = {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#eeebff',
muted: '#eeebff50',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
@@ -22,6 +23,7 @@ export default createTheme({
settings: {
background: '#2a2734',
foreground: '#6c6783',
muted: '#eeebff50',
caret: '#ffad5c',
selection: '#9a86fd',
selectionMatch: '#9a86fd',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
+1
View File
@@ -23,6 +23,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[10],
muted: hex[7],
selection: hex[8],
selectionMatch: hex[0],
gutterBackground: hex[3],
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
@@ -16,6 +17,7 @@ export default createTheme({
settings: {
background: '#0d1117',
foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -17,6 +18,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -10,6 +10,7 @@ export const settings = {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#282828',
foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#b99d555c',
selectionMatch: '#b99d555c',
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#fbf1c7',
foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03',
selection: '#bdae9391',
selectionMatch: '#bdae9391',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#212121',
lineBackground: '#21212199',
foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#212121',
foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f063',
selectionMatch: '#d7d4f063',
+3 -1
View File
@@ -5,7 +5,8 @@ export const settings = {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
foreground: '#6182B8',
muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#FAFAFA',
foreground: '#90A4AE',
muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#80CBC440',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#272822',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#f2f1f8',
foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#2e3440',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#00000073',
selectionMatch: '#00000073',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
muted: '#93a1a150',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
@@ -45,6 +46,7 @@ export default createTheme({
settings: {
background: c.background,
foreground: c.foreground,
muted: c.muted,
caret: c.cursor,
selection: c.selection,
selectionMatch: c.selection,
+2
View File
@@ -8,6 +8,7 @@ export const settings = {
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
muted: '#657b8350',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
@@ -19,6 +20,7 @@ export const settings = {
const c = {
background: '#FDF6E3',
foreground: '#657B83',
muted: '#657b8350',
selection: '#EEE8D5',
selectionMatch: '#EEE8D5',
cursor: '#657B83',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
muted: hex[6],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
+1
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
muted: '#8a919966',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#303841',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
+2 -1
View File
@@ -9,7 +9,8 @@ let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
foreground: colorA,
muted: colorA + '50',
caret: colorC,
selection: colorD,
selectionMatch: colorA,
+4 -2
View File
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#24283b',
foreground: '#7982a9',
foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#343b5f',
+4 -2
View File
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#1a1b26',
foreground: '#787c99',
foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#e1e2e7',
foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#1e1e1e',
foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#ffffff',
lineBackground: '#ffffff50',
foreground: '#383a42',
muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#ffffff',
foreground: '#383a42',
muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
+2 -1
View File
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
foreground: 'black',
muted: '#00000050',
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
File diff suppressed because it is too large Load Diff
+1
View File
@@ -15,6 +15,7 @@ import Fraction, { gcd } from './fraction.mjs';
* - "-" hold previous value
* - "." silence
*
* @tags visualization
* @param {Pattern} pattern the pattern to use
* @param {number} chars max number of characters (approximately)
* @returns string
+5
View File
@@ -61,6 +61,7 @@ export const bjorklund = function (ons, steps) {
*
* @memberof Pattern
* @name euclid
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @returns Pattern
@@ -73,6 +74,7 @@ export const bjorklund = function (ons, steps) {
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
@@ -156,6 +158,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param rotation offset in steps
@@ -187,6 +190,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* the resulting sequence
* @name euclidLegatoRot
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
@@ -208,6 +212,7 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
-1
View File
@@ -31,7 +31,6 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@kabelsalat/web": "^0.4.1",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
+282 -92
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -28,6 +28,7 @@ const _pick = function (lookup, pat, modulo = true) {
/** * 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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -57,6 +58,7 @@ const __pick = register('pick', function (lookup, pat) {
* 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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -67,6 +69,7 @@ export const pickmod = register('pickmod', function (lookup, pat) {
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @tags combiners, functional
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
@@ -83,6 +86,7 @@ export const pickF = register('pickF', function (lookup, funcs, pat) {
/** * 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.
* @tags combiners
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
@@ -93,6 +97,7 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -103,6 +108,7 @@ export const pickOut = register('pickOut', function (lookup, pat) {
/** * 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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -112,6 +118,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -122,6 +129,7 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
/** * 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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -138,6 +146,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -148,6 +157,7 @@ export const pickReset = register('pickReset', function (lookup, pat) {
/** * 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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -159,6 +169,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
/** 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
* @tags combiners
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
@@ -180,6 +191,7 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -192,6 +204,7 @@ export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSq
/**
* 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
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
+8 -10
View File
@@ -12,7 +12,6 @@ import {
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs';
import { SalatRepl } from '@kabelsalat/web';
export function repl({
defaultOutput,
@@ -31,7 +30,6 @@ export function repl({
id,
mondo = false,
}) {
const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -89,11 +87,6 @@ export function repl({
return silence;
};
const compileKabel = (code) => {
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
@@ -123,6 +116,7 @@ export function repl({
* Changes the global tempo to the given cycles per minute
*
* @name setcpm
* @tags temporal
* @alias setCpm
* @param {number} cpm cycles per minute
* @example
@@ -136,7 +130,9 @@ export function repl({
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
/** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
let allTransforms = [];
/**
* Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* a version that applies the function to each pattern separately.
* ```
* $: sound("bd - cp sd")
@@ -148,18 +144,21 @@ export function repl({
* $: sound("hh*8")
* all(x => x.pianoroll())
* ```
*
* @tags combiners
*/
let allTransforms = [];
const all = function (transform) {
allTransforms.push(transform);
return silence;
};
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
*
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* each(fast("<2 3>"))
* ```
* @tags combiners
*/
const each = function (transform) {
eachTransform = transform;
@@ -215,7 +214,6 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
compileKabel,
});
};
+63
View File
@@ -24,6 +24,7 @@ export const signal = (func) => {
* A sawtooth signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
@@ -38,6 +39,7 @@ export const saw = signal((t) => t % 1);
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const saw2 = saw.toBipolar();
@@ -45,6 +47,7 @@ export const saw2 = saw.toBipolar();
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
*
* @return {Pattern}
* @tags generators
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(isaw.slow(2))
@@ -59,6 +62,7 @@ export const isaw = signal((t) => 1 - (t % 1));
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
*
* @return {Pattern}
* @tags generators
*/
export const isaw2 = isaw.toBipolar();
@@ -66,12 +70,14 @@ export const isaw2 = isaw.toBipolar();
* A sine signal between -1 and 1 (like `sine`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
* A sine signal between 0 and 1.
* @return {Pattern}
* @tags generators
* @example
* n(sine.segment(16).range(0,15))
* .scale("C:minor")
@@ -83,6 +89,7 @@ export const sine = sine2.fromBipolar();
* A cosine signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* n(stack(sine,cosine).segment(16).range(0,15))
* .scale("C:minor")
@@ -94,12 +101,14 @@ export const cosine = sine._early(Fraction(1).div(4));
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const cosine2 = sine2._early(Fraction(1).div(4));
/**
* A square signal between 0 and 1.
* @return {Pattern}
* @tags generators
* @example
* n(square.segment(4).range(0,7)).scale("C:minor")
*
@@ -110,6 +119,7 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
* A square signal between -1 and 1 (like `square`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const square2 = square.toBipolar();
@@ -117,6 +127,7 @@ export const square2 = square.toBipolar();
* A triangle signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* n(tri.segment(8).range(0,7)).scale("C:minor")
*
@@ -127,6 +138,7 @@ export const tri = fastcat(saw, isaw);
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const tri2 = fastcat(saw2, isaw2);
@@ -134,6 +146,7 @@ export const tri2 = fastcat(saw2, isaw2);
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
*
* @return {Pattern}
* @tags generators
* @example
* n(itri.segment(8).range(0,7)).scale("C:minor")
*
@@ -144,6 +157,7 @@ export const itri = fastcat(isaw, saw);
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const itri2 = fastcat(isaw2, saw2);
@@ -151,6 +165,7 @@ export const itri2 = fastcat(isaw2, saw2);
* A signal representing the cycle time.
*
* @return {Pattern}
* @tags generators
*/
export const time = signal(id);
@@ -158,6 +173,7 @@ export const time = signal(id);
* The mouse's x position value ranges from 0 to 1.
* @name mousex
* @return {Pattern}
* @tags external_io
* @example
* n(mousex.segment(4).range(0,7)).scale("C:minor")
*
@@ -167,6 +183,7 @@ export const time = signal(id);
* The mouse's y position value ranges from 0 to 1.
* @name mousey
* @return {Pattern}
* @tags external_io
* @example
* n(mousey.segment(4).range(0,7)).scale("C:minor")
*
@@ -269,6 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => {
* precise RNG, try `useRNG('precise')`.
*
* @name useRNG
* @tags generators, math
* @param {string} mod - Mode. One of 'legacy', 'precise'
* @example
* useRNG('legacy')
@@ -280,6 +298,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
/**
* A discrete pattern of numbers from 0 to n-1
* @tags generators
* @example
* n(run(4)).scale("C4:pentatonic")
* // n("0 1 2 3").scale("C4:pentatonic")
@@ -290,6 +309,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
* Creates a binary pattern from a number.
*
* @name binary
* @tags generators
* @param {number} n - input number to convert to binary
* @example
* "hh".s().struct(binary(5))
@@ -304,6 +324,7 @@ export const binary = (n) => {
* Creates a binary pattern from a number, padded to n bits long.
*
* @name binaryN
* @tags generators
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
* @example
@@ -321,6 +342,7 @@ export const binaryN = (n, nBits = 16) => {
* Creates a binary list pattern from a number.
*
* @name binaryL
* @tags generators
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
@@ -334,6 +356,7 @@ export const binaryL = (n) => {
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @tags generators
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
@@ -353,6 +376,7 @@ export const binaryNL = (n, nBits = 16) => {
* Creates a list of random numbers of the given length
*
* @name randL
* @tags generators
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
@@ -384,6 +408,7 @@ const _rearrangeWith = (ipat, n, pat) => {
* Slices a pattern into the given number of parts, then plays those parts in random order.
* Each part will be played exactly once per cycle.
* @name shuffle
* @tags temporal
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
@@ -397,6 +422,7 @@ export const shuffle = register('shuffle', (n, pat) => {
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
* but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @tags temporal
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
@@ -409,6 +435,7 @@ export const scramble = register('scramble', (n, pat) => {
/**
* Modify a pattern by applying a function to the `randomSeed` control if present
*
* @tags math
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
* @param {Pattern} pat Pattern to update
* @returns Pattern
@@ -428,6 +455,7 @@ export const withSeed = (func, pat) => {
* that use randomness, like `shuffle` and `sometimes`.
*
* @name seed
* @tags math
* @param {number} n A new seed. Can be any number.
* @example
* $: s("hh*4").degrade();
@@ -441,6 +469,7 @@ export const seed = register('seed', (n, pat) => {
* A continuous pattern of random numbers, between 0 and 1.
*
* @name rand
* @tags generators
* @example
* // randomly change the cutoff
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
@@ -449,6 +478,7 @@ export const seed = register('seed', (n, pat) => {
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
/**
* A continuous pattern of random numbers, between -1 and 1
* @tags generators
*/
export const rand2 = rand.toBipolar();
@@ -458,6 +488,7 @@ export const _brandBy = (p) => rand.fmap((x) => x < p);
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @tags generators
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
@@ -468,6 +499,7 @@ export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @tags generators
* @example
* s("hh*10").pan(brand)
*/
@@ -479,6 +511,7 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* A continuous pattern of random integers, between 0 and n-1.
*
* @name irand
* @tags generators
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
@@ -501,6 +534,7 @@ export const __chooseWith = (pat, xs) => {
/**
* Choose from the list of values (or patterns of values) using the given
* pattern of numbers, which should be in the range of 0..1
* @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -514,6 +548,7 @@ export const chooseWith = (pat, xs) => {
/**
* As with {chooseWith}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
* @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -524,6 +559,7 @@ export const chooseInWith = (pat, xs) => {
/**
* Chooses randomly from the given list of elements.
* @tags temporal
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
@@ -539,6 +575,7 @@ export const chooseOut = choose;
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -549,6 +586,7 @@ Pattern.prototype.choose = function (...xs) {
/**
* As with choose, but the pattern that this method is called on should be
* in the range -1 .. 1
* @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -558,6 +596,7 @@ Pattern.prototype.choose2 = function (...xs) {
/**
* Picks one of the elements at random each cycle.
* @tags temporal
* @synonyms randcat
* @returns {Pattern}
* @example
@@ -600,6 +639,7 @@ const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @tags temporal
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
@@ -609,6 +649,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
* @tags temporal
* @synonyms wrandcat
* @returns {Pattern}
* @example
@@ -652,6 +693,7 @@ function _berlin(t, seed = 0) {
/**
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
*
* @tags generators
* @name perlin
* @example
* // randomly change the cutoff
@@ -664,6 +706,7 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
* like perlin noise but with sawtooth waves), in the range 0..1.
*
* @tags generators
* @name berlin
* @example
* // ascending arpeggios
@@ -684,6 +727,7 @@ export const degradeByWith = register(
* 0 = 0% chance of removal
* 1 = 100% chance of removal
*
* @tags temporal
* @name degradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -709,6 +753,7 @@ export const degradeBy = register(
*
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
*
* @tags temporal
* @name degrade
* @memberof Pattern
* @returns Pattern
@@ -725,6 +770,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
* 1 = 0% chance of removal
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
*
* @tags temporal
* @name undegradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -753,6 +799,7 @@ export const undegradeBy = register(
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @tags temporal
* @name undegrade
* @memberof Pattern
* @returns Pattern
@@ -771,6 +818,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
* Randomly applies the given function by the given probability.
* Similar to `someCyclesBy`
*
* @tags temporal
* @name sometimesBy
* @memberof Pattern
* @param {number | Pattern} probability - a number between 0 and 1
@@ -790,6 +838,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
*
* Applies the given function with a 50% chance
*
* @tags temporal
* @name sometimes
* @memberof Pattern
* @param {function} function - the transformation to apply
@@ -811,6 +860,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {number | Pattern} probability - a number between 0 and 1
* @param {function} function - the transformation to apply
* @returns Pattern
* @tags temporal
* @example
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
*/
@@ -833,6 +883,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @name someCycles
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
*/
@@ -847,6 +898,7 @@ export const someCycles = register('someCycles', function (func, pat) {
* @name often
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("hh*8").often(x=>x.speed("0.5"))
*/
@@ -861,6 +913,7 @@ export const often = register('often', function (func, pat) {
* @name rarely
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("hh*8").rarely(x=>x.speed("0.5"))
*/
@@ -872,6 +925,7 @@ export const rarely = register('rarely', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.1, fn)`
*
* @tags temporal
* @name almostNever
* @memberof Pattern
* @returns Pattern
@@ -886,6 +940,7 @@ export const almostNever = register('almostNever', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.9, fn)`
*
* @tags temporal
* @name almostAlways
* @memberof Pattern
* @returns Pattern
@@ -900,6 +955,7 @@ export const almostAlways = register('almostAlways', function (func, pat) {
*
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
*
* @tags temporal
* @name never
* @memberof Pattern
* @returns Pattern
@@ -914,6 +970,7 @@ export const never = register('never', function (_, pat) {
*
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
*
* @tags temporal
* @name always
* @memberof Pattern
* @returns Pattern
@@ -942,6 +999,7 @@ export function _keyDown(keyname) {
* Do something on a keypress, or array of keypresses
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @tags external_io
* @name whenKey
* @memberof Pattern
* @returns Pattern
@@ -958,6 +1016,7 @@ export const whenKey = register('whenKey', function (input, func, pat) {
* returns true when a key or array of keys is held
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @tags external_io
* @name keyDown
* @memberof Pattern
* @returns Pattern
@@ -975,6 +1034,8 @@ export const keyDown = register('keyDown', function (pat) {
* event durations, from the pattern that it is combined with.
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
* See also its reciprocal, `per`, also known as `perCycle`.
*
* @tags temporal
* @example
* // Shorter events are lower in pitch
* sound("saw saw [saw saw] saw")
@@ -993,6 +1054,7 @@ export const cyclesPer = new Pattern(function (state) {
* event durations, from the pattern that it is combined with.
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
* See also its reciprocal, `cyclesPer`.
* @tags temporal
* @synonyms perCycle
* @example
* // Shorter events are more distorted
@@ -1010,6 +1072,7 @@ export const perCycle = per;
* particular, where the event duration halves, the
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
* the same as `"3 3 [4 [5 5]] 3"`.
* @tags temporal
*/
export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
+2
View File
@@ -135,6 +135,8 @@ export async function loadOrc(url) {
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
* p6 -- Strudel controls, as a string.
*
* @tags external_io
*/
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
+2
View File
@@ -42,6 +42,7 @@ const getValue = (e) => {
*
* @name pianoroll
* @synonyms punchcard
* @tags visualization
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
@@ -299,6 +300,7 @@ Pattern.prototype.punchcard = function (options) {
* Supports all the same options as pianoroll.
*
* @name wordfall
* @tags visualization
*/
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
+1
View File
@@ -116,6 +116,7 @@ export function pitchwheel({
/**
* Renders a pitch circle to visualize frequencies within one octave
* @name pitchwheel
* @tags visualization
* @param {number} hapcircles
* @param {number} circle
* @param {number} edo
+1
View File
@@ -129,6 +129,7 @@ function drawSpiral(options) {
* Displays a spiral visual.
*
* @name spiral
* @tags visualization
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
* @param {number} size the diameter of the spiral
+5
View File
@@ -138,6 +138,7 @@ function githubPath(base, subpath = '') {
/**
* configures the default midimap, which is used when no "midimap" port is set
* @tags external_io
* @example
* defaultmidimap({ lpf: 74 })
* $: note("c a f e").midi();
@@ -151,6 +152,7 @@ let loadCache = {};
/**
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
* @tags external_io
* @example
* midimaps({ mymap: { lpf: 74 } })
* $: note("c a f e")
@@ -305,6 +307,7 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
/**
* MIDI output: Opens a MIDI output port.
* @tags external_io
* @param {string | number} midiport MIDI device name or index defaulting to 0
* @param {object} options Additional MIDI configuration options
* @example
@@ -526,6 +529,7 @@ async function _initialize(input) {
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
*
* @name midin
* @tags external_io
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
@@ -573,6 +577,7 @@ export async function midin(input) {
* note durations
*
* @name midikeys
* @tags external_io
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
* When queried, the pattern will produces the most recently played midi notes and velocities,
+15
View File
@@ -7,6 +7,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationX
* @return {Pattern}
* @synonyms accX
* @tags external_io
* @example
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
*
@@ -17,6 +18,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationY
* @return {Pattern}
* @synonyms accY
* @tags external_io
* @example
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
*
@@ -27,6 +29,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationZ
* @return {Pattern}
* @synonyms accZ
* @tags external_io
* @example
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
*
@@ -37,6 +40,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityX
* @return {Pattern}
* @synonyms gravX
* @tags external_io
* @example
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
*
@@ -47,6 +51,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityY
* @return {Pattern}
* @synonyms gravY
* @tags external_io
* @example
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
*
@@ -57,6 +62,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityZ
* @return {Pattern}
* @synonyms gravZ
* @tags external_io
* @example
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
*
@@ -67,6 +73,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationAlpha
* @return {Pattern}
* @synonyms rotA, rotZ, rotationZ
* @tags external_io
* @example
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -77,6 +84,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationBeta
* @return {Pattern}
* @synonyms rotB, rotX, rotationX
* @tags external_io
* @example
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -87,6 +95,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationGamma
* @return {Pattern}
* @synonyms rotG, rotY, rotationY
* @tags external_io
* @example
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
*
@@ -97,6 +106,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationAlpha
* @return {Pattern}
* @synonyms oriA, oriZ, orientationZ
* @tags external_io
* @example
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -107,6 +117,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationBeta
* @return {Pattern}
* @synonyms oriB, oriX, orientationX
* @tags external_io
* @example
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -117,6 +128,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationGamma
* @return {Pattern}
* @synonyms oriG, oriY, orientationY
* @tags external_io
* @example
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
*
@@ -127,6 +139,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationAlpha
* @return {Pattern}
* @synonyms absOriA, absOriZ, absoluteOrientationZ
* @tags external_io
* @example
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -137,6 +150,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationBeta
* @return {Pattern}
* @synonyms absOriB, absOriX, absoluteOrientationX
* @tags external_io
* @example
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -147,6 +161,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationGamma
* @return {Pattern}
* @synonyms absOriG, absOriY, absoluteOrientationY
* @tags external_io
* @example
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
*
+1
View File
@@ -79,6 +79,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
*
* @name osc
* @tags external_io
* @memberof Pattern
* @returns Pattern
*/
+2 -1
View File
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
fxi = 'main',
depth = 1,
depthabs,
retrig = 0,
...filteredParams
} = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
const modParams = {
...filteredParams,
frequency: sync !== undefined ? sync * cps : rate,
time: cycle / cps,
time: retrig > 0.5 ? 0 : cycle / cps,
depth: depthValue,
min,
max,
+22 -7
View File
@@ -35,7 +35,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
/** Handles dynamic reallocation of input/output channels buffer
(channel numbers may lety during lifecycle) **/
* (channel numbers may vary during lifecycle)
* @tags internals
**/
reallocateChannelsIfNeeded(inputs, outputs) {
for (let i = 0; i < this.nbInputs; i++) {
let nbChannels = inputs[i].length;
@@ -88,7 +90,10 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Read next web audio block to input buffers **/
/**
* Read next web audio block to input buffers
* @tags internals
**/
readInputs(inputs) {
// when playback is paused, we may stop receiving new samples
if (inputs[0].length && inputs[0][0].length == 0) {
@@ -108,7 +113,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Write next web audio block from output buffers **/
/** Write next web audio block from output buffers
* @tags internals
**/
writeOutputs(outputs) {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
@@ -118,7 +125,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Shift left content of input buffers to receive new web audio block **/
/** Shift left content of input buffers to receive new web audio block
* @tags internals
**/
shiftInputBuffers() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
@@ -127,7 +136,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Shift left content of output buffers to receive new web audio block **/
/** Shift left content of output buffers to receive new web audio block
* @tags internals
**/
shiftOutputBuffers() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
@@ -137,7 +148,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Copy contents of input buffers to buffer actually sent to process **/
/** Copy contents of input buffers to buffer actually sent to process
* @tags internals
**/
prepareInputBuffersToSend() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
@@ -146,7 +159,9 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
/** Add contents of output buffers just processed to output buffers **/
/** Add contents of output buffers just processed to output buffers
* @tags internals
**/
handleOutputBuffersToRetrieve() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
+1
View File
@@ -37,6 +37,7 @@
},
"dependencies": {
"@kabelsalat/lib": "^0.4.1",
"@kabelsalat/web": "^0.4.1",
"nanostores": "^0.11.3"
},
"engines": {
+5 -2
View File
@@ -18,6 +18,7 @@ var reverbGen = {};
/** Generates a reverb impulse response.
@tags internals
@param {!Object} params TODO: Document the properties.
@param {!function(!AudioBuffer)} callback Function to call when
the impulse response has been generated. The impulse response
@@ -50,7 +51,7 @@ reverbGen.generateReverb = function (params, callback) {
/** Creates a canvas element showing a graph of the given data.
@tags internals
@param {!Float32Array} data An array of numbers, or a Float32Array.
@param {number} width Width in pixels of the canvas.
@param {number} height Height in pixels of the canvas.
@@ -81,7 +82,9 @@ reverbGen.generateGraph = function (data, width, height, min, max) {
@param {number} lpFreqEnd
@param {number} lpFreqEndAt
@param {!function(!AudioBuffer)} callback May be called
immediately within the current execution context, or later.*/
immediately within the current execution context, or later.
@tags internals
*/
var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
if (lpFreqStart == 0) {
callback(input);
+1
View File
@@ -235,6 +235,7 @@ export async function fetchSampleMap(url) {
/**
* Loads a collection of samples to use with `s`
* @tags samples
* @example
* samples('github:tidalcycles/dirt-samples');
* s("[bd ~]*2, [~ hh]*2, ~ sd")
+22
View File
@@ -40,6 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY;
* start to die out in first-in-first-out order once the max polyphony has been hit
*
* @name setMaxPolyphony
* @tags fx, superdough
* @param {number} Max polyphony. Defaults to 128
* @example
* setMaxPolyphony(4)
@@ -73,6 +74,7 @@ export function applyGainCurve(val) {
* quadratic, exponential, etc. rather than linear
*
* @name setGainCurve
* @tags fx, superdough
* @param {Function} function to apply to all gain values
* @example
* setGainCurve((x) => x * x) // quadratic gain
@@ -128,6 +130,8 @@ async function aliasBankPath(path) {
* Optionally accepts a single argument string of a path to a JSON file containing bank aliases.
* @param {string} bank - The bank to alias
* @param {string} alias - The alias to use for the bank
*
* @tags samples
*/
export async function aliasBank(...args) {
switch (args.length) {
@@ -146,6 +150,7 @@ export async function aliasBank(...args) {
/**
* Register an alias for a sound.
* @tags samples
* @param {string} original - The original sound name
* @param {string} alias - The alias to use for the sound
*/
@@ -255,6 +260,14 @@ export function loadWorklets() {
return workletsLoading;
}
let kabel;
async function initKabelsalat() {
const { SalatRepl } = await import('@kabelsalat/web');
logger('[kabelsalat] ready');
kabel = new SalatRepl({ localScope: true });
return kabel;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
@@ -301,6 +314,7 @@ export async function initAudio(options = {}) {
} catch (err) {
console.warn('could not load AudioWorklet effects', err);
}
await initKabelsalat();
logger('[superdough] ready');
}
let audioReady;
@@ -436,6 +450,14 @@ class Chain {
}
}
const compileKabel = (code) => {
if (!kabel) {
throw new Error('kabelsalat not loaded');
}
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// mapping from main FX and numbered FX chains to nodes
const nodes = { main: {} };
+3 -1
View File
@@ -110,7 +110,9 @@ export function getCommonSampleInfo(hapValue, bank) {
return { transpose, url, index, midi, label };
}
/** Selects entries from `source` and renames them via `map` */
/** Selects entries from `source` and renames them via `map`
* @tags internals
*/
export const pickAndRename = (source, map) => {
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
};
+1
View File
@@ -186,6 +186,7 @@ export function registerWaveTable(key, tables, params) {
* Loads a collection of wavetables to use with `s`
*
* @name tables
* @tags fx
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
+15 -5
View File
@@ -646,14 +646,18 @@ class PhaseVocoderProcessor extends OLAProcessor {
this.timeCursor += this.hopSize;
}
/** Apply Hann window in-place */
/** Apply Hann window in-place
* @tags internals
*/
applyHannWindow(input) {
for (let i = 0; i < this.blockSize; i++) {
input[i] *= this.hannWindow[i] * 1.62;
}
}
/** Compute squared magnitudes for peak finding **/
/** Compute squared magnitudes for peak finding
* @tags internals
**/
computeMagnitudes() {
let i = 0,
j = 0;
@@ -667,7 +671,9 @@ class PhaseVocoderProcessor extends OLAProcessor {
}
}
/** Find peaks in spectrum magnitudes **/
/** Find peaks in spectrum magnitudes
* @tags internals
**/
findPeaks() {
this.nbPeaks = 0;
let i = 2;
@@ -688,7 +694,9 @@ class PhaseVocoderProcessor extends OLAProcessor {
}
}
/** Shift peaks and regions of influence by pitchFactor into new specturm */
/** Shift peaks and regions of influence by pitchFactor into new specturm
* @tags internals
*/
shiftPeaks(pitchFactor) {
// zero-fill new spectrum
this.freqComplexBufferShifted.fill(0);
@@ -841,7 +849,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
/** BYTE BEATS */
/** BYTE BEATS
* @tags internals
*/
const chyx = {
/*bit*/ bitC: function (x, y, z) {
return x & y ? z : 0;
+1
View File
@@ -19,6 +19,7 @@ function applyGainCurve(val) {
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
* @tags internals
* @returns {number} Crossfaded output value.
*/
function crossfade(a, b, m) {
+3
View File
@@ -100,6 +100,7 @@ function scaleOffset(scale, offset, note) {
* - 5P = perfect fifth
* - 5d = diminished fifth
*
* @tags music_theory
* @param {string | number} amount Either number of semitones or interval string.
* @returns Pattern
* @memberof Pattern
@@ -154,6 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
*
* @memberof Pattern
* @name scaleTranspose
* @tags music_theory
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @synonyms scaleTrans, strans
@@ -244,6 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* The root note defaults to octave 3, if no octave number is given.
*
* @name scale
* @tags music_theory
* @param {string} scale Name of scale
* @returns Pattern
* @example
+4
View File
@@ -90,6 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr
* Adds a new custom voicing dictionary.
*
* @name addVoicings
* @tags music_theory
* @memberof Pattern
* @param {string} name identifier for the voicing dictionary
* @param {Object} dictionary maps chord symbol to possible voicings
@@ -133,6 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
* @name voicings
* @tags music_theory
* @memberof Pattern
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
@@ -157,6 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) {
* Maps the chords of the incoming pattern to root notes in the given octave.
*
* @name rootNotes
* @tags music_theory
* @memberof Pattern
* @param {octave} octave octave to use
* @returns Pattern
@@ -189,6 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @tags music_theory
* @returns Pattern
* @example
* n("0 1 2 3").chord("<C Am F G>").voicing()
+2
View File
@@ -98,6 +98,7 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
/**
* Renders an oscilloscope for the frequency domain of the audio signal.
* @name fscope
* @tags visualization
* @param {string} color line color as hex or color name. defaults to white.
* @param {number} scale scales the y-axis. Defaults to 0.25
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
@@ -122,6 +123,7 @@ Pattern.prototype.fscope = function (config = {}) {
* Renders an oscilloscope for the time domain of the audio signal.
* @name scope
* @synonyms tscope
* @tags visualization
* @param {object} config optional config with options:
* @param {boolean} align if 1, the scope will be aligned to the first zero crossing. defaults to 1
* @param {string} color line color as hex or color name. defaults to white.
+1
View File
@@ -5,6 +5,7 @@ import { analysers, getAnalyzerData } from 'superdough';
/**
* Renders a spectrum analyzer for the incoming audio signal.
* @name spectrum
* @tags visualization
* @param {object} config optional config with options:
* @param {integer} thickness line thickness in px (default 3)
* @param {integer} speed scroll speed (default 1)
+3 -3
View File
@@ -234,9 +234,6 @@ importers:
packages/core:
dependencies:
'@kabelsalat/web':
specifier: ^0.4.1
version: 0.4.1
fraction.js:
specifier: ^5.2.1
version: 5.2.1
@@ -524,6 +521,9 @@ importers:
'@kabelsalat/lib':
specifier: ^0.4.1
version: 0.4.1
'@kabelsalat/web':
specifier: ^0.4.1
version: 0.4.1
nanostores:
specifier: ^0.11.3
version: 0.11.3
+29
View File
@@ -10874,6 +10874,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
]
`;
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
]
`;
exports[`runs examples > example "setGainCurve" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
+2
View File
@@ -1,7 +1,9 @@
import { afterEach } from 'vitest';
import { useRNG } from './packages/core/signal.mjs';
import { setDefaultJoin } from './packages/core/pattern.mjs';
afterEach(() => {
// Avoid bleed between tests
useRNG('legacy');
setDefaultJoin('in');
});
+1
View File
@@ -33,6 +33,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global>
:root {
color-scheme: dark;
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
@@ -32,6 +32,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global>
:root {
color-scheme: dark;
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { BottomPanel } 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} />
<BottomPanel context={context} />
</div>
);
}
-12
View File
@@ -1,15 +1,3 @@
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.darken::before {
content: ' ';
position: fixed;
+1 -1
View File
@@ -8,7 +8,7 @@ export function Code(Props) {
return (
<section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'}
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
id="code"
ref={(el) => {
containerRef.current = el;
@@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { MainPanel } from './panel/Panel';
// type Props = {
// context: replcontext,
@@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) {
return (
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} embedded={true} />
<MainPanel context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
-150
View File
@@ -1,150 +0,0 @@
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs';
import '../Repl.css';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
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, fontFamily } = useSettings();
return (
<header
id="header"
className={cx(
'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',
)}
style={{ fontFamily }}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<div
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 text-foreground 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>
)}
</h1>
</div>
{!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 && !isCSSAnimationDisabled && 'animate-pulse',
)}
>
{!pending ? (
<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>
) : (
<>loading...</>
)}
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx(
'flex items-center space-x-1',
!isEmbedded ? 'p-2' : 'px-2',
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)}
>
{!isEmbedded && <span>update</span>}
</button>
{/* !isEmbedded && (
<button
title="shuffle"
className="hover:opacity-50 p-2 flex items-center space-x-1"
onClick={handleShuffle}
>
<span> shuffle</span>
</button>
) */}
{!isEmbedded && (
<button
title="share"
className={cx(
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
!isEmbedded ? 'p-2' : 'px-2',
)}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
{/* {isEmbedded && (
<button className={cx('hover:opacity-50 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:opacity-50 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
}}
title="Reset"
>
💔
</a>
</button>
)} */}
</div>
)}
</header>
);
}
+12 -8
View File
@@ -1,8 +1,7 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import Loader from '@src/repl/components/Loader';
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { useSettings } from '@src/settings.mjs';
// type Props = {
@@ -14,17 +13,22 @@ export default function ReplEditor(Props) {
const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings();
const { panelPosition, isZen } = settings;
const isEmbedded = typeof window !== 'undefined' && window.location !== window.parent.location;
return (
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
<div className="flex flex-col grow overflow-hidden">
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
<MainPanel context={context} isEmbedded={isEmbedded} />
<div className="flex overflow-hidden h-full">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <RightPanel context={context} />}
</div>
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{!isZen && panelPosition === 'bottom' && <BottomPanel context={context} />}
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
</div>
);
}
@@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
return (
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
<button className={cx('hover:opacity-50 text-xs text-nowrap w-fit', className)} title={label} {...buttonProps}>
{labelIsHidden !== true && label}
{children}
</button>
@@ -13,16 +13,13 @@ export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
<ActionButton
{...buttonProps}
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
/>
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
);
}
export function ActionInput({ label, className, ...props }) {
return (
<label className={cx('inline-flex items-center cursor-pointer', className)}>
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
<input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
@@ -35,7 +32,7 @@ export function SpecialActionInput({ className, ...props }) {
<ActionInput
{...props}
className={className}
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
/>
);
}
@@ -0,0 +1,19 @@
export function SidebarIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-panel-right-icon lucide-panel-right"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
);
}
@@ -0,0 +1,13 @@
// Source: https://commons.wikimedia.org/wiki/File:CHAM_PUNCTUATION_SPIRAL.svg
// License: SIL Open Font License, Version 1.1. The license text is available at https://openfontlicense.org/open-font-license-official-text/
export function StrudelIcon({ className = '' }) {
return (
<svg className={className} width="100%" height="100%" viewBox="0 0 213 213">
<g>
<g id="_1" serif:id="1">
<path d="M199.905,102.467c-0,17.959 -3.695,33.45 -11.078,46.468c-7.369,13.024 -17.9,23.079 -31.586,30.173c-13.673,7.093 -29.994,10.64 -48.961,10.64c-19.964,0 -37.183,-3.874 -51.66,-11.617c-14.482,-7.749 -25.592,-18.775 -33.334,-33.103c-7.749,-14.322 -11.617,-31.407 -11.617,-51.248l17.143,-0c-0,16.95 3.187,31.503 9.561,43.666c6.373,12.15 15.485,21.48 27.346,27.989c11.874,6.509 26.061,9.766 42.561,9.766c15.485,0 28.798,-2.898 39.94,-8.687c11.154,-5.789 19.693,-14.11 25.624,-24.956c5.943,-10.858 8.918,-23.812 8.918,-38.86c-0,-20.85 -5.5,-37.202 -16.5,-49.063c-11,-11.874 -26.344,-17.811 -46.031,-17.811c-17.515,-0 -31.266,4.883 -41.251,14.649c-9.991,9.767 -14.983,23.337 -14.983,40.711c-0,15.781 4.015,28.13 12.054,37.035c8.031,8.893 19.07,13.339 33.103,13.339c12.452,0 22.186,-3.431 29.196,-10.306c7.023,-6.888 10.538,-16.333 10.538,-28.348c-0,-10.281 -2.57,-18.415 -7.711,-24.416c-5.14,-6.014 -12.413,-9.021 -21.82,-9.021c-7.813,-0 -13.93,2.178 -18.351,6.528c-4.407,4.337 -6.605,10.197 -6.605,17.579c0,1.44 0.064,2.815 0.206,4.112c4.485,-7.363 11.141,-11.051 19.97,-11.051c5.5,-0 9.875,1.561 13.133,4.678c3.27,3.103 4.909,7.324 4.909,12.67c-0,5.796 -1.966,10.506 -5.886,14.136c-3.906,3.617 -9.265,5.423 -16.063,5.423c-8.533,-0 -15.337,-2.814 -20.407,-8.456c-5.076,-5.654 -7.607,-13.114 -7.607,-22.386c-0,-11.135 3.431,-19.957 10.306,-26.472c6.888,-6.528 16.121,-9.792 27.706,-9.792c13.467,0 24.03,4.164 31.689,12.491c7.672,8.327 11.515,19.584 11.515,33.771c-0,16.057 -4.884,28.753 -14.65,38.089c-9.767,9.343 -23.195,14.008 -40.274,14.008c-12.594,-0 -23.491,-2.603 -32.692,-7.814c-9.188,-5.204 -16.243,-12.625 -21.178,-22.257c-4.921,-9.625 -7.376,-21.023 -7.376,-34.182c0,-14.342 2.93,-26.64 8.79,-36.907c5.873,-10.281 14.271,-18.203 25.187,-23.774c10.93,-5.584 23.928,-8.379 38.989,-8.379c16.359,0 30.469,3.258 42.33,9.767c11.874,6.515 20.985,15.819 27.346,27.911c6.374,12.08 9.561,26.518 9.561,43.307Zm-86.408,-6.939c-7.235,-0 -11.867,4.491 -13.904,13.467c2.756,3.033 6.528,4.549 11.308,4.549c7.659,0 11.489,-3.11 11.489,-9.329c-0,-2.757 -0.797,-4.89 -2.39,-6.4c-1.581,-1.523 -3.746,-2.287 -6.503,-2.287Z" />
</g>
</g>
</svg>
);
}
@@ -1,4 +1,4 @@
import { Textbox } from '../textbox/Textbox';
import { Textbox } from '@src/repl/components/panel/SettingsTab';
import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) {
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { SelectInput } from './SelectInput';
import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map();
@@ -29,7 +29,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
options.set(deviceName, deviceName);
});
return (
<SelectInput
<SelectInputDuplicate
isDisabled={isDisabled}
options={options}
onClick={onClick}
@@ -1,6 +1,6 @@
import React from 'react';
import { audioEngineTargets } from '../../../settings.mjs';
import { SelectInput } from './SelectInput';
import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
// Allows the user to select an audio interface for Strudel to play through
export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
@@ -12,8 +12,8 @@ export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
[audioEngineTargets.osc, audioEngineTargets.osc],
]);
return (
<div className=" flex flex-col gap-1">
<SelectInput isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
<div className="flex flex-col gap-1">
<SelectInputDuplicate isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
{target === audioEngineTargets.osc && (
<div>
<p className="text-sm italic">
@@ -2,13 +2,23 @@ import cx from '@src/cx.mjs';
import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger';
import { useEffect, useRef } from 'react';
export function ConsoleTab() {
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
const scrollRef = useRef();
// scroll to bottom when log changes
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [log]);
return (
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
<div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
<div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md" ref={scrollRef}>
{' '}
{/* bg-background */}
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
@@ -16,12 +26,13 @@ export function ConsoleTab() {
<div
key={l.id}
className={cx(
'whitespace-nowrap',
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline',
)}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
<span dangerouslySetInnerHTML={{ __html: message }} className="whitespace-nowrap" />
{l.count ? ` (${l.count})` : ''}
</div>
);
@@ -1,10 +1,7 @@
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import cx from '@src/cx.mjs';
import NumberInput from '@src/repl/components/NumberInput';
import { useEffect, useState } from 'react';
import { Textbox } from '../textbox/Textbox';
import { useState } from 'react';
import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { getAudioContext } from '@strudel/webaudio';
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
function Checkbox({ label, value, onChange, disabled = false }) {
return (
@@ -48,7 +45,7 @@ export default function ExportTab(Props) {
return (
<>
<div className="text-foreground w-full p-4 space-y-4">
<div className="text-foreground w-full space-y-4 p-4">
<FormItem label="File name" disabled={exporting}>
<Textbox
onBlur={(e) => {
@@ -59,7 +56,7 @@ export default function ExportTab(Props) {
}}
disabled={exporting}
placeholder="Leave empty to use current date"
className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')}
className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')}
value={downloadName ?? ''}
/>
</FormItem>
+3 -3
View File
@@ -1,15 +1,15 @@
import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) {
export function ButtonGroup({ value, onChange, items, wrap = false }) {
return (
<div className="flex max-w-lg">
<div className={cx('flex max-w-lg space-x-0 text-xs', wrap && 'flex-wrap')}>
{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',
'px-2 border-b-2 h-8 whitespace-nowrap border-box max-h-8 hover:opacity-50',
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
+198 -110
View File
@@ -1,77 +1,204 @@
import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid';
import cx from '@src/cx.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab } from '../../../settings.mjs';
import '../../Repl.css';
import { useLogger } from '../useLogger';
import { ConsoleTab } from './ConsoleTab';
import ExportTab from './ExportTab';
import { FilesTab } from './FilesTab';
import { PatternsTab } from './PatternsTab';
import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
import ExportTab from './ExportTab';
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelOpen, activeFooter: tab } = settings;
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function LogoButton({ context, isEmbedded }) {
const { started } = context;
const { isZen, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<div
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 text-foreground rotate-90">
<StrudelIcon className="w-5 h-5 fill-foreground" />
</span>
</div>
);
}
export function MainPanel({ context, isEmbedded = false, className }) {
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
return (
<nav
id="header"
className={cx(
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
!isZen && !isEmbedded && 'border-b border-muted bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
'flex items-center',
className,
)}
style={{ fontFamily }}
>
<div className={cx('flex w-full justify-between')}>
<div className="px-3 py-1 flex space-x-2 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<LogoButton context={context} isEmbedded={isEmbedded} />
{!isZen && (
<div className="space-x-2 flex items-baseline">
<span className="hidden sm:block">strudel</span>
<span className="text-sm font-medium hidden sm:block">REPL</span>
</div>
)}
</h1>
</div>
{!isZen && (
<div className="flex grow justify-end">
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
</div>
)}
</div>
</nav>
);
}
export function Footer({ context, isEmbedded = false }) {
return (
<div className="border-t border-muted bg-lineHighlight block lg:hidden">
<MainMenu context={context} isEmbedded={isEmbedded} />
</div>
);
}
function MainMenu({ context, isEmbedded = false, className }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
const { isCSSAnimationDisabled } = useSettings();
return (
<div className={cx('flex text-sm max-w-full shrink-0 overflow-hidden text-foreground px-2 h-10', className)}>
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx('px-2 hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
<span className={cx('flex items-center space-x-2')}>
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
{!isEmbedded && <span>{pending ? '...' : started ? 'stop' : 'play'}</span>}
</span>
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx('flex items-center space-x-1 px-2', !isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50')}
>
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
<button
title="share"
className={cx('cursor-pointer hover:opacity-50 flex items-center space-x-1 px-2')}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
</div>
);
}
function PanelCloseButton() {
const { isPanelOpen } = useSettings();
return (
isPanelOpen && (
<button
onClick={() => setIsPanelOpened(false)}
className={cx('px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Close Menu"
>
<XMarkIcon className="w-6 h-6" />
</button>
)
);
}
export function BottomPanel({ context }) {
const { isPanelOpen, activeFooter: tab } = 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')}
className={cx(
isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
'overflow-hidden flex flex-col relative',
)}
>
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className={cx(isPanelOpen && 'border-l border-muted')} />
</div>
{isPanelOpen && (
<div className="flex h-full overflow-auto pr-10 ">
<div className="w-full h-full overflow-auto border-t border-muted">
<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 }) {
export function RightPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
if (!isPanelOpen) {
return;
}
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>
className={cx(
'border-l border-muted shrink-0 h-full overflow-hidden',
isPanelOpen ? `min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12',
)}
>
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className="border-l border-muted" />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
</PanelNav>
);
}
@@ -89,8 +216,8 @@ if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, settings, ...props }) {
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
function PanelNav({ children, className, ...props }) {
const settings = useSettings();
return (
<nav
onClick={() => {
@@ -98,18 +225,8 @@ function PanelNav({ children, className, settings, ...props }) {
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)}
className={cx('h-full bg-lineHighlight group overflow-x-auto', className)}
{...props}
>
{children}
@@ -145,7 +262,7 @@ function PanelTab({ label, isSelected, onClick }) {
<button
onClick={onClick}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
'h-10 px-2 text-sm border-t-2 border-t-transparent text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b-2',
isSelected ? 'border-foreground' : 'border-transparent',
)}
>
@@ -154,65 +271,36 @@ function PanelTab({ label, isSelected, onClick }) {
</>
);
}
function Tabs({ setTab, tab, className }) {
function Tabs({ className }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return (
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
<div
className={cx(
'px-2 w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
className,
)}
>
{Object.keys(tabNames).map((key) => {
const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
return <PanelTab key={key} isSelected={tab === val && isPanelOpen} 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 }) {
export function PanelToggle({ isEmbedded, isZen }) {
const { panelPosition, isPanelOpen } = useSettings();
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"
!isEmbedded &&
!isZen &&
panelPosition === 'right' && (
<button
title="menu"
className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
onClick={() => setIsPanelOpened(!isPanelOpen)}
>
<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>
<Bars3Icon className="w-6 h-6" />
</button>
)
);
}
@@ -8,7 +8,7 @@ import {
useViewingPatternData,
userPattern,
} from '../../../user_pattern_utils.mjs';
import { useMemo } from 'react';
import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
@@ -18,7 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
import { Textbox } from '../textbox/Textbox.jsx';
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@@ -43,7 +43,7 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
className={cx(
'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1',
showHiglight && 'bg-selection',
showHiglight && 'ring-selection',
)}
onClick={onClick}
>
@@ -53,11 +53,10 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
}
function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternData = useViewingPatternData();
const viewingPatternID = viewingPatternData.id;
return (
<div className="">
<div className="p-2">
{Object.values(patterns)
.reverse()
.map((pattern) => {
@@ -83,8 +82,8 @@ const updateCodeWindow = (context, patternData, reset = false) => {
export function PatternsTab({ context }) {
const [search, setSearch] = useState('');
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternData = useViewingPatternData();
const { userPatterns, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id;
@@ -121,74 +120,71 @@ export function PatternsTab({ context }) {
);
}),
);
}, [search, viewingPatternStore]);
}, [search, userPatterns]);
const importRef = useRef();
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<div className="w-full flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<Textbox className="w-full border-0" placeholder="Search..." value={search} onChange={setSearch} />
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-y border-muted">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<input
ref={importRef}
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
<ActionButton label="import" onClick={() => importRef.current.click()} />
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div>
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div>
<div className="overflow-auto">
{/* bg-background */}
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div>
);
+153 -31
View File
@@ -1,7 +1,8 @@
import { useMemo, useState } from 'react';
import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
@@ -10,8 +11,16 @@ const availableFunctions = (() => {
const functions = [];
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
if (seen.has(doc.name)) continue;
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
// @something is. We only want data from comments like `@tags fx, superdough` here.
// If nothing is specified, we default to "untagged" for debugging
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
functions.push(doc);
const synonyms = doc.synonyms || [];
seen.add(doc.name);
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
seen.add(s);
@@ -29,70 +38,182 @@ const availableFunctions = (() => {
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})();
const tagCounts = {};
// const tagOptions = { all: `all (${availableFunctions.length})` };
const tagOptions = { all: `all` };
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
//tagOptions[t] = `${t} (${tagCounts[t]})`;
tagOptions[t] = t;
}
});
}
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Reference() {
export const Reference = memo(function Reference() {
const [search, setSearch] = useState('');
const [selectedFunction, setSelectedFunction] = useState(null);
const { referenceTag } = useSettings();
const visibleFunctions = useMemo(() => {
const toggleTag = (tag) => {
if (referenceTag === tag) {
setReferenceTag('all');
} else {
setReferenceTag(tag);
}
};
const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (referenceTag && referenceTag !== 'all') {
if (!(entry.tags || ['untagged']).includes(referenceTag)) {
return false;
}
}
if (!search) {
return true;
}
const lowerCaseSearch = search.toLowerCase();
return (
entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search]);
}, [search, referenceTag]);
const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => {
if (referenceTag === null || referenceTag === 'all') {
if (search) {
return true;
}
return x.name === selectedFunction;
} else {
return true;
}
});
}, [searchVisibleFunctions, selectedFunction, referenceTag]);
const onSearchTagFilterClick = () => {
setReferenceTag('all');
setSelectedFunction(null);
};
useEffect(() => {
if (selectedFunction) {
const el = document.getElementById(`doc-${selectedFunction}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}
}, [selectedFunction]);
let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value);
return (
<div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div className="flex h-full w-full overflow-hidden">
<div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted">
{/* bg-background */}
<div className="w-full flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<Textbox
className="w-full border-0 border-b border-muted"
placeholder="Search..."
value={search}
onChange={(e) => {
setSelectedFunction(null);
setReferenceTag('all');
setSearch(e);
}}
/>
</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 text-foreground 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);
}}
{/* <div className="flex shrink-0 flex-wrap w-full overflow-auto border-y border-muted">
<ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}></ButtonGroup>
</div> */}
{referenceTag && referenceTag !== 'all' && (
<div className="w-72">
<span
className="text-foreground border border-muted border-t-0 border-l-0 px-1 py-0.5 my-2 cursor-pointer font-sans"
onClick={onSearchTagFilterClick}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
{referenceTag}
</span>
</div>
)}
<div className="h-full p-2 overflow-y-auto bg-opacity-50">
{searchVisibleFunctions.map((entry, i) => (
<Fragment key={`entry-${entry.name}`}>
<a
className={
'cursor-pointer hover:opacity-50 text-ellipsis block' +
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
}
onClick={() => {
if (entry.name === selectedFunction) {
setSelectedFunction(null);
} else {
setSelectedFunction(entry.name);
}
}}
>
{entry.name}
</a>{' '}
</Fragment>
))}
</div>
</div>
<div
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
className="w-2/3 break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<div className="prose dark:prose-invert min-w-full px-1 text-sm">
<h2>API Reference</h2>
<p>
<p className="font-sans text-md">
This is the long list of functions you can use. Remember that you don't need to remember all of those and
that you can already make music with a small set of functions!
</p>
{visibleFunctions.map((entry, i) => (
<section key={i}>
<h3 id={`doc-${i}`}>{entry.name}</h3>
<div>
{/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/}
{Object.entries(tagCounts)
.sort(([a], [b]) => a.localeCompare(b))
.map(([t, count]) => (
<span key={t}>
<a
className={[
'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans',
`${referenceTag === t ? 'bg-muted text-foreground' : ''}`,
].join(' ')}
onClick={() => toggleTag(t)}
>
{t}&nbsp;({count})
</a>{' '}
</span>
))}
</div>
{detailVisibleFunctions.map((entry, i) => (
<section key={i} className="font-sans">
<div className="flex flex-row items-center mt-8 justify-between">
<h3 className="font-mono my-0 pt-4" id={`doc-${entry.name}`}>
{entry.name}
</h3>
{entry.tags && (
<span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
{entry.tags.join(', ')}
</span>
)}
</div>
{!!entry.synonyms_text && (
<p>
Synonyms: <code>{entry.synonyms_text}</code>
</p>
)}
{/* <small>{entry.meta.filename}</small> */}
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
<ul>
{entry.params?.map(({ name, type, description }, i) => (
@@ -102,14 +223,15 @@ export function Reference() {
))}
</ul>
{entry.examples?.map((example, j) => (
<pre className="bg-background" key={j}>
<pre className="bg-background border border-muted" key={j}>
{example}
</pre>
))}
</section>
))}
)) || <p className="font-sans">Search or select a tag to get started.</p>}
{detailVisibleFunctions.length > 0 && <div className="h-screen" />}
</div>
</div>
</div>
);
}
});
@@ -1,20 +0,0 @@
import React from 'react';
// value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
return (
<select
disabled={isDisabled}
onClick={onClick}
className="p-2 bg-background rounded-md text-foreground border-foreground"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
>
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
{Array.from(options.keys()).map((id) => (
<option key={id} className="bg-background" value={id}>
{options.get(id)}
</option>
))}
</select>
);
}
@@ -1,6 +1,5 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
@@ -10,22 +9,60 @@ import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@
import { SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
const inputClass =
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input className={cx('px-2', inputClass, className)} onChange={(e) => onChange(e.target.value)} {...inputProps} />
);
}
function Checkbox({ label, value, onChange, disabled = false }) {
return (
<label>
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} />
<label className="text-xs">
<input
className={cx(
'bg-background text-sm border border-muted focus:outline-none focus:ring-0 focus:border-foreground',
)}
disabled={disabled}
type="checkbox"
checked={value}
onChange={onChange}
/>
{' ' + label}
</label>
);
}
function SelectInput({ value, options, onChange }) {
// value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
export function SelectInputDuplicate({ value, options, onChange, onClick, isDisabled }) {
return (
<select
className="p-2 bg-background rounded-md text-foreground border-foreground"
value={value}
disabled={isDisabled}
onClick={onClick}
className={cx('p-2', inputClass)}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
>
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
{Array.from(options.keys()).map((id) => (
<option key={id} className="bg-background" value={id}>
{options.get(id)}
</option>
))}
</select>
);
}
function SelectInput({ value, options, onChange }) {
return (
<select className={cx('p-2', inputClass)} value={value} onChange={(e) => onChange(e.target.value)}>
{Object.entries(options).map(([k, label]) => (
<option key={k} className="bg-background" value={k}>
{label}
@@ -37,9 +74,9 @@ function SelectInput({ value, options, onChange }) {
function NumberSlider({ value, onChange, step = 1, ...rest }) {
return (
<div className="flex space-x-2 gap-1">
<div className="flex space-x-2 gap-1 overflow-hidden">
<input
className="p-2 grow"
className="p-2 grow accent-foreground"
type="range"
value={value}
step={step}
@@ -50,7 +87,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
type="number"
value={value}
step={step}
className="w-16 bg-background rounded-md"
className={cx('w-16', inputClass)}
onChange={(e) => onChange(Number(e.target.value))}
/>
</div>
@@ -59,8 +96,8 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
function FormItem({ label, children, sublabel }) {
return (
<div className="grid gap-2">
<label>{label}</label>
<div className="grid gap-2 text-xs">
<label className="text-sm">{label}</label>
{children}
</div>
);
@@ -107,7 +144,6 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
togglePanelTrigger,
maxPolyphony,
multiChannelOrbits,
isTabIndentationEnabled,
@@ -118,7 +154,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -178,7 +214,7 @@ export function SettingsTab({ started }) {
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormItem label="Font Family">
<SelectInput
options={fontFamilyOptions}
@@ -219,13 +255,6 @@ 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' }}
/>
</FormItem>
<FormItem label="More Settings">
<Checkbox
label="Enable bracket matching"
@@ -294,7 +323,7 @@ export function SettingsTab({ started }) {
value={isSyncEnabled}
/>
<Checkbox
label="Hide top buttons"
label="Hide action buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden}
/>
@@ -5,7 +5,7 @@ import { useMemo, useRef, useState } from 'react';
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
import { ActionButton } from '../button/action-button.jsx';
import { confirmDialog } from '@src/repl/util.mjs';
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
@@ -77,11 +77,12 @@ export function SoundsTab() {
numRef.current = 0;
});
return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div id="sounds-tab" className="flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search..." className="border-0" value={search} onChange={(v) => setSearch(v)} />
<div className=" flex shrink-0 flex-wrap">
<div className="flex shrink-0 flex-wrap border-y border-muted">
<ButtonGroup
wrap
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
items={{
@@ -114,7 +115,7 @@ export function SoundsTab() {
/>
)}
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
<div className="min-h-0 max-h-full grow overflow-auto break-normal p-2">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -165,7 +166,7 @@ export function SoundsTab() {
);
})}
{!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
<div className="prose dark:prose-invert min-w-full text-sm">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p>
To import sounds into strudel, they must be contained{' '}

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