Merge branch 'main' into audio_device_selection

This commit is contained in:
Jade (Rose) Rowland 2023-12-29 20:37:29 -05:00 committed by GitHub
commit 3152bc123a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
140 changed files with 1186 additions and 6426 deletions

View file

@ -46,41 +46,9 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<script>
import { settings, injectStyle } from '../repl/themes.mjs';
import { settingsMap } from '../settings.mjs';
import { listenKeys } from 'nanostores';
const themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme';
document.head.append(themeStyle);
import { initTheme, codemirrorSettings } from '@strudel/codemirror';
let resetThemeStyle;
function activateTheme(name) {
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);
}
}
activateTheme(settingsMap.get().theme);
listenKeys(settingsMap, ['theme'], ({ theme }) => activateTheme(theme));
initTheme(codemirrorSettings.get().theme);
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
const appHeight = () => {

View file

@ -1,5 +1,5 @@
import useEvent from '@strudel.cycles/react/src/hooks/useEvent.mjs';
import useFrame from '@strudel.cycles/react/src/hooks/useFrame.mjs';
import useEvent from '@src/useEvent.mjs';
import useFrame from '@src/useFrame.mjs';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { midi2note } from '@strudel.cycles/core';
import { useState, useRef, useEffect } from 'react';

10
website/src/cx.mjs Normal file
View file

@ -0,0 +1,10 @@
/*
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
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/>.
*/
export default function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}

38
website/src/docs/Icon.jsx Normal file
View file

