UNPKG

ji-lattice

Version:

Algorithms for projecting just intonation and equally tempered scales onto the screen.

225 lines 8.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.primeSphere = exports.WGP9 = exports.spanLattice3D = exports.mergeEdges3D = void 0; const xen_dev_utils_1 = require("xen-dev-utils"); const utils_1 = require("./utils"); const EPSILON = 1e-6; // Coordinates are based on SVG so positive y-direction points down. // Based on Kraig Grady's coordinate system https://anaphoria.com/wilsontreasure.html // Coordinates for prime 2 and third dimension by Lumi Pakkanen. // X-coordinates for every prime up to 23. const WGP_X = [23, 40, 0, 0, -14, -8, -5, 0, 20]; // Y-coordinates for every prime up to 23. const WGP_Y = [-45, 0, -40, 0, -18, -4, -32, -25, -3]; // Z-coordinates for every prime up to 23. const WGP_Z = [19, 0, 0, 40, 13, 7, 5, 9, 15]; /** * Combine edges that share an endpoint and slope into longer ones. * @param edges Large number of short edges to merge. * @returns Smaller number of long edges. */ function mergeEdges3D(edges) { // Choose a canonical orientation. const oriented = []; for (const edge of edges) { if (edge.x2 < edge.x1 || (edge.x2 === edge.x1 && edge.y2 < edge.y1) || (edge.y2 === edge.y1 && edge.z2 < edge.z1)) { oriented.push({ x1: edge.x2, y1: edge.y2, z1: edge.z2, x2: edge.x1, y2: edge.y1, z2: edge.z1, type: edge.type, }); } else { oriented.push(edge); } } oriented.sort((a, b) => a.x1 - b.x1 || a.y1 - b.y1 || a.z1 - b.z1); const result = []; const spent = new Set(); for (let i = 0; i < oriented.length; ++i) { if (spent.has(i)) { continue; } // eslint-disable-next-line prefer-const let { x1, y1, x2, y2, z1, z2, type } = oriented[i]; const dx = x2 - x1; const dy = y2 - y1; const dz = z2 - z1; for (let j = i + 1; j < oriented.length; ++j) { const e = oriented[j]; if (e.x1 === x2 && e.y1 === y2 && e.z1 === e.z2 && e.type === type) { const dex = e.x2 - e.x1; const dey = e.y2 - e.y1; const dez = e.z2 - e.z1; if (dex * dy === dx * dey && dex * dz === dz * dez) { x2 = e.x2; y2 = e.y2; z2 = e.z2; spent.add(j); } } } result.push({ x1, x2, y1, y2, z1, z2, type }); } return result; } exports.mergeEdges3D = mergeEdges3D; /** * Compute vertices and edges for a 2D graph representing the lattice of a musical scale in just intonation. * @param monzos Prime exponents of the musical intervals in the scale. * @param options Options for connecting vertices in the graph. * @returns Vertices and edges of the graph. */ function spanLattice3D(monzos, options) { var _a; const { horizontalCoordinates, verticalCoordinates, depthwiseCoordinates } = options; const maxDistance = (_a = options.maxDistance) !== null && _a !== void 0 ? _a : 1; const coordss = [ verticalCoordinates, horizontalCoordinates, depthwiseCoordinates, ]; let projected = (0, utils_1.project)(monzos, coordss); const { connections, connectingMonzos } = (0, utils_1.connect)(projected, maxDistance); const unprojected = (0, utils_1.unproject)(connectingMonzos, coordss); const vertices = []; let edges = []; for (let index = 0; index < monzos.length; ++index) { vertices.push({ index, x: (0, xen_dev_utils_1.dot)(monzos[index], horizontalCoordinates), y: (0, xen_dev_utils_1.dot)(monzos[index], verticalCoordinates), z: (0, xen_dev_utils_1.dot)(monzos[index], depthwiseCoordinates), }); } for (const monzo of unprojected) { vertices.push({ index: undefined, x: (0, xen_dev_utils_1.dot)(monzo, horizontalCoordinates), y: (0, xen_dev_utils_1.dot)(monzo, verticalCoordinates), z: (0, xen_dev_utils_1.dot)(monzo, depthwiseCoordinates), }); } for (const connection of connections) { const { index1, index2, type } = connection; edges.push({ x1: vertices[index1].x, y1: vertices[index1].y, z1: vertices[index1].z, x2: vertices[index2].x, y2: vertices[index2].y, z2: vertices[index2].z, type, }); } if (options.edgeMonzos) { projected = projected.concat(connectingMonzos); let ems = (0, utils_1.project)(options.edgeMonzos, coordss); ems = ems.concat(ems.map(em => em.map(e => -e))); for (let i = 0; i < projected.length; ++i) { for (let j = i + 1; j < projected.length; ++j) { const diff = (0, xen_dev_utils_1.sub)(projected[i], projected[j]); for (const em of ems) { if ((0, xen_dev_utils_1.monzosEqual)(diff, em)) { edges.push({ x1: vertices[i].x, y1: vertices[i].y, z1: vertices[i].z, x2: vertices[j].x, y2: vertices[j].y, z2: vertices[j].z, type: i < monzos.length && j < monzos.length ? 'custom' : 'auxiliary', }); } } } } } if (options.mergeEdges) { edges = mergeEdges3D(edges); } return { vertices, edges, }; } exports.spanLattice3D = spanLattice3D; /** * Get Wilson-Grady-Pakkanen coordinates for the first 9 primes. * @param equaveIndex Index of the prime to use as the interval of equivalence. * @returns An array of horizontal coordinates for each prime and the same for vertical and depthwise coordinates. */ function WGP9(equaveIndex = 0) { const horizontalCoordinates = [...WGP_X]; const verticalCoordinates = [...WGP_Y]; const depthwiseCoordinates = [...WGP_Z]; horizontalCoordinates[equaveIndex] = 0; verticalCoordinates[equaveIndex] = 0; depthwiseCoordinates[equaveIndex] = 0; return { horizontalCoordinates, verticalCoordinates, depthwiseCoordinates, }; } exports.WGP9 = WGP9; /** * Compute coordinates based on sizes of primes that lie on the surface of a sphere offset on the x-axis. * @param equaveIndex Index of the prime to use as the interval of equivalence. * @param logs Logarithms of (formal) primes with the prime of equivalence first. Defaults to the first 24 actual primes. * @param searchResolution Search resolution for optimizing orthogonality of the resulting set. * @returns An array of horizontal coordinates for each prime and the same for vertical and depthwise coordinates. */ function primeSphere(equaveIndex = 0, logs, searchResolution = 1024) { logs !== null && logs !== void 0 ? logs : (logs = xen_dev_utils_1.LOG_PRIMES.slice(0, 24)); const dp = (2 * Math.PI) / searchResolution; const horizontalCoordinates = []; const verticalCoordinates = []; const depthwiseCoordinates = []; const dt = (2 * Math.PI) / logs[equaveIndex]; for (const log of logs) { const theta = log * dt; const x = 1 - Math.cos(theta); const u = Math.sin(theta); let y = -u; let z = 0; if (horizontalCoordinates.length > 1) { // Find the most orthogonal rotation around the x-axis let bestError = Infinity; for (let j = 0; j < searchResolution; ++j) { const phi = dp * j; const yc = Math.cos(phi) * u; const zc = Math.sin(-phi) * u; let error = 0; for (let i = 0; i < horizontalCoordinates.length; ++i) { error += (x * horizontalCoordinates[i] + yc * verticalCoordinates[i] + zc * depthwiseCoordinates[i]) ** 2; } if (error + EPSILON < bestError) { bestError = error; y = yc; z = zc; } } } horizontalCoordinates.push(x); verticalCoordinates.push(y); depthwiseCoordinates.push(z); } return { horizontalCoordinates, verticalCoordinates, depthwiseCoordinates, }; } exports.primeSphere = primeSphere; //# sourceMappingURL=lattice-3d.js.map