Skip to content

Commit 222def1

Browse files
committed
UI updates
1 parent 6839557 commit 222def1

5 files changed

Lines changed: 348 additions & 67 deletions

File tree

client/src/components/FMVLayerConfig.vue

Lines changed: 202 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import {
3-
PropType, computed, defineComponent, onMounted, ref, watch,
3+
PropType, computed, defineComponent, onMounted, onUnmounted, ref, watch,
44
} from 'vue';
55
import { FMVStore, FMVVectorTypes, getFMVStore } from '../map/fmvStore';
66
import { FMVLayer } from '../types';
@@ -35,6 +35,34 @@ export default defineComponent({
3535
},
3636
});
3737
38+
const lockZoom = computed({
39+
get: () => fmvStore.value?.lockZoom ?? false,
40+
set: (val) => {
41+
if (fmvStore.value) {
42+
fmvStore.value.lockZoom = val;
43+
}
44+
},
45+
});
46+
47+
const zoomBounds = computed({
48+
get: () => fmvStore.value?.zoomBounds ?? 1.5,
49+
set: (val) => {
50+
if (fmvStore.value) {
51+
fmvStore.value.zoomBounds = val;
52+
}
53+
},
54+
});
55+
56+
const opacity = computed({
57+
get: () => fmvStore.value?.opacity ?? 0,
58+
set: (val) => {
59+
if (fmvStore.value) {
60+
fmvStore.value.opacity = val;
61+
updateFMVLayer(props.layer);
62+
}
63+
},
64+
});
65+
3866
const visibleProperties = computed<(FMVVectorTypes | 'video')[]>({
3967
get: () => fmvStore.value?.visibleProperties ?? [],
4068
set: (val) => {
@@ -63,25 +91,112 @@ export default defineComponent({
6391
}
6492
});
6593
66-
const isPlaying = computed(() => !fmvStore.value?.videoSource?.paused);
94+
const isPlaying = computed(() => fmvStore.value?.videoState === 'playing');
6795
6896
function togglePlayback() {
69-
const video = fmvStore.value?.videoSource;
70-
if (!video) return;
71-
if (video.paused) {
72-
video.play();
73-
} else {
74-
video.pause();
97+
if (fmvStore.value) {
98+
const newState = fmvStore.value.videoState === 'playing' ? 'pause' : 'playing';
99+
fmvStore.value.setVideoState(newState);
75100
}
76101
}
77102
78-
function seekFrames(offset: number) {
103+
function seekOffset(offset: number) {
79104
const store = fmvStore.value;
80105
if (!store) return;
81-
store.seekFrames(offset);
106+
store.seekOffset(offset);
82107
updateFMVVideoMapping(props.layer);
83108
}
84109
110+
let keyHoldInterval: ReturnType<typeof setInterval> | null = null;
111+
let keyHoldStartTime = 0;
112+
113+
function getFrameJump(): number {
114+
const heldDuration = Date.now() - keyHoldStartTime;
115+
const secondsHeld = heldDuration / 1000;
116+
117+
// Increase jump size linearly, maxing out at 100 frames after ~20s
118+
return Math.min(100, Math.floor(1 + secondsHeld * 5));
119+
}
120+
121+
function stopFrameJump() {
122+
if (keyHoldInterval) {
123+
clearInterval(keyHoldInterval);
124+
keyHoldInterval = null;
125+
}
126+
}
127+
128+
function jumpFrame(direction: 'left' | 'right', jump: number = 1) {
129+
if (!fmvStore.value) return;
130+
131+
const total = fmvStore.value.videoData.totalFrames;
132+
let newFrameId = fmvStore.value.frameId + (direction === 'right' ? jump : -jump);
133+
134+
// Wrap around
135+
if (newFrameId < 0) newFrameId = total - 1;
136+
if (newFrameId >= total) newFrameId = 0;
137+
138+
fmvStore.value.frameId = newFrameId;
139+
updateFMVLayer(props.layer);
140+
updateFMVVideoMapping(props.layer);
141+
}
142+
143+
function startFrameJump(direction: 'left' | 'right') {
144+
if (!fmvStore.value) return;
145+
146+
keyHoldStartTime = Date.now();
147+
stopFrameJump(); // In case it's already running
148+
149+
// Start the accelerating jump loop
150+
keyHoldInterval = setInterval(() => {
151+
const jump = getFrameJump();
152+
jumpFrame(direction, jump);
153+
}, 100); // Tune this for responsiveness
154+
}
155+
156+
function handleKeydown(event: KeyboardEvent) {
157+
if (!fmvStore.value) return;
158+
159+
switch (event.code) {
160+
case 'Space':
161+
event.preventDefault();
162+
togglePlayback();
163+
break;
164+
case 'ArrowLeft':
165+
if (!keyHoldInterval) {
166+
// Do one frame step immediately
167+
jumpFrame('left', 1);
168+
startFrameJump('left');
169+
}
170+
break;
171+
case 'ArrowRight':
172+
if (!keyHoldInterval) {
173+
// Do one frame step immediately
174+
jumpFrame('right', 1);
175+
startFrameJump('right');
176+
}
177+
break;
178+
default:
179+
break;
180+
}
181+
}
182+
183+
function handleKeyup(event: KeyboardEvent) {
184+
if (event.code === 'ArrowLeft' || event.code === 'ArrowRight') {
185+
stopFrameJump();
186+
}
187+
}
188+
189+
onMounted(() => {
190+
window.addEventListener('keydown', handleKeydown);
191+
window.addEventListener('keyup', handleKeyup);
192+
});
193+
194+
onUnmounted(() => {
195+
window.removeEventListener('keydown', handleKeydown);
196+
window.removeEventListener('keyup', handleKeyup);
197+
stopFrameJump();
198+
});
199+
85200
return {
86201
frameId,
87202
visibleProperties,
@@ -91,8 +206,10 @@ export default defineComponent({
91206
loaded,
92207
isPlaying,
93208
togglePlayback,
94-
seekFrames,
95-
209+
seekOffset,
210+
opacity,
211+
lockZoom,
212+
zoomBounds,
96213
};
97214
},
98215
});
@@ -103,40 +220,98 @@ export default defineComponent({
103220
<v-card-text v-if="loaded">
104221
<v-row dense align="center" class="icon-row mb-2">
105222
<v-col class="d-flex align-center gap-2">
106-
<v-btn icon variant="plain" size="small" @click="seekFrames(-frameId)">
223+
<v-btn icon variant="plain" size="small" @click="seekOffset(-frameId)">
107224
<v-icon>mdi-skip-backward</v-icon>
108225
</v-btn>
109-
<v-btn icon variant="plain" size="small" @click="seekFrames(-1)">
226+
<v-btn icon variant="plain" size="small" @click="seekOffset(-1)">
110227
<v-icon>mdi-step-backward</v-icon>
111228
</v-btn>
112229
<v-btn icon variant="plain" size="small" @click="togglePlayback">
113230
<v-icon>{{ isPlaying ? 'mdi-pause' : 'mdi-play' }}</v-icon>
114231
</v-btn>
115-
<v-btn icon variant="plain" size="small" @click="seekFrames(1)">
232+
<v-btn icon variant="plain" size="small" @click="seekOffset(1)">
116233
<v-icon>mdi-step-forward</v-icon>
117234
</v-btn>
118-
<v-btn icon variant="plain" size="small" @click="seekFrames(totalFrames - frameId)">
235+
<v-btn icon variant="plain" size="small" @click="seekOffset(totalFrames - frameId)">
119236
<v-icon>mdi-skip-forward</v-icon>
120237
</v-btn>
238+
<v-chip>{{ frameId }}</v-chip>
239+
<v-btn
240+
v-tooltip="'Filter Frames by frameId'"
241+
icon
242+
variant="plain"
243+
size="small"
244+
@click="filterFrameStatus = !filterFrameStatus"
245+
>
246+
<v-icon :color="filterFrameStatus ? 'primary' : ''">
247+
mdi-filter
248+
</v-icon>
249+
</v-btn>
121250
</v-col>
122251
</v-row>
123-
<!-- Frame ID Slider -->
124252
<v-row dense align="center">
125-
<v-col cols="3">
126-
<span>Frame ID:</span>
253+
<v-slider
254+
v-model="frameId"
255+
:min="0"
256+
:max="totalFrames"
257+
step="1"
258+
thumb-label
259+
:disabled="totalFrames === 0"
260+
/>
261+
</v-row>
262+
<v-divider />
263+
264+
<v-row dense align="center">
265+
<v-col cols="1">
266+
<v-tooltip text="Opacity">
267+
<template #activator="{ props }">
268+
<v-icon
269+
class="pl-3"
270+
v-bind="props"
271+
>
272+
mdi-square-opacity
273+
</v-icon>
274+
</template>
275+
</v-tooltip>
276+
</v-col>
277+
<v-col>
278+
<v-slider
279+
v-model="opacity"
280+
density="compact"
281+
hide-details
282+
class="opacity-slider"
283+
min="0"
284+
max="1.0"
285+
/>
286+
</v-col>
287+
</v-row>
288+
<v-row dense align="center">
289+
<v-col cols="1">
290+
<v-tooltip text="Lock Camera to Video, Bounds multiple Value">
291+
<template #activator="{ props }">
292+
<v-icon
293+
class="px-3"
294+
v-bind="props"
295+
:color="lockZoom ? 'primary' : ''"
296+
@click="lockZoom = !lockZoom"
297+
>
298+
{{ lockZoom ? 'mdi-lock-outline' : 'mdi-lock-open-outline' }}
299+
</v-icon>
300+
</template>
301+
</v-tooltip>
127302
</v-col>
128303
<v-col>
129304
<v-slider
130-
v-model="frameId"
131-
:min="0"
132-
:max="totalFrames"
133-
step="1"
134-
thumb-label
135-
:disabled="totalFrames === 0"
305+
v-model="zoomBounds"
306+
density="compact"
307+
hide-details
308+
:min="1"
309+
:max="5"
310+
step="0.25"
136311
/>
137312
</v-col>
138313
<v-col cols="2">
139-
<span>{{ frameId }}</span>
314+
{{ zoomBounds }}
140315
</v-col>
141316
</v-row>
142317

@@ -149,26 +324,14 @@ export default defineComponent({
149324
<v-select
150325
v-model="visibleProperties"
151326
:items="allProperties"
327+
hide-details
152328
label="Visible Properties"
153329
multiple
154330
chips
155331
clearable
156332
/>
157333
</v-col>
158334
</v-row>
159-
160-
<!-- Filter by Frame Checkbox -->
161-
<v-row dense align="center">
162-
<v-col cols="3">
163-
<span>Filter by Frame:</span>
164-
</v-col>
165-
<v-col>
166-
<v-checkbox
167-
v-model="filterFrameStatus"
168-
label="Enable Frame Filtering"
169-
/>
170-
</v-col>
171-
</v-row>
172335
</v-card-text>
173336
<v-card-text v-else>
174337
Loading FMV Layer controls...

0 commit comments

Comments
 (0)