Merge branch 'tidalcycles:main' into envelope_improvements

This commit is contained in:
Jade (Rose) Rowland 2023-12-15 18:27:23 -05:00 committed by GitHub
commit 9f50f6ef0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2596 additions and 323 deletions

View File

@ -0,0 +1,88 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc }) {
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
}
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} />);
return node;
},
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}

View File

@ -1,37 +1,80 @@
import { defaultKeymap } from '@codemirror/commands';
import { closeBrackets } from '@codemirror/autocomplete';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
// import { isAutoCompletionEnabled } from './Autocomplete';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
// isAutoCompletionEnabled,
isPatternHighlightingEnabled,
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
keybindings,
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) {
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
/* search(),
highlightSelectionMatches(), */
...initialSettings,
javascript(),
lineNumbers(),
highlightExtension,
highlightActiveLineGutter(),
sliderPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
history(),
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
Prec.highest(
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
),
],
});
@ -43,71 +86,168 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.painters = [];
this.onDraw = onDraw;
const self = this;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
onDraw?.(haps, time, currentFrame);
this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
// this approach might not work with multiple repls on screen..
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
// this.drawFirstFrame();
this.repl = repl({
...replOptions,
onToggle: async (started) => {
onToggle: (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
this.drawer.start(this.repl.scheduler);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(false);
}
},
beforeEval: async () => {
await prebaked;
cleanupDraw();
this.painters = [];
await this.prebaked;
await replOptions?.beforeEval?.();
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
settings,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
if (v.docChanged) {
this.code = v.state.doc.toString();
// TODO: repl is still untouched to make sure the old Repl.jsx stays untouched..
// this.repl.setCode(this.code);
}
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.backgroundColor = 'var(--background)';
cmEditor.style.backgroundColor = 'transparent';
}
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
}
// draw first frame instantly
await this.prebaked;
try {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler);
this.onDraw?.(this.drawer.visibleHaps, 0, []);
} catch (err) {
console.warn('first frame could not be painted');
}
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
await this.repl.evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
this.repl.scheduler.stop();
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.scheduler.stop();
} else {
this.evaluate();
}
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor.view, time, haps);
highlightMiniLocations(this.editor, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
const scroller = this.root.querySelector('.cm-scroller');
if (scroller) {
scroller.style.fontFamily = family;
}
}
reconfigureExtension(key, value) {
if (!extensions[key]) {
console.warn(`extension ${key} is not known`);
return;
}
value = parseBooleans(value);
const newValue = extensions[key](value, this);
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
}
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
setAutocompletionEnabled(enabled) {
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
}
updateSettings(settings) {
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
}
changeSetting(key, value) {
if (extensions[key]) {
this.reconfigureExtension(key, value);
return;
} else if (key === 'fontFamily') {
this.setFontFamily(value);
} else if (key === 'fontSize') {
this.setFontSize(value);
}
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
}
}
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}

