diff --git a/package-lock.json b/package-lock.json index 4fa86aaf..e596cb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "harmonic-entropy": "^0.2.0", "isomorphic-qwerty": "^0.0.2", - "ji-lattice": "^0.0.3", + "ji-lattice": "^0.2.0", "jszip": "^3.10.1", "moment-of-symmetry": "^0.8.0", "pinia": "^2.1.7", @@ -3802,9 +3802,9 @@ } }, "node_modules/ji-lattice": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/ji-lattice/-/ji-lattice-0.0.3.tgz", - "integrity": "sha512-LAO8u0aO4qpDr7WucJmWRpXocy1pnIYlvDNjXmS4/VUhMc4xxMjTzLf4TN0kL/bWTPIhAxRd+0amTEjN1Mnptg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ji-lattice/-/ji-lattice-0.2.0.tgz", + "integrity": "sha512-kX6Q598XiFTvEFuT+ySMC+YJcOC78aF04hT3VJGtHn6zGKjr6Mq8rFacjhkDgKV27c7vz1ma1x1C6geOT1QP6w==", "dependencies": { "xen-dev-utils": "^0.2.8" }, diff --git a/package.json b/package.json index c3205ece..2470d3bb 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "harmonic-entropy": "^0.2.0", "isomorphic-qwerty": "^0.0.2", - "ji-lattice": "^0.0.3", + "ji-lattice": "^0.2.0", "jszip": "^3.10.1", "moment-of-symmetry": "^0.8.0", "pinia": "^2.1.7", diff --git a/src/components/EdoCycles.vue b/src/components/EdoCycles.vue index 45132a1d..aae015a5 100644 --- a/src/components/EdoCycles.vue +++ b/src/components/EdoCycles.vue @@ -108,7 +108,6 @@ const viewBox = computed( :y="v.y + store.size * store.labelOffset * labelY(j, v.indices.length)" :font-size="`${1.1 * store.size}px`" :stroke-width="store.size * 0.01" - dominant-baseline="middle" > {{ labels[idx] }} diff --git a/src/components/Faux3DLattice.vue b/src/components/Faux3DLattice.vue new file mode 100644 index 00000000..22be0375 --- /dev/null +++ b/src/components/Faux3DLattice.vue @@ -0,0 +1,207 @@ + + + + + + + + {{ element.body }} + + + diff --git a/src/components/GridLattice.vue b/src/components/GridLattice.vue index 7f050b44..b4062946 100644 --- a/src/components/GridLattice.vue +++ b/src/components/GridLattice.vue @@ -126,7 +126,6 @@ watch( :y="v.y + store.size * store.labelOffset * labelY(j, v.indices.length)" :font-size="`${2.5 * store.size}px`" :stroke-width="store.size * 0.05" - dominant-baseline="middle" > {{ labels[idx] }} diff --git a/src/components/JustIntonationLattice.vue b/src/components/JustIntonationLattice.vue index 8b4767bc..05750a7c 100644 --- a/src/components/JustIntonationLattice.vue +++ b/src/components/JustIntonationLattice.vue @@ -192,7 +192,6 @@ watch( v-for="(v, i) of lattice.vertices" :key="i" class="node-label" - dominant-baseline="middle" :x="v.x" :y="v.y - store.labelOffset * store.size" :font-size="`${3 * store.size}px`" diff --git a/src/stores/ji-lattice.ts b/src/stores/ji-lattice.ts index 34e9fab0..73f95f1e 100644 --- a/src/stores/ji-lattice.ts +++ b/src/stores/ji-lattice.ts @@ -1,6 +1,15 @@ import { computed, reactive, ref, watch } from 'vue' import { defineStore } from 'pinia' -import { kraigGrady9, type LatticeOptions, scottDakota24, primeRing72, align } from 'ji-lattice' +import { + kraigGrady9, + type LatticeOptions, + scottDakota24, + primeRing72, + align, + type LatticeOptions3D, + WGP9, + primeSphere +} from 'ji-lattice' import { LOG_PRIMES, mmod } from 'xen-dev-utils' import { computedAndError } from '@/utils' import { TimeMonzo, parseChord } from 'sonic-weave' @@ -52,6 +61,48 @@ export const useJiLatticeStore = defineStore('ji-lattice', () => { ) } }) + const xs = computed({ + get() { + return xCoords.map((x) => x.toFixed(2)).join(' ') + }, + set(value: string) { + xCoords.length = 0 + xCoords.push( + ...value.split(' ').map((v) => { + const c = parseFloat(v) + return isNaN(c) ? 0 : c + }) + ) + } + }) + const ys = computed({ + get() { + return yCoords.map((y) => y.toFixed(2)).join(' ') + }, + set(value: string) { + yCoords.length = 0 + yCoords.push( + ...value.split(' ').map((v) => { + const c = parseFloat(v) + return isNaN(c) ? 0 : c + }) + ) + } + }) + const zs = computed({ + get() { + return zCoords.map((z) => z.toFixed(2)).join(' ') + }, + set(value: string) { + zCoords.length = 0 + zCoords.push( + ...value.split(' ').map((v) => { + const c = parseFloat(v) + return isNaN(c) ? 0 : c + }) + ) + } + }) const edgeMonzos = computed(() => { const numComponents = horizontalCoordinates.length @@ -79,12 +130,31 @@ export const useJiLatticeStore = defineStore('ji-lattice', () => { } }) + const opts3D = WGP9(0) + const xCoords = reactive(opts3D.horizontalCoordinates) + const yCoords = reactive(opts3D.verticalCoordinates) + const zCoords = reactive(opts3D.depthwiseCoordinates) + const depth = ref(100) + + const latticeOptions3D = computed(() => { + return { + horizontalCoordinates: xCoords, + verticalCoordinates: yCoords, + depthwiseCoordinates: zCoords, + maxDistance: maxDistance.value, + edgeMonzos: edgeMonzos.value, + mergeEdges: false + } + }) + watch(rotation, (newValue) => { if (newValue < 0 || newValue >= 360) { rotation.value = mmod(newValue, 360) } }) + // 2D presets + function kraigGrady(equaveIndex = 0) { size.value = 2 const kg = kraigGrady9(equaveIndex) @@ -97,45 +167,112 @@ export const useJiLatticeStore = defineStore('ji-lattice', () => { function scott24(equaveIndex = 0) { size.value = 2 - const logs = LOG_PRIMES.slice(0, 24) - if (equaveIndex !== 0) { - logs.unshift(logs.splice(equaveIndex, 1)[0]) - } - const sd = scottDakota24(logs) + const sd = scottDakota24(equaveIndex) horizontalCoordinates.length = 0 horizontalCoordinates.push(...sd.horizontalCoordinates) verticalCoordinates.length = 0 verticalCoordinates.push(...sd.verticalCoordinates) - edgesString.value = '6/5' + if (equaveIndex === 0) { + edgesString.value = '6/5' + } else { + edgesString.value = '' + } } function pr72(equaveIndex = 0) { size.value = 4 - const logs = LOG_PRIMES.slice(0, 72) - if (equaveIndex !== 0) { - logs.unshift(logs.splice(equaveIndex, 1)[0]) - } - const pr = primeRing72(logs) + const pr = primeRing72(equaveIndex) horizontalCoordinates.length = 0 horizontalCoordinates.push(...pr.horizontalCoordinates) verticalCoordinates.length = 0 verticalCoordinates.push(...pr.verticalCoordinates) - edgesString.value = '6/5' + if (equaveIndex === 0) { + edgesString.value = '6/5' + } else { + edgesString.value = '' + } } function pe72(equaveIndex = 0) { size.value = 4 - const logs = LOG_PRIMES.slice(0, 72) - if (equaveIndex !== 0) { - logs.unshift(logs.splice(equaveIndex, 1)[0]) - } - const pr = primeRing72(logs, false) - align(pr, 1, 2) + const pr = primeRing72(equaveIndex, undefined, false) + align(pr, equaveIndex + 1, equaveIndex + 2) horizontalCoordinates.length = 0 horizontalCoordinates.push(...pr.horizontalCoordinates.map(Math.round)) verticalCoordinates.length = 0 verticalCoordinates.push(...pr.verticalCoordinates.map(Math.round)) - edgesString.value = '6/5' + if (equaveIndex === 0) { + edgesString.value = '6/5' + } else { + edgesString.value = '' + } + } + + // 3D presets + function WGP(equaveIndex = 0) { + size.value = 2 + const w = WGP9(equaveIndex) + xCoords.length = 0 + xCoords.push(...w.horizontalCoordinates) + yCoords.length = 0 + yCoords.push(...w.verticalCoordinates) + zCoords.length = 0 + zCoords.push(...w.depthwiseCoordinates) + edgesString.value = '' + } + + function sphere(equaveIndex = 0, numberOfComponents = 24) { + size.value = 2 + depth.value = 300 + const ps = primeSphere(equaveIndex, LOG_PRIMES.slice(0, numberOfComponents)) + const scale = 3000 + xCoords.length = 0 + xCoords.push(...ps.horizontalCoordinates.map((x) => Math.round(x * scale) / 100)) + yCoords.length = 0 + yCoords.push(...ps.verticalCoordinates.map((y) => Math.round(y * scale) / 100)) + zCoords.length = 0 + zCoords.push(...ps.depthwiseCoordinates.map((z) => Math.round(z * scale) / 100)) + if (equaveIndex === 0) { + edgesString.value = '6/5' + } else { + edgesString.value = '' + } + } + + function pitch(degrees: number) { + const theta = (degrees / 180) * Math.PI + const c = Math.cos(theta) + const s = Math.sin(theta) + for (let i = 0; i < Math.max(yCoords.length, zCoords.length); ++i) { + const y = yCoords[i] ?? 0 + const z = zCoords[i] ?? 0 + yCoords[i] = c * y + s * z + zCoords[i] = c * z - s * y + } + } + + function yaw(degrees: number) { + const theta = (degrees / 180) * Math.PI + const c = Math.cos(theta) + const s = Math.sin(theta) + for (let i = 0; i < Math.max(xCoords.length, zCoords.length); ++i) { + const x = xCoords[i] ?? 0 + const z = zCoords[i] ?? 0 + xCoords[i] = c * x + s * z + zCoords[i] = c * z - s * x + } + } + + function roll(degrees: number) { + const theta = (degrees / 180) * Math.PI + const c = Math.cos(theta) + const s = Math.sin(theta) + for (let i = 0; i < Math.max(xCoords.length, yCoords.length); ++i) { + const x = xCoords[i] ?? 0 + const y = yCoords[i] ?? 0 + xCoords[i] = c * x + s * y + yCoords[i] = c * y - s * x + } } return { @@ -150,16 +287,29 @@ export const useJiLatticeStore = defineStore('ji-lattice', () => { rotation, drawArrows, grayExtras, + xCoords, + yCoords, + zCoords, + depth, // Computed state edgeMonzos, edgesError, horizontals, verticals, latticeOptions, + latticeOptions3D, + xs, + ys, + zs, // Methods kraigGrady, scott24, pr72, - pe72 + pe72, + WGP, + sphere, + pitch, + yaw, + roll } }) diff --git a/src/stores/state.ts b/src/stores/state.ts index 45d2cb4b..30f0fc71 100644 --- a/src/stores/state.ts +++ b/src/stores/state.ts @@ -8,7 +8,7 @@ export const useStateStore = defineStore('state', () => { const heldNotes = reactive(new Map()) const typingActive = ref(true) - const latticeType = ref<'ji' | 'et' | 'cycles' | 'auto'>('auto') + const latticeType = ref<'ji' | 'et' | 'cycles' | '3d' | 'auto'>('auto') // These user preferences are fetched from local storage. const storage = window.localStorage diff --git a/src/views/LatticeView.vue b/src/views/LatticeView.vue index f022b4ef..98bc075a 100644 --- a/src/views/LatticeView.vue +++ b/src/views/LatticeView.vue @@ -11,6 +11,7 @@ import { useJiLatticeStore } from '@/stores/ji-lattice' import { useGridStore } from '@/stores/grid' import { useCyclesStore } from '@/stores/edo-cycles' import { setAndReportValidity } from '@/utils' +import Faux3DLattice from '@/components/Faux3DLattice.vue' const state = useStateStore() const scale = useScaleStore() @@ -21,6 +22,7 @@ const cycles = useCyclesStore() const showConfig = ref(false) const jiPreset = ref<'nothing' | 'grady' | 'grady3' | 'dakota' | 'pr72' | 'pe72'>('nothing') const etPreset = ref<'nothing' | '12' | '53' | '311' | 'b13'>('nothing') +const preset3D = ref<'nothing' | 'WGP' | 'sphere' | 'sphere3'>('nothing') const extraEdgesElement = ref(null) watch(extraEdgesElement, (newElement) => setAndReportValidity(newElement, jiLattice.edgesError), { @@ -91,6 +93,20 @@ watch(etPreset, (newValue) => { } }) +watch(preset3D, (newValue) => { + switch (newValue) { + case 'WGP': + jiLattice.WGP() + return + case 'sphere': + jiLattice.sphere() + return + case 'sphere3': + jiLattice.sphere(1) + return + } +}) + function inferConfig() { // Default to 12-TET even if it looks bad state.latticeType = 'et' @@ -133,11 +149,15 @@ function inferConfig() { if (d === 1 && isPrime(n)) { equaveIndex = primeLimit(n, true) - 1 } + // Set the config for 3D too, but use isometric by default if (limit <= 9) { + jiLattice.WGP(equaveIndex) jiLattice.kraigGrady(equaveIndex) } else if (limit <= 24) { + jiLattice.sphere(equaveIndex, 24) jiLattice.scott24(equaveIndex) } else if (equaveIndex < 72) { + jiLattice.sphere(equaveIndex, 72) jiLattice.pe72(equaveIndex) } else { return @@ -223,6 +243,13 @@ onMounted(() => { :relativeIntervals="scale.latticeIntervals" :heldNotes="heldNotes" /> + Selecting lattice... @@ -252,6 +279,10 @@ onMounted(() => { Cycles + + + 3D + @@ -417,7 +448,7 @@ onMounted(() => { - + Val @@ -439,6 +470,85 @@ onMounted(() => { Show labels + + + Preset + + --Select preset-- + Wilson-Grady-Pakkanen + Prime sphere + Tritave sphere + + + + Maximum connection distance + + + + Object depth + + + + Text size + + + + Text offset + + + + + Show labels + + + + Gray extra edges + + + Extra edges + + + + Pitch + up + down + + + Yaw + left + right + + + Roll + clockwise + counterclockwise + + + Horizontal coordinates + + + + Vertical coordinates + + + + Depthwise coordinates + + + Selecting lattice... @@ -486,6 +596,16 @@ line.edge.auxiliary { stroke-dasharray: 3 1; } +polygon.edge.primary { + fill: var(--color-accent); +} +polygon.edge.custom { + fill: var(--color-accent-deeper); +} +polygon.edge.auxiliary { + fill: var(--color-accent-mute); +} + marker#arrow { fill: var(--color-indicator); } @@ -512,5 +632,6 @@ text.node-label { fill: var(--color-text); text-anchor: middle; stroke: var(--color-background); + dominant-baseline: middle; }