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

Faux 3D lattice with a vanishing point in SVG #739

Merged
merged 1 commit into from
Jun 10, 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
8 changes: 4 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 @@ -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",
Expand Down
1 change: 0 additions & 1 deletion src/components/EdoCycles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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] }}
</text>
Expand Down
207 changes: 207 additions & 0 deletions src/components/Faux3DLattice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<script setup lang="ts">
/**
* Just intonation lattice visualization with a 3D look.
* Not as convincing as OpenGL, but much less bloated than three.js.
*/
import { useJiLatticeStore } from '@/stores/ji-lattice'
import { spanLattice3D } from 'ji-lattice'
import { TimeMonzo, type Interval } from 'sonic-weave'
import { computed, nextTick, reactive, ref, watch } from 'vue'

const EPSILON = 1e-6

/** Near field z-index cutoff */
const NEAR_PLANE = 0.5

const store = useJiLatticeStore()

const props = defineProps<{
relativeIntervals: Interval[]
labels: string[]
colors: string[]
heldNotes: Set<number>
}>()

const svgElement = ref<SVGSVGElement | null>(null)

const viewBox = reactive([-1, -1, 2, 2])

const monzos = computed(() => {
const numComponents = store.horizontalCoordinates.length
const result: number[][] = []
for (const interval of props.relativeIntervals) {
const value = interval.value.clone()
if (value instanceof TimeMonzo) {
value.numberOfComponents = numComponents
const monzo = value.primeExponents.map((pe) => pe.valueOf())
result.push(monzo)
} else {
result.push(Array(numComponents).fill(0))
}
}
return result
})

const lattice = computed(() => {
const result = spanLattice3D(monzos.value, store.latticeOptions3D)
// Center everything so that the vanishing point is at the center of the view box
const inorm = 1 / result.vertices.length
const avgX = result.vertices.reduce((s, v) => s + v.x, 0) * inorm
const avgY = result.vertices.reduce((s, v) => s + v.y, 0) * inorm
const avgZ = result.vertices.reduce((s, v) => s + v.z, 0) * inorm
result.vertices = result.vertices.map((v) => ({
...v,
x: v.x - avgX,
y: v.y - avgY,
z: v.z - avgZ
}))
result.edges = result.edges.map((e) => ({
...e,
x1: e.x1 - avgX,
y1: e.y1 - avgY,
z1: e.z1 - avgZ,
x2: e.x2 - avgX,
y2: e.y2 - avgY,
z2: e.z2 - avgZ
}))
return result
})

watch(
() => [
svgElement.value,
lattice.value,
props.labels,
store.labelOffset,
store.size,
store.showLabels
],
(dependencies) => {
const element = dependencies[0] as unknown as SVGSVGElement
if (!element) {
return
}
// Must wait for Vue to render elements before calculating bounding boxes
nextTick(() => {
viewBox[0] = Infinity
viewBox[1] = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (const el of element.children as unknown as SVGCircleElement[]) {
const { x, y, width, height } = el.getBBox({
stroke: true,
fill: true
})
viewBox[0] = Math.min(viewBox[0], x)
viewBox[1] = Math.min(viewBox[1], y)
maxX = Math.max(maxX, x + width)
maxY = Math.max(maxY, y + height)
}
viewBox[2] = maxX - viewBox[0]
viewBox[3] = maxY - viewBox[1]
viewBox[0] -= store.size * 0.5
viewBox[1] -= store.size * 0.5
viewBox[2] += store.size
viewBox[3] += store.size
})
}
)

const elements = computed(() => {
const result = []
for (const vertex of lattice.value.vertices) {
const z = vertex.z + store.depth
if (z < NEAR_PLANE) {
continue
}
const s = store.depth / z
const cx = vertex.x * s
const cy = vertex.y * s
const r = store.size * s
const node: any = {
tag: 'circle',
cx,
cy,
r,
'stroke-width': 0.1 * r,
class: { node: true, held: props.heldNotes.has(vertex.index!) },
z
}
if (vertex.index === undefined) {
node.class.auxiliary = true
node.r *= 0.4
} else {
node.fill = props.colors[vertex.index]
}
result.push(node)
if (store.showLabels && vertex.index !== undefined) {
result.push({
tag: 'text',
x: cx,
y: cy - store.labelOffset * r,
'font-size': `${3 * r}px`,
'stroke-width': 0.08 * r,
class: { 'node-label': true },
body: props.labels[vertex.index],
z: z - EPSILON
})
}
}
for (const edge of lattice.value.edges) {
const z1 = edge.z1 + store.depth
const z2 = edge.z2 + store.depth
if (z1 < NEAR_PLANE || z2 < NEAR_PLANE) {
continue
}
const s1 = store.depth / z1
const x1 = edge.x1 * s1
const y1 = edge.y1 * s1
const s2 = store.depth / z2
const x2 = edge.x2 * s2
const y2 = edge.y2 * s2
let u = x2 - x1
let v = y2 - y1
const r = Math.hypot(u, v)
if (r < EPSILON) {
continue
}
u /= r
v /= r
if (edge.type === 'auxiliary') {
u *= 0.1
v *= 0.1
} else {
u *= 0.5
v *= 0.5
}
const points = `${x1 + v * s1},${y1 - u * s1} ${x1 - v * s1},${y1 + u * s1} ${x2 - v * s2},${y2 + u * s2} ${x2 + v * s2},${y2 - u * s2}`
if (store.grayExtras && edge.type === 'custom') {
;(edge as any).type = 'border'
}
result.push({
tag: 'polygon',
points,
class: `edge ${edge.type}`,
z: Math.max(z1, z2) + EPSILON
})
}
result.sort((a, b) => b.z - a.z)
return result
})
</script>

<template>
<svg
ref="svgElement"
class="lattice"
xmlns="http://www.w3.org/2000/svg"
:viewBox="viewBox.join(' ')"
preserveAspectRatio="xMidYMid meet"
>
<template v-for="(element, i) of elements" :key="i">
<circle v-if="element.tag === 'circle'" v-bind="element" />
<polygon v-if="element.tag === 'polygon'" v-bind="element" />
<text v-if="element.tag === 'text'" v-bind="element">{{ element.body }}</text>
</template>
</svg>
</template>
1 change: 0 additions & 1 deletion src/components/GridLattice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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] }}
</text>
Expand Down
1 change: 0 additions & 1 deletion src/components/JustIntonationLattice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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`"
Expand Down
Loading
Loading