diff --git a/packages/codemirror/Autocomplete.jsx b/packages/codemirror/autocomplete.mjs
similarity index 57%
rename from packages/codemirror/Autocomplete.jsx
rename to packages/codemirror/autocomplete.mjs
index 18f172ee..c065c365 100644
--- a/packages/codemirror/Autocomplete.jsx
+++ b/packages/codemirror/autocomplete.mjs
@@ -1,7 +1,7 @@
-import { createRoot } from 'react-dom/client';
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
+import { h } from './html';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
@@ -10,36 +10,32 @@ const getInnerText = (html) => {
return div.textContent || div.innerText || '';
};
-export function Autocomplete({ doc }) {
- return (
-
-
{getDocLabel(doc)}
-
-
- {doc.params?.map(({ name, type, description }, i) => (
- -
- {name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}> : ''}
-
- ))}
-
-
- {doc.examples?.map((example, i) => (
-
-
{
- console.log('ola!');
- navigator.clipboard.writeText(example);
- e.stopPropagation();
- }}
- >
- {example}
-
-
- ))}
-
-
- );
+export function Autocomplete({ doc, label }) {
+ return h`
+
${label || getDocLabel(doc)}
+${doc.description}
+
+ ${doc.params?.map(
+ ({ name, type, description }) =>
+ `- ${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}
`,
+ )}
+
+
+ ${doc.examples?.map((example) => `
`)}
+
+
`[0];
+ /*
+ {
+ console.log('ola!');
+ navigator.clipboard.writeText(example);
+ e.stopPropagation();
+}}
+>
+{example}
+
+*/
}
const jsdocCompletions = jsdoc.docs
@@ -54,13 +50,7 @@ const jsdocCompletions = jsdoc.docs
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
- info: () => {
- const node = document.createElement('div');
- // if Autocomplete is non-interactive, it could also be rendered at build time..
- // .. using renderToStaticMarkup
- createRoot(node).render();
- return node;
- },
+ info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 6ad94209..29dca867 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -6,7 +6,8 @@ import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { Compartment, EditorState, Prec } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
-// import { isAutoCompletionEnabled } from './Autocomplete';
+import { isAutoCompletionEnabled } from './autocomplete.mjs';
+import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
@@ -18,7 +19,8 @@ const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
- // isAutoCompletionEnabled,
+ isAutoCompletionEnabled,
+ isTooltipEnabled,
isPatternHighlightingEnabled,
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
@@ -108,7 +110,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
export class StrudelMirror {
constructor(options) {
- const { root, id, initialCode = '', onDraw, drawTime = [-2, 2], autodraw, prebake, ...replOptions } = options;
+ const {
+ root,
+ id,
+ initialCode = '',
+ onDraw,
+ drawTime = [0, 0],
+ autodraw,
+ prebake,
+ bgFill = true,
+ ...replOptions
+ } = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
@@ -183,9 +195,15 @@ export class StrudelMirror {
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.display = 'block';
- this.root.style.backgroundColor = 'var(--background)';
+ if (bgFill) {
+ this.root.style.backgroundColor = 'var(--background)';
+ }
cmEditor.style.backgroundColor = 'transparent';
}
+ const settings = codemirrorSettings.get();
+ this.setFontSize(settings.fontSize);
+ this.setFontFamily(settings.fontFamily);
+
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (e.detail !== this.id) {
diff --git a/packages/codemirror/html.mjs b/packages/codemirror/html.mjs
new file mode 100644
index 00000000..4b4d82e4
--- /dev/null
+++ b/packages/codemirror/html.mjs
@@ -0,0 +1,17 @@
+const parser = new DOMParser();
+export let html = (string) => {
+ return parser.parseFromString(string, 'text/html').querySelectorAll('*');
+};
+let parseChunk = (chunk) => {
+ if (Array.isArray(chunk)) return chunk.flat().join('');
+ if (chunk === undefined) return '';
+ return chunk;
+};
+export let h = (strings, ...vars) => {
+ let string = '';
+ for (let i in strings) {
+ string += parseChunk(strings[i]);
+ string += parseChunk(vars[i]);
+ }
+ return html(string);
+};
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index 0c57db8b..8810a607 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -47,7 +47,6 @@
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
- "react-dom": "^18.2.0",
"nanostores": "^0.8.1",
"@nanostores/persistent": "^0.8.0"
},
diff --git a/packages/codemirror/tooltip.mjs b/packages/codemirror/tooltip.mjs
new file mode 100644
index 00000000..7b7a36db
--- /dev/null
+++ b/packages/codemirror/tooltip.mjs
@@ -0,0 +1,76 @@
+import { hoverTooltip } from '@codemirror/view';
+import jsdoc from '../../doc.json';
+import { Autocomplete } from './autocomplete.mjs';
+
+const getDocLabel = (doc) => doc.name || doc.longname;
+
+let ctrlDown = false;
+
+// Record Control key event to trigger or block the tooltip depending on the state
+window.addEventListener(
+ 'keyup',
+ function (e) {
+ if (e.key == 'Control') {
+ ctrlDown = false;
+ }
+ },
+ true,
+);
+
+window.addEventListener(
+ 'keydown',
+ function (e) {
+ if (e.key == 'Control') {
+ ctrlDown = true;
+ }
+ },
+ true,
+);
+
+export const strudelTooltip = hoverTooltip(
+ (view, pos, side) => {
+ // Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
+ if (!ctrlDown) {
+ return null;
+ }
+ let { from, to, text } = view.state.doc.lineAt(pos);
+ let start = pos,
+ end = pos;
+ while (start > from && /\w/.test(text[start - from - 1])) {
+ start--;
+ }
+ while (end < to && /\w/.test(text[end - from])) {
+ end++;
+ }
+ if ((start == pos && side < 0) || (end == pos && side > 0)) {
+ return null;
+ }
+ let word = text.slice(start - from, end - from);
+ // Get entry from Strudel documentation
+ let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
+ if (!entry) {
+ // Try for synonyms
+ entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
+ if (!entry) {
+ return null;
+ }
+ }
+
+ return {
+ pos: start,
+ end,
+ above: false,
+ arrow: true,
+ create(view) {
+ let dom = document.createElement('div');
+ dom.className = 'strudel-tooltip';
+ const ac = Autocomplete({ doc: entry, label: word });
+ dom.appendChild(ac);
+ return { dom };
+ },
+ };
+ },
+ { hoverTime: 10 },
+);
+
+export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6fecabff..5892af73 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -123,9 +123,6 @@ importers:
nanostores:
specifier: ^0.8.1
version: 0.8.1
- react-dom:
- specifier: ^18.2.0
- version: 18.2.0(react@18.2.0)
devDependencies:
vite:
specifier: ^4.3.3
diff --git a/website/src/components/HeadCommonNext.astro b/website/src/components/HeadCommonNext.astro
new file mode 100644
index 00000000..9f323a7a
--- /dev/null
+++ b/website/src/components/HeadCommonNext.astro
@@ -0,0 +1,58 @@
+---
+import { pwaInfo } from 'virtual:pwa-info';
+import '../styles/index.css';
+
+const { BASE_URL } = import.meta.env;
+const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{pwaInfo && }
+
+
diff --git a/website/src/pages/next/index.astro b/website/src/pages/next/index.astro
new file mode 100644
index 00000000..0db75a1d
--- /dev/null
+++ b/website/src/pages/next/index.astro
@@ -0,0 +1,14 @@
+---
+import HeadCommonNext from '../../components/HeadCommonNext.astro';
+import { Repl2 } from '../../repl/Repl2';
+---
+
+
+
+
+ Strudel REPL
+
+
+
+
+
diff --git a/website/src/pages/technical-manual/docs.mdx b/website/src/pages/technical-manual/docs.mdx
index aa18eab4..386de6ad 100644
--- a/website/src/pages/technical-manual/docs.mdx
+++ b/website/src/pages/technical-manual/docs.mdx
@@ -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)
diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro
deleted file mode 100644
index d4ea40b8..00000000
--- a/website/src/pages/vanilla/index.astro
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
- Strudel Vanilla REPL
-
-
-
-
-
-
-
-
-
-
diff --git a/website/src/repl/Header.jsx b/website/src/repl/Header.jsx
index b6f13060..2592b917 100644
--- a/website/src/repl/Header.jsx
+++ b/website/src/repl/Header.jsx
@@ -18,7 +18,6 @@ export function Header({ context }) {
started,
pending,
isDirty,
- lastShared,
activeCode,
handleTogglePlay,
handleUpdate,
@@ -119,7 +118,7 @@ export function Header({ context }) {
onClick={handleShare}
>
- share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
+ share
)}
{!isEmbedded && (
diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx
index d4f85434..65cc3473 100644
--- a/website/src/repl/Repl.jsx
+++ b/website/src/repl/Repl.jsx
@@ -4,83 +4,33 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-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 PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
+import { cleanupDraw, cleanupUi, code2hash, getDrawContext, logger } from '@strudel.cycles/core';
+import { CodeMirror, cx, flash, useHighlighting, useKeydown, useStrudel } from '@strudel.cycles/react';
+import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
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';
+import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ 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 { prebake } from './prebake.mjs';
-import * as tunes from './tunes.mjs';
-import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { themes } from './themes.mjs';
-import {
- settingsMap,
- useSettings,
- setLatestCode,
- updateUserCode,
- 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 { registerSamplesFromDB, userSamplesDBConfig } from './idbutils.mjs';
+import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
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 modulesLoading = loadModules();
const presets = prebake();
let drawContext, clearCanvas;
@@ -91,43 +41,6 @@ if (typeof window !== 'undefined') {
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);
@@ -135,7 +48,6 @@ 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,
@@ -204,7 +116,6 @@ export function Repl({ embedded = false }) {
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);
});
@@ -289,42 +200,13 @@ export function Repl({ embedded = false }) {
await evaluate(code, false);
};
- 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 || code);
const context = {
scheduler,
embedded,
started,
pending,
isDirty,
- lastShared,
activeCode,
handleChangeCode,
handleTogglePlay,
diff --git a/website/src/repl/Repl2.jsx b/website/src/repl/Repl2.jsx
new file mode 100644
index 00000000..641d2a91
--- /dev/null
+++ b/website/src/repl/Repl2.jsx
@@ -0,0 +1,234 @@
+/*
+App.js -
+Copyright (C) 2022 Strudel contributors - see
+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 .
+*/
+
+import { code2hash, getDrawContext, logger, silence } from '@strudel.cycles/core';
+import { cx } from '@strudel.cycles/react';
+import { transpiler } from '@strudel.cycles/transpiler';
+import { getAudioContext, initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
+import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
+/* import { writeText } from '@tauri-apps/api/clipboard';
+import { nanoid } from 'nanoid'; */
+import { createContext, useCallback, useEffect, useRef, useState } from 'react';
+import {
+ 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 { prebake } from '@strudel/repl';
+import { useStore } from '@nanostores/react';
+import { prebake /* , resetSounds */ } from './prebake.mjs';
+import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
+import './Repl.css';
+
+const { code: randomTune, name } = getRandomTune();
+export const ReplContext = createContext(null);
+
+const { latestCode } = settingsMap.get();
+
+initAudioOnFirstClick();
+
+const modulesLoading = loadModules();
+const presets = prebake();
+
+let drawContext, clearCanvas;
+if (typeof window !== 'undefined') {
+ drawContext = getDrawContext();
+ clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
+}
+
+// const getTime = () => getAudioContext().currentTime;
+
+export function Repl2({ embedded = false }) {
+ //const isEmbedded = embedded || window.location !== window.parent.location;
+ const isEmbedded = false;
+ const { panelPosition, isZen } = useSettings();
+ /* const replState = useStore($replstate);
+ const isDirty = useStore($repldirty); */
+ const shouldDraw = true;
+
+ const init = useCallback(({ shouldDraw }) => {
+ // TODO: find way to make spiral & punchcard work (if there's any)
+ // upping the 2nd value leads to slow eval times
+ // because Drawer.invalidate might query alot at one time
+ const drawTime = [0, 0];
+ const drawContext = shouldDraw ? getDrawContext() : 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({
+ defaultOutput: webaudioOutput,
+ 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 }) => {
+ updateUserCode(code);
+ // setPending(false);
+ setLatestCode(code);
+ window.location.hash = '#' + code2hash(code);
+ },
+ bgFill: false,
+ });
+ // init settings
+ initCode().then((decoded) => {
+ let msg;
+ if (decoded) {
+ editor.setCode(decoded);
+ initUserCode(decoded);
+ msg = `I have loaded the code from the URL.`;
+ } else if (latestCode) {
+ editor.setCode(latestCode);
+ msg = `Your last session has been loaded!`;
+ } /* if(randomTune) */ else {
+ editor.setCode(randomTune);
+ msg = `A random code snippet named "${name}" has been loaded!`;
+ }
+ logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
+ // setPending(false);
+ });
+
+ editorRef.current = editor;
+ }, []);
+
+ const [replState, setReplState] = useState({});
+ const { started, isDirty, error, activeCode } = replState;
+ const editorRef = useRef();
+ const containerRef = useRef();
+ const [client, setClient] = useState(false);
+ useEffect(() => {
+ setClient(true);
+ if (!editorRef.current) {
+ setTimeout(() => {
+ init({ shouldDraw });
+ });
+ }
+ return () => {
+ editorRef.current?.clear();
+ delete editorRef.current;
+ };
+ }, []);
+
+ // 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 handleTogglePlay = async () => editorRef.current?.toggle();
+ const handleUpdate = async (newCode, reset = false) => {
+ if (reset) {
+ clearCanvas();
+ resetLoadedSounds();
+ editorRef.current.repl.setCps(1);
+ await prebake(); // declare default samples
+ }
+ 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();
+ editorRef.current.repl.setCps(1);
+ await prebake(); // declare default samples
+ editorRef.current.setCode(code);
+ editorRef.current.repl.evaluate(code);
+ };
+
+ const handleShare = async () => shareCode(activeCode);
+ const pending = false;
+ //const error = undefined;
+ // const { started, activeCode } = replState;
+
+ const context = {
+ // scheduler,
+ embedded,
+ started,
+ pending,
+ isDirty,
+ activeCode,
+ handleTogglePlay,
+ handleUpdate,
+ handleShuffle,
+ handleShare,
+ };
+
+ return (
+ // bg-gradient-to-t from-blue-900 to-slate-900
+ // bg-gradient-to-t from-green-900 to-slate-900
+
+
+
+
+ {/* isEmbedded && !started && (
+
+ ) */}
+
+
+ {panelPosition === 'right' && !isEmbedded &&
}
+
+ {error && (
+
{error.message || 'Unknown Error :-/'}
+ )}
+ {panelPosition === 'bottom' && !isEmbedded &&
}
+
+
+ );
+}
diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs
index 414006b9..18e15d20 100644
--- a/website/src/repl/idbutils.mjs
+++ b/website/src/repl/idbutils.mjs
@@ -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) => {
diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx
index cee5b286..3efab8f2 100644
--- a/website/src/repl/panel/SettingsTab.jsx
+++ b/website/src/repl/panel/SettingsTab.jsx
@@ -78,6 +78,7 @@ export function SettingsTab() {
theme,
keybindings,
isLineNumbersDisplayed,
+ isPatternHighlightingEnabled,
isActiveLineHighlighted,
isAutoCompletionEnabled,
isTooltipEnabled,
@@ -153,6 +154,11 @@ export function SettingsTab() {
onChange={(cbEvent) => settingsMap.setKey('isActiveLineHighlighted', cbEvent.target.checked)}
value={isActiveLineHighlighted}
/>
+ settingsMap.setKey('isPatternHighlightingEnabled', cbEvent.target.checked)}
+ value={isPatternHighlightingEnabled}
+ />
settingsMap.setKey('isAutoCompletionEnabled', cbEvent.target.checked)}
diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs
index 68f6f8a1..96a484e7 100644
--- a/website/src/repl/prebake.mjs
+++ b/website/src/repl/prebake.mjs
@@ -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
diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs
new file mode 100644
index 00000000..2b93b619
--- /dev/null
+++ b/website/src/repl/util.mjs
@@ -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);
+ }
+}
diff --git a/website/src/repl/vanilla/vanilla.css b/website/src/repl/vanilla/vanilla.css
deleted file mode 100644
index 5387fb04..00000000
--- a/website/src/repl/vanilla/vanilla.css
+++ /dev/null
@@ -1,34 +0,0 @@
-body,
-input {
- font-family: monospace;
- background-color: black;
- color: white;
-}
-
-input,
-select {
- background-color: black !important;
-}
-
-html,
-body,
-#editor,
-.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;
-}
diff --git a/website/src/repl/vanilla/vanilla.mjs b/website/src/repl/vanilla/vanilla.mjs
deleted file mode 100644
index 6f0cc058..00000000
--- a/website/src/repl/vanilla/vanilla.mjs
+++ /dev/null
@@ -1,117 +0,0 @@
-import { hash2code, logger } from '@strudel.cycles/core';
-import { codemirrorSettings, defaultSettings } from '@strudel/codemirror';
-import './vanilla.css';
-
-let editor;
-
-async function run() {
- const repl = document.getElementById('editor');
- editor = repl.editor;
- 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(",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("/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, codemirrorSettings.get());
-form.addEventListener('change', () => {
- const values = getFormValues(form, defaultSettings);
- editor?.updateSettings(values);
-});
diff --git a/website/src/settings.mjs b/website/src/settings.mjs
index 570b6446..489bd71c 100644
--- a/website/src/settings.mjs
+++ b/website/src/settings.mjs
@@ -12,6 +12,7 @@ export const defaultSettings = {
isAutoCompletionEnabled: false,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
+ isPatternHighlightingEnabled: true,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -50,6 +51,7 @@ 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,
fontSize: Number(state.fontSize),