View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StrudelMirror Example</title>
</head>
<body>
<div class="settings">
<form name="settings">
<label
>theme
<select name="theme">
<option>strudelTheme</option>
<option>bluescreen</option>
<option>blackscreen</option>
<option>whitescreen</option>
<option>teletext</option>
<option>algoboy</option>
<option>terminal</option>
<option>abcdef</option>
<option>androidstudio</option>
<option>atomone</option>
<option>aura</option>
<option>bespin</option>
<option>darcula</option>
<option>dracula</option>
<option>duotoneDark</option>
<option>eclipse</option>
<option>githubDark</option>
<option>gruvboxDark</option>
<option>materialDark</option>
<option>nord</option>
<option>okaidia</option>
<option>solarizedDark</option>
<option>sublime</option>
<option>tokyoNight</option>
<option>tokyoNightStorm</option>
<option>vscodeDark</option>
<option>xcodeDark</option>
<option>bbedit</option>
<option>duotoneLight</option>
<option>githubLight</option>
<option>gruvboxLight</option>
<option>materialLight</option>
<option>noctisLilac</option>
<option>solarizedLight</option>
<option>tokyoNightDay</option>
<option>xcodeLight</option>
</select> </label
><br />
<!-- <label
>keybindings
<select name="keybindings">
<option>codemirror</option>
<option>vim</option>
<option>emacs</option>
<option>vscode</option>
</select> </label
><br />
<label
>fontFamily
<select name="fontFamily">
<option>monospace</option>
<option>helvetica</option>
</select> </label
><br /> -->
<label>fontSize <input type="number" name="fontSize" /></label>
<br />
<label><input type="checkbox" name="isLineNumbersDisplayed" />isLineNumbersDisplayed</label>
<br />
<label><input type="checkbox" name="isActiveLineHighlighted" />isActiveLineHighlighted</label>
<br />
<label><input type="checkbox" name="isPatternHighlightingEnabled" />isPatternHighlightingEnabled</label>
<br />
<label><input type="checkbox" name="isFlashEnabled" />isFlashEnabled</label>
<br />
<label><input type="checkbox" name="isLineWrappingEnabled" />isLineWrappingEnabled</label>
<!-- <label><input type="checkbox" name="isAutoCompletionEnabled" />isAutoCompletionEnabled</label> -->
<!-- <label><input type="checkbox" name="isTooltipEnabled" />isTooltipEnabled</label> -->
</form>
</div>
<div id="code"></div>
<script type="module" src="/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,199 @@
import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core';
import { StrudelMirror } from '@strudel/codemirror';
import { transpiler } from '@strudel.cycles/transpiler';
import {
getAudioContext,
webaudioOutput,
registerSynthSounds,
registerZZFXSounds,
samples,
} from '@strudel.cycles/webaudio';
import './style.css';
let editor;
const initialSettings = {
keybindings: 'codemirror',
isLineNumbersDisplayed: true,
isActiveLineHighlighted: true,
isAutoCompletionEnabled: false,
isPatternHighlightingEnabled: true,
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
theme: 'teletext',
fontFamily: 'monospace',
fontSize: 18,
};
async function run() {
const container = document.getElementById('code');
if (!container) {
console.warn('could not init: no container found');
return;
}
const drawContext = getDrawContext();
const drawTime = [-2, 2];
editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: container,
initialCode: '// LOADING',
pattern: silence,
settings: initialSettings,
drawTime,
onDraw: (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
},
prebake: async () => {
// populate scope / lazy load modules
const modulesLoading = evalScope(
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
// import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel/codemirror'),
/* import('@strudel/hydra'), */
// import('@strudel.cycles/serial'),
/* import('@strudel.cycles/soundfonts'), */
// import('@strudel.cycles/csound'),
/* import('@strudel.cycles/midi'), */
// import('@strudel.cycles/osc'),
controls, // sadly, this cannot be exported from core directly (yet)
);
// load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
await Promise.all([
modulesLoading,
registerSynthSounds(),
registerZZFXSounds(),
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/vcsl.json`),
]);
},
afterEval: ({ code }) => {
window.location.hash = '#' + code2hash(code);
},
});
// init settings
editor.updateSettings(initialSettings);
logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight');
const codeParam = window.location.href.split('#')[1] || '';
const initialCode = codeParam
? hash2code(codeParam)
: `// @date 23-11-30
// "teigrührgerät" @by froos
stack(
stack(
s("bd(<3!3 5>,6)/2").bank('RolandTR707')
,
s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>")
.lastOf(8, x=>x.segment("12").end(.2).gain(isaw))
,
s("[tb ~ tb]").bank('RolandTR707')
.clip(0).release(.08).room(.2)
).off(-1/6, x=>x.speed(.7).gain(.2).degrade())
,
stack(
note("<g1(<3 4>,6) ~!2 [f1?]*2>")
.s("sawtooth").lpf(perlin.range(400,1000))
.lpa(.1).lpenv(-3).room(.2)
.lpq(8).noise(.2)
.add(note("0,.1"))
,
chord("<~ Gm9 ~!2>")
.dict('ireal').voicing()
.s("sawtooth").vib("2:.1")
.lpf(1000).lpa(.1).lpenv(-4)
.room(.5)
,
n(run(3)).chord("<Gm9 Gm11>/8")
.dict('ireal-ext')
.off(1/2, add(n(4)))
.voicing()
.clip(.1).release(.05)
.s("sine").jux(rev)
.sometimesBy(sine.slow(16), add(note(12)))
.room(.75)
.lpf(sine.range(200,2000).slow(16))
.gain(saw.slow(4).div(2))
).add(note(perlin.range(0,.5)))
)`;
editor.setCode(initialCode); // simpler alternative to above init
// settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key]));
onEvent('strudel-toggle-play', () => editor.toggle());
}
run();
function onEvent(key, callback) {
const listener = (e) => {
if (e.data === key) {
callback();
}
};
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}
// settings form
function getInput(form, name) {
return form.querySelector(`input[name=${name}]`) || form.querySelector(`select[name=${name}]`);
}
function getFormValues(form, initial) {
const entries = Object.entries(initial).map(([key, initialValue]) => {
const input = getInput(form, key);
if (!input) {
return [key, initialValue]; // fallback
}
if (input.type === 'checkbox') {
return [key, input.checked];
}
if (input.type === 'number') {
return [key, Number(input.value)];
}
if (input.tagName === 'SELECT') {
return [key, input.value];
}
return [key, input.value];
});
return Object.fromEntries(entries);
}
function setFormValues(form, values) {
Object.entries(values).forEach(([key, value]) => {
const input = getInput(form, key);
if (!input) {
return;
}
if (input.type === 'checkbox') {
input.checked = !!value;
} else if (input.type === 'number') {
input.value = value;
} else if (input.tagName) {
input.value = value;
}
});
}
const form = document.querySelector('form[name=settings]');
setFormValues(form, initialSettings);
form.addEventListener('change', () => {
const values = getFormValues(form, initialSettings);
// console.log('values', values);
editor.updateSettings(values);
});

View File

@ -0,0 +1,29 @@
{
"name": "strudelmirror",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.8"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core":"workspace:*",
"@strudel.cycles/transpiler":"workspace:*",
"@strudel.cycles/tonal":"workspace:*",
"@strudel.cycles/mini":"workspace:*",
"@strudel.cycles/xen":"workspace:*",
"@strudel.cycles/webaudio":"workspace:*",
"@strudel/hydra":"workspace:*",
"@strudel.cycles/serial":"workspace:*",
"@strudel.cycles/soundfonts":"workspace:*",
"@strudel.cycles/csound":"workspace:*",
"@strudel.cycles/midi":"workspace:*",
"@strudel.cycles/osc":"workspace:*"
}
}

View File

@ -0,0 +1,33 @@
:root {
--foreground: white;
}
body,
input {
font-family: monospace;
background: black;
color: white;
}
html,
body,
#code,
.cm-editor,
.cm-scroller {
padding: 0;
margin: 0;
height: 100%;
}
.settings {
position: fixed;
right: 0;
top: 0;
z-index: 1000;
display: flex-col;
padding: 10px;
}
.settings > form > * + * {
margin-top: 10px;
}

View File

@ -33,3 +33,5 @@ export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export const isFlashEnabled = (on) => (on ? flashField : []);

View File

@ -124,3 +124,12 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
export const isPatternHighlightingEnabled = (on, config) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? highlightExtension : [];
};

View File

@ -2,3 +2,4 @@ export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';

View File

@ -0,0 +1,31 @@
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor() {}
},
{
provide: () => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
}

View File

@ -33,13 +33,21 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
"react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "^4.3.3"

521
packages/codemirror/themes.mjs vendored Normal file
View File

@ -0,0 +1,521 @@
import {
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
} from '@uiw/codemirror-themes-all';
import strudelTheme from './themes/strudel-theme';
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal';
export const themes = {
strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
terminal,
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
};
// lineBackground is background with 50% opacity, to make sure the selection below is visible
export const settings = {
strudelTheme: {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
bluescreen: bluescreenSettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: {
background: '#0f0f0f',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
gutterBackground: '#555',
gutterForeground: '#FFFFFF',
lineHighlight: '#314151',
},
androidstudio: {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
lineHighlight: '#343739',
},
atomone: {
background: '#272C35',
lineBackground: '#272C3599',
foreground: '#9d9b97',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
gutterBackground: '#272C35',
gutterForeground: '#465063',
gutterBorder: 'transparent',
lineHighlight: '#2B323D',
},
aura: {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
gutterBackground: '#21202e',
gutterForeground: '#edecee',
gutterBorder: 'transparent',
lineHighlight: '#a394f033',
},
bbedit: {
light: true,
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
gutterBackground: '#f5f5f5',
gutterForeground: '#4D4D4C',
gutterBorder: 'transparent',
lineHighlight: '#00000012',
},
bespin: {
background: '#28211c',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
darcula: {
background: '#2B2B2B',
lineBackground: '#2B2B2B99',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: 'rgba(255, 255, 255, 0.1)',
gutterForeground: '#999',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
dracula: {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: '#282a36',
gutterForeground: '#6D8A88',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
duotoneLight: {
light: true,
background: '#faf8f5',
lineBackground: '#faf8f599',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
selectionMatch: '#e3dcce',
gutterBackground: '#faf8f5',
gutterForeground: '#cdc4b1',
gutterBorder: 'transparent',
lineHighlight: '#EFEFEF',
},
duotoneDark: {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#6c6783',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
gutterForeground: '#545167',
lineHighlight: '#36334280',
},
eclipse: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#f7f7f7',
gutterForeground: '#999',
lineHighlight: '#e8f2ff',
gutterBorder: 'transparent',
},
githubLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#6e7781',
},
githubDark: {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
lineHighlight: '#36334280',
},
gruvboxDark: {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
lineHighlight: '#3c3836',
gutterBackground: '#282828',
gutterForeground: '#7c6f64',
},
gruvboxLight: {
light: true,
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
lineHighlight: '#ebdbb2',
gutterBackground: '#ebdbb2',
gutterForeground: '#665c54',
gutterBorder: 'transparent',
},
materialDark: {
background: '#2e3235',
lineBackground: '#2e323599',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#2e3235',
gutterForeground: '#999',
gutterActiveForeground: '#4f5b66',
lineHighlight: '#545b61',
},
materialLight: {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
gutterBackground: '#FAFAFA',
gutterForeground: '#90A4AE',
gutterBorder: 'transparent',
lineHighlight: '#CCD7DA50',
},
noctisLilac: {
light: true,
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
gutterBackground: '#f2f1f8',
gutterForeground: '#0c006b70',
lineHighlight: '#e1def3',
},
nord: {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
gutterBackground: '#2e3440',
gutterForeground: '#4c566a',
gutterActiveForeground: '#d8dee9',
lineHighlight: '#4c566a',
},
okaidia: {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
gutterBackground: '#272822',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
solarizedLight: {
light: true,
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
gutterBackground: '#00000010',
gutterForeground: '#657b83',
lineHighlight: '#dfd9c8',
},
solarizedDark: {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
gutterBackground: '#00252f',
gutterForeground: '#839496',
lineHighlight: '#173541',
},
sublime: {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
gutterBackground: '#303841',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
tokyoNightDay: {
light: true,
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
gutterBackground: '#e1e2e7',
gutterForeground: '#3760bf',
gutterBorder: 'transparent',
lineHighlight: '#5f5faf11',
},
tokyoNightStorm: {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
gutterBackground: '#24283b',
gutterForeground: '#7982a9',
gutterBorder: 'transparent',
lineHighlight: '#292e42',
},
tokyoNight: {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
gutterBackground: '#1a1b26',
gutterForeground: '#787c99',
gutterBorder: 'transparent',
lineHighlight: '#1e202e',
},
vscodeDark: {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#9cdcfe',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
lineHighlight: '#ffffff0f',
gutterBackground: '#1e1e1e',
gutterForeground: '#838383',
gutterActiveForeground: '#fff',
},
xcodeLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#AFAFAF',
lineHighlight: '#EDF4FF',
},
xcodeDark: {
background: '#292A30',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#2F3239',
},
};
function getColors(str) {
const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
const colors = [];
let match;
while ((match = colorRegex.exec(str)) !== null) {
const color = match[0];
if (!colors.includes(color)) {
colors.push(color);
}
}
return colors;
}
// TODO: remove
export function themeColors(theme) {
return getColors(stringifySafe(theme));
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}
function stringifySafe(json) {
return JSON.stringify(json, getCircularReplacer());
}
export const theme = (theme) => themes[theme] || themes.strudelTheme;
// css style injection helpers
export function injectStyle(rule) {
const newStyle = document.createElement('style');
document.head.appendChild(newStyle);
const styleSheet = newStyle.sheet;
const ruleIndex = styleSheet.insertRule(rule, 0);
return () => styleSheet.deleteRule(ruleIndex);
}
let currentTheme, resetThemeStyle, themeStyle;
export function initTheme(theme) {
themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme';
document.head.append(themeStyle);
activateTheme(theme);
}
export function activateTheme(name) {
if (currentTheme === name) {
return;
}
if (!settings[name]) {
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
}
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')}
}`;
// tailwind dark mode
if (themeSettings.light) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
resetThemeStyle?.();
resetThemeStyle = undefined;
if (themeSettings.customStyle) {
resetThemeStyle = injectStyle(themeSettings.customStyle);
}
}

41
packages/codemirror/themes/algoboy.mjs vendored Normal file
View File

@ -0,0 +1,41 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#9bbc0f',
foreground: '#0f380f', // whats that?
caret: '#0f380f',
selection: '#306230',
selectionMatch: '#ffffff26',
lineHighlight: '#8bac0f',
lineBackground: '#9bbc0f50',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ 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' },
],
});

