From e3010907041b91c75433b8d62c01679fcca643a4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:27:30 +0200 Subject: [PATCH 1/4] pianoroll improvements: - add label / activeLabel controls - add new pianoroll options: - fill - fillActive - strokeActive - stroke - hideInactive - colorizeInactive - fontFamily - add wordfall method - fix: some haps were drawn with wrong length - pianoroll labels now use set fontFamily - hide fold gutter --- packages/core/controls.mjs | 3 + packages/core/pianoroll.mjs | 131 ++++++++++++------- packages/react/src/hooks/usePatternFrame.mjs | 4 +- packages/react/src/hooks/useStrudel.mjs | 5 +- website/src/repl/Repl.css | 4 + website/src/repl/Repl.jsx | 5 + 6 files changed, 101 insertions(+), 51 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 78e517dc..717a8353 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -506,6 +506,9 @@ const generic_params = [ * */ ['lsize'], + // label for pianoroll + ['activeLabel'], + [['label', 'activeLabel']], // ['lfo'], // ['lfocutoffint'], // ['lfodelay'], diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index ce5020c9..1592ab96 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -191,6 +191,13 @@ export function pianoroll({ fold = 0, vertical = 0, labels = false, + fill, + fillActive = false, + strokeActive = true, + stroke, + hideInactive = 0, + colorizeInactive = 1, + fontFamily, ctx, } = {}) { const w = ctx.canvas.width; @@ -241,51 +248,77 @@ export function pianoroll({ ctx.clearRect(0, 0, w, h); ctx.fillRect(0, 0, w, h); } - /* const inFrame = (event) => - (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */ - haps - // .filter(inFrame) - .forEach((event) => { - const isActive = event.whole.begin <= time && event.endClipped > time; - const color = event.value?.color || event.context?.color; - ctx.fillStyle = color || inactive; - ctx.strokeStyle = color || active; - ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 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(time / 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); - if (labels) { - const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); - ctx.font = `${barThickness * 0.75}px monospace`; - ctx.strokeStyle = 'black'; - ctx.fillStyle = isActive ? 'white' : 'black'; - ctx.textBaseline = 'top'; - ctx.fillText(label, ...coords); - } - }); + haps.forEach((event) => { + const isActive = event.whole.begin <= time && event.endClipped > time; + let strokeCurrent = stroke ?? (strokeActive && isActive); + let fillCurrent = fill ?? (fillActive && isActive); + if (hideInactive && !isActive) { + return; + } + let color = event.value?.color || event.context?.color; + active = color || active; + inactive = colorizeInactive ? color || inactive : inactive; + color = isActive ? active : inactive; + ctx.fillStyle = fillCurrent ? color : 'transparent'; + ctx.strokeStyle = color; + ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; + const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent; + const timePx = scale(timeProgress, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + const value = getValue(event); + const valueProgress = fold + ? foldValues.indexOf(value) / foldValues.length + : (Number(value) - minMidi) / valueExtent; + const valuePx = scale(valueProgress, ...valueRange); + let margin = 0; + const offset = scale(time / 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 + ]; + } + /* const xFactor = Math.sin(performance.now() / 500) + 1; + coords[0] *= xFactor; */ + + if (strokeCurrent) { + ctx.strokeRect(...coords); + } + if (fillCurrent) { + ctx.fillRect(...coords); + } + //ctx.ellipse(...ellipseFromRect(...coords)) + if (labels) { + const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); + const { label: inactiveLabel, activeLabel } = event.value; + const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel; + const label = customLabel ?? defaultLabel; + let measure = vertical ? durationPx : barThickness * 0.75; + ctx.font = `${measure}px ${fontFamily}`; + //ctx.strokeStyle = 'white'; + //ctx.lineWidth = 2; + // font color + ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black'; + ctx.textBaseline = 'top'; + //ctx.strokeText(label, ...coords); + + /* ctx.translate(coords[0], coords[1]); + ctx.rotate(Math.PI * 4); */ + + ctx.fillText(label, ...coords); + //ctx.setTransform(1, 0, 0, 1, 0, 0); // Sets the identity matrix + } + }); ctx.globalAlpha = 1; // reset! const playheadPosition = scale(-from / timeExtent, ...timeRange); // draw playhead @@ -311,11 +344,15 @@ export function getDrawOptions(drawTime, options = {}) { } Pattern.prototype.punchcard = function (options) { - return this.onPaint((ctx, time, haps, drawTime) => - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }), + return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }), ); }; +Pattern.prototype.wordfall = function (options) { + return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options }); +}; + /* Pattern.prototype.pianoroll = function (options) { return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), diff --git a/packages/react/src/hooks/usePatternFrame.mjs b/packages/react/src/hooks/usePatternFrame.mjs index 065c6ba7..725fe0a3 100644 --- a/packages/react/src/hooks/usePatternFrame.mjs +++ b/packages/react/src/hooks/usePatternFrame.mjs @@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2] const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase); lastFrame.current = phase; visibleHaps.current = (visibleHaps.current || []) - .filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame + .filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame .concat(haps.filter((h) => h.hasOnset())); onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime); - }, [pattern]), + }, [pattern, onDraw]), ); useEffect(() => { if (started) { diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 223c21ba..a10998e7 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -18,6 +18,7 @@ function useStrudel({ canvasId, drawContext, drawTime = [-2, 2], + paintOptions = {}, }) { const id = useMemo(() => s4(), []); canvasId = canvasId || `canvas-${id}`; @@ -85,9 +86,9 @@ function useStrudel({ (pattern, time, haps, drawTime) => { const { onPaint } = pattern.context || {}; const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext; - onPaint?.(ctx, time, haps, drawTime); + onPaint?.(ctx, time, haps, drawTime, paintOptions); }, - [drawContext, canvasId], + [drawContext, canvasId, paintOptions], ); const drawFirstFrame = useCallback( diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index f7227d7d..0400db7a 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -53,3 +53,7 @@ #code .cm-cursor { border-left: 2px solid currentcolor !important; } + +#code .cm-foldGutter { + display: none !important; +} diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index aa83317d..3674c581 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,6 +50,7 @@ const modules = [ import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), import('@strudel.cycles/csound'), + import('@strudel.cycles/emoji'), ]; const modulesLoading = evalScope( @@ -125,6 +126,8 @@ export function Repl({ embedded = false }) { panelPosition, } = useSettings(); + const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]); + const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = useStrudel({ initialCode: '// LOADING...', @@ -147,6 +150,8 @@ export function Repl({ embedded = false }) { }, onToggle: (play) => !play && cleanupDraw(false), drawContext, + // drawTime: [0, 6], + paintOptions, }); // init code From 88651149d3d1b74b4873dcaf6c388e04eeb46a23 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:28:46 +0200 Subject: [PATCH 2/4] fix: don't import emoji pkg --- website/src/repl/Repl.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 3674c581..4bca419a 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,7 +50,6 @@ const modules = [ import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), import('@strudel.cycles/csound'), - import('@strudel.cycles/emoji'), ]; const modulesLoading = evalScope( From 6f6def34f7ebc35dd3a21594835049a986dd600e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:35:44 +0200 Subject: [PATCH 3/4] fix: improve performance of setting patterning --- website/src/settings.mjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 036c5509..5608b894 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -38,13 +38,15 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code); export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) => - register(key, (value, pat) => { - value = Array.isArray(value) ? value.join(' ') : value; - if (value !== settingsMap.get()[key]) { - settingsMap.setKey(key, value); - } - return pat; - }); + register(key, (value, pat) => + pat.onTrigger(() => { + value = Array.isArray(value) ? value.join(' ') : value; + if (value !== settingsMap.get()[key]) { + settingsMap.setKey(key, value); + } + return pat; + }, false), + ); export const theme = patternSetting('theme'); export const fontFamily = patternSetting('fontFamily'); From 038e6c312b9aeef74893812f4dbae322a5e1f619 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 16:05:58 +0200 Subject: [PATCH 4/4] dedupe .pianoroll --- packages/core/pianoroll.mjs | 145 +++--------------------------------- 1 file changed, 12 insertions(+), 133 deletions(-) diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 1592ab96..f3d2c38a 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -29,138 +29,26 @@ 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, - labels = 0, -} = {}) { - const ctx = getDrawContext(); - const w = ctx.canvas.width; - const h = ctx.canvas.height; +Pattern.prototype.pianoroll = function (options = {}) { + let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options; + 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); - } + (ctx, haps, t) => { const inFrame = (event) => (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; - events.filter(inFrame).forEach((event) => { - const isActive = event.whole.begin <= t && event.endClipped > t; - ctx.fillStyle = event.context?.color || inactive; - ctx.strokeStyle = event.context?.color || active; - ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 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); - if (labels) { - const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); - ctx.font = `${barThickness * 0.75}px monospace`; - ctx.strokeStyle = 'black'; - ctx.fillStyle = isActive ? 'white' : 'black'; - ctx.textBaseline = 'top'; - ctx.fillText(label, ...coords); - } + pianoroll({ + ...options, + time: t, + ctx, + haps: haps.filter(inFrame), }); - 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; @@ -191,7 +79,7 @@ export function pianoroll({ fold = 0, vertical = 0, labels = false, - fill, + fill = 1, fillActive = false, strokeActive = true, stroke, @@ -241,7 +129,6 @@ export function pianoroll({ // foldValues = values.sort((a, b) => a - b); foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; - ctx.fillStyle = background; ctx.globalAlpha = 1; // reset! if (!smear) { @@ -251,7 +138,7 @@ export function pianoroll({ haps.forEach((event) => { const isActive = event.whole.begin <= time && event.endClipped > time; let strokeCurrent = stroke ?? (strokeActive && isActive); - let fillCurrent = fill ?? (fillActive && isActive); + let fillCurrent = (!isActive && fill) || (isActive && fillActive); if (hideInactive && !isActive) { return; } @@ -304,19 +191,11 @@ export function pianoroll({ const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel; const label = customLabel ?? defaultLabel; let measure = vertical ? durationPx : barThickness * 0.75; - ctx.font = `${measure}px ${fontFamily}`; - //ctx.strokeStyle = 'white'; - //ctx.lineWidth = 2; + ctx.font = `${measure}px ${fontFamily || 'monospace'}`; // font color ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black'; ctx.textBaseline = 'top'; - //ctx.strokeText(label, ...coords); - - /* ctx.translate(coords[0], coords[1]); - ctx.rotate(Math.PI * 4); */ - ctx.fillText(label, ...coords); - //ctx.setTransform(1, 0, 0, 1, 0, 0); // Sets the identity matrix } }); ctx.globalAlpha = 1; // reset!