source: main/trunk/model-interfaces-dev/atea/korero-maori-asr/src/components/WordTimingSelector.vue@ 35602

Last change on this file since 35602 was 35602, checked in by cstephen, 3 years ago

Broken attempt to update all words

File size: 11.0 KB
Line 
1<template>
2<div class="word-timing-selector-root">
3 <span>{{ minTimeString }}</span>
4
5 <div class="words-container" v-width="onWordsContainerWidthChanged">
6 <div class="word" v-for="word in mySurroundingWords" :key="word.id" :style="{ left: `${word.left}px`, width: `${word.length}px` }"
7 :class="{ 'hoisted': shouldHoist(word) }">
8 <span>{{ word.word }}</span>
9 </div>
10
11 <div class="word-attachment-container" :style="{ left: leftHandleLeft }">
12 <span>{{ wordStartTimeString }}</span>
13 <span v-if="isMounted" class="material-icons mdi-m word-handle" @mousedown="canAdjustWordStartTime = true">
14 first_page
15 </span>
16 </div>
17
18 <div class="word-attachment-container" :style="{ left: rightHandleLeft }">
19 <span v-if="isMounted" class="material-icons mdi-m word-handle" @mousedown="canAdjustWordEndTime = true">
20 last_page
21 </span>
22 <span>{{ wordEndTimeString }}</span>
23 </div>
24 </div>
25
26 <span>{{ maxTimeString }}</span>
27</div>
28</template>
29
30<style scoped lang="scss">
31.word-timing-selector-root {
32 display: flex;
33 align-items: flex-end;
34 justify-content: center;
35 gap: 1em;
36 height: 3em;
37}
38
39.words-container {
40 flex-grow: 0.6;
41 position: relative;
42 height: 1.5em;
43}
44
45.word {
46 position: absolute;
47 text-align: center;
48
49 user-select: none;
50 text-overflow: ellipsis;
51 white-space: nowrap;
52
53 overflow: hidden;
54 background-color: rgba(var(--bg-color-raw), 0.3);
55 border: 1px solid rgba(var(--bg-color-raw), 0.6);
56}
57
58.word-attachment-container {
59 position: absolute;
60 top: -105%;
61 display: flex;
62 align-items: center
63}
64
65.word-handle {
66 @extend .theme-flat;
67
68 color: var(--fg-color);
69 cursor: pointer;
70 user-select: none;
71
72 animation: pulse 0.8s linear 0s infinite alternate;
73}
74
75@keyframes pulse {
76 from {
77 opacity: 1;
78 }
79 to {
80 opacity: 0.2;
81 }
82}
83
84.hoisted {
85 top: -100%;
86 background-color: var(--highlighted-word-bg);
87}
88</style>
89
90<script>
91import { Word as TWord } from "./TranscriptionItemEditor.vue"
92import Util from "../js/Util"
93
94class Word {
95 constructor(word, startTime, endTime, left, length) {
96 this.id = Util.generateUuid();
97 this.word = word;
98 this.startTime = startTime;
99 this.endTime = endTime;
100 this.left = left;
101 this.length = length;
102 }
103
104 right() {
105 return this.left + this.length;
106 }
107}
108
109export default {
110 name: "WordTimingSelector",
111 props: {
112 /** @type {Array<TWord>} */
113 surroundingWords: Array,
114 wordIndex: Number
115 },
116 data() {
117 return {
118 isMounted: false,
119 canAdjustWordStartTime: false,
120 canAdjustWordEndTime: false,
121 wordsContainerWidth: 0,
122 isProduction: false
123 }
124 },
125 emits: [ "update:surroundingWords" ],
126 computed: {
127 word() {
128 return this.surroundingWords[this.wordIndex];
129 },
130 myWord() {
131 return this.mySurroundingWords[this.wordIndex];
132 },
133 minTimeString() {
134 if (this.surroundingWords.length === 0) {
135 return 0;
136 }
137
138 return Util.formatSecondsTimeString(this.surroundingWords[0].startTime, false, 2);
139 },
140 maxTimeString() {
141 if (this.surroundingWords.length === 0) {
142 return 0;
143 }
144
145 return Util.formatSecondsTimeString(this.surroundingWords[this.surroundingWords.length - 1].endTime, false, 2);
146 },
147 wordStartTimeString() {
148 return Util.formatSecondsTimeString(this.word.startTime, false, 2);
149 },
150 wordEndTimeString() {
151 return Util.formatSecondsTimeString(this.word.endTime, false, 2);
152 },
153 leftHandleLeft() {
154 // The time component of the handles is a fixed width, so we can get away with this horrid hardcoded 5.5em
155 return `calc(${this.myWord.left}px - 5.5em)`;
156 },
157 rightHandleLeft() {
158 return `calc(${this.myWord.left}px + ${this.myWord.length}px)`;
159 },
160 audioSnippetLength() {
161 if (this.surroundingWords.length === 0) {
162 return 0;
163 }
164 else {
165 let length = this.surroundingWords[this.surroundingWords.length - 1].endTime;
166
167 // Removes the buffer from 0s
168 if (this.wordIndex !== 0) {
169 length -= this.surroundingWords[0].startTime;
170 }
171
172 return length;
173 }
174 },
175 /**
176 * Gets the factor used to scale word's audio parameters into on-screen positional values.
177 * @returns {Number} The scaling factor.
178 */
179 scalingFactor() {
180 return this.wordsContainerWidth / this.audioSnippetLength;
181 },
182 /**
183 * Gets the required pixel offset of a word's left position.
184 * @returns {Number} The offset.
185 */
186 wordLeftOffsetPx() {
187 if (this.wordIndex === 0) {
188 return 0;
189 }
190 else {
191 return this.surroundingWords[0].startTime * this.scalingFactor;
192 }
193 },
194 mySurroundingWords() {
195 const myWords = [];
196
197 if (this.surroundingWords.length === 0) {
198 return myWords;
199 }
200
201 for (const word of this.surroundingWords) {
202 myWords.push(this.convertWord(word));
203 }
204
205 return myWords;
206 }
207 },
208 methods: {
209 onWordsContainerWidthChanged(newValue) {
210 this.wordsContainerWidth = newValue;
211 },
212 scaleTime(time) {
213 return time * this.scalingFactor;
214 },
215 timeToPosition(time) {
216 return time * this.scalingFactor - this.wordLeftOffsetPx;
217 },
218 convertWord(word) {
219 const left = this.timeToPosition(word.startTime);
220 const length = (word.endTime - word.startTime) * this.scalingFactor;
221
222 return new Word(word.word, word.startTime, word.endTime, left, length);
223 },
224 shouldHoist(word) {
225 return word === this.myWord;
226 },
227 onDocumentMouseMove(event) {
228 if (this.isProduction) {
229 return;
230 }
231
232 if (this.canAdjustWordStartTime) {
233 this.onMinHandleMouseMove(event);
234 }
235 else if (this.canAdjustWordEndTime) {
236 this.onMaxHandleMouseMove(event);
237 }
238 },
239 /**
240 * @param {MouseEvent} event
241 */
242 onMinHandleMouseMove(event) {
243 if (event.buttons < 1) {
244 return;
245 }
246
247 // const word = this.mySurroundingWords[this.wordIndex];
248
249 // if (event.movementX < 0 && word.left + event.movementX < 0) {
250 // return;
251 // }
252
253 // if (event.movementX > 0 && word.left + event.movementX >= word.right()) {
254 // return;
255 // }
256
257 // word.left += event.movementX;
258 // word.length -= event.movementX;
259
260 this.adjustWordStartTime(event.movementX);
261
262 this.updateSurroundingWords();
263 },
264 onMaxHandleMouseMove(event) {
265 if (event.buttons < 1) {
266 return;
267 }
268
269 const word = this.myWord;
270
271 const lastWord = this.mySurroundingWords[this.mySurroundingWords.length - 1];
272
273 if (event.movementX > 0 && word.right() + event.movementX > lastWord.right()) {
274 return;
275 }
276
277 if (event.movementX < 0 && word.right() + event.movementX <= word.left) {
278 return;
279 }
280
281 word.length += event.movementX;
282
283 this.updateSurroundingWords();
284 },
285 onDocumentMouseUp() {
286 this.canAdjustWordStartTime = false;
287 this.canAdjustWordEndTime = false;
288 },
289 adjustWordStartTime(amount) {
290 const word = this.myWord;
291 word.left += amount;
292 word.length -= amount;
293
294 if (amount < 0) {
295 if (this.wordIndex === 0) {
296 if (word.left > 0) {
297 return;
298 }
299
300 const diff = 0 - word.left;
301 word.left = 0;
302 word.length -= diff;
303 }
304 else {
305 // Adjust previous word
306 const previousWord = this.mySurroundingWords[this.wordIndex - 1];
307
308 if (word.left > previousWord.right()) {
309 return;
310 }
311
312 const diff = word.left - previousWord.right(); // Will be negative
313 word.left -= diff;
314 word.length += diff;
315
316 // TODO: This logic needs all words to be reactively updated to work
317
318 // previousWord.length += amount;
319 // const leftBoundary = previousWord.left + this.timeToPosition(0.1);
320
321 // if (previousWord.right() > leftBoundary) {
322 // return;
323 // }
324
325 // const diff = leftBoundary - previousWord.right(); // Will be negative
326 // word.left -= diff;
327 // word.length += diff;
328 // previousWord.length -= diff;
329 }
330 }
331 else {
332 if (word.left > (word.right() - this.scaleTime(0.1))) {
333 const diff = word.left - (word.right() - this.scaleTime(0.1)) // Will be positive
334 word.left -= diff;
335 word.length += diff;
336 }
337 }
338 },
339 updateSurroundingWords() {
340 const updatedWords = [];
341
342 for (let i = 0; i < this.mySurroundingWords.length; i++) {
343 updatedWords.push(
344 this.convertMyWord(this.mySurroundingWords[i], this.surroundingWords[i])
345 );
346 }
347
348 console.log(updatedWords);
349 this.$emit("update:surroundingWords", updatedWords);
350 },
351 convertMyWord(myWord, actualWord) {
352 const startTime = (myWord.left + this.wordLeftOffsetPx) / this.scalingFactor;
353 const endTime = (myWord.right() + this.wordLeftOffsetPx) / this.scalingFactor;
354
355 return new TWord(actualWord.word, startTime, endTime, actualWord.id, actualWord.shouldHighlight, actualWord.deletionAttempts);
356 }
357 },
358 beforeMount() {
359 this.isProduction = process.env.NODE_ENV === "production";
360 },
361 mounted() {
362 this.isMounted = true;
363
364 document.addEventListener("mousemove", this.onDocumentMouseMove);
365 document.addEventListener("mouseup", this.onDocumentMouseUp);
366 },
367 beforeUnmount() {
368 document.removeEventListener("mousemove", this.onDocumentMouseMove);
369 document.removeEventListener("mouseup", this.onDocumentMouseUp);
370 }
371}
372</script>
Note: See TracBrowser for help on using the repository browser.