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