mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f99b13efc2 | |||
| 4993ec4cbf | |||
| 8e5ca57edd | |||
| 107a3acd3a | |||
| 1d887e81ef | |||
| 3364b9f05f | |||
| f9ed71fbf8 | |||
| e9ea0bc7cc | |||
| a87daf3d21 | |||
| 784b9c08ec | |||
| 139b5376fc | |||
| a052def314 | |||
| bcce1fa68a | |||
| 9a10cac8e9 | |||
| cf999462b7 | |||
| 9fc084b291 | |||
| cb46dd7840 | |||
| 84e4dde3d7 | |||
| 5f8d028d4f | |||
| e3abdd41c0 | |||
| bce6ffde08 | |||
| 35a3e32238 | |||
| 60b5024f63 | |||
| 4f14ff00d5 | |||
| 3d3130e880 | |||
| 83d542d06a | |||
| 7e34069021 | |||
| b75ca193a2 | |||
| afac1f62ca | |||
| de83805fd9 | |||
| 38b7bcb3d2 | |||
| 8ec1d4d034 | |||
| 6981638f70 | |||
| f5823ce981 | |||
| f8750901f0 | |||
| c1fd8c82c6 | |||
| 6e36bdfff7 | |||
| 8dbdd0a9b8 | |||
| f4c0281613 | |||
| 99cf256ee2 | |||
| 42d0bfadce | |||
| 22f78fbb2f | |||
| 77076a1c3e | |||
| a1d31df556 | |||
| f9d0e154e8 | |||
| 9ffd0e496f | |||
| 550d7ff6cb | |||
| 726e5ef14a | |||
| cc15e3cd36 | |||
| 2fa8ed1424 | |||
| b7827cb89f | |||
| 29fd541f1f | |||
| 0830a9df69 | |||
| 4159c8394d | |||
| 466bf819d8 | |||
| 7e4cbefc8f | |||
| 2483c673a4 | |||
| 69360b3ec8 | |||
| 96d0f2053e | |||
| b952e4c735 | |||
| a4212589e5 | |||
| ee1a894f3b | |||
| c62428ca27 | |||
| d516d765d5 | |||
| 5032b4e966 | |||
| a4d7803d43 | |||
| 4a395f372d | |||
| c754dab1ef | |||
| df54404733 |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -434,6 +434,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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+1
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
Vendored
+2
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
Vendored
+2
@@ -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
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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],
|
||||
|
||||
@@ -5,6 +5,7 @@ export const settings = {
|
||||
background: '#222',
|
||||
lineBackground: '#22222299',
|
||||
foreground: '#fff',
|
||||
muted: '#8a919966',
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
|
||||
+2
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
},
|
||||
"homepage": "https://strudel.cc",
|
||||
"dependencies": {
|
||||
"@kabelsalat/web": "^0.4.1",
|
||||
"fraction.js": "^5.2.1"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
|
||||
+169
-7
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@kabelsalat/lib": "^0.4.1",
|
||||
"@kabelsalat/web": "^0.4.1",
|
||||
"nanostores": "^0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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: {} };
|
||||
|
||||
@@ -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]]));
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+3
-3
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { BottomPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<HorizontalPanel context={context} />
|
||||
<BottomPanel context={context} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,7 +8,7 @@ export function Code(Props) {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'}
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||
id="code"
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
|
||||
@@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
import { MainPanel } from './panel/Panel';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
@@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) {
|
||||
return (
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} embedded={true} />
|
||||
<MainPanel context={context} embedded={true} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { useSettings, setIsZen } from '../../settings.mjs';
|
||||
import '../Repl.css';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
|
||||
!isZen && !isEmbedded && 'bg-lineHighlight',
|
||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
|
||||
isEmbedded ? 'flex' : 'md:flex',
|
||||
)}
|
||||
style={{ fontFamily }}
|
||||
>
|
||||
<div className="px-4 flex space-x-2 md:pt-0 select-none">
|
||||
<h1
|
||||
onClick={() => {
|
||||
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
|
||||
}}
|
||||
className={cx(
|
||||
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
|
||||
'text-foreground font-bold flex space-x-2 items-center',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'mt-[1px]',
|
||||
started && !isCSSAnimationDisabled && 'animate-spin',
|
||||
'cursor-pointer text-blue-500',
|
||||
isZen && 'fixed top-2 right-4',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block text-foreground rotate-90">꩜</span>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="space-x-2">
|
||||
<span className="">strudel</span>
|
||||
<span className="text-sm font-medium">REPL</span>
|
||||
{!isEmbedded && isButtonRowHidden && (
|
||||
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
|
||||
DOCS
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
{!isZen && !isButtonRowHidden && (
|
||||
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
title={started ? 'stop' : 'play'}
|
||||
className={cx(
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
'hover:opacity-50',
|
||||
!started && !isCSSAnimationDisabled && 'animate-pulse',
|
||||
)}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-2')}>
|
||||
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
|
||||
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
|
||||
</span>
|
||||
) : (
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEvaluate}
|
||||
title="update"
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
|
||||
)}
|
||||
>
|
||||
{!isEmbedded && <span>update</span>}
|
||||
</button>
|
||||
{/* !isEmbedded && (
|
||||
<button
|
||||
title="shuffle"
|
||||
className="hover:opacity-50 p-2 flex items-center space-x-1"
|
||||
onClick={handleShuffle}
|
||||
>
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
) */}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
title="share"
|
||||
className={cx(
|
||||
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<a
|
||||
title="learn"
|
||||
href={`${baseNoTrailing}/workshop/getting-started/`}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
{/* {isEmbedded && (
|
||||
<button className={cx('hover:opacity-50 px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
🚀
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:opacity-50 px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
💔
|
||||
</a>
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
// type Props = {
|
||||
@@ -14,17 +13,22 @@ export default function ReplEditor(Props) {
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition, isZen } = settings;
|
||||
const isEmbedded = typeof window !== 'undefined' && window.location !== window.parent.location;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative" {...editorProps}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
|
||||
<div className="flex flex-col grow overflow-hidden">
|
||||
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
|
||||
<MainPanel context={context} isEmbedded={isEmbedded} />
|
||||
<div className="flex overflow-hidden h-full">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{!isZen && panelPosition === 'right' && <RightPanel context={context} />}
|
||||
</div>
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
|
||||
{!isZen && panelPosition === 'bottom' && <BottomPanel context={context} />}
|
||||
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
|
||||
|
||||
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
|
||||
return (
|
||||
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
|
||||
<button className={cx('hover:opacity-50 text-xs text-nowrap w-fit', className)} title={label} {...buttonProps}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
@@ -13,16 +13,13 @@ export function SpecialActionButton(props) {
|
||||
const { className, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
{...buttonProps}
|
||||
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
|
||||
/>
|
||||
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionInput({ label, className, ...props }) {
|
||||
return (
|
||||
<label className={cx('inline-flex items-center cursor-pointer', className)}>
|
||||
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
|
||||
<input {...props} className="sr-only peer" />
|
||||
|
||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
||||
@@ -35,7 +32,7 @@ export function SpecialActionInput({ className, ...props }) {
|
||||
<ActionInput
|
||||
{...props}
|
||||
className={className}
|
||||
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
|
||||
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function SidebarIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-panel-right-icon lucide-panel-right"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M15 3v18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Source: https://commons.wikimedia.org/wiki/File:CHAM_PUNCTUATION_SPIRAL.svg
|
||||
// License: SIL Open Font License, Version 1.1. The license text is available at https://openfontlicense.org/open-font-license-official-text/
|
||||
export function StrudelIcon({ className = '' }) {
|
||||
return (
|
||||
<svg className={className} width="100%" height="100%" viewBox="0 0 213 213">
|
||||
<g>
|
||||
<g id="_1" serif:id="1">
|
||||
<path d="M199.905,102.467c-0,17.959 -3.695,33.45 -11.078,46.468c-7.369,13.024 -17.9,23.079 -31.586,30.173c-13.673,7.093 -29.994,10.64 -48.961,10.64c-19.964,0 -37.183,-3.874 -51.66,-11.617c-14.482,-7.749 -25.592,-18.775 -33.334,-33.103c-7.749,-14.322 -11.617,-31.407 -11.617,-51.248l17.143,-0c-0,16.95 3.187,31.503 9.561,43.666c6.373,12.15 15.485,21.48 27.346,27.989c11.874,6.509 26.061,9.766 42.561,9.766c15.485,0 28.798,-2.898 39.94,-8.687c11.154,-5.789 19.693,-14.11 25.624,-24.956c5.943,-10.858 8.918,-23.812 8.918,-38.86c-0,-20.85 -5.5,-37.202 -16.5,-49.063c-11,-11.874 -26.344,-17.811 -46.031,-17.811c-17.515,-0 -31.266,4.883 -41.251,14.649c-9.991,9.767 -14.983,23.337 -14.983,40.711c-0,15.781 4.015,28.13 12.054,37.035c8.031,8.893 19.07,13.339 33.103,13.339c12.452,0 22.186,-3.431 29.196,-10.306c7.023,-6.888 10.538,-16.333 10.538,-28.348c-0,-10.281 -2.57,-18.415 -7.711,-24.416c-5.14,-6.014 -12.413,-9.021 -21.82,-9.021c-7.813,-0 -13.93,2.178 -18.351,6.528c-4.407,4.337 -6.605,10.197 -6.605,17.579c0,1.44 0.064,2.815 0.206,4.112c4.485,-7.363 11.141,-11.051 19.97,-11.051c5.5,-0 9.875,1.561 13.133,4.678c3.27,3.103 4.909,7.324 4.909,12.67c-0,5.796 -1.966,10.506 -5.886,14.136c-3.906,3.617 -9.265,5.423 -16.063,5.423c-8.533,-0 -15.337,-2.814 -20.407,-8.456c-5.076,-5.654 -7.607,-13.114 -7.607,-22.386c-0,-11.135 3.431,-19.957 10.306,-26.472c6.888,-6.528 16.121,-9.792 27.706,-9.792c13.467,0 24.03,4.164 31.689,12.491c7.672,8.327 11.515,19.584 11.515,33.771c-0,16.057 -4.884,28.753 -14.65,38.089c-9.767,9.343 -23.195,14.008 -40.274,14.008c-12.594,-0 -23.491,-2.603 -32.692,-7.814c-9.188,-5.204 -16.243,-12.625 -21.178,-22.257c-4.921,-9.625 -7.376,-21.023 -7.376,-34.182c0,-14.342 2.93,-26.64 8.79,-36.907c5.873,-10.281 14.271,-18.203 25.187,-23.774c10.93,-5.584 23.928,-8.379 38.989,-8.379c16.359,0 30.469,3.258 42.33,9.767c11.874,6.515 20.985,15.819 27.346,27.911c6.374,12.08 9.561,26.518 9.561,43.307Zm-86.408,-6.939c-7.235,-0 -11.867,4.491 -13.904,13.467c2.756,3.033 6.528,4.549 11.308,4.549c7.659,0 11.489,-3.11 11.489,-9.329c-0,-2.757 -0.797,-4.89 -2.39,-6.4c-1.581,-1.523 -3.746,-2.287 -6.503,-2.287Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
function IncButton({ children, className, ...buttonProps }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SelectInput } from './SelectInput';
|
||||
import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
|
||||
import { getAudioDevices } from '@strudel/webaudio';
|
||||
|
||||
const initdevices = new Map();
|
||||
@@ -29,7 +29,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
|
||||
options.set(deviceName, deviceName);
|
||||
});
|
||||
return (
|
||||
<SelectInput
|
||||
<SelectInputDuplicate
|
||||
isDisabled={isDisabled}
|
||||
options={options}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { audioEngineTargets } from '../../../settings.mjs';
|
||||
import { SelectInput } from './SelectInput';
|
||||
import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
|
||||
|
||||
// Allows the user to select an audio interface for Strudel to play through
|
||||
export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
|
||||
@@ -12,8 +12,8 @@ export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
|
||||
[audioEngineTargets.osc, audioEngineTargets.osc],
|
||||
]);
|
||||
return (
|
||||
<div className=" flex flex-col gap-1">
|
||||
<SelectInput isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<SelectInputDuplicate isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
|
||||
{target === audioEngineTargets.osc && (
|
||||
<div>
|
||||
<p className="text-sm italic">
|
||||
|
||||
@@ -2,13 +2,23 @@ import cx from '@src/cx.mjs';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $strudel_log_history } from '../useLogger';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function ConsoleTab() {
|
||||
const log = useStore($strudel_log_history);
|
||||
const { fontFamily } = useSettings();
|
||||
const scrollRef = useRef();
|
||||
// scroll to bottom when log changes
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [log]);
|
||||
return (
|
||||
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
|
||||
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
|
||||
<div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
|
||||
<div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md" ref={scrollRef}>
|
||||
{' '}
|
||||
{/* bg-background */}
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
const color = l.data?.hap?.value?.color;
|
||||
@@ -16,12 +26,13 @@ export function ConsoleTab() {
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(
|
||||
'whitespace-nowrap',
|
||||
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
|
||||
l.type === 'highlight' && 'underline',
|
||||
)}
|
||||
style={color ? { color } : {}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} className="whitespace-nowrap" />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import cx from '@src/cx.mjs';
|
||||
import NumberInput from '@src/repl/components/NumberInput';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
import { useState } from 'react';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
@@ -48,7 +45,7 @@ export default function ExportTab(Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-foreground w-full p-4 space-y-4">
|
||||
<div className="text-foreground w-full space-y-4 p-4">
|
||||
<FormItem label="File name" disabled={exporting}>
|
||||
<Textbox
|
||||
onBlur={(e) => {
|
||||
@@ -59,7 +56,7 @@ export default function ExportTab(Props) {
|
||||
}}
|
||||
disabled={exporting}
|
||||
placeholder="Leave empty to use current date"
|
||||
className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')}
|
||||
className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')}
|
||||
value={downloadName ?? ''}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
export function ButtonGroup({ value, onChange, items }) {
|
||||
export function ButtonGroup({ value, onChange, items, wrap = false }) {
|
||||
return (
|
||||
<div className="flex max-w-lg">
|
||||
<div className={cx('flex max-w-lg space-x-0 text-xs', wrap && 'flex-wrap')}>
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
id={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'px-2 border-b h-8 whitespace-nowrap',
|
||||
'px-2 border-b-2 h-8 whitespace-nowrap border-box max-h-8 hover:opacity-50',
|
||||
// i === 0 && 'rounded-l-md',
|
||||
// i === arr.length - 1 && 'rounded-r-md',
|
||||
// value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
|
||||
@@ -1,77 +1,204 @@
|
||||
import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
|
||||
import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
|
||||
import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab } from '../../../settings.mjs';
|
||||
import '../../Repl.css';
|
||||
import { useLogger } from '../useLogger';
|
||||
import { ConsoleTab } from './ConsoleTab';
|
||||
import ExportTab from './ExportTab';
|
||||
import { FilesTab } from './FilesTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { Reference } from './Reference';
|
||||
import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { useLogger } from '../useLogger';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||
import ExportTab from './ExportTab';
|
||||
|
||||
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
|
||||
|
||||
export function HorizontalPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { isPanelOpen, activeFooter: tab } = settings;
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function LogoButton({ context, isEmbedded }) {
|
||||
const { started } = context;
|
||||
const { isZen, isCSSAnimationDisabled, fontFamily } = useSettings();
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'mt-[1px]',
|
||||
started && !isCSSAnimationDisabled && 'animate-spin',
|
||||
'cursor-pointer text-blue-500',
|
||||
isZen && 'fixed top-2 right-4',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block text-foreground rotate-90">
|
||||
<StrudelIcon className="w-5 h-5 fill-foreground" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainPanel({ context, isEmbedded = false, className }) {
|
||||
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
|
||||
return (
|
||||
<nav
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
|
||||
!isZen && !isEmbedded && 'border-b border-muted bg-lineHighlight',
|
||||
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
|
||||
'flex items-center',
|
||||
className,
|
||||
)}
|
||||
style={{ fontFamily }}
|
||||
>
|
||||
<div className={cx('flex w-full justify-between')}>
|
||||
<div className="px-3 py-1 flex space-x-2 select-none">
|
||||
<h1
|
||||
onClick={() => {
|
||||
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
|
||||
}}
|
||||
className={cx(
|
||||
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
|
||||
'text-foreground font-bold flex space-x-2 items-center',
|
||||
)}
|
||||
>
|
||||
<LogoButton context={context} isEmbedded={isEmbedded} />
|
||||
{!isZen && (
|
||||
<div className="space-x-2 flex items-baseline">
|
||||
<span className="hidden sm:block">strudel</span>
|
||||
<span className="text-sm font-medium hidden sm:block">REPL</span>
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="flex grow justify-end">
|
||||
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
|
||||
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function Footer({ context, isEmbedded = false }) {
|
||||
return (
|
||||
<div className="border-t border-muted bg-lineHighlight block lg:hidden">
|
||||
<MainMenu context={context} isEmbedded={isEmbedded} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MainMenu({ context, isEmbedded = false, className }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
|
||||
const { isCSSAnimationDisabled } = useSettings();
|
||||
return (
|
||||
<div className={cx('flex text-sm max-w-full shrink-0 overflow-hidden text-foreground px-2 h-10', className)}>
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
title={started ? 'stop' : 'play'}
|
||||
className={cx('px-2 hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
|
||||
>
|
||||
<span className={cx('flex items-center space-x-2')}>
|
||||
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
|
||||
{!isEmbedded && <span>{pending ? '...' : started ? 'stop' : 'play'}</span>}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEvaluate}
|
||||
title="update"
|
||||
className={cx('flex items-center space-x-1 px-2', !isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50')}
|
||||
>
|
||||
{!isEmbedded && <span>update</span>}
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
title="share"
|
||||
className={cx('cursor-pointer hover:opacity-50 flex items-center space-x-1 px-2')}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<a
|
||||
title="learn"
|
||||
href={`${baseNoTrailing}/workshop/getting-started/`}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelCloseButton() {
|
||||
const { isPanelOpen } = useSettings();
|
||||
return (
|
||||
isPanelOpen && (
|
||||
<button
|
||||
onClick={() => setIsPanelOpened(false)}
|
||||
className={cx('px-2 py-0 text-foreground hover:opacity-50')}
|
||||
aria-label="Close Menu"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function BottomPanel({ context }) {
|
||||
const { isPanelOpen, activeFooter: tab } = useSettings();
|
||||
return (
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')}
|
||||
className={cx(
|
||||
isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
|
||||
'overflow-hidden flex flex-col relative',
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
|
||||
<PanelCloseButton />
|
||||
<Tabs setTab={setTab} tab={tab} className={cx(isPanelOpen && 'border-l border-muted')} />
|
||||
</div>
|
||||
{isPanelOpen && (
|
||||
<div className="flex h-full overflow-auto pr-10 ">
|
||||
<div className="w-full h-full overflow-auto border-t border-muted">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute right-4 pt-4">
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
</div>
|
||||
</PanelNav>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerticalPanel({ context }) {
|
||||
export function RightPanel({ context }) {
|
||||
const settings = useSettings();
|
||||
const { activeFooter: tab, isPanelOpen } = settings;
|
||||
|
||||
if (!isPanelOpen) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<PanelNav
|
||||
settings={settings}
|
||||
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')}
|
||||
>
|
||||
{isPanelOpen ? (
|
||||
<div className={cx('flex flex-col h-full')}>
|
||||
<div className="flex justify-between w-full ">
|
||||
<Tabs setTab={setTab} tab={tab} />
|
||||
<PanelActionButton settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setIsPanelOpened(true);
|
||||
}}
|
||||
aria-label="open menu panel"
|
||||
className={cx(
|
||||
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
|
||||
)}
|
||||
>
|
||||
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
|
||||
</button>
|
||||
className={cx(
|
||||
'border-l border-muted shrink-0 h-full overflow-hidden',
|
||||
isPanelOpen ? `min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12',
|
||||
)}
|
||||
>
|
||||
<div className={cx('flex flex-col h-full')}>
|
||||
<div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
|
||||
<PanelCloseButton />
|
||||
<Tabs setTab={setTab} tab={tab} className="border-l border-muted" />
|
||||
</div>
|
||||
<div className="overflow-auto h-full">
|
||||
<PanelContent context={context} tab={tab} />
|
||||
</div>
|
||||
</div>
|
||||
</PanelNav>
|
||||
);
|
||||
}
|
||||
@@ -89,8 +216,8 @@ if (TAURI) {
|
||||
tabNames.files = 'files';
|
||||
}
|
||||
|
||||
function PanelNav({ children, className, settings, ...props }) {
|
||||
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
|
||||
function PanelNav({ children, className, ...props }) {
|
||||
const settings = useSettings();
|
||||
return (
|
||||
<nav
|
||||
onClick={() => {
|
||||
@@ -98,18 +225,8 @@ function PanelNav({ children, className, settings, ...props }) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (isHoverBehavior && !settings.isPanelOpen) {
|
||||
setIsPanelOpened(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isHoverBehavior && !settings.isPanelPinned) {
|
||||
setIsPanelOpened(false);
|
||||
}
|
||||
}}
|
||||
aria-label="Menu Panel"
|
||||
className={cx('bg-lineHighlight group overflow-x-auto', className)}
|
||||
className={cx('h-full bg-lineHighlight group overflow-x-auto', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -145,7 +262,7 @@ function PanelTab({ label, isSelected, onClick }) {
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
'h-10 px-2 text-sm border-t-2 border-t-transparent text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b-2',
|
||||
isSelected ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
@@ -154,65 +271,36 @@ function PanelTab({ label, isSelected, onClick }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
function Tabs({ setTab, tab, className }) {
|
||||
function Tabs({ className }) {
|
||||
const { isPanelOpen, activeFooter: tab } = useSettings();
|
||||
return (
|
||||
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
|
||||
<div
|
||||
className={cx(
|
||||
'px-2 w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{Object.keys(tabNames).map((key) => {
|
||||
const val = tabNames[key];
|
||||
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
|
||||
return <PanelTab key={key} isSelected={tab === val && isPanelOpen} label={key} onClick={() => setTab(val)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelActionButton({ settings }) {
|
||||
const { togglePanelTrigger, isPanelPinned, isPanelOpen } = settings;
|
||||
const isHoverBehavior = togglePanelTrigger === 'hover';
|
||||
if (!isPanelOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHoverBehavior) {
|
||||
return <PinButton pinned={isPanelPinned} />;
|
||||
}
|
||||
return <CloseButton onClick={() => setIsPanelOpened(false)} />;
|
||||
}
|
||||
|
||||
function PinButton({ pinned }) {
|
||||
export function PanelToggle({ isEmbedded, isZen }) {
|
||||
const { panelPosition, isPanelOpen } = useSettings();
|
||||
return (
|
||||
<button
|
||||
onClick={() => setPanelPinned(!pinned)}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
pinned ? 'flex' : 'hidden',
|
||||
)}
|
||||
aria-label="Pin Menu Panel"
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill={'currentColor'}
|
||||
strokeWidth="0"
|
||||
className="w-full h-full"
|
||||
opacity={pinned ? 1 : '.3'}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
!isEmbedded &&
|
||||
!isZen &&
|
||||
panelPosition === 'right' && (
|
||||
<button
|
||||
title="menu"
|
||||
className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
|
||||
onClick={() => setIsPanelOpened(!isPanelOpen)}
|
||||
>
|
||||
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
|
||||
)}
|
||||
aria-label="Close Menu"
|
||||
>
|
||||
<XMarkIcon />
|
||||
</button>
|
||||
<Bars3Icon className="w-6 h-6" />
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useViewingPatternData,
|
||||
userPattern,
|
||||
} from '../../../user_pattern_utils.mjs';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getMetadata } from '../../../metadata_parser.js';
|
||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
@@ -18,7 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -43,7 +43,7 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
className={cx(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||
showOutline && 'outline outline-1',
|
||||
showHiglight && 'bg-selection',
|
||||
showHiglight && 'ring-selection',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -53,11 +53,10 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
}
|
||||
|
||||
function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const viewingPatternData = useViewingPatternData();
|
||||
const viewingPatternID = viewingPatternData.id;
|
||||
return (
|
||||
<div className="">
|
||||
<div className="p-2">
|
||||
{Object.values(patterns)
|
||||
.reverse()
|
||||
.map((pattern) => {
|
||||
@@ -83,8 +82,8 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
export function PatternsTab({ context }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const viewingPatternData = useViewingPatternData();
|
||||
|
||||
const { userPatterns, patternAutoStart } = useSettings();
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
|
||||
@@ -121,74 +120,71 @@ export function PatternsTab({ context }) {
|
||||
);
|
||||
}),
|
||||
);
|
||||
}, [search, viewingPatternStore]);
|
||||
}, [search, userPatterns]);
|
||||
|
||||
const importRef = useRef();
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
|
||||
<Textbox className="w-full border-0" placeholder="Search..." value={search} onChange={setSearch} />
|
||||
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-y border-muted">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={importRef}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
<ActionButton label="import" onClick={() => importRef.current.click()} />
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
{/* bg-background */}
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
}
|
||||
}}
|
||||
patterns={visiblePatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
</div>
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
}
|
||||
}}
|
||||
patterns={visiblePatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState, Fragment } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab';
|
||||
import { settingsMap, useSettings } from '@src/settings.mjs';
|
||||
|
||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||
|
||||
@@ -10,8 +11,16 @@ const availableFunctions = (() => {
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
if (seen.has(doc.name)) continue;
|
||||
|
||||
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
|
||||
// @something is. We only want data from comments like `@tags fx, superdough` here.
|
||||
// If nothing is specified, we default to "untagged" for debugging
|
||||
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
|
||||
functions.push(doc);
|
||||
|
||||
const synonyms = doc.synonyms || [];
|
||||
seen.add(doc.name);
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
@@ -29,70 +38,182 @@ const availableFunctions = (() => {
|
||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
const tagCounts = {};
|
||||
// const tagOptions = { all: `all (${availableFunctions.length})` };
|
||||
const tagOptions = { all: `all` };
|
||||
for (const doc of availableFunctions) {
|
||||
(doc.tags || ['untagged']).forEach((t) => {
|
||||
if (typeof t === 'string' && t) {
|
||||
tagCounts[t] = (tagCounts[t] || 0) + 1;
|
||||
//tagOptions[t] = `${t} (${tagCounts[t]})`;
|
||||
tagOptions[t] = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Reference() {
|
||||
export const Reference = memo(function Reference() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedFunction, setSelectedFunction] = useState(null);
|
||||
const { referenceTag } = useSettings();
|
||||
|
||||
const visibleFunctions = useMemo(() => {
|
||||
const toggleTag = (tag) => {
|
||||
if (referenceTag === tag) {
|
||||
setReferenceTag('all');
|
||||
} else {
|
||||
setReferenceTag(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const searchVisibleFunctions = useMemo(() => {
|
||||
return availableFunctions.filter((entry) => {
|
||||
if (referenceTag && referenceTag !== 'all') {
|
||||
if (!(entry.tags || ['untagged']).includes(referenceTag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lowerCaseSearch = search.toLowerCase();
|
||||
return (
|
||||
entry.name.toLowerCase().includes(lowerCaseSearch) ||
|
||||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||
);
|
||||
});
|
||||
}, [search]);
|
||||
}, [search, referenceTag]);
|
||||
|
||||
const detailVisibleFunctions = useMemo(() => {
|
||||
return searchVisibleFunctions.filter((x) => {
|
||||
if (referenceTag === null || referenceTag === 'all') {
|
||||
if (search) {
|
||||
return true;
|
||||
}
|
||||
return x.name === selectedFunction;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}, [searchVisibleFunctions, selectedFunction, referenceTag]);
|
||||
|
||||
const onSearchTagFilterClick = () => {
|
||||
setReferenceTag('all');
|
||||
setSelectedFunction(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFunction) {
|
||||
const el = document.getElementById(`doc-${selectedFunction}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}
|
||||
}, [selectedFunction]);
|
||||
|
||||
let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div className="flex h-full w-full overflow-hidden">
|
||||
<div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted">
|
||||
{/* bg-background */}
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
<Textbox
|
||||
className="w-full border-0 border-b border-muted"
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSelectedFunction(null);
|
||||
setReferenceTag('all');
|
||||
setSearch(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
|
||||
{/* <div className="flex shrink-0 flex-wrap w-full overflow-auto border-y border-muted">
|
||||
<ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}></ButtonGroup>
|
||||
</div> */}
|
||||
{referenceTag && referenceTag !== 'all' && (
|
||||
<div className="w-72">
|
||||
<span
|
||||
className="text-foreground border border-muted border-t-0 border-l-0 px-1 py-0.5 my-2 cursor-pointer font-sans"
|
||||
onClick={onSearchTagFilterClick}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
{referenceTag}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full p-2 overflow-y-auto bg-opacity-50">
|
||||
{searchVisibleFunctions.map((entry, i) => (
|
||||
<Fragment key={`entry-${entry.name}`}>
|
||||
<a
|
||||
className={
|
||||
'cursor-pointer hover:opacity-50 text-ellipsis block' +
|
||||
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
|
||||
}
|
||||
onClick={() => {
|
||||
if (entry.name === selectedFunction) {
|
||||
setSelectedFunction(null);
|
||||
} else {
|
||||
setSelectedFunction(entry.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{entry.name}
|
||||
</a>{' '}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
|
||||
className="w-2/3 break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
|
||||
id="reference-container"
|
||||
>
|
||||
<div className="prose dark:prose-invert min-w-full px-1 ">
|
||||
<div className="prose dark:prose-invert min-w-full px-1 text-sm">
|
||||
<h2>API Reference</h2>
|
||||
<p>
|
||||
<p className="font-sans text-md">
|
||||
This is the long list of functions you can use. Remember that you don't need to remember all of those and
|
||||
that you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
<h3 id={`doc-${i}`}>{entry.name}</h3>
|
||||
<div>
|
||||
{/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/}
|
||||
{Object.entries(tagCounts)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([t, count]) => (
|
||||
<span key={t}>
|
||||
<a
|
||||
className={[
|
||||
'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans',
|
||||
`${referenceTag === t ? 'bg-muted text-foreground' : ''}`,
|
||||
].join(' ')}
|
||||
onClick={() => toggleTag(t)}
|
||||
>
|
||||
{t} ({count})
|
||||
</a>{' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{detailVisibleFunctions.map((entry, i) => (
|
||||
<section key={i} className="font-sans">
|
||||
<div className="flex flex-row items-center mt-8 justify-between">
|
||||
<h3 className="font-mono my-0 pt-4" id={`doc-${entry.name}`}>
|
||||
{entry.name}
|
||||
</h3>
|
||||
{entry.tags && (
|
||||
<span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
|
||||
{entry.tags.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!!entry.synonyms_text && (
|
||||
<p>
|
||||
Synonyms: <code>{entry.synonyms_text}</code>
|
||||
</p>
|
||||
)}
|
||||
{/* <small>{entry.meta.filename}</small> */}
|
||||
|
||||
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
|
||||
<ul>
|
||||
{entry.params?.map(({ name, type, description }, i) => (
|
||||
@@ -102,14 +223,15 @@ export function Reference() {
|
||||
))}
|
||||
</ul>
|
||||
{entry.examples?.map((example, j) => (
|
||||
<pre className="bg-background" key={j}>
|
||||
<pre className="bg-background border border-muted" key={j}>
|
||||
{example}
|
||||
</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
)) || <p className="font-sans">Search or select a tag to get started.</p>}
|
||||
{detailVisibleFunctions.length > 0 && <div className="h-screen" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
// value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
|
||||
export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
|
||||
return (
|
||||
<select
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
className="p-2 bg-background rounded-md text-foreground border-foreground"
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
|
||||
{Array.from(options.keys()).map((id) => (
|
||||
<option key={id} className="bg-background" value={id}>
|
||||
{options.get(id)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { themes } from '@strudel/codemirror';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
@@ -10,22 +9,60 @@ import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@
|
||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
||||
|
||||
export function Textbox({ onChange, className, ...inputProps }) {
|
||||
return (
|
||||
<input className={cx('px-2', inputClass, className)} onChange={(e) => onChange(e.target.value)} {...inputProps} />
|
||||
);
|
||||
}
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
<label>
|
||||
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} />
|
||||
<label className="text-xs">
|
||||
<input
|
||||
className={cx(
|
||||
'bg-background text-sm border border-muted focus:outline-none focus:ring-0 focus:border-foreground',
|
||||
)}
|
||||
disabled={disabled}
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{' ' + label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectInput({ value, options, onChange }) {
|
||||
// value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
|
||||
export function SelectInputDuplicate({ value, options, onChange, onClick, isDisabled }) {
|
||||
return (
|
||||
<select
|
||||
className="p-2 bg-background rounded-md text-foreground border-foreground"
|
||||
value={value}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
className={cx('p-2', inputClass)}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
|
||||
{Array.from(options.keys()).map((id) => (
|
||||
<option key={id} className="bg-background" value={id}>
|
||||
{options.get(id)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectInput({ value, options, onChange }) {
|
||||
return (
|
||||
<select className={cx('p-2', inputClass)} value={value} onChange={(e) => onChange(e.target.value)}>
|
||||
{Object.entries(options).map(([k, label]) => (
|
||||
<option key={k} className="bg-background" value={k}>
|
||||
{label}
|
||||
@@ -37,9 +74,9 @@ function SelectInput({ value, options, onChange }) {
|
||||
|
||||
function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
||||
return (
|
||||
<div className="flex space-x-2 gap-1">
|
||||
<div className="flex space-x-2 gap-1 overflow-hidden">
|
||||
<input
|
||||
className="p-2 grow"
|
||||
className="p-2 grow accent-foreground"
|
||||
type="range"
|
||||
value={value}
|
||||
step={step}
|
||||
@@ -50,7 +87,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
||||
type="number"
|
||||
value={value}
|
||||
step={step}
|
||||
className="w-16 bg-background rounded-md"
|
||||
className={cx('w-16', inputClass)}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
@@ -59,8 +96,8 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
||||
|
||||
function FormItem({ label, children, sublabel }) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<label>{label}</label>
|
||||
<div className="grid gap-2 text-xs">
|
||||
<label className="text-sm">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -107,7 +144,6 @@ export function SettingsTab({ started }) {
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
togglePanelTrigger,
|
||||
maxPolyphony,
|
||||
multiChannelOrbits,
|
||||
isTabIndentationEnabled,
|
||||
@@ -118,7 +154,7 @@ export function SettingsTab({ started }) {
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
|
||||
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -178,7 +214,7 @@ export function SettingsTab({ started }) {
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem label="Font Family">
|
||||
<SelectInput
|
||||
options={fontFamilyOptions}
|
||||
@@ -219,13 +255,6 @@ export function SettingsTab({ started }) {
|
||||
items={{ bottom: 'Bottom', right: 'Right' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
<FormItem label="Open Panel on: ">
|
||||
<ButtonGroup
|
||||
value={togglePanelTrigger}
|
||||
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
|
||||
items={{ click: 'Click', hover: 'Hover' }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="More Settings">
|
||||
<Checkbox
|
||||
label="Enable bracket matching"
|
||||
@@ -294,7 +323,7 @@ export function SettingsTab({ started }) {
|
||||
value={isSyncEnabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hide top buttons"
|
||||
label="Hide action buttons"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
|
||||
value={isButtonRowHidden}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { confirmDialog } from '@src/repl/util.mjs';
|
||||
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
|
||||
@@ -77,11 +77,12 @@ export function SoundsTab() {
|
||||
numRef.current = 0;
|
||||
});
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
<div id="sounds-tab" className="flex flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search..." className="border-0" value={search} onChange={(v) => setSearch(v)} />
|
||||
|
||||
<div className=" flex shrink-0 flex-wrap">
|
||||
<div className="flex shrink-0 flex-wrap border-y border-muted">
|
||||
<ButtonGroup
|
||||
wrap
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
items={{
|
||||
@@ -114,7 +115,7 @@ export function SoundsTab() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
|
||||
<div className="min-h-0 max-h-full grow overflow-auto break-normal p-2">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
@@ -165,7 +166,7 @@ export function SoundsTab() {
|
||||
);
|
||||
})}
|
||||
{!soundEntries.length && soundsFilter === 'importSounds' ? (
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
|
||||
<div className="prose dark:prose-invert min-w-full text-sm">
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
<p>
|
||||
To import sounds into strudel, they must be contained{' '}
|
||||
|
||||
@@ -6,8 +6,8 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
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>
|
||||
<div className="prose dark:prose-invert min-w-full py-4 font-sans px-4 text-sm" style={{ fontFamily }}>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ function getUpdatedLog(log, event) {
|
||||
} else {
|
||||
log = log.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return log.slice(-20);
|
||||
return log.slice(-40);
|
||||
}
|
||||
|
||||
export function useLogger() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { persistentMap } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel/core';
|
||||
import { isUdels } from './repl/util.mjs';
|
||||
import { computed } from 'nanostores';
|
||||
|
||||
export const audioEngineTargets = {
|
||||
webaudio: 'webaudio',
|
||||
@@ -38,12 +39,12 @@ export const defaultSettings = {
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
soundsFilter: soundFilterType.ALL,
|
||||
referenceTag: 'all',
|
||||
patternFilter: 'community',
|
||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||
panelPosition: 'right',
|
||||
isPanelPinned: false,
|
||||
isPanelOpen: true,
|
||||
togglePanelTrigger: 'click', //click | hover
|
||||
userPatterns: '{}',
|
||||
prebakeScript: '',
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
@@ -64,11 +65,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
|
||||
|
||||
export const settingsMap = persistentMap(settings_key, defaultSettings);
|
||||
|
||||
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
|
||||
|
||||
export function useSettings() {
|
||||
const state = useStore(settingsMap);
|
||||
|
||||
export const $settings = computed(settingsMap, (state) => {
|
||||
const userPatterns = JSON.parse(state.userPatterns);
|
||||
Object.keys(userPatterns).forEach((key) => {
|
||||
const data = userPatterns[key];
|
||||
@@ -105,6 +102,12 @@ export function useSettings() {
|
||||
? true
|
||||
: parseBoolean(state.patternAutoStart),
|
||||
};
|
||||
});
|
||||
|
||||
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
|
||||
|
||||
export function useSettings() {
|
||||
return useStore($settings);
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user