levita-js 0.2.0 → 0.3.0

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 CHANGED
@@ -15,6 +15,8 @@ const OPTION_KEYS = [
15
15
  "maxGlare",
16
16
  "shadow",
17
17
  "gyroscope",
18
+ "gyroRange",
19
+ "gyroSmoothing",
18
20
  "disabled",
19
21
  "eventsEl"
20
22
  ];
@@ -40,6 +42,8 @@ const DEFAULT_OPTIONS = {
40
42
  maxGlare: .5,
41
43
  shadow: false,
42
44
  gyroscope: "auto",
45
+ gyroRange: 60,
46
+ gyroSmoothing: .15,
43
47
  disabled: false,
44
48
  eventsEl: null
45
49
  };
@@ -184,9 +188,12 @@ const cleanupLayers = (layers) => {
184
188
  */
185
189
  var MotionSensor = class MotionSensor {
186
190
  onMove;
191
+ onFirstEvent;
187
192
  axis;
188
193
  active = false;
189
194
  permitted = false;
195
+ receivedEvent = false;
196
+ warmup = false;
190
197
  minAngle;
191
198
  maxAngle;
192
199
  smoothing;
@@ -194,19 +201,13 @@ var MotionSensor = class MotionSensor {
194
201
  lastY = 0;
195
202
  baseBeta = null;
196
203
  baseGamma = null;
197
- /**
198
- * @param onMove - Callback receiving normalized { x, y } values
199
- * @param axis - Restrict input to a single axis, or null for both
200
- * @param minAngle - Minimum device angle mapped to -1 (default: -45)
201
- * @param maxAngle - Maximum device angle mapped to 1 (default: 45)
202
- * @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)
203
- */
204
- constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15) {
204
+ constructor({ onMove, axis, minAngle = -30, maxAngle = 30, smoothing = .15, onFirstEvent = null }) {
205
205
  this.onMove = onMove;
206
206
  this.axis = axis;
207
207
  this.minAngle = minAngle;
208
208
  this.maxAngle = maxAngle;
209
209
  this.smoothing = smoothing;
210
+ this.onFirstEvent = onFirstEvent;
210
211
  }
211
212
  /** Check if the DeviceOrientationEvent API is available in this environment. */
212
213
  static isSupported = () => {
@@ -245,6 +246,8 @@ var MotionSensor = class MotionSensor {
245
246
  start = () => {
246
247
  if (this.active || !this.permitted) return;
247
248
  this.active = true;
249
+ this.receivedEvent = false;
250
+ this.warmup = false;
248
251
  this.baseBeta = null;
249
252
  this.baseGamma = null;
250
253
  window.addEventListener("deviceorientation", this.handleOrientation);
@@ -259,6 +262,15 @@ var MotionSensor = class MotionSensor {
259
262
  setAxis = (axis) => {
260
263
  this.axis = axis;
261
264
  };
265
+ /** Update the angle range at runtime. */
266
+ setRange = (minAngle, maxAngle) => {
267
+ this.minAngle = minAngle;
268
+ this.maxAngle = maxAngle;
269
+ };
270
+ /** Update the smoothing factor at runtime. */
271
+ setSmoothing = (smoothing) => {
272
+ this.smoothing = smoothing;
273
+ };
262
274
  /**
263
275
  * Normalize device orientation angles to [-1, 1] and apply
264
276
  * exponential moving average smoothing.
@@ -268,8 +280,17 @@ var MotionSensor = class MotionSensor {
268
280
  */
269
281
  handleOrientation = (e) => {
270
282
  const evt = e;
283
+ if (evt.beta === null && evt.gamma === null) return;
284
+ if (!this.warmup) {
285
+ this.warmup = true;
286
+ return;
287
+ }
271
288
  const beta = evt.beta ?? 0;
272
289
  const gamma = evt.gamma ?? 0;
290
+ if (!this.receivedEvent) {
291
+ this.receivedEvent = true;
292
+ this.onFirstEvent?.();
293
+ }
273
294
  if (this.baseBeta === null) {
274
295
  this.baseBeta = beta;
275
296
  this.baseGamma = gamma;
@@ -406,6 +427,7 @@ var Levita = class {
406
427
  listeners = /* @__PURE__ */ new Map();
407
428
  destroyed = false;
408
429
  gyroscopeRequested = false;
430
+ gyroscopeEvent = null;
409
431
  rafId = null;
410
432
  /**
411
433
  * @param el - The DOM element to apply the tilt effect to
@@ -424,23 +446,38 @@ var Levita = class {
424
446
  if (this.options.shadow) this.shadowEffect = new ShadowEffect(this.el);
425
447
  this.pointerSensor = new PointerSensor(this.el, (values) => this.handleSensorInput(values), () => this.handleEnter(), () => this.handleLeave(), this.options.axis, this.options.eventsEl);
426
448
  if (this.options.gyroscope !== false && MotionSensor.isSupported()) {
427
- this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis);
428
- if (this.options.gyroscope === "auto") this.el.addEventListener("click", this.handleFirstTouch, { once: true });
449
+ this.motionSensor = new MotionSensor({
450
+ onMove: (values) => this.handleSensorInput(values),
451
+ axis: this.options.axis,
452
+ minAngle: -(this.options.gyroRange / 2),
453
+ maxAngle: this.options.gyroRange / 2,
454
+ smoothing: this.options.gyroSmoothing,
455
+ onFirstEvent: () => this.handleMotionReady()
456
+ });
457
+ if (this.options.gyroscope === "auto") {
458
+ this.gyroscopeEvent = MotionSensor.needsPermission() ? "click" : "pointerup";
459
+ this.el.addEventListener(this.gyroscopeEvent, this.handleFirstTouch, { once: true });
460
+ }
429
461
  }
430
462
  if (!this.options.disabled) this.enable();
431
463
  }
432
464
  /**
433
- * On first touch (iOS auto mode), request accelerometer permission
434
- * and switch from pointer to motion sensor if granted.
465
+ * On first interaction, request accelerometer permission and start the
466
+ * motion sensor. The pointer sensor keeps running until the first valid
467
+ * deviceorientation event is received (see handleMotionReady).
435
468
  */
436
469
  handleFirstTouch = async () => {
437
470
  if (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;
438
471
  this.gyroscopeRequested = true;
439
- if (await this.motionSensor.requestPermission()) {
440
- this.pointerSensor.stop();
441
- this.setTransition(false);
442
- this.motionSensor.start();
443
- }
472
+ if (await this.motionSensor.requestPermission()) this.motionSensor.start();
473
+ };
474
+ /**
475
+ * Called when the motion sensor receives its first valid event.
476
+ * At this point it's safe to hand off from pointer to motion.
477
+ */
478
+ handleMotionReady = () => {
479
+ this.pointerSensor.stop();
480
+ this.setTransition(false);
444
481
  };
445
482
  /**
446
483
  * Update options at runtime without destroying/recreating the instance.
@@ -458,6 +495,8 @@ var Levita = class {
458
495
  this.pointerSensor.setAxis(options.axis);
459
496
  this.motionSensor?.setAxis(options.axis);
460
497
  }
498
+ if (options.gyroRange !== void 0) this.motionSensor?.setRange(-(options.gyroRange / 2), options.gyroRange / 2);
499
+ if (options.gyroSmoothing !== void 0) this.motionSensor?.setSmoothing(options.gyroSmoothing);
461
500
  };
462
501
  /** Re-enable the tilt effect after a `disable()` call. */
463
502
  enable = () => {
@@ -481,11 +520,7 @@ var Levita = class {
481
520
  requestPermission = async () => {
482
521
  if (!this.motionSensor) return false;
483
522
  const granted = await this.motionSensor.requestPermission();
484
- if (granted) {
485
- this.pointerSensor.stop();
486
- this.setTransition(false);
487
- this.motionSensor.start();
488
- }
523
+ if (granted) this.motionSensor.start();
489
524
  return granted;
490
525
  };
491
526
  /**
@@ -504,7 +539,7 @@ var Levita = class {
504
539
  this.el.classList.remove("levita");
505
540
  this.removeBaseProperties();
506
541
  this.listeners.clear();
507
- this.el.removeEventListener("click", this.handleFirstTouch);
542
+ if (this.gyroscopeEvent) this.el.removeEventListener(this.gyroscopeEvent, this.handleFirstTouch);
508
543
  };
509
544
  /**
510
545
  * Register an event listener.
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ type GyroscopeMode = "auto" | boolean;
5
5
  * Options that can be updated at runtime via `update()`.
6
6
  * Limited to lightweight options that don't require DOM mutations.
7
7
  */
8
- type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset">;
8
+ type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset" | "gyroRange" | "gyroSmoothing">;
9
9
  interface LevitaOptions {
10
10
  /** Max tilt angle in degrees. Default: 15 */
11
11
  max: number;
@@ -31,6 +31,10 @@ interface LevitaOptions {
31
31
  shadow: boolean;
32
32
  /** Gyroscope mode. Default: 'auto' */
33
33
  gyroscope: GyroscopeMode;
34
+ /** Physical tilt range in degrees for gyroscope input. Default: 60 */
35
+ gyroRange: number;
36
+ /** Gyroscope smoothing factor (0-1, lower = smoother). Default: 0.15 */
37
+ gyroSmoothing: number;
34
38
  /** Disable the effect. Default: false */
35
39
  disabled: boolean;
36
40
  /** Element to listen for pointer events on. Default: the element itself */
@@ -91,6 +95,7 @@ declare class Levita {
91
95
  private listeners;
92
96
  private destroyed;
93
97
  private gyroscopeRequested;
98
+ private gyroscopeEvent;
94
99
  private rafId;
95
100
  /**
96
101
  * @param el - The DOM element to apply the tilt effect to
@@ -98,10 +103,16 @@ declare class Levita {
98
103
  */
99
104
  constructor(el: HTMLElement, options?: Partial<LevitaOptions>);
100
105
  /**
101
- * On first touch (iOS auto mode), request accelerometer permission
102
- * and switch from pointer to motion sensor if granted.
106
+ * On first interaction, request accelerometer permission and start the
107
+ * motion sensor. The pointer sensor keeps running until the first valid
108
+ * deviceorientation event is received (see handleMotionReady).
103
109
  */
104
110
  private handleFirstTouch;
111
+ /**
112
+ * Called when the motion sensor receives its first valid event.
113
+ * At this point it's safe to hand off from pointer to motion.
114
+ */
115
+ private handleMotionReady;
105
116
  /**
106
117
  * Update options at runtime without destroying/recreating the instance.
107
118
  *
@@ -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;;;;;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,KAAA;EFnBR;;;;cEyBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFjB9C;;;;EAAA,QE8DQ,gBAAA;EFtDR;;;;AAGD;;;;EEuEC,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;EFnE3B;EE+EA,MAAA;EF3EA;EEkFA,OAAA;EFlFQ;AAGT;;;;;EE4FC,iBAAA,QAA8B,OAAA;EF1F9B;;;;EEyGA,OAAA;EFrGwB;;;;;;EE6HxB,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EF/HF;;;;ACzDvC;;ECsMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDxMC;EAAA,QC8MjC,IAAA;EDzLI;;;;;;;;;EAAA,QC2MJ,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDhPqD;EAAA,QC6PrD,aAAA;ED7P0E;EAAA,QCkQ1E,cAAA;EDvPI;EAAA,QCkQJ,mBAAA;;UAOA,oBAAA;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,UAagB,aAAA;;EAEhB,GAAA;EAtBwB;EAwBxB,WAAA;EAlB2B;EAoB3B,KAAA;EApB8B;EAsB9B,KAAA;EARgB;EAUhB,MAAA;;EAEA,OAAA;EAYW;EAVX,IAAA,EAAM,IAAA;EAkBe;EAhBrB,KAAA;EAdA;EAgBA,KAAA;EAZA;EAcA,QAAA;EAVA;EAYA,MAAA;EARA;EAUA,SAAA,EAAW,aAAA;EARX;EAUA,SAAA;EANA;EAQA,aAAA;EAJA;EAMA,QAAA;EAJA;EAMA,QAAA,EAAU,WAAA;AAAA;AAAA,UAGM,UAAA;EAHN;EAKV,CAAA;EALqB;EAOrB,CAAA;EAJ0B;EAM1B,QAAA;EAN0B;EAQ1B,QAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,IAAA,EAAM,UAAA;EACN,KAAA;EACA,KAAA;AAAA;AAAA,KAGW,aAAA,OAAoB,IAAA,EAAM,CAAA;;;AAzEtC;AAAA,cCGa,WAAA,kBAA6B,aAAA;;;;ADF1C;cCyBa,YAAA,GAAgB,MAAA,EAAQ,OAAA,CAAQ,aAAA,MAAiB,OAAA,CAAQ,aAAA;AAAA,cAWzD,eAAA,EAAiB,aAAA;;;ADrC9B;;;;;AACA;;;;;AAMA;;;;;AAcA;AArBA,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;EFTR;;;;cEeY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFP9C;;;;;EAAA,QE4DQ,gBAAA;EFlDR;;;;EAAA,QEgEQ,iBAAA;EF7DkB;;;;;;;;EE0E1B,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;EF/DX;EEiFhB,MAAA;;EAOA,OAAA;EFvFA;;;;;;EEoGA,iBAAA,QAA8B,OAAA;EF/FN;;;;EE4GxB,OAAA;EF5G+B;;;;;;EEsI/B,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;ED7L/B;;;;AAMV;;ECqMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDvMI;EAAA,QC6MpC,IAAA;ED7M6D;;;;;;;;;EAAA,QC+N7D,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDxOR;EAAA,QCqPQ,aAAA;EDtQqB;EAAA,QC2QrB,cAAA;;UAWA,mBAAA;;UAOA,oBAAA;AAAA"}
package/dist/index.d.mts CHANGED
@@ -5,7 +5,7 @@ type GyroscopeMode = "auto" | boolean;
5
5
  * Options that can be updated at runtime via `update()`.
6
6
  * Limited to lightweight options that don't require DOM mutations.
7
7
  */
8
- type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset">;
8
+ type UpdatableOptions = Pick<LevitaOptions, "max" | "scale" | "speed" | "easing" | "perspective" | "reverse" | "axis" | "reset" | "gyroRange" | "gyroSmoothing">;
9
9
  interface LevitaOptions {
10
10
  /** Max tilt angle in degrees. Default: 15 */
11
11
  max: number;
@@ -31,6 +31,10 @@ interface LevitaOptions {
31
31
  shadow: boolean;
32
32
  /** Gyroscope mode. Default: 'auto' */
33
33
  gyroscope: GyroscopeMode;
34
+ /** Physical tilt range in degrees for gyroscope input. Default: 60 */
35
+ gyroRange: number;
36
+ /** Gyroscope smoothing factor (0-1, lower = smoother). Default: 0.15 */
37
+ gyroSmoothing: number;
34
38
  /** Disable the effect. Default: false */
35
39
  disabled: boolean;
36
40
  /** Element to listen for pointer events on. Default: the element itself */
@@ -91,6 +95,7 @@ declare class Levita {
91
95
  private listeners;
92
96
  private destroyed;
93
97
  private gyroscopeRequested;
98
+ private gyroscopeEvent;
94
99
  private rafId;
95
100
  /**
96
101
  * @param el - The DOM element to apply the tilt effect to
@@ -98,10 +103,16 @@ declare class Levita {
98
103
  */
99
104
  constructor(el: HTMLElement, options?: Partial<LevitaOptions>);
100
105
  /**
101
- * On first touch (iOS auto mode), request accelerometer permission
102
- * and switch from pointer to motion sensor if granted.
106
+ * On first interaction, request accelerometer permission and start the
107
+ * motion sensor. The pointer sensor keeps running until the first valid
108
+ * deviceorientation event is received (see handleMotionReady).
103
109
  */
104
110
  private handleFirstTouch;
111
+ /**
112
+ * Called when the motion sensor receives its first valid event.
113
+ * At this point it's safe to hand off from pointer to motion.
114
+ */
115
+ private handleMotionReady;
105
116
  /**
106
117
  * Update options at runtime without destroying/recreating the instance.
107
118
  *
@@ -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;;;;;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,KAAA;EFnBR;;;;cEyBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFjB9C;;;;EAAA,QE8DQ,gBAAA;EFtDR;;;;AAGD;;;;EEuEC,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;EFnE3B;EE+EA,MAAA;EF3EA;EEkFA,OAAA;EFlFQ;AAGT;;;;;EE4FC,iBAAA,QAA8B,OAAA;EF1F9B;;;;EEyGA,OAAA;EFrGwB;;;;;;EE6HxB,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EF/HF;;;;ACzDvC;;ECsMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDxMC;EAAA,QC8MjC,IAAA;EDzLI;;;;;;;;;EAAA,QC2MJ,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDhPqD;EAAA,QC6PrD,aAAA;ED7P0E;EAAA,QCkQ1E,cAAA;EDvPI;EAAA,QCkQJ,mBAAA;;UAOA,oBAAA;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,UAagB,aAAA;;EAEhB,GAAA;EAtBwB;EAwBxB,WAAA;EAlB2B;EAoB3B,KAAA;EApB8B;EAsB9B,KAAA;EARgB;EAUhB,MAAA;;EAEA,OAAA;EAYW;EAVX,IAAA,EAAM,IAAA;EAkBe;EAhBrB,KAAA;EAdA;EAgBA,KAAA;EAZA;EAcA,QAAA;EAVA;EAYA,MAAA;EARA;EAUA,SAAA,EAAW,aAAA;EARX;EAUA,SAAA;EANA;EAQA,aAAA;EAJA;EAMA,QAAA;EAJA;EAMA,QAAA,EAAU,WAAA;AAAA;AAAA,UAGM,UAAA;EAHN;EAKV,CAAA;EALqB;EAOrB,CAAA;EAJ0B;EAM1B,QAAA;EAN0B;EAQ1B,QAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,IAAA,EAAM,UAAA;EACN,KAAA;EACA,KAAA;AAAA;AAAA,KAGW,aAAA,OAAoB,IAAA,EAAM,CAAA;;;AAzEtC;AAAA,cCGa,WAAA,kBAA6B,aAAA;;;;ADF1C;cCyBa,YAAA,GAAgB,MAAA,EAAQ,OAAA,CAAQ,aAAA,MAAiB,OAAA,CAAQ,aAAA;AAAA,cAWzD,eAAA,EAAiB,aAAA;;;ADrC9B;;;;;AACA;;;;;AAMA;;;;;AAcA;AArBA,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;EFTR;;;;cEeY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFP9C;;;;;EAAA,QE4DQ,gBAAA;EFlDR;;;;EAAA,QEgEQ,iBAAA;EF7DkB;;;;;;;;EE0E1B,MAAA,GAAU,OAAA,EAAS,OAAA,CAAQ,gBAAA;EF/DX;EEiFhB,MAAA;;EAOA,OAAA;EFvFA;;;;;;EEoGA,iBAAA,QAA8B,OAAA;EF/FN;;;;EE4GxB,OAAA;EF5G+B;;;;;;EEsI/B,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;ED7L/B;;;;AAMV;;ECqMC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EDvMI;EAAA,QC6MpC,IAAA;ED7M6D;;;;;;;;;EAAA,QC+N7D,iBAAA;EAAA,QA+BA,WAAA;EAAA,QAMA,WAAA;EDxOR;EAAA,QCqPQ,aAAA;EDtQqB;EAAA,QC2QrB,cAAA;;UAWA,mBAAA;;UAOA,oBAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -13,6 +13,8 @@ const OPTION_KEYS = [
13
13
  "maxGlare",
14
14
  "shadow",
15
15
  "gyroscope",
16
+ "gyroRange",
17
+ "gyroSmoothing",
16
18
  "disabled",
17
19
  "eventsEl"
18
20
  ];
@@ -38,6 +40,8 @@ const DEFAULT_OPTIONS = {
38
40
  maxGlare: .5,
39
41
  shadow: false,
40
42
  gyroscope: "auto",
43
+ gyroRange: 60,
44
+ gyroSmoothing: .15,
41
45
  disabled: false,
42
46
  eventsEl: null
43
47
  };
@@ -182,9 +186,12 @@ const cleanupLayers = (layers) => {
182
186
  */
183
187
  var MotionSensor = class MotionSensor {
184
188
  onMove;
189
+ onFirstEvent;
185
190
  axis;
186
191
  active = false;
187
192
  permitted = false;
193
+ receivedEvent = false;
194
+ warmup = false;
188
195
  minAngle;
189
196
  maxAngle;
190
197
  smoothing;
@@ -192,19 +199,13 @@ var MotionSensor = class MotionSensor {
192
199
  lastY = 0;
193
200
  baseBeta = null;
194
201
  baseGamma = null;
195
- /**
196
- * @param onMove - Callback receiving normalized { x, y } values
197
- * @param axis - Restrict input to a single axis, or null for both
198
- * @param minAngle - Minimum device angle mapped to -1 (default: -45)
199
- * @param maxAngle - Maximum device angle mapped to 1 (default: 45)
200
- * @param smoothing - Exponential moving average factor 0-1 (default: 0.15, lower = smoother)
201
- */
202
- constructor(onMove, axis, minAngle = -45, maxAngle = 45, smoothing = .15) {
202
+ constructor({ onMove, axis, minAngle = -30, maxAngle = 30, smoothing = .15, onFirstEvent = null }) {
203
203
  this.onMove = onMove;
204
204
  this.axis = axis;
205
205
  this.minAngle = minAngle;
206
206
  this.maxAngle = maxAngle;
207
207
  this.smoothing = smoothing;
208
+ this.onFirstEvent = onFirstEvent;
208
209
  }
209
210
  /** Check if the DeviceOrientationEvent API is available in this environment. */
210
211
  static isSupported = () => {
@@ -243,6 +244,8 @@ var MotionSensor = class MotionSensor {
243
244
  start = () => {
244
245
  if (this.active || !this.permitted) return;
245
246
  this.active = true;
247
+ this.receivedEvent = false;
248
+ this.warmup = false;
246
249
  this.baseBeta = null;
247
250
  this.baseGamma = null;
248
251
  window.addEventListener("deviceorientation", this.handleOrientation);
@@ -257,6 +260,15 @@ var MotionSensor = class MotionSensor {
257
260
  setAxis = (axis) => {
258
261
  this.axis = axis;
259
262
  };
263
+ /** Update the angle range at runtime. */
264
+ setRange = (minAngle, maxAngle) => {
265
+ this.minAngle = minAngle;
266
+ this.maxAngle = maxAngle;
267
+ };
268
+ /** Update the smoothing factor at runtime. */
269
+ setSmoothing = (smoothing) => {
270
+ this.smoothing = smoothing;
271
+ };
260
272
  /**
261
273
  * Normalize device orientation angles to [-1, 1] and apply
262
274
  * exponential moving average smoothing.
@@ -266,8 +278,17 @@ var MotionSensor = class MotionSensor {
266
278
  */
267
279
  handleOrientation = (e) => {
268
280
  const evt = e;
281
+ if (evt.beta === null && evt.gamma === null) return;
282
+ if (!this.warmup) {
283
+ this.warmup = true;
284
+ return;
285
+ }
269
286
  const beta = evt.beta ?? 0;
270
287
  const gamma = evt.gamma ?? 0;
288
+ if (!this.receivedEvent) {
289
+ this.receivedEvent = true;
290
+ this.onFirstEvent?.();
291
+ }
271
292
  if (this.baseBeta === null) {
272
293
  this.baseBeta = beta;
273
294
  this.baseGamma = gamma;
@@ -404,6 +425,7 @@ var Levita = class {
404
425
  listeners = /* @__PURE__ */ new Map();
405
426
  destroyed = false;
406
427
  gyroscopeRequested = false;
428
+ gyroscopeEvent = null;
407
429
  rafId = null;
408
430
  /**
409
431
  * @param el - The DOM element to apply the tilt effect to
@@ -422,23 +444,38 @@ var Levita = class {
422
444
  if (this.options.shadow) this.shadowEffect = new ShadowEffect(this.el);
423
445
  this.pointerSensor = new PointerSensor(this.el, (values) => this.handleSensorInput(values), () => this.handleEnter(), () => this.handleLeave(), this.options.axis, this.options.eventsEl);
424
446
  if (this.options.gyroscope !== false && MotionSensor.isSupported()) {
425
- this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis);
426
- if (this.options.gyroscope === "auto") this.el.addEventListener("click", this.handleFirstTouch, { once: true });
447
+ this.motionSensor = new MotionSensor({
448
+ onMove: (values) => this.handleSensorInput(values),
449
+ axis: this.options.axis,
450
+ minAngle: -(this.options.gyroRange / 2),
451
+ maxAngle: this.options.gyroRange / 2,
452
+ smoothing: this.options.gyroSmoothing,
453
+ onFirstEvent: () => this.handleMotionReady()
454
+ });
455
+ if (this.options.gyroscope === "auto") {
456
+ this.gyroscopeEvent = MotionSensor.needsPermission() ? "click" : "pointerup";
457
+ this.el.addEventListener(this.gyroscopeEvent, this.handleFirstTouch, { once: true });
458
+ }
427
459
  }
428
460
  if (!this.options.disabled) this.enable();
429
461
  }
430
462
  /**
431
- * On first touch (iOS auto mode), request accelerometer permission
432
- * and switch from pointer to motion sensor if granted.
463
+ * On first interaction, request accelerometer permission and start the
464
+ * motion sensor. The pointer sensor keeps running until the first valid
465
+ * deviceorientation event is received (see handleMotionReady).
433
466
  */
434
467
  handleFirstTouch = async () => {
435
468
  if (this.destroyed || this.gyroscopeRequested || !this.motionSensor) return;
436
469
  this.gyroscopeRequested = true;
437
- if (await this.motionSensor.requestPermission()) {
438
- this.pointerSensor.stop();
439
- this.setTransition(false);
440
- this.motionSensor.start();
441
- }
470
+ if (await this.motionSensor.requestPermission()) this.motionSensor.start();
471
+ };
472
+ /**
473
+ * Called when the motion sensor receives its first valid event.
474
+ * At this point it's safe to hand off from pointer to motion.
475
+ */
476
+ handleMotionReady = () => {
477
+ this.pointerSensor.stop();
478
+ this.setTransition(false);
442
479
  };
443
480
  /**
444
481
  * Update options at runtime without destroying/recreating the instance.
@@ -456,6 +493,8 @@ var Levita = class {
456
493
  this.pointerSensor.setAxis(options.axis);
457
494
  this.motionSensor?.setAxis(options.axis);
458
495
  }
496
+ if (options.gyroRange !== void 0) this.motionSensor?.setRange(-(options.gyroRange / 2), options.gyroRange / 2);
497
+ if (options.gyroSmoothing !== void 0) this.motionSensor?.setSmoothing(options.gyroSmoothing);
459
498
  };
460
499
  /** Re-enable the tilt effect after a `disable()` call. */
461
500
  enable = () => {
@@ -479,11 +518,7 @@ var Levita = class {
479
518
  requestPermission = async () => {
480
519
  if (!this.motionSensor) return false;
481
520
  const granted = await this.motionSensor.requestPermission();
482
- if (granted) {
483
- this.pointerSensor.stop();
484
- this.setTransition(false);
485
- this.motionSensor.start();
486
- }
521
+ if (granted) this.motionSensor.start();
487
522
  return granted;
488
523
  };
489
524
  /**
@@ -502,7 +537,7 @@ var Levita = class {
502
537
  this.el.classList.remove("levita");
503
538
  this.removeBaseProperties();
504
539
  this.listeners.clear();
505
- this.el.removeEventListener("click", this.handleFirstTouch);
540
+ if (this.gyroscopeEvent) this.el.removeEventListener(this.gyroscopeEvent, this.handleFirstTouch);
506
541
  };
507
542
  /**
508
543
  * Register an event listener.
@@ -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\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 */\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\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\tconst beta = evt.beta ?? 0;\n\t\tconst gamma = evt.gamma ?? 0;\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 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);\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/**\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.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\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\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\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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"}
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\"gyroRange\",\n\t\"gyroSmoothing\",\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\tgyroRange: 60,\n\tgyroSmoothing: 0.15,\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\nexport interface MotionSensorOptions {\n\t/** Callback receiving normalized { x, y } values */\n\tonMove: SensorCallback;\n\t/** Restrict input to a single axis, or null for both */\n\taxis: Axis;\n\t/** Minimum device angle mapped to -1 (default: -30) */\n\tminAngle?: number;\n\t/** Maximum device angle mapped to 1 (default: 30) */\n\tmaxAngle?: number;\n\t/** Exponential moving average factor 0-1 (default: 0.15, lower = smoother) */\n\tsmoothing?: number;\n\t/** Called once when the first valid deviceorientation event is received */\n\tonFirstEvent?: (() => void) | 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\tconstructor({\n\t\tonMove,\n\t\taxis,\n\t\tminAngle = -30,\n\t\tmaxAngle = 30,\n\t\tsmoothing = 0.15,\n\t\tonFirstEvent = null,\n\t}: MotionSensorOptions) {\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/** Update the angle range at runtime. */\n\tsetRange = (minAngle: number, maxAngle: number): void => {\n\t\tthis.minAngle = minAngle;\n\t\tthis.maxAngle = maxAngle;\n\t};\n\n\t/** Update the smoothing factor at runtime. */\n\tsetSmoothing = (smoothing: number): void => {\n\t\tthis.smoothing = smoothing;\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\tonMove: (values) => this.handleSensorInput(values),\n\t\t\t\taxis: this.options.axis,\n\t\t\t\tminAngle: -(this.options.gyroRange / 2),\n\t\t\t\tmaxAngle: this.options.gyroRange / 2,\n\t\t\t\tsmoothing: this.options.gyroSmoothing,\n\t\t\t\tonFirstEvent: () => 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\tif (options.gyroRange !== undefined) {\n\t\t\tthis.motionSensor?.setRange(-(options.gyroRange / 2), options.gyroRange / 2);\n\t\t}\n\t\tif (options.gyroSmoothing !== undefined) {\n\t\t\tthis.motionSensor?.setSmoothing(options.gyroSmoothing);\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levita-js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight 3D tilt & parallax with accelerometer support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",