Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut

This commit is contained in:
Jade (Rose) Rowland
2026-01-22 00:25:41 -05:00
97 changed files with 1052 additions and 401 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 };
+1
View File
@@ -489,6 +489,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')
+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: '#ffffff50',
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",
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
+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;
-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;
+85 -119
View File
@@ -2,149 +2,115 @@ 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 { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
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);
export function Header({ context, isEmbedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
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',
'flex-none text-black z-[100] text-sm select-none min-h-10',
!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',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full justify-between',
'flex items-center',
)}
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',
)}
<div className={cx(isEmbedded ? 'flex' : 'sm:flex', 'w-full sm:justify-between')}>
<div className="px-3 pt-1 flex space-x-2 sm:pt-0 select-none">
<h1
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
>
<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',
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
{!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"
<div
className={cx(
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
!isEmbedded ? 'p-2' : 'px-2',
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={handleShare}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<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>
)} */}
<span className="block text-foreground rotate-90">
<StrudelIcon className="w-5 h-5 fill-foreground" />
</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-3 h-10">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx('hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
{!pending ? (
<span className={cx('flex items-center space-x-2')}>
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
) : (
<>loading...</>
)}
</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>
)}
</div>
</header>
);
}
+5 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel, VerticalPanel, PanelToggle } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
@@ -14,14 +14,17 @@ 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} />
<Header context={context} isEmbedded={isEmbedded} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
@@ -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-nowrap w-fit text-sm ', 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,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">
@@ -7,7 +7,7 @@ export function ConsoleTab() {
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
return (
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
<div id="console-tab" className="break-all w-full 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">
{log.map((l, i) => {
const message = linkify(l.message);
@@ -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 (
+2 -2
View File
@@ -2,14 +2,14 @@ import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) {
return (
<div className="flex max-w-lg">
<div className="flex max-w-lg space-x-0 text-sm">
{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',
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
+62 -104
View File
@@ -1,5 +1,5 @@
import cx from '@src/cx.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -8,33 +8,44 @@ 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 { Bars3Icon, 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;
function PanelCloseButton() {
const { isPanelOpen } = useSettings();
return (
isPanelOpen && (
<button
onClick={() => setIsPanelOpened(false)}
className={cx('px-4 py-2 text-foreground hover:opacity-50')}
aria-label="Close Menu"
>
<XMarkIcon className="w-6 h-6" />
</button>
)
);
}
export function HorizontalPanel({ 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">
<Tabs setTab={setTab} tab={tab} />
<PanelCloseButton />
</div>
{isPanelOpen && (
<div className="flex h-full overflow-auto pr-10 ">
<div className="flex h-full overflow-auto w-full">
<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>
);
}
@@ -42,36 +53,24 @@ export function HorizontalPanel({ context }) {
export function VerticalPanel({ 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')}
className={cx(isPanelOpen ? `min-w-[min(600px,100vw)] 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 className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} />
<PanelCloseButton />
</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>
)}
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
</PanelNav>
);
}
@@ -89,8 +88,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,16 +97,6 @@ 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)}
{...props}
@@ -145,7 +134,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-8 px-2 text-sm text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
isSelected ? 'border-foreground' : 'border-transparent',
)}
>
@@ -154,65 +143,34 @@ 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('flex select-none max-w-full 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="share"
className={cx(
'absolute top-0 right-3 rounded-0 px-2 py-2 bg-background z-[1000] cursor-pointer hover:opacity-80 flex justify-center items-center space-x-1 text-foreground ',
isPanelOpen && 'hidden',
)}
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>
)
);
}
@@ -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]);
@@ -172,7 +172,7 @@ export function PatternsTab({ context }) {
/>
</div>
<div className="overflow-auto h-full bg-background p-2 rounded-md">
<div className="overflow-auto h-full bg-background p-2">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
+136 -26
View File
@@ -1,7 +1,7 @@
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';
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
@@ -10,8 +10,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);
@@ -35,11 +43,27 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
export function Reference() {
export const Reference = memo(function Reference() {
const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const [selectedFunction, setSelectedFunction] = useState(null);
const visibleFunctions = useMemo(() => {
const toggleTag = (tag) => {
if (selectedTag === tag) {
setSelectedTag(null);
} else {
setSelectedTag(tag);
}
};
const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (selectedTag) {
if (!(entry.tags || ['untagged']).includes(selectedTag)) {
return false;
}
}
if (!search) {
return true;
}
@@ -50,43 +74,128 @@ export function Reference() {
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search]);
}, [search, selectedTag]);
const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => {
if (selectedTag === null) {
if (search) {
return true;
}
return x.name === selectedFunction;
} else {
return true;
}
});
}, [searchVisibleFunctions, selectedFunction, selectedTag]);
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
}
});
}
const onSearchTagFilterClick = () => {
setSelectedTag(null);
setSelectedFunction(null);
};
useEffect(() => {
if (selectedFunction) {
const el = document.getElementById(`doc-${selectedFunction}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}
}, [selectedFunction]);
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="h-full text-foreground flex flex-col gap-3 w-1/3 ">
<div className="w-full flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<Textbox
className="w-full"
placeholder="Search"
value={search}
onChange={(e) => {
setSelectedFunction(null);
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);
}}
{selectedTag && (
<div className="w-72">
<span
className="text-foreground border border-muted 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>
{selectedTag}
</span>
</div>
)}
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50">
{searchVisibleFunctions.map((entry, i) => (
<Fragment key={`entry-${entry.name}`}>
<a
className={
'cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis ' +
(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="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 ">
<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>
{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',
`${selectedTag === 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" 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>
@@ -107,9 +216,10 @@ export function Reference() {
</pre>
))}
</section>
))}
)) || <p className="font-sans">Searcb 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-sm border rounded-0 text-foreground border-muted placeholder-foreground focus:outline-none focus:ring-0 focus:border-foreground';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input className={cx('p-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-sm">
<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-sm">
<label className="text-sm">{label}</label>
{children}
</div>
);
@@ -107,7 +144,6 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
togglePanelTrigger,
maxPolyphony,
multiChannelOrbits,
isTabIndentationEnabled,
@@ -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"
@@ -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';
@@ -7,7 +7,7 @@ export function WelcomeTab({ context }) {
const { fontFamily } = useSettings();
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
<h3> welcome</h3>
<h3>welcome</h3>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
@@ -30,7 +30,7 @@ export function WelcomeTab({ context }) {
</a>{' '}
to ask any questions, give feedback or just say hello.
</p>
<h3> about</h3>
<h3>about</h3>
<p>
strudel is a JavaScript version of{' '}
<a href="https://tidalcycles.org/" target="_blank">
@@ -1,14 +0,0 @@
import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx(
'p-2 bg-background rounded-md border-foreground text-foreground placeholder-foreground',
className,
)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
);
}
-1
View File
@@ -43,7 +43,6 @@ export const defaultSettings = {
panelPosition: 'right',
isPanelPinned: false,
isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover
userPatterns: '{}',
prebakeScript: '',
audioEngineTarget: audioEngineTargets.webaudio,
+10 -9
View File
@@ -24,6 +24,7 @@ module.exports = {
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
muted: 'var(--muted)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
@@ -46,28 +47,28 @@ module.exports = {
'code::after': {
content: 'none',
},
color: 'var(--foreground)',
color: 'var(--foreground) !important',
a: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
h1: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
h2: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
h3: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
h4: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
pre: {
color: 'var(--foreground)',
background: 'var(--background)',
color: 'var(--foreground) !important',
background: 'var(--background) !important',
},
code: {
color: 'var(--foreground)',
color: 'var(--foreground) !important',
},
},
},