Public
Edited
Jan 5, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
function createPiano({
totalWidth = width,
totalHeight = Math.max(totalWidth / 4, 120),
margin = {
top: 10,
right: 10,
left: 10,
bottom: 10
},
minNote = 'C3',
maxNote = 'C6'
} = {}) {
const svg = d3
.create('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);

const allNotes = tonal.Scale.rangeOf('C chromatic')(minNote, maxNote).map(
normalizeNote
);

const whiteKeys = allNotes.filter(isWhiteKey);
const blackKeys = allNotes.filter(isBlackKey);

const boardWidth = totalWidth - margin.left - margin.right;
const blackKeyToWhiteKeyRatio = 3 / 4;

const isFirstNoteBlackKey = isBlackKey(allNotes[0]);
const isLastNoteBlackKey = isBlackKey(allNotes[allNotes.length - 1]);

const numBlackKeysOnEdge = isFirstNoteBlackKey
? isLastNoteBlackKey
? 2
: 1
: 0;

const whiteKeyWidth =
boardWidth /
(whiteKeys.length + (numBlackKeysOnEdge * blackKeyToWhiteKeyRatio) / 2);

const blackKeyWidth = whiteKeyWidth * blackKeyToWhiteKeyRatio;

const whiteKeyToX = d3
.scaleLinear()
.domain([
getOrdinal(whiteKeys[0]),
getOrdinal(whiteKeys[whiteKeys.length - 1])
])
.range([
margin.left + (isFirstNoteBlackKey ? blackKeyWidth / 2 : 0),
totalWidth -
margin.right -
whiteKeyWidth -
(isLastNoteBlackKey ? blackKeyWidth / 2 : 0)
]);

const blackKeyToX = i => whiteKeyToX(i + 1) - blackKeyWidth / 2;

const noteToKey = n => {
const note = normalizeNote(n);
const ordinal = getOrdinal(note);
if (isBlackKey(note)) {
return {
note,
isBlackKey: true,
...elaborateRect(
blackKeyToX(ordinal),
margin.top,
blackKeyWidth,
((totalHeight - margin.bottom - margin.top) * 2) / 3
)
};
} else {
return {
note,
isBlackKey: false,
...elaborateRect(
whiteKeyToX(ordinal),
margin.top,
whiteKeyWidth,
totalHeight - margin.bottom - margin.top
)
};
}
};

const allKeys = allNotes.map(noteToKey).sort(a => (a.isBlackKey ? 1 : -1));

svg
.selectAll('.key')
.data(allKeys)
.enter()
.append('rect')
.attr('class', d => `key ${tonal.Note.get(d.note).name}`)
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('width', d => d.width)
.attr('height', d => d.height)
.attr('fill', d => (d.isBlackKey ? 'gainsboro' : 'white'))
.attr('stroke', 'gainsboro');

return Object.assign(svg.node(), {
svg,
noteToKey,
minNote,
maxNote
});
}
Insert cell
function elaborateRect(x, y, width, height) {
return {
x,
y,
width,
height,
top: y,
left: x,
right: x + width,
bottom: y + height
};
}
Insert cell
function scaleHas(scale, note) {
const normal = tonal.Note.get(normalizeNote(note));
return scale.notes.map(d => tonal.Note.get(d).pc).includes(normal.pc);
}
Insert cell
function normalizeNote(note) {
const n = tonal.Note.get(note);
if (n.alt < 0) {
return normalizeNote(tonal.Note.enharmonic(n));
} else {
return tonal.Note.simplify(note);
}
}
Insert cell
function isWhiteKey(note) {
return scaleHas(tonal.Scale.get('C major'), note)
}
Insert cell
function isBlackKey(note) {
return scaleHas(tonal.Scale.get('F# pentatonic'), note);
}
Insert cell
function getOrdinal(note) {
const n = tonal.Note.get(note);
return n.oct * 7 + n.step;
}
Insert cell
function getAlt(note) {
return tonal.Note.get(note).alt;
}
Insert cell
getOrdinal('D#3')
Insert cell
tonal.Note.get('D4')
Insert cell
tonal.Note.get('D3')
Insert cell
tonal.Note.get('Db3')
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more