View File

@ -0,0 +1,38 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
],
});

View File

@ -0,0 +1,41 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
],
});

View File

@ -1,139 +0,0 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];

View File

@ -0,0 +1,45 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export default createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
// { tag: t.typeName, color: '#f07178' }, // original
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original
{ tag: t.variableName, color: '#c792ea' },
// { tag: t.tagName, color: '#ff5370' }, // original
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});

50
packages/codemirror/themes/teletext.mjs vendored Normal file
View File

@ -0,0 +1,50 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});

36
packages/codemirror/themes/terminal.mjs vendored Normal file
View File

@ -0,0 +1,36 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ 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' },
],
});

View File

@ -0,0 +1,38 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ 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' },
],
});

View File

@ -274,3 +274,31 @@ export const sol2note = (n, notation = 'letters') => {
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
// code hashing helpers
export function unicodeToBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const base64String = btoa(String.fromCharCode(...utf8Bytes));
return base64String;
}
export function base64ToUnicode(base64String) {
const utf8Bytes = new Uint8Array(
atob(base64String)
.split('')
.map((char) => char.charCodeAt(0)),
);
const decodedText = new TextDecoder().decode(utf8Bytes);
return decodedText;
}
export function code2hash(code) {
return encodeURIComponent(unicodeToBase64(code));
//return '#' + encodeURIComponent(btoa(code));
}
export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || ''));
}

View File

@ -37,7 +37,7 @@ function connect() {
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output)
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
*
* @name osc
* @memberof Pattern

View File

@ -139,8 +139,8 @@ export function registerSoundfonts() {
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
};
bufferSource.onended = () => {
bufferSource.disconnect();

View File

@ -10,21 +10,24 @@ export function gainNode(value) {
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
let phase = begin;
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
phase += attack;
gainNode.gain.linearRampToValueAtTime(velocity, phase); // attack
phase += decay;
let sustainLevel = sustain * velocity;
gainNode.gain.linearRampToValueAtTime(sustainLevel, phase); // decay / sustain
// sustain end
return {
node: gainNode,
stop: (t) => {
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
//} else {
// firefox: this will glitch when the sustain has not been reached yet at the time of release
gainNode.gain.setValueAtTime(sustain * velocity, t);
//}
gainNode.gain.linearRampToValueAtTime(0, t + release);
// to make sure the release won't begin before sustain is reached
phase = Math.max(t, phase);
// see https://github.com/tidalcycles/strudel/issues/522
gainNode.gain.setValueAtTime(sustainLevel, phase);
phase += release;
gainNode.gain.linearRampToValueAtTime(0, phase); // release
return phase;
},
};
};

View File

@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.11",
"version": "0.9.12",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",

View File

@ -313,8 +313,8 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
};
const handle = { node: out, bufferSource, stop };

View File

@ -59,10 +59,9 @@ export function registerSynthSounds() {
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
const silentAt = releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
let end = releaseTime + release;
stop(end);
stop(silentAt);
},
};
},

280
pnpm-lock.yaml generated
View File

