Compare commits

...

47 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
Felix Roos d9bd6f9e7d hotfix: alt+dot breakage 2024-10-20 21:04:18 +02:00
Felix Roos 1e0cc2ab17 hotfix: ignore .astro folder + delete files from git 2024-10-20 20:45:49 +02:00
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
Jade (Rose) Rowland 835c7b6879 Merge pull request #1193 from daslyfe/orgin/daslyfe/feat/pinmenu
Menu Panel Improvements!
2024-10-18 22:28:35 -04:00
Felix Roos 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
Felix Roos 721e7f06b8 Merge pull request #1198 from tidalcycles/patchday2
update lockfile + minor versions
2024-10-19 01:10:06 +02:00
Felix Roos a46bbeaa10 fix: format + ignore samples 2024-10-19 01:05:23 +02:00
Felix Roos e3a3c0c5f9 Revert "failing to upgrade eslint"
This reverts commit 67964c7e23.
2024-10-19 00:56:55 +02:00
Felix Roos 67964c7e23 failing to upgrade eslint 2024-10-19 00:31:52 +02:00
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
Felix Roos 9e5f9d37ef update vitest from 1 to 2 2024-10-18 23:45:54 +02:00
Felix Roos 0c1234a35f update minor versions 2024-10-18 23:38:35 +02:00
Felix Roos c6cc487fc6 update lockfile version from 6 to 9 2024-10-18 23:35:19 +02:00
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
Jade (Rose) Rowland dd171f8320 panel position height dependent 2024-10-15 13:33:37 -04:00
Jade (Rose) Rowland c22c786cf2 welcome width 2024-10-14 15:19:16 -04:00
Jade (Rose) Rowland c191471e1e better width calc 2024-10-14 15:12:32 -04:00
Jade (Rose) Rowland 0d0cb3fd93 fix: tiny vertical menu 2024-10-14 15:05:10 -04:00
Jade (Rose) Rowland bb4b7dc533 fix: vertical breakpoints on mobile 2024-10-14 15:01:05 -04:00
Jade (Rose) Rowland 8440fa42ea remove unessecary hover logic 2024-10-14 13:51:14 -04:00
Jade (Rose) Rowland 61f070744a fix: lint 2024-10-14 13:09:24 -04:00
Jade (Rose) Rowland 2e6a561184 remove spillover 2024-10-14 13:08:39 -04:00
Jade (Rose) Rowland 8703d684c4 fix: lint 2024-10-14 13:01:02 -04:00
Jade (Rose) Rowland b0d2115f43 feat: mobile responsiveness 2024-10-14 12:45:55 -04:00
Jade (Rose) Rowland 96802a271c feat: menu pinning 2024-10-13 23:24:54 -04:00
Jade (Rose) Rowland a91f4576f3 Merge branch 'main' of github.com:daslyfe/strudel 2024-10-13 00:01:12 -04:00
Jade (Rose) Rowland 07bcaa65a9 create meta params 2024-09-06 00:16:30 -04:00
35 changed files with 12098 additions and 9664 deletions
+1
View File
@@ -23,3 +23,4 @@ reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/hs2js/src/hs2js.mjs
samples
+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'),
);
+7 -7
View File
@@ -56,18 +56,18 @@
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"@vitest/ui": "^2.1.3",
"acorn": "^8.13.0",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.31.0",
"events": "^3.3.0",
"jsdoc": "^4.0.2",
"jsdoc": "^4.0.3",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.0.1",
"prettier": "^3.1.1",
"lerna": "^8.1.8",
"prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+2 -4
View File
@@ -99,10 +99,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
preventDefault: true,
run: () => onStop?.(),
},
/* {
key: 'Ctrl-Shift-.',
+1 -1
View File
@@ -36,6 +36,6 @@
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+1 -1
View File
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+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'),
+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',
},
});
+1 -1
View File
@@ -37,6 +37,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+1 -1
View File
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+1 -1
View File
@@ -34,6 +34,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^1.1.0"
"vitest": "^2.1.3"
}
}
+11469 -9230
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -3,11 +3,6 @@ import { configDefaults, defineConfig } from 'vitest/config';
/// <reference types="vitest" />
export default defineConfig({
test: {
// ...
deps: {
registerNodeLoader: true,
},
threads: false,
reporters: 'verbose',
isolate: false,
silent: true,
-5
View File
@@ -1,5 +0,0 @@
{
"devToolbar": {
"enabled": false
}
}
-290
View File
@@ -1,290 +0,0 @@
declare module 'astro:content' {
interface Render {
'.mdx': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
interface Render {
'.md': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
export { z } from 'astro/zod';
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
// This needs to be in sync with ImageMetadata
export type ImageFunction = () => import('astro/zod').ZodObject<{
src: import('astro/zod').ZodString;
width: import('astro/zod').ZodNumber;
height: import('astro/zod').ZodNumber;
format: import('astro/zod').ZodUnion<
[
import('astro/zod').ZodLiteral<'png'>,
import('astro/zod').ZodLiteral<'jpg'>,
import('astro/zod').ZodLiteral<'jpeg'>,
import('astro/zod').ZodLiteral<'tiff'>,
import('astro/zod').ZodLiteral<'webp'>,
import('astro/zod').ZodLiteral<'gif'>,
import('astro/zod').ZodLiteral<'svg'>,
import('astro/zod').ZodLiteral<'avif'>,
]
>;
}>;
type BaseSchemaWithoutEffects =
| import('astro/zod').AnyZodObject
| import('astro/zod').ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
| import('astro/zod').ZodDiscriminatedUnion<string, import('astro/zod').AnyZodObject[]>
| import('astro/zod').ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
type BaseSchema =
| BaseSchemaWithoutEffects
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
export type SchemaContext = { image: ImageFunction };
type DataCollectionConfig<S extends BaseSchema> = {
type: 'data';
schema?: S | ((context: SchemaContext) => S);
};
type ContentCollectionConfig<S extends BaseSchema> = {
type?: 'content';
schema?: S | ((context: SchemaContext) => S);
};
type CollectionConfig<S> = ContentCollectionConfig<S> | DataCollectionConfig<S>;
export function defineCollection<S extends BaseSchema>(
input: CollectionConfig<S>
): CollectionConfig<S>;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown
): Promise<CollectionEntry<C>[]>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(entry: {
collection: C;
slug: E;
}): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(entry: {
collection: C;
id: E;
}): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: {
collection: C;
slug: ValidContentEntrySlug<C>;
}[]
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: {
collection: C;
id: keyof DataEntryMap[C];
}[]
): Promise<CollectionEntry<C>[]>;
export function reference<C extends keyof AnyEntryMap>(
collection: C
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? {
collection: C;
slug: ValidContentEntrySlug<C>;
}
: {
collection: C;
id: keyof DataEntryMap[C];
}
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
"blog": {
"release-0.0.2-schwindlig.mdx": {
id: "release-0.0.2-schwindlig.mdx";
slug: "release-002-schwindlig";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.2.1-stuermisch.mdx": {
id: "release-0.0.2.1-stuermisch.mdx";
slug: "release-0021-stuermisch";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.3-maelstrom.mdx": {
id: "release-0.0.3-maelstrom.mdx";
slug: "release-003-maelstrom";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.4-gischt.mdx": {
id: "release-0.0.4-gischt.mdx";
slug: "release-004-gischt";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.3.0-donauwelle.mdx": {
id: "release-0.3.0-donauwelle.mdx";
slug: "release-030-donauwelle";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.4.0-brandung.mdx": {
id: "release-0.4.0-brandung.mdx";
slug: "release-040-brandung";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.5.0-wirbel.mdx": {
id: "release-0.5.0-wirbel.mdx";
slug: "release-050-wirbel";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.6.0-zimtschnecke.mdx": {
id: "release-0.6.0-zimtschnecke.mdx";
slug: "release-060-zimtschnecke";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.7.0-zuckerguss.mdx": {
id: "release-0.7.0-zuckerguss.mdx";
slug: "release-070-zuckerguss";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.8.0-himbeermuffin.mdx": {
id: "release-0.8.0-himbeermuffin.mdx";
slug: "release-080-himbeermuffin";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.9.0-bananenbrot.mdx": {
id: "release-0.9.0-bananenbrot.mdx";
slug: "release-090-bananenbrot";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-1.0.0-geburtstagskuchen.mdx": {
id: "release-1.0.0-geburtstagskuchen.mdx";
slug: "release-100-geburtstagskuchen";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"year-2.mdx": {
id: "year-2.mdx";
slug: "year-2";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
};
};
type DataEntryMap = {
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ContentConfig = typeof import("../src/content/config");
}
+1
View File
@@ -1,3 +1,4 @@
.astro
# build output
dist/
+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:*",
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<Panel context={context} />
<HorizontalPanel context={context} />
</div>
);
}
+1 -2
View File
@@ -1,7 +1,6 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon';
import { silence, noteToMidi, _mod } from '@strudel/core';
import { clearHydra } from '@strudel/hydra';
import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
@@ -37,7 +36,7 @@ export function MiniRepl({
const shouldShowCanvas = !!punchcard;
const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
autodraw = !!punchcard || !!claviature || !!autodraw;
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
drawTime = (drawTime ?? punchcard) ? [0, 4] : [-2, 2];
if (claviature) {
drawTime = [0, 0];
}
+3 -3
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
@@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
<Header context={context} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{panelPosition === 'right' && <Panel context={context} />}
{panelPosition === 'right' && <VerticalPanel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{panelPosition === 'bottom' && <Panel context={context} />}
{panelPosition === 'bottom' && <HorizontalPanel context={context} />}
</div>
);
}
@@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ConsoleTab({ log }) {
return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-2">
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
+134 -75
View File
@@ -1,10 +1,9 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
import { setActiveFooter, useSettings } from '../../../settings.mjs';
import { 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';
@@ -12,33 +11,87 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import useClient from '@src/useClient.mjs';
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
import { ChevronLeftIcon } from '@heroicons/react/16/solid';
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function Panel({ context }) {
const footerContent = useRef();
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
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="flex h-full overflow-auto ">
<PanelContent context={context} tab={tab} />
</div>
<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>
);
}
export function VerticalPanel({ context }) {
const settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
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>
);
}
const tabNames = {
welcome: 'intro',
patterns: 'patterns',
sounds: 'sounds',
reference: 'reference',
console: 'console',
settings: 'settings',
};
if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, ...props }) {
return (
<nav
aria-label="Settings Menu"
className={cx('bg-lineHighlight group transition-all overflow-x-auto', className)}
{...props}
>
{children}
</nav>
);
}
function PanelContent({ context, tab }) {
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
useIsomorphicLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
// scroll log box to bottom when log changes
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
}
}, [log, activeFooter]);
useIsomorphicLayoutEffect(() => {
if (!footerContent.current) {
} else if (activeFooter === 'console') {
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
} else {
footerContent.current.scrollTop = 0;
}
}, [activeFooter]);
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
@@ -60,66 +113,72 @@ export function Panel({ context }) {
}, []),
);
const PanelTab = ({ children, name, label }) => (
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
case tabNames.console:
return <ConsoleTab log={log} />;
case tabNames.sounds:
return <SoundsTab />;
case tabNames.reference:
return <Reference />;
case tabNames.settings:
return <SettingsTab started={context.started} />;
case tabNames.files:
return <FilesTab />;
default:
return <WelcomeTab context={context} />;
}
}
function PanelTab({ label, isSelected, onClick }) {
return (
<>
<div
onClick={() => setActiveFooter(name)}
onClick={onClick}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
activeFooter === name ? 'border-foreground' : 'border-transparent',
isSelected ? 'border-foreground' : 'border-transparent',
)}
>
{label || name}
{label}
</div>
{activeFooter === name && <>{children}</>}
</>
);
const client = useClient();
if (isZen) {
return null;
}
const isActive = activeFooter !== '';
let positions = {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
if (!client) {
return null;
}
}
function Tabs({ setTab, tab }) {
return (
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
<div className="flex justify-between px-2">
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
<PanelTab name="intro" label="welcome" />
<PanelTab name="patterns" />
<PanelTab name="sounds" />
<PanelTab name="console" />
<PanelTab name="reference" />
<PanelTab name="settings" />
{TAURI && <PanelTab name="files" />}
</div>
{activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
{activeFooter !== '' && (
<div className="relative overflow-hidden">
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab context={context} />}
{activeFooter === 'patterns' && <PatternsTab context={context} />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
{activeFooter === 'files' && <FilesTab />}
</div>
</div>
<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)} />;
})}
</div>
);
}
function PinButton({ pinned, setPinned }) {
return (
<button
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',
)}
</nav>
aria-label="Pin Settings Menu"
>
<svg
stroke="currentColor"
fill={'currentColor'}
strokeWidth="0"
className="w-full h-full"
opacity={pinned ? 1 : '.3'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
</svg>
</button>
);
}
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50" onClick={onClick} title={label}>
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
@@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
const autoResetPatternOnChange = !isUdels();
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
</div>
)}
<section className="flex overflow-y-scroll max-h-full flex-col">
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
+24 -19
View File
@@ -25,32 +25,37 @@ export function Reference() {
}, [search]);
return (
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
<div class="w-full ml-2 mb-2 top-0 sticky">
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex">
<input
className="w-full p-1 bg-background rounded-md"
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</div>
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
</div>
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container">
<div className="prose dark:prose-invert max-w-full pr-4">
<div
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<h2>API Reference</h2>
<p>
This is the long list functions you can use! Remember that you don't need to remember all of those and that
@@ -105,7 +105,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4">
<div className="text-foreground p-4 space-y-4 w-full">
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
+10 -10
View File
@@ -1,7 +1,7 @@
import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
import React, { useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
@@ -53,14 +53,13 @@ export function SoundsTab() {
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="w-full ml-2 mb-2 top-0 sticky">
<input
className="w-full p-1 bg-background rounded-md"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<input
className="w-full p-1 bg-background rounded-md pb-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
@@ -74,7 +73,8 @@ export function SoundsTab() {
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -5,7 +5,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
return (
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3>
+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 -1
View File
@@ -30,7 +30,8 @@ export const defaultSettings = {
isZen: false,
soundsFilter: 'all',
patternFilter: 'community',
panelPosition: 'right',
panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom',
isPanelPinned: true,
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
@@ -60,6 +61,7 @@ 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),
@@ -77,6 +79,7 @@ export function useSettings() {
}
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (isPinned) => settingsMap.setKey('isPanelPinned', isPinned);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
+1
View File
@@ -52,6 +52,7 @@
}
:root {
--app-height: 100vh;
--app-width: 100vw;
}
#console-tab {
+1
View File
@@ -34,6 +34,7 @@ module.exports = {
},
spacing: {
'app-height': 'var(--app-height)',
'app-width': 'var(--app-width)',
},
typography(theme) {
return {