wavesurfer.js 7.0.0-alpha.30 → 7.0.0-alpha.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,7 @@
1
1
  import BasePlugin from '../base-plugin.js';
2
+ import EventEmitter from '../event-emitter.js';
2
3
  import type { WaveSurferPluginParams } from '../wavesurfer.js';
3
- export type RegionsPluginOptions = {
4
- dragSelection?: boolean;
5
- draggable?: boolean;
6
- resizable?: boolean;
7
- };
8
- export type Region = {
9
- startTime: number;
10
- endTime: number;
11
- title: string;
12
- start: number;
13
- end: number;
14
- element: HTMLElement;
15
- };
4
+ export type RegionsPluginOptions = undefined;
16
5
  export type RegionsPluginEvents = {
17
6
  'region-created': {
18
7
  region: Region;
@@ -24,39 +13,72 @@ export type RegionsPluginEvents = {
24
13
  region: Region;
25
14
  };
26
15
  };
16
+ export type RegionEvents = {
17
+ remove: void;
18
+ update: void;
19
+ 'update-end': void;
20
+ play: void;
21
+ click: {
22
+ event: MouseEvent;
23
+ };
24
+ dblclick: {
25
+ event: MouseEvent;
26
+ };
27
+ over: {
28
+ event: MouseEvent;
29
+ };
30
+ leave: {
31
+ event: MouseEvent;
32
+ };
33
+ };
34
+ export type RegionParams = {
35
+ id?: string;
36
+ start: number;
37
+ end?: number;
38
+ drag?: boolean;
39
+ resize?: boolean;
40
+ color?: string;
41
+ content?: string | HTMLElement;
42
+ };
43
+ declare class Region extends EventEmitter<RegionEvents> {
44
+ private totalDuration;
45
+ element: HTMLElement;
46
+ id: string;
47
+ start: number;
48
+ end: number;
49
+ drag: boolean;
50
+ resize: boolean;
51
+ color: string;
52
+ content?: HTMLElement;
53
+ constructor(params: RegionParams, totalDuration: number);
54
+ private initElement;
55
+ private renderPosition;
56
+ private initMouseEvents;
57
+ private onStartMoving;
58
+ private onEndMoving;
59
+ private onUpdate;
60
+ private onMove;
61
+ private onResize;
62
+ private onEndResizing;
63
+ play(): void;
64
+ setTotalDuration(totalDuration: number): void;
65
+ remove(): void;
66
+ }
27
67
  declare class RegionsPlugin extends BasePlugin<RegionsPluginEvents, RegionsPluginOptions> {
28
- private dragStart;
29
- private regionsContainer;
30
68
  private regions;
31
- private createdRegion;
32
- private modifiedRegion;
33
- private isResizingLeft;
34
- private isMoving;
35
- private wasInteractive;
69
+ private regionsContainer;
36
70
  /** Create an instance of RegionsPlugin */
37
71
  constructor(options?: RegionsPluginOptions);
38
72
  static create(options?: RegionsPluginOptions): RegionsPlugin;
39
73
  init(params: WaveSurferPluginParams): void;
40
- /** Set options */
41
- setOptions(options: RegionsPluginOptions): void;
42
- /** Unmount */
43
- destroy(): void;
44
74
  private initRegionsContainer;
45
- private handleMouseDown;
46
- private handleMouseMove;
47
- private handleMouseUp;
48
- private createRegionElement;
49
- private createRegion;
50
- private addRegion;
51
- private updateRegion;
52
- private moveRegion;
53
- /** Create a region at a given start and end time, with an optional title */
54
- add(startTime: number, endTime: number, title?: string, color?: string): Region;
55
- /** Remove a region */
56
- remove(region: Region): void;
57
- /** Set the background color of a region */
58
- setRegionColor(region: Region, color: string): void;
59
- /** Disable or enable mouse events for this region */
60
- setRegionInteractive(region: Region, isInteractive: boolean): void;
75
+ getRegions(): Region[];
76
+ private avoidOverlapping;
77
+ private saveRegion;
78
+ addRegion(options: RegionParams): Region;
79
+ add(start: number, end: number, content?: string, color?: string): Region;
80
+ enableDragSelection(options: RegionParams): void;
81
+ clearRegions(): void;
82
+ destroy(): void;
61
83
  }
62
84
  export default RegionsPlugin;
@@ -1,33 +1,192 @@
1
1
  import BasePlugin from '../base-plugin.js';
2
- const MIN_WIDTH = 10;
3
- const defaultOptions = {
4
- dragSelection: true,
5
- draggable: true,
6
- resizable: true,
7
- };
8
- const style = (element, styles) => {
9
- for (const key in styles) {
10
- element.style[key] = styles[key] || '';
2
+ import EventEmitter from '../event-emitter.js';
3
+ function makeDraggable(element, onStart, onMove, onEnd) {
4
+ if (!element)
5
+ return;
6
+ let preventClickPropagation = false;
7
+ element.addEventListener('click', (event) => {
8
+ preventClickPropagation && event.stopPropagation();
9
+ });
10
+ element.addEventListener('mousedown', (e) => {
11
+ e.stopPropagation();
12
+ let x = e.clientX;
13
+ onStart(x);
14
+ const onMouseMove = (e) => {
15
+ const newX = e.clientX;
16
+ const dx = newX - x;
17
+ x = newX;
18
+ preventClickPropagation = true;
19
+ onMove(dx);
20
+ };
21
+ const onMouseUp = () => {
22
+ onEnd();
23
+ setTimeout(() => (preventClickPropagation = false), 10);
24
+ document.removeEventListener('mousemove', onMouseMove);
25
+ document.removeEventListener('mouseup', onMouseUp);
26
+ };
27
+ document.addEventListener('mousemove', onMouseMove);
28
+ document.addEventListener('mouseup', onMouseUp);
29
+ });
30
+ }
31
+ class Region extends EventEmitter {
32
+ totalDuration;
33
+ element;
34
+ id;
35
+ start;
36
+ end;
37
+ drag;
38
+ resize;
39
+ color;
40
+ content;
41
+ constructor(params, totalDuration) {
42
+ super();
43
+ this.totalDuration = totalDuration;
44
+ this.id = params.id || Math.random().toString(32).slice(2);
45
+ this.start = params.start;
46
+ this.end = params.end ?? params.start;
47
+ this.drag = params.drag ?? true;
48
+ this.resize = params.resize ?? true;
49
+ this.color = params.color ?? 'rgba(0, 0, 0, 0.1)';
50
+ this.element = this.initElement(params.content);
51
+ this.renderPosition();
52
+ this.initMouseEvents();
53
+ }
54
+ initElement(content) {
55
+ const element = document.createElement('div');
56
+ const isMarker = this.start === this.end;
57
+ element.id = this.id;
58
+ element.setAttribute('style', `
59
+ position: absolute;
60
+ height: 100%;
61
+ background-color: ${isMarker ? 'none' : this.color};
62
+ border-left: ${isMarker ? '2px solid ' + this.color : 'none'};
63
+ border-radius: 2px;
64
+ box-sizing: border-box;
65
+ transition: background-color 0.2s ease;
66
+ cursor: ${this.drag ? 'grab' : 'default'};
67
+ pointer-events: all;
68
+ padding: 0.2em ${isMarker ? 0.2 : 0.4}em;
69
+ pointer-events: all;
70
+ `);
71
+ // Init content
72
+ if (content) {
73
+ if (typeof content === 'string') {
74
+ this.content = document.createElement('div');
75
+ this.content.textContent = content;
76
+ }
77
+ else {
78
+ this.content = content;
79
+ }
80
+ element.appendChild(this.content);
81
+ }
82
+ // Add resize handles
83
+ if (!isMarker) {
84
+ const leftHandle = document.createElement('div');
85
+ leftHandle.setAttribute('data-resize', 'left');
86
+ leftHandle.setAttribute('style', `
87
+ position: absolute;
88
+ z-index: 2;
89
+ width: 6px;
90
+ height: 100%;
91
+ top: 0;
92
+ left: 0;
93
+ border-left: 2px solid rgba(0, 0, 0, 0.5);
94
+ border-radius: 2px 0 0 2px;
95
+ cursor: ${this.resize ? 'ew-resize' : 'default'};
96
+ word-break: keep-all;
97
+ `);
98
+ const rightHandle = leftHandle.cloneNode();
99
+ rightHandle.setAttribute('data-resize', 'right');
100
+ rightHandle.style.left = '';
101
+ rightHandle.style.right = '0';
102
+ rightHandle.style.borderRight = rightHandle.style.borderLeft;
103
+ rightHandle.style.borderLeft = '';
104
+ rightHandle.style.borderRadius = '0 2px 2px 0';
105
+ element.appendChild(leftHandle);
106
+ element.appendChild(rightHandle);
107
+ }
108
+ return element;
109
+ }
110
+ renderPosition() {
111
+ const start = this.start / this.totalDuration;
112
+ const end = this.end / this.totalDuration;
113
+ this.element.style.left = `${start * 100}%`;
114
+ this.element.style.width = `${(end - start) * 100}%`;
115
+ }
116
+ initMouseEvents() {
117
+ const { element } = this;
118
+ element.addEventListener('mouseenter', (event) => this.emit('over', { event }));
119
+ element.addEventListener('mouseleave', (event) => this.emit('leave', { event }));
120
+ element.addEventListener('click', (event) => this.emit('click', { event }));
121
+ element.addEventListener('dblclick', (event) => this.emit('dblclick', { event }));
122
+ // Drag
123
+ makeDraggable(element, () => this.onStartMoving(), (dx) => this.onMove(dx), () => this.onEndMoving());
124
+ // Resize
125
+ makeDraggable(element.querySelector('[data-resize="left"]'), () => null, (dx) => this.onResize(dx, 'start'), () => this.onEndResizing());
126
+ makeDraggable(element.querySelector('[data-resize="right"]'), () => null, (dx) => this.onResize(dx, 'end'), () => this.onEndResizing());
11
127
  }
12
- };
13
- const el = (tagName, css) => {
14
- const element = document.createElement(tagName);
15
- style(element, css);
16
- return element;
17
- };
128
+ onStartMoving() {
129
+ if (!this.drag)
130
+ return;
131
+ this.element.style.cursor = 'grabbing';
132
+ }
133
+ onEndMoving() {
134
+ if (!this.drag)
135
+ return;
136
+ this.element.style.cursor = 'grab';
137
+ this.emit('update-end');
138
+ }
139
+ onUpdate(dx, sides) {
140
+ if (!this.element.parentElement)
141
+ return;
142
+ const deltaSeconds = (dx / this.element.parentElement.clientWidth) * this.totalDuration;
143
+ sides.forEach((side) => {
144
+ this[side] += deltaSeconds;
145
+ if (side === 'start') {
146
+ this.start = Math.max(0, Math.min(this.start, this.end));
147
+ }
148
+ else {
149
+ this.end = Math.max(this.start, Math.min(this.end, this.totalDuration));
150
+ }
151
+ });
152
+ this.renderPosition();
153
+ this.emit('update');
154
+ }
155
+ onMove(dx) {
156
+ if (!this.drag)
157
+ return;
158
+ this.onUpdate(dx, ['start', 'end']);
159
+ }
160
+ onResize(dx, side) {
161
+ if (!this.resize)
162
+ return;
163
+ this.onUpdate(dx, [side]);
164
+ }
165
+ onEndResizing() {
166
+ if (!this.resize)
167
+ return;
168
+ this.emit('update-end');
169
+ }
170
+ // Play the region from start to end
171
+ play() {
172
+ this.emit('play');
173
+ }
174
+ setTotalDuration(totalDuration) {
175
+ this.totalDuration = totalDuration;
176
+ this.renderPosition();
177
+ }
178
+ // Remove the region
179
+ remove() {
180
+ this.emit('remove');
181
+ this.element.remove();
182
+ }
183
+ }
18
184
  class RegionsPlugin extends BasePlugin {
19
- dragStart = NaN;
20
- regionsContainer;
21
185
  regions = [];
22
- createdRegion = null;
23
- modifiedRegion = null;
24
- isResizingLeft = false;
25
- isMoving = false;
26
- wasInteractive = true;
186
+ regionsContainer;
27
187
  /** Create an instance of RegionsPlugin */
28
188
  constructor(options) {
29
- super(options || {});
30
- this.options = Object.assign({}, defaultOptions, options);
189
+ super(options);
31
190
  this.regionsContainer = this.initRegionsContainer();
32
191
  }
33
192
  static create(options) {
@@ -38,222 +197,120 @@ class RegionsPlugin extends BasePlugin {
38
197
  if (!this.wavesurfer || !this.wrapper) {
39
198
  throw Error('WaveSurfer is not initialized');
40
199
  }
41
- this.wrapper?.appendChild(this.regionsContainer);
42
- this.wrapper.addEventListener('mousedown', this.handleMouseDown);
43
- document.addEventListener('mousemove', this.handleMouseMove);
44
- document.addEventListener('mouseup', this.handleMouseUp);
45
- }
46
- /** Set options */
47
- setOptions(options) {
48
- this.options = { ...this.options, ...options };
49
- }
50
- /** Unmount */
51
- destroy() {
52
- this.wrapper?.removeEventListener('mousedown', this.handleMouseDown);
53
- document.removeEventListener('mousemove', this.handleMouseMove);
54
- document.removeEventListener('mouseup', this.handleMouseUp, true);
55
- this.regionsContainer.remove();
56
- super.destroy();
200
+ this.wrapper.appendChild(this.regionsContainer);
57
201
  }
58
202
  initRegionsContainer() {
59
- return el('div', {
60
- position: 'absolute',
61
- top: '0',
62
- left: '0',
63
- width: '100%',
64
- height: '100%',
65
- zIndex: '3',
66
- pointerEvents: 'none',
67
- });
203
+ const div = document.createElement('div');
204
+ div.setAttribute('style', `
205
+ position: absolute;
206
+ top: 0;
207
+ left: 0;
208
+ width: 100%;
209
+ height: 100%;
210
+ z-index: 3;
211
+ pointer-events: none;
212
+ `);
213
+ return div;
68
214
  }
69
- handleMouseDown = (e) => {
70
- if (this.options.draggable || this.options.resizable || this.options.dragSelection) {
71
- if (!this.wrapper)
72
- return;
73
- this.dragStart = e.clientX - this.wrapper.getBoundingClientRect().left;
74
- }
75
- };
76
- handleMouseMove = (e) => {
77
- if (!this.wrapper)
78
- return;
79
- const box = this.wrapper.getBoundingClientRect();
80
- const { width } = box;
81
- const dragEnd = e.clientX - box.left;
82
- if (this.options.draggable && this.modifiedRegion && this.isMoving) {
83
- this.moveRegion(this.modifiedRegion, (dragEnd - this.dragStart) / width);
84
- this.dragStart = dragEnd;
85
- return;
86
- }
87
- if (this.options.resizable && this.modifiedRegion) {
88
- this.updateRegion(this.modifiedRegion, this.isResizingLeft ? dragEnd / width : undefined, this.isResizingLeft ? undefined : dragEnd / width);
215
+ getRegions() {
216
+ return this.regions;
217
+ }
218
+ avoidOverlapping(region) {
219
+ if (!region.content)
89
220
  return;
90
- }
91
- if (this.options.dragSelection && !isNaN(this.dragStart)) {
92
- const dragEnd = e.clientX - this.regionsContainer.getBoundingClientRect().left;
93
- if (dragEnd - this.dragStart >= MIN_WIDTH) {
94
- if (!this.createdRegion) {
95
- this.wasInteractive = this.wavesurfer?.options.interact || true;
96
- this.wavesurfer?.toggleInteraction(false);
97
- this.createdRegion = this.createRegion(this.dragStart / width, dragEnd / width);
98
- }
99
- else {
100
- this.updateRegion(this.createdRegion, this.dragStart / width, dragEnd / width);
101
- }
102
- }
103
- }
104
- };
105
- handleMouseUp = () => {
106
- if (this.createdRegion) {
107
- this.addRegion(this.createdRegion);
108
- this.createdRegion = null;
109
- }
110
- this.modifiedRegion = null;
111
- this.isMoving = false;
112
- this.dragStart = NaN;
113
- this.wavesurfer?.toggleInteraction(this.wasInteractive);
114
- };
115
- createRegionElement(start, end, title = '') {
116
- const noWidth = start === end;
117
- const div = el('div', {
118
- position: 'absolute',
119
- left: `${start * 100}%`,
120
- width: `${(end - start) * 100}%`,
121
- height: '100%',
122
- backgroundColor: noWidth ? '' : 'rgba(0, 0, 0, 0.1)',
123
- borderRadius: '2px',
124
- boxSizing: 'border-box',
125
- borderLeft: noWidth ? '2px solid rgba(0, 0, 0, 0.5)' : '',
126
- transition: 'background-color 0.2s ease',
127
- cursor: this.options.draggable ? 'move' : '',
128
- pointerEvents: 'all',
129
- whiteSpace: noWidth ? 'nowrap' : '',
130
- padding: '0.2em',
131
- });
132
- div.textContent = title;
133
- const leftHandle = el('div', {
134
- position: 'absolute',
135
- left: '0',
136
- top: '0',
137
- width: '6px',
138
- height: '100%',
139
- cursor: this.options.resizable ? 'ew-resize' : '',
140
- pointerEvents: 'all',
141
- borderLeft: noWidth ? '' : '2px solid rgba(0, 0, 0, 0.5)',
142
- borderRadius: '2px 0 0 2px',
143
- });
144
- div.appendChild(leftHandle);
145
- const rightHandle = leftHandle.cloneNode();
146
- style(rightHandle, {
147
- left: '',
148
- right: '0',
149
- borderLeft: '',
150
- borderRight: noWidth ? '' : '2px solid rgba(0, 0, 0, 0.5)',
151
- borderRadius: '0 2px 2px 0',
152
- });
153
- div.appendChild(rightHandle);
154
- leftHandle.addEventListener('mousedown', (e) => {
155
- if (!this.options.resizable)
156
- return;
157
- e.stopPropagation();
158
- this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
159
- this.isResizingLeft = true;
160
- this.isMoving = false;
161
- });
162
- rightHandle.addEventListener('mousedown', (e) => {
163
- if (!this.options.resizable)
164
- return;
165
- e.stopPropagation();
166
- this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
167
- this.isResizingLeft = false;
168
- this.isMoving = false;
169
- });
170
- div.addEventListener('mousedown', () => {
171
- if (!this.options.draggable)
172
- return;
173
- this.modifiedRegion = this.regions.find((r) => r.element === div) || null;
174
- this.isMoving = this.modifiedRegion !== null;
175
- });
176
- div.addEventListener('click', () => {
177
- const region = this.regions.find((r) => r.element === div);
178
- if (region) {
179
- this.emit('region-clicked', { region });
180
- }
181
- });
182
- this.regionsContainer.appendChild(div);
183
221
  // Check that the label doesn't overlap with other labels
184
- // If it does, push it down
222
+ // If it does, push it down until it doesn't
223
+ const div = region.content;
185
224
  const labelLeft = div.getBoundingClientRect().left;
186
- const labelWidth = div.scrollWidth;
225
+ const labelWidth = region.element.scrollWidth;
187
226
  const overlap = this.regions
188
227
  .filter((reg) => {
189
- const { left } = reg.element.getBoundingClientRect();
228
+ if (reg === region || !reg.content)
229
+ return false;
230
+ const left = reg.content.getBoundingClientRect().left;
190
231
  const width = reg.element.scrollWidth;
191
232
  return labelLeft < left + width && left < labelLeft + labelWidth;
192
233
  })
193
- .map((reg) => parseFloat(reg.element.style.paddingTop))
234
+ .map((reg) => reg.content?.getBoundingClientRect().height || 0)
194
235
  .reduce((sum, val) => sum + val, 0);
195
- if (overlap > 0) {
196
- div.style.paddingTop = `${overlap + 1}em`;
197
- }
198
- return div;
199
- }
200
- createRegion(start, end, title = '') {
201
- const duration = this.wavesurfer?.getDuration() || 0;
202
- return {
203
- element: this.createRegionElement(start, end, title),
204
- start,
205
- end,
206
- startTime: start * duration,
207
- endTime: end * duration,
208
- title,
209
- };
236
+ div.style.marginTop = `${overlap}px`;
210
237
  }
211
- addRegion(region) {
238
+ saveRegion(region) {
239
+ this.regionsContainer.appendChild(region.element);
240
+ this.avoidOverlapping(region);
212
241
  this.regions.push(region);
213
242
  this.emit('region-created', { region });
243
+ this.subscriptions.push(region.on('update-end', () => {
244
+ this.avoidOverlapping(region);
245
+ this.emit('region-updated', { region });
246
+ }), region.on('play', () => {
247
+ this.wavesurfer?.play();
248
+ this.wavesurfer?.setTime(region.start);
249
+ }), region.on('click', () => {
250
+ this.emit('region-clicked', { region });
251
+ }));
214
252
  }
215
- updateRegion(region, start, end) {
216
- const duration = this.wavesurfer?.getDuration() || 0;
217
- if (start != null) {
218
- region.start = start;
219
- region.element.style.left = `${region.start * 100}%`;
220
- region.element.style.width = `${(region.end - region.start) * 100}%`;
221
- region.startTime = start * duration;
253
+ addRegion(options) {
254
+ if (!this.wavesurfer) {
255
+ throw Error('WaveSurfer is not initialized');
222
256
  }
223
- if (end != null) {
224
- region.end = end;
225
- region.element.style.width = `${(region.end - region.start) * 100}%`;
226
- region.endTime = end * duration;
257
+ const duration = this.wavesurfer.getDuration();
258
+ const region = new Region(options, duration);
259
+ if (!duration) {
260
+ this.subscriptions.push(this.wavesurfer.once('canplay', ({ duration }) => {
261
+ region.setTotalDuration(duration);
262
+ this.saveRegion(region);
263
+ }));
264
+ }
265
+ else {
266
+ this.saveRegion(region);
227
267
  }
228
- this.emit('region-updated', { region });
229
- }
230
- moveRegion(region, delta) {
231
- this.updateRegion(region, region.start + delta, region.end + delta);
232
- }
233
- /** Create a region at a given start and end time, with an optional title */
234
- add(startTime, endTime, title = '', color = '') {
235
- const duration = this.wavesurfer?.getDuration() || 0;
236
- const start = startTime / duration;
237
- const end = endTime / duration;
238
- const region = this.createRegion(start, end, title);
239
- this.addRegion(region);
240
- if (color)
241
- this.setRegionColor(region, color);
242
268
  return region;
243
269
  }
244
- /** Remove a region */
245
- remove(region) {
246
- region.element.remove();
247
- region.element = null;
248
- this.regions = this.regions.filter((r) => r !== region);
270
+ // The same as addRegion but with spread params
271
+ add(start, end, content, color) {
272
+ return this.addRegion({ start, end, content, color });
249
273
  }
250
- /** Set the background color of a region */
251
- setRegionColor(region, color) {
252
- region.element.style[region.startTime === region.endTime ? 'borderColor' : 'backgroundColor'] = color;
274
+ enableDragSelection(options) {
275
+ if (!this.wrapper)
276
+ return;
277
+ const minWidth = 5; // min 5 pixels
278
+ let region = null;
279
+ let startX = 0;
280
+ let sumDx = 0;
281
+ makeDraggable(this.wrapper,
282
+ // On mousedown
283
+ (x) => (startX = x),
284
+ // On mousemove
285
+ (dx) => {
286
+ sumDx += dx;
287
+ if (!this.wavesurfer || !this.wrapper)
288
+ return;
289
+ if (!region && sumDx > minWidth) {
290
+ const duration = this.wavesurfer.getDuration();
291
+ const box = this.wrapper.getBoundingClientRect();
292
+ const start = ((startX + sumDx - box.left) / box.width) * duration;
293
+ region = new Region({
294
+ ...options,
295
+ start,
296
+ end: start,
297
+ }, duration);
298
+ this.regionsContainer.appendChild(region.element);
299
+ }
300
+ if (region) {
301
+ const privateRegion = region;
302
+ privateRegion.onUpdate(dx, ['end']);
303
+ }
304
+ },
305
+ // On mouseup
306
+ () => region && this.saveRegion(region));
307
+ }
308
+ clearRegions() {
309
+ this.regions.forEach((region) => region.remove());
253
310
  }
254
- /** Disable or enable mouse events for this region */
255
- setRegionInteractive(region, isInteractive) {
256
- region.element.style.pointerEvents = isInteractive ? 'all' : 'none';
311
+ destroy() {
312
+ this.clearRegions();
313
+ super.destroy();
257
314
  }
258
315
  }
259
316
  export default RegionsPlugin;