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

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

Add basic time selection for words

File size: 5.4 KB
Line 
1<template>
2<div class="word-timing-selector-root">
3 <span>{{ minTime }}</span>
4
5 <div class="words-container" ref="wordsContainer">
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 <span v-if="isMounted" class="material-icons mdi-m word-handle" :style="{ left: leftHandleLeft }"
12 @mousemove="onMinHandleMouseMove" @mouseup="onHandleMouseUp" @mousedown="canShiftMinValue = true">
13 first_page
14 </span>
15 <span v-if="isMounted" class="material-icons mdi-m word-handle" :style="{ left: rightHandleLeft }" @mousemove="onMaxHandleMouseMove">last_page</span>
16 </div>
17
18 <span>{{ maxTime }}</span>
19</div>
20</template>
21
22<style scoped lang="scss">
23.word-timing-selector-root {
24 display: flex;
25 align-items: flex-end;
26 justify-content: center;
27 gap: 1em;
28 height: 3em;
29}
30
31.words-container {
32 flex-grow: 0.6;
33 position: relative;
34 height: 1.5em;
35}
36
37.word {
38 position: absolute;
39 text-align: center;
40
41 user-select: none;
42 text-overflow: ellipsis;
43 white-space: nowrap;
44
45 overflow: hidden;
46 background-color: rgba(var(--bg-color-raw), 0.3);
47 border: 1px solid rgba(var(--bg-color-raw), 0.6);
48}
49
50.word-handle {
51 @extend .theme-flat;
52
53 position: absolute;
54 top: -105%;
55
56 color: var(--fg-color);
57 cursor: pointer;
58 user-select: none;
59
60 animation: pulse 0.8s linear 0s infinite alternate;
61}
62
63@keyframes pulse {
64 from {
65 opacity: 1;
66 }
67 to {
68 opacity: 0.2;
69 }
70}
71
72.hoisted {
73 top: -100%;
74 background-color: var(--highlighted-word-bg);
75}
76</style>
77
78<script>
79import Util from "../js/Util"
80
81class Word {
82 constructor(word, startTime, endTime, left, length) {
83 this.id = Util.generateUuid();
84 this.word = word;
85 this.startTime = startTime;
86 this.endTime = endTime;
87 this.left = left;
88 this.length = length;
89 }
90}
91
92export default {
93 name: "WordTimingSelector",
94 props: {
95 /** @type {Array<{ word: String, startTime: Number, endTime: Number }>} */
96 surroundingWords: Array,
97 word: { word: String, startTime: Number, endTime: Number },
98 upperBound: Number
99 },
100 data() {
101 return {
102 isMounted: false,
103 /** @type {Array<Word>} */
104 mySurroundingWords: [],
105 canShiftMinValue: false,
106 canShiftMaxValue: false
107 }
108 },
109 emits: [ "update:word" ],
110 computed: {
111 minTime() {
112 if (this.surroundingWords.length === 0) {
113 return 0;
114 }
115
116 return Util.formatSecondsTimeString(this.surroundingWords[0].startTime, false, 2);
117 },
118 maxTime() {
119 if (this.surroundingWords.length === 0) {
120 return 0;
121 }
122
123 return Util.formatSecondsTimeString(this.surroundingWords[this.surroundingWords.length - 1].endTime, false, 2);
124 },
125 leftHandleLeft() {
126 return `calc(${this.getWord().left}px - 1em)`;
127 },
128 rightHandleLeft() {
129 return `calc(${this.getWord().left}px + ${this.getWord().length}px)`;
130 },
131 wordIndex() {
132 return this.surroundingWords.indexOf(this.word);
133 }
134 },
135 watch: {
136 surroundingWords() {
137 this.getMySurroundingWords();
138 }
139 },
140 methods: {
141 getMySurroundingWords() {
142 if (!this.isMounted) {
143 return;
144 }
145
146 const myWords = [];
147 if (this.surroundingWords.length === 0) {
148 return myWords;
149 }
150
151 const audioLength = this.surroundingWords[this.surroundingWords.length - 1].endTime - this.surroundingWords[0].startTime;
152 const sliderLengthPx = this.$refs.wordsContainer.offsetWidth;
153
154 const scalingFactor = sliderLengthPx / audioLength;
155
156 const offset = this.surroundingWords[0].startTime * scalingFactor;
157
158 for (const word of this.surroundingWords) {
159 const left = (word.startTime * scalingFactor - offset);
160 const length = (word.endTime - word.startTime) * scalingFactor;
161
162 myWords.push(new Word(word.word, word.startTime, word.endTime, left, length));
163 }
164
165 this.mySurroundingWords = myWords;
166 },
167 shouldHoist(word) {
168 return word.startTime === this.word.startTime && word.word === this.word.word;
169 },
170 getWord() {
171 return this.mySurroundingWords[this.wordIndex];
172 },
173 /**
174 * @param {MouseEvent} event
175 */
176 onMinHandleMouseMove(event) {
177 if (event.buttons < 1) {
178 return;
179 }
180
181 const word = this.getWord();
182 word.left += event.movementX;
183 word.length -= event.movementX;
184 },
185 onMaxHandleMouseMove(event) {
186 if (event.buttons < 1) {
187 return;
188 }
189
190 const word = this.getWord();
191 word.length += event.movementX;
192 },
193 onHandleMouseUp() {
194 this.canShiftMinValue = false;
195 this.canShiftMaxValue = false;
196 }
197 },
198 mounted() {
199 this.isMounted = true;
200 this.getMySurroundingWords();
201 }
202}
203</script>
Note: See TracBrowser for help on using the repository browser.