mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 05:33:13 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de436f0b5e | |||
| 69244ba445 | |||
| e3bc15a8d2 | |||
| b11de530f4 | |||
| ba4db567cf | |||
| b312ff63a9 | |||
| 93bc353b6b | |||
| 14a0dea826 | |||
| 009f5445be | |||
| 3d37c73103 | |||
| 0b0f9fe13b | |||
| 1d189763b2 | |||
| a4b55776d2 | |||
| 0287be547a | |||
| 6a83ede33d | |||
| 1e3f546918 | |||
| faa60be211 | |||
| dde2b9c6f8 | |||
| 382dce65df | |||
| 63f4f202f1 | |||
| 366637026b | |||
| 608dfd515e | |||
| 5e4eb7fc53 | |||
| 45bccf13ff | |||
| 65f1760fad | |||
| b8d2d02466 | |||
| 058cb36640 |
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
|||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
import { dragDropPlugin } from './dragdrop.mjs';
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
@@ -78,6 +79,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
javascript(),
|
javascript(),
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
widgetPlugin,
|
widgetPlugin,
|
||||||
|
dragDropPlugin,
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
// bracketMatching(), // does not do anything
|
// bracketMatching(), // does not do anything
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { ViewPlugin } from '@codemirror/view';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
|
// Helper function to read file content
|
||||||
|
async function readFileContent(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => resolve(e.target.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common text file formats, to avoid
|
||||||
|
// accidentally loading images or other files
|
||||||
|
function isCodeFile(file) {
|
||||||
|
const codeExtensions = ['.js', '.strudel', '.str'];
|
||||||
|
const fileName = file.name.toLowerCase();
|
||||||
|
return codeExtensions.some((ext) => fileName.endsWith(ext)) || file.type.startsWith('text/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create drag and drop extension
|
||||||
|
export const dragDropPlugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor(view) {
|
||||||
|
this.view = view;
|
||||||
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
|
this.handleDragOver = this.handleDragOver.bind(this);
|
||||||
|
this.handleDragEnter = this.handleDragEnter.bind(this);
|
||||||
|
this.handleDragLeave = this.handleDragLeave.bind(this);
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
view.dom.addEventListener('drop', this.handleDrop);
|
||||||
|
view.dom.addEventListener('dragover', this.handleDragOver);
|
||||||
|
view.dom.addEventListener('dragenter', this.handleDragEnter);
|
||||||
|
view.dom.addEventListener('dragleave', this.handleDragLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.view.dom.classList.add('cm-drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// Only remove the class if we're leaving the editor entirely
|
||||||
|
if (!this.view.dom.contains(e.relatedTarget)) {
|
||||||
|
this.view.dom.classList.remove('cm-drag-over');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.view.dom.classList.remove('cm-drag-over');
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
|
||||||
|
// Filter for code files only
|
||||||
|
const codeFiles = files.filter(isCodeFile);
|
||||||
|
|
||||||
|
if (codeFiles.length === 0) {
|
||||||
|
logger('No code files were dropped. Please drop text-based files.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read all files
|
||||||
|
const fileContents = await Promise.all(
|
||||||
|
codeFiles.map(async (file) => {
|
||||||
|
const content = await readFileContent(file);
|
||||||
|
return `// File: ${file.name}\n${content}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine content
|
||||||
|
const newContent = fileContents.join('\n\n');
|
||||||
|
|
||||||
|
// Replace entire editor contents
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: { from: 0, to: this.view.state.doc.length, insert: newContent },
|
||||||
|
selection: { anchor: newContent.length },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus the editor
|
||||||
|
this.view.focus();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
const fileNames = codeFiles.map((f) => f.name).join(', ');
|
||||||
|
logger(`Successfully loaded ${codeFiles.length} file(s): ${fileNames}`, 'highlight');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading dropped files:', error);
|
||||||
|
logger(`Error loading files: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.view.dom.removeEventListener('drop', this.handleDrop);
|
||||||
|
this.view.dom.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
this.view.dom.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
|
this.view.dom.removeEventListener('dragleave', this.handleDragLeave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -445,6 +445,32 @@ export const { coarse } = registerControl('coarse');
|
|||||||
*/
|
*/
|
||||||
export const { drive } = registerControl('drive');
|
export const { drive } = registerControl('drive');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create byte beats with custom expressions
|
||||||
|
*
|
||||||
|
* @name byteBeatExpression
|
||||||
|
* @synonyms bbexpr
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
|
||||||
|
* @example
|
||||||
|
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create byte beats with custom expressions
|
||||||
|
*
|
||||||
|
* @name byteBeatStartTime
|
||||||
|
* @synonyms bbst
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} byteBeatStartTime in samples (t)
|
||||||
|
* @example
|
||||||
|
* note("c3!8".add("{0 0 12 0 7 5 3}%8")).s("bytebeat:5").bbst("<3 1>".mul(10000))._scope()
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', 'bbst');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows you to set the output channels on the interface
|
* Allows you to set the output channels on the interface
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/core",
|
"name": "@strudel/core",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -2441,9 +2441,14 @@ const _chunk = function (n, func, pat, back = false, fast = false) {
|
|||||||
return pat.when(binary_pat, func);
|
return pat.when(binary_pat, func);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
export const { chunk, slowchunk, slowChunk } = register(
|
||||||
return _chunk(n, func, pat, false, false);
|
['chunk', 'slowchunk', 'slowChunk'],
|
||||||
});
|
function (n, func, pat) {
|
||||||
|
return _chunk(n, func, pat, false, false);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
|
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
|
||||||
@@ -2455,9 +2460,14 @@ export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', '
|
|||||||
* "0 1 2 3".chunkBack(4, x=>x.add(7))
|
* "0 1 2 3".chunkBack(4, x=>x.add(7))
|
||||||
* .scale("A:minor").note()
|
* .scale("A:minor").note()
|
||||||
*/
|
*/
|
||||||
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
|
export const { chunkBack, chunkback } = register(
|
||||||
return _chunk(n, func, pat, true);
|
['chunkBack', 'chunkback'],
|
||||||
});
|
function (n, func, pat) {
|
||||||
|
return _chunk(n, func, pat, true);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like `chunk`, but the cycles of the source pattern aren't repeated
|
* Like `chunk`, but the cycles of the source pattern aren't repeated
|
||||||
@@ -2471,9 +2481,14 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
|
|||||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||||
* .scale("C2:major").note()
|
* .scale("C2:major").note()
|
||||||
*/
|
*/
|
||||||
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
|
export const { fastchunk, fastChunk } = register(
|
||||||
return _chunk(n, func, pat, false, true);
|
['fastchunk', 'fastChunk'],
|
||||||
});
|
function (n, func, pat) {
|
||||||
|
return _chunk(n, func, pat, false, true);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// TODO - redefine elsewhere in terms of mask
|
// TODO - redefine elsewhere in terms of mask
|
||||||
export const bypass = register(
|
export const bypass = register(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/draw",
|
"name": "@strudel/draw",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Helpers for drawing with Strudel",
|
"description": "Helpers for drawing with Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/gamepad",
|
"name": "@strudel/gamepad",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Gamepad Inputs for strudel",
|
"description": "Gamepad Inputs for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/hydra",
|
"name": "@strudel/hydra",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Hydra integration for strudel",
|
"description": "Hydra integration for strudel",
|
||||||
"main": "hydra.mjs",
|
"main": "hydra.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/midi",
|
"name": "@strudel/midi",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mini",
|
"name": "@strudel/mini",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/motion",
|
"name": "@strudel/motion",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "DeviceMotion API for strudel",
|
"description": "DeviceMotion API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mqtt",
|
"name": "@strudel/mqtt",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "MQTT API for strudel",
|
"description": "MQTT API for strudel",
|
||||||
"main": "mqtt.mjs",
|
"main": "mqtt.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/serial",
|
"name": "@strudel/serial",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/soundfonts",
|
"name": "@strudel/soundfonts",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const defaultDefaultValues = {
|
|||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
delaytime: 0.25,
|
delaytime: 0.25,
|
||||||
orbit: 1,
|
orbit: 1,
|
||||||
@@ -445,7 +446,7 @@ function mapChannelNumbers(channels) {
|
|||||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const superdough = async (value, t, hapDuration) => {
|
export const superdough = async (value, t, hapDuration, cps) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||||
let { stretch } = value;
|
let { stretch } = value;
|
||||||
@@ -578,7 +579,7 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration);
|
sourceNode = source(t, value, hapDuration, cps);
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
|
|||||||
@@ -144,6 +144,82 @@ export function registerSynthSounds() {
|
|||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerSound(
|
||||||
|
'bytebeat',
|
||||||
|
(begin, value, onended) => {
|
||||||
|
const defaultBeats = [
|
||||||
|
'(t%255 >= t/255%255)*255',
|
||||||
|
'(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400',
|
||||||
|
't',
|
||||||
|
't*(t >> 10^t)',
|
||||||
|
't&128',
|
||||||
|
't&t>>8',
|
||||||
|
'((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)',
|
||||||
|
'((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)',
|
||||||
|
'(t&(t>>7)-t)',
|
||||||
|
'(sin(t*PI/128)*127+127)',
|
||||||
|
'((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)',
|
||||||
|
'((t^t/2+t+64*(cos >> 0))%127.85*2)',
|
||||||
|
'((t^t/2+t+64)%128*2)',
|
||||||
|
'(((t * .25)^(t * .25)/100+(t * .25))%128)*2',
|
||||||
|
'((t^t/2+t+64)%7 * 24)',
|
||||||
|
];
|
||||||
|
const { n = 0 } = value;
|
||||||
|
const frequency = getFrequencyFromValue(value);
|
||||||
|
const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value;
|
||||||
|
|
||||||
|
const ac = getAudioContext();
|
||||||
|
|
||||||
|
let { duration } = value;
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues(
|
||||||
|
[value.attack, value.decay, value.sustain, value.release],
|
||||||
|
'linear',
|
||||||
|
[0.001, 0.05, 0.6, 0.01],
|
||||||
|
);
|
||||||
|
const holdend = begin + duration;
|
||||||
|
const end = holdend + release + 0.01;
|
||||||
|
|
||||||
|
let o = getWorklet(
|
||||||
|
ac,
|
||||||
|
'byte-beat-processor',
|
||||||
|
{
|
||||||
|
frequency,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputChannelCount: [2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency });
|
||||||
|
|
||||||
|
let envGain = gainNode(1);
|
||||||
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
|
||||||
|
let timeoutNode = webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
destroyAudioWorkletNode(o);
|
||||||
|
envGain.disconnect();
|
||||||
|
onended();
|
||||||
|
},
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: envGain,
|
||||||
|
stop: (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ prebake: true, type: 'synth' },
|
||||||
|
);
|
||||||
|
|
||||||
registerSound(
|
registerSound(
|
||||||
'pulse',
|
'pulse',
|
||||||
(begin, value, onended) => {
|
(begin, value, onended) => {
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor {
|
|||||||
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||||
|
|
||||||
const k = Math.min(8, resonance * 0.4);
|
const k = Math.min(8, resonance * 0.13);
|
||||||
// drive makeup * resonance volume loss makeup
|
// drive makeup * resonance volume loss makeup
|
||||||
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
|
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
|
||||||
|
|
||||||
@@ -761,3 +761,137 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||||
|
|
||||||
|
/** BYTE BEATS */
|
||||||
|
const chyx = {
|
||||||
|
/*bit*/ bitC: function (x, y, z) {
|
||||||
|
return x & y ? z : 0;
|
||||||
|
},
|
||||||
|
/*bit reverse*/ br: function (x, size = 8) {
|
||||||
|
if (size > 32) {
|
||||||
|
throw new Error('br() Size cannot be greater than 32');
|
||||||
|
} else {
|
||||||
|
let result = 0;
|
||||||
|
for (let idx = 0; idx < size - 0; idx++) {
|
||||||
|
result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
|
||||||
|
return Math.sin(x / (128 / Math.PI));
|
||||||
|
},
|
||||||
|
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
|
||||||
|
return Math.cos(x / (128 / Math.PI));
|
||||||
|
},
|
||||||
|
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
|
||||||
|
return Math.tan(x / (128 / Math.PI));
|
||||||
|
},
|
||||||
|
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
|
||||||
|
return X.test(t.toString(2));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create shortened Math functions
|
||||||
|
let mathParams, byteBeatHelperFuncs;
|
||||||
|
function getByteBeatFunc(codetext) {
|
||||||
|
if ((mathParams || byteBeatHelperFuncs) == null) {
|
||||||
|
mathParams = Object.getOwnPropertyNames(Math);
|
||||||
|
byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
|
||||||
|
const chyxNames = Object.getOwnPropertyNames(chyx);
|
||||||
|
const chyxFuncs = chyxNames.map((k) => chyx[k]);
|
||||||
|
mathParams.push('int', 'window', ...chyxNames);
|
||||||
|
byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs);
|
||||||
|
}
|
||||||
|
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
let { codeText } = event.data;
|
||||||
|
const { byteBeatStartTime } = event.data;
|
||||||
|
if (byteBeatStartTime != null) {
|
||||||
|
this.t = 0;
|
||||||
|
this.initialOffset = Math.floor(byteBeatStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
|
||||||
|
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
|
||||||
|
codeText = codeText
|
||||||
|
.trim()
|
||||||
|
.replace(
|
||||||
|
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
||||||
|
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.func = getByteBeatFunc(codeText);
|
||||||
|
};
|
||||||
|
this.initialOffset = null;
|
||||||
|
this.t = null;
|
||||||
|
this.func = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'begin',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'frequency',
|
||||||
|
defaultValue: 440,
|
||||||
|
min: Number.EPSILON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'detune',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: Number.NEGATIVE_INFINITY,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'end',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime <= params.begin[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentTime >= params.end[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.t == null) {
|
||||||
|
this.t = params.begin[0] * sampleRate;
|
||||||
|
}
|
||||||
|
const output = outputs[0];
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
const detune = getParamValue(i, params.detune);
|
||||||
|
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||||
|
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset;
|
||||||
|
const funcValue = this.func(local_t);
|
||||||
|
let signal = (funcValue & 255) / 127.5 - 1;
|
||||||
|
const out = signal * 0.2;
|
||||||
|
|
||||||
|
for (let c = 0; c < output.length; c++) {
|
||||||
|
//prevent speaker blowout via clipping if threshold exceeds
|
||||||
|
output[c][i] = clamp(out, -0.4, 0.4);
|
||||||
|
}
|
||||||
|
this.t = this.t + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // keep the audio processing going
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/tonal",
|
"name": "@strudel/tonal",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/transpiler",
|
"name": "@strudel/transpiler",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||||
"module": "web.mjs",
|
"module": "web.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/webaudio",
|
"name": "@strudel/webaudio",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/xen",
|
"name": "@strudel/xen",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Xenharmonic API for strudel",
|
"description": "Xenharmonic API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1516,6 +1516,52 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "byteBeatExpression" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 1/1 → 2/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 2/1 → 3/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 3/1 → 4/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "byteBeatStartTime" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "cat" example index 0 1`] = `
|
exports[`runs examples > example "cat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:e5 ]",
|
"[ 0/1 → 1/1 | note:e5 ]",
|
||||||
@@ -8022,56 +8068,6 @@ exports[`runs examples > example "scope" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "scramble
|
|
||||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
|
||||||
but parts might be played more than once, or not at all, per cycle." example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
|
||||||
"[ 1/4 → 1/2 | note:d s:piano ]",
|
|
||||||
"[ 1/2 → 3/4 | note:d s:piano ]",
|
|
||||||
"[ 3/4 → 1/1 | note:c s:piano ]",
|
|
||||||
"[ 1/1 → 5/4 | note:e s:piano ]",
|
|
||||||
"[ 5/4 → 3/2 | note:e s:piano ]",
|
|
||||||
"[ 3/2 → 7/4 | note:e s:piano ]",
|
|
||||||
"[ 7/4 → 2/1 | note:c s:piano ]",
|
|
||||||
"[ 2/1 → 9/4 | note:f s:piano ]",
|
|
||||||
"[ 9/4 → 5/2 | note:d s:piano ]",
|
|
||||||
"[ 5/2 → 11/4 | note:d s:piano ]",
|
|
||||||
"[ 11/4 → 3/1 | note:e s:piano ]",
|
|
||||||
"[ 3/1 → 13/4 | note:c s:piano ]",
|
|
||||||
"[ 13/4 → 7/2 | note:f s:piano ]",
|
|
||||||
"[ 7/2 → 15/4 | note:d s:piano ]",
|
|
||||||
"[ 15/4 → 4/1 | note:f s:piano ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "scramble
|
|
||||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
|
||||||
but parts might be played more than once, or not at all, per cycle." example index 1 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/8 | note:c s:piano ]",
|
|
||||||
"[ 1/8 → 1/4 | note:d s:piano ]",
|
|
||||||
"[ 1/4 → 3/8 | note:d s:piano ]",
|
|
||||||
"[ 3/8 → 1/2 | note:c s:piano ]",
|
|
||||||
"[ 1/2 → 1/1 | note:g s:piano ]",
|
|
||||||
"[ 1/1 → 9/8 | note:e s:piano ]",
|
|
||||||
"[ 9/8 → 5/4 | note:e s:piano ]",
|
|
||||||
"[ 5/4 → 11/8 | note:e s:piano ]",
|
|
||||||
"[ 11/8 → 3/2 | note:c s:piano ]",
|
|
||||||
"[ 3/2 → 2/1 | note:g s:piano ]",
|
|
||||||
"[ 2/1 → 17/8 | note:f s:piano ]",
|
|
||||||
"[ 17/8 → 9/4 | note:d s:piano ]",
|
|
||||||
"[ 9/4 → 19/8 | note:d s:piano ]",
|
|
||||||
"[ 19/8 → 5/2 | note:e s:piano ]",
|
|
||||||
"[ 5/2 → 3/1 | note:g s:piano ]",
|
|
||||||
"[ 3/1 → 25/8 | note:c s:piano ]",
|
|
||||||
"[ 25/8 → 13/4 | note:f s:piano ]",
|
|
||||||
"[ 13/4 → 27/8 | note:d s:piano ]",
|
|
||||||
"[ 27/8 → 7/2 | note:f s:piano ]",
|
|
||||||
"[ 7/2 → 4/1 | note:g s:piano ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "scramble" example index 0 1`] = `
|
exports[`runs examples > example "scramble" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
"[ 0/1 → 1/4 | note:c s:piano ]",
|
||||||
|
|||||||
@@ -69,3 +69,40 @@
|
|||||||
text-decoration: underline 0.18rem;
|
text-decoration: underline 0.18rem;
|
||||||
text-underline-offset: 0.22rem;
|
text-underline-offset: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drag and drop styles */
|
||||||
|
#code .cm-editor.cm-drag-over {
|
||||||
|
outline: 2px dashed #4caf50;
|
||||||
|
outline-offset: -2px;
|
||||||
|
background-color: rgba(76, 175, 80, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Download button styles */
|
||||||
|
.download-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import DownloadButton from './DownloadButton';
|
||||||
|
|
||||||
// type Props = {
|
// type Props = {
|
||||||
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||||
// init: () => void
|
// init: () => void
|
||||||
// }
|
// }
|
||||||
export function Code(Props) {
|
export function Code(Props) {
|
||||||
const { editorRef, containerRef, init } = Props;
|
const { editorRef, containerRef, init, context } = Props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'}
|
className={'text-gray-100 cursor-text pb-0 overflow-auto grow relative'}
|
||||||
id="code"
|
id="code"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
containerRef.current = el;
|
containerRef.current = el;
|
||||||
@@ -16,6 +18,8 @@ export function Code(Props) {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
></section>
|
>
|
||||||
|
{context && <DownloadButton context={context} />}
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export default function DownloadButton({ context }) {
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
// Get the current code from the editor
|
||||||
|
const code = context.editorRef?.current?.code || '';
|
||||||
|
|
||||||
|
// Create a blob with the code
|
||||||
|
const blob = new Blob([code], { type: 'text/javascript' });
|
||||||
|
|
||||||
|
// Create a temporary URL for the blob
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a temporary anchor element and trigger download
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `strudel-pattern-${new Date().toISOString().slice(0, 10)}.js`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
// Clean up the URL
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="download-button"
|
||||||
|
title="Download pattern as .js file"
|
||||||
|
aria-label="Download pattern"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 12L3 7L4.4 5.6L7 8.2V0H9V8.2L11.6 5.6L13 7L8 12Z" fill="currentColor" />
|
||||||
|
<path d="M14 14V10H16V16H0V10H2V14H14Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||||
|
import ArrowDownTrayIcon from '@heroicons/react/20/solid/ArrowDownTrayIcon';
|
||||||
import cx from '@src/cx.mjs';
|
import cx from '@src/cx.mjs';
|
||||||
import { useSettings, setIsZen } from '../../settings.mjs';
|
import { useSettings, setIsZen } from '../../settings.mjs';
|
||||||
import '../Repl.css';
|
import '../Repl.css';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function ReplEditor(Props) {
|
|||||||
<Loader active={pending} />
|
<Loader active={pending} />
|
||||||
<Header context={context} />
|
<Header context={context} />
|
||||||
<div className="grow flex relative overflow-hidden">
|
<div className="grow flex relative overflow-hidden">
|
||||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
<Code containerRef={containerRef} editorRef={editorRef} init={init} context={context} />
|
||||||
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
|
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
|
||||||
</div>
|
</div>
|
||||||
<UserFacingErrorMessage error={error} />
|
<UserFacingErrorMessage error={error} />
|
||||||
|
|||||||
Reference in New Issue
Block a user