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