Skip to content

Commit

Permalink
(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 7, 2024
1 parent a3ed316 commit 4c27c66
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 56 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
139 changes: 139 additions & 0 deletions src/components/EdoCycles.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<script setup lang="ts">
import { useCyclesStore } from '@/stores/edo-cycles'
import { labelX, labelY } from '@/utils'
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 = useCyclesStore()
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.numCycles
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.numCycles; ++n) {
const xs: number[] = []
const ys: number[] = []
for (let i = 0; i <= store.cycleLength; ++i) {
const theta = dt * (n + store.numCycles * 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 viewScale = computed(() =>
Math.max(2.2, 2.2 + Math.abs(store.labelOffset * store.size) + 1.1 * store.size)
)
const viewBox = computed(
() => `${-viewScale.value} ${-viewScale.value} ${viewScale.value * 2} ${viewScale.value * 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.size * store.labelOffset * labelX(j, v.indices.length)"
: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] }}
</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>
29 changes: 3 additions & 26 deletions src/components/GridLattice.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useGridStore } from '@/stores/grid'
import { debounce } from '@/utils'
import { debounce, labelX, labelY } from '@/utils'
import { spanGrid } from 'ji-lattice'
import { type Interval } from 'sonic-weave'
import { computed, ref, watch } from 'vue'
Expand All @@ -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.size * store.labelOffset * labelX(j, v.indices.length)"
: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"
Expand Down
39 changes: 39 additions & 0 deletions src/stores/edo-cycles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { mmod, modInv } from 'xen-dev-utils'
import { parseVal } from '@/utils'

export const useCyclesStore = defineStore('edo-cycles', () => {
// View
const size = ref(0.15)
const labelOffset = ref(2)
const showLabels = ref(true)

// Elements
const valString = ref('12p')
const generator = ref(7)

const val = computed(() => parseVal(valString.value))

const modulus = computed(() => val.value.divisions.round().valueOf())
const generatorPseudoInverse = computed(() => modInv(generator.value, modulus.value, false))
const numCycles = computed(
() => mmod(generator.value * generatorPseudoInverse.value, modulus.value) || 1
)
const cycleLength = computed(() => modulus.value / numCycles.value)

return {
// State
size,
labelOffset,
showLabels,
valString,
generator,
// Computed state
val,
modulus,
generatorPseudoInverse,
numCycles,
cycleLength
}
})
26 changes: 3 additions & 23 deletions src/stores/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ 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 { Val, evaluateExpression, parseChord } from 'sonic-weave'
import { computedAndError } from '@/utils'
import { parseChord } from 'sonic-weave'
import { computedAndError, parseVal } from '@/utils'
import { FIFTH, THIRD } from '@/constants'

const TWELVE = evaluateExpression('12@', false) as Val

export const useGridStore = defineStore('grid', () => {
// View
const size = ref(0.15)
Expand Down Expand Up @@ -36,25 +34,7 @@ export const useGridStore = defineStore('grid', () => {
const diagonals1 = ref(false)
const diagonals2 = ref(false)

const val = computed<Val>(() => {
try {
const val = evaluateExpression(valString.value)
if (val instanceof Val) {
return val
}
} catch {
/* empty */
}
try {
const val = evaluateExpression(valString.value.trim() + '@')
if (val instanceof Val) {
return val
}
} catch {
/* empty */
}
return TWELVE
})
const val = computed(() => parseVal(valString.value))

const modulus = computed(() => val.value.divisions.round().valueOf())

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 4c27c66

Please sign in to comment.