Merge branch 'main' into audio_device_selection
This commit is contained in:
commit
3152bc123a
140 changed files with 1186 additions and 6426 deletions
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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
10
website/src/cx.mjs
Normal 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
38
website/src/docs/Icon.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { cx } from '@strudel.cycles/react';
|
||||
import React from 'react';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
function Loader({ active }) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
112
website/src/repl/util.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
9
website/src/useClient.mjs
Normal file
9
website/src/useClient.mjs
Normal 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
12
website/src/useEvent.mjs
Normal 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
43
website/src/useFrame.mjs
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue