mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
Merge pull request #873 from tidalcycles/vanilla-repl-3
main repl vanillification
This commit is contained in:
commit
edfa0c65f9
@ -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 (
|
||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
||||
<ul>
|
||||
{doc.params?.map(({ name, type, description }, i) => (
|
||||
<li key={i}>
|
||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{doc.examples?.map((example, i) => (
|
||||
<div key={i}>
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export function Autocomplete({ doc, label }) {
|
||||
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||
${doc.description}
|
||||
<ul>
|
||||
${doc.params?.map(
|
||||
({ name, type, description }) =>
|
||||
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
${doc.examples?.map((example) => `<div><pre>${example}</pre></div>`)}
|
||||
</div>
|
||||
</div>`[0];
|
||||
/*
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
*/
|
||||
}
|
||||
|
||||
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(<Autocomplete doc={doc} />);
|
||||
return node;
|
||||
},
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
|
||||
@ -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) {
|
||||
|
||||
17
packages/codemirror/html.mjs
Normal file
17
packages/codemirror/html.mjs
Normal file
@ -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);
|
||||
};
|
||||
@ -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"
|
||||
},
|
||||
|
||||
76
packages/codemirror/tooltip.mjs
Normal file
76
packages/codemirror/tooltip.mjs
Normal file
@ -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 : []);
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
58
website/src/components/HeadCommonNext.astro
Normal file
58
website/src/components/HeadCommonNext.astro
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import '../styles/index.css';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href={`${baseNoTrailing}/favicon.ico`} />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript."
|
||||
/>
|
||||
<link rel="icon" href={`${baseNoTrailing}/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" href={`${baseNoTrailing}/icons/apple-icon-180.png`} sizes="180x180" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
<!-- this does not work for some reason: -->
|
||||
<!-- <style is:global define:vars={strudelTheme}></style> -->
|
||||
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
|
||||
<style is:global>
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222299;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
</style>
|
||||
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
|
||||
|
||||
<script>
|
||||
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
|
||||
const appHeight = () => {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight - 1}px`);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', appHeight);
|
||||
appHeight();
|
||||
}
|
||||
</script>
|
||||
14
website/src/pages/next/index.astro
Normal file
14
website/src/pages/next/index.astro
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
import HeadCommonNext from '../../components/HeadCommonNext.astro';
|
||||
import { Repl2 } from '../../repl/Repl2';
|
||||
---
|
||||
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<HeadCommonNext />
|
||||
<title>Strudel REPL</title>
|
||||
</head>
|
||||
<body class="h-app-height bg-background">
|
||||
<Repl2 client:only="react" />
|
||||
</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)
|
||||
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<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>
|
||||
<strudel-editor id="editor"></strudel-editor>
|
||||
<script src="../../repl/vanilla/vanilla.mjs"></script>
|
||||
<script src="@strudel/repl"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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}
|
||||
>
|
||||
<LinkIcon className="w-6 h-6" />
|
||||
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
|
||||
@ -4,83 +4,33 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
cleanupDraw,
|
||||
cleanupUi,
|
||||
controls,
|
||||
evalScope,
|
||||
getDrawContext,
|
||||
logger,
|
||||
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,
|
||||
|
||||
234
website/src/repl/Repl2.jsx
Normal file
234
website/src/repl/Repl2.jsx
Normal file
@ -0,0 +1,234 @@
|
||||
/*
|
||||
App.js - <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 '@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
|
||||
<ReplContext.Provider value={context}>
|
||||
<div
|
||||
className={cx(
|
||||
'h-full flex flex-col relative',
|
||||
// overflow-hidden
|
||||
)}
|
||||
>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
{/* isEmbedded && !started && (
|
||||
<button
|
||||
onClick={() => handleTogglePlay()}
|
||||
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircleIcon className="w-6 h-6" />
|
||||
<span>play</span>
|
||||
</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"
|
||||
ref={containerRef}
|
||||
></section>
|
||||
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>
|
||||
)}
|
||||
{panelPosition === 'bottom' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
<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)}
|
||||
|
||||
@ -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
|
||||
|
||||
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,
|
||||
#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;
|
||||
}
|
||||
@ -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("<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, codemirrorSettings.get());
|
||||
form.addEventListener('change', () => {
|
||||
const values = getFormValues(form, defaultSettings);
|
||||
editor?.updateSettings(values);
|
||||
});
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user