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

(Edo) cycle option on Lattice tab #730

Merged
merged 1 commit into from
Jun 7, 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
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
Loading