diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 512c873a..c06ac4a0 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -20,7 +20,8 @@ import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs'; -import { updateWidgets, sliderPlugin } from './slider.mjs'; +import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; const extensions = { @@ -72,6 +73,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo ...initialSettings, javascript(), sliderPlugin, + widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything closeBrackets(), @@ -187,7 +189,10 @@ export class StrudelMirror { // remember for when highlighting is toggled on this.miniLocations = options.meta?.miniLocations; this.widgets = options.meta?.widgets; - updateWidgets(this.editor, this.widgets); + const sliders = this.widgets.filter((w) => w.type === 'slider'); + updateSliderWidgets(this.editor, sliders); + const widgets = this.widgets.filter((w) => w.type !== 'slider'); + updateWidgets(this.editor, widgets); updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); this.adjustDrawTime(); diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 8f2d1630..3a5f2a23 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -3,3 +3,4 @@ export * from './highlight.mjs'; export * from './flash.mjs'; export * from './slider.mjs'; export * from './themes.mjs'; +export * from './widget.mjs'; diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 0b39ebdd..133c4148 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -46,6 +46,7 @@ "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/transpiler": "workspace:*", "@uiw/codemirror-themes": "^4.21.21", "@uiw/codemirror-themes-all": "^4.21.21", "nanostores": "^0.9.5" diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index a46f0e1e..72f95125 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,6 +1,6 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; -import { StateEffect, StateField } from '@codemirror/state'; +import { StateEffect } from '@codemirror/state'; export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; @@ -60,19 +60,21 @@ export class SliderWidget extends WidgetType { } } -export const setWidgets = StateEffect.define(); +export const setSliderWidgets = StateEffect.define(); -export const updateWidgets = (view, widgets) => { - view.dispatch({ effects: setWidgets.of(widgets) }); +export const updateSliderWidgets = (view, widgets) => { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); }; -function getWidgets(widgetConfigs, view) { - return widgetConfigs.map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); +function getSliders(widgetConfigs, view) { + return widgetConfigs + .filter((w) => w.type === 'slider') + .map(({ from, to, value, min, max, step }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view), + side: 0, + }).range(from /* , to */); + }); } export const sliderPlugin = ViewPlugin.fromClass( @@ -99,8 +101,8 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setWidgets)) { - this.decorations = Decoration.set(getWidgets(e.value, update.view)); + if (e.is(setSliderWidgets)) { + this.decorations = Decoration.set(getSliders(e.value, update.view)); } } }); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs new file mode 100644 index 00000000..facfe1c2 --- /dev/null +++ b/packages/codemirror/widget.mjs @@ -0,0 +1,122 @@ +import { StateEffect, StateField } from '@codemirror/state'; +import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { getWidgetID, registerWidgetType } from '@strudel/transpiler'; +import { Pattern } from '@strudel/core'; + +export const addWidget = StateEffect.define({ + map: ({ from, to }, change) => { + return { from: change.mapPos(from), to: change.mapPos(to) }; + }, +}); + +export const updateWidgets = (view, widgets) => { + view.dispatch({ effects: addWidget.of(widgets) }); +}; + +function getWidgets(widgetConfigs) { + return ( + widgetConfigs + // codemirror throws an error if we don't sort + .sort((a, b) => a.to - b.to) + .map((widgetConfig) => { + return Decoration.widget({ + widget: new BlockWidget(widgetConfig), + side: 0, + block: true, + }).range(widgetConfig.to); + }) + ); +} + +const widgetField = StateField.define( + /* */ { + create() { + return Decoration.none; + }, + update(widgets, tr) { + widgets = widgets.map(tr.changes); + for (let e of tr.effects) { + if (e.is(addWidget)) { + try { + widgets = widgets.update({ + filter: () => false, + add: getWidgets(e.value), + }); + } catch (error) { + console.log('err', error); + } + } + } + return widgets; + }, + provide: (f) => EditorView.decorations.from(f), + }, +); + +const widgetElements = {}; +export function setWidget(id, el) { + widgetElements[id] = el; + el.id = id; +} + +export class BlockWidget extends WidgetType { + constructor(widgetConfig) { + super(); + this.widgetConfig = widgetConfig; + } + eq() { + return true; + } + toDOM() { + const id = getWidgetID(this.widgetConfig); + const el = widgetElements[id]; + return el; + } + ignoreEvent(e) { + return true; + } +} + +export const widgetPlugin = [widgetField]; + +// widget implementer API to create a new widget type +export function registerWidget(type, fn) { + registerWidgetType(type); + if (fn) { + Pattern.prototype[type] = function (id, options = { fold: 1 }) { + // fn is expected to create a dom element and call setWidget(id, el); + // fn should also return the pattern + return fn(id, options, this); + }; + } +} + +// wire up @strudel/draw functions + +function getCanvasWidget(id, options = {}) { + const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options; + let canvas = document.getElementById(id) || document.createElement('canvas'); + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + setWidget(id, canvas); + return canvas; +} + +registerWidget('_pianoroll', (id, options = {}, pat) => { + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.pianoroll({ fold: 1, ...options, ctx, id }); +}); + +/* registerWidget('_spiral', (id, options = {}, pat) => { + options = { width: 200, height: 200, size: 36, ...options }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.spiral({ ...options, ctx, id }); +}); */ + +registerWidget('_scope', (id, options = {}, pat) => { + options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.scope({ ...options, ctx, id }); +}); diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index 9343e062..86ceb286 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;'; @@ -35,11 +22,6 @@ export const backgroundImage = function (src, animateOptions = {}) { if (funcOptions.length === 0) { return; } - frame((_, t) => - funcOptions.forEach(([option, value]) => { - handleOption(option, value(t)); - }), - ); }; export const cleanupUi = () => { diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 2ea531cf..4e84878f 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -29,14 +29,22 @@ export const getDrawContext = (id = 'test-canvas', options) => { return canvas.getContext(contextType); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { +let animationFrames = {}; +function stopAnimationFrame(id) { + if (animationFrames[id] !== undefined) { + cancelAnimationFrame(animationFrames[id]); + delete animationFrames[id]; + } +} +function stopAllAnimations() { + Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id)); +} +Pattern.prototype.draw = function (callback, { id = 'std', from, to, onQuery, ctx } = {}) { if (typeof window === 'undefined') { return this; } - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const ctx = getDrawContext(); + stopAnimationFrame(id); + ctx = ctx || getDrawContext(); let cycle, events = []; const animate = (time) => { @@ -56,7 +64,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { } } callback(ctx, events, t, time); - window.strudelAnimation = requestAnimationFrame(animate); + animationFrames[id] = requestAnimationFrame(animate); }; requestAnimationFrame(animate); return this; @@ -64,18 +72,16 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { // this is a more generic helper to get a rendering callback for the currently active haps // TODO: this misses events that are prolonged with clip or duration (would need state) -Pattern.prototype.onFrame = function (fn, offset = 0) { +Pattern.prototype.onFrame = function (id, fn, offset = 0) { if (typeof window === 'undefined') { return this; } - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } + stopAnimationFrame(id); const animate = () => { const t = getTime() + offset; const haps = this.queryArc(t, t); fn(haps, t, this); - window.strudelAnimation = requestAnimationFrame(animate); + animationFrames[id] = requestAnimationFrame(animate); }; requestAnimationFrame(animate); return this; @@ -84,9 +90,7 @@ Pattern.prototype.onFrame = function (fn, offset = 0) { export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } + stopAllAnimations(); if (window.strudelScheduler) { clearInterval(window.strudelScheduler); } diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index 74b1480e..2ec2d7ee 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -18,7 +18,13 @@ const getValue = (e) => { } note = note ?? n; if (typeof note === 'string') { - return noteToMidi(note); + try { + // TODO: n(run(32)).scale("D:minor") fails when trying to query negative time.. + return noteToMidi(note); + } catch (err) { + // console.warn(`error converting note to midi: ${err}`); // this spams to crazy + return 0; + } } if (typeof note === 'number') { return note; @@ -30,7 +36,7 @@ const getValue = (e) => { }; Pattern.prototype.pianoroll = function (options = {}) { - let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options; + let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx, id } = options; let from = -cycles * playhead; let to = cycles * (1 - playhead); @@ -49,6 +55,8 @@ Pattern.prototype.pianoroll = function (options = {}) { { from: from - overscan, to: to + overscan, + ctx, + id, }, ); return this; diff --git a/packages/draw/spiral.mjs b/packages/draw/spiral.mjs index 00bd62ec..4762e843 100644 --- a/packages/draw/spiral.mjs +++ b/packages/draw/spiral.mjs @@ -49,7 +49,7 @@ function spiralSegment(options) { ctx.stroke(); } -Pattern.prototype.spiral = function (options = {}) { +function drawSpiral(options) { const { stretch = 1, size = 80, @@ -65,54 +65,58 @@ Pattern.prototype.spiral = function (options = {}) { colorizeInactive = 0, fade = true, // logSpiral = true, + ctx, + time, + haps, + drawTime, } = options; - function spiral({ ctx, time, haps, drawTime }) { - const [w, h] = [ctx.canvas.width, ctx.canvas.height]; - ctx.clearRect(0, 0, w * 2, h * 2); - const [cx, cy] = [w / 2, h / 2]; - const settings = { - margin: size / stretch, - cx, - cy, - stretch, - cap, - thickness, - }; + const [w, h] = [ctx.canvas.width, ctx.canvas.height]; + ctx.clearRect(0, 0, w * 2, h * 2); + const [cx, cy] = [w / 2, h / 2]; + const settings = { + margin: size / stretch, + cx, + cy, + stretch, + cap, + thickness, + }; - const playhead = { - ...settings, - thickness: playheadThickness, - from: inset - playheadLength, - to: inset, - color: playheadColor, - }; + const playhead = { + ...settings, + thickness: playheadThickness, + from: inset - playheadLength, + to: inset, + color: playheadColor, + }; - const [min] = drawTime; - const rotate = steady * time; - haps.forEach((hap) => { - const isActive = hap.whole.begin <= time && hap.endClipped > time; - const from = hap.whole.begin - time + inset; - const to = hap.endClipped - time + inset - padding; - const { color } = hap.context; - const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; - spiralSegment({ - ctx, - ...settings, - from, - to, - rotate, - color: colorizeInactive || isActive ? color : inactiveColor, - fromOpacity: opacity, - toOpacity: opacity, - }); - }); + const [min] = drawTime; + const rotate = steady * time; + haps.forEach((hap) => { + const isActive = hap.whole.begin <= time && hap.endClipped > time; + const from = hap.whole.begin - time + inset; + const to = hap.endClipped - time + inset - padding; + const { color } = hap.context; + const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; spiralSegment({ ctx, - ...playhead, + ...settings, + from, + to, rotate, + color: colorizeInactive || isActive ? color : inactiveColor, + fromOpacity: opacity, + toOpacity: opacity, }); - } + }); + spiralSegment({ + ctx, + ...playhead, + rotate, + }); +} - return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime })); +Pattern.prototype.spiral = function (options = {}) { + return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options })); }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3e6448d9..cf9bd181 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -215,35 +215,35 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { return reverbs[orbit]; } -export let analyser, analyserData /* s = {} */; +export let analysers = {}, + analysersData = {}; -export function getAnalyser(/* orbit, */ fftSize = 2048) { - if (!analyser /*s [orbit] */) { +export function getAnalyserById(id, fftSize = 1024) { + if (!analysers[id]) { + // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; // getDestination().connect(analyserNode); - analyser /* s[orbit] */ = analyserNode; - //analyserData = new Uint8Array(analyser.frequencyBinCount); - analyserData = new Float32Array(analyser.frequencyBinCount); + analysers[id] = analyserNode; + analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); } - if (analyser /* s[orbit] */.fftSize !== fftSize) { - analyser /* s[orbit] */.fftSize = fftSize; - //analyserData = new Uint8Array(analyser.frequencyBinCount); - analyserData = new Float32Array(analyser.frequencyBinCount); + if (analysers[id].fftSize !== fftSize) { + analysers[id].fftSize = fftSize; + analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); } - return analyser /* s[orbit] */; + return analysers[id]; } -export function getAnalyzerData(type = 'time') { +export function getAnalyzerData(type = 'time', id = 1) { const getter = { - time: () => analyser?.getFloatTimeDomainData(analyserData), - frequency: () => analyser?.getFloatFrequencyData(analyserData), + time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]), + frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]), }[type]; if (!getter) { throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); } getter(); - return analyserData; + return analysersData[id]; } function effectSend(input, effect, wet) { @@ -256,6 +256,8 @@ function effectSend(input, effect, wet) { export function resetGlobalEffects() { delays = {}; reverbs = {}; + analysers = {}; + analysersData = {}; } export const superdough = async (value, deadline, hapDuration) => { @@ -512,8 +514,8 @@ export const superdough = async (value, deadline, hapDuration) => { // analyser let analyserSend; if (analyze) { - const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5)); - analyserSend = effectSend(post, analyserNode, analyze); + const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); + analyserSend = effectSend(post, analyserNode, 1); } // connect chain elements together diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index a49b353d..d4b474d9 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -3,6 +3,11 @@ import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; +let widgetMethods = []; +export function registerWidgetType(type) { + widgetMethods.push(type); +} + export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -34,7 +39,7 @@ export function transpiler(input, options = {}) { emitMiniLocations && collectMiniLocations(value, node); return this.replace(miniWithLocation(value, node)); } - if (isWidgetFunction(node)) { + if (isSliderFunction(node)) { emitWidgets && widgets.push({ from: node.arguments[0].start, @@ -43,8 +48,18 @@ export function transpiler(input, options = {}) { min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, step: node.arguments[3]?.value, + type: 'slider', }); - return this.replace(widgetWithLocation(node)); + return this.replace(sliderWithLocation(node)); + } + if (isWidgetMethod(node)) { + const widgetConfig = { + to: node.end, + index: widgets.length, + type: node.callee.property.name, + }; + emitWidgets && widgets.push(widgetConfig); + return this.replace(widgetWithLocation(node, widgetConfig)); } if (isBareSamplesCall(node, parent)) { return this.replace(withAwait(node)); @@ -108,11 +123,15 @@ function miniWithLocation(value, node) { // these functions are connected to @strudel/codemirror -> slider.mjs // maybe someday there will be pluggable transpiler functions, then move this there -function isWidgetFunction(node) { +function isSliderFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } -function widgetWithLocation(node) { +function isWidgetMethod(node) { + return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); +} + +function sliderWithLocation(node) { const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) @@ -125,6 +144,27 @@ function widgetWithLocation(node) { return node; } +export function getWidgetID(widgetConfig) { + // the widget id is used as id for the dom element + as key for eventual resources + // for example, for each scope widget, a new analyser + buffer (large) is created + // that means, if we use the index index of line position as id, less garbage is generated + // return `widget_${widgetConfig.to}`; // more gargabe + //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage + return `widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage +} + +function widgetWithLocation(node, widgetConfig) { + const id = getWidgetID(widgetConfig); + // add loc as identifier to first argument + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: id, + raw: id, + }); + return node; +} + function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index 0371366c..c9ee1f33 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,19 +1,37 @@ import { Pattern, clamp } from '@strudel/core'; import { getDrawContext } from '../draw/index.mjs'; -import { analyser, getAnalyzerData } from 'superdough'; +import { analysers, getAnalyzerData } from 'superdough'; export function drawTimeScope( analyser, - { align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {}, + { + align = true, + color = 'white', + thickness = 3, + scale = 0.25, + pos = 0.75, + trigger = 0, + ctx = getDrawContext(), + id = 1, + } = {}, ) { - const ctx = getDrawContext(); - const dataArray = getAnalyzerData('time'); - ctx.lineWidth = thickness; ctx.strokeStyle = color; + let canvas = ctx.canvas; + + if (!analyser) { + // if analyser is undefined, draw straight line + // it may be undefined when no sound has been played yet + ctx.beginPath(); + let y = pos * canvas.height; + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + return; + } + const dataArray = getAnalyzerData('time', id); ctx.beginPath(); - let canvas = ctx.canvas; const bufferSize = analyser.frequencyBinCount; let triggerIndex = align @@ -39,10 +57,17 @@ export function drawTimeScope( export function drawFrequencyScope( analyser, - { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {}, + { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {}, ) { - const dataArray = getAnalyzerData('frequency'); - const ctx = getDrawContext(); + if (!analyser) { + ctx.beginPath(); + let y = pos * canvas.height; + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + return; + } + const dataArray = getAnalyzerData('frequency', id); const canvas = ctx.canvas; ctx.fillStyle = color; @@ -61,8 +86,7 @@ export function drawFrequencyScope( } } -function clearScreen(smear = 0, smearRGB = `0,0,0`) { - const ctx = getDrawContext(); +function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) { if (!smear) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } else { @@ -84,10 +108,14 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`) { * s("sawtooth").fscope() */ Pattern.prototype.fscope = function (config = {}) { - return this.analyze(1).draw(() => { - clearScreen(config.smear); - analyser && drawFrequencyScope(analyser, config); - }); + let id = config.id ?? 1; + return this.analyze(id).draw( + () => { + clearScreen(config.smear, '0,0,0', config.ctx); + analysers[id] && drawFrequencyScope(analysers[id], config); + }, + { id }, + ); }; /** @@ -105,10 +133,14 @@ Pattern.prototype.fscope = function (config = {}) { * s("sawtooth").scope() */ Pattern.prototype.tscope = function (config = {}) { - return this.analyze(1).draw(() => { - clearScreen(config.smear); - analyser && drawTimeScope(analyser, config); - }); + let id = config.id ?? 1; + return this.analyze(id).draw( + () => { + clearScreen(config.smear, '0,0,0', config.ctx); + drawTimeScope(analysers[id], config); + }, + { id }, + ); }; Pattern.prototype.scope = Pattern.prototype.tscope; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b123e2b7..033c6a7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler '@uiw/codemirror-themes': specifier: ^4.21.21 version: 4.21.21(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0) @@ -826,7 +829,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} @@ -3061,7 +3064,6 @@ packages: /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} @@ -3091,7 +3093,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /@jsdoc/salty@0.2.3: resolution: {integrity: sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==}