Skip to content

Commit

Permalink
WIP: (Edo) cycle option on Lattice tab
Browse files Browse the repository at this point in the history
ref #678
  • Loading branch information
frostburn committed Jun 6, 2024
1 parent a3ed316 commit b7dbf14
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 7 deletions.
32 changes: 28 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"vue": "^3.3.4",
"vue-router": "^4.3.0",
"webmidi": "^3.1.8",
"xen-dev-utils": "^0.8.0",
"xen-dev-utils": "^0.9.0",
"xen-midi": "^0.2.0"
},
"devDependencies": {
Expand Down
108 changes: 108 additions & 0 deletions src/components/EdoCycles.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { useGridStore } from '@/stores/grid'
import { type MultiVertex } from 'ji-lattice'
import { type Interval } from 'sonic-weave'
import { computed, ref } from 'vue'
import { mmod } from 'xen-dev-utils'
const store = useGridStore()
const props = defineProps<{
relativeIntervals: Interval[]
labels: string[]
colors: string[]
heldNotes: Set<number>
}>()
const svgElement = ref<SVGSVGElement | null>(null)
const steps = computed(() => {
const result: number[] = []
for (const interval of props.relativeIntervals) {
result.push(interval.dot(store.val).valueOf())
}
return result
})
// Multi-label offsets
function lx(n: number, num: number) {
if (num < 3) {
return 0
}
if (num & 1) {
// Odd counts exploit a different starting angle.
return store.labelOffset * store.size * Math.cos((2 * Math.PI * n) / num)
}
// Text tends to extend horizontally so we draw an ellipse.
return 1.5 * store.labelOffset * store.size * Math.sin((2 * Math.PI * n) / num)
}
function ly(n: number, num: number) {
if (num === 1) {
return -store.labelOffset * store.size
}
if (num & 1) {
// Odd counts exploit a different starting angle.
return store.labelOffset * store.size * Math.sin((2 * Math.PI * n) / num)
}
return -store.labelOffset * store.size * Math.cos((2 * Math.PI * n) / num)
}
const vertices = computed(() => {
const m = store.modulus;
const n = store.numEdoCycles;
const gpi = store.generatorPseudoInverse;
const result = new Map<number, MultiVertex>();
const dt = 2 * Math.PI / m;
for (let i = 0; i < steps.value.length; ++i) {
const s = steps.value[i];
const c = mmod(s, n)
const j = mmod((s - c) * gpi, m) + c
const vertex = result.get(j) ?? {
x: Math.sin(dt * j),
y: -Math.cos(dt * j),
indices: []
}
vertex.indices.push(i)
result.set(j, vertex)
}
return Array.from(result.values())
})
</script>

<template>
<svg
ref="svgElement"
class="lattice"
xmlns="http://www.w3.org/2000/svg"
viewBox="-2 -2 4 4"
preserveAspectRatio="xMidYMid meet"
>
<circle
v-for="(v, i) of vertices"
:key="i"
:class="{ node: true, held: v.indices.some((idx) => heldNotes.has(idx)) }"
:cx="v.x"
:cy="v.y"
:r="0.5 * store.size"
:fill="colors[v.indices[0]] ?? 'none'"
:stroke="colors[v.indices[0]] ?? 'none'"
:stroke-width="store.size * 0.1"
/>
<template v-if="store.showLabels">
<template v-for="(v, i) of vertices" :key="i">
<text
v-for="(idx, j) of v.indices"
:key="idx"
class="node-label"
:x="v.x + lx(j, v.indices.length)"
:y="v.y + ly(j, v.indices.length)"
:font-size="`${1.1 * store.size}px`"
:stroke-width="store.size * 0.01"
dominant-baseline="middle"
>
{{ labels[idx] }}
</text>
</template>
</template>
</svg>
</template>
12 changes: 11 additions & 1 deletion src/stores/grid.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { shortestEdge, type GridOptions } from 'ji-lattice'
import { LOG_PRIMES, mmod } from 'xen-dev-utils'
import { LOG_PRIMES, mmod, modInv } from 'xen-dev-utils'
import { Val, evaluateExpression, parseChord } from 'sonic-weave'
import { computedAndError } from '@/utils'
import { FIFTH, THIRD } from '@/constants'
Expand Down Expand Up @@ -36,6 +36,9 @@ export const useGridStore = defineStore('grid', () => {
const diagonals1 = ref(false)
const diagonals2 = ref(false)

// Edo-cycles
const generator = ref(7)

const val = computed<Val>(() => {
try {
const val = evaluateExpression(valString.value)
Expand Down Expand Up @@ -114,6 +117,10 @@ export const useGridStore = defineStore('grid', () => {
}
})

// Edo-cycles
const generatorPseudoInverse = computed(() => modInv(generator.value, modulus.value, false))
const numEdoCycles = computed(() => mmod(generator.value * generatorPseudoInverse.value, modulus.value))

function resetBounds() {
minX.value = -3.1
maxX.value = 3.1
Expand Down Expand Up @@ -287,13 +294,16 @@ export const useGridStore = defineStore('grid', () => {
gridlines2,
diagonals1,
diagonals2,
generator,
// Computed state
edges,
edgesError,
edgeVectors,
val,
modulus,
gridOptions,
generatorPseudoInverse,
numEdoCycles,
// Methods (presets)
square,
squareBP,
Expand Down
2 changes: 1 addition & 1 deletion src/stores/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const useStateStore = defineStore('state', () => {
const heldNotes = reactive(new Map<number, number>())
const typingActive = ref(true)

const latticeType = ref<'ji' | 'et' | 'auto'>('auto')
const latticeType = ref<'ji' | 'et' | 'cycles' | 'auto'>('auto')

// These user preferences are fetched from local storage.
const storage = window.localStorage
Expand Down
22 changes: 22 additions & 0 deletions src/views/LatticeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { computed, onMounted, ref, watch } from 'vue'
import { Fraction, isPrime, mmod, nthPrime, primeLimit } from 'xen-dev-utils'
import GridLattice from '@/components/GridLattice.vue'
import EdoCycles from '@/components/EdoCycles.vue'
import JustIntonationLattice from '@/components/JustIntonationLattice.vue'
import Modal from '@/components/ModalDialog.vue'
import { useStateStore } from '@/stores/state'
Expand Down Expand Up @@ -210,6 +211,13 @@ onMounted(() => {
:relativeIntervals="scale.latticeIntervals"
:heldNotes="heldNotes"
/>
<EdoCycles
v-else-if="state.latticeType === 'cycles'"
:labels="scale.latticeLabels"
:colors="scale.latticeColors"
:relativeIntervals="scale.latticeIntervals"
:heldNotes="heldNotes"
/>
<template v-else>
<h1>Selecting lattice...</h1>
</template>
Expand All @@ -235,6 +243,10 @@ onMounted(() => {
<input type="radio" id="et" value="et" v-model="state.latticeType" />
<label for="et">Equal temperament</label>
</span>
<span>
<input type="radio" id="cycles" value="cycles" v-model="state.latticeType" />
<label for="cycles">Cycles</label>
</span>
</div>
<template v-if="state.latticeType === 'ji'">
<div class="control">
Expand Down Expand Up @@ -400,6 +412,16 @@ onMounted(() => {
<input id="view-y" type="number" step="0.1" v-model="grid.viewCenterY" />
</div>
</template>
<template v-if="state.latticeType === 'cycles'">
<div class="control">
<label for="val">Val</label>
<input type="text" id="val" placeholder="17c" v-model="grid.valString" />
</div>
<div class="control">
<label for="generator">Generator</label>
<input type="number" min="1" v-model="grid.generator" />
</div>
</template>
<template v-else>
<h2>Selecting lattice...</h2>
</template>
Expand Down

0 comments on commit b7dbf14

Please sign in to comment.