diff --git a/webapp/path_navigator.html b/webapp/path_navigator.html index 31041d1..4a6ac39 100644 --- a/webapp/path_navigator.html +++ b/webapp/path_navigator.html @@ -245,10 +245,11 @@ const hs1 = pitch1.hs_array; const hs2 = pitch2.hs_array; + // Check if they differ by ±1 in exactly one dimension (ignoring dim 0) let diffCount = 0; 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]; if (Math.abs(diff) === 1) { diffCount++; @@ -261,11 +262,24 @@ // Must differ by exactly ±1 in exactly one dimension if (diffCount !== 1) return null; - // Check dimension 0 (prime 2) - must be same - if (hs1[0] !== hs2[0]) return null; + // Calculate frequency ratio from pitch difference (hs1 - hs2) + // 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 - const ratio = parseFraction(pitch1.fraction) / parseFraction(pitch2.fraction); + for (let i = 0; i < hs1.length; i++) { + 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 { dim: diffDim, @@ -335,25 +349,47 @@ } function buildGraph(chord) { - const nodes = chord.map((pitch, i) => ({ - id: i, - pitch: pitch, - label: pitch.fraction, - hs: pitch.hs_array - })); + // Calculate cents for each pitch + const centsData = chord.map((pitch, i) => { + const fr = parseFraction(pitch.fraction); + const cents = 1200 * Math.log2(fr); + 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 labelSet = new Set(); for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch); if (edgeInfo) { - const label = formatRatio(edgeInfo.ratio); links.push({ source: i, target: j, - label: label + label: edgeInfo.ratio }); } } @@ -378,11 +414,11 @@ return; } - // Force simulation + // Force simulation with fixed y positions simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(100)) .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)); // Draw links @@ -419,7 +455,7 @@ node.append("text") .attr("class", "node-label") .attr("dy", 4) - .text(d => d.label); + .text(d => d.cents); // Update positions on tick simulation.on("tick", () => {