Compare commits

..

24 Commits

Author SHA1 Message Date
Tristan de Cacqueray fc335c9930 Remove PicoGL dependency 2024-12-22 08:04:07 -05:00
Tristan de Cacqueray 0d67176a98 Improve the API when using a frame modifier 2024-10-27 11:46:49 -04:00
Tristan de Cacqueray 77bbb9626a Remove uniform pattern function and expose the shader instance 2024-10-27 10:17:41 -04:00
Tristan de Cacqueray 65cc317382 Automatically set onTrigger when the hap value is not a number
This change makes the following demo works: `s("bd").uniform("rot")`
2024-10-21 10:20:50 -04:00
Tristan de Cacqueray 36d4d89052 Fix lint error 2024-10-21 09:23:31 -04:00
Tristan de Cacqueray f8a241401c Add uniform pattern function doc 2024-10-20 22:06:41 -04:00
Tristan de Cacqueray 1baa6da370 Update uniform method to take an option object 2024-10-20 19:55:12 -04:00
Tristan de Cacqueray 9e0b840afd Fix default uniformTrigger indexs 2024-10-20 15:13:49 -04:00
Felix Roos e6fbef95f2 Merge branch 'main' into shader 2024-10-20 21:07:26 +02:00
Tristan de Cacqueray cad11e1e48 Update uniform value and add tests 2024-10-20 13:46:33 -04:00
Tristan de Cacqueray b85a5a9179 Use pat.withValue and add uniformTrigger 2024-10-20 09:58:10 -04:00
Tristan de Cacqueray 9b4675f1ce Update the shader todos 2024-10-19 09:24:01 -04:00
Felix Roos 67c0ebaf54 snapshot 2024-10-19 15:17:09 +02:00
Felix Roos 8d1180f87a fix: test runtime for uniform function 2024-10-19 15:15:25 +02:00
Felix Roos fcd550b5bd codeformat 2024-10-19 15:13:21 +02:00
Felix Roos d3e624f673 Merge branch 'shader' of https://github.com/TristanCacqueray/strudel into shader 2024-10-19 15:12:02 +02:00
Tristan de Cacqueray b0149f8190 Refactor the uniform function and add documentation 2024-10-19 09:08:33 -04:00
Felix Roos bf3a095852 codeformat 2024-10-19 15:07:48 +02:00
Tristan de Cacqueray 5c983ceb8f Extract the uniform helper to its own module 2024-10-19 06:43:50 -04:00
Tristan de Cacqueray 735d531cce Move shader module into it's own package 2024-10-19 06:29:19 -04:00
Felix Roos 13168bb9bc codeformat 2024-10-19 01:15:26 +02:00
Felix Roos 46b4aff0ef Merge remote-tracking branch 'origin/main' into shader 2024-10-19 01:14:06 +02:00
Tristan de Cacqueray 1b8998ac14 Fix shader reload
The attribute for the draw call is called drawFrame, not draw.
2024-10-18 18:15:15 -04:00
Tristan de Cacqueray f0ddb2d595 Add initial shader module
This change adds the `loadShader` and `shader` function, to be used like this:

```strudel
await loadShader`
  // The modulation targets
  uniform float iColor;

  void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    vec2 uv = fragCoord / iResolution.xy;
    vec3 col = 0.5 + 0.5*cos(iColor+uv.xyx+vec3(0,2,4));
    fragColor = vec4(col, 0);
  }
