11<script lang="ts">
22import {
3- PropType , computed , defineComponent , onMounted , ref , watch ,
3+ PropType , computed , defineComponent , onMounted , onUnmounted , ref , watch ,
44} from ' vue' ;
55import { FMVStore , FMVVectorTypes , getFMVStore } from ' ../map/fmvStore' ;
66import { 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