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 231d62f
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 32 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
134 changes: 134 additions & 0 deletions src/components/EdoCycles.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<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 RADIUS = 2
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
})
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: RADIUS * Math.sin(dt * j),
y: -RADIUS * Math.cos(dt * j),
indices: []
}
vertex.indices.push(i)
result.set(j, vertex)
}
return Array.from(result.values())
})
const cycles = computed(() => {
const result: string[] = []
const dt = (2 * Math.PI) / store.modulus
for (let n = 0; n < store.numEdoCycles; ++n) {
const xs: number[] = []
const ys: number[] = []
for (let i = 0; i <= store.cycleLength; ++i) {
const theta = dt * (n + store.numEdoCycles * i)
xs.push(RADIUS * Math.sin(theta))
ys.push(-RADIUS * Math.cos(theta))
}
let d = `M ${xs[0]} ${ys[0]} `
for (let i = 0; i < store.cycleLength; ++i) {
d += `Q ${0.45 * (xs[i] + xs[i + 1])} ${0.45 * (ys[i] + ys[i + 1])} ${xs[i + 1]} ${ys[i + 1]} `
}
d += 'Z'
result.push(d)
}
return result
})
const viewBox = computed(
() => `${-store.viewScale} ${-store.viewScale} ${store.viewScale * 2} ${store.viewScale * 2}`
)
</script>

<template>
<svg
ref="svgElement"
class="lattice"
xmlns="http://www.w3.org/2000/svg"
:viewBox="viewBox"
preserveAspectRatio="xMidYMid meet"
>
<path v-for="(d, i) of cycles" :key="i" :d="d" stroke-width="0.03" />
<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.7 * 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 + store.lx(j, v.indices.length)"
:y="v.y + store.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>

<style scoped>
path {
fill: none;
}
path:nth-child(5n + 1) {
stroke: var(--color-bright-indicator);
}
path:nth-child(5n + 2) {
stroke: var(--color-accent);
}
path:nth-child(5n + 3) {
stroke: var(--color-dark-indicator);
}
path:nth-child(5n + 4) {
stroke: var(--color-accent-mute);
}
path:nth-child(5n) {
stroke: var(--color-border);
}
</style>
27 changes: 2 additions & 25 deletions src/components/GridLattice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,6 @@ const steps = computed(() => {
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 grid = computed(() => {
const result = spanGrid(steps.value, store.gridOptions)
return result
Expand Down Expand Up @@ -145,8 +122,8 @@ watch(
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)"
:x="v.x + store.lx(j, v.indices.length)"
:y="v.y + store.ly(j, v.indices.length)"
:font-size="`${2.5 * store.size}px`"
:stroke-width="store.size * 0.05"
dominant-baseline="middle"
Expand Down
82 changes: 81 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,33 @@ export const useGridStore = defineStore('grid', () => {
const diagonals1 = ref(false)
const diagonals2 = ref(false)

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

const labelOffsetCacheX = computed(() => {
const result: number[][] = []
for (let num = 1; num < 6; ++num) {
const xs: number[] = []
for (let n = 0; n < num; ++n) {
xs.push(labelX(n, num))
}
result.push(xs)
}
return result
})

const labelOffsetCacheY = computed(() => {
const result: number[][] = []
for (let num = 1; num < 6; ++num) {
const ys: number[] = []
for (let n = 0; n < num; ++n) {
ys.push(labelY(n, num))
}
result.push(ys)
}
return result
})

const val = computed<Val>(() => {
try {
const val = evaluateExpression(valString.value)
Expand Down Expand Up @@ -114,6 +141,53 @@ 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)
)
const cycleLength = computed(() => modulus.value / numEdoCycles.value)

// Multi-label offsets
function labelX(n: number, num: number) {
if (num < 3) {
return 0
}
const r1 = labelOffset.value * size.value
if (num & 1) {
// Odd counts exploit a different starting angle.
return r1 * Math.cos((2 * Math.PI * n) / num)
}
// Text tends to extend horizontally so we draw an ellipse.
return 1.5 * r1 * Math.sin((2 * Math.PI * n) / num)
}

function labelY(n: number, num: number) {
const r = labelOffset.value * size.value
if (num === 1) {
return -r
}
if (num & 1) {
// Odd counts exploit a different starting angle.
return r * Math.sin((2 * Math.PI * n) / num)
}
return -r * Math.cos((2 * Math.PI * n) / num)
}

function lx(n: number, num: number) {
if (num < 6) {
return labelOffsetCacheX.value[num][n]
}
return labelX(n, num)
}

function ly(n: number, num: number) {
if (num < 6) {
return labelOffsetCacheY.value[num][n]
}
return labelY(n, num)
}

function resetBounds() {
minX.value = -3.1
maxX.value = 3.1
Expand Down Expand Up @@ -287,13 +361,19 @@ export const useGridStore = defineStore('grid', () => {
gridlines2,
diagonals1,
diagonals2,
generator,
// Computed state
lx,
ly,
edges,
edgesError,
edgeVectors,
val,
modulus,
gridOptions,
generatorPseudoInverse,
numEdoCycles,
cycleLength,
// 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
Loading

0 comments on commit 231d62f

Please sign in to comment.