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'], ['lsize'],
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'], // ['lfo'],
// ['lfocutoffint'], // ['lfocutoffint'],
// ['lfodelay'], // ['lfodelay'],

View File

@ -191,6 +191,13 @@ export function pianoroll({
fold = 0, fold = 0,
vertical = 0, vertical = 0,
labels = false, labels = false,
fill,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx, ctx,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
@ -241,51 +248,77 @@ export function pianoroll({
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 = fill ?? (fillActive && isActive);
.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}`;
//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! ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange); const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead // draw playhead
@ -311,11 +344,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

@ -50,6 +50,7 @@ const modules = [
import('@strudel.cycles/serial'), import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'), import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'), import('@strudel.cycles/csound'),
import('@strudel.cycles/emoji'),
]; ];
const modulesLoading = evalScope( const modulesLoading = evalScope(
@ -125,6 +126,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 +150,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