`

$: s("bd").shader({uniform: 'iColor'})
```
2024-10-18 11:40:09 -04:00
38 changed files with 907 additions and 705 deletions
+4 -10
View File
@@ -14,16 +14,10 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/)
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
pnpm i
```
4. Run the development server:
```bash
pnpm dev
```
```bash
pnpm i
pnpm dev
```
## Using Strudel In Your Project
+1
View File
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
import('@strudel/core'),
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/shader'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
);
+8 -23
View File
@@ -13,7 +13,7 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { repl, registerControl } from '@strudel/core';
import { Pattern, repl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
@@ -24,7 +24,6 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { prettierPlugin } from './prettier.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -78,7 +77,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
javascript(),
sliderPlugin,
widgetPlugin,
prettierPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
@@ -135,7 +133,6 @@ export class StrudelMirror {
autodraw,
prebake,
bgFill = true,
solo = true,
...replOptions
} = options;
this.code = initialCode;
@@ -146,7 +143,6 @@ export class StrudelMirror {
this.drawContext = drawContext;
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -163,14 +159,12 @@ export class StrudelMirror {
replOptions?.onToggle?.(started);
if (started) {
this.drawer.start(this.repl.scheduler);
if (this.solo) {
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
}
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
@@ -225,7 +219,7 @@ export class StrudelMirror {
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (this.solo && e.detail !== this.id) {
if (e.detail !== this.id) {
this.stop();
}
};
@@ -360,12 +354,3 @@ function s4() {
.toString(16)
.substring(1);
}
/**
* Overrides the css of highlighted events. Make sure to use single quotes!
* @name markcss
* @example
* note("c a f e")
* .markcss('text-decoration:underline')
*/
export const markcss = registerControl('markcss');
+1 -2
View File
@@ -93,7 +93,6 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)';
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
@@ -115,7 +114,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style },
attributes: { style: `outline: solid 2px ${color}` },
}),
);
}
+1 -2
View File
@@ -49,8 +49,7 @@
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5",
"prettier": "^3.3.3"
"nanostores": "^0.9.5"
},
"devDependencies": {
"vite": "^5.0.10"
-55
View File
@@ -1,55 +0,0 @@
import { EditorSelection } from '@codemirror/state';
import * as prettier from 'prettier/standalone';
import typescriptPlugin from 'prettier/plugins/typescript';
import estreePlugin from 'prettier/plugins/estree';
import { keymap } from '@codemirror/view';
export async function runPrettier(editorView) {
const currentState = editorView.state.doc.toString();
// Prettier insists on consistent quotes, but in Strudel double quotes are interpreted
// as patterns and single quotes are for everything else, so a consistent setting won't work.
// It's a great formatter though, so as a workaround it works to put "// prettier-ignore" comments
// before the single quoted stuff to preserve it, but this is a pain to make users to, so an extra
// hack is to take the preformatted code, insert these comments behind the scenes, format, then
// remove them before setting the editor state.
const preFormat = currentState
.split('\n')
.map((line) => (line.match(/'.*'/) != null ? '// prettier-ignore\n' + line : line))
.join('\n');
console.log(preFormat);
const formattedState = (
await prettier.format(preFormat, {
parser: 'typescript',
plugins: [typescriptPlugin, estreePlugin],
semi: false,
})
).replace(/.*\/\/ prettier-ignore.*\n/g, '');
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: formattedState },
selection: EditorSelection.single(
// keep cursor close to the original position, but also keep it within the bounds
// of the formatted document
Math.min(editorView.state.selection.main.to, formattedState.length),
),
scrollIntoView: true,
});
}
export const prettierPlugin = keymap.of([
{
key: 'Alt-,',
preventDefault: true,
run: runPrettier,
},
{
key: 'Ctrl-,',
preventDefault: true,
run: runPrettier,
},
{
key: 'Alt-Shift-f',
preventDefault: true,
run: runPrettier,
},
]);
+20 -20
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
+20 -20
View File
@@ -15,25 +15,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+20 -20
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+19 -19
View File
@@ -14,24 +14,24 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});
+19 -19
View File
@@ -16,24 +16,24 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});
-1
View File
@@ -14,7 +14,6 @@ export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
export * from './pick.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
+2 -9
View File
@@ -2593,7 +2593,7 @@ export const steps = register('steps', function (targetTactus, pat) {
// avoid divide by zero..
return nothing;
}
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
return pat.fast(Fraction(targetTactus).div(pat.tactus));
});
export function _polymeterListSteps(steps, ...args) {
@@ -2801,7 +2801,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
return pat.s_add(pat.tactus.sub(i));
});
export const s_extend = stepRegister('s_extend', function (factor, pat) {
export const s_cycles = stepRegister('s_extend', function (factor, pat) {
return pat.fast(factor).s_expand(factor);
});
@@ -2882,13 +2882,6 @@ export const s_tour = function (pat, ...many) {
return pat.s_tour(...many);
};
const s_zip = function (...pats) {
pats = pats.filter((pat) => pat.hasTactus);
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
// Should maybe use lcm or gcd for tactus?
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
};
//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of
// objects
-214
View File
@@ -1,214 +0,0 @@
/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, reify, silence, register } from './pattern.mjs';
import { _mod, clamp, objectMap } from './util.mjs';
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
+221 -29
View File
@@ -1,13 +1,13 @@
/*
signal.mjs - continuous patterns
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
signal.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, _mod } from './util.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -253,6 +253,211 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
export const __chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
@@ -391,11 +596,8 @@ export const perlinWith = (pat) => {
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register(
'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
true,
true,
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
);
/**
@@ -412,14 +614,9 @@ export const degradeByWith = register(
* @example
* s("[hh?0.2]*8")
*/
export const degradeBy = register(
'degradeBy',
function (x, pat) {
return pat._degradeByWith(rand, x);
},
true,
true,
);
export const degradeBy = register('degradeBy', function (x, pat) {
return pat._degradeByWith(rand, x);
});
/**
*
@@ -433,7 +630,7 @@ export const degradeBy = register(
* @example
* s("[hh?]*8")
*/
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
/**
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
@@ -453,17 +650,12 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
* x => x.undegradeBy(0.8).pan(1)
* )
*/
export const undegradeBy = register(
'undegradeBy',
function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
},
true,
true,
);
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
});
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
@@ -480,7 +672,7 @@ export const undegradeBy = register(
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
*
+1
View File
@@ -12,6 +12,7 @@ export async function prebake() {
import('@strudel/webaudio'),
import('@strudel/codemirror'),
import('@strudel/hydra'),
import('@strudel/shader'),
import('@strudel/soundfonts'),
import('@strudel/midi'),
// import('@strudel/xen'),
-4
View File
@@ -10,8 +10,6 @@ if (typeof HTMLElement !== 'undefined') {
static observedAttributes = ['code'];
settings = codemirrorSettings.get();
editor = null;
sync = false;
solo = true;
constructor() {
super();
}
@@ -51,8 +49,6 @@ if (typeof HTMLElement !== 'undefined') {
});
this.dispatchEvent(event);
},
solo: this.solo,
sync: this.sync,
});
// init settings
this.editor.updateSettings(this.settings);
+16
View File
@@ -0,0 +1,16 @@
# @strudel/shader
Helpers for drawing shader.
## Todos
Here are the things that needs to be implemented:
- [ ] Shader compilation error reporting, e.g. to show the line number
- [ ] Shader import from url, like shadertoy or git
- [ ] Display shader author attribution, e.g. to respect CC-BY
- [ ] Handle WebGL context lost by restoring the objects.
- [ ] Multiple instances and custom canvas positions
- [ ] Multiple programs, to be swapped like a pattern
- [ ] Texture inputs
- [ ] Buffer inputs, e.g. to generate a texture
+1
View File
@@ -0,0 +1 @@
export * from './shader.mjs';
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@strudel/shader",
"version": "1.0.0",
"description": "Helpers for drawing shader",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave",
"shader"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"picogl": "^0.17.9"
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
}
}
+347
View File
@@ -0,0 +1,347 @@
/*
shader.mjs - implements the `loadShader` function
Copyright (C) 2024 Strudel contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
/// Here is a feature demo
// Setup a shader
let truchetFTW = fetch('https://raw.githubusercontent.com/TristanCacqueray/shaders/refs/heads/main/shaders/Truchet%20%2B%20Kaleidoscope%20FTW.glsl').then((res) => res.text())
// This shader provides the following uniforms:
// uniform float icolor;
// uniform float moveFWD;
// uniform float rotations[4];
// uniform float modulations[6];
// Start the instance and binds the uniforms
let {uniforms} = await loadShader(truchetFTW)
setcpm(96)
// A smoothing function that is called for each frame
let smooth = (desired, speed) => (value) => value + ((desired - value) / speed)
// Each kick updates a different rotation value.
let rotationIndex = 0
$: s("bd").bank("RolandTR808")
.gain(2).dist("<1 .7 .7 .7>")
.mask("<1@30 0@2>")
.onTrigger(() => uniforms.rotations.set(
cur => smooth(cur + 1, 20), rotationIndex++),
false)
// Each hat increase the icolor value.
$: sound("hh*4").bank("RolandTR808")
.room(.3).gain(".25 .3 .4")
.mask("<0@8 1@32>")
.onTrigger(() => uniforms.icolor.incr(0.1), false)
// The snare smoothly increase the moveFWD value
$: s("cp/8").bank("RolandTR808")
.hpf(500).hpa(.8).hpenv("<-3 -2 -3 -2 -1>/8")
.room(0.5).roomsize(7).rlp(5000).gain(.2)
.onTrigger(() => uniforms.moveFWD.set(cur => smooth(cur + 1, 30)), false)
// Each piano note updates a different modulations value
let pianoPitches = {}
$: note("<C D G A Bb D C A G D Bb A>*[2,2.02]")
.clip(1.1)
.transpose("<-12 -24 -12 0>/8")
// .sound("sawtooth")
.sound("triangle")
.cutoff(perlin.slow(5).range(20,1200))
.room(.8).roomsize(.6)
.gain(.4)
.onTrigger((_, hap) => {
const n = hap.value.note
// assign unique array position for each new notes
if (!pianoPitches[n]) pianoPitches[n] = Object.keys(pianoPitches).length + 1
const idx = pianoPitches[n]
uniforms.modulations.set(cur => smooth(cur + .5, 55), idx)
}, false)
*/
import { logger } from '@strudel/core';
// The standard fullscreen vertex shader.
const vertexShader = `#version 300 es
precision highp float;
layout(location=0) in vec2 position;
void main() {
gl_Position = vec4(position, 1, 1);
}
`;
// Make the fragment source, similar to the one from shadertoy.
function mkFragmentShader(code) {
return `#version 300 es
precision highp float;
out vec4 oColor;
uniform float iTime;
uniform vec2 iResolution;
#define STRUDEL 1
${code}
void main(void) {
mainImage(oColor, gl_FragCoord.xy);
}
`;
}
// Helper class to handle uniform updates
class UniformValue {
constructor(count, draw) {
this.draw = draw;
this.value = new Array(count).fill(0);
this.frameModifier = new Array(count).fill(null);
}
// Helper to perform a simple increment
incr(value, pos = 0) {
const idx = pos % this.value.length;
this.value[idx] += value;
this.frameModifier[idx] = null;
this.draw();
}
// The value can be a function that will be called for each rendering frame
set(value, pos = 0) {
const idx = pos % this.value.length;
if (typeof value === 'function') {
this.frameModifier[idx] = value(this.value[idx]);
} else {
this.value[idx] = value;
this.frameModifier[idx] = null;
}
this.draw();
}
get(pos = 0) {
return this.value[pos % this.value.length];
}
// This function is called for every frame, allowing to run a smooth modifier
_frameUpdate(elapsed) {
this.value = this.value.map((value, idx) =>
this.frameModifier[idx] ? this.frameModifier[idx](value, elapsed) : value,
);
return this.value;
}
// When the shader is update, this function adjust the number of values, preserving the current one
_resize(count) {
if (count != this.count) {
count = Math.max(1, count);
resizeArray(this.value, count, 0);
resizeArray(this.frameModifier, count, null);
}
}
}
// Shrink or extend an array
function resizeArray(arr, size, defval) {
if (arr.length > size) arr.length = size;
else arr.push(...new Array(size - arr.length).fill(defval));
}
// Setup the instance's uniform after shader compilation.
function setupUniforms(instance) {
const newUniforms = new Set();
const draw = () => {
// Start the drawing loop
instance.age = 0;
if (!instance.drawing) {
instance.drawing = requestAnimationFrame(instance.update);
}
};
// Collect every available uniforms
let gl = instance.gl;
const numUniforms = instance.gl.getProgramParameter(instance.program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < numUniforms; ++i) {
const inf = gl.getActiveUniform(instance.program, i);
// Arrays have a `[0]` suffix in their name, drop that
const name = inf.name.replace('[0]', '');
// Figure out how many values is this uniform, and how to update it.
let count = inf.size;
let updateFunc = 'uniform1fv';
switch (inf.type) {
case gl.FLOAT_VEC2:
count *= 2;
updateFunc = 'uniform2fv';
break;
case gl.FLOAT_VEC3:
count *= 3;
updateFunc = 'uniform3fv';
break;
case gl.FLOAT_VEC4:
count *= 4;
updateFunc = 'uniform4fv';
break;
}
// This is a new uniform
if (!instance.uniforms[name]) instance.uniforms[name] = new UniformValue(count, draw);
// This is a known uniform, make sure it's size is correct
else instance.uniforms[name]._resize(count);
// Record it's location for the 'updateUniforms' below.
instance.uniforms[name].loc = gl.getUniformLocation(instance.program, inf.name);
instance.uniforms[name].updateFunc = updateFunc;
// Record the name so that unused uniform can be deleted below
newUniforms.add(name);
}
// Remove deleted uniforms
Object.keys(instance.uniforms).forEach((name) => {
if (!newUniforms.has(name)) delete instance.uniforms[name];
});
}
// Update the uniforms for a given drawFrame call.
function updateUniforms(gl, now, elapsed, uniforms) {
Object.entries(uniforms).forEach(([name, uniform]) => {
try {
if (name == 'iTime') {
gl.uniform1f(uniform.loc, now);
} else if (name == 'iResolution') {
gl.uniform2f(uniform.loc, gl.canvas.width, gl.canvas.height);
} else {
const value = uniform._frameUpdate(elapsed);
// Send the value to the GPU
// console.log('updateUniforms:', name, uniform.updateFunc, value);
gl[uniform.updateFunc](uniform.loc, value);
}
} catch (err) {
console.warn('uniform error');
console.error(err);
}
});
}
// Setup the canvas and return the WebGL context.
function setupCanvas(name) {
// TODO: support custom size
const width = 400;
const height = 300;
const canvas = document.createElement('canvas');
canvas.id = 'cnv-' + name;
canvas.width = width;
canvas.height = height;
const top = 60 + Object.keys(_instances).length * height;
canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`;
document.body.append(canvas);
return canvas.getContext('webgl2');
}
function createProgram(gl, vertex, fragment) {
const compile = (type, source) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
const err = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw err;
}
return shader;
};
const program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vertex));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragment));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const err = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw err;
}
gl.useProgram(program);
return program;
}
// Setup the shader instance
function initializeShaderInstance(name, code) {
const gl = setupCanvas(name);
// Two triangle to cover the whole canvas
const mkPositionArray = () => {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(buf, 2, gl.FLOAT, false, 0, 0);
return vao;
};
try {
let array = mkPositionArray();
let program = createProgram(gl, vertexShader, code);
const instance = { gl, code, program, array, uniforms: {} };
setupUniforms(instance);
// Render frame logic
let prev = performance.now() / 1000;
instance.age = 0;
instance.update = () => {
const now = performance.now() / 1000;
const elapsed = instance.age == 0 ? 1 / 60 : now - prev;
prev = now;
// console.log('drawing!');
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(array);
// Send the uniform values to the GPU
updateUniforms(instance.gl, now, elapsed, instance.uniforms);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// After sometime, if no update happened, stop the animation loop to save cpu cycles
if (instance.age++ < 100) requestAnimationFrame(instance.update);
else instance.drawing = false;
};
instance.update();
return instance;
} catch (err) {
gl.canvas.remove();
throw err;
}
}
// Update the instance program
function reloadShaderInstanceCode(instance, code) {
const program = createProgram(instance.gl, vertexShader, code);
instance.gl.deleteProgram(instance.program);
instance.program = program;
instance.code = code;
setupUniforms(instance);
}
// Keep track of the running shader instances
let _instances = {};
export function loadShader(code = '', name = 'default') {
if (code) {
code = mkFragmentShader(code);
}
if (!_instances[name]) {
_instances[name] = initializeShaderInstance(name, code);
logger('[shader] ready');
} else if (_instances[name].code != code) {
reloadShaderInstanceCode(_instances[name], code);
logger('[shader] reloaded');
}
return _instances[name];
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+24 -3
View File
@@ -212,9 +212,6 @@ importers:
nanostores:
specifier: ^0.9.5
version: 0.9.5
prettier:
specifier: ^3.3.3
version: 3.3.3
devDependencies:
vite:
specifier: ^5.0.10
@@ -409,6 +406,22 @@ importers:
specifier: ^5.0.10
version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
packages/shader:
dependencies:
'@strudel/core':
specifier: workspace:*
version: link:../core
picogl:
specifier: ^0.17.9
version: 0.17.9
devDependencies:
vite:
specifier: ^5.0.10
version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
vitest:
specifier: ^2.1.3
version: 2.1.3(@types/node@22.7.6)(@vitest/ui@2.1.3)(terser@5.36.0)
packages/soundfonts:
dependencies:
'@strudel/core':
@@ -632,6 +645,9 @@ importers:
'@strudel/serial':
specifier: workspace:*
version: link:../packages/serial
'@strudel/shader':
specifier: workspace:*
version: link:../packages/shader
'@strudel/soundfonts':
specifier: workspace:*
version: link:../packages/soundfonts
@@ -6087,6 +6103,9 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picogl@0.17.9:
resolution: {integrity: sha512-TfqB7jlD5FTO4a/Rp9wnMhVjPA0XZ/xbtLS2f8eHtOmVtIOhFD7Wf0jUL6kDjww3bgF/REEV9oPreBeGvWFlUQ==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@@ -14793,6 +14812,8 @@ snapshots:
picocolors@1.1.1: {}
picogl@0.17.9: {}
picomatch@2.3.1: {}
picomatch@4.0.2: {}
-21
View File
@@ -4551,27 +4551,6 @@ exports[`runs examples > example "lsize" example index 0 1`] = `
]
`;
exports[`runs examples > example "markcss" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 1/4 → 1/2 | note:a ]",
"[ 1/2 → 3/4 | note:f ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 5/4 → 3/2 | note:a ]",
"[ 3/2 → 7/4 | note:f ]",
"[ 7/4 → 2/1 | note:e ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 9/4 → 5/2 | note:a ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:e ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 13/4 → 7/2 | note:a ]",
"[ 7/2 → 15/4 | note:f ]",
"[ 15/4 → 4/1 | note:e ]",
]
`;
exports[`runs examples > example "mask" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
-3
View File
@@ -134,9 +134,6 @@ strudel.Pattern.prototype._pitchwheel = function () {
strudel.Pattern.prototype._pianoroll = function () {
return this;
};
strudel.Pattern.prototype.markcss = function () {
return this;
};
const uiHelpersMocked = {
backgroundImage: id,
+1
View File
@@ -35,6 +35,7 @@
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/shader": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
+5 -5
View File
@@ -26,15 +26,15 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
title="Top Navigation"
>
<div class="flex overflow-visible items-center grow" style="overflow:visible">
<div class="flex items-center text-2xl space-x-2">
<a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span class="block rotate-90 text-blue-500">꩜</span>
<div class="flex space-x-2 items-baseline">
<span>🌀</span>
<div class="flex space-x-1 items-baseline">
<span class="">strudel</span>
<span class="text-sm font-medium">DOCS</span>
<span class="text-sm">DOCS</span>
</div>
</h1>
</div>
</a>
</div>
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
<div class="search-item h-10">
@@ -98,7 +98,3 @@ What follows is the API doc of all the options you can pass:
## Pitchwheel
<JsDoc client:idle name="pitchwheel" h={0} />
## markcss
<JsDoc client:idle name="markcss" h={0} />
+24 -25
View File
@@ -1,4 +1,8 @@
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon';
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs';
@@ -11,19 +15,24 @@ export function Header({ context, embedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
context;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
const { isZen } = useSettings();
return (
<header
id="header"
className={cx(
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
'flex-none text-black z-[100] text-lg select-none',
!isZen && !isEmbedded && 'bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex',
)}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
{/* <img
src={logo}
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
alt="logo"
/> */}
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
@@ -34,46 +43,31 @@ export function Header({ context, embedded = false }) {
)}
>
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer', isZen && 'fixed top-2 right-4')}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block rotate-90"></span>
🌀
</div>
{!isZen && (
<div className="space-x-2">
<span className="">strudel</span>
<span className="text-sm font-medium">REPL</span>
{!isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
)}
<div className={cx(started && 'animate-pulse')}>
<span className="">strudel</span> <span className="text-sm">REPL</span>
</div>
)}
</h1>
</div>
{!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
{!isZen && (
<div className="flex max-w-full overflow-auto text-foreground">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx(
!isEmbedded ? 'p-2' : 'px-2',
'hover:opacity-50',
!started && !isCSSAnimationDisabled && 'animate-pulse',
)}
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
>
{!pending ? (
<span className={cx('flex items-center space-x-2')}>
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
@@ -90,6 +84,8 @@ export function Header({ context, embedded = false }) {
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)}
>
{/* <CommandLineIcon className="w-6 h-6" /> */}
<ArrowPathIcon className="w-6 h-6" />
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
@@ -98,6 +94,7 @@ export function Header({ context, embedded = false }) {
className="hover:opacity-50 p-2 flex items-center space-x-1"
onClick={handleShuffle}
>
<SparklesIcon className="w-6 h-6" />
<span> shuffle</span>
</button>
)}
@@ -110,6 +107,7 @@ export function Header({ context, embedded = false }) {
)}
onClick={handleShare}
>
<LinkIcon className="w-6 h-6" />
<span>share</span>
</button>
)}
@@ -119,6 +117,7 @@ export function Header({ context, embedded = false }) {
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<AcademicCapIcon className="w-6 h-6" />
<span>learn</span>
</a>
)}
+3 -3
View File
@@ -13,7 +13,7 @@ export default function ReplEditor(Props) {
const { context } = Props;
const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings();
const { panelPosition, isZen } = settings;
const { panelPosition } = settings;
return (
<div className="h-full flex flex-col relative">
@@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
<Header context={context} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
{panelPosition === 'right' && <VerticalPanel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{panelPosition === 'bottom' && <HorizontalPanel context={context} />}
</div>
);
}
@@ -1,48 +1,18 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useSettings } from '../../../settings.mjs';
export function ConsoleTab() {
const [log, setLog] = useState([]);
const { fontFamily, fontSize } = useSettings();
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
export function ConsoleTab({ log }) {
return (
<div
id="console-tab"
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
style={{ fontFamily, fontSize }}
>
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-2">
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
`}</pre>
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
@@ -72,7 +42,3 @@ function linkify(inputText) {
return replacedText;
}
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -6,7 +6,6 @@ export function ButtonGroup({ value, onChange, items }) {
{Object.entries(items).map(([key, label], i, arr) => (
<button
key={key}
id={key}
onClick={() => onChange(key)}
className={cx(
'px-2 border-b h-8 whitespace-nowrap',
+69 -94
View File
@@ -1,5 +1,9 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { setPanelPinned, setActiveFooter as setTab, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -7,31 +11,28 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
import { ChevronLeftIcon } from '@heroicons/react/16/solid';
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelOpen, activeFooter: tab } = settings;
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
settings={settings}
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')}
>
{isPanelOpen && (
<div className="flex h-full overflow-auto pr-10 ">
<PanelContent context={context} tab={tab} />
</div>
className={cx(
'hover:max-h-[360px] hover:min-h-[360px] justify-between flex flex-col',
pinned ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
)}
<div className="absolute right-4 pt-4">
<PanelActionButton settings={settings} />
>
<div className="flex h-full overflow-auto ">
<PanelContent context={context} tab={tab} />
</div>
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
<Tabs setTab={setTab} tab={tab} />
<div className="flex justify-between min-h-10 max-h-10 pr-2 items-center">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
</PanelNav>
);
@@ -39,37 +40,28 @@ export function HorizontalPanel({ context }) {
export function VerticalPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
settings={settings}
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')}
>
{isPanelOpen ? (
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} />
<PanelActionButton settings={settings} />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
) : (
<button
onClick={(e) => {
setIsPanelOpened(true);
}}
aria-label="open menu panel"
className={cx(
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
)}
>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</button>
className={cx(
'hover:min-w-[min(600px,80vw)] hover:max-w-[min(600px,80vw)]',
pinned ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-8',
)}
>
<div className={cx('group-hover:flex flex-col h-full', pinned ? 'flex' : 'hidden')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
<div className={cx(pinned ? 'hidden' : 'flex flex-col items-center justify-center h-full group-hover:hidden ')}>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</div>
</PanelNav>
);
}
@@ -86,27 +78,11 @@ if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, settings, ...props }) {
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
function PanelNav({ children, className, ...props }) {
return (
<nav
onClick={() => {
if (!settings.isPanelOpen) {
setIsPanelOpened(true);
}
}}
onMouseEnter={() => {
if (isHoverBehavior && !settings.isPanelOpen) {
setIsPanelOpened(true);
}
}}
onMouseLeave={() => {
if (isHoverBehavior && !settings.isPanelPinned) {
setIsPanelOpened(false);
}
}}
aria-label="Menu Panel"
className={cx('bg-lineHighlight group overflow-x-auto', className)}
aria-label="Settings Menu"
className={cx('bg-lineHighlight group transition-all overflow-x-auto', className)}
{...props}
>
{children}
@@ -115,11 +91,33 @@ function PanelNav({ children, className, settings, ...props }) {
}
function PanelContent({ context, tab }) {
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
case tabNames.console:
return <ConsoleTab />;
return <ConsoleTab log={log} />;
case tabNames.sounds:
return <SoundsTab />;
case tabNames.reference:
@@ -136,7 +134,7 @@ function PanelContent({ context, tab }) {
function PanelTab({ label, isSelected, onClick }) {
return (
<>
<button
<div
onClick={onClick}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
@@ -144,13 +142,13 @@ function PanelTab({ label, isSelected, onClick }) {
)}
>
{label}
</button>
</div>
</>
);
}
function Tabs({ setTab, tab, className }) {
function Tabs({ setTab, tab }) {
return (
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
<div className={cx('flex select-none max-w-full overflow-auto pb-2')}>
{Object.keys(tabNames).map((key) => {
const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
@@ -159,28 +157,15 @@ function Tabs({ setTab, tab, className }) {
);
}
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 }) {
function PinButton({ pinned, setPinned }) {
return (
<button
onClick={() => setPanelPinned(!pinned)}
onClick={() => setPinned(!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"
aria-label="Pin Settings Menu"
>
<svg
stroke="currentColor"
@@ -197,16 +182,6 @@ function PinButton({ pinned }) {
);
}
function CloseButton({ onClick }) {
return (
<button
onClick={onClick}
className={cx(
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
)}
aria-label="Close Menu"
>
<XMarkIcon />
</button>
);
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -94,8 +94,6 @@ export function SettingsTab({ started }) {
isAutoCompletionEnabled,
isTooltipEnabled,
isFlashEnabled,
isButtonRowHidden,
isCSSAnimationDisabled,
isSyncEnabled,
isLineWrappingEnabled,
fontSize,
@@ -103,7 +101,6 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
togglePanelTrigger,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -173,32 +170,7 @@ export function SettingsTab({ started }) {
items={{ bottom: 'Bottom', right: 'Right' }}
></ButtonGroup>
</FormItem>
<FormItem label="Open Panel on: ">
<ButtonGroup
value={togglePanelTrigger}
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
items={{ click: 'Click', hover: 'Hover' }}
></ButtonGroup>
{/* <Checkbox
label="Click"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'click');
}
}}
value={togglePanelTrigger != 'hover'}
/>
<Checkbox
label="Hover"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'hover');
}
}}
value={togglePanelTrigger == 'hover'}
/> */}
</FormItem>
<FormItem label="More Settings">
<FormItem label="Code Settings">
<Checkbox
label="Enable bracket matching"
onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)}
@@ -258,16 +230,6 @@ export function SettingsTab({ started }) {
disabled={shouldAlwaysSync}
value={isSyncEnabled}
/>
<Checkbox
label="Hide top buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden}
/>
<Checkbox
label="Disable CSS Animations"
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
value={isCSSAnimationDisabled}
/>
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings">
@@ -52,9 +52,9 @@ export function SoundsTab() {
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<input
className="w-full p-1 bg-background rounded-md my-2"
className="w-full p-1 bg-background rounded-md pb-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -6,7 +6,9 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3> welcome</h3>
<h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
@@ -28,7 +30,7 @@ export function WelcomeTab({ context }) {
</a>{' '}
to ask any questions, give feedback or just say hello.
</p>
<h3> about</h3>
<h3>about</h3>
<p>
strudel is a JavaScript version of{' '}
<a href="https://tidalcycles.org/" target="_blank">
+1
View File
@@ -78,6 +78,7 @@ export function loadModules() {
import('@strudel/codemirror'),
import('@strudel/hydra'),
import('@strudel/serial'),
import('@strudel/shader'),
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
+4 -13
View File
@@ -30,16 +30,11 @@ export const defaultSettings = {
isZen: false,
soundsFilter: 'all',
patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right',
isPanelPinned: false,
isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover
panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom',
isPanelPinned: true,
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false,
isCSSAnimationDisabled: false,
};
let search = null;
@@ -66,29 +61,25 @@ export function useSettings() {
return {
...state,
isZen: parseBoolean(state.isZen),
isPanelPinned: parseBoolean(state.isPanelPinned),
isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled),
isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled),
isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed),
isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted),
isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled),
isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled),
isButtonRowHidden: parseBoolean(state.isButtonRowHidden),
isCSSAnimationDisabled: parseBoolean(state.isCSSAnimationDisabled),
isTooltipEnabled: parseBoolean(state.isTooltipEnabled),
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
isFlashEnabled: parseBoolean(state.isFlashEnabled),
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
};
}
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
export const setPanelPinned = (isPinned) => settingsMap.setKey('isPanelPinned', isPinned);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);