@ -0,0 +1,38 @@
export function Icon({ type }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
{
{
refresh: (
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
),
play: (
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clipRule="evenodd"
/>
),
pause: (
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
),
stop: (
<path
fillRule="evenodd"
d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
);
}

View file

@ -1,84 +1,156 @@
import { evalScope, controls, noteToMidi } from '@strudel.cycles/core';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { useEffect, useState } from 'react';
import { prebake } from '../repl/prebake';
import { themes, settings } from '../repl/themes.mjs';
import './MiniRepl.css';
import { useSettings } from '../settings.mjs';
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon';
import { silence, getPunchcardPainter, noteToMidi } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import { StrudelMirror } from '@strudel/codemirror';
// import { prebake } from '@strudel/repl';
import { prebake } from '../repl/prebake.mjs';
import { loadModules } from '../repl/util.mjs';
import Claviature from '@components/Claviature';
import useClient from '@src/useClient.mjs';
let modules;
let prebaked, modulesLoading;
if (typeof window !== 'undefined') {
modules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/csound'),
import('@strudel.cycles/soundfonts'),
import('@strudel/hydra'),
);
}
if (typeof window !== 'undefined') {
initAudioOnFirstClick();
prebake();
prebaked = prebake();
modulesLoading = loadModules();
}
export function MiniRepl({
tune,
drawTime,
tune: code,
hideHeader = false,
canvasHeight = 100,
onTrigger,
punchcard,
punchcardLabels = true,
span = [0, 4],
canvasHeight = 100,
hideHeader,
claviature,
claviatureLabels,
}) {
const [Repl, setRepl] = useState();
const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed, isActiveLineHighlighted } = useSettings();
const id = useMemo(() => s4(), []);
const canvasId = useMemo(() => `canvas-${id}`, [id]);
const shouldDraw = !!punchcard || !!claviature;
const shouldShowCanvas = !!punchcard;
const drawTime = punchcard ? [0, 4] : [0, 0];
const [activeNotes, setActiveNotes] = useState([]);
useEffect(() => {
// we have to load this package on the client
// because codemirror throws an error on the server
Promise.all([import('@strudel.cycles/react'), modules])
.then(([res]) => setRepl(() => res.MiniRepl))
.catch((err) => console.error(err));
}, []);
return Repl ? (
<div className="mb-4 mini-repl">
<Repl
tune={tune}
hideOutsideView={true}
drawTime={claviature ? [0, 0] : drawTime}
punchcard={punchcard}
punchcardLabels={punchcardLabels}
span={span}
canvasHeight={canvasHeight}
theme={themes[theme]}
hideHeader={hideHeader}
keybindings={keybindings}
fontFamily={fontFamily}
fontSize={fontSize}
isLineNumbersDisplayed={isLineNumbersDisplayed}
isActiveLineHighlighted={isActiveLineHighlighted}
onPaint={
claviature
? (ctx, time, haps, drawTime) => {
const active = haps
.map((hap) => hap.value.note)
.filter(Boolean)
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
setActiveNotes(active);
}
: undefined
const init = useCallback(({ code, shouldDraw }) => {
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
let onDraw;
if (shouldDraw) {
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 });
});
};
}
const editor = new StrudelMirror({
id,
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
autodraw: !!shouldDraw,
root: containerRef.current,
initialCode: '// LOADING',
pattern: silence,
drawTime,
onDraw,
editPattern: (pat, id) => {
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
/>
if (claviature) {
editor?.painters.push((ctx, time, haps, drawTime) => {
const active = haps
.map((hap) => hap.value.note)
.filter(Boolean)
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
setActiveNotes(active);
});
}
if (punchcard) {
editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels }));
}
return pat;
},
prebake: async () => Promise.all([modulesLoading, prebaked]),
onUpdateState: (state) => {
setReplState({ ...state });
},
});
// init settings
editor.setCode(code);
editorRef.current = editor;
}, []);
const [replState, setReplState] = useState({});
const { started, isDirty, error } = replState;
const editorRef = useRef();
const containerRef = useRef();
const client = useClient();
if (!client) {
return <pre>{code}</pre>;
}
return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight">
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => editorRef.current?.toggle()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => editorRef.current?.evaluate()}
>
<Icon type="refresh" />
</button>
</div>
</div>
)}
<div className="overflow-auto relative p-1">
<div
ref={(el) => {
if (!editorRef.current) {
containerRef.current = el;
init({ code, shouldDraw });
}
}}
></div>
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{shouldShowCanvas && (
<canvas
id={canvasId}
className="w-full pointer-events-none border-t border-lineHighlight"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
{/* !!log.length && (
<div className="bg-gray-800 rounded-md p-2">
{log.map(({ message }, i) => (
<div key={i}>{message}</div>
))}
</div>
) */}
{claviature && (
<Claviature
options={{
@ -90,7 +162,16 @@ export function MiniRepl({
/>
)}
</div>
) : (
<pre>{tune}</pre>
);
}
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}

View file

@ -1,6 +1,6 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import { Repl } from '../repl/Repl.jsx';
import { Repl } from '../repl/Repl';
---
<html lang="en" class="dark">
@ -9,6 +9,6 @@ import { Repl } from '../repl/Repl.jsx';
<title>Strudel REPL</title>
</head>
<body class="h-app-height bg-background">
<Repl client:only="react" />
<Repl client:load />
</body>
</html>

View file

@ -9,7 +9,7 @@ The docs page is built ontop of astro's [docs site](https://github.com/withastro
## Adding a new Docs Page
1. add a `.mdx` file in a path under `website/src/pages/`, e.g. [website/src/pages/learn/code.mdx](https://raw.githubusercontent.com/tidalcycles/strudel/main/website/src/pages/learn/code.mdx) will be available under https://strudel.cc/learn/code (or locally under `http://localhost:4321/learn/code`)
1. add a `.mdx` file in a path under `website/src/pages/`, e.g. [website/src/pages/learn/code.mdx](https://raw.githubusercontent.com/tidalcycles/strudel/main/website/src/pages/learn/code.mdx) will be available under https://strudel.cc/learn/code/ (or locally under `http://localhost:4321/learn/code/`)
2. make sure to copy the top part of another existing docs page. Adjust the title accordingly
3. To add a link to the sidebar, add a new entry to `SIDEBAR` to [`config.ts`](https://github.com/tidalcycles/strudel/blob/main/website/src/config.ts)

View file

@ -14,7 +14,7 @@ Example:
<MiniRepl
client:idle
tune={`const pattern = sequence(c3, [e3, g3])
tune={`const pattern = sequence("c3", ["e3", "g3"])
const events = pattern.queryArc(0, 1)
console.log(events.map((e) => e.show()))
silence`}

View file

@ -1,96 +0,0 @@
---
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

@ -83,7 +83,6 @@ Can you find melodies that are actual words? Hint: ☕ 😉 ⚪
client:visible
tune={`note("c2 e3 g4 b5").sound("piano")`}
claviature
claviatureLabels={Object.fromEntries(['c1', 'c2', 'c3', 'c4', 'c5'].map((n) => [n, n]))}
claviatureLabels={Object.fromEntries(
Array(49)
.fill()

View file

@ -4,8 +4,7 @@ import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import { cx } from '@strudel.cycles/react';
import React, { useContext } from 'react';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../settings.mjs';
// import { ReplContext } from './Repl';
import './Repl.css';
@ -18,14 +17,13 @@ export function Header({ context }) {
started,
pending,
isDirty,
lastShared,
activeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
handleShare,
} = context;
const isEmbedded = embedded || window.location !== window.parent.location;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen } = useSettings();
return (
@ -119,7 +117,7 @@ export function Header({ context }) {
onClick={handleShare}
>
<LinkIcon className="w-6 h-6" />
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
<span>share</span>
</button>
)}
{!isEmbedded && (

View file

@ -1,5 +1,4 @@
import { cx } from '@strudel.cycles/react';
import React from 'react';
import cx from '@src/cx.mjs';
function Loader({ active }) {
return (

View file

@ -1,367 +1,174 @@
/*
App.js - <short description TODO>
Repl.jsx - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
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 { code2hash, getDrawContext, logger, silence } from '@strudel.cycles/core';
import cx from '@src/cx.mjs';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
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';
import { nanoid } from 'nanoid';
import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react';
initUserCode,
setActivePattern,
setLatestCode,
settingsMap,
updateUserCode,
useSettings,
} from '../settings.mjs';
import { Header } from './Header';
import Loader from './Loader';
import './Repl.css';
import { Panel } from './panel/Panel';
import { Header } from './Header';
import { useStore } from '@nanostores/react';
import { prebake } from './prebake.mjs';
import * as tunes from './tunes.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { themes } from './themes.mjs';
import {
settingsMap,
useSettings,
setLatestCode,
updateUserCode,
setActivePattern,
getActivePattern,
getUserPattern,
initUserCode,
} from '../settings.mjs';
import Loader from './Loader';
import { settingPatterns } from '../settings.mjs';
import { isTauri } from '../tauri.mjs';
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
import { writeText } from '@tauri-apps/api/clipboard';
import { defaultAudioDeviceName, getAudioDevices, setAudioDevice } from './panel/AudioDeviceSelector';
import { registerSamplesFromDB, userSamplesDBConfig } from './idbutils.mjs';
import './Repl.css';
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
const { latestCode } = settingsMap.get();
initAudioOnFirstClick();
// Create a single supabase client for interacting with your database
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
let modules = [
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'),
];
if (isTauri()) {
modules = modules.concat([
import('@strudel/desktopbridge/loggerbridge.mjs'),
import('@strudel/desktopbridge/midibridge.mjs'),
import('@strudel/desktopbridge/oscbridge.mjs'),
]);
} else {
modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
}
const modulesLoading = evalScope(
controls, // sadly, this cannot be exported from core direclty
settingPatterns,
...modules,
);
const presets = prebake();
let drawContext, clearCanvas;
let modulesLoading, presets, drawContext, clearCanvas, isIframe;
if (typeof window !== 'undefined') {
initAudioOnFirstClick();
modulesLoading = loadModules();
presets = prebake();
drawContext = getDrawContext();
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
isIframe = window.location !== window.parent.location;
}
const getTime = () => getAudioContext().currentTime;
async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
.select('code')
.eq('hash', hash)
.then(({ data, error }) => {
if (error) {
console.warn('failed to load hash', err);
}
if (data.length) {
//console.log('load hash from database', hash);
return data[0].code;
}
});
}
} catch (err) {
console.warn('failed to decode', err);
}
}
function getRandomTune() {
const allTunes = Object.entries(tunes);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const [name, code] = randomItem(allTunes);
return { name, code };
}
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
export function Repl({ embedded = false }) {
const isEmbedded = embedded || window.location !== window.parent.location;
const [view, setView] = useState(); // codemirror view
const [lastShared, setLastShared] = useState();
const [pending, setPending] = useState(true);
const {
theme,
keybindings,
fontSize,
fontFamily,
isLineNumbersDisplayed,
isActiveLineHighlighted,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
panelPosition,
isZen,
audio_device_selection,
activePattern,
audioDeviceName,
} = useSettings();
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
const { setWidgets } = useWidgets(view);
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({
initialCode: '// LOADING...',
const isEmbedded = embedded || isIframe;
const { panelPosition, isZen } = useSettings();
const init = useCallback(() => {
const drawTime = [-2, 2];
const drawContext = getDrawContext();
const 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 });
});
};
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime,
beforeEval: async () => {
setPending(true);
await modulesLoading;
cleanupUi();
cleanupDraw();
getTime: () => getAudioContext().currentTime,
transpiler,
autodraw: false,
root: containerRef.current,
initialCode: '// LOADING',
pattern: silence,
drawTime,
onDraw,
prebake: async () => Promise.all([modulesLoading, presets]),
onUpdateState: (state) => {
setReplState({ ...state });
},
afterEval: ({ code, meta }) => {
afterEval: ({ code }) => {
updateUserCode(code);
setMiniLocations(meta.miniLocations);
setWidgets(meta.widgets);
setPending(false);
// setPending(false);
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
},
onEvalError: (err) => {
setPending(false);
},
onToggle: (play) => {
if (!play) {
cleanupDraw(false);
window.postMessage('strudel-stop');
} else {
window.postMessage('strudel-start');
}
},
drawContext,
// drawTime: [0, 6],
paintOptions,
bgFill: false,
});
//on first load...
useEffect(() => {
// init code
// init settings
initCode().then((decoded) => {
let msg;
if (decoded) {
setCode(decoded);
editor.setCode(decoded);
initUserCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
setCode(latestCode);
editor.setCode(latestCode);
msg = `Your last session has been loaded!`;
} /* if(randomTune) */ else {
setCode(randomTune);
} else {
editor.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);
// setPending(false);
});
// Initialize user audio device if it has been saved to settings
if (audioDeviceName !== defaultAudioDeviceName) {
getAudioDevices().then((devices) => {
const deviceID = devices.get(audioDeviceName);
if (deviceID == null) {
return;
}
setAudioDevice(deviceID);
});
}
editorRef.current = editor;
}, []);
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
if (getAudioContext().state !== 'running') {
alert('please click play to initialize the audio. you can use shortcuts after that!');
return;
}
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
}
},
[activateCode, stop, view],
),
);
const [replState, setReplState] = useState({});
const { started, isDirty, error, activeCode, pending } = replState;
const editorRef = useRef();
const containerRef = useRef();
// highlighting
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
// this will be the case when the main repl is being replaced
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
useEffect(() => {
let editorSettings = {};
Object.keys(defaultSettings).forEach((key) => {
if (_settings.hasOwnProperty(key)) {
editorSettings[key] = _settings[key];
}
});
editorRef.current?.updateSettings(editorSettings);
}, [_settings]);
//
// UI Actions
//
const handleChangeCode = useCallback(
(c) => {
setCode(c);
// started && logger('[edit] code changed. hit ctrl+enter to update');
},
[started],
);
const handleSelectionChange = useCallback((selection) => {
// TODO: scroll to selected function in reference
// console.log('selectino change', selection.ranges[0].from);
}, []);
const handleTogglePlay = async () => {
await getAudioContext().resume(); // fixes no sound in ios webkit
if (!started) {
logger('[repl] started. tip: you can also start by pressing ctrl+enter', 'highlight');
activateCode();
} else {
logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight');
stop();
}
};
const handleTogglePlay = async () => editorRef.current?.toggle();
const handleUpdate = async (newCode, reset = false) => {
if (reset) {
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
}
(newCode || isDirty) && activateCode(newCode);
if (newCode || isDirty) {
editorRef.current.setCode(newCode);
editorRef.current.repl.evaluate(newCode);
}
logger('[repl] code updated!');
};
const handleShuffle = async () => {
// window.postMessage('strudel-shuffle');
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
setActivePattern(name);
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
await evaluate(code, false);
editorRef.current.setCode(code);
editorRef.current.repl.evaluate(code);
};
const handleShare = async () => {
const codeToShare = activeCode || code;
if (lastShared === codeToShare) {
logger(`Link already generated!`, 'error');
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
setLastShared(activeCode || code);
// copy shareUrl to clipboard
if (isTauri()) {
await writeText(shareUrl);
} else {
await navigator.clipboard.writeText(shareUrl);
}
const message = `Link copied to clipboard: ${shareUrl}`;
alert(message);
// alert(message);
logger(message, 'highlight');
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
logger(message);
}
};
const handleShare = async () => shareCode(activeCode);
const context = {
scheduler,
embedded,
started,
pending,
isDirty,
lastShared,
activeCode,
handleChangeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
handleShare,
};
const currentTheme = useMemo(() => themes[theme] || themes.strudelTheme, [theme]);
const handleViewChanged = useCallback((v) => {
setView(v);
}, []);
return (
// bg-gradient-to-t from-blue-900 to-slate-900
// bg-gradient-to-t from-green-900 to-slate-900
<ReplContext.Provider value={context}>
<div
className={cx(
'h-full flex flex-col relative',
// overflow-hidden
)}
>
<div className={cx('h-full flex flex-col relative')}>
<Loader active={pending} />
<Header context={context} />
{isEmbedded && !started && (
@ -374,23 +181,16 @@ export function Repl({ embedded = false }) {
</button>
)}
<div className="grow flex relative overflow-hidden">
<section className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')} id="code">
<CodeMirror
theme={currentTheme}
value={code}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
isActiveLineHighlighted={isActiveLineHighlighted}
isAutoCompletionEnabled={isAutoCompletionEnabled}
isTooltipEnabled={isTooltipEnabled}
isLineWrappingEnabled={isLineWrappingEnabled}
fontSize={fontSize}
fontFamily={fontFamily}
onChange={handleChangeCode}
onViewChanged={handleViewChanged}
onSelectionChange={handleSelectionChange}
/>
</section>
<section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
id="code"
ref={(el) => {
containerRef.current = el;
if (!editorRef.current) {
init();
}
}}
></section>
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
</div>
{error && (

View file

@ -24,7 +24,7 @@ const clearIDB = () => {
};
// queries the DB, and registers the sounds so they can be played
export const registerSamplesFromDB = (config, onComplete = () => {}) => {
export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = () => {}) => {
openDB(config, (objectStore) => {
let query = objectStore.getAll();
query.onsuccess = (event) => {
@ -77,6 +77,9 @@ async function bufferToDataUrl(buf) {
//open db and initialize it if necessary
const openDB = (config, onOpened) => {
const { dbName, version, table, columns } = config;
if (typeof window === 'undefined') {
return;
}
if (!('indexedDB' in window)) {
console.log('IndexedDB is not supported.');
return;

View file

@ -1,5 +1,4 @@
import { cx } from '@strudel.cycles/react';
import React from 'react';
import cx from '@src/cx.mjs';
export function ConsoleTab({ log }) {
return (

View file

@ -1,5 +1,4 @@
import { cx } from '@strudel.cycles/react';
import React from 'react';
import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) {
return (

View file

@ -1,8 +1,9 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel.cycles/core';
import { cx, useEvent } from '@strudel.cycles/react';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
import { setActiveFooter, useSettings } from '../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
@ -11,21 +12,25 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import useClient from '@src/useClient.mjs';
const TAURI = window.__TAURI__;
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function Panel({ context }) {
const footerContent = useRef();
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
// scroll log box to bottom when log changes
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
}
}, [log, activeFooter]);
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
if (!footerContent.current) {
} else if (activeFooter === 'console') {
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
@ -79,6 +84,10 @@ export function Panel({ context }) {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
const client = useClient();
if (!client) {
return null;
}
return (
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
<div className="flex justify-between px-2">
@ -105,7 +114,7 @@ export function Panel({ context }) {
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
{activeFooter === 'settings' && <SettingsTab />}
{activeFooter === 'files' && <FilesTab />}
</div>
</div>

View file

@ -1,6 +1,5 @@
import React from 'react';
import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs';
import { themes } from '../themes.mjs';
import { themes } from '@strudel/codemirror';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
@ -79,9 +78,11 @@ export function SettingsTab() {
theme,
keybindings,
isLineNumbersDisplayed,
isPatternHighlightingEnabled,
isActiveLineHighlighted,
isAutoCompletionEnabled,
isTooltipEnabled,
isFlashEnabled,
isLineWrappingEnabled,
fontSize,
fontFamily,
@ -91,24 +92,6 @@ export function SettingsTab() {
return (
<div className="text-foreground p-4 space-y-4">
{/* <FormItem label="Tempo">
<div className="space-x-4">
<button
onClick={() => {
scheduler.setCps(scheduler.cps - 0.1);
}}
>
slower
</button>
<button
onClick={() => {
scheduler.setCps(scheduler.cps + 0.1);
}}
>
faster
</button>
</div>
</FormItem> */}
{AudioContext.prototype.setSinkId != null && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@ -163,6 +146,11 @@ export function SettingsTab() {
onChange={(cbEvent) => settingsMap.setKey('isActiveLineHighlighted', cbEvent.target.checked)}
value={isActiveLineHighlighted}
/>
<Checkbox
label="Highlight events in code"
onChange={(cbEvent) => settingsMap.setKey('isPatternHighlightingEnabled', cbEvent.target.checked)}
value={isPatternHighlightingEnabled}
/>
<Checkbox
label="Enable auto-completion"
onChange={(cbEvent) => settingsMap.setKey('isAutoCompletionEnabled', cbEvent.target.checked)}
@ -178,6 +166,11 @@ export function SettingsTab() {
onChange={(cbEvent) => settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)}
value={isLineWrappingEnabled}
/>
<Checkbox
label="Enable flashing on evaluation"
onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
value={isFlashEnabled}
/>
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings">

View file

@ -1,5 +1,4 @@
import { useEvent } from '@strudel.cycles/react';
// import { cx } from '@strudel.cycles/react';
import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel.cycles/webaudio';
import React, { useMemo, useRef } from 'react';

View file

@ -1,6 +1,4 @@
import { cx } from '@strudel.cycles/react';
import React from 'react';
import * as tunes from '../tunes.mjs';
import cx from '@src/cx.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;

View file

@ -1,5 +1,6 @@
import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio';
import { registerSamplesFromDB } from './idbutils.mjs';
import './piano.mjs';
import './files.mjs';
@ -12,6 +13,7 @@ export async function prebake() {
await Promise.all([
registerSynthSounds(),
registerZZFXSounds(),
registerSamplesFromDB(),
//registerSoundfonts(),
// need dynamic import here, because importing @strudel.cycles/soundfonts fails on server:
// => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically

View file

@ -1,482 +0,0 @@
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 '@strudel.cycles/react/src/themes/strudel-theme';
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
import teletext, { settings as teletextSettings } from '@strudel.cycles/react/src/themes/teletext';
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/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 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);
}

112
website/src/repl/util.mjs Normal file
View file

@ -0,0 +1,112 @@
import { controls, evalScope, hash2code, logger } from '@strudel.cycles/core';
import { settingPatterns } from '../settings.mjs';
import { isTauri } from '../tauri.mjs';
import './Repl.css';
import * as tunes from './tunes.mjs';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
import { writeText } from '@tauri-apps/api/clipboard';
// Create a single supabase client for interacting with your database
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
export async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
.select('code')
.eq('hash', hash)
.then(({ data, error }) => {
if (error) {
console.warn('failed to load hash', error);
}
if (data.length) {
//console.log('load hash from database', hash);
return data[0].code;
}
});
}
} catch (err) {
console.warn('failed to decode', err);
}
}
export function getRandomTune() {
const allTunes = Object.entries(tunes);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const [name, code] = randomItem(allTunes);
return { name, code };
}
export function loadModules() {
let modules = [
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'),
];
if (isTauri()) {
modules = modules.concat([
import('@strudel/desktopbridge/loggerbridge.mjs'),
import('@strudel/desktopbridge/midibridge.mjs'),
import('@strudel/desktopbridge/oscbridge.mjs'),
]);
} else {
modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
}
return evalScope(
controls, // sadly, this cannot be exported from core direclty
settingPatterns,
...modules,
);
}
let lastShared;
export async function shareCode(codeToShare) {
// const codeToShare = activeCode || code;
if (lastShared === codeToShare) {
logger(`Link already generated!`, 'error');
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
lastShared = codeToShare;
// copy shareUrl to clipboard
if (isTauri()) {
await writeText(shareUrl);
} else {
await navigator.clipboard.writeText(shareUrl);
}
const message = `Link copied to clipboard: ${shareUrl}`;
alert(message);
// alert(message);
logger(message, 'highlight');
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
logger(message);
}
}

View file

@ -1,34 +0,0 @@
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

@ -1,202 +0,0 @@
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

@ -14,14 +14,16 @@ export const defaultSettings = {
isActiveLineHighlighted: true,
isAutoCompletionEnabled: false,
isTooltipEnabled: false,
isFlashEnabled: true,
isLineWrappingEnabled: false,
isPatternHighlightingEnabled: true,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
latestCode: '',
isZen: false,
soundsFilter: 'all',
panelPosition: 'bottom',
panelPosition: 'right',
userPatterns: '{}',
activePattern: '',
audioDeviceName: defaultAudioDeviceName,
@ -55,10 +57,12 @@ export function useSettings() {
isLineNumbersDisplayed: [true, 'true'].includes(state.isLineNumbersDisplayed) ? true : false,
isActiveLineHighlighted: [true, 'true'].includes(state.isActiveLineHighlighted) ? true : false,
isAutoCompletionEnabled: [true, 'true'].includes(state.isAutoCompletionEnabled) ? true : false,
isPatternHighlightingEnabled: [true, 'true'].includes(state.isPatternHighlightingEnabled) ? true : false,
isTooltipEnabled: [true, 'true'].includes(state.isTooltipEnabled) ? true : false,
isLineWrappingEnabled: [true, 'true'].includes(state.isLineWrappingEnabled) ? true : false,
isFlashEnabled: [true, 'true'].includes(state.isFlashEnabled) ? true : false,
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom',
panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
userPatterns: JSON.parse(state.userPatterns),
};
}

View file

@ -0,0 +1,9 @@
import { useEffect, useState } from 'react';
export default function useClient() {
const [client, setClient] = useState(false);
useEffect(() => {
setClient(true);
}, []);
return client;
}

12
website/src/useEvent.mjs Normal file
View file

@ -0,0 +1,12 @@
import { useEffect } from 'react';
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
export default useEvent;

43
website/src/useFrame.mjs Normal file
View file

@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
function useFrame(callback, autostart = false) {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(time, deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
const start = () => {
requestRef.current = requestAnimationFrame(animate);
};
const stop = () => {
requestRef.current && cancelAnimationFrame(requestRef.current);
delete requestRef.current;
};
useEffect(() => {
if (requestRef.current) {
stop();
start();
}
}, [callback]);
useEffect(() => {
if (autostart) {
start();
}
return stop;
}, []); // Make sure the effect only runs once
return {
start,
stop,
};
}
export default useFrame;