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
This commit is contained in:
Felix Roos 2023-08-27 15:27:30 +02:00
parent d0f8a612ad
commit e301090704
6 changed files with 101 additions and 51 deletions

View File

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

View File

@ -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 }) }),

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);
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) {

View File

@ -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(

View File

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

View File

@ -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