@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
@ -71,6 +75,9 @@ importers:
packages/codemirror:
dependencies:
'@codemirror/autocomplete':
specifier: ^6.6.0
version: 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2)
'@codemirror/commands':
specifier: ^6.2.4
version: 6.2.4
@ -80,6 +87,9 @@ importers:
'@codemirror/language':
specifier: ^6.6.0
version: 6.6.0
'@codemirror/search':
specifier: ^6.0.0
version: 6.2.3
'@codemirror/state':
specifier: ^6.2.0
version: 6.2.0
@ -89,14 +99,78 @@ importers:
'@lezer/highlight':
specifier: ^1.1.4
version: 1.1.4
'@replit/codemirror-emacs':
specifier: ^6.0.1
version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@replit/codemirror-vim':
specifier: ^6.0.14
version: 6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@replit/codemirror-vscode-keymap':
specifier: ^6.0.2
version: 6.0.2(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
'@uiw/codemirror-themes':
specifier: ^4.19.16
version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@uiw/codemirror-themes-all':
specifier: ^4.19.16
version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3
packages/codemirror/examples/strudelmirror:
dependencies:
'@strudel.cycles/core':
specifier: workspace:*
version: link:../../../core
'@strudel.cycles/csound':
specifier: workspace:*
version: link:../../../csound
'@strudel.cycles/midi':
specifier: workspace:*
version: link:../../../midi
'@strudel.cycles/mini':
specifier: workspace:*
version: link:../../../mini
'@strudel.cycles/osc':
specifier: workspace:*
version: link:../../../osc
'@strudel.cycles/serial':
specifier: workspace:*
version: link:../../../serial
'@strudel.cycles/soundfonts':
specifier: workspace:*
version: link:../../../soundfonts
'@strudel.cycles/tonal':
specifier: workspace:*
version: link:../../../tonal
'@strudel.cycles/transpiler':
specifier: workspace:*
version: link:../../../transpiler
'@strudel.cycles/webaudio':
specifier: workspace:*
version: link:../../../webaudio
'@strudel.cycles/xen':
specifier: workspace:*
version: link:../../../xen
'@strudel/codemirror':
specifier: workspace:*
version: link:../..
'@strudel/hydra':
specifier: workspace:*
version: link:../../../hydra
devDependencies:
vite:
specifier: ^5.0.8
version: 5.0.8
packages/core:
dependencies:
fraction.js:
@ -4053,6 +4127,110 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/rollup-android-arm-eabi@4.9.0:
resolution: {integrity: sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-android-arm64@4.9.0:
resolution: {integrity: sha512-im6hUEyQ7ZfoZdNvtwgEJvBWZYauC9KVKq1w58LG2Zfz6zMd8gRrbN+xCVoqA2hv/v6fm9lp5LFGJ3za8EQH3A==}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-arm64@4.9.0:
resolution: {integrity: sha512-u7aTMskN6Dmg1lCT0QJ+tINRt+ntUrvVkhbPfFz4bCwRZvjItx2nJtwJnJRlKMMaQCHRjrNqHRDYvE4mBm3DlQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-x64@4.9.0:
resolution: {integrity: sha512-8FvEl3w2ExmpcOmX5RJD0yqXcVSOqAJJUJ29Lca29Ik+3zPS1yFimr2fr5JSZ4Z5gt8/d7WqycpgkX9nocijSw==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.9.0:
resolution: {integrity: sha512-lHoKYaRwd4gge+IpqJHCY+8Vc3hhdJfU6ukFnnrJasEBUvVlydP8PuwndbWfGkdgSvZhHfSEw6urrlBj0TSSfg==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.9.0:
resolution: {integrity: sha512-JbEPfhndYeWHfOSeh4DOFvNXrj7ls9S/2omijVsao+LBPTPayT1uKcK3dHW3MwDJ7KO11t9m2cVTqXnTKpeaiw==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.9.0:
resolution: {integrity: sha512-ahqcSXLlcV2XUBM3/f/C6cRoh7NxYA/W7Yzuv4bDU1YscTFw7ay4LmD7l6OS8EMhTNvcrWGkEettL1Bhjf+B+w==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-riscv64-gnu@4.9.0:
resolution: {integrity: sha512-uwvOYNtLw8gVtrExKhdFsYHA/kotURUmZYlinH2VcQxNCQJeJXnkmWgw2hI9Xgzhgu7J9QvWiq9TtTVwWMDa+w==}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.9.0:
resolution: {integrity: sha512-m6pkSwcZZD2LCFHZX/zW2aLIISyzWLU3hrLLzQKMI12+OLEzgruTovAxY5sCZJkipklaZqPy/2bEEBNjp+Y7xg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-musl@4.9.0:
resolution: {integrity: sha512-VFAC1RDRSbU3iOF98X42KaVicAfKf0m0OvIu8dbnqhTe26Kh6Ym9JrDulz7Hbk7/9zGc41JkV02g+p3BivOdAg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.9.0:
resolution: {integrity: sha512-9jPgMvTKXARz4inw6jezMLA2ihDBvgIU9Ml01hjdVpOcMKyxFBJrn83KVQINnbeqDv0+HdO1c09hgZ8N0s820Q==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.9.0:
resolution: {integrity: sha512-WE4pT2kTXQN2bAv40Uog0AsV7/s9nT9HBWXAou8+++MBCnY51QS02KYtm6dQxxosKi1VIz/wZIrTQO5UP2EW+Q==}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.9.0:
resolution: {integrity: sha512-aPP5Q5AqNGuT0tnuEkK/g4mnt3ZhheiXrDIiSVIHN9mcN21OyXDVbEMqmXPE7e2OplNLDkcvV+ZoGJa2ZImFgw==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@sigstore/protobuf-specs@0.1.0:
resolution: {integrity: sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -5572,6 +5750,7 @@ packages:
/b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
requiresBuild: true
optional: true
/babel-plugin-add-module-exports@0.2.1:
@ -5968,7 +6147,7 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
/chord-voicings@0.0.1:
resolution: {integrity: sha512-SutgB/4ynkkuiK6qdQ/k3QvCFcH0Vj8Ch4t6LbRyRQbVzP/TOztiCk3kvXd516UZ6fqk7ijDRELEFcKN+6V8sA==}
@ -6154,6 +6333,7 @@ packages:
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
requiresBuild: true
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
@ -6166,6 +6346,7 @@ packages:
/color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
requiresBuild: true
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
@ -6629,6 +6810,7 @@ packages:
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
requiresBuild: true
optional: true
/detective-amd@4.0.1:
@ -7472,6 +7654,7 @@ packages:
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
requiresBuild: true
optional: true
/fast-glob@3.2.12:
@ -7736,8 +7919,8 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
@ -10521,6 +10704,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
/nanoid@4.0.2:
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
engines: {node: ^14 || ^16 || >=18}
@ -10579,6 +10768,7 @@ packages:
/node-addon-api@6.1.0:
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
requiresBuild: true
optional: true
/node-domexception@1.0.0:
@ -11675,6 +11865,15 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
/postcss@8.4.32:
resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.7
picocolors: 1.0.0
source-map-js: 1.0.2
dev: true
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
@ -11875,6 +12074,7 @@ packages:
/queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
requiresBuild: true
optional: true
/quick-lru@4.0.1:
@ -12504,7 +12704,7 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/rollup@3.21.0:
@ -12512,7 +12712,7 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/rollup@3.28.0:
@ -12520,7 +12720,28 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
/rollup@4.9.0:
resolution: {integrity: sha512-bUHW/9N21z64gw8s6tP4c88P382Bq/L5uZDowHlHx6s/QWpjJXivIAbEw6LZthgSvlEizZBfLC4OAvWe7aoF7A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.9.0
'@rollup/rollup-android-arm64': 4.9.0
'@rollup/rollup-darwin-arm64': 4.9.0
'@rollup/rollup-darwin-x64': 4.9.0
'@rollup/rollup-linux-arm-gnueabihf': 4.9.0
'@rollup/rollup-linux-arm64-gnu': 4.9.0
'@rollup/rollup-linux-arm64-musl': 4.9.0
'@rollup/rollup-linux-riscv64-gnu': 4.9.0
'@rollup/rollup-linux-x64-gnu': 4.9.0
'@rollup/rollup-linux-x64-musl': 4.9.0
'@rollup/rollup-win32-arm64-msvc': 4.9.0
'@rollup/rollup-win32-ia32-msvc': 4.9.0
'@rollup/rollup-win32-x64-msvc': 4.9.0
fsevents: 2.3.3
dev: true
/run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
@ -12730,6 +12951,7 @@ packages:
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
requiresBuild: true
dependencies:
is-arrayish: 0.3.2
optional: true
@ -12943,6 +13165,7 @@ packages:
/streamx@2.15.2:
resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==}
requiresBuild: true
dependencies:
fast-fifo: 1.3.2
queue-tick: 1.0.1
@ -13220,6 +13443,7 @@ packages:
/tar-fs@3.0.4:
resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==}
requiresBuild: true
dependencies:
mkdirp-classic: 0.5.3
pump: 3.0.0
@ -13238,6 +13462,7 @@ packages:
/tar-stream@3.1.6:
resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==}
requiresBuild: true
dependencies:
b4a: 1.6.4
fast-fifo: 1.3.2
@ -14008,7 +14233,7 @@ packages:
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
vite: 4.4.5(@types/node@18.16.3)
vite: 4.5.0(@types/node@18.16.3)
transitivePeerDependencies:
- '@types/node'
- less
@ -14067,7 +14292,7 @@ packages:
postcss: 8.4.23
rollup: 3.21.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/vite@4.4.5(@types/node@18.16.3):
@ -14103,7 +14328,7 @@ packages:
postcss: 8.4.27
rollup: 3.28.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/vite@4.5.0(@types/node@18.16.3):
@ -14139,7 +14364,42 @@ packages:
postcss: 8.4.31
rollup: 3.28.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
/vite@5.0.8:
resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.19.5
postcss: 8.4.32
rollup: 4.9.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/vitefu@0.2.4(vite@4.5.0):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}

View File

@ -7,3 +7,4 @@ packages:
- "packages/react/examples/nano-repl"
- "packages/web/examples/repl-example"
- "packages/superdough/example"
- "packages/codemirror/examples/strudelmirror"

View File

@ -0,0 +1,58 @@
---
import { pwaInfo } from 'virtual:pwa-info';
import '../styles/index.css';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<link rel="icon" type="image/svg+xml" href={`${baseNoTrailing}/favicon.ico`} />
<meta
name="description"
content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript."
/>
<link rel="icon" href={`${baseNoTrailing}/favicon.ico`} />
<link rel="apple-touch-icon" href={`${baseNoTrailing}/icons/apple-icon-180.png`} sizes="180x180" />
<meta name="theme-color" content="#222222" />
<base href={BASE_URL} />
<!-- Scrollable a11y code helper -->
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: -->
<!-- <style is:global define:vars={strudelTheme}></style> -->
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global>
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
</style>
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<script>
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
const appHeight = () => {
const doc = document.documentElement;
doc.style.setProperty('--app-height', `${window.innerHeight - 1}px`);
};
if (typeof window !== 'undefined') {
window.addEventListener('resize', appHeight);
appHeight();
}
</script>

View File

@ -37,4 +37,4 @@ What about combining different notes with different sounds at the same time?
Hmm, something interesting is going on there, related to there being five notes and three sounds.
Let's now take a step back and think about the Strudel [Code](/learn/code) we've been hearing so far.
Let's now take a step back and think about the Strudel [Code](/learn/code/) we've been hearing so far.

View File

@ -64,4 +64,4 @@ Together with layer, struct and voicings, this can be used to create a basic bac
)`}
/>
So far, we've stayed within the browser. [MIDI and OSC](/learn/input-output) are ways to break out of it.
So far, we've stayed within the browser. [MIDI and OSC](/learn/input-output/) are ways to break out of it.

View File

@ -0,0 +1,96 @@
---
import HeadCommonNew from '../../components/HeadCommonNew.astro';
---
<html lang="en" class="dark">
<head>
<HeadCommonNew />
<title>Strudel Vanilla REPL</title>
</head>
<body class="h-app-height">
<div class="settings">
<form name="settings" class="flex flex-col space-y-1 bg-[#00000080]">
<label
>theme
<select name="theme">
<option>strudelTheme</option>
<option>bluescreen</option>
<option>blackscreen</option>
<option>whitescreen</option>
<option>teletext</option>
<option>algoboy</option>
<option>terminal</option>
<option>abcdef</option>
<option>androidstudio</option>
<option>atomone</option>
<option>aura</option>
<option>bespin</option>
<option>darcula</option>
<option>dracula</option>
<option>duotoneDark</option>
<option>eclipse</option>
<option>githubDark</option>
<option>gruvboxDark</option>
<option>materialDark</option>
<option>nord</option>
<option>okaidia</option>
<option>solarizedDark</option>
<option>sublime</option>
<option>tokyoNight</option>
<option>tokyoNightStorm</option>
<option>vscodeDark</option>
<option>xcodeDark</option>
<option>bbedit</option>
<option>duotoneLight</option>
<option>githubLight</option>
<option>gruvboxLight</option>
<option>materialLight</option>
<option>noctisLilac</option>
<option>solarizedLight</option>
<option>tokyoNightDay</option>
<option>xcodeLight</option>
</select> </label
><br />
<label
>keybindings
<select name="keybindings">
<option>codemirror</option>
<option>vim</option>
<option>emacs</option>
<option>vscode</option>
</select> </label
><br />
<label>fontFamily
<select name="fontFamily">
<option value="monospace">monospace</option>
<option value="BigBlueTerminal">BigBlueTerminal</option>
<option value="x3270">x3270</option>
<option value="PressStart">PressStart2P</option>
<option value="galactico">galactico</option>
<option value="we-come-in-peace">we-come-in-peace</option>
<option value="FiraCode">FiraCode</option>
<option value="FiraCode-SemiBold">FiraCode-SemiBold</option>
<option value="teletext">teletext</option>
<option value="mode7">mode7</option>
</select>
</label>
<br />
<label>fontSize <input type="number" name="fontSize" /></label>
<br />
<label><input type="checkbox" name="isLineNumbersDisplayed" />isLineNumbersDisplayed</label>
<br />
<label><input type="checkbox" name="isActiveLineHighlighted" />isActiveLineHighlighted</label>
<br />
<label><input type="checkbox" name="isPatternHighlightingEnabled" />isPatternHighlightingEnabled</label>
<br />
<label><input type="checkbox" name="isFlashEnabled" />isFlashEnabled</label>
<br />
<label><input type="checkbox" name="isLineWrappingEnabled" />isLineWrappingEnabled</label>
<!-- <label><input type="checkbox" name="isAutoCompletionEnabled" />isAutoCompletionEnabled</label> -->
<!-- <label><input type="checkbox" name="isTooltipEnabled" />isTooltipEnabled</label> -->
</form>
</div>
<div id="code"></div>
<script src="../../repl/vanilla/vanilla.mjs"></script>
</body>
</html>

View File

@ -87,7 +87,7 @@ export function Header({ context }) {
)}
</button>
<button
onClick={handleUpdate}
onClick={() => handleUpdate()}
title="update"
className={cx(
'flex items-center space-x-1',

View File

@ -4,7 +4,16 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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 { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import {
cleanupDraw,
cleanupUi,
controls,
evalScope,
getDrawContext,
logger,
code2hash,
hash2code,
} from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
@ -17,13 +26,22 @@ import { prebake } from './prebake.mjs';
import * as tunes from './tunes.mjs';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { themes } from './themes.mjs';
import { settingsMap, useSettings, setLatestCode, updateUserCode } from '../settings.mjs';
import {
settingsMap,
useSettings,
setLatestCode,
updateUserCode,
setActivePattern,
getActivePattern,
getUserPattern,
initUserCode,
} from '../settings.mjs';
import Loader from './Loader';
import { settingPatterns } from '../settings.mjs';
import { code2hash, hash2code } from './helpers.mjs';
import { isTauri } from '../tauri.mjs';
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
import { writeText } from '@tauri-apps/api/clipboard';
import { registerSamplesFromDB, userSamplesDBConfig } from './idbutils.mjs';
const { latestCode } = settingsMap.get();
@ -131,7 +149,6 @@ export function Repl({ embedded = false }) {
isLineWrappingEnabled,
panelPosition,
isZen,
activePattern,
} = useSettings();
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
@ -177,6 +194,7 @@ export function Repl({ embedded = false }) {
let msg;
if (decoded) {
setCode(decoded);
initUserCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
setCode(latestCode);
@ -185,6 +203,8 @@ export function Repl({ embedded = false }) {
setCode(randomTune);
msg = `A random code snippet named "${name}" has been loaded!`;
}
//registers samples that have been saved to the index DB
registerSamplesFromDB(userSamplesDBConfig);
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
setPending(false);
});
@ -247,14 +267,21 @@ export function Repl({ embedded = false }) {
stop();
}
};
const handleUpdate = (newCode) => {
const handleUpdate = async (newCode, reset = false) => {
if (reset) {
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);
await prebake(); // declare default samples
}
(newCode || isDirty) && activateCode(newCode);
logger('[repl] code updated! tip: you can also update the code by pressing ctrl+enter', 'highlight');
logger('[repl] code updated!');
};
const handleShuffle = async () => {
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
setActivePattern(name);
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);

View File

@ -1,25 +0,0 @@
export function unicodeToBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const base64String = btoa(String.fromCharCode(...utf8Bytes));
return base64String;
}
export function base64ToUnicode(base64String) {
const utf8Bytes = new Uint8Array(
atob(base64String)
.split('')
.map((char) => char.charCodeAt(0)),
);
const decodedText = new TextDecoder().decode(utf8Bytes);
return decodedText;
}
export function code2hash(code) {
return encodeURIComponent(unicodeToBase64(code));
//return '#' + encodeURIComponent(btoa(code));
}
export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || ''));
}

View File

@ -0,0 +1,148 @@
import { registerSound, onTriggerSample } from '@strudel.cycles/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel.cycles/core';
//utilites for writing and reading to the indexdb
export const userSamplesDBConfig = {
dbName: 'samples',
table: 'usersamples',
columns: ['blob', 'title'],
version: 1,
};
// deletes all of the databases, useful for debugging
const clearIDB = () => {
window.indexedDB
.databases()
.then((r) => {
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
})
.then(() => {
alert('All data cleared.');
});
};
// queries the DB, and registers the sounds so they can be played
export const registerSamplesFromDB = (config, onComplete = () => {}) => {
openDB(config, (objectStore) => {
let query = objectStore.getAll();
query.onsuccess = (event) => {
const soundFiles = event.target.result;
if (!soundFiles?.length) {
return;
}
const sounds = new Map();
[...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.forEach((soundFile) => {
const title = soundFile.title;
if (!isAudioFile(title)) {
return;
}
const splitRelativePath = soundFile.id?.split('/');
const parentDirectory = splitRelativePath[splitRelativePath.length - 2];
const soundPath = soundFile.blob;
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths);
});
sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths);
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
});
logger('imported sounds registered!', 'success');
onComplete();
};
});
};
// creates a blob from a buffer that can be read
async function bufferToDataUrl(buf) {
return new Promise((resolve) => {
var blob = new Blob([buf], { type: 'application/octet-binary' });
var reader = new FileReader();
reader.onload = function (event) {
resolve(event.target.result);
};
reader.readAsDataURL(blob);
});
}
//open db and initialize it if necessary
const openDB = (config, onOpened) => {
const { dbName, version, table, columns } = config;
if (!('indexedDB' in window)) {
console.log('IndexedDB is not supported.');
return;
}
const dbOpen = indexedDB.open(dbName, version);
dbOpen.onupgradeneeded = (_event) => {
const db = dbOpen.result;
const objectStore = db.createObjectStore(table, { keyPath: 'id', autoIncrement: false });
columns.forEach((c) => {
objectStore.createIndex(c, c, { unique: false });
});
};
dbOpen.onerror = (err) => {
logger('Something went wrong while trying to open the the client DB', 'error');
console.error(`indexedDB error: ${err.errorCode}`);
};
dbOpen.onsuccess = () => {
const db = dbOpen.result;
const // lock store for writing
writeTransaction = db.transaction([table], 'readwrite'),
// get object store
objectStore = writeTransaction.objectStore(table);
onOpened(objectStore, db);
};
};
const processFilesForIDB = async (files) => {
return await Promise.all(
Array.from(files)
.map(async (s) => {
const title = s.name;
if (!isAudioFile(title)) {
return;
}
//create obscured url to file system that can be fetched
const sUrl = URL.createObjectURL(s);
//fetch the sound and turn it into a buffer array
const buf = await fetch(sUrl).then((res) => res.arrayBuffer());
//create a url blob containing all of the buffer data
const base64 = await bufferToDataUrl(buf);
return {
title,
blob: base64,
id: s.webkitRelativePath,
};
})
.filter(Boolean),
).catch((error) => {
logger('Something went wrong while processing uploaded files', 'error');
console.error(error);
});
};
export const uploadSamplesToDB = async (config, files) => {
await processFilesForIDB(files).then((files) => {
const onOpened = (objectStore, _db) => {
files.forEach((file) => {
if (file == null) {
return;
}
objectStore.put(file);
});
};
openDB(config, onOpened);
});
};

View File

@ -9,7 +9,7 @@ export function ButtonGroup({ value, onChange, items }) {
key={key}
onClick={() => onChange(key)}
className={cx(
'px-2 border-b h-8',
'px-2 border-b h-8 whitespace-nowrap',
// i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',

View File

@ -0,0 +1,43 @@
import React, { useCallback, useState } from 'react';
import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs';
//choose a directory to locally import samples
export default function ImportSoundsButton({ onComplete }) {
let fileUploadRef = React.createRef();
const [isUploading, setIsUploading] = useState(false);
const onChange = useCallback(async () => {
if (!fileUploadRef.current.files?.length) {
return;
}
setIsUploading(true);
await uploadSamplesToDB(userSamplesDBConfig, fileUploadRef.current.files).then(() => {
registerSamplesFromDB(userSamplesDBConfig, () => {
onComplete();
setIsUploading(false);
});
});
});
return (
<label
style={{ alignItems: 'center' }}
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds'}
</label>
);
}

View File

@ -1,93 +1,119 @@
import React from 'react';
import * as tunes from '../tunes.mjs';
import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid';
import { useMemo } from 'react';
import {
useSettings,
clearUserPatterns,
newUserPattern,
setActivePattern,
deleteActivePattern,
duplicateActivePattern,
exportPatterns,
getUserPattern,
importPatterns,
newUserPattern,
renameActivePattern,
setActivePattern,
useActivePattern,
useSettings,
} from '../../settings.mjs';
import * as tunes from '../tunes.mjs';
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
export function PatternsTab({ context }) {
const { userPatterns, activePattern } = useSettings();
const { userPatterns } = useSettings();
const activePattern = useActivePattern();
const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]);
return (
<div className="px-4 w-full text-foreground space-y-4">
<div className="px-4 w-full dark:text-white text-stone-900 space-y-4 pb-4">
<section>
<h2 className="text-xl mb-2">Pattern Collection</h2>
<div className="space-x-4 border-b border-foreground mb-1">
{activePattern && (
<div className="flex items-center mb-2 space-x-2 overflow-auto">
<h1 className="text-xl">{activePattern}</h1>
<div className="space-x-4 flex w-min">
{!isExample && (
<button className="hover:opacity-50" onClick={() => renameActivePattern()} title="Rename">
<PencilSquareIcon className="w-5 h-5" />
{/* <PencilIcon className="w-5 h-5" /> */}
</button>
)}
<button className="hover:opacity-50" onClick={() => duplicateActivePattern()} title="Duplicate">
<DocumentDuplicateIcon className="w-5 h-5" />
</button>
{!isExample && (
<button className="hover:opacity-50" onClick={() => deleteActivePattern()} title="Delete">
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
</div>
)}
<div className="font-mono text-sm">
{Object.entries(userPatterns).map(([key, up]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
const { code } = up;
setActivePattern(key);
context.handleUpdate(code, true);
}}
>
{key}
</a>
))}
</div>
<div className="pr-4 space-x-4 border-b border-foreground mb-2 h-8 flex overflow-auto max-w-full items-center">
<button
className="hover:opacity-50"
onClick={() => {
const name = newUserPattern();
const { code } = getUserPattern(name);
context.handleUpdate(code);
context.handleUpdate(code, true);
}}
>
new
</button>
<button className="hover:opacity-50" onClick={() => duplicateActivePattern()}>
duplicate
</button>
<button className="hover:opacity-50" onClick={() => renameActivePattern()}>
rename
</button>
<button className="hover:opacity-50" onClick={() => deleteActivePattern()}>
delete
</button>
<button className="hover:opacity-50" onClick={() => clearUserPatterns()}>
clear
</button>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<button className="hover:opacity-50" onClick={() => exportPatterns()}>
export
</button>
</div>
{Object.entries(userPatterns).map(([key, up]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'underline' : '',
)}
onClick={() => {
const { code } = up;
setActivePattern(key);
context.handleUpdate(code);
}}
>
{key}
</a>
))}
</section>
<section>
<h2 className="text-xl mb-2">Examples</h2>
{Object.entries(tunes).map(([key, tune]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'underline' : '',
)}
onClick={() => {
setActivePattern(key);
context.handleUpdate(tune);
}}
>
{key}
</a>
))}
<div className="font-mono text-sm">
{Object.entries(tunes).map(([key, tune]) => (
<a
key={key}
className={classNames(
'mr-4 hover:opacity-50 cursor-pointer inline-block',
key === activePattern ? 'outline outline-1' : '',
)}
onClick={() => {
setActivePattern(key);
context.handleUpdate(tune, true);
}}
>
{key}
</a>
))}
</div>
</section>
</div>
);
}
/*
selectable examples
if example selected
type character -> create new user pattern with exampleName_n
even if
clicking (+) opens the "new" example with same behavior as above
*/

View File

@ -5,6 +5,7 @@ import { getAudioContext, soundMap, connectToDestination } from '@strudel.cycles
import React, { useMemo, useRef } from 'react';
import { settingsMap, useSettings } from '../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
@ -42,8 +43,8 @@ export function SoundsTab() {
});
});
return (
<div id="sounds-tab" className="flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="px-2 pb-2 flex-none">
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="pb-2 flex shrink-0 overflow-auto">
<ButtonGroup
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
@ -54,8 +55,9 @@ export function SoundsTab() {
user: 'User',
}}
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="p-2 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">
{soundEntries.map(([name, { data, onTrigger }]) => (
<span
key={name}

View File

@ -114,6 +114,9 @@ export async function prebake() {
],
},
'github:tidalcycles/Dirt-Samples/master/',
{
prebake: true,
},
),
]);
// await samples('github:tidalcycles/Dirt-Samples/master');

View File

@ -0,0 +1,34 @@
body,
input {
font-family: monospace;
background-color: black;
color: white;
}
input,
select {
background-color: black !important;
}
html,
body,
#code,
.cm-editor,
.cm-scroller {
padding: 0;
margin: 0;
height: 100%;
}
.settings {
position: fixed;
right: 0;
top: 0;
z-index: 1000;
display: flex-col;
padding: 10px;
}
.settings > form > * + * {
margin-top: 10px;
}

View File

@ -0,0 +1,202 @@
import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core';
import { StrudelMirror, initTheme, activateTheme } from '@strudel/codemirror';
import { transpiler } from '@strudel.cycles/transpiler';
import {
getAudioContext,
webaudioOutput,
registerSynthSounds,
registerZZFXSounds,
samples,
} from '@strudel.cycles/webaudio';
import './vanilla.css';
let editor;
const initialSettings = {
keybindings: 'codemirror',
isLineNumbersDisplayed: true,
isActiveLineHighlighted: true,
isAutoCompletionEnabled: false,
isPatternHighlightingEnabled: true,
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
theme: 'teletext',
fontFamily: 'monospace',
fontSize: 18,
};
initTheme(initialSettings.theme);
async function run() {
const container = document.getElementById('code');
if (!container) {
console.warn('could not init: no container found');
return;
}
const drawContext = getDrawContext();
const drawTime = [-2, 2];
editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: container,
initialCode: '// LOADING',
pattern: silence,
settings: initialSettings,
drawTime,
onDraw: (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
},
prebake: async () => {
// populate scope / lazy load modules
const modulesLoading = evalScope(
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
// import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel/codemirror'),
/* import('@strudel/hydra'), */
// import('@strudel.cycles/serial'),
/* import('@strudel.cycles/soundfonts'), */
// import('@strudel.cycles/csound'),
/* import('@strudel.cycles/midi'), */
// import('@strudel.cycles/osc'),
controls, // sadly, this cannot be exported from core directly (yet)
);
// load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
await Promise.all([
modulesLoading,
registerSynthSounds(),
registerZZFXSounds(),
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/vcsl.json`),
]);
},
afterEval: ({ code }) => {
window.location.hash = '#' + code2hash(code);
},
});
// init settings
editor.updateSettings(initialSettings);
logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight');
const codeParam = window.location.href.split('#')[1] || '';
const initialCode = codeParam
? hash2code(codeParam)
: `// @date 23-11-30
// "teigrührgerät" @by froos
stack(
stack(
s("bd(<3!3 5>,6)/2").bank('RolandTR707')
,
s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>")
.lastOf(8, x=>x.segment("12").end(.2).gain(isaw))
,
s("[tb ~ tb]").bank('RolandTR707')
.clip(0).release(.08).room(.2)
).off(-1/6, x=>x.speed(.7).gain(.2).degrade())
,
stack(
note("<g1(<3 4>,6) ~!2 [f1?]*2>")
.s("sawtooth").lpf(perlin.range(400,1000))
.lpa(.1).lpenv(-3).room(.2)
.lpq(8).noise(.2)
.add(note("0,.1"))
,
chord("<~ Gm9 ~!2>")
.dict('ireal').voicing()
.s("sawtooth").vib("2:.1")
.lpf(1000).lpa(.1).lpenv(-4)
.room(.5)
,
n(run(3)).chord("<Gm9 Gm11>/8")
.dict('ireal-ext')
.off(1/2, add(n(4)))
.voicing()
.clip(.1).release(.05)
.s("sine").jux(rev)
.sometimesBy(sine.slow(16), add(note(12)))
.room(.75)
.lpf(sine.range(200,2000).slow(16))
.gain(saw.slow(4).div(2))
).add(note(perlin.range(0,.5)))
)`;
editor.setCode(initialCode); // simpler alternative to above init
// settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key]));
onEvent('strudel-toggle-play', () => editor.toggle());
}
run();
function onEvent(key, callback) {
const listener = (e) => {
if (e.data === key) {
callback();
}
};
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}
// settings form
function getInput(form, name) {
return form.querySelector(`input[name=${name}]`) || form.querySelector(`select[name=${name}]`);
}
function getFormValues(form, initial) {
const entries = Object.entries(initial).map(([key, initialValue]) => {
const input = getInput(form, key);
if (!input) {
return [key, initialValue]; // fallback
}
if (input.type === 'checkbox') {
return [key, input.checked];
}
if (input.type === 'number') {
return [key, Number(input.value)];
}
if (input.tagName === 'SELECT') {
return [key, input.value];
}
return [key, input.value];
});
return Object.fromEntries(entries);
}
function setFormValues(form, values) {
Object.entries(values).forEach(([key, value]) => {
const input = getInput(form, key);
if (!input) {
return;
}
if (input.type === 'checkbox') {
input.checked = !!value;
} else if (input.type === 'number') {
input.value = value;
} else if (input.tagName) {
input.value = value;
}
});
}
const form = document.querySelector('form[name=settings]');
setFormValues(form, initialSettings);
form.addEventListener('change', () => {
const values = getFormValues(form, initialSettings);
// console.log('values', values);
editor.updateSettings(values);
// TODO: only activateTheme when it changes
activateTheme(values.theme);
});

View File

@ -1,7 +1,8 @@
import { persistentMap } from '@nanostores/persistent';
import { persistentMap, persistentAtom } from '@nanostores/persistent';
import { useStore } from '@nanostores/react';
import { register } from '@strudel.cycles/core';
import * as tunes from './repl/tunes.mjs';
import { logger } from '@strudel.cycles/core';
export const defaultSettings = {
activeFooter: 'intro',
@ -19,11 +20,28 @@ export const defaultSettings = {
soundsFilter: 'all',
panelPosition: 'bottom',
userPatterns: '{}',
activePattern: '',
};
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
// active pattern is separate, because it shouldn't sync state across tabs
// reason: https://github.com/tidalcycles/strudel/issues/857
const $activePattern = persistentAtom('activePattern', '', { listen: false });
export function setActivePattern(key) {
$activePattern.set(key);
}
export function getActivePattern() {
return $activePattern.get();
}
export function useActivePattern() {
return useStore($activePattern);
}
export function initUserCode(code) {
const userPatterns = getUserPatterns();
const match = Object.entries(userPatterns).find(([_, pat]) => pat.code === code);
setActivePattern(match?.[0] || '');
}
export function useSettings() {
const state = useStore(settingsMap);
return {
@ -62,14 +80,14 @@ export const fontSize = patternSetting('fontSize');
export const settingPatterns = { theme, fontFamily, fontSize };
function getUserPatterns() {
export function getUserPatterns() {
return JSON.parse(settingsMap.get().userPatterns);
}
function getSetting(key) {
return settingsMap.get()[key];
}
function setUserPatterns(obj) {
export function setUserPatterns(obj) {
settingsMap.setKey('userPatterns', JSON.stringify(obj));
}
@ -116,13 +134,17 @@ export function getUserPattern(key) {
}
export function renameActivePattern() {
let activePattern = getSetting('activePattern');
let activePattern = getActivePattern();
let userPatterns = getUserPatterns();
if (!userPatterns[activePattern]) {
alert('Cannot rename examples');
return;
}
const newName = prompt('Enter new name', activePattern);
if (newName === null) {
// canceled
return;
}
if (userPatterns[newName]) {
alert('Name already taken!');
return;
@ -135,7 +157,7 @@ export function renameActivePattern() {
export function updateUserCode(code) {
const userPatterns = getUserPatterns();
let activePattern = getSetting('activePattern');
let activePattern = getActivePattern();
// check if code is that of an example tune
const [example] = Object.entries(tunes).find(([_, tune]) => tune === code) || [];
if (example && (!activePattern || activePattern === example)) {
@ -156,7 +178,7 @@ export function updateUserCode(code) {
}
export function deleteActivePattern() {
let activePattern = getSetting('activePattern');
let activePattern = getActivePattern();
if (!activePattern) {
console.warn('cannot delete: no pattern selected');
return;
@ -174,7 +196,7 @@ export function deleteActivePattern() {
}
export function duplicateActivePattern() {
let activePattern = getSetting('activePattern');
let activePattern = getActivePattern();
let latestCode = getSetting('latestCode');
if (!activePattern) {
console.warn('cannot duplicate: no pattern selected');
@ -186,7 +208,31 @@ export function duplicateActivePattern() {
setActivePattern(activePattern);
}
export function setActivePattern(key) {
console.log('set', key);
settingsMap.setKey('activePattern', key);
export async function importPatterns(fileList) {
const files = Array.from(fileList);
await Promise.all(
files.map(async (file, i) => {
const content = await file.text();
if (file.type === 'application/json') {
const userPatterns = getUserPatterns() || {};
setUserPatterns({ ...userPatterns, ...JSON.parse(content) });
} else if (file.type === 'text/plain') {
const name = file.name.replace(/\.[^/.]+$/, '');
addUserPattern(name, { code: content });
}
}),
);
logger(`import done!`);
}
export async function exportPatterns() {
const userPatterns = getUserPatterns() || {};
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
const date = new Date().toISOString().split('T')[0];
downloadLink.download = `strudel_patterns_${date}.json`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}