Merge pull request #679 from tidalcycles/pianoroll-improvements

Pianoroll improvements
This commit is contained in:
Felix Roos 2023-08-27 16:12:32 +02:00 committed by GitHub
commit 338c866d83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 180 deletions

View file

@ -506,6 +506,9 @@ const generic_params = [
* *
*/ */
['lsize'], ['lsize'],
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'], // ['lfo'],
// ['lfocutoffint'], // ['lfocutoffint'],
// ['lfodelay'], // ['lfodelay'],

View file

@ -29,138 +29,26 @@ const getValue = (e) => {
return value; return value;
}; };
Pattern.prototype.pianoroll = function ({ Pattern.prototype.pianoroll = function (options = {}) {
cycles = 4, let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
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;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - 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( this.draw(
(ctx, events, t) => { (ctx, haps, 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) => const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
events.filter(inFrame).forEach((event) => { pianoroll({
const isActive = event.whole.begin <= t && event.endClipped > t; ...options,
ctx.fillStyle = event.context?.color || inactive; time: t,
ctx.strokeStyle = event.context?.color || active; ctx,
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; haps: haps.filter(inFrame),
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);
}
}); });
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, from: from - overscan,
to: to + 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; return this;
@ -191,6 +79,13 @@ export function pianoroll({
fold = 0, fold = 0,
vertical = 0, vertical = 0,
labels = false, labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx, ctx,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
@ -234,58 +129,75 @@ export function pianoroll({
// foldValues = values.sort((a, b) => a - b); // foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background; ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
if (!smear) { if (!smear) {
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
} }
/* const inFrame = (event) => haps.forEach((event) => {
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */ const isActive = event.whole.begin <= time && event.endClipped > time;
haps let strokeCurrent = stroke ?? (strokeActive && isActive);
// .filter(inFrame) let fillCurrent = (!isActive && fill) || (isActive && fillActive);
.forEach((event) => { if (hideInactive && !isActive) {
const isActive = event.whole.begin <= time && event.endClipped > time; return;
const color = event.value?.color || event.context?.color; }
ctx.fillStyle = color || inactive; let color = event.value?.color || event.context?.color;
ctx.strokeStyle = color || active; active = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; inactive = colorizeInactive ? color || inactive : inactive;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); color = isActive ? active : inactive;
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); ctx.fillStyle = fillCurrent ? color : 'transparent';
const value = getValue(event); ctx.strokeStyle = color;
const valuePx = scale( ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
...valueRange, const timePx = scale(timeProgress, ...timeRange);
); let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
let margin = 0; const value = getValue(event);
const offset = scale(time / timeExtent, ...timeRange); const valueProgress = fold
let coords; ? foldValues.indexOf(value) / foldValues.length
if (vertical) { : (Number(value) - minMidi) / valueExtent;
coords = [ const valuePx = scale(valueProgress, ...valueRange);
valuePx + 1 - (flipValues ? barThickness : 0), // x let margin = 0;
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y const offset = scale(time / timeExtent, ...timeRange);
barThickness - 2, // width let coords;
durationPx - 2, // height if (vertical) {
]; coords = [
} else { valuePx + 1 - (flipValues ? barThickness : 0), // x
coords = [ timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x barThickness - 2, // width
valuePx + 1 - (flipValues ? 0 : barThickness), // y durationPx - 2, // height
durationPx - 2, // widith ];
barThickness - 2, // height } else {
]; coords = [
} timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); valuePx + 1 - (flipValues ? 0 : barThickness), // y
if (labels) { durationPx - 2, // widith
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); barThickness - 2, // height
ctx.font = `${barThickness * 0.75}px monospace`; ];
ctx.strokeStyle = 'black'; }
ctx.fillStyle = isActive ? 'white' : 'black'; /* const xFactor = Math.sin(performance.now() / 500) + 1;
ctx.textBaseline = 'top'; coords[0] *= xFactor; */
ctx.fillText(label, ...coords);
} 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 || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange); const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead // draw playhead
@ -311,11 +223,15 @@ export function getDrawOptions(drawTime, options = {}) {
} }
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }), 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) { /* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),

View file

@ -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); const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase; lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || []) 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())); .concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime); onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern]), }, [pattern, onDraw]),
); );
useEffect(() => { useEffect(() => {
if (started) { if (started) {

View file

@ -18,6 +18,7 @@ function useStrudel({
canvasId, canvasId,
drawContext, drawContext,
drawTime = [-2, 2], drawTime = [-2, 2],
paintOptions = {},
}) { }) {
const id = useMemo(() => s4(), []); const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`; canvasId = canvasId || `canvas-${id}`;
@ -85,9 +86,9 @@ function useStrudel({
(pattern, time, haps, drawTime) => { (pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {}; const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext; 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( const drawFirstFrame = useCallback(

View file

@ -53,3 +53,7 @@
#code .cm-cursor { #code .cm-cursor {
border-left: 2px solid currentcolor !important; border-left: 2px solid currentcolor !important;
} }
#code .cm-foldGutter {
display: none !important;
}

View file

@ -125,6 +125,8 @@ export function Repl({ embedded = false }) {
panelPosition, panelPosition,
} = useSettings(); } = useSettings();
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({ useStrudel({
initialCode: '// LOADING...', initialCode: '// LOADING...',
@ -147,6 +149,8 @@ export function Repl({ embedded = false }) {
}, },
onToggle: (play) => !play && cleanupDraw(false), onToggle: (play) => !play && cleanupDraw(false),
drawContext, drawContext,
// drawTime: [0, 6],
paintOptions,
}); });
// init code // init code

View file

@ -38,13 +38,15 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) => const patternSetting = (key) =>
register(key, (value, pat) => { register(key, (value, pat) =>
value = Array.isArray(value) ? value.join(' ') : value; pat.onTrigger(() => {
if (value !== settingsMap.get()[key]) { value = Array.isArray(value) ? value.join(' ') : value;
settingsMap.setKey(key, value); if (value !== settingsMap.get()[key]) {
} settingsMap.setKey(key, value);
return pat; }
}); return pat;
}, false),
);
export const theme = patternSetting('theme'); export const theme = patternSetting('theme');
export const fontFamily = patternSetting('fontFamily'); export const fontFamily = patternSetting('fontFamily');