ji-lattice
Version:
Algorithms for projecting just intonation and equally tempered scales onto the screen.
225 lines • 8.76 kB
JavaScript
"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