From 1494cc38fce198d74cdd5b07aaecbad87bac3044 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 08:55:43 +0200 Subject: [PATCH 01/36] add vite-vanilla-repl-cm6 --- .../examples/vite-vanilla-repl-cm6/.gitignore | 24 ++++++ .../examples/vite-vanilla-repl-cm6/README.md | 8 ++ .../vite-vanilla-repl-cm6/codemirror.js | 83 +++++++++++++++++++ .../vite-vanilla-repl-cm6/highlighter.js | 44 ++++++++++ .../examples/vite-vanilla-repl-cm6/index.html | 21 +++++ .../examples/vite-vanilla-repl-cm6/main.js | 32 +++++++ .../vite-vanilla-repl-cm6/package.json | 27 ++++++ .../examples/vite-vanilla-repl-cm6/strudel.js | 38 +++++++++ .../examples/vite-vanilla-repl-cm6/style.css | 24 ++++++ .../examples/vite-vanilla-repl-cm6/tunes.mjs | 32 +++++++ .../core/examples/vite-vanilla-repl/README.md | 2 - pnpm-lock.yaml | 66 ++++++++++++--- pnpm-workspace.yaml | 1 + 13 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/.gitignore create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/README.md create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/codemirror.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/highlighter.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/index.html create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/main.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/package.json create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/strudel.js create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/style.css create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs diff --git a/packages/core/examples/vite-vanilla-repl-cm6/.gitignore b/packages/core/examples/vite-vanilla-repl-cm6/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/core/examples/vite-vanilla-repl-cm6/README.md b/packages/core/examples/vite-vanilla-repl-cm6/README.md new file mode 100644 index 00000000..70d18e09 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/README.md @@ -0,0 +1,8 @@ +# vite-vanilla-repl-cm6 + +This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using: + +```sh +npm i +npm run dev +``` diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js new file mode 100644 index 00000000..7ba377ab --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -0,0 +1,83 @@ +import { EditorState } from '@codemirror/state'; +import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view'; +import { defaultKeymap } from '@codemirror/commands'; +import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; +import { javascript } from '@codemirror/lang-javascript'; +import { StateField, StateEffect } from '@codemirror/state'; +import './style.css'; + +// https://codemirror.net/docs/guide/ +export function initEditor(initialCode, onUpdate) { + let state = EditorState.create({ + doc: initialCode, + extensions: [ + javascript(), + lineNumbers(), + /*gutter({ + class: "cm-mygutter" + }),*/ + highlightField, + highlightActiveLineGutter(), + //markLineGutter, + syntaxHighlighting(defaultHighlightStyle), + keymap.of(defaultKeymap), + //flashField, + EditorView.updateListener.of((v) => { + onUpdate(v); + }), + ], + }); + + return new EditorView({ + state, + parent: document.getElementById('editor'), + }); +} + +// codemirror specific highlighting logic + +export const setHighlights = StateEffect.define(); +export const highlightField = StateField.define({ + create() { + return Decoration.none; + }, + update(highlights, tr) { + try { + for (let e of tr.effects) { + if (e.is(setHighlights)) { + const { haps } = e.value; + const marks = + haps + .map((hap) => + (hap.context.locations || []).map(({ start, end }) => { + // const color = hap.context.color || e.value.color || '#FFCA28'; + let from = tr.newDoc.line(start.line).from + start.column; + let to = tr.newDoc.line(end.line).from + end.column; + const l = tr.newDoc.length; + if (from > l || to > l) { + return; // dont mark outside of range, as it will throw an error + } + const mark = Decoration.mark({ + attributes: { style: `outline: 2px solid #FFCA28;` }, + }); + return mark.range(from, to); + }), + ) + .flat() + .filter(Boolean) || []; + highlights = Decoration.set(marks, true); + } + } + return highlights; + } catch (err) { + // console.warn('highlighting error', err); + return Decoration.set([]); + } + }, + provide: (f) => EditorView.decorations.from(f), +}); + +// helper to simply trigger highlighting for given haps +export function highlightHaps(view, haps) { + view.dispatch({ effects: setHighlights.of({ haps }) }); +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js new file mode 100644 index 00000000..0a306e70 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js @@ -0,0 +1,44 @@ +const round = (x) => Math.round(x * 1000) / 1000; + +// this class can be used to create a code highlighter +// it is encapsulated from the editor via the onUpdate callback +// the scheduler is expected to be an instance of Cyclist +export class Highlighter { + constructor(onUpdate) { + this.onUpdate = onUpdate; + } + start(scheduler) { + let highlights = []; + let lastEnd = 0; + this.stop(); + const self = this; + let frame = requestAnimationFrame(function updateHighlights() { + try { + const time = scheduler.now(); + // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away + // see https://github.com/tidalcycles/strudel/issues/108 + const begin = Math.max(lastEnd ?? time, time - 1 / 10, -0.01); // negative time seems buggy + const span = [round(begin), round(time + 1 / 60)]; + lastEnd = span[1]; + highlights = highlights.filter((hap) => hap.whole.end > time); // keep only highlights that are still active + const haps = scheduler.pattern + .queryArc(...span) + .filter((hap) => hap.hasOnset()); + highlights = highlights.concat(haps); // add potential new onsets + self.onUpdate(highlights); // highlight all still active + new active haps + } catch (err) { + self.onUpdate([]); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + this.onUpdate([]); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/index.html b/packages/core/examples/vite-vanilla-repl-cm6/index.html new file mode 100644 index 00000000..32381f03 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/index.html @@ -0,0 +1,21 @@ + + + + + + Vite Vanilla Strudel REPL + + +
+ +
+
+
+
+
+ + + diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js new file mode 100644 index 00000000..814f3609 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -0,0 +1,32 @@ +// moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 + +import { initEditor, highlightHaps } from './codemirror'; +import { initStrudel } from './strudel'; +import { Highlighter } from './highlighter'; +import { bumpStreet } from './tunes'; +let code = bumpStreet; + +const view = initEditor(code, (v) => { + code = v.state.doc.toString(); +}); +const repl = initStrudel(); + +let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); + +document.getElementById('play').addEventListener('click', async () => { + const { evaluate, scheduler } = await repl; + if (!scheduler.started) { + scheduler.stop(); + scheduler.lastEnd = 0; + await evaluate(code); + highlighter.start(scheduler); + } else { + await evaluate(code); + } +}); + +document.getElementById('stop').addEventListener('click', async () => { + const { stop } = await repl; + stop(); + highlighter.stop(); +}); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json new file mode 100644 index 00000000..f84b23ed --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -0,0 +1,27 @@ +{ + "name": "vite-vanilla-repl-cm6", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^4.3.2" + }, + "dependencies": { + "@codemirror/commands": "^6.2.4", + "@codemirror/lang-javascript": "^6.1.7", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.10.0", + "@strudel.cycles/core": "workspace:*", + "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/tonal": "workspace:*", + "@strudel.cycles/transpiler": "workspace:*", + "@strudel.cycles/webaudio": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*" + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js new file mode 100644 index 00000000..a1e999ee --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -0,0 +1,38 @@ +import { controls, repl, evalScope } from "@strudel.cycles/core"; +import { transpiler } from "@strudel.cycles/transpiler"; +import { + getAudioContext, + webaudioOutput, + initAudioOnFirstClick, + registerSynthSounds +} from "@strudel.cycles/webaudio"; +import { registerSoundfonts } from "@strudel.cycles/soundfonts"; + +const initAudio = initAudioOnFirstClick(); +const ctx = getAudioContext(); + +const loadModules = (scope = {}) => + evalScope( + controls, + import("@strudel.cycles/core"), + import("@strudel.cycles/mini"), + import("@strudel.cycles/tonal"), + import("@strudel.cycles/webaudio"), + scope + ); + +export async function initStrudel(options = {}) { + await Promise.all([ + initAudio, + loadModules(), + registerSynthSounds(), + registerSoundfonts() + ]); + + return repl({ + defaultOutput: webaudioOutput, + getTime: () => ctx.currentTime, + transpiler, + ...options + }); +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css new file mode 100644 index 00000000..20970778 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -0,0 +1,24 @@ +body, +html { + margin: 0; + height: 100%; +} + +#editor { + overflow: auto; + height: 100%; +} + +.cm-editor { + height: 100%; +} + +main { + height: 100%; + display: flex; + flex-direction: column; +} + +.container { + flex-grow: 1; +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs new file mode 100644 index 00000000..4b0155e4 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -0,0 +1,32 @@ +export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0 +await samples('github:felixroos/samples/main') +await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') + +"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2" + // make it 22 edo + .fmap(v => Math.pow(2,v/22)) + // mess with the base frequency + .mul("<300 [300@3 200]>/8").freq() + .layer( + // chords + x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8) + .shape(.4).lpf(sine.range(800,4000).slow(8)), + // adlibs + x=>x.arp("{0 3 2 [1 3]}%1.5") + .s('xylo').mul(freq(2)) + .delay(.5).delayfeedback(.4).juxBy(.5, rev) + .hpf(sine.range(200,3000).slow(8)), + // bass + x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4)) + .lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4) + .off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3) + ).clip(1).release(.2) +.stack( + // drums + s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909') + .off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8) + .mask("<0@4 1@12>/4") + .reset("") + // wait for it... +).fast(2/3) + //.crush(6) // remove "//" if you dare`; diff --git a/packages/core/examples/vite-vanilla-repl/README.md b/packages/core/examples/vite-vanilla-repl/README.md index ac2ed20f..d7f65720 100644 --- a/packages/core/examples/vite-vanilla-repl/README.md +++ b/packages/core/examples/vite-vanilla-repl/README.md @@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS. npm i npm run dev ``` - -or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4df850b..40a6442f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,46 @@ importers: specifier: ^4.3.3 version: 4.3.3(@types/node@18.16.3) + packages/core/examples/vite-vanilla-repl-cm6: + dependencies: + '@codemirror/commands': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-javascript': + specifier: ^6.1.7 + version: 6.1.7 + '@codemirror/language': + specifier: ^6.6.0 + version: 6.6.0 + '@codemirror/state': + specifier: ^6.2.0 + version: 6.2.0 + '@codemirror/view': + specifier: ^6.10.0 + version: 6.10.0 + '@strudel.cycles/core': + specifier: workspace:* + version: link:../.. + '@strudel.cycles/mini': + specifier: workspace:* + version: link:../../../mini + '@strudel.cycles/soundfonts': + specifier: workspace:* + version: link:../../../soundfonts + '@strudel.cycles/tonal': + specifier: workspace:* + version: link:../../../tonal + '@strudel.cycles/transpiler': + specifier: workspace:* + version: link:../../../transpiler + '@strudel.cycles/webaudio': + specifier: workspace:* + version: link:../../../webaudio + devDependencies: + vite: + specifier: ^4.3.2 + version: 4.3.3(@types/node@18.16.3) + packages/csound: dependencies: '@csound/browser': @@ -220,10 +260,10 @@ importers: version: 1.1.4 '@replit/codemirror-emacs': specifier: ^6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) '@replit/codemirror-vim': specifier: ^6.0.14 - version: 6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + version: 6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) '@strudel.cycles/core': specifier: workspace:* version: link:../core @@ -2268,8 +2308,8 @@ packages: '@lezer/common': 1.0.2 dev: false - /@codemirror/commands@6.2.0: - resolution: {integrity: sha512-+00smmZBradoGFEkRjliN7BjqPh/Hx0KCHWOEibUmflUqZz2RwBTU0MrVovEEHozhx3AUSGcO/rl3/5f9e9Biw==} + /@codemirror/commands@6.2.4: + resolution: {integrity: sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==} dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -3451,7 +3491,7 @@ packages: escalade: 3.1.1 dev: false - /@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@replit/codemirror-emacs@6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==} peerDependencies: '@codemirror/autocomplete': ^6.0.2 @@ -3461,13 +3501,13 @@ packages: '@codemirror/view': ^6.3.0 dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.10.0 dev: false - /@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@replit/codemirror-vim@6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==} peerDependencies: '@codemirror/commands': ^6.0.0 @@ -3476,7 +3516,7 @@ packages: '@codemirror/state': ^6.0.1 '@codemirror/view': ^6.0.3 dependencies: - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 @@ -4069,7 +4109,7 @@ packages: eslint-visitor-keys: 3.3.0 dev: false - /@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): + /@uiw/codemirror-extensions-basic-setup@4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0): resolution: {integrity: sha512-Xm0RDpyYVZ/8hWqaBs3+wZwi4uLwZUBwp/uCt89X80FeR6mr3BFuC+a+gcDO4dBu3l+WQE3jJdhjKjB2TCY/PQ==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' @@ -4081,7 +4121,7 @@ packages: '@codemirror/view': '>=6.0.0' dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 @@ -4376,11 +4416,11 @@ packages: react-dom: '>=16.8.0' dependencies: '@babel/runtime': 7.20.13 - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.10.0 - '@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.0)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) + '@uiw/codemirror-extensions-basic-setup': 4.19.16(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0) codemirror: 6.0.1(@lezer/common@1.0.2) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5441,7 +5481,7 @@ packages: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} dependencies: '@codemirror/autocomplete': 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2) - '@codemirror/commands': 6.2.0 + '@codemirror/commands': 6.2.4 '@codemirror/language': 6.6.0 '@codemirror/lint': 6.1.0 '@codemirror/search': 6.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3b068aa6..4b821d33 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,5 @@ packages: - "packages/*" - "website/" - "packages/core/examples/vite-vanilla-repl" + - "packages/core/examples/vite-vanilla-repl-cm6" - "packages/react/examples/nano-repl" \ No newline at end of file From f069f53faee20265c2e0c9928d1437ddff194201 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:06:27 +0200 Subject: [PATCH 02/36] vanilla-repl-cm6: add keybindings --- .../vite-vanilla-repl-cm6/codemirror.js | 16 ++++++--- .../examples/vite-vanilla-repl-cm6/main.js | 34 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index 7ba377ab..7a0d95d8 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -7,7 +7,7 @@ import { StateField, StateEffect } from '@codemirror/state'; import './style.css'; // https://codemirror.net/docs/guide/ -export function initEditor(initialCode, onUpdate) { +export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { let state = EditorState.create({ doc: initialCode, extensions: [ @@ -22,9 +22,17 @@ export function initEditor(initialCode, onUpdate) { syntaxHighlighting(defaultHighlightStyle), keymap.of(defaultKeymap), //flashField, - EditorView.updateListener.of((v) => { - onUpdate(v); - }), + EditorView.updateListener.of((v) => onChange(v)), + keymap.of([ + { + key: 'Ctrl-Enter', + run: () => onEvaluate(), + }, + { + key: 'Ctrl-.', + run: () => onStop(), + }, + ]), ], }); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 814f3609..fafed909 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -5,28 +5,36 @@ import { initStrudel } from './strudel'; import { Highlighter } from './highlighter'; import { bumpStreet } from './tunes'; let code = bumpStreet; - -const view = initEditor(code, (v) => { - code = v.state.doc.toString(); -}); const repl = initStrudel(); -let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); - -document.getElementById('play').addEventListener('click', async () => { +async function onEvaluate() { const { evaluate, scheduler } = await repl; if (!scheduler.started) { scheduler.stop(); - scheduler.lastEnd = 0; await evaluate(code); highlighter.start(scheduler); } else { await evaluate(code); } +} + +async function onStop() { + const { scheduler } = await repl; + scheduler.stop(); + highlighter.stop(); +} + +const view = initEditor({ + initialCode: code, + onChange: (v) => { + code = v.state.doc.toString(); + }, + onEvaluate, + onStop, }); -document.getElementById('stop').addEventListener('click', async () => { - const { stop } = await repl; - stop(); - highlighter.stop(); -}); +let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); + +document.getElementById('play').addEventListener('click', () => onEvaluate()); + +document.getElementById('stop').addEventListener('click', async () => onStop()); From 4b921c47f5798b5e727ab8d2d15df8fe09721c92 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:11:40 +0200 Subject: [PATCH 03/36] vanilla-repl-cm6: add flash effect --- .../vite-vanilla-repl-cm6/codemirror.js | 47 ++++++++++++++++--- .../examples/vite-vanilla-repl-cm6/main.js | 21 +++++---- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index 7a0d95d8..a79aaf4f 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -13,15 +13,11 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { extensions: [ javascript(), lineNumbers(), - /*gutter({ - class: "cm-mygutter" - }),*/ highlightField, highlightActiveLineGutter(), - //markLineGutter, syntaxHighlighting(defaultHighlightStyle), keymap.of(defaultKeymap), - //flashField, + flashField, EditorView.updateListener.of((v) => onChange(v)), keymap.of([ { @@ -42,7 +38,9 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { }); } -// codemirror specific highlighting logic +// +// highlighting +// export const setHighlights = StateEffect.define(); export const highlightField = StateField.define({ @@ -89,3 +87,40 @@ export const highlightField = StateField.define({ export function highlightHaps(view, haps) { view.dispatch({ effects: setHighlights.of({ haps }) }); } + +// +// flash +// + +export const setFlash = StateEffect.define(); +const flashField = StateField.define({ + create() { + return Decoration.none; + }, + update(flash, tr) { + try { + for (let e of tr.effects) { + if (e.is(setFlash)) { + if (e.value) { + const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); + flash = Decoration.set([mark.range(0, tr.newDoc.length)]); + } else { + flash = Decoration.set([]); + } + } + } + return flash; + } catch (err) { + console.warn('flash error', err); + return flash; + } + }, + provide: (f) => EditorView.decorations.from(f), +}); + +export const flash = (view) => { + view.dispatch({ effects: setFlash.of(true) }); + setTimeout(() => { + view.dispatch({ effects: setFlash.of(false) }); + }, 200); +}; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index fafed909..ff10ad64 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,14 +1,24 @@ // moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 -import { initEditor, highlightHaps } from './codemirror'; +import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; import { Highlighter } from './highlighter'; import { bumpStreet } from './tunes'; let code = bumpStreet; const repl = initStrudel(); +const view = initEditor({ + initialCode: code, + onChange: (v) => { + code = v.state.doc.toString(); + }, + onEvaluate, + onStop, +}); + async function onEvaluate() { const { evaluate, scheduler } = await repl; + flash(view); if (!scheduler.started) { scheduler.stop(); await evaluate(code); @@ -24,15 +34,6 @@ async function onStop() { highlighter.stop(); } -const view = initEditor({ - initialCode: code, - onChange: (v) => { - code = v.state.doc.toString(); - }, - onEvaluate, - onStop, -}); - let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); document.getElementById('play').addEventListener('click', () => onEvaluate()); From 95719654f3bfc5d3cc98c33816b402e2c0e38bd7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 09:52:07 +0200 Subject: [PATCH 04/36] refactor: remove old draw logic + pianoroll now uses .onPaint --- packages/core/draw.mjs | 38 +--------- packages/core/index.mjs | 1 - packages/core/pianoroll.mjs | 136 ++---------------------------------- packages/core/repl.mjs | 2 - packages/core/time.mjs | 11 --- packages/core/ui.mjs | 23 ------ website/src/repl/Repl.jsx | 5 +- 7 files changed, 10 insertions(+), 206 deletions(-) delete mode 100644 packages/core/time.mjs diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 4bfd3257..81700f6c 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, getTime, State, TimeSpan } from './index.mjs'; +import { Pattern } from './index.mjs'; export const getDrawContext = (id = 'test-canvas') => { let canvas = document.querySelector('#' + id); @@ -19,45 +19,9 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery }) { - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const ctx = getDrawContext(); - let cycle, - events = []; - const animate = (time) => { - const t = getTime(); - if (from !== undefined && to !== undefined) { - const currentCycle = Math.floor(t); - if (cycle !== currentCycle) { - cycle = currentCycle; - const begin = currentCycle + from; - const end = currentCycle + to; - setTimeout(() => { - events = this.query(new State(new TimeSpan(begin, end))) - .filter(Boolean) - .filter((event) => event.part.begin.equals(event.whole.begin)); - onQuery?.(events); - }, 0); - } - } - callback(ctx, events, t, time); - window.strudelAnimation = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); - return this; -}; - export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - if (window.strudelScheduler) { - clearInterval(window.strudelScheduler); - } }; Pattern.prototype.onPaint = function (onPaint) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index b6c74848..30504a5b 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,7 +19,6 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './logger.mjs'; -export * from './time.mjs'; export * from './draw.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 635b7fac..7f5f2fec 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; +import { Pattern, noteToMidi, freqToMidi } from './index.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -29,134 +29,6 @@ const getValue = (e) => { return value; }; -Pattern.prototype.pianoroll = function ({ - cycles = 4, - playhead = 0.5, - overscan = 1, - flipTime = 0, - flipValues = 0, - hideNegative = false, - // inactive = '#C9E597', - // inactive = '#FFCA28', - inactive = '#7491D2', - active = '#FFCA28', - // background = '#2A3236', - background = 'transparent', - smear = 0, - playheadColor = 'white', - minMidi = 10, - maxMidi = 90, - autorange = 0, - timeframe: timeframeProp, - fold = 0, - vertical = 0, -} = {}) { - const ctx = getDrawContext(); - const w = ctx.canvas.width; - const h = ctx.canvas.height; - let from = -cycles * playhead; - let to = cycles * (1 - playhead); - - if (timeframeProp) { - console.warn('timeframe is deprecated! use from/to instead'); - from = 0; - to = timeframeProp; - } - const timeAxis = vertical ? h : w; - const valueAxis = vertical ? w : h; - let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time - const timeExtent = to - from; // number of seconds that fit inside the canvas frame - const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values - let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true - let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true - let foldValues = []; - flipTime && timeRange.reverse(); - flipValues && valueRange.reverse(); - - this.draw( - (ctx, events, t) => { - ctx.fillStyle = background; - ctx.globalAlpha = 1; // reset! - if (!smear) { - ctx.clearRect(0, 0, w, h); - ctx.fillRect(0, 0, w, h); - } - const inFrame = (event) => - (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from; - events.filter(inFrame).forEach((event) => { - const isActive = event.whole.begin <= t && event.whole.end > t; - ctx.fillStyle = event.context?.color || inactive; - ctx.strokeStyle = event.context?.color || active; - ctx.globalAlpha = event.context.velocity ?? 1; - const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); - let durationPx = scale(event.duration / timeExtent, 0, timeAxis); - const value = getValue(event); - const valuePx = scale( - fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, - ...valueRange, - ); - let margin = 0; - const offset = scale(t / timeExtent, ...timeRange); - let coords; - if (vertical) { - coords = [ - valuePx + 1 - (flipValues ? barThickness : 0), // x - timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y - barThickness - 2, // width - durationPx - 2, // height - ]; - } else { - coords = [ - timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x - valuePx + 1 - (flipValues ? 0 : barThickness), // y - durationPx - 2, // widith - barThickness - 2, // height - ]; - } - isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); - }); - ctx.globalAlpha = 1; // reset! - const playheadPosition = scale(-from / timeExtent, ...timeRange); - // draw playhead - ctx.strokeStyle = playheadColor; - ctx.beginPath(); - if (vertical) { - ctx.moveTo(0, playheadPosition); - ctx.lineTo(valueAxis, playheadPosition); - } else { - ctx.moveTo(playheadPosition, 0); - ctx.lineTo(playheadPosition, valueAxis); - } - ctx.stroke(); - }, - { - from: from - overscan, - to: to + overscan, - onQuery: (events) => { - const { min, max, values } = events.reduce( - ({ min, max, values }, e) => { - const v = getValue(e); - return { - min: v < min ? v : min, - max: v > max ? v : max, - values: values.includes(v) ? values : [...values, v], - }; - }, - { min: Infinity, max: -Infinity, values: [] }, - ); - if (autorange) { - minMidi = min; - maxMidi = max; - valueExtent = maxMidi - minMidi + 1; - } - foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); - barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; - }, - }, - ); - return this; -}; - // this function allows drawing a pianoroll without ties to Pattern.prototype // it will probably replace the above in the future export function pianoroll({ @@ -295,3 +167,9 @@ function getOptions(drawTime, options = {}) { Pattern.prototype.punchcard = function (options) { return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) })); }; + +Pattern.prototype.pianoroll = function (options) { + return this.onPaint((ctx, time, haps, drawTime) => + pianoroll({ ctx, time, haps, ...getOptions(drawTime, { fold: 0, ...options }) }), + ); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 7c96bb66..de979258 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,7 +1,6 @@ import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; -import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; export function repl({ @@ -35,7 +34,6 @@ export function repl({ getTime, onToggle, }); - setTime(() => scheduler.now()); // TODO: refactor? const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); diff --git a/packages/core/time.mjs b/packages/core/time.mjs deleted file mode 100644 index 80daaf53..00000000 --- a/packages/core/time.mjs +++ /dev/null @@ -1,11 +0,0 @@ -let time; -export function getTime() { - if (!time) { - throw new Error('no time set! use setTime to define a time source'); - } - return time(); -} - -export function setTime(func) { - time = func; -} diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index df8230ec..cc148553 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -4,19 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { getTime } from './time.mjs'; - -function frame(callback) { - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const animate = (animationTime) => { - callback(animationTime, getTime()); - window.strudelAnimation = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); -} - export const backgroundImage = function (src, animateOptions = {}) { const container = document.getElementById('code'); const bg = 'background-image:url(' + src + ');background-size:contain;'; @@ -28,18 +15,8 @@ export const backgroundImage = function (src, animateOptions = {}) { className: () => (container.className = value + ' ' + initialClassName), })[option](); }; - const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function'); const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string'); stringOptions.forEach(([option, value]) => handleOption(option, value)); - - if (funcOptions.length === 0) { - return; - } - frame((_, t) => - funcOptions.forEach(([option, value]) => { - handleOption(option, value(t)); - }), - ); }; export const cleanupUi = () => { diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4ad387fe..d8270b16 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,10 +50,9 @@ const modulesLoading = evalScope( const presets = prebake(); -let drawContext, clearCanvas; +let drawContext; if (typeof window !== 'undefined') { drawContext = getDrawContext(); - clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); } const getTime = () => getAudioContext().currentTime; @@ -208,7 +207,7 @@ export function Repl({ embedded = false }) { const handleShuffle = async () => { const { code, name } = getRandomTune(); logger(`[repl] ✨ loading random tune "${name}"`); - clearCanvas(); + cleanupDraw(); resetLoadedSounds(); scheduler.setCps(1); await prebake(); // declare default samples From 1f4c2f8c5a0d21599da4fc0cb258281ebda77ae9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 10:52:21 +0200 Subject: [PATCH 05/36] draw scheduler in vanilla js --- .../examples/vite-vanilla-repl-cm6/drawer.js | 91 +++++++++++++++++++ .../vite-vanilla-repl-cm6/highlighter.js | 44 --------- .../examples/vite-vanilla-repl-cm6/index.html | 1 + .../examples/vite-vanilla-repl-cm6/main.js | 22 +++-- .../examples/vite-vanilla-repl-cm6/style.css | 4 + packages/core/pianoroll.mjs | 8 +- 6 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/drawer.js delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/highlighter.js diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js new file mode 100644 index 00000000..3bd6fbc9 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js @@ -0,0 +1,91 @@ +const round = (x) => Math.round(x * 1000) / 1000; + +export class Framer { + constructor(onFrame, onError) { + this.onFrame = onFrame; + this.onError = onError; + } + start() { + const self = this; + let frame = requestAnimationFrame(function updateHighlights(time) { + try { + self.onFrame(time); + } catch (err) { + self.onError(err); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + } + } +} + +export class Drawer { + constructor(onDraw, drawTime) { + let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] + lookbehind = Math.abs(lookbehind); + this.visibleHaps = []; + this.lastFrame = null; + this.drawTime = drawTime; + this.framer = new Framer( + () => { + if (!this.scheduler) { + console.warn('Drawer: no scheduler'); + return; + } + // calculate current frame time (think right side of screen for pianoroll) + const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase + if (this.lastFrame === null) { + this.lastFrame = phase; + return; + } + // query haps from last frame till now. take last 100ms max + const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); + this.lastFrame = phase; + this.visibleHaps = (this.visibleHaps || []) + // filter out haps that are too far in the past (think left edge of screen for pianoroll) + .filter((h) => h.whole.end >= phase - lookbehind - lookahead) + // add new haps with onset (think right edge bars scrolling in) + .concat(haps.filter((h) => h.hasOnset())); + const time = phase - lookahead; + onDraw(this.visibleHaps, time, this); + }, + (err) => { + console.warn('draw error', err); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + // append future haps + this.visibleHaps = this.visibleHaps.concat(futureHaps); + } + start(scheduler) { + this.scheduler = scheduler; + this.invalidate(); + this.framer.start(); + } + stop() { + if (this.framer) { + this.framer.stop(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js b/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js deleted file mode 100644 index 0a306e70..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/highlighter.js +++ /dev/null @@ -1,44 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -// this class can be used to create a code highlighter -// it is encapsulated from the editor via the onUpdate callback -// the scheduler is expected to be an instance of Cyclist -export class Highlighter { - constructor(onUpdate) { - this.onUpdate = onUpdate; - } - start(scheduler) { - let highlights = []; - let lastEnd = 0; - this.stop(); - const self = this; - let frame = requestAnimationFrame(function updateHighlights() { - try { - const time = scheduler.now(); - // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away - // see https://github.com/tidalcycles/strudel/issues/108 - const begin = Math.max(lastEnd ?? time, time - 1 / 10, -0.01); // negative time seems buggy - const span = [round(begin), round(time + 1 / 60)]; - lastEnd = span[1]; - highlights = highlights.filter((hap) => hap.whole.end > time); // keep only highlights that are still active - const haps = scheduler.pattern - .queryArc(...span) - .filter((hap) => hap.hasOnset()); - highlights = highlights.concat(haps); // add potential new onsets - self.onUpdate(highlights); // highlight all still active + new active haps - } catch (err) { - self.onUpdate([]); - } - frame = requestAnimationFrame(updateHighlights); - }); - self.cancel = () => { - cancelAnimationFrame(frame); - }; - } - stop() { - if (this.cancel) { - this.cancel(); - this.onUpdate([]); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/index.html b/packages/core/examples/vite-vanilla-repl-cm6/index.html index 32381f03..1a214021 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/index.html +++ b/packages/core/examples/vite-vanilla-repl-cm6/index.html @@ -15,6 +15,7 @@
+ diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index ff10ad64..6e0ed677 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,10 +2,13 @@ import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; -import { Highlighter } from './highlighter'; +import { Drawer } from './drawer'; import { bumpStreet } from './tunes'; +import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; + let code = bumpStreet; const repl = initStrudel(); +const roll = document.getElementById('roll'); const view = initEditor({ initialCode: code, @@ -22,20 +25,27 @@ async function onEvaluate() { if (!scheduler.started) { scheduler.stop(); await evaluate(code); - highlighter.start(scheduler); + drawer.start(scheduler); } else { await evaluate(code); + drawer.invalidate(); // this is a bit mystic } } async function onStop() { const { scheduler } = await repl; scheduler.stop(); - highlighter.stop(); + drawer.stop(); } - -let highlighter = new Highlighter((haps) => highlightHaps(view, haps)); +const ctx = roll.getContext('2d'); +let drawer = new Drawer( + (haps, time, { drawTime }) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + highlightHaps(view, currentFrame); + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 1 }) }); + }, + [-2, 2], +); document.getElementById('play').addEventListener('click', () => onEvaluate()); - document.getElementById('stop').addEventListener('click', async () => onStop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 20970778..fed3f86a 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -22,3 +22,7 @@ main { .container { flex-grow: 1; } + +#roll { + height: 300px; +} diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 7f5f2fec..b0f4c074 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -156,7 +156,7 @@ export function pianoroll({ return this; } -function getOptions(drawTime, options = {}) { +export function getDrawOptions(drawTime, options = {}) { let [lookbehind, lookahead] = drawTime; lookbehind = Math.abs(lookbehind); const cycles = lookahead + lookbehind; @@ -165,11 +165,13 @@ function getOptions(drawTime, options = {}) { } Pattern.prototype.punchcard = function (options) { - return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) })); + return this.onPaint((ctx, time, haps, drawTime) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }), + ); }; Pattern.prototype.pianoroll = function (options) { return this.onPaint((ctx, time, haps, drawTime) => - pianoroll({ ctx, time, haps, ...getOptions(drawTime, { fold: 0, ...options }) }), + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); }; From d96f3f9dd4f9a53c9847a31876570133b2b880f4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 11:09:44 +0200 Subject: [PATCH 06/36] vanilla-repl-cm6: style + codesplitting --- .../examples/vite-vanilla-repl-cm6/strudel.js | 39 +++++++------------ .../examples/vite-vanilla-repl-cm6/style.css | 19 ++++----- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js index a1e999ee..efd899ea 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -1,38 +1,29 @@ -import { controls, repl, evalScope } from "@strudel.cycles/core"; -import { transpiler } from "@strudel.cycles/transpiler"; -import { - getAudioContext, - webaudioOutput, - initAudioOnFirstClick, - registerSynthSounds -} from "@strudel.cycles/webaudio"; -import { registerSoundfonts } from "@strudel.cycles/soundfonts"; +import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel.cycles/webaudio'; const initAudio = initAudioOnFirstClick(); const ctx = getAudioContext(); -const loadModules = (scope = {}) => - evalScope( +export async function initStrudel(options = {}) { + const [{ controls, repl, evalScope }, { registerSoundfonts }, { transpiler }] = await Promise.all([ + import('@strudel.cycles/core'), + import('@strudel.cycles/soundfonts'), + import('@strudel.cycles/transpiler'), + ]); + + const loadModules = evalScope( controls, - import("@strudel.cycles/core"), - import("@strudel.cycles/mini"), - import("@strudel.cycles/tonal"), - import("@strudel.cycles/webaudio"), - scope + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), ); -export async function initStrudel(options = {}) { - await Promise.all([ - initAudio, - loadModules(), - registerSynthSounds(), - registerSoundfonts() - ]); + await Promise.all([initAudio, loadModules, registerSynthSounds(), registerSoundfonts()]); return repl({ defaultOutput: webaudioOutput, getTime: () => ctx.currentTime, transpiler, - ...options + ...options, }); } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index fed3f86a..21f011a0 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -4,15 +4,6 @@ html { height: 100%; } -#editor { - overflow: auto; - height: 100%; -} - -.cm-editor { - height: 100%; -} - main { height: 100%; display: flex; @@ -21,6 +12,16 @@ main { .container { flex-grow: 1; + max-height: 100%; + position: relative; +} + +#editor { + overflow: auto; +} + +.cm-editor { + height: 100%; } #roll { From d35bf9591c5b5f8021c8f5505db9701ebba4f81e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 11:54:09 +0200 Subject: [PATCH 07/36] vanilla-repl-cm6: better theme --- .../vite-vanilla-repl-cm6/codemirror.js | 3 +- .../examples/vite-vanilla-repl-cm6/main.js | 13 +- .../vite-vanilla-repl-cm6/one-dark.js | 139 ++++++++++++++++++ .../vite-vanilla-repl-cm6/package.json | 5 +- .../examples/vite-vanilla-repl-cm6/style.css | 2 + .../examples/vite-vanilla-repl-cm6/tunes.mjs | 80 ++++++++++ pnpm-lock.yaml | 3 + 7 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 packages/core/examples/vite-vanilla-repl-cm6/one-dark.js diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js index a79aaf4f..103edd92 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js @@ -4,13 +4,14 @@ import { defaultKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; import { javascript } from '@codemirror/lang-javascript'; import { StateField, StateEffect } from '@codemirror/state'; -import './style.css'; +import { oneDark } from './one-dark'; // https://codemirror.net/docs/guide/ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { let state = EditorState.create({ doc: initialCode, extensions: [ + oneDark, javascript(), lineNumbers(), highlightField, diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 6e0ed677..a73ccff3 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -3,12 +3,15 @@ import { initEditor, highlightHaps, flash } from './codemirror'; import { initStrudel } from './strudel'; import { Drawer } from './drawer'; -import { bumpStreet } from './tunes'; +import { bumpStreet, trafficFlam, funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; +import './style.css'; -let code = bumpStreet; +let code = funk42; const repl = initStrudel(); -const roll = document.getElementById('roll'); +const canvas = document.getElementById('roll'); +canvas.width = canvas.width * 2; +canvas.height = canvas.height * 2; const view = initEditor({ initialCode: code, @@ -37,12 +40,12 @@ async function onStop() { scheduler.stop(); drawer.stop(); } -const ctx = roll.getContext('2d'); +const ctx = canvas.getContext('2d'); let drawer = new Drawer( (haps, time, { drawTime }) => { const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); highlightHaps(view, currentFrame); - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 1 }) }); + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); }, [-2, 2], ); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js new file mode 100644 index 00000000..cce83699 --- /dev/null +++ b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js @@ -0,0 +1,139 @@ +import { EditorView } from '@codemirror/view'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors + +const chalky = '#e5c07b', + coral = '#e06c75', + cyan = '#56b6c2', + invalid = '#ffffff', + ivory = '#abb2bf', + stone = '#7d8799', // Brightened compared to original to increase contrast + malibu = '#61afef', + sage = '#98c379', + whiskey = '#d19a66', + violet = '#c678dd', + darkBackground = '#21252b', + highlightBackground = '#2c313a', + background = '#282c34', + tooltipBackground = '#353a42', + selection = '#3E4451', + cursor = '#528bff'; + +/// The colors used in the theme, as CSS color strings. +export const color = { + chalky, + coral, + cyan, + invalid, + ivory, + stone, + malibu, + sage, + whiskey, + violet, + darkBackground, + highlightBackground, + background, + tooltipBackground, + selection, + cursor, +}; + +/// The editor theme styles for One Dark. +export const oneDarkTheme = EditorView.theme( + { + '&': { + color: ivory, + backgroundColor: background, + }, + + '.cm-content': { + caretColor: cursor, + }, + + '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { backgroundColor: selection }, + + '.cm-panels': { backgroundColor: darkBackground, color: ivory }, + '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, + '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, + + '.cm-searchMatch': { + backgroundColor: '#72a1ff59', + outline: '1px solid #457dff', + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: '#6199ff2f', + }, + + '.cm-activeLine': { backgroundColor: '#6699ff0b' }, + '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, + + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: '#bad0f847', + }, + + '.cm-gutters': { + backgroundColor: background, + color: stone, + border: 'none', + }, + + '.cm-activeLineGutter': { + backgroundColor: highlightBackground, + }, + + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: '#ddd', + }, + + '.cm-tooltip': { + border: 'none', + backgroundColor: tooltipBackground, + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: tooltipBackground, + borderBottomColor: tooltipBackground, + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: highlightBackground, + color: ivory, + }, + }, + }, + { dark: true }, +); + +/// The highlighting style for code in the One Dark theme. +export const oneDarkHighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: violet }, + { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral }, + { tag: [t.function(t.variableName), t.labelName], color: malibu }, + { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey }, + { tag: [t.definition(t.name), t.separator], color: ivory }, + { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky }, + { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan }, + { tag: [t.meta, t.comment], color: stone }, + { tag: t.strong, fontWeight: 'bold' }, + { tag: t.emphasis, fontStyle: 'italic' }, + { tag: t.strikethrough, textDecoration: 'line-through' }, + { tag: t.link, color: stone, textDecoration: 'underline' }, + { tag: t.heading, fontWeight: 'bold', color: coral }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey }, + { tag: [t.processingInstruction, t.string, t.inserted], color: sage }, + { tag: t.invalid, color: invalid }, +]); + +/// Extension to enable the One Dark theme (both the editor theme and +/// the highlight style). +export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)]; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json index f84b23ed..0251bf96 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/package.json +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -17,11 +17,12 @@ "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.10.0", + "@lezer/highlight": "^1.1.4", "@strudel.cycles/core": "workspace:*", "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*", "@strudel.cycles/tonal": "workspace:*", "@strudel.cycles/transpiler": "workspace:*", - "@strudel.cycles/webaudio": "workspace:*", - "@strudel.cycles/soundfonts": "workspace:*" + "@strudel.cycles/webaudio": "workspace:*" } } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 21f011a0..5a95388a 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -2,6 +2,7 @@ body, html { margin: 0; height: 100%; + background: #282c34; } main { @@ -14,6 +15,7 @@ main { flex-grow: 1; max-height: 100%; position: relative; + overflow:auto; } #editor { diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs index 4b0155e4..b235aa95 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -30,3 +30,83 @@ await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'githu // wait for it... ).fast(2/3) //.crush(6) // remove "//" if you dare`; + +export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0 + +await samples('github:felixroos/samples/main') +await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') + +addVoicings('hip', { + m11: ['2M 3m 4P 7m'], + '^7#11': ['3M 4A 5P 7M'], +}, ['C4', 'C6']) + +stack( + stack( + "/2".voicings('hip').note() + .s("gm_epiano1:2") + .arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2") + .release(2).late(.25).lpf(2000), + "/2".note().s('gm_acoustic_bass'), + n("<0 2 3>(3,8)".off(1/8, add(4))) + .scale("/2") + .s('gm_electric_guitar_jazz') + .decay(sine.range(.05, .2).slow(32)).sustain(0) + .delay(.5).lpf(sine.range(100,5000).slow(64)) + .gain(.7).room(.5).pan(sine.range(0,1).slow(11)) + ).add(perlin.range(0,.25).note()), + stack( + s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>") + .when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))), + s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2) + ) +) + .late("[0 .05]*2").late(12) + +`; + +export const funk42 = `// froos - how to funk in 42 lines of code +// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg +// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4 + +await samples('github:felixroos/samples/main') +await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') + +setcps(.5) + +let drums = stack( + s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"), + s("hh*4").begin(.2).release(.02).end(.25).release(.02) + .gain(.3).bank('RolandTR707').late(.02).room(.5), + s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) +).fast(2) + +let wurli = note(\`< +[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3 +[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`) + .s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>") + .gain("<[.8@2 .4@14]!3 .7>").room(.3) + +let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12)) + .s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3); + +let clav = note(\`< +[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3 +[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`) + .s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0) + .gain(.7).pan(.8).room(.2); + +let bass = note(\`< +[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]] +[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]] +[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]] +[e2@6 e1@5 e1 [[d2 e3] g1]@4] +>\`).s("gm_electric_bass_pick:1").release(.1) + +stack( + drums + ,wurli + ,organ + ,clav + ,bass +)`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a6442f..a2e65f7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: '@codemirror/view': specifier: ^6.10.0 version: 6.10.0 + '@lezer/highlight': + specifier: ^1.1.4 + version: 1.1.4 '@strudel.cycles/core': specifier: workspace:* version: link:../.. From a6f57bced87e8946fc3f2ff983481d62f921d410 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 12:00:13 +0200 Subject: [PATCH 08/36] prettier --- packages/core/examples/vite-vanilla-repl-cm6/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/style.css b/packages/core/examples/vite-vanilla-repl-cm6/style.css index 5a95388a..67ab3917 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/style.css +++ b/packages/core/examples/vite-vanilla-repl-cm6/style.css @@ -15,7 +15,7 @@ main { flex-grow: 1; max-height: 100%; position: relative; - overflow:auto; + overflow: auto; } #editor { From f5075906e2166361dc58c9c16b8d4fdb81eaaff1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:13:17 +0200 Subject: [PATCH 09/36] add codemirror package + use it in vite-vanilla-repl-cm6 --- packages/codemirror/README.md | 3 + .../codemirror.mjs} | 32 +++- packages/codemirror/package.json | 46 ++++++ packages/codemirror/themes/one-dark.mjs | 139 ++++++++++++++++++ .../examples/vite-vanilla-repl-cm6/main.js | 12 +- .../vite-vanilla-repl-cm6/package.json | 7 +- 6 files changed, 222 insertions(+), 17 deletions(-) create mode 100644 packages/codemirror/README.md rename packages/{core/examples/vite-vanilla-repl-cm6/codemirror.js => codemirror/codemirror.mjs} (84%) create mode 100644 packages/codemirror/package.json create mode 100644 packages/codemirror/themes/one-dark.mjs diff --git a/packages/codemirror/README.md b/packages/codemirror/README.md new file mode 100644 index 00000000..300e30b3 --- /dev/null +++ b/packages/codemirror/README.md @@ -0,0 +1,3 @@ +# @strudel/codemirror + +This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it. diff --git a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js b/packages/codemirror/codemirror.mjs similarity index 84% rename from packages/core/examples/vite-vanilla-repl-cm6/codemirror.js rename to packages/codemirror/codemirror.mjs index 103edd92..1d874aff 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/codemirror.js +++ b/packages/codemirror/codemirror.mjs @@ -4,14 +4,14 @@ import { defaultKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; import { javascript } from '@codemirror/lang-javascript'; import { StateField, StateEffect } from '@codemirror/state'; -import { oneDark } from './one-dark'; +import { oneDark } from './themes/one-dark'; // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) { let state = EditorState.create({ doc: initialCode, extensions: [ - oneDark, + theme, javascript(), lineNumbers(), highlightField, @@ -35,7 +35,7 @@ export function initEditor({ initialCode, onChange, onEvaluate, onStop }) { return new EditorView({ state, - parent: document.getElementById('editor'), + parent: root, }); } @@ -119,9 +119,29 @@ const flashField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const flash = (view) => { +export const flash = (view, ms = 200) => { view.dispatch({ effects: setFlash.of(true) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); - }, 200); + }, ms); }; + +export class StrudelMirror { + constructor({ root, initialCode = '', onEvaluate, onStop }) { + this.view = initEditor({ + root, + initialCode, + onChange: (v) => { + this.code = v.state.doc.toString(); + }, + onEvaluate, + onStop, + }); + } + flash(ms) { + flash(this.view, ms); + } + highlight(haps) { + highlightHaps(this.view, haps); + } +} diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json new file mode 100644 index 00000000..6df43e80 --- /dev/null +++ b/packages/codemirror/package.json @@ -0,0 +1,46 @@ +{ + "name": "@strudel/codemirror", + "version": "0.8.0", + "description": "Codemirror Extensions for Strudel", + "main": "codemirror.mjs", + "publishConfig": { + "main": "dist/index.js", + "module": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "contributors": [ + "Alex McLean " + ], + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@codemirror/commands": "^6.2.4", + "@codemirror/lang-javascript": "^6.1.7", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.10.0", + "@lezer/highlight": "^1.1.4" + }, + "devDependencies": { + "vite": "^4.3.3" + } +} diff --git a/packages/codemirror/themes/one-dark.mjs b/packages/codemirror/themes/one-dark.mjs new file mode 100644 index 00000000..cce83699 --- /dev/null +++ b/packages/codemirror/themes/one-dark.mjs @@ -0,0 +1,139 @@ +import { EditorView } from '@codemirror/view'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors + +const chalky = '#e5c07b', + coral = '#e06c75', + cyan = '#56b6c2', + invalid = '#ffffff', + ivory = '#abb2bf', + stone = '#7d8799', // Brightened compared to original to increase contrast + malibu = '#61afef', + sage = '#98c379', + whiskey = '#d19a66', + violet = '#c678dd', + darkBackground = '#21252b', + highlightBackground = '#2c313a', + background = '#282c34', + tooltipBackground = '#353a42', + selection = '#3E4451', + cursor = '#528bff'; + +/// The colors used in the theme, as CSS color strings. +export const color = { + chalky, + coral, + cyan, + invalid, + ivory, + stone, + malibu, + sage, + whiskey, + violet, + darkBackground, + highlightBackground, + background, + tooltipBackground, + selection, + cursor, +}; + +/// The editor theme styles for One Dark. +export const oneDarkTheme = EditorView.theme( + { + '&': { + color: ivory, + backgroundColor: background, + }, + + '.cm-content': { + caretColor: cursor, + }, + + '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { backgroundColor: selection }, + + '.cm-panels': { backgroundColor: darkBackground, color: ivory }, + '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, + '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, + + '.cm-searchMatch': { + backgroundColor: '#72a1ff59', + outline: '1px solid #457dff', + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: '#6199ff2f', + }, + + '.cm-activeLine': { backgroundColor: '#6699ff0b' }, + '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, + + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: '#bad0f847', + }, + + '.cm-gutters': { + backgroundColor: background, + color: stone, + border: 'none', + }, + + '.cm-activeLineGutter': { + backgroundColor: highlightBackground, + }, + + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: '#ddd', + }, + + '.cm-tooltip': { + border: 'none', + backgroundColor: tooltipBackground, + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: tooltipBackground, + borderBottomColor: tooltipBackground, + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: highlightBackground, + color: ivory, + }, + }, + }, + { dark: true }, +); + +/// The highlighting style for code in the One Dark theme. +export const oneDarkHighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: violet }, + { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral }, + { tag: [t.function(t.variableName), t.labelName], color: malibu }, + { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey }, + { tag: [t.definition(t.name), t.separator], color: ivory }, + { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky }, + { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan }, + { tag: [t.meta, t.comment], color: stone }, + { tag: t.strong, fontWeight: 'bold' }, + { tag: t.emphasis, fontStyle: 'italic' }, + { tag: t.strikethrough, textDecoration: 'line-through' }, + { tag: t.link, color: stone, textDecoration: 'underline' }, + { tag: t.heading, fontWeight: 'bold', color: coral }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey }, + { tag: [t.processingInstruction, t.string, t.inserted], color: sage }, + { tag: t.invalid, color: invalid }, +]); + +/// Extension to enable the One Dark theme (both the editor theme and +/// the highlight style). +export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)]; diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index a73ccff3..7b19596d 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,9 +1,9 @@ // moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 -import { initEditor, highlightHaps, flash } from './codemirror'; +import { StrudelMirror } from '@strudel/codemirror'; import { initStrudel } from './strudel'; import { Drawer } from './drawer'; -import { bumpStreet, trafficFlam, funk42 } from './tunes'; +import { funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; import './style.css'; @@ -13,7 +13,8 @@ const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; canvas.height = canvas.height * 2; -const view = initEditor({ +const editor = new StrudelMirror({ + root: document.getElementById('editor'), initialCode: code, onChange: (v) => { code = v.state.doc.toString(); @@ -24,7 +25,7 @@ const view = initEditor({ async function onEvaluate() { const { evaluate, scheduler } = await repl; - flash(view); + editor.flash(); if (!scheduler.started) { scheduler.stop(); await evaluate(code); @@ -40,11 +41,12 @@ async function onStop() { scheduler.stop(); drawer.stop(); } + const ctx = canvas.getContext('2d'); let drawer = new Drawer( (haps, time, { drawTime }) => { const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - highlightHaps(view, currentFrame); + editor.highlight(currentFrame); pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); }, [-2, 2], diff --git a/packages/core/examples/vite-vanilla-repl-cm6/package.json b/packages/core/examples/vite-vanilla-repl-cm6/package.json index 0251bf96..4e093d10 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/package.json +++ b/packages/core/examples/vite-vanilla-repl-cm6/package.json @@ -12,12 +12,7 @@ "vite": "^4.3.2" }, "dependencies": { - "@codemirror/commands": "^6.2.4", - "@codemirror/lang-javascript": "^6.1.7", - "@codemirror/language": "^6.6.0", - "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.10.0", - "@lezer/highlight": "^1.1.4", + "@strudel/codemirror": "workspace:*", "@strudel.cycles/core": "workspace:*", "@strudel.cycles/mini": "workspace:*", "@strudel.cycles/soundfonts": "workspace:*", From e6b66f25250cf1a681277c25717cc9657e576e69 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:14:46 +0200 Subject: [PATCH 10/36] lockfile --- pnpm-lock.yaml | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2e65f7a..570983bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,31 @@ importers: specifier: ^5.8.1 version: 5.9.0 + packages/codemirror: + dependencies: + '@codemirror/commands': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-javascript': + specifier: ^6.1.7 + version: 6.1.7 + '@codemirror/language': + specifier: ^6.6.0 + version: 6.6.0 + '@codemirror/state': + specifier: ^6.2.0 + version: 6.2.0 + '@codemirror/view': + specifier: ^6.10.0 + version: 6.10.0 + '@lezer/highlight': + specifier: ^1.1.4 + version: 1.1.4 + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.3.3(@types/node@18.16.3) + packages/core: dependencies: fraction.js: @@ -103,24 +128,6 @@ importers: packages/core/examples/vite-vanilla-repl-cm6: dependencies: - '@codemirror/commands': - specifier: ^6.2.4 - version: 6.2.4 - '@codemirror/lang-javascript': - specifier: ^6.1.7 - version: 6.1.7 - '@codemirror/language': - specifier: ^6.6.0 - version: 6.6.0 - '@codemirror/state': - specifier: ^6.2.0 - version: 6.2.0 - '@codemirror/view': - specifier: ^6.10.0 - version: 6.10.0 - '@lezer/highlight': - specifier: ^1.1.4 - version: 1.1.4 '@strudel.cycles/core': specifier: workspace:* version: link:../.. @@ -139,6 +146,9 @@ importers: '@strudel.cycles/webaudio': specifier: workspace:* version: link:../../../webaudio + '@strudel/codemirror': + specifier: workspace:* + version: link:../../../codemirror devDependencies: vite: specifier: ^4.3.2 From 352e647d37169fcf1572dba30bbf622ff4ae5d43 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:15:45 +0200 Subject: [PATCH 11/36] add build script for codemirror package --- packages/codemirror/vite.config.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/codemirror/vite.config.js diff --git a/packages/codemirror/vite.config.js b/packages/codemirror/vite.config.js new file mode 100644 index 00000000..8562915c --- /dev/null +++ b/packages/codemirror/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'codemirror.mjs'), + formats: ['es', 'cjs'], + fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]), + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); From b5b0156e080025eb8de0657d2138387e7d062206 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:31:04 +0200 Subject: [PATCH 12/36] fix: codemirror reeval --- packages/codemirror/codemirror.mjs | 1 + packages/core/examples/vite-vanilla-repl-cm6/main.js | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 1d874aff..b9147226 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -128,6 +128,7 @@ export const flash = (view, ms = 200) => { export class StrudelMirror { constructor({ root, initialCode = '', onEvaluate, onStop }) { + this.code = initialCode; this.view = initEditor({ root, initialCode, diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 7b19596d..33f529ff 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -7,7 +7,6 @@ import { funk42 } from './tunes'; import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; import './style.css'; -let code = funk42; const repl = initStrudel(); const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; @@ -15,10 +14,7 @@ canvas.height = canvas.height * 2; const editor = new StrudelMirror({ root: document.getElementById('editor'), - initialCode: code, - onChange: (v) => { - code = v.state.doc.toString(); - }, + initialCode: funk42, onEvaluate, onStop, }); @@ -28,10 +24,10 @@ async function onEvaluate() { editor.flash(); if (!scheduler.started) { scheduler.stop(); - await evaluate(code); + await evaluate(editor.code); drawer.start(scheduler); } else { - await evaluate(code); + await evaluate(editor.code); drawer.invalidate(); // this is a bit mystic } } From 9039600f6dda4d4f8aab2f0440b2576ce0bbd73e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 15:31:19 +0200 Subject: [PATCH 13/36] bump codemirror to 0.8.1 --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 6df43e80..6ea2ac25 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.0", + "version": "0.8.1", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From 9807c4b7a1dc6adb30a0157dd45570c8935eb5e2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 16:06:11 +0200 Subject: [PATCH 14/36] move Framer + Drawer to core ... from vite-vanilla-repl-cm6 --- packages/core/draw.mjs | 95 ++++++++++++ .../examples/vite-vanilla-repl-cm6/drawer.js | 91 ------------ .../examples/vite-vanilla-repl-cm6/main.js | 3 +- .../vite-vanilla-repl-cm6/one-dark.js | 139 ------------------ 4 files changed, 96 insertions(+), 232 deletions(-) delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/drawer.js delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/one-dark.js diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 81700f6c..bc6d3cd8 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -29,3 +29,98 @@ Pattern.prototype.onPaint = function (onPaint) { this.context = { onPaint }; return this; }; + +// const round = (x) => Math.round(x * 1000) / 1000; + +// encapsulates starting and stopping animation frames +export class Framer { + constructor(onFrame, onError) { + this.onFrame = onFrame; + this.onError = onError; + } + start() { + const self = this; + let frame = requestAnimationFrame(function updateHighlights(time) { + try { + self.onFrame(time); + } catch (err) { + self.onError(err); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + } + } +} + +// syncs animation frames to a cyclist scheduler +// see vite-vanilla-repl-cm6 for an example +export class Drawer { + constructor(onDraw, drawTime) { + let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] + lookbehind = Math.abs(lookbehind); + this.visibleHaps = []; + this.lastFrame = null; + this.drawTime = drawTime; + this.framer = new Framer( + () => { + if (!this.scheduler) { + console.warn('Drawer: no scheduler'); + return; + } + // calculate current frame time (think right side of screen for pianoroll) + const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase + if (this.lastFrame === null) { + this.lastFrame = phase; + return; + } + // query haps from last frame till now. take last 100ms max + const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); + this.lastFrame = phase; + this.visibleHaps = (this.visibleHaps || []) + // filter out haps that are too far in the past (think left edge of screen for pianoroll) + .filter((h) => h.whole.end >= phase - lookbehind - lookahead) + // add new haps with onset (think right edge bars scrolling in) + .concat(haps.filter((h) => h.hasOnset())); + const time = phase - lookahead; + onDraw(this.visibleHaps, time, this); + }, + (err) => { + console.warn('draw error', err); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + // append future haps + this.visibleHaps = this.visibleHaps.concat(futureHaps); + } + start(scheduler) { + this.scheduler = scheduler; + this.invalidate(); + this.framer.start(); + } + stop() { + if (this.framer) { + this.framer.stop(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js deleted file mode 100644 index 3bd6fbc9..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js +++ /dev/null @@ -1,91 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -export class Framer { - constructor(onFrame, onError) { - this.onFrame = onFrame; - this.onError = onError; - } - start() { - const self = this; - let frame = requestAnimationFrame(function updateHighlights(time) { - try { - self.onFrame(time); - } catch (err) { - self.onError(err); - } - frame = requestAnimationFrame(updateHighlights); - }); - self.cancel = () => { - cancelAnimationFrame(frame); - }; - } - stop() { - if (this.cancel) { - this.cancel(); - } - } -} - -export class Drawer { - constructor(onDraw, drawTime) { - let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] - lookbehind = Math.abs(lookbehind); - this.visibleHaps = []; - this.lastFrame = null; - this.drawTime = drawTime; - this.framer = new Framer( - () => { - if (!this.scheduler) { - console.warn('Drawer: no scheduler'); - return; - } - // calculate current frame time (think right side of screen for pianoroll) - const phase = this.scheduler.now() + lookahead; - // first frame just captures the phase - if (this.lastFrame === null) { - this.lastFrame = phase; - return; - } - // query haps from last frame till now. take last 100ms max - const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); - this.lastFrame = phase; - this.visibleHaps = (this.visibleHaps || []) - // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.whole.end >= phase - lookbehind - lookahead) - // add new haps with onset (think right edge bars scrolling in) - .concat(haps.filter((h) => h.hasOnset())); - const time = phase - lookahead; - onDraw(this.visibleHaps, time, this); - }, - (err) => { - console.warn('draw error', err); - }, - ); - } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); - } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); - let [_, lookahead] = this.drawTime; - // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); - // query future haps - const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. - // append future haps - this.visibleHaps = this.visibleHaps.concat(futureHaps); - } - start(scheduler) { - this.scheduler = scheduler; - this.invalidate(); - this.framer.start(); - } - stop() { - if (this.framer) { - this.framer.stop(); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 33f529ff..bede9161 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,9 +2,8 @@ import { StrudelMirror } from '@strudel/codemirror'; import { initStrudel } from './strudel'; -import { Drawer } from './drawer'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; +import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; import './style.css'; const repl = initStrudel(); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js deleted file mode 100644 index cce83699..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js +++ /dev/null @@ -1,139 +0,0 @@ -import { EditorView } from '@codemirror/view'; -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { tags as t } from '@lezer/highlight'; - -// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors - -const chalky = '#e5c07b', - coral = '#e06c75', - cyan = '#56b6c2', - invalid = '#ffffff', - ivory = '#abb2bf', - stone = '#7d8799', // Brightened compared to original to increase contrast - malibu = '#61afef', - sage = '#98c379', - whiskey = '#d19a66', - violet = '#c678dd', - darkBackground = '#21252b', - highlightBackground = '#2c313a', - background = '#282c34', - tooltipBackground = '#353a42', - selection = '#3E4451', - cursor = '#528bff'; - -/// The colors used in the theme, as CSS color strings. -export const color = { - chalky, - coral, - cyan, - invalid, - ivory, - stone, - malibu, - sage, - whiskey, - violet, - darkBackground, - highlightBackground, - background, - tooltipBackground, - selection, - cursor, -}; - -/// The editor theme styles for One Dark. -export const oneDarkTheme = EditorView.theme( - { - '&': { - color: ivory, - backgroundColor: background, - }, - - '.cm-content': { - caretColor: cursor, - }, - - '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': - { backgroundColor: selection }, - - '.cm-panels': { backgroundColor: darkBackground, color: ivory }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, - - '.cm-searchMatch': { - backgroundColor: '#72a1ff59', - outline: '1px solid #457dff', - }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: '#6199ff2f', - }, - - '.cm-activeLine': { backgroundColor: '#6699ff0b' }, - '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, - - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - backgroundColor: '#bad0f847', - }, - - '.cm-gutters': { - backgroundColor: background, - color: stone, - border: 'none', - }, - - '.cm-activeLineGutter': { - backgroundColor: highlightBackground, - }, - - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: '#ddd', - }, - - '.cm-tooltip': { - border: 'none', - backgroundColor: tooltipBackground, - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent', - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground, - }, - '.cm-tooltip-autocomplete': { - '& > ul > li[aria-selected]': { - backgroundColor: highlightBackground, - color: ivory, - }, - }, - }, - { dark: true }, -); - -/// The highlighting style for code in the One Dark theme. -export const oneDarkHighlightStyle = HighlightStyle.define([ - { tag: t.keyword, color: violet }, - { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral }, - { tag: [t.function(t.variableName), t.labelName], color: malibu }, - { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey }, - { tag: [t.definition(t.name), t.separator], color: ivory }, - { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky }, - { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan }, - { tag: [t.meta, t.comment], color: stone }, - { tag: t.strong, fontWeight: 'bold' }, - { tag: t.emphasis, fontStyle: 'italic' }, - { tag: t.strikethrough, textDecoration: 'line-through' }, - { tag: t.link, color: stone, textDecoration: 'underline' }, - { tag: t.heading, fontWeight: 'bold', color: coral }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey }, - { tag: [t.processingInstruction, t.string, t.inserted], color: sage }, - { tag: t.invalid, color: invalid }, -]); - -/// Extension to enable the One Dark theme (both the editor theme and -/// the highlight style). -export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)]; From 5b67fccb1b10fb7fd4edd25569c4b54e3fa156ad Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 16:17:22 +0200 Subject: [PATCH 15/36] vanilla-repl-cm6: draw first frame --- .../examples/vite-vanilla-repl-cm6/main.js | 42 +++++++++++++------ .../examples/vite-vanilla-repl-cm6/strudel.js | 5 +-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index bede9161..a8a1c754 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -5,11 +5,9 @@ import { initStrudel } from './strudel'; import { funk42 } from './tunes'; import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; import './style.css'; +import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; -const repl = initStrudel(); -const canvas = document.getElementById('roll'); -canvas.width = canvas.width * 2; -canvas.height = canvas.height * 2; +const initAudio = initAudioOnFirstClick(); const editor = new StrudelMirror({ root: document.getElementById('editor'), @@ -18,8 +16,26 @@ const editor = new StrudelMirror({ onStop, }); +async function drawFirstFrame(editor, repl) { + const { evaluate } = repl; + const pattern = await evaluate(editor.code, false); + const initialHaps = pattern.queryArc(0, drawTime[1]); + drawPianoroll(initialHaps, 0); + return repl; +} + +const repl = initStrudel().then(async (repl) => { + await drawFirstFrame(editor, repl); + return repl; +}); + +const canvas = document.getElementById('roll'); +canvas.width = canvas.width * 2; +canvas.height = canvas.height * 2; + async function onEvaluate() { const { evaluate, scheduler } = await repl; + await initAudio; editor.flash(); if (!scheduler.started) { scheduler.stop(); @@ -37,15 +53,17 @@ async function onStop() { drawer.stop(); } +const drawTime = [-2, 2]; +function drawPianoroll(haps, time) { + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); +} + const ctx = canvas.getContext('2d'); -let drawer = new Drawer( - (haps, time, { drawTime }) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - editor.highlight(currentFrame); - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); - }, - [-2, 2], -); +let drawer = new Drawer((haps, time) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + editor.highlight(currentFrame); + drawPianoroll(haps, time); +}, drawTime); document.getElementById('play').addEventListener('click', () => onEvaluate()); document.getElementById('stop').addEventListener('click', async () => onStop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js index efd899ea..de9b2cd7 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js @@ -1,6 +1,5 @@ -import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel.cycles/webaudio'; +import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; -const initAudio = initAudioOnFirstClick(); const ctx = getAudioContext(); export async function initStrudel(options = {}) { @@ -18,7 +17,7 @@ export async function initStrudel(options = {}) { import('@strudel.cycles/webaudio'), ); - await Promise.all([initAudio, loadModules, registerSynthSounds(), registerSoundfonts()]); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); return repl({ defaultOutput: webaudioOutput, From 9ec7109dc8dbb2fa894cfd9a0b01413dbff32962 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:43:07 +0200 Subject: [PATCH 16/36] + simplify vanilla setup drastically + move repl + drawer boilerplate inside StrudelMirror --- packages/codemirror/codemirror.mjs | 63 +++++++++++-- packages/codemirror/package.json | 3 +- packages/core/draw.mjs | 15 ++-- .../examples/vite-vanilla-repl-cm6/main.js | 88 ++++++------------- .../examples/vite-vanilla-repl-cm6/strudel.js | 28 ------ .../examples/vite-vanilla-repl-cm6/tunes.mjs | 2 +- packages/core/pianoroll.mjs | 5 ++ 7 files changed, 101 insertions(+), 103 deletions(-) delete mode 100644 packages/core/examples/vite-vanilla-repl-cm6/strudel.js diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index b9147226..39429432 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -5,6 +5,7 @@ import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language' import { javascript } from '@codemirror/lang-javascript'; import { StateField, StateEffect } from '@codemirror/state'; import { oneDark } from './themes/one-dark'; +import { repl, Drawer } from '@strudel.cycles/core'; // https://codemirror.net/docs/guide/ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) { @@ -127,22 +128,72 @@ export const flash = (view, ms = 200) => { }; export class StrudelMirror { - constructor({ root, initialCode = '', onEvaluate, onStop }) { + constructor(options) { + const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options; this.code = initialCode; - this.view = initEditor({ + + this.drawer = new Drawer((haps, time) => { + const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); + this.highlight(currentFrame); + onDraw?.(haps, time, currentFrame); + }, drawTime); + + const prebaked = prebake(); + prebaked.then(async () => { + if (!onDraw) { + return; + } + const { scheduler, evaluate } = await this.repl; + // draw first frame instantly + prebaked.then(async () => { + await evaluate(this.code, false); + this.drawer.invalidate(scheduler); + onDraw?.(this.drawer.visibleHaps, 0, []); + }); + }); + + this.repl = repl({ + ...replOptions, + onToggle: async (started) => { + replOptions?.onToggle?.(started); + const { scheduler } = await this.repl; + if (started) { + this.drawer.start(scheduler); + } else { + this.drawer.stop(); + } + }, + beforeEval: async () => { + await prebaked; + }, + afterEval: (options) => { + replOptions?.afterEval?.(options); + this.drawer.invalidate(); + }, + }); + this.editor = initEditor({ root, initialCode, onChange: (v) => { this.code = v.state.doc.toString(); }, - onEvaluate, - onStop, + onEvaluate: () => this.evaluate(), + onStop: () => this.stop(), }); } + async evaluate() { + const { evaluate } = await this.repl; + this.flash(); + await evaluate(this.code); + } + async stop() { + const { scheduler } = await this.repl; + scheduler.stop(); + } flash(ms) { - flash(this.view, ms); + flash(this.editor, ms); } highlight(haps) { - highlightHaps(this.view, haps); + highlightHaps(this.editor, haps); } } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 6ea2ac25..a4ebf03a 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -38,7 +38,8 @@ "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.10.0", - "@lezer/highlight": "^1.1.4" + "@lezer/highlight": "^1.1.4", + "@strudel.cycles/core": "workspace:*" }, "devDependencies": { "vite": "^4.3.3" diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index bc6d3cd8..58b14040 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -97,19 +97,18 @@ export class Drawer { }, ); } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); + invalidate(scheduler = this.scheduler) { + if (!scheduler) { + return; } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); + this.scheduler = scheduler; + const t = scheduler.now(); let [_, lookahead] = this.drawTime; + const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); // query future haps - const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query.. // append future haps this.visibleHaps = this.visibleHaps.concat(futureHaps); } diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index a8a1c754..425799be 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -1,69 +1,39 @@ -// moved from sandbox: https://codesandbox.io/s/vanilla-codemirror-strudel-2wb7yw?file=/index.html:114-186 - import { StrudelMirror } from '@strudel/codemirror'; -import { initStrudel } from './strudel'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; +import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core'; import './style.css'; import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; +import { transpiler } from '@strudel.cycles/transpiler'; +import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; -const initAudio = initAudioOnFirstClick(); - -const editor = new StrudelMirror({ - root: document.getElementById('editor'), - initialCode: funk42, - onEvaluate, - onStop, -}); - -async function drawFirstFrame(editor, repl) { - const { evaluate } = repl; - const pattern = await evaluate(editor.code, false); - const initialHaps = pattern.queryArc(0, drawTime[1]); - drawPianoroll(initialHaps, 0); - return repl; -} - -const repl = initStrudel().then(async (repl) => { - await drawFirstFrame(editor, repl); - return repl; -}); - +// init canvas const canvas = document.getElementById('roll'); canvas.width = canvas.width * 2; canvas.height = canvas.height * 2; +const drawContext = canvas.getContext('2d'); +const drawTime = [-2, 2]; // time window of drawn haps -async function onEvaluate() { - const { evaluate, scheduler } = await repl; - await initAudio; - editor.flash(); - if (!scheduler.started) { - scheduler.stop(); - await evaluate(editor.code); - drawer.start(scheduler); - } else { - await evaluate(editor.code); - drawer.invalidate(); // this is a bit mystic - } -} +const editor = new StrudelMirror({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + root: document.getElementById('editor'), + initialCode: funk42, + drawTime, + onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }), + prebake: async () => { + initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..) + const loadModules = evalScope( + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + ); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); + }, +}); -async function onStop() { - const { scheduler } = await repl; - scheduler.stop(); - drawer.stop(); -} - -const drawTime = [-2, 2]; -function drawPianoroll(haps, time) { - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0 }) }); -} - -const ctx = canvas.getContext('2d'); -let drawer = new Drawer((haps, time) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end); - editor.highlight(currentFrame); - drawPianoroll(haps, time); -}, drawTime); - -document.getElementById('play').addEventListener('click', () => onEvaluate()); -document.getElementById('stop').addEventListener('click', async () => onStop()); +document.getElementById('play').addEventListener('click', () => editor.evaluate()); +document.getElementById('stop').addEventListener('click', () => editor.stop()); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js b/packages/core/examples/vite-vanilla-repl-cm6/strudel.js deleted file mode 100644 index de9b2cd7..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/strudel.js +++ /dev/null @@ -1,28 +0,0 @@ -import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio'; - -const ctx = getAudioContext(); - -export async function initStrudel(options = {}) { - const [{ controls, repl, evalScope }, { registerSoundfonts }, { transpiler }] = await Promise.all([ - import('@strudel.cycles/core'), - import('@strudel.cycles/soundfonts'), - import('@strudel.cycles/transpiler'), - ]); - - const loadModules = evalScope( - controls, - import('@strudel.cycles/core'), - import('@strudel.cycles/mini'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/webaudio'), - ); - - await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); - - return repl({ - defaultOutput: webaudioOutput, - getTime: () => ctx.currentTime, - transpiler, - ...options, - }); -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs index b235aa95..242a0d4b 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs +++ b/packages/core/examples/vite-vanilla-repl-cm6/tunes.mjs @@ -78,7 +78,7 @@ let drums = stack( s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"), s("hh*4").begin(.2).release(.02).end(.25).release(.02) .gain(.3).bank('RolandTR707').late(.02).room(.5), - s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) + //s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02) ).fast(2) let wurli = note(\`< diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index b0f4c074..336bd428 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -175,3 +175,8 @@ Pattern.prototype.pianoroll = function (options) { pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); }; + +export function drawPianoroll(options) { + const { drawTime, ...rest } = options; + pianoroll({ ...getDrawOptions(drawTime), ...rest }); +} From bb71c519dbfd4b16af0f47a382633f63cc1e384b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:46:42 +0200 Subject: [PATCH 17/36] bump codemirror package --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index a4ebf03a..a6bb1bfe 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.1", + "version": "0.8.2", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From 299275e36805388e18711b6a91c37e3c52212fa8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:47:15 +0200 Subject: [PATCH 18/36] lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 570983bd..596351fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: '@lezer/highlight': specifier: ^1.1.4 version: 1.1.4 + '@strudel.cycles/core': + specifier: workspace:* + version: link:../core devDependencies: vite: specifier: ^4.3.3 From b52f65c4d7e4508d402aa3201a1f98c85cc76988 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:50:06 +0200 Subject: [PATCH 19/36] rename core package + bump to 0.8.0 --- packages/core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 14237689..f1666260 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "@strudel.cycles/core", - "version": "0.7.2", + "name": "@strudel/core", + "version": "0.8.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", From 692fc4e11d38c8fbae3163a09d3540292ca1047e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:52:04 +0200 Subject: [PATCH 20/36] revert rename core package --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index f1666260..9aebd5f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@strudel/core", + "name": "@strudel.cycles/core", "version": "0.8.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", From 308f1391937aea44b552f36e4d82a22a9d0fa9a8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:52:55 +0200 Subject: [PATCH 21/36] bump codemirror again to get new core version --- packages/codemirror/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index a6bb1bfe..c3b885db 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.8.2", + "version": "0.8.3", "description": "Codemirror Extensions for Strudel", "main": "codemirror.mjs", "publishConfig": { From b27a58df7ea4ba9c1bd685e5974fcbb91594c5a5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 May 2023 23:58:47 +0200 Subject: [PATCH 22/36] bump more packages to 0.8.0 --- packages/mini/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/webaudio/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mini/package.json b/packages/mini/package.json index 1f8ab829..eb525e9f 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/mini", - "version": "0.7.2", + "version": "0.8.0", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index ce1b4446..95bf91b0 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/soundfonts", - "version": "0.7.1", + "version": "0.8.0", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 2309a7d1..f50c7ff7 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/tonal", - "version": "0.7.1", + "version": "0.8.0", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 6b08191c..39046dc3 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/transpiler", - "version": "0.7.1", + "version": "0.8.0", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index bac45088..bb806543 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/webaudio", - "version": "0.7.1", + "version": "0.8.0", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", From 3b631cb6af8b10f2dff6ff9a6c7b1aa5e7bc2814 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 May 2023 11:56:35 +0200 Subject: [PATCH 23/36] add @strudel/web umbrella package + example --- packages/web/README.md | 35 ++++++++++++++ packages/web/examples/repl-example/.gitignore | 24 ++++++++++ packages/web/examples/repl-example/index.html | 29 ++++++++++++ .../web/examples/repl-example/package.json | 18 ++++++++ packages/web/package.json | 46 +++++++++++++++++++ packages/web/repl.mjs | 38 +++++++++++++++ pnpm-lock.yaml | 35 ++++++++++++++ pnpm-workspace.yaml | 3 +- 8 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 packages/web/README.md create mode 100644 packages/web/examples/repl-example/.gitignore create mode 100644 packages/web/examples/repl-example/index.html create mode 100644 packages/web/examples/repl-example/package.json create mode 100644 packages/web/package.json create mode 100644 packages/web/repl.mjs diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 00000000..c70a5df7 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,35 @@ +# @strudel/web + +This package provides an easy to use bundle of multiple strudel packages for the web. + +## Usage + +```js +import { repl } from '@strudel/web'; + +const strudel = repl(); + +document.getElementById('play').addEventListener('click', + () => strudel.evaluate('note("c a f e").jux(rev)') +); +``` + +Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event. + +### Loading samples + +By default, no external samples are loaded, but you can add them like this: + +```js +import { repl, samples } from '@strudel/web'; + +const strudel = repl({ + prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), +}); + +document.getElementById('play').addEventListener('click', + () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)') +); +``` + +You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples). diff --git a/packages/web/examples/repl-example/.gitignore b/packages/web/examples/repl-example/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/packages/web/examples/repl-example/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html new file mode 100644 index 00000000..a7954e64 --- /dev/null +++ b/packages/web/examples/repl-example/index.html @@ -0,0 +1,29 @@ + + + + + + + @strudel/web REPL Example + + +
+ + + + + + + diff --git a/packages/web/examples/repl-example/package.json b/packages/web/examples/repl-example/package.json new file mode 100644 index 00000000..42fb8b83 --- /dev/null +++ b/packages/web/examples/repl-example/package.json @@ -0,0 +1,18 @@ +{ + "name": "repl-example", + "private": true, + "version": "0.0.0", + "type": "module", + "license": "AGPL-3.0-or-later", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^4.3.2" + }, + "dependencies": { + "@strudel/web": "workspace:*" + } +} diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 00000000..11e9153e --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,46 @@ +{ + "name": "@strudel/web", + "version": "0.8.0", + "description": "Easy to setup, opiniated bundle of Strudel for the browser.", + "main": "repl.mjs", + "publishConfig": { + "main": "dist/index.js", + "module": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "contributors": [ + "Alex McLean " + ], + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel.cycles/core": "workspace:*", + "@strudel.cycles/webaudio": "workspace:*", + "@strudel.cycles/soundfonts": "workspace:*", + "@strudel.cycles/mini": "workspace:*", + "@strudel.cycles/tonal": "workspace:*", + "@strudel.cycles/transpiler": "workspace:*" + }, + "devDependencies": { + "vite": "^4.3.3" + } +} diff --git a/packages/web/repl.mjs b/packages/web/repl.mjs new file mode 100644 index 00000000..f86459b1 --- /dev/null +++ b/packages/web/repl.mjs @@ -0,0 +1,38 @@ +export * from '@strudel.cycles/core'; +export * from '@strudel.cycles/webaudio'; +export * from '@strudel.cycles/soundfonts'; +export * from '@strudel.cycles/transpiler'; +export * from '@strudel.cycles/mini'; +export * from '@strudel.cycles/tonal'; +export * from '@strudel.cycles/webaudio'; +import { repl as _repl, evalScope, controls } from '@strudel.cycles/core'; +import { initAudioOnFirstClick, getAudioContext, registerSynthSounds, webaudioOutput } from '@strudel.cycles/webaudio'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; +import { transpiler } from '@strudel.cycles/transpiler'; + +async function prebake(userPrebake) { + const loadModules = evalScope( + evalScope, + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + ); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts(), userPrebake?.()]); +} + +export function repl(options = {}) { + const prebaked = prebake(options?.prebake); + initAudioOnFirstClick(); + return _repl({ + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + transpiler, + ...options, + beforeEval: async (args) => { + options?.beforeEval?.(args); + await prebaked; + }, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 596351fa..ee27ce0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,41 @@ importers: specifier: ^0.28.0 version: 0.28.0(@vitest/ui@0.28.0) + packages/web: + dependencies: + '@strudel.cycles/core': + specifier: workspace:* + version: link:../core + '@strudel.cycles/mini': + specifier: workspace:* + version: link:../mini + '@strudel.cycles/soundfonts': + specifier: workspace:* + version: link:../soundfonts + '@strudel.cycles/tonal': + specifier: workspace:* + version: link:../tonal + '@strudel.cycles/transpiler': + specifier: workspace:* + version: link:../transpiler + '@strudel.cycles/webaudio': + specifier: workspace:* + version: link:../webaudio + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.3.3(@types/node@18.16.3) + + packages/web/examples/repl-example: + dependencies: + '@strudel/web': + specifier: workspace:* + version: link:../.. + devDependencies: + vite: + specifier: ^4.3.2 + version: 4.3.3(@types/node@18.16.3) + packages/webaudio: dependencies: '@strudel.cycles/core': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4b821d33..984f2447 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,4 +4,5 @@ packages: - "website/" - "packages/core/examples/vite-vanilla-repl" - "packages/core/examples/vite-vanilla-repl-cm6" - - "packages/react/examples/nano-repl" \ No newline at end of file + - "packages/react/examples/nano-repl" + - "packages/web/examples/repl-example" From 12228c56d97d004cb2ca536ea8514c02c690882c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 May 2023 22:36:26 +0200 Subject: [PATCH 24/36] improve api for web package --- packages/core/index.mjs | 1 + packages/core/repl.mjs | 39 ++++++----- packages/mini/mini.mjs | 5 ++ packages/web/README.md | 39 +++++++---- packages/web/examples/repl-example/index.html | 13 ++-- packages/web/package.json | 2 +- packages/web/repl.mjs | 38 ----------- packages/web/vite.config.js | 19 ++++++ packages/web/web.mjs | 64 +++++++++++++++++++ packages/webaudio/webaudio.mjs | 13 ++++ 10 files changed, 160 insertions(+), 73 deletions(-) delete mode 100644 packages/web/repl.mjs create mode 100644 packages/web/vite.config.js create mode 100644 packages/web/web.mjs diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 30504a5b..78241f74 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -18,6 +18,7 @@ export * from './util.mjs'; export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; +export * from './cyclist.mjs'; export * from './logger.mjs'; export * from './draw.mjs'; export * from './animate.mjs'; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index de979258..abb9ef80 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -17,23 +17,15 @@ export function repl({ }) { const scheduler = new Cyclist({ interval, - onTrigger: async (hap, deadline, duration, cps) => { - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps); - } - } catch (err) { - logger(`[cyclist] error: ${err.message}`, 'error'); - } - }, + onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, getTime, onToggle, }); + const setPattern = (pattern, autostart = true) => { + pattern = editPattern?.(pattern) || pattern; + scheduler.setPattern(pattern, autostart); + }; const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); @@ -43,8 +35,7 @@ export function repl({ let { pattern } = await _evaluate(code, transpiler); logger(`[eval] code updated`); - pattern = editPattern?.(pattern) || pattern; - scheduler.setPattern(pattern, autostart); + setPattern(pattern, autostart); afterEval?.({ code, pattern }); return pattern; } catch (err) { @@ -61,5 +52,21 @@ export function repl({ setCps, setcps: setCps, }); - return { scheduler, evaluate, start, stop, pause, setCps }; + return { scheduler, evaluate, start, stop, pause, setCps, setPattern }; } + +export const getTrigger = + ({ getTime, defaultOutput }) => + async (hap, deadline, duration, cps) => { + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps); + } + } catch (err) { + logger(`[cyclist] error: ${err.message}`, 'error'); + } + }; diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 648499e6..3f9228d9 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -190,3 +190,8 @@ export function minify(thing) { } return strudel.reify(thing); } + +// calling this function will cause patterns to parse strings as mini notation by default +export function miniAllStrings() { + strudel.setStringParser(mini); +} diff --git a/packages/web/README.md b/packages/web/README.md index c70a5df7..53609c3e 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -4,16 +4,20 @@ This package provides an easy to use bundle of multiple strudel packages for the ## Usage -```js -import { repl } from '@strudel/web'; +Minimal example: -const strudel = repl(); +```js +import '@strudel/web'; document.getElementById('play').addEventListener('click', - () => strudel.evaluate('note("c a f e").jux(rev)') -); + () => note("c a f e").play() +) ``` +As soon as you `import '@strudel/web'`, all strudel functions will be available in the global scope. +In this case, we are using `note` to create a pattern. +To actually play the pattern, you have to append `.play()` to the end. + Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event. ### Loading samples @@ -21,15 +25,28 @@ Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/ By default, no external samples are loaded, but you can add them like this: ```js -import { repl, samples } from '@strudel/web'; +import { prebake } from '@strudel/web'; -const strudel = repl({ - prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), -}); +prebake(() => samples('github:tidalcycles/Dirt-Samples/master')) document.getElementById('play').addEventListener('click', - () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)') -); + () => s("bd sd").play() +) ``` You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples). + +### Evaluating Code + +Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern. +This is called evaluation: Taking a piece of code and executing it on the fly. + +To do that, you can use the `evaluate` function: + +```js +import '@strudel/web'; + +document.getElementById('play').addEventListener('click', + () => evaluate('note("c a f e").jux(rev)') +); +``` diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html index a7954e64..853e02db 100644 --- a/packages/web/examples/repl-example/index.html +++ b/packages/web/examples/repl-example/index.html @@ -13,17 +13,16 @@ diff --git a/packages/web/package.json b/packages/web/package.json index 11e9153e..be1a4bb9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@strudel/web", "version": "0.8.0", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", - "main": "repl.mjs", + "main": "web.mjs", "publishConfig": { "main": "dist/index.js", "module": "dist/index.mjs" diff --git a/packages/web/repl.mjs b/packages/web/repl.mjs deleted file mode 100644 index f86459b1..00000000 --- a/packages/web/repl.mjs +++ /dev/null @@ -1,38 +0,0 @@ -export * from '@strudel.cycles/core'; -export * from '@strudel.cycles/webaudio'; -export * from '@strudel.cycles/soundfonts'; -export * from '@strudel.cycles/transpiler'; -export * from '@strudel.cycles/mini'; -export * from '@strudel.cycles/tonal'; -export * from '@strudel.cycles/webaudio'; -import { repl as _repl, evalScope, controls } from '@strudel.cycles/core'; -import { initAudioOnFirstClick, getAudioContext, registerSynthSounds, webaudioOutput } from '@strudel.cycles/webaudio'; -import { registerSoundfonts } from '@strudel.cycles/soundfonts'; -import { transpiler } from '@strudel.cycles/transpiler'; - -async function prebake(userPrebake) { - const loadModules = evalScope( - evalScope, - controls, - import('@strudel.cycles/core'), - import('@strudel.cycles/mini'), - import('@strudel.cycles/tonal'), - import('@strudel.cycles/webaudio'), - ); - await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts(), userPrebake?.()]); -} - -export function repl(options = {}) { - const prebaked = prebake(options?.prebake); - initAudioOnFirstClick(); - return _repl({ - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - transpiler, - ...options, - beforeEval: async (args) => { - options?.beforeEval?.(args); - await prebaked; - }, - }); -} diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js new file mode 100644 index 00000000..ffa2ad27 --- /dev/null +++ b/packages/web/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'web.mjs'), + formats: ['es', 'cjs'], + fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]), + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/web/web.mjs b/packages/web/web.mjs new file mode 100644 index 00000000..e546a8f9 --- /dev/null +++ b/packages/web/web.mjs @@ -0,0 +1,64 @@ +export * from '@strudel.cycles/core'; +export * from '@strudel.cycles/webaudio'; +export * from '@strudel.cycles/soundfonts'; +export * from '@strudel.cycles/transpiler'; +export * from '@strudel.cycles/mini'; +export * from '@strudel.cycles/tonal'; +export * from '@strudel.cycles/webaudio'; +import { Pattern, evalScope, controls } from '@strudel.cycles/core'; +import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio'; +import { registerSoundfonts } from '@strudel.cycles/soundfonts'; +import { evaluate as _evaluate } from '@strudel.cycles/transpiler'; +import { miniAllStrings } from '@strudel.cycles/mini'; + +// init logic +export async function defaultPrebake() { + const loadModules = evalScope( + evalScope, + controls, + import('@strudel.cycles/core'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/webaudio'), + { hush, evaluate }, + ); + await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]); +} + +// when this function finishes, everything is initialized +let initDone; + +let scheduler; +export function init(options = {}) { + initAudioOnFirstClick(); + miniAllStrings(); + const { prebake, ...schedulerOptions } = options; + + initDone = (async () => { + await defaultPrebake(); + await prebake?.(); + })(); + scheduler = webaudioScheduler(schedulerOptions); +} + +// this method will play the pattern on the default scheduler +Pattern.prototype.play = function () { + if (!scheduler) { + throw new Error('.play: no scheduler found. Have you called init?'); + } + initDone.then(() => { + scheduler.setPattern(this, true); + }); + return this; +}; + +// stop playback +export function hush() { + scheduler.stop(); +} + +// evaluate and play the given code using the transpiler +export async function evaluate(code, autoplay = true) { + const { pattern } = await _evaluate(code); + autoplay && pattern.play(); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c75bde0b..6b069a85 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -243,3 +243,16 @@ Pattern.prototype.webaudio = function () { // TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ? return this.onTrigger(webaudioOutputTrigger); }; + +export function webaudioScheduler(options = {}) { + options = { + getTime: () => getAudioContext().currentTime, + defaultOutput: webaudioOutput, + ...options, + }; + const { defaultOutput, getTime } = options; + return new strudel.Cyclist({ + ...options, + onTrigger: strudel.getTrigger({ defaultOutput, getTime }), + }); +} From 6e02bf59e94024985d7f9f3dc2bdd3365a8f6a72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 11:44:55 +0200 Subject: [PATCH 25/36] Revert "refactor: remove old draw logic" This reverts commit 95719654f3bfc5d3cc98c33816b402e2c0e38bd7. --- packages/core/draw.mjs | 38 +++++++++- packages/core/index.mjs | 1 + packages/core/pianoroll.mjs | 134 +++++++++++++++++++++++++++++++++++- packages/core/repl.mjs | 2 + packages/core/time.mjs | 11 +++ packages/core/ui.mjs | 23 +++++++ website/src/repl/Repl.jsx | 5 +- 7 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 packages/core/time.mjs diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 58b14040..1b5c87e7 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern } from './index.mjs'; +import { Pattern, getTime, State, TimeSpan } from './index.mjs'; export const getDrawContext = (id = 'test-canvas') => { let canvas = document.querySelector('#' + id); @@ -19,9 +19,45 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; +Pattern.prototype.draw = function (callback, { from, to, onQuery }) { + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + const ctx = getDrawContext(); + let cycle, + events = []; + const animate = (time) => { + const t = getTime(); + if (from !== undefined && to !== undefined) { + const currentCycle = Math.floor(t); + if (cycle !== currentCycle) { + cycle = currentCycle; + const begin = currentCycle + from; + const end = currentCycle + to; + setTimeout(() => { + events = this.query(new State(new TimeSpan(begin, end))) + .filter(Boolean) + .filter((event) => event.part.begin.equals(event.whole.begin)); + onQuery?.(events); + }, 0); + } + } + callback(ctx, events, t, time); + window.strudelAnimation = requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); + return this; +}; + export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + if (window.strudelScheduler) { + clearInterval(window.strudelScheduler); + } }; Pattern.prototype.onPaint = function (onPaint) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 78241f74..16ef3be4 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -20,6 +20,7 @@ export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; export * from './logger.mjs'; +export * from './time.mjs'; export * from './draw.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 336bd428..59476015 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, noteToMidi, freqToMidi } from './index.mjs'; +import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -29,6 +29,134 @@ const getValue = (e) => { return value; }; +Pattern.prototype.pianoroll = function ({ + cycles = 4, + playhead = 0.5, + overscan = 1, + flipTime = 0, + flipValues = 0, + hideNegative = false, + // inactive = '#C9E597', + // inactive = '#FFCA28', + inactive = '#7491D2', + active = '#FFCA28', + // background = '#2A3236', + background = 'transparent', + smear = 0, + playheadColor = 'white', + minMidi = 10, + maxMidi = 90, + autorange = 0, + timeframe: timeframeProp, + fold = 0, + vertical = 0, +} = {}) { + const ctx = getDrawContext(); + const w = ctx.canvas.width; + const h = ctx.canvas.height; + let from = -cycles * playhead; + let to = cycles * (1 - playhead); + + if (timeframeProp) { + console.warn('timeframe is deprecated! use from/to instead'); + from = 0; + to = timeframeProp; + } + const timeAxis = vertical ? h : w; + const valueAxis = vertical ? w : h; + let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time + const timeExtent = to - from; // number of seconds that fit inside the canvas frame + const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values + let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true + let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true + let foldValues = []; + flipTime && timeRange.reverse(); + flipValues && valueRange.reverse(); + + this.draw( + (ctx, events, t) => { + ctx.fillStyle = background; + ctx.globalAlpha = 1; // reset! + if (!smear) { + ctx.clearRect(0, 0, w, h); + ctx.fillRect(0, 0, w, h); + } + const inFrame = (event) => + (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from; + events.filter(inFrame).forEach((event) => { + const isActive = event.whole.begin <= t && event.whole.end > t; + ctx.fillStyle = event.context?.color || inactive; + ctx.strokeStyle = event.context?.color || active; + ctx.globalAlpha = event.context.velocity ?? 1; + const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + const value = getValue(event); + const valuePx = scale( + fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, + ...valueRange, + ); + let margin = 0; + const offset = scale(t / timeExtent, ...timeRange); + let coords; + if (vertical) { + coords = [ + valuePx + 1 - (flipValues ? barThickness : 0), // x + timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y + barThickness - 2, // width + durationPx - 2, // height + ]; + } else { + coords = [ + timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x + valuePx + 1 - (flipValues ? 0 : barThickness), // y + durationPx - 2, // widith + barThickness - 2, // height + ]; + } + isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); + }); + ctx.globalAlpha = 1; // reset! + const playheadPosition = scale(-from / timeExtent, ...timeRange); + // draw playhead + ctx.strokeStyle = playheadColor; + ctx.beginPath(); + if (vertical) { + ctx.moveTo(0, playheadPosition); + ctx.lineTo(valueAxis, playheadPosition); + } else { + ctx.moveTo(playheadPosition, 0); + ctx.lineTo(playheadPosition, valueAxis); + } + ctx.stroke(); + }, + { + from: from - overscan, + to: to + overscan, + onQuery: (events) => { + const { min, max, values } = events.reduce( + ({ min, max, values }, e) => { + const v = getValue(e); + return { + min: v < min ? v : min, + max: v > max ? v : max, + values: values.includes(v) ? values : [...values, v], + }; + }, + { min: Infinity, max: -Infinity, values: [] }, + ); + if (autorange) { + minMidi = min; + maxMidi = max; + valueExtent = maxMidi - minMidi + 1; + } + foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); + barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; + }, + }, + ); + return this; +}; + // this function allows drawing a pianoroll without ties to Pattern.prototype // it will probably replace the above in the future export function pianoroll({ @@ -170,11 +298,11 @@ Pattern.prototype.punchcard = function (options) { ); }; -Pattern.prototype.pianoroll = function (options) { +/* Pattern.prototype.pianoroll = function (options) { return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), ); -}; +}; */ export function drawPianoroll(options) { const { drawTime, ...rest } = options; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index abb9ef80..c88145d1 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,6 +1,7 @@ import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; +import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; export function repl({ @@ -26,6 +27,7 @@ export function repl({ pattern = editPattern?.(pattern) || pattern; scheduler.setPattern(pattern, autostart); }; + setTime(() => scheduler.now()); // TODO: refactor? const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); diff --git a/packages/core/time.mjs b/packages/core/time.mjs new file mode 100644 index 00000000..80daaf53 --- /dev/null +++ b/packages/core/time.mjs @@ -0,0 +1,11 @@ +let time; +export function getTime() { + if (!time) { + throw new Error('no time set! use setTime to define a time source'); + } + return time(); +} + +export function setTime(func) { + time = func; +} diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index cc148553..df8230ec 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -4,6 +4,19 @@ Copyright (C) 2022 Strudel contributors - see . */ +import { getTime } from './time.mjs'; + +function frame(callback) { + if (window.strudelAnimation) { + cancelAnimationFrame(window.strudelAnimation); + } + const animate = (animationTime) => { + callback(animationTime, getTime()); + window.strudelAnimation = requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); +} + export const backgroundImage = function (src, animateOptions = {}) { const container = document.getElementById('code'); const bg = 'background-image:url(' + src + ');background-size:contain;'; @@ -15,8 +28,18 @@ export const backgroundImage = function (src, animateOptions = {}) { className: () => (container.className = value + ' ' + initialClassName), })[option](); }; + const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function'); const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string'); stringOptions.forEach(([option, value]) => handleOption(option, value)); + + if (funcOptions.length === 0) { + return; + } + frame((_, t) => + funcOptions.forEach(([option, value]) => { + handleOption(option, value(t)); + }), + ); }; export const cleanupUi = () => { diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index d8270b16..4ad387fe 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,9 +50,10 @@ const modulesLoading = evalScope( const presets = prebake(); -let drawContext; +let drawContext, clearCanvas; if (typeof window !== 'undefined') { drawContext = getDrawContext(); + clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); } const getTime = () => getAudioContext().currentTime; @@ -207,7 +208,7 @@ export function Repl({ embedded = false }) { const handleShuffle = async () => { const { code, name } = getRandomTune(); logger(`[repl] ✨ loading random tune "${name}"`); - cleanupDraw(); + clearCanvas(); resetLoadedSounds(); scheduler.setCps(1); await prebake(); // declare default samples From 6089849d351aba12f72b94ff93fe660ce8c0e19b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 11:53:26 +0200 Subject: [PATCH 26/36] rename init to initStrudel + add to window --- packages/web/examples/repl-example/index.html | 4 ++-- packages/web/web.mjs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html index 853e02db..12003e4b 100644 --- a/packages/web/examples/repl-example/index.html +++ b/packages/web/examples/repl-example/index.html @@ -13,8 +13,8 @@ From 087f57435542f3194ab2073b6e9de07c8ee050e4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 12:49:45 +0200 Subject: [PATCH 31/36] update web readme + add more html examples --- packages/web/README.md | 56 ++++++++++++++++++++++------- packages/web/examples/evaluate.html | 10 ++++++ packages/web/examples/minimal.html | 10 ++++++ packages/web/examples/samples.html | 12 +++++++ pnpm-lock.yaml | 3 -- 5 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 packages/web/examples/evaluate.html create mode 100644 packages/web/examples/minimal.html create mode 100644 packages/web/examples/samples.html diff --git a/packages/web/README.md b/packages/web/README.md index 53609c3e..f1ed2bbc 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -4,30 +4,47 @@ This package provides an easy to use bundle of multiple strudel packages for the ## Usage -Minimal example: +Save this code as a `.html` file and double click it: -```js -import '@strudel/web'; +```html + + + + ``` -As soon as you `import '@strudel/web'`, all strudel functions will be available in the global scope. -In this case, we are using `note` to create a pattern. +With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server. + +As soon as you call `initStrudel()`, all strudel functions are made available. +In this case, we are using the `note` function to create a pattern. To actually play the pattern, you have to append `.play()` to the end. Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event. +### Via npm + +If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like: + +```js +import { initStrudel } from '@strudel/web'; +``` + +The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server. + ### Loading samples By default, no external samples are loaded, but you can add them like this: ```js -import { prebake } from '@strudel/web'; - -prebake(() => samples('github:tidalcycles/Dirt-Samples/master')) +initStrudel({ + prebake: () => samples('github:tidalcycles/Dirt-Samples/master'), +}); document.getElementById('play').addEventListener('click', () => s("bd sd").play() @@ -44,9 +61,22 @@ This is called evaluation: Taking a piece of code and executing it on the fly. To do that, you can use the `evaluate` function: ```js -import '@strudel/web'; - +initStrudel(); document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)') ); +document.getElementById('play').addEventListener('stop', + () => hush() +); ``` + +### Double vs Single Quotes + +There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`. +In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns. +In `@strudel/web`, it does not matter which types of quotes you're using. +There will probably be an escapte hatch for that in the future. + +## More Examples + +Check out the examples folder for more examples, both using plain html and vite! \ No newline at end of file diff --git a/packages/web/examples/evaluate.html b/packages/web/examples/evaluate.html new file mode 100644 index 00000000..77e6431f --- /dev/null +++ b/packages/web/examples/evaluate.html @@ -0,0 +1,10 @@ + + + + diff --git a/packages/web/examples/minimal.html b/packages/web/examples/minimal.html new file mode 100644 index 00000000..58487009 --- /dev/null +++ b/packages/web/examples/minimal.html @@ -0,0 +1,10 @@ + + + + diff --git a/packages/web/examples/samples.html b/packages/web/examples/samples.html new file mode 100644 index 00000000..4a839581 --- /dev/null +++ b/packages/web/examples/samples.html @@ -0,0 +1,12 @@ + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee27ce0d..9710374f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -479,9 +479,6 @@ importers: '@strudel.cycles/mini': specifier: workspace:* version: link:../mini - '@strudel.cycles/soundfonts': - specifier: workspace:* - version: link:../soundfonts '@strudel.cycles/tonal': specifier: workspace:* version: link:../tonal From a1ec8545fbc71a934716052c3f3f3d8524363874 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 May 2023 13:03:06 +0200 Subject: [PATCH 32/36] format --- packages/web/examples/samples.html | 2 +- packages/web/web.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/examples/samples.html b/packages/web/examples/samples.html index 4a839581..8b967ec2 100644 --- a/packages/web/examples/samples.html +++ b/packages/web/examples/samples.html @@ -3,7 +3,7 @@