From 187307c6bce04712d78222ca9a31099991372706 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 28 Mar 2024 15:06:50 +0100 Subject: [PATCH 1/2] half working claviature --- packages/codemirror/widget.mjs | 5 ++ packages/draw/claviature.mjs | 115 +++++++++++++++++++++++++++++++++ packages/draw/index.mjs | 1 + 3 files changed, 121 insertions(+) create mode 100644 packages/draw/claviature.mjs diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 1d86dc359..68ea2bc42 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -126,3 +126,8 @@ registerWidget('_scope', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.tag(id).scope({ ...options, ctx, id }); }); + +registerWidget('_claviature', (id, options = {}, pat) => { + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.tag(id).claviature({ ...options, ctx, id }); +}); diff --git a/packages/draw/claviature.mjs b/packages/draw/claviature.mjs new file mode 100644 index 000000000..970e6e343 --- /dev/null +++ b/packages/draw/claviature.mjs @@ -0,0 +1,115 @@ +import { Pattern, noteToMidi } from '@strudel/core'; + +const blackPattern = [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0]; + +export const tokenizeNote = (note) => { + if (typeof note !== 'string') { + return []; + } + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || []; + if (!pc) { + return []; + } + return [pc, acc, oct ? Number(oct) : undefined]; +}; +const accs = { '#': 1, b: -1, s: 1 }; +const toMidi = (note) => { + if (typeof note === 'number') { + return note; + } + const [pc, acc, oct] = tokenizeNote(note); + if (!pc) { + throw new Error('not a note: "' + note + '"'); + } + const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()]; + const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + return (Number(oct) + 1) * 12 + chroma + offset; +}; + +const getMidiKeys = (range, offset) => { + const white /* : number[] */ = []; + const black /* : number[] */ = []; + const to = noteToMidi(range[1]); + for (let i = offset; i <= to; i++) { + // + (blackPattern[i % 12] ? black : white).push(i); + } + return [white, black]; +}; + +const whiteWidth = (midi, topWidth) => (midi % 12 > 4 ? 7 / 4 : 5 / 3) * topWidth; + +const whiteX = (midi, offset, topWidth) => + Array.from({ length: midi - offset }, (_, i) => i + offset).reduce( + (sum, m) => (!blackPattern[m % 12] ? sum + whiteWidth(m, topWidth) : sum), + 0, + ); // TODO: calculate mathematically + +/* const blackX = (index, offset, topWidth) => { + const cDiff = 12 - (offset % 12); + console.log('cDiff', cDiff); + const cOffset = whiteX(cDiff + offset); + const blackOffset = cOffset + cDiff * topWidth; + return (index - offset) * topWidth + blackOffset; +}; */ + +const parseNote = (note) => (typeof note === 'number' ? note : toMidi(note)); + +export function claviature(haps, options) { + const { + ctx = getDrawContext(), + range = ['A1', 'C6'], + scaleX = 1, + scaleY = 1, + palette = [getTheme().foreground, getTheme().background], + strokeWidth = 0, + stroke = getTheme().foreground, + upperWidth = 14, + upperHeight = 100, + lowerHeight = 45, + } = options || {}; + const offset = parseNote(range[0]); + const colorize = haps.map((hap) => ({ keys: [hap.value.note], color: hap.value.color || getTheme().selection })); + /* const to = parseNote(range[1]); + const totalKeys = to - offset + 1; */ + /* const width = totalKeys * topWidth + topWidth + strokeWidth * 2; + const height = whiteHeight; */ + + const topWidth = upperWidth * scaleX; + const [white, black] = getMidiKeys(range, offset); + + const whiteHeight = (upperHeight + lowerHeight) * scaleY; + const blackHeight = upperHeight * scaleY; + + const cDiff = 12 - (offset % 12); + const cOffset = whiteX(cDiff + offset); + const blackOffset = cOffset - cDiff * topWidth; + + const blackX = (index) => (index - offset) * topWidth + blackOffset; + + const colorizedMidi = colorize.map((c) => ({ + ...c, + keys: c.keys.map((key) => parseNote(key)), + })); + const getColor = (midi) => colorizedMidi.find(({ keys }) => keys.includes(midi))?.color; + ctx.clearRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height * 2); + ctx.strokeStyle = stroke; + ctx.strokeWidth = strokeWidth; + white.forEach((midi) => { + ctx.fillStyle = getColor(midi) ?? palette[1]; + const x = whiteX(midi, offset, topWidth); + const width = whiteWidth(midi, topWidth); + ctx.fillRect(x, 0, width, whiteHeight); + ctx.strokeRect(x, 0, width, whiteHeight); + }); + black.forEach((midi) => { + ctx.fillStyle = getColor(midi) ?? palette[0]; + const x = blackX(midi, offset, topWidth); + //ctx.strokeRect(x, 0, topWidth, blackHeight); + ctx.fillRect(x, 0, topWidth, blackHeight); + }); +} + +Pattern.prototype.claviature = function (options) { + return this.draw((haps) => claviature(haps, options), { id: options.id }); +}; diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 89cda805e..b3c9d6f99 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -3,3 +3,4 @@ export * from './color.mjs'; export * from './draw.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; +export * from './claviature.mjs'; From 6350dfa31f3040c0b266794890567891e355e982 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Apr 2024 21:31:17 +0200 Subject: [PATCH 2/2] claviature tweaks --- packages/codemirror/widget.mjs | 1 + packages/draw/claviature.mjs | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 68ea2bc42..8ac97a06c 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -128,6 +128,7 @@ registerWidget('_scope', (id, options = {}, pat) => { }); registerWidget('_claviature', (id, options = {}, pat) => { + options = { height: 75, width: 640, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.tag(id).claviature({ ...options, ctx, id }); }); diff --git a/packages/draw/claviature.mjs b/packages/draw/claviature.mjs index 970e6e343..3cb94685c 100644 --- a/packages/draw/claviature.mjs +++ b/packages/draw/claviature.mjs @@ -58,7 +58,7 @@ const parseNote = (note) => (typeof note === 'number' ? note : toMidi(note)); export function claviature(haps, options) { const { ctx = getDrawContext(), - range = ['A1', 'C6'], + range = ['C1', 'D3'], scaleX = 1, scaleY = 1, palette = [getTheme().foreground, getTheme().background], @@ -69,7 +69,10 @@ export function claviature(haps, options) { lowerHeight = 45, } = options || {}; const offset = parseNote(range[0]); - const colorize = haps.map((hap) => ({ keys: [hap.value.note], color: hap.value.color || getTheme().selection })); + const colorizedMidi = haps.map((hap) => ({ + keys: [parseNote(hap.value.note)], + color: hap.value.color || getTheme().selection, + })); /* const to = parseNote(range[1]); const totalKeys = to - offset + 1; */ /* const width = totalKeys * topWidth + topWidth + strokeWidth * 2; @@ -83,17 +86,13 @@ export function claviature(haps, options) { const cDiff = 12 - (offset % 12); const cOffset = whiteX(cDiff + offset); - const blackOffset = cOffset - cDiff * topWidth; + const blackOffset = cOffset; - const blackX = (index) => (index - offset) * topWidth + blackOffset; + const blackX = (midi) => (midi - offset) * topWidth + blackOffset; - const colorizedMidi = colorize.map((c) => ({ - ...c, - keys: c.keys.map((key) => parseNote(key)), - })); const getColor = (midi) => colorizedMidi.find(({ keys }) => keys.includes(midi))?.color; ctx.clearRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height * 2); - ctx.strokeStyle = stroke; + ctx.strokeStyle = 'white'; ctx.strokeWidth = strokeWidth; white.forEach((midi) => { ctx.fillStyle = getColor(midi) ?? palette[1];