Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a helper to align prime directions horizontally #10

Merged
merged 1 commit into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {describe, it, expect} from 'vitest';
import {Fraction, LOG_PRIMES, circleDistance, toMonzo} from 'xen-dev-utils';
import {
GridOptions,
align,
kraigGrady9,
modVal,
primeRing72,
Expand Down Expand Up @@ -244,6 +245,67 @@ describe('Prime ring 72 coordinates', () => {
});
});

describe('Coordinate aligner', () => {
it('aligns PR72 horizontally by rotating', () => {
const options = primeRing72(undefined, false);
const lengths: number[] = [];
for (let i = 0; i < options.horizontalCoordinates.length; ++i) {
lengths.push(
Math.hypot(
options.horizontalCoordinates[i],
options.verticalCoordinates[i]
)
);
}
align(options, 1);
expect(options.horizontalCoordinates[0]).toBeCloseTo(0);
expect(options.verticalCoordinates[0]).toBeCloseTo(0);
expect(options.horizontalCoordinates[1]).toBeCloseTo(lengths[1]);
expect(options.verticalCoordinates[1]).toBeCloseTo(0);
for (let i = 0; i < options.horizontalCoordinates.length; ++i) {
expect(
Math.hypot(
options.horizontalCoordinates[i],
options.verticalCoordinates[i]
)
).toBeCloseTo(lengths[i]);
}
});

it('aligns PR72 with a Tonnetz lattice by shearing', () => {
const options = primeRing72(undefined, false);
const lengths: number[] = [];
for (let i = 0; i < options.horizontalCoordinates.length; ++i) {
lengths.push(
Math.hypot(
options.horizontalCoordinates[i],
options.verticalCoordinates[i]
)
);
}
align(options, 1, 2);
expect(options.horizontalCoordinates[0]).toBeCloseTo(0);
expect(options.verticalCoordinates[0]).toBeCloseTo(0);
expect(options.horizontalCoordinates[1]).toBeCloseTo(lengths[1]);
expect(options.verticalCoordinates[1]).toBeCloseTo(0);
expect(options.horizontalCoordinates[2]).toBeCloseTo(lengths[1] * 0.5);
expect(
Math.hypot(
options.horizontalCoordinates[2],
options.verticalCoordinates[2]
)
).toBeCloseTo(lengths[2]);
for (let i = 3; i < options.horizontalCoordinates.length; ++i) {
expect(
Math.hypot(
options.horizontalCoordinates[i],
options.verticalCoordinates[i]
)
).toBeCloseTo(lengths[i], -1);
}
});
});

describe('Grid spanner', () => {
it('spans pentatonic major in 12-TET', () => {
const steps = [0, 2, 4, 7, 9];
Expand Down
69 changes: 66 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,24 +540,87 @@ export function scottDakota24(logs?: number[]): LatticeOptions {
/**
* Compute prime ring 72 coordinates.
* @param logs Logarithms of (formal) primes with the prime of equivalence first. Defaults to the actual primes.
* @param round Round coordinates to nearest integers.
* @returns An array of horizontal coordinates for each prime and the same for vertical coordinates.
*/
export function primeRing72(logs?: number[]): LatticeOptions {
export function primeRing72(logs?: number[], round = true): LatticeOptions {
logs ??= LOG_PRIMES.slice(0, 72);
const mv = modVal(logs, 72);
const horizontalCoordinates: number[] = [];
const verticalCoordinates: number[] = [];
for (const steps of mv) {
const theta = (Math.PI * steps) / 36;
horizontalCoordinates.push(37 - Math.round(Math.cos(theta) * 36.7));
verticalCoordinates.push(-Math.round(Math.sin(theta) * 36.7));
if (round) {
horizontalCoordinates.push(37 - Math.round(Math.cos(theta) * 36.7));
verticalCoordinates.push(-Math.round(Math.sin(theta) * 36.7));
} else {
horizontalCoordinates.push(36.7 - Math.cos(theta) * 36.7);
verticalCoordinates.push(-Math.sin(theta) * 36.7);
}
}
return {
horizontalCoordinates,
verticalCoordinates,
};
}

/**
* Rotate coordinates to make one of the primes horizontal.
* @param options Lattice options with coordinates to modify in-place.
* @param horizontalIndex Index of prime to make horizontal.
* @param tonnetzIndex Index of another prime to align with the up-left direction of a triangular lattice. Coordinates are sheared as necessary.
*/
export function align(
options: LatticeOptions,
horizontalIndex: number,
tonnetzIndex?: number
) {
const {horizontalCoordinates, verticalCoordinates} = options;

if (tonnetzIndex === undefined) {
const x = horizontalCoordinates[horizontalIndex];
const y = verticalCoordinates[horizontalIndex];
const c = 1 / Math.sqrt(1 + (y * y) / (x * x));
const s = (y / x) * c;
for (let i = 0; i < horizontalCoordinates.length; ++i) {
const u = horizontalCoordinates[i];
const v = verticalCoordinates[i];
horizontalCoordinates[i] = u * c + v * s;
verticalCoordinates[i] = v * c - u * s;
}
} else {
const x1 = horizontalCoordinates[horizontalIndex];
const y1 = verticalCoordinates[horizontalIndex];
const x2 = horizontalCoordinates[tonnetzIndex];
const y2 = verticalCoordinates[tonnetzIndex];

const r1 = Math.hypot(x1, y1);
const R2 = x2 * x2 + y2 * y2;
const u2 = 0.5 * r1;
// Solve: u2^2 + v2^2 = R2
const v2 = -Math.sqrt(R2 - u2 * u2);

// Solve:
// [u_i, v_i] = A [x_i, y_i]
// u1 = r1 = a00 x1 + a10 y1
// v1 = 0 = a01 x1 + a11 y1
// u2 = a00 x2 + a10 y2
// v2 = a01 x2 + a11 y2

const a00 = (r1 * y2 - u2 * y1) / (x1 * y2 - x2 * y1);
const a01 = (-v2 * y1) / (x1 * y2 - x2 * y1);
const a10 = (-r1 * x2 + u2 * x1) / (x1 * y2 - x2 * y1);
const a11 = (v2 * x1) / (x1 * y2 - x2 * y1);

for (let i = 0; i < horizontalCoordinates.length; ++i) {
const u = horizontalCoordinates[i];
const v = verticalCoordinates[i];
horizontalCoordinates[i] = a00 * u + a10 * v;
verticalCoordinates[i] = a01 * u + a11 * v;
}
}
}

function gridline(
uX: number,
uY: number,
Expand Down
Loading