Add vertical node positioning by frequency in Path Navigator
This commit is contained in:
parent
27d34fdafc
commit
6924c85d19
|
|
@ -245,10 +245,11 @@
|
||||||
const hs1 = pitch1.hs_array;
|
const hs1 = pitch1.hs_array;
|
||||||
const hs2 = pitch2.hs_array;
|
const hs2 = pitch2.hs_array;
|
||||||
|
|
||||||
|
// Check if they differ by ±1 in exactly one dimension (ignoring dim 0)
|
||||||
let diffCount = 0;
|
let diffCount = 0;
|
||||||
let diffDim = -1;
|
let diffDim = -1;
|
||||||
|
|
||||||
for (let i = 1; i < hs1.length; i++) { // Skip dimension 0 (prime 2)
|
for (let i = 1; i < hs1.length; i++) {
|
||||||
const diff = hs2[i] - hs1[i];
|
const diff = hs2[i] - hs1[i];
|
||||||
if (Math.abs(diff) === 1) {
|
if (Math.abs(diff) === 1) {
|
||||||
diffCount++;
|
diffCount++;
|
||||||
|
|
@ -261,11 +262,24 @@
|
||||||
// Must differ by exactly ±1 in exactly one dimension
|
// Must differ by exactly ±1 in exactly one dimension
|
||||||
if (diffCount !== 1) return null;
|
if (diffCount !== 1) return null;
|
||||||
|
|
||||||
// Check dimension 0 (prime 2) - must be same
|
// Calculate frequency ratio from pitch difference (hs1 - hs2)
|
||||||
if (hs1[0] !== hs2[0]) return null;
|
// Using the formula from pitch.py:
|
||||||
|
// numerator *= dims[i] ** diff[i] for diff[i] >= 0
|
||||||
|
// denominator *= dims[i] ** (-diff[i]) for diff[i] < 0
|
||||||
|
const dims = [2, 3, 5, 7];
|
||||||
|
let numerator = 1;
|
||||||
|
let denominator = 1;
|
||||||
|
|
||||||
// Calculate ratio
|
for (let i = 0; i < hs1.length; i++) {
|
||||||
const ratio = parseFraction(pitch1.fraction) / parseFraction(pitch2.fraction);
|
const diff = hs1[i] - hs2[i]; // pitch1 - pitch2
|
||||||
|
if (diff > 0) {
|
||||||
|
numerator *= Math.pow(dims[i], diff);
|
||||||
|
} else if (diff < 0) {
|
||||||
|
denominator *= Math.pow(dims[i], -diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = numerator + "/" + denominator;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dim: diffDim,
|
dim: diffDim,
|
||||||
|
|
@ -335,25 +349,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGraph(chord) {
|
function buildGraph(chord) {
|
||||||
const nodes = chord.map((pitch, i) => ({
|
// Calculate cents for each pitch
|
||||||
id: i,
|
const centsData = chord.map((pitch, i) => {
|
||||||
pitch: pitch,
|
const fr = parseFraction(pitch.fraction);
|
||||||
label: pitch.fraction,
|
const cents = 1200 * Math.log2(fr);
|
||||||
hs: pitch.hs_array
|
return { i, cents };
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
// Find cents for node 0 (voice 0) - use as bottom reference
|
||||||
|
const node0Cents = centsData.find(d => d.i === 0)?.cents || 0;
|
||||||
|
const maxCents = Math.max(...centsData.map(d => d.cents));
|
||||||
|
const range = maxCents - node0Cents || 1;
|
||||||
|
|
||||||
|
// Create nodes with y position: node 0 at bottom, others relative to it
|
||||||
|
const nodes = chord.map((pitch, i) => {
|
||||||
|
const fr = parseFraction(pitch.fraction);
|
||||||
|
const cents = 1200 * Math.log2(fr);
|
||||||
|
// Map to screen: top = high cents, bottom = node 0
|
||||||
|
const y = height * 0.1 + (height * 0.8) * (1 - (cents - node0Cents) / range);
|
||||||
|
// Initial x position: spread horizontally
|
||||||
|
const x = width * 0.2 + (width * 0.6) * (i / (chord.length - 1 || 1));
|
||||||
|
return {
|
||||||
|
id: i,
|
||||||
|
pitch: pitch,
|
||||||
|
label: String(i),
|
||||||
|
cents: cents.toFixed(0),
|
||||||
|
hs: pitch.hs_array,
|
||||||
|
x: x, // Initial x
|
||||||
|
y: y // Initial y (will be fixed by forceY)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const links = [];
|
const links = [];
|
||||||
const labelSet = new Set();
|
|
||||||
|
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
for (let j = i + 1; j < nodes.length; j++) {
|
for (let j = i + 1; j < nodes.length; j++) {
|
||||||
const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch);
|
const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch);
|
||||||
if (edgeInfo) {
|
if (edgeInfo) {
|
||||||
const label = formatRatio(edgeInfo.ratio);
|
|
||||||
links.push({
|
links.push({
|
||||||
source: i,
|
source: i,
|
||||||
target: j,
|
target: j,
|
||||||
label: label
|
label: edgeInfo.ratio
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -378,11 +414,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force simulation
|
// Force simulation with fixed y positions
|
||||||
simulation = d3.forceSimulation(nodes)
|
simulation = d3.forceSimulation(nodes)
|
||||||
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
|
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
|
||||||
.force("charge", d3.forceManyBody().strength(-300))
|
.force("charge", d3.forceManyBody().strength(-300))
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
.force("y", d3.forceY(d => d.y).strength(1)) // Fix y position
|
||||||
.force("collision", d3.forceCollide().radius(40));
|
.force("collision", d3.forceCollide().radius(40));
|
||||||
|
|
||||||
// Draw links
|
// Draw links
|
||||||
|
|
@ -419,7 +455,7 @@
|
||||||
node.append("text")
|
node.append("text")
|
||||||
.attr("class", "node-label")
|
.attr("class", "node-label")
|
||||||
.attr("dy", 4)
|
.attr("dy", 4)
|
||||||
.text(d => d.label);
|
.text(d => d.cents);
|
||||||
|
|
||||||
// Update positions on tick
|
// Update positions on tick
|
||||||
simulation.on("tick", () => {
|
simulation.on("tick", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue