levita-js 0.1.6 → 0.2.1
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.
- package/dist/index.cjs +98 -44
- package/dist/index.d.cts +30 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +30 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +98 -44
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -184,27 +184,34 @@ const cleanupLayers = (layers) => {
|
|
|
184
184
|
*/
|
|
185
185
|
var MotionSensor = class MotionSensor {
|
|
186
186
|
onMove;
|
|
187
|
+
onFirstEvent;
|
|
187
188
|
axis;
|
|
188
189
|
active = false;
|
|
189
190
|
permitted = false;
|
|
191
|
+
receivedEvent = false;
|
|
192
|
+
warmup = false;
|
|
190
193
|
minAngle;
|
|
191
194
|
maxAngle;
|
|
192
195
|
smoothing;
|
|
193
196
|
lastX = 0;
|
|
194
197
|
lastY = 0;
|
|
198
|
+
baseBeta = null;
|
|
199
|
+
baseGamma = null;
|
|
195
200
|
/**
|
|
196
201
|
* @param onMove - Callback receiving normalized { x, y } values
|
|
197
202
|
* @param axis - Restrict input to a single axis, or null for both
|
|
198
203
|
* @param minAngle - Minimum device angle mapped to -1 (default: -45)
|
|
199
204
|
* @param maxAngle - Maximum device angle mapped to 1 (default: 45)
|
|
200
205
|
* @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)
|
|
206
|
+
* @param onFirstEvent - Called once when the first valid deviceorientation event is received
|
|
201
207
|
*/
|
|
202
|
-
constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15) {
|
|
208
|
+
constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15, onFirstEvent = null) {
|
|
203
209
|
this.onMove = onMove;
|
|
204
210
|
this.axis = axis;
|
|
205
211
|
this.minAngle = minAngle;
|
|
206
212
|
this.maxAngle = maxAngle;
|
|
207
213
|
this.smoothing = smoothing;
|
|
214
|
+
this.onFirstEvent = onFirstEvent;
|
|
208
215
|
}
|
|
209
216
|
/** Check if the DeviceOrientationEvent API is available in this environment. */
|
|
210
217
|
static isSupported = () => {
|
|
@@ -243,6 +250,10 @@ var MotionSensor = class MotionSensor {
|
|
|
243
250
|
start = () => {
|
|
244
251
|
if (this.active || !this.permitted) return;
|
|
245
252
|
this.active = true;
|
|
253
|
+
this.receivedEvent = false;
|
|
254
|
+
this.warmup = false;
|
|
255
|
+
this.baseBeta = null;
|
|
256
|
+
this.baseGamma = null;
|
|
246
257
|
window.addEventListener("deviceorientation", this.handleOrientation);
|
|
247
258
|
};
|
|
248
259
|
/** Stop listening and remove the deviceorientation event listener. */
|
|
@@ -264,11 +275,26 @@ var MotionSensor = class MotionSensor {
|
|
|
264
275
|
*/
|
|
265
276
|
handleOrientation = (e) => {
|
|
266
277
|
const evt = e;
|
|
278
|
+
if (evt.beta === null && evt.gamma === null) return;
|
|
279
|
+
if (!this.warmup) {
|
|
280
|
+
this.warmup = true;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
267
283
|
const beta = evt.beta ?? 0;
|
|
268
284
|
const gamma = evt.gamma ?? 0;
|
|
285
|
+
if (!this.receivedEvent) {
|
|
286
|
+
this.receivedEvent = true;
|
|
287
|
+
this.onFirstEvent?.();
|
|
288
|
+
}
|
|
289
|
+
if (this.baseBeta === null) {
|
|
290
|
+
this.baseBeta = beta;
|
|
291
|
+
this.baseGamma = gamma;
|
|
292
|
+
}
|
|
293
|
+
const relativeBeta = beta - this.baseBeta;
|
|
294
|
+
const relativeGamma = gamma - this.baseGamma;
|
|
269
295
|
const range = this.maxAngle - this.minAngle;
|
|
270
|
-
const rawX = this.axis === "y" ? 0 : this.clamp(
|
|
271
|
-
const rawY = this.axis === "x" ? 0 : this.clamp(
|
|
296
|
+
const rawX = this.axis === "y" ? 0 : this.clamp(relativeGamma / (range / 2));
|
|
297
|
+
const rawY = this.axis === "x" ? 0 : this.clamp(relativeBeta / (range / 2));
|
|
272
298
|
this.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;
|
|
273
299
|
this.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;
|
|
274
300
|
this.onMove({
|
|
@@ -298,7 +324,6 @@ var PointerSensor = class {
|
|
|
298
324
|
axis;
|
|
299
325
|
active = false;
|
|
300
326
|
rect = null;
|
|
301
|
-
isPointerDown = false;
|
|
302
327
|
constructor(el, onMove, onEnter, onLeave, axis, eventsEl = null) {
|
|
303
328
|
this.eventsEl = eventsEl ?? el;
|
|
304
329
|
this.onMove = onMove;
|
|
@@ -333,14 +358,10 @@ var PointerSensor = class {
|
|
|
333
358
|
this.axis = axis;
|
|
334
359
|
};
|
|
335
360
|
handlePointerDown = (e) => {
|
|
336
|
-
this.isPointerDown = true;
|
|
337
|
-
this.eventsEl.setPointerCapture(e.pointerId);
|
|
338
361
|
this.handlePointerEnter();
|
|
339
362
|
this.handlePointerMove(e);
|
|
340
363
|
};
|
|
341
|
-
handlePointerUp = (
|
|
342
|
-
this.isPointerDown = false;
|
|
343
|
-
this.eventsEl.releasePointerCapture(e.pointerId);
|
|
364
|
+
handlePointerUp = () => {
|
|
344
365
|
this.handlePointerLeave();
|
|
345
366
|
};
|
|
346
367
|
/**
|
|
@@ -349,13 +370,10 @@ var PointerSensor = class {
|
|
|
349
370
|
*/
|
|
350
371
|
handlePointerMove = (e) => {
|
|
351
372
|
if (!this.rect) this.rect = this.eventsEl.getBoundingClientRect();
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
this.handlePointerLeave();
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
373
|
+
const width = this.eventsEl.offsetWidth || this.rect.width;
|
|
374
|
+
const height = this.eventsEl.offsetHeight || this.rect.height;
|
|
375
|
+
const rawX = (e.clientX - this.rect.left) / width;
|
|
376
|
+
const rawY = (e.clientY - this.rect.top) / height;
|
|
359
377
|
const clampedX = Math.max(0, Math.min(1, rawX));
|
|
360
378
|
const clampedY = Math.max(0, Math.min(1, rawY));
|
|
361
379
|
const x = this.axis === "y" ? 0 : clampedX * 2 - 1;
|
|
@@ -404,6 +422,8 @@ var Levita = class {
|
|
|
404
422
|
listeners = /* @__PURE__ */ new Map();
|
|
405
423
|
destroyed = false;
|
|
406
424
|
gyroscopeRequested = false;
|
|
425
|
+
gyroscopeEvent = null;
|
|
426
|
+
rafId = null;
|
|
407
427
|
/**
|
|
408
428
|
* @param el - The DOM element to apply the tilt effect to
|
|
409
429
|
* @param options - Configuration options (all optional, sensible defaults)
|
|
@@ -421,22 +441,47 @@ var Levita = class {
|
|
|
421
441
|
if (this.options.shadow) this.shadowEffect = new ShadowEffect(this.el);
|
|
422
442
|
this.pointerSensor = new PointerSensor(this.el, (values) => this.handleSensorInput(values), () => this.handleEnter(), () => this.handleLeave(), this.options.axis, this.options.eventsEl);
|
|
423
443
|
if (this.options.gyroscope !== false && MotionSensor.isSupported()) {
|
|
424
|
-
this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis);
|
|
425
|
-
if (this.options.gyroscope === "auto")
|
|
444
|
+
this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis, void 0, void 0, void 0, () => this.handleMotionReady());
|
|
445
|
+
if (this.options.gyroscope === "auto") {
|
|
446
|
+
this.gyroscopeEvent = MotionSensor.needsPermission() ? "click" : "pointerup";
|
|
447
|
+
this.el.addEventListener(this.gyroscopeEvent, this.handleFirstTouch, { once: true });
|
|
448
|
+
}
|
|
426
449
|
}
|
|
427
450
|
if (!this.options.disabled) this.enable();
|
|
428
451
|
}
|
|
429
452
|
/**
|
|
430
|
-
* On first
|
|
431
|
-
*
|
|
453
|
+
* On first interaction, request accelerometer permission and start the
|
|
454
|
+
* motion sensor. The pointer sensor keeps running until the first valid
|
|
455
|
+
* deviceorientation event is received (see handleMotionReady).
|
|
432
456
|
*/
|
|
433
457
|
handleFirstTouch = async () => {
|
|
434
458
|
if (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;
|
|
435
459
|
this.gyroscopeRequested = true;
|
|
436
|
-
if (await this.motionSensor.requestPermission())
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
460
|
+
if (await this.motionSensor.requestPermission()) this.motionSensor.start();
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* Called when the motion sensor receives its first valid event.
|
|
464
|
+
* At this point it's safe to hand off from pointer to motion.
|
|
465
|
+
*/
|
|
466
|
+
handleMotionReady = () => {
|
|
467
|
+
this.pointerSensor.stop();
|
|
468
|
+
this.setTransition(false);
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* Update options at runtime without destroying/recreating the instance.
|
|
472
|
+
*
|
|
473
|
+
* Only "lightweight" options are supported — those that don't require
|
|
474
|
+
* DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
|
|
475
|
+
* `disabled`, and `maxGlare` require a full destroy/recreate cycle
|
|
476
|
+
* (or use `enable()`/`disable()` for toggling the effect).
|
|
477
|
+
*/
|
|
478
|
+
update = (options) => {
|
|
479
|
+
if (this.destroyed) return;
|
|
480
|
+
Object.assign(this.options, options);
|
|
481
|
+
this.applyBaseProperties();
|
|
482
|
+
if (options.axis !== void 0) {
|
|
483
|
+
this.pointerSensor.setAxis(options.axis);
|
|
484
|
+
this.motionSensor?.setAxis(options.axis);
|
|
440
485
|
}
|
|
441
486
|
};
|
|
442
487
|
/** Re-enable the tilt effect after a `disable()` call. */
|
|
@@ -461,11 +506,7 @@ var Levita = class {
|
|
|
461
506
|
requestPermission = async () => {
|
|
462
507
|
if (!this.motionSensor) return false;
|
|
463
508
|
const granted = await this.motionSensor.requestPermission();
|
|
464
|
-
if (granted)
|
|
465
|
-
this.pointerSensor.stop();
|
|
466
|
-
this.setTransition(false);
|
|
467
|
-
this.motionSensor.start();
|
|
468
|
-
}
|
|
509
|
+
if (granted) this.motionSensor.start();
|
|
469
510
|
return granted;
|
|
470
511
|
};
|
|
471
512
|
/**
|
|
@@ -475,6 +516,7 @@ var Levita = class {
|
|
|
475
516
|
destroy = () => {
|
|
476
517
|
if (this.destroyed) return;
|
|
477
518
|
this.destroyed = true;
|
|
519
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
478
520
|
this.pointerSensor.stop();
|
|
479
521
|
this.motionSensor?.stop();
|
|
480
522
|
this.glareEffect?.destroy();
|
|
@@ -483,7 +525,7 @@ var Levita = class {
|
|
|
483
525
|
this.el.classList.remove("levita");
|
|
484
526
|
this.removeBaseProperties();
|
|
485
527
|
this.listeners.clear();
|
|
486
|
-
this.el.removeEventListener(
|
|
528
|
+
if (this.gyroscopeEvent) this.el.removeEventListener(this.gyroscopeEvent, this.handleFirstTouch);
|
|
487
529
|
};
|
|
488
530
|
/**
|
|
489
531
|
* Register an event listener.
|
|
@@ -512,26 +554,34 @@ var Levita = class {
|
|
|
512
554
|
/**
|
|
513
555
|
* Process normalized sensor input and update CSS custom properties.
|
|
514
556
|
* Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
|
|
557
|
+
*
|
|
558
|
+
* Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
|
|
559
|
+
* can fire hundreds of events per second, which would saturate the main thread
|
|
560
|
+
* with CSS variable updates and style recalculations. This ensures we only
|
|
561
|
+
* update the DOM once per browser frame.
|
|
515
562
|
*/
|
|
516
563
|
handleSensorInput = (input) => {
|
|
517
564
|
if (this.options.disabled) return;
|
|
518
565
|
const multiplier = this.options.reverse ? -1 : 1;
|
|
519
566
|
const x = input.y * this.options.max * multiplier;
|
|
520
567
|
const y = input.x * this.options.max * multiplier * -1;
|
|
521
|
-
this.
|
|
522
|
-
this.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
568
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
569
|
+
this.rafId = requestAnimationFrame(() => {
|
|
570
|
+
this.el.style.setProperty("--levita-x", `${x}deg`);
|
|
571
|
+
this.el.style.setProperty("--levita-y", `${y}deg`);
|
|
572
|
+
this.el.style.setProperty("--levita-percent-x", String(input.x));
|
|
573
|
+
this.el.style.setProperty("--levita-percent-y", String(input.y));
|
|
574
|
+
this.glareEffect?.update(input.x, input.y);
|
|
575
|
+
this.shadowEffect?.update(input.x, input.y);
|
|
576
|
+
const values = {
|
|
577
|
+
x,
|
|
578
|
+
y,
|
|
579
|
+
percentX: input.x,
|
|
580
|
+
percentY: input.y
|
|
581
|
+
};
|
|
582
|
+
this.emit("move", values);
|
|
583
|
+
this.rafId = null;
|
|
584
|
+
});
|
|
535
585
|
};
|
|
536
586
|
handleEnter = () => {
|
|
537
587
|
this.setTransition(false);
|
|
@@ -539,6 +589,10 @@ var Levita = class {
|
|
|
539
589
|
this.emit("enter", void 0);
|
|
540
590
|
};
|
|
541
591
|
handleLeave = () => {
|
|
592
|
+
if (this.rafId) {
|
|
593
|
+
cancelAnimationFrame(this.rafId);
|
|
594
|
+
this.rafId = null;
|
|
595
|
+
}
|
|
542
596
|
this.setTransition(true);
|
|
543
597
|
if (this.options.reset) this.resetTransform();
|
|
544
598
|
this.emit("leave", void 0);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
type Axis = "x" | "y" | null;
|
|
3
3
|
type GyroscopeMode = "auto" | boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Options that can be updated at runtime via `update()`.
|
|
6
|
+
* Limited to lightweight options that don't require DOM mutations.
|
|
7
|
+
*/
|
|
8
|
+
type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset">;
|
|
4
9
|
interface LevitaOptions {
|
|
5
10
|
/** Max tilt angle in degrees. Default: 15 */
|
|
6
11
|
max: number;
|
|
@@ -86,16 +91,33 @@ declare class Levita {
|
|
|
86
91
|
private listeners;
|
|
87
92
|
private destroyed;
|
|
88
93
|
private gyroscopeRequested;
|
|
94
|
+
private gyroscopeEvent;
|
|
95
|
+
private rafId;
|
|
89
96
|
/**
|
|
90
97
|
* @param el - The DOM element to apply the tilt effect to
|
|
91
98
|
* @param options - Configuration options (all optional, sensible defaults)
|
|
92
99
|
*/
|
|
93
100
|
constructor(el: HTMLElement, options?: Partial<LevitaOptions>);
|
|
94
101
|
/**
|
|
95
|
-
* On first
|
|
96
|
-
*
|
|
102
|
+
* On first interaction, request accelerometer permission and start the
|
|
103
|
+
* motion sensor. The pointer sensor keeps running until the first valid
|
|
104
|
+
* deviceorientation event is received (see handleMotionReady).
|
|
97
105
|
*/
|
|
98
106
|
private handleFirstTouch;
|
|
107
|
+
/**
|
|
108
|
+
* Called when the motion sensor receives its first valid event.
|
|
109
|
+
* At this point it's safe to hand off from pointer to motion.
|
|
110
|
+
*/
|
|
111
|
+
private handleMotionReady;
|
|
112
|
+
/**
|
|
113
|
+
* Update options at runtime without destroying/recreating the instance.
|
|
114
|
+
*
|
|
115
|
+
* Only "lightweight" options are supported — those that don't require
|
|
116
|
+
* DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
|
|
117
|
+
* `disabled`, and `maxGlare` require a full destroy/recreate cycle
|
|
118
|
+
* (or use `enable()`/`disable()` for toggling the effect).
|
|
119
|
+
*/
|
|
120
|
+
update: (options: Partial<UpdatableOptions>) => void;
|
|
99
121
|
/** Re-enable the tilt effect after a `disable()` call. */
|
|
100
122
|
enable: () => void;
|
|
101
123
|
/** Pause the tilt effect and reset the element to its neutral position. */
|
|
@@ -131,6 +153,11 @@ declare class Levita {
|
|
|
131
153
|
/**
|
|
132
154
|
* Process normalized sensor input and update CSS custom properties.
|
|
133
155
|
* Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
|
|
156
|
+
*
|
|
157
|
+
* Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
|
|
158
|
+
* can fire hundreds of events per second, which would saturate the main thread
|
|
159
|
+
* with CSS variable updates and style recalculations. This ensures we only
|
|
160
|
+
* update the DOM once per browser frame.
|
|
134
161
|
*/
|
|
135
162
|
private handleSensorInput;
|
|
136
163
|
private handleEnter;
|
|
@@ -145,5 +172,5 @@ declare class Levita {
|
|
|
145
172
|
private removeBaseProperties;
|
|
146
173
|
}
|
|
147
174
|
//#endregion
|
|
148
|
-
export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, buildOptions };
|
|
175
|
+
export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, type UpdatableOptions, buildOptions };
|
|
149
176
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/constants.ts","../src/levita.ts"],"mappings":";KAAY,IAAA;AAAA,KACA,aAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/constants.ts","../src/levita.ts"],"mappings":";KAAY,IAAA;AAAA,KACA,aAAA;;;;;KAMA,gBAAA,GAAmB,IAAA,CAC9B,aAAA;AAAA,UAIgB,aAAA;;EAEhB,GAAA;EAbwB;EAexB,WAAA;EAT2B;EAW3B,KAAA;EAX8B;EAa9B,KAAA;EARgB;EAUhB,MAAA;;EAEA,OAAA;EAYW;EAVX,IAAA,EAAM,IAAA;EAce;EAZrB,KAAA;EAdA;EAgBA,KAAA;EAZA;EAcA,QAAA;EAVA;EAYA,MAAA;EARA;EAUA,SAAA,EAAW,aAAA;EARX;EAUA,QAAA;EANA;EAQA,QAAA,EAAU,WAAA;AAAA;AAAA,UAGM,UAAA;EALhB;EAOA,CAAA;EALU;EAOV,CAAA;EAPqB;EASrB,QAAA;EAN0B;EAQ1B,QAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,IAAA,EAAM,UAAA;EACN,KAAA;EACA,KAAA;AAAA;AAAA,KAGW,aAAA,OAAoB,IAAA,EAAM,CAAA;;;AA5DtC;AAAA,cCGa,WAAA,kBAA6B,aAAA;;;;ADF1C;cCuBa,YAAA,GAAgB,MAAA,EAAQ,OAAA,CAAQ,aAAA,MAAiB,OAAA,CAAQ,aAAA;AAAA,cAWzD,eAAA,EAAiB,aAAA;;;ADnC9B;;;;;AACA;;;;;AAMA;;;;;AAKA;AAZA,cEgCa,MAAA;EAAA,QACJ,EAAA;EAAA,QACA,OAAA;EAAA,QACA,aAAA;EAAA,QACA,YAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,QACA,SAAA;EAAA,QACA,kBAAA;EAAA,QACA,cAAA;EAAA,QACA,KAAA;EFlBR;;;;cEwBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFhB9C;;;;;EAAA,QEqEQ,gBAAA;EF/Da;;AAGtB;;EAHsB,QE6Eb,iBAAA;EF1EkB;;;;;;;AAW3B;EE4EC,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;;EAY3B,MAAA;EFvFA;EE8FA,OAAA;EF7FA;;;;AAID;;EEsGC,iBAAA,QAA8B,OAAA;EFtGQ;;;;EEmHtC,OAAA;EFnHsC;;;;ACzDvC;;ECsMC,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDxMC;;AAqB1C;;;;ECiMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDnM6B;EAAA,QCyM7D,IAAA;EDzM4D;;;;;;;;;EAAA,QC2N5D,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDrPqB;EAAA,QCkQrB,aAAA;;UAKA,cAAA;;UAWA,mBAAA;EArRU;EAAA,QA4RV,oBAAA;AAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
type Axis = "x" | "y" | null;
|
|
3
3
|
type GyroscopeMode = "auto" | boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Options that can be updated at runtime via `update()`.
|
|
6
|
+
* Limited to lightweight options that don't require DOM mutations.
|
|
7
|
+
*/
|
|
8
|
+
type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset">;
|
|
4
9
|
interface LevitaOptions {
|
|
5
10
|
/** Max tilt angle in degrees. Default: 15 */
|
|
6
11
|
max: number;
|
|
@@ -86,16 +91,33 @@ declare class Levita {
|
|
|
86
91
|
private listeners;
|
|
87
92
|
private destroyed;
|
|
88
93
|
private gyroscopeRequested;
|
|
94
|
+
private gyroscopeEvent;
|
|
95
|
+
private rafId;
|
|
89
96
|
/**
|
|
90
97
|
* @param el - The DOM element to apply the tilt effect to
|
|
91
98
|
* @param options - Configuration options (all optional, sensible defaults)
|
|
92
99
|
*/
|
|
93
100
|
constructor(el: HTMLElement, options?: Partial<LevitaOptions>);
|
|
94
101
|
/**
|
|
95
|
-
* On first
|
|
96
|
-
*
|
|
102
|
+
* On first interaction, request accelerometer permission and start the
|
|
103
|
+
* motion sensor. The pointer sensor keeps running until the first valid
|
|
104
|
+
* deviceorientation event is received (see handleMotionReady).
|
|
97
105
|
*/
|
|
98
106
|
private handleFirstTouch;
|
|
107
|
+
/**
|
|
108
|
+
* Called when the motion sensor receives its first valid event.
|
|
109
|
+
* At this point it's safe to hand off from pointer to motion.
|
|
110
|
+
*/
|
|
111
|
+
private handleMotionReady;
|
|
112
|
+
/**
|
|
113
|
+
* Update options at runtime without destroying/recreating the instance.
|
|
114
|
+
*
|
|
115
|
+
* Only "lightweight" options are supported — those that don't require
|
|
116
|
+
* DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
|
|
117
|
+
* `disabled`, and `maxGlare` require a full destroy/recreate cycle
|
|
118
|
+
* (or use `enable()`/`disable()` for toggling the effect).
|
|
119
|
+
*/
|
|
120
|
+
update: (options: Partial<UpdatableOptions>) => void;
|
|
99
121
|
/** Re-enable the tilt effect after a `disable()` call. */
|
|
100
122
|
enable: () => void;
|
|
101
123
|
/** Pause the tilt effect and reset the element to its neutral position. */
|
|
@@ -131,6 +153,11 @@ declare class Levita {
|
|
|
131
153
|
/**
|
|
132
154
|
* Process normalized sensor input and update CSS custom properties.
|
|
133
155
|
* Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
|
|
156
|
+
*
|
|
157
|
+
* Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
|
|
158
|
+
* can fire hundreds of events per second, which would saturate the main thread
|
|
159
|
+
* with CSS variable updates and style recalculations. This ensures we only
|
|
160
|
+
* update the DOM once per browser frame.
|
|
134
161
|
*/
|
|
135
162
|
private handleSensorInput;
|
|
136
163
|
private handleEnter;
|
|
@@ -145,5 +172,5 @@ declare class Levita {
|
|
|
145
172
|
private removeBaseProperties;
|
|
146
173
|
}
|
|
147
174
|
//#endregion
|
|
148
|
-
export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, buildOptions };
|
|
175
|
+
export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, type UpdatableOptions, buildOptions };
|
|
149
176
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/constants.ts","../src/levita.ts"],"mappings":";KAAY,IAAA;AAAA,KACA,aAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/constants.ts","../src/levita.ts"],"mappings":";KAAY,IAAA;AAAA,KACA,aAAA;;;;;KAMA,gBAAA,GAAmB,IAAA,CAC9B,aAAA;AAAA,UAIgB,aAAA;;EAEhB,GAAA;EAbwB;EAexB,WAAA;EAT2B;EAW3B,KAAA;EAX8B;EAa9B,KAAA;EARgB;EAUhB,MAAA;;EAEA,OAAA;EAYW;EAVX,IAAA,EAAM,IAAA;EAce;EAZrB,KAAA;EAdA;EAgBA,KAAA;EAZA;EAcA,QAAA;EAVA;EAYA,MAAA;EARA;EAUA,SAAA,EAAW,aAAA;EARX;EAUA,QAAA;EANA;EAQA,QAAA,EAAU,WAAA;AAAA;AAAA,UAGM,UAAA;EALhB;EAOA,CAAA;EALU;EAOV,CAAA;EAPqB;EASrB,QAAA;EAN0B;EAQ1B,QAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,IAAA,EAAM,UAAA;EACN,KAAA;EACA,KAAA;AAAA;AAAA,KAGW,aAAA,OAAoB,IAAA,EAAM,CAAA;;;AA5DtC;AAAA,cCGa,WAAA,kBAA6B,aAAA;;;;ADF1C;cCuBa,YAAA,GAAgB,MAAA,EAAQ,OAAA,CAAQ,aAAA,MAAiB,OAAA,CAAQ,aAAA;AAAA,cAWzD,eAAA,EAAiB,aAAA;;;ADnC9B;;;;;AACA;;;;;AAMA;;;;;AAKA;AAZA,cEgCa,MAAA;EAAA,QACJ,EAAA;EAAA,QACA,OAAA;EAAA,QACA,aAAA;EAAA,QACA,YAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,QACA,SAAA;EAAA,QACA,kBAAA;EAAA,QACA,cAAA;EAAA,QACA,KAAA;EFlBR;;;;cEwBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFhB9C;;;;;EAAA,QEqEQ,gBAAA;EF/Da;;AAGtB;;EAHsB,QE6Eb,iBAAA;EF1EkB;;;;;;;AAW3B;EE4EC,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;;EAY3B,MAAA;EFvFA;EE8FA,OAAA;EF7FA;;;;AAID;;EEsGC,iBAAA,QAA8B,OAAA;EFtGQ;;;;EEmHtC,OAAA;EFnHsC;;;;ACzDvC;;ECsMC,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDxMC;;AAqB1C;;;;ECiMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDnM6B;EAAA,QCyM7D,IAAA;EDzM4D;;;;;;;;;EAAA,QC2N5D,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDrPqB;EAAA,QCkQrB,aAAA;;UAKA,cAAA;;UAWA,mBAAA;EArRU;EAAA,QA4RV,oBAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -182,27 +182,34 @@ const cleanupLayers = (layers) => {
|
|
|
182
182
|
*/
|
|
183
183
|
var MotionSensor = class MotionSensor {
|
|
184
184
|
onMove;
|
|
185
|
+
onFirstEvent;
|
|
185
186
|
axis;
|
|
186
187
|
active = false;
|
|
187
188
|
permitted = false;
|
|
189
|
+
receivedEvent = false;
|
|
190
|
+
warmup = false;
|
|
188
191
|
minAngle;
|
|
189
192
|
maxAngle;
|
|
190
193
|
smoothing;
|
|
191
194
|
lastX = 0;
|
|
192
195
|
lastY = 0;
|
|
196
|
+
baseBeta = null;
|
|
197
|
+
baseGamma = null;
|
|
193
198
|
/**
|
|
194
199
|
* @param onMove - Callback receiving normalized { x, y } values
|
|
195
200
|
* @param axis - Restrict input to a single axis, or null for both
|
|
196
201
|
* @param minAngle - Minimum device angle mapped to -1 (default: -45)
|
|
197
202
|
* @param maxAngle - Maximum device angle mapped to 1 (default: 45)
|
|
198
203
|
* @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)
|
|
204
|
+
* @param onFirstEvent - Called once when the first valid deviceorientation event is received
|
|
199
205
|
*/
|
|
200
|
-
constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15) {
|
|
206
|
+
constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15, onFirstEvent = null) {
|
|
201
207
|
this.onMove = onMove;
|
|
202
208
|
this.axis = axis;
|
|
203
209
|
this.minAngle = minAngle;
|
|
204
210
|
this.maxAngle = maxAngle;
|
|
205
211
|
this.smoothing = smoothing;
|
|
212
|
+
this.onFirstEvent = onFirstEvent;
|
|
206
213
|
}
|
|
207
214
|
/** Check if the DeviceOrientationEvent API is available in this environment. */
|
|
208
215
|
static isSupported = () => {
|
|
@@ -241,6 +248,10 @@ var MotionSensor = class MotionSensor {
|
|
|
241
248
|
start = () => {
|
|
242
249
|
if (this.active || !this.permitted) return;
|
|
243
250
|
this.active = true;
|
|
251
|
+
this.receivedEvent = false;
|
|
252
|
+
this.warmup = false;
|
|
253
|
+
this.baseBeta = null;
|
|
254
|
+
this.baseGamma = null;
|
|
244
255
|
window.addEventListener("deviceorientation", this.handleOrientation);
|
|
245
256
|
};
|
|
246
257
|
/** Stop listening and remove the deviceorientation event listener. */
|
|
@@ -262,11 +273,26 @@ var MotionSensor = class MotionSensor {
|
|
|
262
273
|
*/
|
|
263
274
|
handleOrientation = (e) => {
|
|
264
275
|
const evt = e;
|
|
276
|
+
if (evt.beta === null && evt.gamma === null) return;
|
|
277
|
+
if (!this.warmup) {
|
|
278
|
+
this.warmup = true;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
265
281
|
const beta = evt.beta ?? 0;
|
|
266
282
|
const gamma = evt.gamma ?? 0;
|
|
283
|
+
if (!this.receivedEvent) {
|
|
284
|
+
this.receivedEvent = true;
|
|
285
|
+
this.onFirstEvent?.();
|
|
286
|
+
}
|
|
287
|
+
if (this.baseBeta === null) {
|
|
288
|
+
this.baseBeta = beta;
|
|
289
|
+
this.baseGamma = gamma;
|
|
290
|
+
}
|
|
291
|
+
const relativeBeta = beta - this.baseBeta;
|
|
292
|
+
const relativeGamma = gamma - this.baseGamma;
|
|
267
293
|
const range = this.maxAngle - this.minAngle;
|
|
268
|
-
const rawX = this.axis === "y" ? 0 : this.clamp(
|
|
269
|
-
const rawY = this.axis === "x" ? 0 : this.clamp(
|
|
294
|
+
const rawX = this.axis === "y" ? 0 : this.clamp(relativeGamma / (range / 2));
|
|
295
|
+
const rawY = this.axis === "x" ? 0 : this.clamp(relativeBeta / (range / 2));
|
|
270
296
|
this.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;
|
|
271
297
|
this.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;
|
|
272
298
|
this.onMove({
|
|
@@ -296,7 +322,6 @@ var PointerSensor = class {
|
|
|
296
322
|
axis;
|
|
297
323
|
active = false;
|
|
298
324
|
rect = null;
|
|
299
|
-
isPointerDown = false;
|
|
300
325
|
constructor(el, onMove, onEnter, onLeave, axis, eventsEl = null) {
|
|
301
326
|
this.eventsEl = eventsEl ?? el;
|
|
302
327
|
this.onMove = onMove;
|
|
@@ -331,14 +356,10 @@ var PointerSensor = class {
|
|
|
331
356
|
this.axis = axis;
|
|
332
357
|
};
|
|
333
358
|
handlePointerDown = (e) => {
|
|
334
|
-
this.isPointerDown = true;
|
|
335
|
-
this.eventsEl.setPointerCapture(e.pointerId);
|
|
336
359
|
this.handlePointerEnter();
|
|
337
360
|
this.handlePointerMove(e);
|
|
338
361
|
};
|
|
339
|
-
handlePointerUp = (
|
|
340
|
-
this.isPointerDown = false;
|
|
341
|
-
this.eventsEl.releasePointerCapture(e.pointerId);
|
|
362
|
+
handlePointerUp = () => {
|
|
342
363
|
this.handlePointerLeave();
|
|
343
364
|
};
|
|
344
365
|
/**
|
|
@@ -347,13 +368,10 @@ var PointerSensor = class {
|
|
|
347
368
|
*/
|
|
348
369
|
handlePointerMove = (e) => {
|
|
349
370
|
if (!this.rect) this.rect = this.eventsEl.getBoundingClientRect();
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
this.handlePointerLeave();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
371
|
+
const width = this.eventsEl.offsetWidth || this.rect.width;
|
|
372
|
+
const height = this.eventsEl.offsetHeight || this.rect.height;
|
|
373
|
+
const rawX = (e.clientX - this.rect.left) / width;
|
|
374
|
+
const rawY = (e.clientY - this.rect.top) / height;
|
|
357
375
|
const clampedX = Math.max(0, Math.min(1, rawX));
|
|
358
376
|
const clampedY = Math.max(0, Math.min(1, rawY));
|
|
359
377
|
const x = this.axis === "y" ? 0 : clampedX * 2 - 1;
|
|
@@ -402,6 +420,8 @@ var Levita = class {
|
|
|
402
420
|
listeners = /* @__PURE__ */ new Map();
|
|
403
421
|
destroyed = false;
|
|
404
422
|
gyroscopeRequested = false;
|
|
423
|
+
gyroscopeEvent = null;
|
|
424
|
+
rafId = null;
|
|
405
425
|
/**
|
|
406
426
|
* @param el - The DOM element to apply the tilt effect to
|
|
407
427
|
* @param options - Configuration options (all optional, sensible defaults)
|
|
@@ -419,22 +439,47 @@ var Levita = class {
|
|
|
419
439
|
if (this.options.shadow) this.shadowEffect = new ShadowEffect(this.el);
|
|
420
440
|
this.pointerSensor = new PointerSensor(this.el, (values) => this.handleSensorInput(values), () => this.handleEnter(), () => this.handleLeave(), this.options.axis, this.options.eventsEl);
|
|
421
441
|
if (this.options.gyroscope !== false && MotionSensor.isSupported()) {
|
|
422
|
-
this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis);
|
|
423
|
-
if (this.options.gyroscope === "auto")
|
|
442
|
+
this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis, void 0, void 0, void 0, () => this.handleMotionReady());
|
|
443
|
+
if (this.options.gyroscope === "auto") {
|
|
444
|
+
this.gyroscopeEvent = MotionSensor.needsPermission() ? "click" : "pointerup";
|
|
445
|
+
this.el.addEventListener(this.gyroscopeEvent, this.handleFirstTouch, { once: true });
|
|
446
|
+
}
|
|
424
447
|
}
|
|
425
448
|
if (!this.options.disabled) this.enable();
|
|
426
449
|
}
|
|
427
450
|
/**
|
|
428
|
-
* On first
|
|
429
|
-
*
|
|
451
|
+
* On first interaction, request accelerometer permission and start the
|
|
452
|
+
* motion sensor. The pointer sensor keeps running until the first valid
|
|
453
|
+
* deviceorientation event is received (see handleMotionReady).
|
|
430
454
|
*/
|
|
431
455
|
handleFirstTouch = async () => {
|
|
432
456
|
if (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;
|
|
433
457
|
this.gyroscopeRequested = true;
|
|
434
|
-
if (await this.motionSensor.requestPermission())
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
if (await this.motionSensor.requestPermission()) this.motionSensor.start();
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Called when the motion sensor receives its first valid event.
|
|
462
|
+
* At this point it's safe to hand off from pointer to motion.
|
|
463
|
+
*/
|
|
464
|
+
handleMotionReady = () => {
|
|
465
|
+
this.pointerSensor.stop();
|
|
466
|
+
this.setTransition(false);
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* Update options at runtime without destroying/recreating the instance.
|
|
470
|
+
*
|
|
471
|
+
* Only "lightweight" options are supported — those that don't require
|
|
472
|
+
* DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
|
|
473
|
+
* `disabled`, and `maxGlare` require a full destroy/recreate cycle
|
|
474
|
+
* (or use `enable()`/`disable()` for toggling the effect).
|
|
475
|
+
*/
|
|
476
|
+
update = (options) => {
|
|
477
|
+
if (this.destroyed) return;
|
|
478
|
+
Object.assign(this.options, options);
|
|
479
|
+
this.applyBaseProperties();
|
|
480
|
+
if (options.axis !== void 0) {
|
|
481
|
+
this.pointerSensor.setAxis(options.axis);
|
|
482
|
+
this.motionSensor?.setAxis(options.axis);
|
|
438
483
|
}
|
|
439
484
|
};
|
|
440
485
|
/** Re-enable the tilt effect after a `disable()` call. */
|
|
@@ -459,11 +504,7 @@ var Levita = class {
|
|
|
459
504
|
requestPermission = async () => {
|
|
460
505
|
if (!this.motionSensor) return false;
|
|
461
506
|
const granted = await this.motionSensor.requestPermission();
|
|
462
|
-
if (granted)
|
|
463
|
-
this.pointerSensor.stop();
|
|
464
|
-
this.setTransition(false);
|
|
465
|
-
this.motionSensor.start();
|
|
466
|
-
}
|
|
507
|
+
if (granted) this.motionSensor.start();
|
|
467
508
|
return granted;
|
|
468
509
|
};
|
|
469
510
|
/**
|
|
@@ -473,6 +514,7 @@ var Levita = class {
|
|
|
473
514
|
destroy = () => {
|
|
474
515
|
if (this.destroyed) return;
|
|
475
516
|
this.destroyed = true;
|
|
517
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
476
518
|
this.pointerSensor.stop();
|
|
477
519
|
this.motionSensor?.stop();
|
|
478
520
|
this.glareEffect?.destroy();
|
|
@@ -481,7 +523,7 @@ var Levita = class {
|
|
|
481
523
|
this.el.classList.remove("levita");
|
|
482
524
|
this.removeBaseProperties();
|
|
483
525
|
this.listeners.clear();
|
|
484
|
-
this.el.removeEventListener(
|
|
526
|
+
if (this.gyroscopeEvent) this.el.removeEventListener(this.gyroscopeEvent, this.handleFirstTouch);
|
|
485
527
|
};
|
|
486
528
|
/**
|
|
487
529
|
* Register an event listener.
|
|
@@ -510,26 +552,34 @@ var Levita = class {
|
|
|
510
552
|
/**
|
|
511
553
|
* Process normalized sensor input and update CSS custom properties.
|
|
512
554
|
* Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
|
|
555
|
+
*
|
|
556
|
+
* Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
|
|
557
|
+
* can fire hundreds of events per second, which would saturate the main thread
|
|
558
|
+
* with CSS variable updates and style recalculations. This ensures we only
|
|
559
|
+
* update the DOM once per browser frame.
|
|
513
560
|
*/
|
|
514
561
|
handleSensorInput = (input) => {
|
|
515
562
|
if (this.options.disabled) return;
|
|
516
563
|
const multiplier = this.options.reverse ? -1 : 1;
|
|
517
564
|
const x = input.y * this.options.max * multiplier;
|
|
518
565
|
const y = input.x * this.options.max * multiplier * -1;
|
|
519
|
-
this.
|
|
520
|
-
this.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
566
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
567
|
+
this.rafId = requestAnimationFrame(() => {
|
|
568
|
+
this.el.style.setProperty("--levita-x", `${x}deg`);
|
|
569
|
+
this.el.style.setProperty("--levita-y", `${y}deg`);
|
|
570
|
+
this.el.style.setProperty("--levita-percent-x", String(input.x));
|
|
571
|
+
this.el.style.setProperty("--levita-percent-y", String(input.y));
|
|
572
|
+
this.glareEffect?.update(input.x, input.y);
|
|
573
|
+
this.shadowEffect?.update(input.x, input.y);
|
|
574
|
+
const values = {
|
|
575
|
+
x,
|
|
576
|
+
y,
|
|
577
|
+
percentX: input.x,
|
|
578
|
+
percentY: input.y
|
|
579
|
+
};
|
|
580
|
+
this.emit("move", values);
|
|
581
|
+
this.rafId = null;
|
|
582
|
+
});
|
|
533
583
|
};
|
|
534
584
|
handleEnter = () => {
|
|
535
585
|
this.setTransition(false);
|
|
@@ -537,6 +587,10 @@ var Levita = class {
|
|
|
537
587
|
this.emit("enter", void 0);
|
|
538
588
|
};
|
|
539
589
|
handleLeave = () => {
|
|
590
|
+
if (this.rafId) {
|
|
591
|
+
cancelAnimationFrame(this.rafId);
|
|
592
|
+
this.rafId = null;
|
|
593
|
+
}
|
|
540
594
|
this.setTransition(true);
|
|
541
595
|
if (this.options.reset) this.resetTransform();
|
|
542
596
|
this.emit("leave", void 0);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/effects/glare.ts","../src/effects/shadow.ts","../src/layers.ts","../src/sensors/motion.ts","../src/sensors/pointer.ts","../src/levita.ts"],"sourcesContent":["import type { LevitaOptions } from \"./types.js\";\n\n/** All keys of `LevitaOptions`, derived from `DEFAULT_OPTIONS`. */\nexport const OPTION_KEYS: readonly (keyof LevitaOptions)[] = [\n\t\"max\",\n\t\"perspective\",\n\t\"scale\",\n\t\"speed\",\n\t\"easing\",\n\t\"reverse\",\n\t\"axis\",\n\t\"reset\",\n\t\"glare\",\n\t\"maxGlare\",\n\t\"shadow\",\n\t\"gyroscope\",\n\t\"disabled\",\n\t\"eventsEl\",\n] as const;\n\n/**\n * Build a partial `LevitaOptions` object from a source,\n * including only keys that are explicitly defined.\n */\nexport const buildOptions = (source: Partial<LevitaOptions>): Partial<LevitaOptions> => {\n\tconst options: Partial<LevitaOptions> = {};\n\tfor (const key of OPTION_KEYS) {\n\t\tif (source[key] !== undefined) {\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: generic key assignment\n\t\t\t(options as any)[key] = source[key];\n\t\t}\n\t}\n\treturn options;\n};\n\nexport const DEFAULT_OPTIONS: LevitaOptions = {\n\tmax: 15,\n\tperspective: 1000,\n\tscale: 1.05,\n\tspeed: 200,\n\teasing: \"ease-out\",\n\treverse: false,\n\taxis: null,\n\treset: true,\n\tglare: false,\n\tmaxGlare: 0.5,\n\tshadow: false,\n\tgyroscope: \"auto\",\n\tdisabled: false,\n\teventsEl: null,\n};\n","/**\n * Creates a radial gradient overlay that follows the tilt position,\n * simulating light reflection on the surface.\n *\n * Injects two DOM elements (.levita-glare > .levita-glare-inner) into\n * the target element. Position and opacity are driven by CSS custom\n * properties — no JS runs per animation frame.\n */\nexport class GlareEffect {\n\tprivate container: HTMLElement;\n\tprivate inner: HTMLElement;\n\tprivate maxOpacity: number;\n\n\t/**\n\t * @param el - The element to attach the glare overlay to\n\t * @param maxOpacity - Maximum glare opacity (0-1)\n\t */\n\tconstructor(el: HTMLElement, maxOpacity: number) {\n\t\tthis.maxOpacity = maxOpacity;\n\n\t\tthis.container = document.createElement(\"div\");\n\t\tthis.container.classList.add(\"levita-glare\");\n\n\t\tthis.inner = document.createElement(\"div\");\n\t\tthis.inner.classList.add(\"levita-glare-inner\");\n\n\t\tthis.container.appendChild(this.inner);\n\t\tel.appendChild(this.container);\n\n\t\tif (!el.style.position || el.style.position === \"static\") {\n\t\t\tel.style.position = \"relative\";\n\t\t}\n\t}\n\n\t/**\n\t * Update glare position and intensity based on normalized tilt values.\n\t * Sets CSS custom properties that the stylesheet uses for rendering.\n\t *\n\t * @param normalizedX - Horizontal position [-1, 1]\n\t * @param normalizedY - Vertical position [-1, 1]\n\t */\n\tupdate = (normalizedX: number, normalizedY: number): void => {\n\t\tconst glareX = ((normalizedX + 1) / 2) * 100;\n\t\tconst glareY = ((normalizedY + 1) / 2) * 100;\n\t\tconst intensity = Math.sqrt(normalizedX ** 2 + normalizedY ** 2) / Math.SQRT2;\n\n\t\tthis.inner.style.setProperty(\"--levita-glare-x\", `${glareX}%`);\n\t\tthis.inner.style.setProperty(\"--levita-glare-y\", `${glareY}%`);\n\t\tthis.inner.style.setProperty(\"--levita-glare-opacity\", `${intensity * this.maxOpacity}`);\n\t};\n\n\t/** Remove the glare DOM elements from the parent. */\n\tdestroy = (): void => {\n\t\tthis.container.remove();\n\t};\n}\n","/**\n * Adds a dynamic drop shadow that shifts based on the tilt angle,\n * reinforcing the 3D depth illusion.\n *\n * Uses CSS custom properties (--levita-shadow-x, --levita-shadow-y)\n * combined with `filter: drop-shadow()` — no JS runs per animation frame.\n */\nexport class ShadowEffect {\n\tprivate el: HTMLElement;\n\tprivate maxOffset: number;\n\n\t/**\n\t * @param el - The element to apply the shadow to\n\t * @param maxOffset - Maximum shadow offset in pixels (default: 20)\n\t */\n\tconstructor(el: HTMLElement, maxOffset = 20) {\n\t\tthis.el = el;\n\t\tthis.maxOffset = maxOffset;\n\t\tthis.el.classList.add(\"levita-shadow\");\n\t}\n\n\t/**\n\t * Update shadow offset based on normalized tilt values.\n\t *\n\t * @param normalizedX - Horizontal position [-1, 1]\n\t * @param normalizedY - Vertical position [-1, 1]\n\t */\n\tupdate = (normalizedX: number, normalizedY: number): void => {\n\t\tconst shadowX = normalizedX * this.maxOffset;\n\t\tconst shadowY = normalizedY * this.maxOffset;\n\n\t\tthis.el.style.setProperty(\"--levita-shadow-x\", `${shadowX}px`);\n\t\tthis.el.style.setProperty(\"--levita-shadow-y\", `${shadowY}px`);\n\t};\n\n\t/** Remove the shadow class and clean up CSS custom properties. */\n\tdestroy = (): void => {\n\t\tthis.el.classList.remove(\"levita-shadow\");\n\t\tthis.el.style.removeProperty(\"--levita-shadow-x\");\n\t\tthis.el.style.removeProperty(\"--levita-shadow-y\");\n\t};\n}\n","/** Represents a child element with a parallax depth offset. */\nexport interface Layer {\n\t/** The DOM element */\n\tel: HTMLElement;\n\t/** The depth offset value (positive = forward, negative = back) */\n\toffset: number;\n}\n\n/**\n * Scan a container for children with `data-levita-offset` attributes\n * and set the `--levita-offset` CSS custom property on each one.\n *\n * The CSS stylesheet uses this variable with `translateZ()` to position\n * layers at different depths — no JS runs per animation frame.\n *\n * @param container - The parent element to scan for layer children\n * @returns Array of discovered layers with their elements and offsets\n */\nexport const scanLayers = (container: HTMLElement): Layer[] => {\n\tconst elements = container.querySelectorAll<HTMLElement>(\"[data-levita-offset]\");\n\tconst layers: Layer[] = [];\n\n\tfor (const el of elements) {\n\t\tconst raw = el.dataset.levitaOffset;\n\t\tconst offset = Number.parseFloat(raw ?? \"0\");\n\t\tif (!Number.isNaN(offset)) {\n\t\t\tel.style.setProperty(\"--levita-offset\", String(offset));\n\t\t\tlayers.push({ el, offset });\n\t\t}\n\t}\n\n\treturn layers;\n};\n\n/**\n * Remove the `--levita-offset` CSS custom property from all layer elements.\n *\n * @param layers - The layers to clean up\n */\nexport const cleanupLayers = (layers: Layer[]): void => {\n\tfor (const layer of layers) {\n\t\tlayer.el.style.removeProperty(\"--levita-offset\");\n\t}\n};\n","import type { Axis } from \"../types.js\";\nimport type { SensorCallback } from \"./pointer.js\";\n\ninterface DeviceOrientationEvt extends Event {\n\tbeta: number | null;\n\tgamma: number | null;\n}\n\n/**\n * Reads device orientation (accelerometer/gyroscope) and normalizes\n * the tilt angles to a [-1, 1] range.\n *\n * Handles iOS 13+ permission flow via async `requestPermission()`.\n * On Android, permission is granted automatically.\n *\n * Uses exponential moving average for smoothing raw sensor data.\n */\nexport class MotionSensor {\n\tprivate onMove: SensorCallback;\n\tprivate axis: Axis;\n\tprivate active = false;\n\tprivate permitted = false;\n\tprivate minAngle: number;\n\tprivate maxAngle: number;\n\tprivate smoothing: number;\n\tprivate lastX = 0;\n\tprivate lastY = 0;\n\n\t/**\n\t * @param onMove - Callback receiving normalized { x, y } values\n\t * @param axis - Restrict input to a single axis, or null for both\n\t * @param minAngle - Minimum device angle mapped to -1 (default: -45)\n\t * @param maxAngle - Maximum device angle mapped to 1 (default: 45)\n\t * @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)\n\t */\n\tconstructor(onMove: SensorCallback, axis: Axis, minAngle = -45, maxAngle = 45, smoothing = 0.15) {\n\t\tthis.onMove = onMove;\n\t\tthis.axis = axis;\n\t\tthis.minAngle = minAngle;\n\t\tthis.maxAngle = maxAngle;\n\t\tthis.smoothing = smoothing;\n\t}\n\n\t/** Check if the DeviceOrientationEvent API is available in this environment. */\n\tstatic isSupported = (): boolean => {\n\t\treturn typeof window !== \"undefined\" && \"DeviceOrientationEvent\" in window;\n\t};\n\n\t/**\n\t * Check if explicit permission is required (iOS 13+).\n\t * On Android and desktop, this returns false.\n\t */\n\tstatic needsPermission = (): boolean => {\n\t\treturn (\n\t\t\ttypeof DeviceOrientationEvent !== \"undefined\" && \"requestPermission\" in DeviceOrientationEvent\n\t\t);\n\t};\n\n\t/**\n\t * Request permission to access device orientation.\n\t * On Android, resolves immediately to true.\n\t * On iOS 13+, triggers the native permission dialog.\n\t * Must be called from a user gesture on iOS.\n\t *\n\t * @returns Whether permission was granted\n\t */\n\trequestPermission = async (): Promise<boolean> => {\n\t\tif (!MotionSensor.isSupported()) return false;\n\n\t\tif (!MotionSensor.needsPermission()) {\n\t\t\tthis.permitted = true;\n\t\t\treturn true;\n\t\t}\n\n\t\ttry {\n\t\t\tconst DOE = DeviceOrientationEvent as unknown as {\n\t\t\t\trequestPermission: () => Promise<string>;\n\t\t\t};\n\t\t\tconst result = await DOE.requestPermission();\n\t\t\tthis.permitted = result === \"granted\";\n\t\t\treturn this.permitted;\n\t\t} catch {\n\t\t\tthis.permitted = false;\n\t\t\treturn false;\n\t\t}\n\t};\n\n\t/** Start listening to deviceorientation events. Requires prior permission. */\n\tstart = (): void => {\n\t\tif (this.active || !this.permitted) return;\n\t\tthis.active = true;\n\t\twindow.addEventListener(\"deviceorientation\", this.handleOrientation);\n\t};\n\n\t/** Stop listening and remove the deviceorientation event listener. */\n\tstop = (): void => {\n\t\tif (!this.active) return;\n\t\tthis.active = false;\n\t\twindow.removeEventListener(\"deviceorientation\", this.handleOrientation);\n\t};\n\n\t/** Update the axis lock at runtime. */\n\tsetAxis = (axis: Axis): void => {\n\t\tthis.axis = axis;\n\t};\n\n\t/**\n\t * Normalize device orientation angles to [-1, 1] and apply\n\t * exponential moving average smoothing.\n\t *\n\t * beta = X-axis rotation [-180, 180] (front-back tilt)\n\t * gamma = Y-axis rotation [-90, 90] (left-right tilt)\n\t */\n\tprivate handleOrientation = (e: Event): void => {\n\t\tconst evt = e as DeviceOrientationEvt;\n\t\tconst beta = evt.beta ?? 0;\n\t\tconst gamma = evt.gamma ?? 0;\n\n\t\tconst range = this.maxAngle - this.minAngle;\n\t\tconst rawX = this.axis === \"y\" ? 0 : this.clamp(((gamma - this.minAngle) / range) * 2 - 1);\n\t\tconst rawY = this.axis === \"x\" ? 0 : this.clamp(((beta - this.minAngle) / range) * 2 - 1);\n\n\t\tthis.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;\n\t\tthis.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;\n\n\t\tthis.onMove({ x: this.lastX, y: this.lastY });\n\t};\n\n\t/** Clamp a value to the [-1, 1] range. */\n\tprivate clamp = (value: number): number => {\n\t\treturn Math.max(-1, Math.min(1, value));\n\t};\n}\n","import type { Axis } from \"../types.js\";\n\n/** Normalized sensor output with values in [-1, 1] range. */\nexport interface SensorOutput {\n\t/** Normalized X position [-1, 1] */\n\tx: number;\n\t/** Normalized Y position [-1, 1] */\n\ty: number;\n}\n\n/** Callback invoked when sensor detects movement. */\nexport type SensorCallback = (values: SensorOutput) => void;\n\n/**\n * Tracks pointer (mouse/touch) position over an element and normalizes\n * it to a [-1, 1] range relative to the element's bounds.\n *\n * Uses PointerEvent for unified mouse + touch input.\n */\nexport class PointerSensor {\n\tprivate eventsEl: HTMLElement;\n\tprivate onMove: SensorCallback;\n\tprivate onEnter: (() => void) | null;\n\tprivate onLeave: (() => void) | null;\n\tprivate axis: Axis;\n\tprivate active = false;\n\tprivate rect: DOMRect | null = null;\n\tprivate isPointerDown = false;\n\n\tconstructor(\n\t\tel: HTMLElement,\n\t\tonMove: SensorCallback,\n\t\tonEnter: (() => void) | null,\n\t\tonLeave: (() => void) | null,\n\t\taxis: Axis,\n\t\teventsEl: HTMLElement | null = null,\n\t) {\n\t\tthis.eventsEl = eventsEl ?? el;\n\t\tthis.onMove = onMove;\n\t\tthis.onEnter = onEnter;\n\t\tthis.onLeave = onLeave;\n\t\tthis.axis = axis;\n\t}\n\n\t/** Start listening to pointer events. */\n\tstart = (): void => {\n\t\tif (this.active) return;\n\t\tthis.active = true;\n\t\tthis.eventsEl.addEventListener(\"pointermove\", this.handlePointerMove);\n\t\tthis.eventsEl.addEventListener(\"pointerenter\", this.handlePointerEnter);\n\t\tthis.eventsEl.addEventListener(\"pointerleave\", this.handlePointerLeave);\n\t\tthis.eventsEl.addEventListener(\"pointerdown\", this.handlePointerDown);\n\t\tthis.eventsEl.addEventListener(\"pointerup\", this.handlePointerUp);\n\t\tthis.eventsEl.addEventListener(\"pointercancel\", this.handlePointerUp);\n\t};\n\n\t/** Stop listening and remove all pointer event listeners. */\n\tstop = (): void => {\n\t\tif (!this.active) return;\n\t\tthis.active = false;\n\t\tthis.eventsEl.removeEventListener(\"pointermove\", this.handlePointerMove);\n\t\tthis.eventsEl.removeEventListener(\"pointerenter\", this.handlePointerEnter);\n\t\tthis.eventsEl.removeEventListener(\"pointerleave\", this.handlePointerLeave);\n\t\tthis.eventsEl.removeEventListener(\"pointerdown\", this.handlePointerDown);\n\t\tthis.eventsEl.removeEventListener(\"pointerup\", this.handlePointerUp);\n\t\tthis.eventsEl.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\t};\n\n\t/** Update the axis lock at runtime. */\n\tsetAxis = (axis: Axis): void => {\n\t\tthis.axis = axis;\n\t};\n\n\tprivate handlePointerDown = (e: PointerEvent): void => {\n\t\tthis.isPointerDown = true;\n\t\tthis.eventsEl.setPointerCapture(e.pointerId);\n\t\tthis.handlePointerEnter();\n\t\tthis.handlePointerMove(e);\n\t};\n\n\tprivate handlePointerUp = (e: PointerEvent): void => {\n\t\tthis.isPointerDown = false;\n\t\tthis.eventsEl.releasePointerCapture(e.pointerId);\n\t\tthis.handlePointerLeave();\n\t};\n\n\t/**\n\t * Compute normalized [-1, 1] position from pointer coordinates\n\t * relative to the element's bounding rect. Respects axis locking.\n\t */\n\tprivate handlePointerMove = (e: PointerEvent): void => {\n\t\t// On touch devices, pointermove might be the only event we get after pointerdown\n\t\tif (!this.rect) {\n\t\t\tthis.rect = this.eventsEl.getBoundingClientRect();\n\t\t}\n\t\tif (!this.rect) return;\n\n\t\tconst rawX = (e.clientX - this.rect.left) / this.rect.width;\n\t\tconst rawY = (e.clientY - this.rect.top) / this.rect.height;\n\n\t\t// Check if pointer is still within bounds (especially for mouse move without down)\n\t\tconst isOut = rawX < 0 || rawX > 1 || rawY < 0 || rawY > 1;\n\n\t\tif (isOut && !this.isPointerDown) {\n\t\t\tthis.handlePointerLeave();\n\t\t\treturn;\n\t\t}\n\n\t\t// Clamp values if pointer is captured but outside bounds\n\t\tconst clampedX = Math.max(0, Math.min(1, rawX));\n\t\tconst clampedY = Math.max(0, Math.min(1, rawY));\n\n\t\tconst x = this.axis === \"y\" ? 0 : clampedX * 2 - 1;\n\t\tconst y = this.axis === \"x\" ? 0 : clampedY * 2 - 1;\n\n\t\tthis.onMove({ x, y });\n\t};\n\n\tprivate handlePointerEnter = (): void => {\n\t\tthis.rect = this.eventsEl.getBoundingClientRect();\n\t\tthis.onEnter?.();\n\t};\n\n\tprivate handlePointerLeave = (): void => {\n\t\tthis.rect = null;\n\t\tthis.onLeave?.();\n\t};\n}\n","import { DEFAULT_OPTIONS } from \"./constants.js\";\nimport { GlareEffect } from \"./effects/glare.js\";\nimport { ShadowEffect } from \"./effects/shadow.js\";\nimport type { Layer } from \"./layers.js\";\nimport { cleanupLayers, scanLayers } from \"./layers.js\";\nimport { MotionSensor } from \"./sensors/motion.js\";\nimport type { SensorOutput } from \"./sensors/pointer.js\";\nimport { PointerSensor } from \"./sensors/pointer.js\";\nimport type { EventCallback, LevitaEventMap, LevitaOptions, TiltValues } from \"./types.js\";\n\n/**\n * Main entry point for the Levita 3D tilt effect.\n *\n * Orchestrates sensors (pointer, accelerometer), visual effects (glare, shadow),\n * and multi-layer parallax. All rendering is driven by CSS custom properties —\n * no requestAnimationFrame loop runs during interaction.\n *\n * @example\n * ```ts\n * import { Levita } from 'levita-js';\n * import 'levita-js/style.css';\n *\n * const tilt = new Levita(element, { glare: true, shadow: true });\n * tilt.on('move', ({ x, y }) => console.log(x, y));\n * ```\n */\nexport class Levita {\n\tprivate el: HTMLElement;\n\tprivate options: LevitaOptions;\n\tprivate pointerSensor: PointerSensor;\n\tprivate motionSensor: MotionSensor | null = null;\n\tprivate glareEffect: GlareEffect | null = null;\n\tprivate shadowEffect: ShadowEffect | null = null;\n\tprivate layers: Layer[] = [];\n\tprivate listeners = new Map<string, Set<EventCallback<unknown>>>();\n\tprivate destroyed = false;\n\tprivate gyroscopeRequested = false;\n\n\t/**\n\t * @param el - The DOM element to apply the tilt effect to\n\t * @param options - Configuration options (all optional, sensible defaults)\n\t */\n\tconstructor(el: HTMLElement, options: Partial<LevitaOptions> = {}) {\n\t\tthis.el = el;\n\t\tthis.options = { ...DEFAULT_OPTIONS, ...options };\n\n\t\tthis.el.classList.add(\"levita\");\n\t\tthis.applyBaseProperties();\n\n\t\tthis.layers = scanLayers(this.el);\n\n\t\tif (this.options.glare) {\n\t\t\tthis.glareEffect = new GlareEffect(this.el, this.options.maxGlare);\n\t\t}\n\t\tif (this.options.shadow) {\n\t\t\tthis.shadowEffect = new ShadowEffect(this.el);\n\t\t}\n\n\t\tthis.pointerSensor = new PointerSensor(\n\t\t\tthis.el,\n\t\t\t(values) => this.handleSensorInput(values),\n\t\t\t() => this.handleEnter(),\n\t\t\t() => this.handleLeave(),\n\t\t\tthis.options.axis,\n\t\t\tthis.options.eventsEl,\n\t\t);\n\n\t\tif (this.options.gyroscope !== false && MotionSensor.isSupported()) {\n\t\t\tthis.motionSensor = new MotionSensor(\n\t\t\t\t(values) => this.handleSensorInput(values),\n\t\t\t\tthis.options.axis,\n\t\t\t);\n\n\t\t\tif (this.options.gyroscope === \"auto\") {\n\t\t\t\tthis.el.addEventListener(\"click\", this.handleFirstTouch, { once: true });\n\t\t\t}\n\t\t}\n\n\t\tif (!this.options.disabled) {\n\t\t\tthis.enable();\n\t\t}\n\t}\n\n\t/**\n\t * On first touch (iOS auto mode), request accelerometer permission\n\t * and switch from pointer to motion sensor if granted.\n\t */\n\tprivate handleFirstTouch = async (): Promise<void> => {\n\t\tif (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;\n\t\tthis.gyroscopeRequested = true;\n\n\t\tconst granted = await this.motionSensor.requestPermission();\n\t\tif (granted) {\n\t\t\tthis.pointerSensor.stop();\n\t\t\tthis.setTransition(false);\n\t\t\tthis.motionSensor.start();\n\t\t}\n\t};\n\n\t/** Re-enable the tilt effect after a `disable()` call. */\n\tenable = (): void => {\n\t\tif (this.destroyed) return;\n\t\tthis.options.disabled = false;\n\t\tthis.pointerSensor.start();\n\t};\n\n\t/** Pause the tilt effect and reset the element to its neutral position. */\n\tdisable = (): void => {\n\t\tthis.options.disabled = true;\n\t\tthis.pointerSensor.stop();\n\t\tthis.motionSensor?.stop();\n\t\tthis.resetTransform();\n\t};\n\n\t/**\n\t * Manually request accelerometer permission (for `gyroscope: true` mode).\n\t * Must be called from a user gesture on iOS 13+.\n\t *\n\t * @returns Whether permission was granted\n\t */\n\trequestPermission = async (): Promise<boolean> => {\n\t\tif (!this.motionSensor) return false;\n\t\tconst granted = await this.motionSensor.requestPermission();\n\t\tif (granted) {\n\t\t\tthis.pointerSensor.stop();\n\t\t\tthis.setTransition(false);\n\t\t\tthis.motionSensor.start();\n\t\t}\n\t\treturn granted;\n\t};\n\n\t/**\n\t * Fully clean up: stop sensors, remove effects, restore the element\n\t * to its original state. The instance cannot be reused after this.\n\t */\n\tdestroy = (): void => {\n\t\tif (this.destroyed) return;\n\t\tthis.destroyed = true;\n\n\t\tthis.pointerSensor.stop();\n\t\tthis.motionSensor?.stop();\n\t\tthis.glareEffect?.destroy();\n\t\tthis.shadowEffect?.destroy();\n\t\tcleanupLayers(this.layers);\n\n\t\tthis.el.classList.remove(\"levita\");\n\t\tthis.removeBaseProperties();\n\t\tthis.listeners.clear();\n\n\t\tthis.el.removeEventListener(\"click\", this.handleFirstTouch);\n\t};\n\n\t/**\n\t * Register an event listener.\n\t *\n\t * @param event - Event name: 'move', 'enter', or 'leave'\n\t * @param callback - Handler function\n\t */\n\ton = <K extends keyof LevitaEventMap>(\n\t\tevent: K,\n\t\tcallback: EventCallback<LevitaEventMap[K]>,\n\t): void => {\n\t\tif (!this.listeners.has(event)) {\n\t\t\tthis.listeners.set(event, new Set());\n\t\t}\n\t\tthis.listeners.get(event)?.add(callback as EventCallback<unknown>);\n\t};\n\n\t/**\n\t * Remove a previously registered event listener.\n\t *\n\t * @param event - Event name\n\t * @param callback - The exact handler reference passed to `on()`\n\t */\n\toff = <K extends keyof LevitaEventMap>(\n\t\tevent: K,\n\t\tcallback: EventCallback<LevitaEventMap[K]>,\n\t): void => {\n\t\tthis.listeners.get(event)?.delete(callback as EventCallback<unknown>);\n\t};\n\n\t/** Emit an event to all registered listeners. */\n\tprivate emit = <K extends keyof LevitaEventMap>(event: K, data: LevitaEventMap[K]): void => {\n\t\tconst callbacks = this.listeners.get(event);\n\t\tif (callbacks) {\n\t\t\tfor (const cb of callbacks) {\n\t\t\t\tcb(data);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Process normalized sensor input and update CSS custom properties.\n\t * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).\n\t */\n\tprivate handleSensorInput = (input: SensorOutput): void => {\n\t\tif (this.options.disabled) return;\n\n\t\tconst multiplier = this.options.reverse ? -1 : 1;\n\t\tconst x = input.y * this.options.max * multiplier;\n\t\tconst y = input.x * this.options.max * multiplier * -1;\n\n\t\tthis.el.style.setProperty(\"--levita-x\", `${x}deg`);\n\t\tthis.el.style.setProperty(\"--levita-y\", `${y}deg`);\n\t\tthis.el.style.setProperty(\"--levita-scale\", String(this.options.scale));\n\t\tthis.el.style.setProperty(\"--levita-percent-x\", String(input.x));\n\t\tthis.el.style.setProperty(\"--levita-percent-y\", String(input.y));\n\n\t\tthis.glareEffect?.update(input.x, input.y);\n\t\tthis.shadowEffect?.update(input.x, input.y);\n\n\t\tconst values: TiltValues = {\n\t\t\tx,\n\t\t\ty,\n\t\t\tpercentX: input.x,\n\t\t\tpercentY: input.y,\n\t\t};\n\t\tthis.emit(\"move\", values);\n\t};\n\n\tprivate handleEnter = (): void => {\n\t\tthis.setTransition(false);\n\t\tthis.el.style.setProperty(\"--levita-scale\", String(this.options.scale));\n\t\tthis.emit(\"enter\", undefined);\n\t};\n\n\tprivate handleLeave = (): void => {\n\t\tthis.setTransition(true);\n\t\tif (this.options.reset) {\n\t\t\tthis.resetTransform();\n\t\t}\n\t\tthis.emit(\"leave\", undefined);\n\t};\n\n\t/** Toggle the CSS transition on or off. */\n\tprivate setTransition = (on: boolean): void => {\n\t\tthis.el.style.setProperty(\"--levita-speed\", on ? `${this.options.speed}ms` : \"0ms\");\n\t};\n\n\t/** Reset the element to its neutral (non-tilted) position. */\n\tprivate resetTransform = (): void => {\n\t\tthis.el.style.setProperty(\"--levita-x\", \"0deg\");\n\t\tthis.el.style.setProperty(\"--levita-y\", \"0deg\");\n\t\tthis.el.style.setProperty(\"--levita-scale\", \"1\");\n\t\tthis.el.style.setProperty(\"--levita-percent-x\", \"0\");\n\t\tthis.el.style.setProperty(\"--levita-percent-y\", \"0\");\n\t\tthis.glareEffect?.update(0, 0);\n\t\tthis.shadowEffect?.update(0, 0);\n\t};\n\n\t/** Apply initial CSS custom properties from options. */\n\tprivate applyBaseProperties = (): void => {\n\t\tthis.el.style.setProperty(\"--levita-perspective\", `${this.options.perspective}px`);\n\t\tthis.el.style.setProperty(\"--levita-speed\", `${this.options.speed}ms`);\n\t\tthis.el.style.setProperty(\"--levita-easing\", this.options.easing);\n\t};\n\n\t/** Remove all Levita CSS custom properties from the element. */\n\tprivate removeBaseProperties = (): void => {\n\t\tthis.el.style.removeProperty(\"--levita-perspective\");\n\t\tthis.el.style.removeProperty(\"--levita-speed\");\n\t\tthis.el.style.removeProperty(\"--levita-easing\");\n\t\tthis.el.style.removeProperty(\"--levita-x\");\n\t\tthis.el.style.removeProperty(\"--levita-y\");\n\t\tthis.el.style.removeProperty(\"--levita-scale\");\n\t\tthis.el.style.removeProperty(\"--levita-percent-x\");\n\t\tthis.el.style.removeProperty(\"--levita-percent-y\");\n\t};\n}\n"],"mappings":";;AAGA,MAAa,cAAgD;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;;AAMD,MAAa,gBAAgB,WAA2D;CACvF,MAAM,UAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,YACjB,KAAI,OAAO,SAAS,OAEnB,CAAC,QAAgB,OAAO,OAAO;AAGjC,QAAO;;AAGR,MAAa,kBAAiC;CAC7C,KAAK;CACL,aAAa;CACb,OAAO;CACP,OAAO;CACP,QAAQ;CACR,SAAS;CACT,MAAM;CACN,OAAO;CACP,OAAO;CACP,UAAU;CACV,QAAQ;CACR,WAAW;CACX,UAAU;CACV,UAAU;CACV;;;;;;;;;;;;AC1CD,IAAa,cAAb,MAAyB;CACxB,AAAQ;CACR,AAAQ;CACR,AAAQ;;;;;CAMR,YAAY,IAAiB,YAAoB;AAChD,OAAK,aAAa;AAElB,OAAK,YAAY,SAAS,cAAc,MAAM;AAC9C,OAAK,UAAU,UAAU,IAAI,eAAe;AAE5C,OAAK,QAAQ,SAAS,cAAc,MAAM;AAC1C,OAAK,MAAM,UAAU,IAAI,qBAAqB;AAE9C,OAAK,UAAU,YAAY,KAAK,MAAM;AACtC,KAAG,YAAY,KAAK,UAAU;AAE9B,MAAI,CAAC,GAAG,MAAM,YAAY,GAAG,MAAM,aAAa,SAC/C,IAAG,MAAM,WAAW;;;;;;;;;CAWtB,UAAU,aAAqB,gBAA8B;EAC5D,MAAM,UAAW,cAAc,KAAK,IAAK;EACzC,MAAM,UAAW,cAAc,KAAK,IAAK;EACzC,MAAM,YAAY,KAAK,KAAK,eAAe,IAAI,eAAe,EAAE,GAAG,KAAK;AAExE,OAAK,MAAM,MAAM,YAAY,oBAAoB,GAAG,OAAO,GAAG;AAC9D,OAAK,MAAM,MAAM,YAAY,oBAAoB,GAAG,OAAO,GAAG;AAC9D,OAAK,MAAM,MAAM,YAAY,0BAA0B,GAAG,YAAY,KAAK,aAAa;;;CAIzF,gBAAsB;AACrB,OAAK,UAAU,QAAQ;;;;;;;;;;;;;AC9CzB,IAAa,eAAb,MAA0B;CACzB,AAAQ;CACR,AAAQ;;;;;CAMR,YAAY,IAAiB,YAAY,IAAI;AAC5C,OAAK,KAAK;AACV,OAAK,YAAY;AACjB,OAAK,GAAG,UAAU,IAAI,gBAAgB;;;;;;;;CASvC,UAAU,aAAqB,gBAA8B;EAC5D,MAAM,UAAU,cAAc,KAAK;EACnC,MAAM,UAAU,cAAc,KAAK;AAEnC,OAAK,GAAG,MAAM,YAAY,qBAAqB,GAAG,QAAQ,IAAI;AAC9D,OAAK,GAAG,MAAM,YAAY,qBAAqB,GAAG,QAAQ,IAAI;;;CAI/D,gBAAsB;AACrB,OAAK,GAAG,UAAU,OAAO,gBAAgB;AACzC,OAAK,GAAG,MAAM,eAAe,oBAAoB;AACjD,OAAK,GAAG,MAAM,eAAe,oBAAoB;;;;;;;;;;;;;;;;ACrBnD,MAAa,cAAc,cAAoC;CAC9D,MAAM,WAAW,UAAU,iBAA8B,uBAAuB;CAChF,MAAM,SAAkB,EAAE;AAE1B,MAAK,MAAM,MAAM,UAAU;EAC1B,MAAM,MAAM,GAAG,QAAQ;EACvB,MAAM,SAAS,OAAO,WAAW,OAAO,IAAI;AAC5C,MAAI,CAAC,OAAO,MAAM,OAAO,EAAE;AAC1B,MAAG,MAAM,YAAY,mBAAmB,OAAO,OAAO,CAAC;AACvD,UAAO,KAAK;IAAE;IAAI;IAAQ,CAAC;;;AAI7B,QAAO;;;;;;;AAQR,MAAa,iBAAiB,WAA0B;AACvD,MAAK,MAAM,SAAS,OACnB,OAAM,GAAG,MAAM,eAAe,kBAAkB;;;;;;;;;;;;;;ACxBlD,IAAa,eAAb,MAAa,aAAa;CACzB,AAAQ;CACR,AAAQ;CACR,AAAQ,SAAS;CACjB,AAAQ,YAAY;CACpB,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,QAAQ;CAChB,AAAQ,QAAQ;;;;;;;;CAShB,YAAY,QAAwB,MAAY,WAAW,KAAK,WAAW,IAAI,YAAY,KAAM;AAChG,OAAK,SAAS;AACd,OAAK,OAAO;AACZ,OAAK,WAAW;AAChB,OAAK,WAAW;AAChB,OAAK,YAAY;;;CAIlB,OAAO,oBAA6B;AACnC,SAAO,OAAO,WAAW,eAAe,4BAA4B;;;;;;CAOrE,OAAO,wBAAiC;AACvC,SACC,OAAO,2BAA2B,eAAe,uBAAuB;;;;;;;;;;CAY1E,oBAAoB,YAA8B;AACjD,MAAI,CAAC,aAAa,aAAa,CAAE,QAAO;AAExC,MAAI,CAAC,aAAa,iBAAiB,EAAE;AACpC,QAAK,YAAY;AACjB,UAAO;;AAGR,MAAI;AAKH,QAAK,YADU,MAHH,uBAGa,mBAAmB,KAChB;AAC5B,UAAO,KAAK;UACL;AACP,QAAK,YAAY;AACjB,UAAO;;;;CAKT,cAAoB;AACnB,MAAI,KAAK,UAAU,CAAC,KAAK,UAAW;AACpC,OAAK,SAAS;AACd,SAAO,iBAAiB,qBAAqB,KAAK,kBAAkB;;;CAIrE,aAAmB;AAClB,MAAI,CAAC,KAAK,OAAQ;AAClB,OAAK,SAAS;AACd,SAAO,oBAAoB,qBAAqB,KAAK,kBAAkB;;;CAIxE,WAAW,SAAqB;AAC/B,OAAK,OAAO;;;;;;;;;CAUb,AAAQ,qBAAqB,MAAmB;EAC/C,MAAM,MAAM;EACZ,MAAM,OAAO,IAAI,QAAQ;EACzB,MAAM,QAAQ,IAAI,SAAS;EAE3B,MAAM,QAAQ,KAAK,WAAW,KAAK;EACnC,MAAM,OAAO,KAAK,SAAS,MAAM,IAAI,KAAK,OAAQ,QAAQ,KAAK,YAAY,QAAS,IAAI,EAAE;EAC1F,MAAM,OAAO,KAAK,SAAS,MAAM,IAAI,KAAK,OAAQ,OAAO,KAAK,YAAY,QAAS,IAAI,EAAE;AAEzF,OAAK,QAAQ,KAAK,SAAS,OAAO,KAAK,SAAS,KAAK;AACrD,OAAK,QAAQ,KAAK,SAAS,OAAO,KAAK,SAAS,KAAK;AAErD,OAAK,OAAO;GAAE,GAAG,KAAK;GAAO,GAAG,KAAK;GAAO,CAAC;;;CAI9C,AAAQ,SAAS,UAA0B;AAC1C,SAAO,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,MAAM,CAAC;;;;;;;;;;;;AC/GzC,IAAa,gBAAb,MAA2B;CAC1B,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,SAAS;CACjB,AAAQ,OAAuB;CAC/B,AAAQ,gBAAgB;CAExB,YACC,IACA,QACA,SACA,SACA,MACA,WAA+B,MAC9B;AACD,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;AACd,OAAK,UAAU;AACf,OAAK,UAAU;AACf,OAAK,OAAO;;;CAIb,cAAoB;AACnB,MAAI,KAAK,OAAQ;AACjB,OAAK,SAAS;AACd,OAAK,SAAS,iBAAiB,eAAe,KAAK,kBAAkB;AACrE,OAAK,SAAS,iBAAiB,gBAAgB,KAAK,mBAAmB;AACvE,OAAK,SAAS,iBAAiB,gBAAgB,KAAK,mBAAmB;AACvE,OAAK,SAAS,iBAAiB,eAAe,KAAK,kBAAkB;AACrE,OAAK,SAAS,iBAAiB,aAAa,KAAK,gBAAgB;AACjE,OAAK,SAAS,iBAAiB,iBAAiB,KAAK,gBAAgB;;;CAItE,aAAmB;AAClB,MAAI,CAAC,KAAK,OAAQ;AAClB,OAAK,SAAS;AACd,OAAK,SAAS,oBAAoB,eAAe,KAAK,kBAAkB;AACxE,OAAK,SAAS,oBAAoB,gBAAgB,KAAK,mBAAmB;AAC1E,OAAK,SAAS,oBAAoB,gBAAgB,KAAK,mBAAmB;AAC1E,OAAK,SAAS,oBAAoB,eAAe,KAAK,kBAAkB;AACxE,OAAK,SAAS,oBAAoB,aAAa,KAAK,gBAAgB;AACpE,OAAK,SAAS,oBAAoB,iBAAiB,KAAK,gBAAgB;;;CAIzE,WAAW,SAAqB;AAC/B,OAAK,OAAO;;CAGb,AAAQ,qBAAqB,MAA0B;AACtD,OAAK,gBAAgB;AACrB,OAAK,SAAS,kBAAkB,EAAE,UAAU;AAC5C,OAAK,oBAAoB;AACzB,OAAK,kBAAkB,EAAE;;CAG1B,AAAQ,mBAAmB,MAA0B;AACpD,OAAK,gBAAgB;AACrB,OAAK,SAAS,sBAAsB,EAAE,UAAU;AAChD,OAAK,oBAAoB;;;;;;CAO1B,AAAQ,qBAAqB,MAA0B;AAEtD,MAAI,CAAC,KAAK,KACT,MAAK,OAAO,KAAK,SAAS,uBAAuB;AAElD,MAAI,CAAC,KAAK,KAAM;EAEhB,MAAM,QAAQ,EAAE,UAAU,KAAK,KAAK,QAAQ,KAAK,KAAK;EACtD,MAAM,QAAQ,EAAE,UAAU,KAAK,KAAK,OAAO,KAAK,KAAK;AAKrD,OAFc,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,MAE5C,CAAC,KAAK,eAAe;AACjC,QAAK,oBAAoB;AACzB;;EAID,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;EAC/C,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;EAE/C,MAAM,IAAI,KAAK,SAAS,MAAM,IAAI,WAAW,IAAI;EACjD,MAAM,IAAI,KAAK,SAAS,MAAM,IAAI,WAAW,IAAI;AAEjD,OAAK,OAAO;GAAE;GAAG;GAAG,CAAC;;CAGtB,AAAQ,2BAAiC;AACxC,OAAK,OAAO,KAAK,SAAS,uBAAuB;AACjD,OAAK,WAAW;;CAGjB,AAAQ,2BAAiC;AACxC,OAAK,OAAO;AACZ,OAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;ACnGlB,IAAa,SAAb,MAAoB;CACnB,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,eAAoC;CAC5C,AAAQ,cAAkC;CAC1C,AAAQ,eAAoC;CAC5C,AAAQ,SAAkB,EAAE;CAC5B,AAAQ,4BAAY,IAAI,KAA0C;CAClE,AAAQ,YAAY;CACpB,AAAQ,qBAAqB;;;;;CAM7B,YAAY,IAAiB,UAAkC,EAAE,EAAE;AAClE,OAAK,KAAK;AACV,OAAK,UAAU;GAAE,GAAG;GAAiB,GAAG;GAAS;AAEjD,OAAK,GAAG,UAAU,IAAI,SAAS;AAC/B,OAAK,qBAAqB;AAE1B,OAAK,SAAS,WAAW,KAAK,GAAG;AAEjC,MAAI,KAAK,QAAQ,MAChB,MAAK,cAAc,IAAI,YAAY,KAAK,IAAI,KAAK,QAAQ,SAAS;AAEnE,MAAI,KAAK,QAAQ,OAChB,MAAK,eAAe,IAAI,aAAa,KAAK,GAAG;AAG9C,OAAK,gBAAgB,IAAI,cACxB,KAAK,KACJ,WAAW,KAAK,kBAAkB,OAAO,QACpC,KAAK,aAAa,QAClB,KAAK,aAAa,EACxB,KAAK,QAAQ,MACb,KAAK,QAAQ,SACb;AAED,MAAI,KAAK,QAAQ,cAAc,SAAS,aAAa,aAAa,EAAE;AACnE,QAAK,eAAe,IAAI,cACtB,WAAW,KAAK,kBAAkB,OAAO,EAC1C,KAAK,QAAQ,KACb;AAED,OAAI,KAAK,QAAQ,cAAc,OAC9B,MAAK,GAAG,iBAAiB,SAAS,KAAK,kBAAkB,EAAE,MAAM,MAAM,CAAC;;AAI1E,MAAI,CAAC,KAAK,QAAQ,SACjB,MAAK,QAAQ;;;;;;CAQf,AAAQ,mBAAmB,YAA2B;AACrD,MAAI,KAAK,aAAa,KAAK,sBAAsB,CAAC,KAAK,aAAc;AACrE,OAAK,qBAAqB;AAG1B,MADgB,MAAM,KAAK,aAAa,mBAAmB,EAC9C;AACZ,QAAK,cAAc,MAAM;AACzB,QAAK,cAAc,MAAM;AACzB,QAAK,aAAa,OAAO;;;;CAK3B,eAAqB;AACpB,MAAI,KAAK,UAAW;AACpB,OAAK,QAAQ,WAAW;AACxB,OAAK,cAAc,OAAO;;;CAI3B,gBAAsB;AACrB,OAAK,QAAQ,WAAW;AACxB,OAAK,cAAc,MAAM;AACzB,OAAK,cAAc,MAAM;AACzB,OAAK,gBAAgB;;;;;;;;CAStB,oBAAoB,YAA8B;AACjD,MAAI,CAAC,KAAK,aAAc,QAAO;EAC/B,MAAM,UAAU,MAAM,KAAK,aAAa,mBAAmB;AAC3D,MAAI,SAAS;AACZ,QAAK,cAAc,MAAM;AACzB,QAAK,cAAc,MAAM;AACzB,QAAK,aAAa,OAAO;;AAE1B,SAAO;;;;;;CAOR,gBAAsB;AACrB,MAAI,KAAK,UAAW;AACpB,OAAK,YAAY;AAEjB,OAAK,cAAc,MAAM;AACzB,OAAK,cAAc,MAAM;AACzB,OAAK,aAAa,SAAS;AAC3B,OAAK,cAAc,SAAS;AAC5B,gBAAc,KAAK,OAAO;AAE1B,OAAK,GAAG,UAAU,OAAO,SAAS;AAClC,OAAK,sBAAsB;AAC3B,OAAK,UAAU,OAAO;AAEtB,OAAK,GAAG,oBAAoB,SAAS,KAAK,iBAAiB;;;;;;;;CAS5D,MACC,OACA,aACU;AACV,MAAI,CAAC,KAAK,UAAU,IAAI,MAAM,CAC7B,MAAK,UAAU,IAAI,uBAAO,IAAI,KAAK,CAAC;AAErC,OAAK,UAAU,IAAI,MAAM,EAAE,IAAI,SAAmC;;;;;;;;CASnE,OACC,OACA,aACU;AACV,OAAK,UAAU,IAAI,MAAM,EAAE,OAAO,SAAmC;;;CAItE,AAAQ,QAAwC,OAAU,SAAkC;EAC3F,MAAM,YAAY,KAAK,UAAU,IAAI,MAAM;AAC3C,MAAI,UACH,MAAK,MAAM,MAAM,UAChB,IAAG,KAAK;;;;;;CASX,AAAQ,qBAAqB,UAA8B;AAC1D,MAAI,KAAK,QAAQ,SAAU;EAE3B,MAAM,aAAa,KAAK,QAAQ,UAAU,KAAK;EAC/C,MAAM,IAAI,MAAM,IAAI,KAAK,QAAQ,MAAM;EACvC,MAAM,IAAI,MAAM,IAAI,KAAK,QAAQ,MAAM,aAAa;AAEpD,OAAK,GAAG,MAAM,YAAY,cAAc,GAAG,EAAE,KAAK;AAClD,OAAK,GAAG,MAAM,YAAY,cAAc,GAAG,EAAE,KAAK;AAClD,OAAK,GAAG,MAAM,YAAY,kBAAkB,OAAO,KAAK,QAAQ,MAAM,CAAC;AACvE,OAAK,GAAG,MAAM,YAAY,sBAAsB,OAAO,MAAM,EAAE,CAAC;AAChE,OAAK,GAAG,MAAM,YAAY,sBAAsB,OAAO,MAAM,EAAE,CAAC;AAEhE,OAAK,aAAa,OAAO,MAAM,GAAG,MAAM,EAAE;AAC1C,OAAK,cAAc,OAAO,MAAM,GAAG,MAAM,EAAE;EAE3C,MAAM,SAAqB;GAC1B;GACA;GACA,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB;AACD,OAAK,KAAK,QAAQ,OAAO;;CAG1B,AAAQ,oBAA0B;AACjC,OAAK,cAAc,MAAM;AACzB,OAAK,GAAG,MAAM,YAAY,kBAAkB,OAAO,KAAK,QAAQ,MAAM,CAAC;AACvE,OAAK,KAAK,SAAS,OAAU;;CAG9B,AAAQ,oBAA0B;AACjC,OAAK,cAAc,KAAK;AACxB,MAAI,KAAK,QAAQ,MAChB,MAAK,gBAAgB;AAEtB,OAAK,KAAK,SAAS,OAAU;;;CAI9B,AAAQ,iBAAiB,OAAsB;AAC9C,OAAK,GAAG,MAAM,YAAY,kBAAkB,KAAK,GAAG,KAAK,QAAQ,MAAM,MAAM,MAAM;;;CAIpF,AAAQ,uBAA6B;AACpC,OAAK,GAAG,MAAM,YAAY,cAAc,OAAO;AAC/C,OAAK,GAAG,MAAM,YAAY,cAAc,OAAO;AAC/C,OAAK,GAAG,MAAM,YAAY,kBAAkB,IAAI;AAChD,OAAK,GAAG,MAAM,YAAY,sBAAsB,IAAI;AACpD,OAAK,GAAG,MAAM,YAAY,sBAAsB,IAAI;AACpD,OAAK,aAAa,OAAO,GAAG,EAAE;AAC9B,OAAK,cAAc,OAAO,GAAG,EAAE;;;CAIhC,AAAQ,4BAAkC;AACzC,OAAK,GAAG,MAAM,YAAY,wBAAwB,GAAG,KAAK,QAAQ,YAAY,IAAI;AAClF,OAAK,GAAG,MAAM,YAAY,kBAAkB,GAAG,KAAK,QAAQ,MAAM,IAAI;AACtE,OAAK,GAAG,MAAM,YAAY,mBAAmB,KAAK,QAAQ,OAAO;;;CAIlE,AAAQ,6BAAmC;AAC1C,OAAK,GAAG,MAAM,eAAe,uBAAuB;AACpD,OAAK,GAAG,MAAM,eAAe,iBAAiB;AAC9C,OAAK,GAAG,MAAM,eAAe,kBAAkB;AAC/C,OAAK,GAAG,MAAM,eAAe,aAAa;AAC1C,OAAK,GAAG,MAAM,eAAe,aAAa;AAC1C,OAAK,GAAG,MAAM,eAAe,iBAAiB;AAC9C,OAAK,GAAG,MAAM,eAAe,qBAAqB;AAClD,OAAK,GAAG,MAAM,eAAe,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/effects/glare.ts","../src/effects/shadow.ts","../src/layers.ts","../src/sensors/motion.ts","../src/sensors/pointer.ts","../src/levita.ts"],"sourcesContent":["import type { LevitaOptions } from \"./types.js\";\n\n/** All keys of `LevitaOptions`, derived from `DEFAULT_OPTIONS`. */\nexport const OPTION_KEYS: readonly (keyof LevitaOptions)[] = [\n\t\"max\",\n\t\"perspective\",\n\t\"scale\",\n\t\"speed\",\n\t\"easing\",\n\t\"reverse\",\n\t\"axis\",\n\t\"reset\",\n\t\"glare\",\n\t\"maxGlare\",\n\t\"shadow\",\n\t\"gyroscope\",\n\t\"disabled\",\n\t\"eventsEl\",\n] as const;\n\n/**\n * Build a partial `LevitaOptions` object from a source,\n * including only keys that are explicitly defined.\n */\nexport const buildOptions = (source: Partial<LevitaOptions>): Partial<LevitaOptions> => {\n\tconst options: Partial<LevitaOptions> = {};\n\tfor (const key of OPTION_KEYS) {\n\t\tif (source[key] !== undefined) {\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: generic key assignment\n\t\t\t(options as any)[key] = source[key];\n\t\t}\n\t}\n\treturn options;\n};\n\nexport const DEFAULT_OPTIONS: LevitaOptions = {\n\tmax: 15,\n\tperspective: 1000,\n\tscale: 1.05,\n\tspeed: 200,\n\teasing: \"ease-out\",\n\treverse: false,\n\taxis: null,\n\treset: true,\n\tglare: false,\n\tmaxGlare: 0.5,\n\tshadow: false,\n\tgyroscope: \"auto\",\n\tdisabled: false,\n\teventsEl: null,\n};\n","/**\n * Creates a radial gradient overlay that follows the tilt position,\n * simulating light reflection on the surface.\n *\n * Injects two DOM elements (.levita-glare > .levita-glare-inner) into\n * the target element. Position and opacity are driven by CSS custom\n * properties — no JS runs per animation frame.\n */\nexport class GlareEffect {\n\tprivate container: HTMLElement;\n\tprivate inner: HTMLElement;\n\tprivate maxOpacity: number;\n\n\t/**\n\t * @param el - The element to attach the glare overlay to\n\t * @param maxOpacity - Maximum glare opacity (0-1)\n\t */\n\tconstructor(el: HTMLElement, maxOpacity: number) {\n\t\tthis.maxOpacity = maxOpacity;\n\n\t\tthis.container = document.createElement(\"div\");\n\t\tthis.container.classList.add(\"levita-glare\");\n\n\t\tthis.inner = document.createElement(\"div\");\n\t\tthis.inner.classList.add(\"levita-glare-inner\");\n\n\t\tthis.container.appendChild(this.inner);\n\t\tel.appendChild(this.container);\n\n\t\tif (!el.style.position || el.style.position === \"static\") {\n\t\t\tel.style.position = \"relative\";\n\t\t}\n\t}\n\n\t/**\n\t * Update glare position and intensity based on normalized tilt values.\n\t * Sets CSS custom properties that the stylesheet uses for rendering.\n\t *\n\t * @param normalizedX - Horizontal position [-1, 1]\n\t * @param normalizedY - Vertical position [-1, 1]\n\t */\n\tupdate = (normalizedX: number, normalizedY: number): void => {\n\t\tconst glareX = ((normalizedX + 1) / 2) * 100;\n\t\tconst glareY = ((normalizedY + 1) / 2) * 100;\n\t\tconst intensity = Math.sqrt(normalizedX ** 2 + normalizedY ** 2) / Math.SQRT2;\n\n\t\tthis.inner.style.setProperty(\"--levita-glare-x\", `${glareX}%`);\n\t\tthis.inner.style.setProperty(\"--levita-glare-y\", `${glareY}%`);\n\t\tthis.inner.style.setProperty(\"--levita-glare-opacity\", `${intensity * this.maxOpacity}`);\n\t};\n\n\t/** Remove the glare DOM elements from the parent. */\n\tdestroy = (): void => {\n\t\tthis.container.remove();\n\t};\n}\n","/**\n * Adds a dynamic drop shadow that shifts based on the tilt angle,\n * reinforcing the 3D depth illusion.\n *\n * Uses CSS custom properties (--levita-shadow-x, --levita-shadow-y)\n * combined with `filter: drop-shadow()` — no JS runs per animation frame.\n */\nexport class ShadowEffect {\n\tprivate el: HTMLElement;\n\tprivate maxOffset: number;\n\n\t/**\n\t * @param el - The element to apply the shadow to\n\t * @param maxOffset - Maximum shadow offset in pixels (default: 20)\n\t */\n\tconstructor(el: HTMLElement, maxOffset = 20) {\n\t\tthis.el = el;\n\t\tthis.maxOffset = maxOffset;\n\t\tthis.el.classList.add(\"levita-shadow\");\n\t}\n\n\t/**\n\t * Update shadow offset based on normalized tilt values.\n\t *\n\t * @param normalizedX - Horizontal position [-1, 1]\n\t * @param normalizedY - Vertical position [-1, 1]\n\t */\n\tupdate = (normalizedX: number, normalizedY: number): void => {\n\t\tconst shadowX = normalizedX * this.maxOffset;\n\t\tconst shadowY = normalizedY * this.maxOffset;\n\n\t\tthis.el.style.setProperty(\"--levita-shadow-x\", `${shadowX}px`);\n\t\tthis.el.style.setProperty(\"--levita-shadow-y\", `${shadowY}px`);\n\t};\n\n\t/** Remove the shadow class and clean up CSS custom properties. */\n\tdestroy = (): void => {\n\t\tthis.el.classList.remove(\"levita-shadow\");\n\t\tthis.el.style.removeProperty(\"--levita-shadow-x\");\n\t\tthis.el.style.removeProperty(\"--levita-shadow-y\");\n\t};\n}\n","/** Represents a child element with a parallax depth offset. */\nexport interface Layer {\n\t/** The DOM element */\n\tel: HTMLElement;\n\t/** The depth offset value (positive = forward, negative = back) */\n\toffset: number;\n}\n\n/**\n * Scan a container for children with `data-levita-offset` attributes\n * and set the `--levita-offset` CSS custom property on each one.\n *\n * The CSS stylesheet uses this variable with `translateZ()` to position\n * layers at different depths — no JS runs per animation frame.\n *\n * @param container - The parent element to scan for layer children\n * @returns Array of discovered layers with their elements and offsets\n */\nexport const scanLayers = (container: HTMLElement): Layer[] => {\n\tconst elements = container.querySelectorAll<HTMLElement>(\"[data-levita-offset]\");\n\tconst layers: Layer[] = [];\n\n\tfor (const el of elements) {\n\t\tconst raw = el.dataset.levitaOffset;\n\t\tconst offset = Number.parseFloat(raw ?? \"0\");\n\t\tif (!Number.isNaN(offset)) {\n\t\t\tel.style.setProperty(\"--levita-offset\", String(offset));\n\t\t\tlayers.push({ el, offset });\n\t\t}\n\t}\n\n\treturn layers;\n};\n\n/**\n * Remove the `--levita-offset` CSS custom property from all layer elements.\n *\n * @param layers - The layers to clean up\n */\nexport const cleanupLayers = (layers: Layer[]): void => {\n\tfor (const layer of layers) {\n\t\tlayer.el.style.removeProperty(\"--levita-offset\");\n\t}\n};\n","import type { Axis } from \"../types.js\";\nimport type { SensorCallback } from \"./pointer.js\";\n\ninterface DeviceOrientationEvt extends Event {\n\tbeta: number | null;\n\tgamma: number | null;\n}\n\n/**\n * Reads device orientation (accelerometer/gyroscope) and normalizes\n * the tilt angles to a [-1, 1] range.\n *\n * Handles iOS 13+ permission flow via async `requestPermission()`.\n * On Android, permission is granted automatically.\n *\n * Uses exponential moving average for smoothing raw sensor data.\n */\nexport class MotionSensor {\n\tprivate onMove: SensorCallback;\n\tprivate onFirstEvent: (() => void) | null;\n\tprivate axis: Axis;\n\tprivate active = false;\n\tprivate permitted = false;\n\tprivate receivedEvent = false;\n\tprivate warmup = false;\n\tprivate minAngle: number;\n\tprivate maxAngle: number;\n\tprivate smoothing: number;\n\tprivate lastX = 0;\n\tprivate lastY = 0;\n\tprivate baseBeta: number | null = null;\n\tprivate baseGamma: number | null = null;\n\n\t/**\n\t * @param onMove - Callback receiving normalized { x, y } values\n\t * @param axis - Restrict input to a single axis, or null for both\n\t * @param minAngle - Minimum device angle mapped to -1 (default: -45)\n\t * @param maxAngle - Maximum device angle mapped to 1 (default: 45)\n\t * @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)\n\t * @param onFirstEvent - Called once when the first valid deviceorientation event is received\n\t */\n\tconstructor(\n\t\tonMove: SensorCallback,\n\t\taxis: Axis,\n\t\tminAngle = -45,\n\t\tmaxAngle = 45,\n\t\tsmoothing = 0.15,\n\t\tonFirstEvent: (() => void) | null = null,\n\t) {\n\t\tthis.onMove = onMove;\n\t\tthis.axis = axis;\n\t\tthis.minAngle = minAngle;\n\t\tthis.maxAngle = maxAngle;\n\t\tthis.smoothing = smoothing;\n\t\tthis.onFirstEvent = onFirstEvent;\n\t}\n\n\t/** Check if the DeviceOrientationEvent API is available in this environment. */\n\tstatic isSupported = (): boolean => {\n\t\treturn typeof window !== \"undefined\" && \"DeviceOrientationEvent\" in window;\n\t};\n\n\t/**\n\t * Check if explicit permission is required (iOS 13+).\n\t * On Android and desktop, this returns false.\n\t */\n\tstatic needsPermission = (): boolean => {\n\t\treturn (\n\t\t\ttypeof DeviceOrientationEvent !== \"undefined\" && \"requestPermission\" in DeviceOrientationEvent\n\t\t);\n\t};\n\n\t/**\n\t * Request permission to access device orientation.\n\t * On Android, resolves immediately to true.\n\t * On iOS 13+, triggers the native permission dialog.\n\t * Must be called from a user gesture on iOS.\n\t *\n\t * @returns Whether permission was granted\n\t */\n\trequestPermission = async (): Promise<boolean> => {\n\t\tif (!MotionSensor.isSupported()) return false;\n\n\t\tif (!MotionSensor.needsPermission()) {\n\t\t\tthis.permitted = true;\n\t\t\treturn true;\n\t\t}\n\n\t\ttry {\n\t\t\tconst DOE = DeviceOrientationEvent as unknown as {\n\t\t\t\trequestPermission: () => Promise<string>;\n\t\t\t};\n\t\t\tconst result = await DOE.requestPermission();\n\t\t\tthis.permitted = result === \"granted\";\n\t\t\treturn this.permitted;\n\t\t} catch {\n\t\t\tthis.permitted = false;\n\t\t\treturn false;\n\t\t}\n\t};\n\n\t/** Start listening to deviceorientation events. Requires prior permission. */\n\tstart = (): void => {\n\t\tif (this.active || !this.permitted) return;\n\t\tthis.active = true;\n\t\tthis.receivedEvent = false;\n\t\tthis.warmup = false;\n\t\tthis.baseBeta = null;\n\t\tthis.baseGamma = null;\n\t\twindow.addEventListener(\"deviceorientation\", this.handleOrientation);\n\t};\n\n\t/** Stop listening and remove the deviceorientation event listener. */\n\tstop = (): void => {\n\t\tif (!this.active) return;\n\t\tthis.active = false;\n\t\twindow.removeEventListener(\"deviceorientation\", this.handleOrientation);\n\t};\n\n\t/** Update the axis lock at runtime. */\n\tsetAxis = (axis: Axis): void => {\n\t\tthis.axis = axis;\n\t};\n\n\t/**\n\t * Normalize device orientation angles to [-1, 1] and apply\n\t * exponential moving average smoothing.\n\t *\n\t * beta = X-axis rotation [-180, 180] (front-back tilt)\n\t * gamma = Y-axis rotation [-90, 90] (left-right tilt)\n\t */\n\tprivate handleOrientation = (e: Event): void => {\n\t\tconst evt = e as DeviceOrientationEvt;\n\t\tif (evt.beta === null && evt.gamma === null) return;\n\n\t\t// Skip first event — some Android browsers fire stale sensor data\n\t\t// that doesn't reflect the actual device position.\n\t\tif (!this.warmup) {\n\t\t\tthis.warmup = true;\n\t\t\treturn;\n\t\t}\n\n\t\tconst beta = evt.beta ?? 0;\n\t\tconst gamma = evt.gamma ?? 0;\n\n\t\t// Notify on first valid event (used for pointer→motion handoff)\n\t\tif (!this.receivedEvent) {\n\t\t\tthis.receivedEvent = true;\n\t\t\tthis.onFirstEvent?.();\n\t\t}\n\n\t\t// Calibrate: capture the first reading as the neutral position\n\t\tif (this.baseBeta === null) {\n\t\t\tthis.baseBeta = beta;\n\t\t\tthis.baseGamma = gamma;\n\t\t}\n\n\t\t// Compute tilt relative to initial device position\n\t\tconst relativeBeta = beta - this.baseBeta;\n\t\tconst relativeGamma = gamma - (this.baseGamma as number);\n\n\t\tconst range = this.maxAngle - this.minAngle;\n\t\tconst rawX = this.axis === \"y\" ? 0 : this.clamp(relativeGamma / (range / 2));\n\t\tconst rawY = this.axis === \"x\" ? 0 : this.clamp(relativeBeta / (range / 2));\n\n\t\tthis.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;\n\t\tthis.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;\n\n\t\tthis.onMove({ x: this.lastX, y: this.lastY });\n\t};\n\n\t/** Clamp a value to the [-1, 1] range. */\n\tprivate clamp = (value: number): number => {\n\t\treturn Math.max(-1, Math.min(1, value));\n\t};\n}\n","import type { Axis } from \"../types.js\";\n\n/** Normalized sensor output with values in [-1, 1] range. */\nexport interface SensorOutput {\n\t/** Normalized X position [-1, 1] */\n\tx: number;\n\t/** Normalized Y position [-1, 1] */\n\ty: number;\n}\n\n/** Callback invoked when sensor detects movement. */\nexport type SensorCallback = (values: SensorOutput) => void;\n\n/**\n * Tracks pointer (mouse/touch) position over an element and normalizes\n * it to a [-1, 1] range relative to the element's bounds.\n *\n * Uses PointerEvent for unified mouse + touch input.\n */\nexport class PointerSensor {\n\tprivate eventsEl: HTMLElement;\n\tprivate onMove: SensorCallback;\n\tprivate onEnter: (() => void) | null;\n\tprivate onLeave: (() => void) | null;\n\tprivate axis: Axis;\n\tprivate active = false;\n\tprivate rect: DOMRect | null = null;\n\n\tconstructor(\n\t\tel: HTMLElement,\n\t\tonMove: SensorCallback,\n\t\tonEnter: (() => void) | null,\n\t\tonLeave: (() => void) | null,\n\t\taxis: Axis,\n\t\teventsEl: HTMLElement | null = null,\n\t) {\n\t\tthis.eventsEl = eventsEl ?? el;\n\t\tthis.onMove = onMove;\n\t\tthis.onEnter = onEnter;\n\t\tthis.onLeave = onLeave;\n\t\tthis.axis = axis;\n\t}\n\n\t/** Start listening to pointer events. */\n\tstart = (): void => {\n\t\tif (this.active) return;\n\t\tthis.active = true;\n\t\tthis.eventsEl.addEventListener(\"pointermove\", this.handlePointerMove);\n\t\tthis.eventsEl.addEventListener(\"pointerenter\", this.handlePointerEnter);\n\t\tthis.eventsEl.addEventListener(\"pointerleave\", this.handlePointerLeave);\n\t\tthis.eventsEl.addEventListener(\"pointerdown\", this.handlePointerDown);\n\t\tthis.eventsEl.addEventListener(\"pointerup\", this.handlePointerUp);\n\t\tthis.eventsEl.addEventListener(\"pointercancel\", this.handlePointerUp);\n\t};\n\n\t/** Stop listening and remove all pointer event listeners. */\n\tstop = (): void => {\n\t\tif (!this.active) return;\n\t\tthis.active = false;\n\t\tthis.eventsEl.removeEventListener(\"pointermove\", this.handlePointerMove);\n\t\tthis.eventsEl.removeEventListener(\"pointerenter\", this.handlePointerEnter);\n\t\tthis.eventsEl.removeEventListener(\"pointerleave\", this.handlePointerLeave);\n\t\tthis.eventsEl.removeEventListener(\"pointerdown\", this.handlePointerDown);\n\t\tthis.eventsEl.removeEventListener(\"pointerup\", this.handlePointerUp);\n\t\tthis.eventsEl.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\t};\n\n\t/** Update the axis lock at runtime. */\n\tsetAxis = (axis: Axis): void => {\n\t\tthis.axis = axis;\n\t};\n\n\tprivate handlePointerDown = (e: PointerEvent): void => {\n\t\tthis.handlePointerEnter();\n\t\tthis.handlePointerMove(e);\n\t};\n\n\tprivate handlePointerUp = (): void => {\n\t\tthis.handlePointerLeave();\n\t};\n\n\t/**\n\t * Compute normalized [-1, 1] position from pointer coordinates\n\t * relative to the element's bounding rect. Respects axis locking.\n\t */\n\tprivate handlePointerMove = (e: PointerEvent): void => {\n\t\tif (!this.rect) {\n\t\t\tthis.rect = this.eventsEl.getBoundingClientRect();\n\t\t}\n\n\t\t// Use offsetWidth/Height for more stable dimensions (not affected by 3D transforms)\n\t\tconst width = this.eventsEl.offsetWidth || this.rect.width;\n\t\tconst height = this.eventsEl.offsetHeight || this.rect.height;\n\n\t\tconst rawX = (e.clientX - this.rect.left) / width;\n\t\tconst rawY = (e.clientY - this.rect.top) / height;\n\n\t\t// Clamp values if pointer is captured but outside bounds\n\t\tconst clampedX = Math.max(0, Math.min(1, rawX));\n\t\tconst clampedY = Math.max(0, Math.min(1, rawY));\n\n\t\tconst x = this.axis === \"y\" ? 0 : clampedX * 2 - 1;\n\t\tconst y = this.axis === \"x\" ? 0 : clampedY * 2 - 1;\n\n\t\tthis.onMove({ x, y });\n\t};\n\n\tprivate handlePointerEnter = (): void => {\n\t\t// Refresh rect on enter. If it's already in motion, this might be slightly off,\n\t\t// but it's still better than refreshing it on every move.\n\t\tthis.rect = this.eventsEl.getBoundingClientRect();\n\t\tthis.onEnter?.();\n\t};\n\n\tprivate handlePointerLeave = (): void => {\n\t\tthis.rect = null;\n\t\tthis.onLeave?.();\n\t};\n}\n","import { DEFAULT_OPTIONS } from \"./constants.js\";\nimport { GlareEffect } from \"./effects/glare.js\";\nimport { ShadowEffect } from \"./effects/shadow.js\";\nimport type { Layer } from \"./layers.js\";\nimport { cleanupLayers, scanLayers } from \"./layers.js\";\nimport { MotionSensor } from \"./sensors/motion.js\";\nimport type { SensorOutput } from \"./sensors/pointer.js\";\nimport { PointerSensor } from \"./sensors/pointer.js\";\nimport type {\n\tEventCallback,\n\tLevitaEventMap,\n\tLevitaOptions,\n\tTiltValues,\n\tUpdatableOptions,\n} from \"./types.js\";\n\n/**\n * Main entry point for the Levita 3D tilt effect.\n *\n * Orchestrates sensors (pointer, accelerometer), visual effects (glare, shadow),\n * and multi-layer parallax. All rendering is driven by CSS custom properties —\n * no requestAnimationFrame loop runs during interaction.\n *\n * @example\n * ```ts\n * import { Levita } from 'levita-js';\n * import 'levita-js/style.css';\n *\n * const tilt = new Levita(element, { glare: true, shadow: true });\n * tilt.on('move', ({ x, y }) => console.log(x, y));\n * ```\n */\nexport class Levita {\n\tprivate el: HTMLElement;\n\tprivate options: LevitaOptions;\n\tprivate pointerSensor: PointerSensor;\n\tprivate motionSensor: MotionSensor | null = null;\n\tprivate glareEffect: GlareEffect | null = null;\n\tprivate shadowEffect: ShadowEffect | null = null;\n\tprivate layers: Layer[] = [];\n\tprivate listeners = new Map<string, Set<EventCallback<unknown>>>();\n\tprivate destroyed = false;\n\tprivate gyroscopeRequested = false;\n\tprivate gyroscopeEvent: string | null = null;\n\tprivate rafId: number | null = null;\n\n\t/**\n\t * @param el - The DOM element to apply the tilt effect to\n\t * @param options - Configuration options (all optional, sensible defaults)\n\t */\n\tconstructor(el: HTMLElement, options: Partial<LevitaOptions> = {}) {\n\t\tthis.el = el;\n\t\tthis.options = { ...DEFAULT_OPTIONS, ...options };\n\n\t\tthis.el.classList.add(\"levita\");\n\t\tthis.applyBaseProperties();\n\n\t\tthis.layers = scanLayers(this.el);\n\n\t\tif (this.options.glare) {\n\t\t\tthis.glareEffect = new GlareEffect(this.el, this.options.maxGlare);\n\t\t}\n\t\tif (this.options.shadow) {\n\t\t\tthis.shadowEffect = new ShadowEffect(this.el);\n\t\t}\n\n\t\tthis.pointerSensor = new PointerSensor(\n\t\t\tthis.el,\n\t\t\t(values) => this.handleSensorInput(values),\n\t\t\t() => this.handleEnter(),\n\t\t\t() => this.handleLeave(),\n\t\t\tthis.options.axis,\n\t\t\tthis.options.eventsEl,\n\t\t);\n\n\t\tif (this.options.gyroscope !== false && MotionSensor.isSupported()) {\n\t\t\tthis.motionSensor = new MotionSensor(\n\t\t\t\t(values) => this.handleSensorInput(values),\n\t\t\t\tthis.options.axis,\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\t() => this.handleMotionReady(),\n\t\t\t);\n\n\t\t\tif (this.options.gyroscope === \"auto\") {\n\t\t\t\t// iOS requires \"click\" for DeviceOrientationEvent.requestPermission(),\n\t\t\t\t// Android fires \"pointerup\" more reliably on touch interactions.\n\t\t\t\tthis.gyroscopeEvent = MotionSensor.needsPermission() ? \"click\" : \"pointerup\";\n\t\t\t\tthis.el.addEventListener(this.gyroscopeEvent, this.handleFirstTouch, { once: true });\n\t\t\t}\n\t\t}\n\n\t\tif (!this.options.disabled) {\n\t\t\tthis.enable();\n\t\t}\n\t}\n\n\t/**\n\t * On first interaction, request accelerometer permission and start the\n\t * motion sensor. The pointer sensor keeps running until the first valid\n\t * deviceorientation event is received (see handleMotionReady).\n\t */\n\tprivate handleFirstTouch = async (): Promise<void> => {\n\t\tif (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;\n\t\tthis.gyroscopeRequested = true;\n\n\t\tconst granted = await this.motionSensor.requestPermission();\n\t\tif (granted) {\n\t\t\tthis.motionSensor.start();\n\t\t}\n\t};\n\n\t/**\n\t * Called when the motion sensor receives its first valid event.\n\t * At this point it's safe to hand off from pointer to motion.\n\t */\n\tprivate handleMotionReady = (): void => {\n\t\tthis.pointerSensor.stop();\n\t\tthis.setTransition(false);\n\t};\n\n\t/**\n\t * Update options at runtime without destroying/recreating the instance.\n\t *\n\t * Only \"lightweight\" options are supported — those that don't require\n\t * DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,\n\t * `disabled`, and `maxGlare` require a full destroy/recreate cycle\n\t * (or use `enable()`/`disable()` for toggling the effect).\n\t */\n\tupdate = (options: Partial<UpdatableOptions>): void => {\n\t\tif (this.destroyed) return;\n\t\tObject.assign(this.options, options);\n\t\tthis.applyBaseProperties();\n\n\t\tif (options.axis !== undefined) {\n\t\t\tthis.pointerSensor.setAxis(options.axis);\n\t\t\tthis.motionSensor?.setAxis(options.axis);\n\t\t}\n\t};\n\n\t/** Re-enable the tilt effect after a `disable()` call. */\n\tenable = (): void => {\n\t\tif (this.destroyed) return;\n\t\tthis.options.disabled = false;\n\t\tthis.pointerSensor.start();\n\t};\n\n\t/** Pause the tilt effect and reset the element to its neutral position. */\n\tdisable = (): void => {\n\t\tthis.options.disabled = true;\n\t\tthis.pointerSensor.stop();\n\t\tthis.motionSensor?.stop();\n\t\tthis.resetTransform();\n\t};\n\n\t/**\n\t * Manually request accelerometer permission (for `gyroscope: true` mode).\n\t * Must be called from a user gesture on iOS 13+.\n\t *\n\t * @returns Whether permission was granted\n\t */\n\trequestPermission = async (): Promise<boolean> => {\n\t\tif (!this.motionSensor) return false;\n\t\tconst granted = await this.motionSensor.requestPermission();\n\t\tif (granted) {\n\t\t\tthis.motionSensor.start();\n\t\t}\n\t\treturn granted;\n\t};\n\n\t/**\n\t * Fully clean up: stop sensors, remove effects, restore the element\n\t * to its original state. The instance cannot be reused after this.\n\t */\n\tdestroy = (): void => {\n\t\tif (this.destroyed) return;\n\t\tthis.destroyed = true;\n\n\t\tif (this.rafId) cancelAnimationFrame(this.rafId);\n\t\tthis.pointerSensor.stop();\n\t\tthis.motionSensor?.stop();\n\t\tthis.glareEffect?.destroy();\n\t\tthis.shadowEffect?.destroy();\n\t\tcleanupLayers(this.layers);\n\n\t\tthis.el.classList.remove(\"levita\");\n\t\tthis.removeBaseProperties();\n\t\tthis.listeners.clear();\n\n\t\tif (this.gyroscopeEvent) {\n\t\t\tthis.el.removeEventListener(this.gyroscopeEvent, this.handleFirstTouch);\n\t\t}\n\t};\n\n\t/**\n\t * Register an event listener.\n\t *\n\t * @param event - Event name: 'move', 'enter', or 'leave'\n\t * @param callback - Handler function\n\t */\n\ton = <K extends keyof LevitaEventMap>(\n\t\tevent: K,\n\t\tcallback: EventCallback<LevitaEventMap[K]>,\n\t): void => {\n\t\tif (!this.listeners.has(event)) {\n\t\t\tthis.listeners.set(event, new Set());\n\t\t}\n\t\tthis.listeners.get(event)?.add(callback as EventCallback<unknown>);\n\t};\n\n\t/**\n\t * Remove a previously registered event listener.\n\t *\n\t * @param event - Event name\n\t * @param callback - The exact handler reference passed to `on()`\n\t */\n\toff = <K extends keyof LevitaEventMap>(\n\t\tevent: K,\n\t\tcallback: EventCallback<LevitaEventMap[K]>,\n\t): void => {\n\t\tthis.listeners.get(event)?.delete(callback as EventCallback<unknown>);\n\t};\n\n\t/** Emit an event to all registered listeners. */\n\tprivate emit = <K extends keyof LevitaEventMap>(event: K, data: LevitaEventMap[K]): void => {\n\t\tconst callbacks = this.listeners.get(event);\n\t\tif (callbacks) {\n\t\t\tfor (const cb of callbacks) {\n\t\t\t\tcb(data);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Process normalized sensor input and update CSS custom properties.\n\t * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).\n\t *\n\t * Note: We use requestAnimationFrame to throttle updates. High-polling rate mice\n\t * can fire hundreds of events per second, which would saturate the main thread\n\t * with CSS variable updates and style recalculations. This ensures we only\n\t * update the DOM once per browser frame.\n\t */\n\tprivate handleSensorInput = (input: SensorOutput): void => {\n\t\tif (this.options.disabled) return;\n\n\t\tconst multiplier = this.options.reverse ? -1 : 1;\n\t\tconst x = input.y * this.options.max * multiplier;\n\t\tconst y = input.x * this.options.max * multiplier * -1;\n\n\t\t// Cancel any pending update from the same frame to avoid style thrashing\n\t\tif (this.rafId) cancelAnimationFrame(this.rafId);\n\n\t\tthis.rafId = requestAnimationFrame(() => {\n\t\t\tthis.el.style.setProperty(\"--levita-x\", `${x}deg`);\n\t\t\tthis.el.style.setProperty(\"--levita-y\", `${y}deg`);\n\t\t\tthis.el.style.setProperty(\"--levita-percent-x\", String(input.x));\n\t\t\tthis.el.style.setProperty(\"--levita-percent-y\", String(input.y));\n\n\t\t\tthis.glareEffect?.update(input.x, input.y);\n\t\t\tthis.shadowEffect?.update(input.x, input.y);\n\n\t\t\tconst values: TiltValues = {\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\tpercentX: input.x,\n\t\t\t\tpercentY: input.y,\n\t\t\t};\n\t\t\tthis.emit(\"move\", values);\n\n\t\t\tthis.rafId = null;\n\t\t});\n\t};\n\n\tprivate handleEnter = (): void => {\n\t\tthis.setTransition(false);\n\t\tthis.el.style.setProperty(\"--levita-scale\", String(this.options.scale));\n\t\tthis.emit(\"enter\", undefined);\n\t};\n\n\tprivate handleLeave = (): void => {\n\t\tif (this.rafId) {\n\t\t\tcancelAnimationFrame(this.rafId);\n\t\t\tthis.rafId = null;\n\t\t}\n\t\tthis.setTransition(true);\n\t\tif (this.options.reset) {\n\t\t\tthis.resetTransform();\n\t\t}\n\t\tthis.emit(\"leave\", undefined);\n\t};\n\n\t/** Toggle the CSS transition on or off. */\n\tprivate setTransition = (on: boolean): void => {\n\t\tthis.el.style.setProperty(\"--levita-speed\", on ? `${this.options.speed}ms` : \"0ms\");\n\t};\n\n\t/** Reset the element to its neutral (non-tilted) position. */\n\tprivate resetTransform = (): void => {\n\t\tthis.el.style.setProperty(\"--levita-x\", \"0deg\");\n\t\tthis.el.style.setProperty(\"--levita-y\", \"0deg\");\n\t\tthis.el.style.setProperty(\"--levita-scale\", \"1\");\n\t\tthis.el.style.setProperty(\"--levita-percent-x\", \"0\");\n\t\tthis.el.style.setProperty(\"--levita-percent-y\", \"0\");\n\t\tthis.glareEffect?.update(0, 0);\n\t\tthis.shadowEffect?.update(0, 0);\n\t};\n\n\t/** Apply initial CSS custom properties from options. */\n\tprivate applyBaseProperties = (): void => {\n\t\tthis.el.style.setProperty(\"--levita-perspective\", `${this.options.perspective}px`);\n\t\tthis.el.style.setProperty(\"--levita-speed\", `${this.options.speed}ms`);\n\t\tthis.el.style.setProperty(\"--levita-easing\", this.options.easing);\n\t};\n\n\t/** Remove all Levita CSS custom properties from the element. */\n\tprivate removeBaseProperties = (): void => {\n\t\tthis.el.style.removeProperty(\"--levita-perspective\");\n\t\tthis.el.style.removeProperty(\"--levita-speed\");\n\t\tthis.el.style.removeProperty(\"--levita-easing\");\n\t\tthis.el.style.removeProperty(\"--levita-x\");\n\t\tthis.el.style.removeProperty(\"--levita-y\");\n\t\tthis.el.style.removeProperty(\"--levita-scale\");\n\t\tthis.el.style.removeProperty(\"--levita-percent-x\");\n\t\tthis.el.style.removeProperty(\"--levita-percent-y\");\n\t};\n}\n"],"mappings":"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"}
|
package/dist/style.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "levita-js",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Lightweight 3D tilt & parallax with accelerometer support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsdown && cp src/style.css dist/style.css",
|
|
53
|
+
"type-check": "tsc --noEmit",
|
|
53
54
|
"dev": "tsdown --watch"
|
|
54
55
|
}
|
|
55
56
|
}
|