mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(annotations): render the screen-space annotation board from a store
The screen annotation renderer keeps each mark's SVG parts as attribute records and presents the board to a store after every rendered frame; a view teleports it to the page and hands back the callout text and mark groups the renderer measures. The tracked-entity fade is measured once the frame is in the document, removed marks fade out before they leave, and the styles it injected move to annotations.css.
This commit is contained in:
1 parent
265aa605bd
commit
662f51fdf3
13 files changed
+768
-672
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { markRaw, shallowRef } from 'vue';
|
||||
|
||||
/**
|
||||
* The screen-space annotation board as its view renders it. The screen
|
||||
* annotation renderer (src/annotations/screenAnnotationRenderer.js) presents
|
||||
* every mark's SVG parts after each rendered frame; the view hands back the
|
||||
* elements the renderer measures and reports when a draw-on stroke finishes.
|
||||
*/
|
||||
export const useScreenAnnotationsStore = defineStore(
|
||||
'gev-screen-annotations',
|
||||
() => {
|
||||
/**
|
||||
* `{owner, viewBox, markers, marks}`, or null while no renderer exists.
|
||||
* Each mark is `{key, className, opacity, display, drawn, children}`;
|
||||
* `owner` is passed back with each element and report.
|
||||
*/
|
||||
const board = shallowRef(null);
|
||||
|
||||
/** @param {?object} next A fresh presentation; every publish re-renders. */
|
||||
function publish(next) {
|
||||
board.value = next && markRaw(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* The controls whose `attach(owner, key, kind, element)` and
|
||||
* `drawn(owner, key)` the view calls.
|
||||
*/
|
||||
const controls = shallowRef(null);
|
||||
|
||||
/** @param {?object} next The controls, or null to disconnect. */
|
||||
function connect(next) {
|
||||
controls.value = next && markRaw(next);
|
||||
}
|
||||
|
||||
return { board, publish, controls, connect };
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/* ── Screen-space annotations (src/annotations/screenAnnotationRenderer.js) ── */
|
||||
.gev-screen-whiteboard { position: fixed; inset: 0; pointer-events: none; z-index: 90; }
|
||||
.gev-screen-whiteboard-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
||||
/* No CSS opacity transition here on purpose: group opacity is driven per-frame in JS
|
||||
(the 260ms fade-in via computeAlpha, and the tracked-entity z-order ease). A CSS
|
||||
transition fought those per-frame writes and made the tracked-fade opacity oscillate
|
||||
(flicker). Fade-OUT keeps its own transition via .gev-anno.gev-out below. */
|
||||
.gev-anno-ring { filter: drop-shadow(0 0 6px currentColor); }
|
||||
.gev-anno-dot { filter: drop-shadow(0 0 5px rgba(255,255,255,0.6)); }
|
||||
.gev-anno-area { filter: drop-shadow(0 0 5px currentColor); }
|
||||
.gev-anno-arrow { filter: drop-shadow(0 0 4px currentColor); stroke-linecap: round; }
|
||||
.gev-anno-leader { stroke-dasharray: 2 3; }
|
||||
.gev-anno-card { fill: rgba(8,18,28,0.78); stroke: rgba(255,255,255,0.14); stroke-width: 1; }
|
||||
.gev-anno-text { fill: #eaf6ff; font: 600 13px "JetBrains Mono", ui-monospace, monospace; letter-spacing: 0.02em; }
|
||||
/* draw-on: outlined shapes reveal their stroke */
|
||||
.gev-draw { stroke-dasharray: 1400; stroke-dashoffset: 1400; }
|
||||
.gev-anno.gev-in .gev-draw { transition: stroke-dashoffset 900ms ease-out; stroke-dashoffset: 0; }
|
||||
/* opacity-only fade-in: a CSS transform here would override the SVG transform
|
||||
attribute used to POSITION the callout and snap it to 0,0. */
|
||||
.gev-anno-callout { opacity: 0; }
|
||||
.gev-anno.gev-in .gev-anno-callout { animation: gev-pop 320ms ease-out both; }
|
||||
.gev-anno.gev-out { opacity: 0 !important; transition: opacity 320ms ease; }
|
||||
.gev-pulse { animation: gev-ring-pulse 1.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
|
||||
@keyframes gev-ring-pulse { 0%,100% { opacity: 0.9; stroke-width: 2; } 50% { opacity: 0.35; stroke-width: 3.5; } }
|
||||
@keyframes gev-pop { from { opacity: 0; } to { opacity: 1; } }
|
||||
@@ -12,6 +12,7 @@ import SceneInteractions from './panels/SceneInteractions.vue';
|
||||
import SplitFlapText from './panels/SplitFlapText.vue';
|
||||
import WindInspectionMarker from './panels/WindInspectionMarker.vue';
|
||||
import CelestialRing from './panels/CelestialRing.vue';
|
||||
import ScreenAnnotations from './panels/ScreenAnnotations.vue';
|
||||
|
||||
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
|
||||
const { style } = storeToRefs(useDisplayStore());
|
||||
@@ -106,4 +107,5 @@ const vToastText = {
|
||||
<SceneInteractions />
|
||||
<WindInspectionMarker />
|
||||
<MissionReplayOverlay />
|
||||
<ScreenAnnotations />
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useScreenAnnotationsStore } from '../../stores/screenAnnotations.js';
|
||||
|
||||
// The screen-space annotation board over the page, as the screen annotation
|
||||
// renderer (src/annotations/screenAnnotationRenderer.js) presents it.
|
||||
const { board, controls } = storeToRefs(useScreenAnnotationsStore());
|
||||
|
||||
// Each element goes to the renderer it was rendered for, which measures it.
|
||||
const vAnnoElement = {
|
||||
mounted(el, { value }) {
|
||||
controls.value?.attach(value.owner, value.key, value.kind, el);
|
||||
},
|
||||
beforeUnmount(el, { value }) {
|
||||
controls.value?.attach(value.owner, value.key, value.kind, null);
|
||||
},
|
||||
};
|
||||
// The draw-on stroke reads solid once its dashoffset transition ends.
|
||||
const onTransitionEnd = (owner, key) => (event) => {
|
||||
if (event.propertyName === 'stroke-dashoffset')
|
||||
controls.value?.drawn(owner, key);
|
||||
};
|
||||
const display = (value) => (value ? { display: value } : undefined);
|
||||
const drawStyle = (mark, child) =>
|
||||
mark.drawn && child.attrs.class?.includes('gev-draw')
|
||||
? { strokeDasharray: 'none', ...display(child.display) }
|
||||
: display(child.display);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="board" to="body">
|
||||
<div class="gev-screen-whiteboard"><svg class="gev-screen-whiteboard-svg" preserveAspectRatio="none" :viewBox="board.viewBox ?? undefined"><defs><filter id="gev-sketch" x="-20%" y="-20%" width="140%" height="140%"><feTurbulence type="fractalNoise" baseFrequency="0.014" numOctaves="2" seed="7" result="n" /><feDisplacementMap in="SourceGraphic" in2="n" scale="2.2" xChannelSelector="R" yChannelSelector="G" /></filter><marker v-for="marker in board.markers" :id="`gev-arrow-${marker.key}`" :key="marker.key" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" :fill="marker.fill" /></marker></defs><g v-for="mark in board.marks" :key="mark.key" v-anno-element="{ owner: board.owner, key: mark.key, kind: 'group' }" :class="mark.className" :opacity="mark.opacity" :style="display(mark.display)" :onTransitionend="onTransitionEnd(board.owner, mark.key)"><template v-for="child in mark.children" :key="child.key"><polygon v-if="child.tag === 'polygon'" v-bind="child.attrs" :style="drawStyle(mark, child)" /><polyline v-else-if="child.tag === 'polyline'" v-bind="child.attrs" :style="drawStyle(mark, child)" /><path v-else-if="child.tag === 'path'" v-bind="child.attrs" :style="drawStyle(mark, child)" /><circle v-else-if="child.tag === 'circle'" v-bind="child.attrs" :style="display(child.display)" /><line v-else-if="child.tag === 'line'" v-bind="child.attrs" :style="display(child.display)" /><g v-else-if="child.tag === 'callout'" class="gev-anno-callout" :transform="child.transform ?? undefined"><rect rx="5" class="gev-anno-card" v-bind="child.card" /><rect class="gev-anno-accent" width="3" rx="1.5" :fill="child.fill" v-bind="child.accent" /><text v-anno-element="{ owner: board.owner, key: mark.key, kind: 'text' }" class="gev-anno-text" x="9" y="16">{{ child.text }}</text></g></template></g></svg></div>
|
||||
</Teleport>
|
||||
</template>
|
||||
Reference in new issue
Block a user