levita-js 0.1.5 → 0.2.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
@@ -192,6 +192,8 @@ var MotionSensor = class MotionSensor {
192
192
  smoothing;
193
193
  lastX = 0;
194
194
  lastY = 0;
195
+ baseBeta = null;
196
+ baseGamma = null;
195
197
  /**
196
198
  * @param onMove - Callback receiving normalized { x, y } values
197
199
  * @param axis - Restrict input to a single axis, or null for both
@@ -243,6 +245,8 @@ var MotionSensor = class MotionSensor {
243
245
  start = () => {
244
246
  if (this.active || !this.permitted) return;
245
247
  this.active = true;
248
+ this.baseBeta = null;
249
+ this.baseGamma = null;
246
250
  window.addEventListener("deviceorientation", this.handleOrientation);
247
251
  };
248
252
  /** Stop listening and remove the deviceorientation event listener. */
@@ -266,9 +270,15 @@ var MotionSensor = class MotionSensor {
266
270
  const evt = e;
267
271
  const beta = evt.beta ?? 0;
268
272
  const gamma = evt.gamma ?? 0;
273
+ if (this.baseBeta === null) {
274
+ this.baseBeta = beta;
275
+ this.baseGamma = gamma;
276
+ }
277
+ const relativeBeta = beta - this.baseBeta;
278
+ const relativeGamma = gamma - this.baseGamma;
269
279
  const range = this.maxAngle - this.minAngle;
270
- const rawX = this.axis === "y" ? 0 : this.clamp((gamma - this.minAngle) / range * 2 - 1);
271
- const rawY = this.axis === "x" ? 0 : this.clamp((beta - this.minAngle) / range * 2 - 1);
280
+ const rawX = this.axis === "y" ? 0 : this.clamp(relativeGamma / (range / 2));
281
+ const rawY = this.axis === "x" ? 0 : this.clamp(relativeBeta / (range / 2));
272
282
  this.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;
273
283
  this.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;
274
284
  this.onMove({
@@ -298,7 +308,6 @@ var PointerSensor = class {
298
308
  axis;
299
309
  active = false;
300
310
  rect = null;
301
- isPointerDown = false;
302
311
  constructor(el, onMove, onEnter, onLeave, axis, eventsEl = null) {
303
312
  this.eventsEl = eventsEl ?? el;
304
313
  this.onMove = onMove;
@@ -333,14 +342,10 @@ var PointerSensor = class {
333
342
  this.axis = axis;
334
343
  };
335
344
  handlePointerDown = (e) => {
336
- this.isPointerDown = true;
337
- this.eventsEl.setPointerCapture(e.pointerId);
338
345
  this.handlePointerEnter();
339
346
  this.handlePointerMove(e);
340
347
  };
341
- handlePointerUp = (e) => {
342
- this.isPointerDown = false;
343
- this.eventsEl.releasePointerCapture(e.pointerId);
348
+ handlePointerUp = () => {
344
349
  this.handlePointerLeave();
345
350
  };
346
351
  /**
@@ -349,13 +354,10 @@ var PointerSensor = class {
349
354
  */
350
355
  handlePointerMove = (e) => {
351
356
  if (!this.rect) this.rect = this.eventsEl.getBoundingClientRect();
352
- if (!this.rect) return;
353
- const rawX = (e.clientX - this.rect.left) / this.rect.width;
354
- const rawY = (e.clientY - this.rect.top) / this.rect.height;
355
- if ((rawX < 0 || rawX > 1 || rawY < 0 || rawY > 1) && !this.isPointerDown) {
356
- this.handlePointerLeave();
357
- return;
358
- }
357
+ const width = this.eventsEl.offsetWidth || this.rect.width;
358
+ const height = this.eventsEl.offsetHeight || this.rect.height;
359
+ const rawX = (e.clientX - this.rect.left) / width;
360
+ const rawY = (e.clientY - this.rect.top) / height;
359
361
  const clampedX = Math.max(0, Math.min(1, rawX));
360
362
  const clampedY = Math.max(0, Math.min(1, rawY));
361
363
  const x = this.axis === "y" ? 0 : clampedX * 2 - 1;
@@ -404,6 +406,7 @@ var Levita = class {
404
406
  listeners = /* @__PURE__ */ new Map();
405
407
  destroyed = false;
406
408
  gyroscopeRequested = false;
409
+ rafId = null;
407
410
  /**
408
411
  * @param el - The DOM element to apply the tilt effect to
409
412
  * @param options - Configuration options (all optional, sensible defaults)
@@ -439,6 +442,23 @@ var Levita = class {
439
442
  this.motionSensor.start();
440
443
  }
441
444
  };
445
+ /**
446
+ * Update options at runtime without destroying/recreating the instance.
447
+ *
448
+ * Only "lightweight" options are supported — those that don't require
449
+ * DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
450
+ * `disabled`, and `maxGlare` require a full destroy/recreate cycle
451
+ * (or use `enable()`/`disable()` for toggling the effect).
452
+ */
453
+ update = (options) => {
454
+ if (this.destroyed) return;
455
+ Object.assign(this.options, options);
456
+ this.applyBaseProperties();
457
+ if (options.axis !== void 0) {
458
+ this.pointerSensor.setAxis(options.axis);
459
+ this.motionSensor?.setAxis(options.axis);
460
+ }
461
+ };
442
462
  /** Re-enable the tilt effect after a `disable()` call. */
443
463
  enable = () => {
444
464
  if (this.destroyed) return;
@@ -475,6 +495,7 @@ var Levita = class {
475
495
  destroy = () => {
476
496
  if (this.destroyed) return;
477
497
  this.destroyed = true;
498
+ if (this.rafId) cancelAnimationFrame(this.rafId);
478
499
  this.pointerSensor.stop();
479
500
  this.motionSensor?.stop();
480
501
  this.glareEffect?.destroy();
@@ -512,26 +533,34 @@ var Levita = class {
512
533
  /**
513
534
  * Process normalized sensor input and update CSS custom properties.
514
535
  * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
536
+ *
537
+ * Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
538
+ * can fire hundreds of events per second, which would saturate the main thread
539
+ * with CSS variable updates and style recalculations. This ensures we only
540
+ * update the DOM once per browser frame.
515
541
  */
516
542
  handleSensorInput = (input) => {
517
543
  if (this.options.disabled) return;
518
544
  const multiplier = this.options.reverse ? -1 : 1;
519
545
  const x = input.y * this.options.max * multiplier;
520
546
  const y = input.x * this.options.max * multiplier * -1;
521
- this.el.style.setProperty("--levita-x", `${x}deg`);
522
- this.el.style.setProperty("--levita-y", `${y}deg`);
523
- this.el.style.setProperty("--levita-scale", String(this.options.scale));
524
- this.el.style.setProperty("--levita-percent-x", String(input.x));
525
- this.el.style.setProperty("--levita-percent-y", String(input.y));
526
- this.glareEffect?.update(input.x, input.y);
527
- this.shadowEffect?.update(input.x, input.y);
528
- const values = {
529
- x,
530
- y,
531
- percentX: input.x,
532
- percentY: input.y
533
- };
534
- this.emit("move", values);
547
+ if (this.rafId) cancelAnimationFrame(this.rafId);
548
+ this.rafId = requestAnimationFrame(() => {
549
+ this.el.style.setProperty("--levita-x", `${x}deg`);
550
+ this.el.style.setProperty("--levita-y", `${y}deg`);
551
+ this.el.style.setProperty("--levita-percent-x", String(input.x));
552
+ this.el.style.setProperty("--levita-percent-y", String(input.y));
553
+ this.glareEffect?.update(input.x, input.y);
554
+ this.shadowEffect?.update(input.x, input.y);
555
+ const values = {
556
+ x,
557
+ y,
558
+ percentX: input.x,
559
+ percentY: input.y
560
+ };
561
+ this.emit("move", values);
562
+ this.rafId = null;
563
+ });
535
564
  };
536
565
  handleEnter = () => {
537
566
  this.setTransition(false);
@@ -539,6 +568,10 @@ var Levita = class {
539
568
  this.emit("enter", void 0);
540
569
  };
541
570
  handleLeave = () => {
571
+ if (this.rafId) {
572
+ cancelAnimationFrame(this.rafId);
573
+ this.rafId = null;
574
+ }
542
575
  this.setTransition(true);
543
576
  if (this.options.reset) this.resetTransform();
544
577
  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,6 +91,7 @@ declare class Levita {
86
91
  private listeners;
87
92
  private destroyed;
88
93
  private gyroscopeRequested;
94
+ private rafId;
89
95
  /**
90
96
  * @param el - The DOM element to apply the tilt effect to
91
97
  * @param options - Configuration options (all optional, sensible defaults)
@@ -96,6 +102,15 @@ declare class Levita {
96
102
  * and switch from pointer to motion sensor if granted.
97
103
  */
98
104
  private handleFirstTouch;
105
+ /**
106
+ * Update options at runtime without destroying/recreating the instance.
107
+ *
108
+ * Only "lightweight" options are supported — those that don't require
109
+ * DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
110
+ * `disabled`, and `maxGlare` require a full destroy/recreate cycle
111
+ * (or use `enable()`/`disable()` for toggling the effect).
112
+ */
113
+ update: (options: Partial<UpdatableOptions>) => void;
99
114
  /** Re-enable the tilt effect after a `disable()` call. */
100
115
  enable: () => void;
101
116
  /** Pause the tilt effect and reset the element to its neutral position. */
@@ -131,6 +146,11 @@ declare class Levita {
131
146
  /**
132
147
  * Process normalized sensor input and update CSS custom properties.
133
148
  * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
149
+ *
150
+ * Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
151
+ * can fire hundreds of events per second, which would saturate the main thread
152
+ * with CSS variable updates and style recalculations. This ensures we only
153
+ * update the DOM once per browser frame.
134
154
  */
135
155
  private handleSensorInput;
136
156
  private handleEnter;
@@ -145,5 +165,5 @@ declare class Levita {
145
165
  private removeBaseProperties;
146
166
  }
147
167
  //#endregion
148
- export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, buildOptions };
168
+ export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, type UpdatableOptions, buildOptions };
149
169
  //# sourceMappingURL=index.d.cts.map
@@ -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,UAEK,aAAA;;EAEhB,GAAA;EALe;EAOf,WAAA;EANwB;EAQxB,KAAA;EARwB;EAUxB,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;;;AAnDtC;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;;;;;AAEA;;;;;;AAHA,cE0Ba,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;EFfR;;;;cEqBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFb9C;;;;EAAA,QE0DQ,gBAAA;EFrDQ;EEkEhB,MAAA;;EAOA,OAAA;EFvEA;;;;;;EEoFA,iBAAA,QAA8B,OAAA;EF3EA;;;;EE0F9B,OAAA;EFxFA;;;;AAID;;EE2GC,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EF7GF;;;;;;EE2HtC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;;UAMhC,IAAA;EDnLI;;;;EAAA,QCgMJ,iBAAA;EAAA,QAyBA,WAAA;EAAA,QAMA,WAAA;;UASA,aAAA;EDnN4B;EAAA,QCwN5B,cAAA;EDxNqD;EAAA,QCmOrD,mBAAA;EDnO4D;EAAA,QC0O5D,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,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"}
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,6 +91,7 @@ declare class Levita {
86
91
  private listeners;
87
92
  private destroyed;
88
93
  private gyroscopeRequested;
94
+ private rafId;
89
95
  /**
90
96
  * @param el - The DOM element to apply the tilt effect to
91
97
  * @param options - Configuration options (all optional, sensible defaults)
@@ -96,6 +102,15 @@ declare class Levita {
96
102
  * and switch from pointer to motion sensor if granted.
97
103
  */
98
104
  private handleFirstTouch;
105
+ /**
106
+ * Update options at runtime without destroying/recreating the instance.
107
+ *
108
+ * Only "lightweight" options are supported — those that don't require
109
+ * DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
110
+ * `disabled`, and `maxGlare` require a full destroy/recreate cycle
111
+ * (or use `enable()`/`disable()` for toggling the effect).
112
+ */
113
+ update: (options: Partial<UpdatableOptions>) => void;
99
114
  /** Re-enable the tilt effect after a `disable()` call. */
100
115
  enable: () => void;
101
116
  /** Pause the tilt effect and reset the element to its neutral position. */
@@ -131,6 +146,11 @@ declare class Levita {
131
146
  /**
132
147
  * Process normalized sensor input and update CSS custom properties.
133
148
  * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
149
+ *
150
+ * Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
151
+ * can fire hundreds of events per second, which would saturate the main thread
152
+ * with CSS variable updates and style recalculations. This ensures we only
153
+ * update the DOM once per browser frame.
134
154
  */
135
155
  private handleSensorInput;
136
156
  private handleEnter;
@@ -145,5 +165,5 @@ declare class Levita {
145
165
  private removeBaseProperties;
146
166
  }
147
167
  //#endregion
148
- export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, buildOptions };
168
+ export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, type UpdatableOptions, buildOptions };
149
169
  //# sourceMappingURL=index.d.mts.map
@@ -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,UAEK,aAAA;;EAEhB,GAAA;EALe;EAOf,WAAA;EANwB;EAQxB,KAAA;EARwB;EAUxB,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;;;AAnDtC;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;;;;;AAEA;;;;;;AAHA,cE0Ba,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;EFfR;;;;cEqBY,EAAA,EAAI,WAAA,EAAa,OAAA,GAAS,OAAA,CAAQ,aAAA;EFb9C;;;;EAAA,QE0DQ,gBAAA;EFrDQ;EEkEhB,MAAA;;EAOA,OAAA;EFvEA;;;;;;EEoFA,iBAAA,QAA8B,OAAA;EF3EA;;;;EE0F9B,OAAA;EFxFA;;;;AAID;;EE2GC,EAAA,mBAAsB,cAAA,EACrB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;EF7GF;;;;;;EE2HtC,GAAA,mBAAuB,cAAA,EACtB,KAAA,EAAO,CAAA,EACP,QAAA,EAAU,aAAA,CAAc,cAAA,CAAe,CAAA;;UAMhC,IAAA;EDnLI;;;;EAAA,QCgMJ,iBAAA;EAAA,QAyBA,WAAA;EAAA,QAMA,WAAA;;UASA,aAAA;EDnN4B;EAAA,QCwN5B,cAAA;EDxNqD;EAAA,QCmOrD,mBAAA;EDnO4D;EAAA,QC0O5D,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,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"}
package/dist/index.mjs CHANGED
@@ -190,6 +190,8 @@ var MotionSensor = class MotionSensor {
190
190
  smoothing;
191
191
  lastX = 0;
192
192
  lastY = 0;
193
+ baseBeta = null;
194
+ baseGamma = null;
193
195
  /**
194
196
  * @param onMove - Callback receiving normalized { x, y } values
195
197
  * @param axis - Restrict input to a single axis, or null for both
@@ -241,6 +243,8 @@ var MotionSensor = class MotionSensor {
241
243
  start = () => {
242
244
  if (this.active || !this.permitted) return;
243
245
  this.active = true;
246
+ this.baseBeta = null;
247
+ this.baseGamma = null;
244
248
  window.addEventListener("deviceorientation", this.handleOrientation);
245
249
  };
246
250
  /** Stop listening and remove the deviceorientation event listener. */
@@ -264,9 +268,15 @@ var MotionSensor = class MotionSensor {
264
268
  const evt = e;
265
269
  const beta = evt.beta ?? 0;
266
270
  const gamma = evt.gamma ?? 0;
271
+ if (this.baseBeta === null) {
272
+ this.baseBeta = beta;
273
+ this.baseGamma = gamma;
274
+ }
275
+ const relativeBeta = beta - this.baseBeta;
276
+ const relativeGamma = gamma - this.baseGamma;
267
277
  const range = this.maxAngle - this.minAngle;
268
- const rawX = this.axis === "y" ? 0 : this.clamp((gamma - this.minAngle) / range * 2 - 1);
269
- const rawY = this.axis === "x" ? 0 : this.clamp((beta - this.minAngle) / range * 2 - 1);
278
+ const rawX = this.axis === "y" ? 0 : this.clamp(relativeGamma / (range / 2));
279
+ const rawY = this.axis === "x" ? 0 : this.clamp(relativeBeta / (range / 2));
270
280
  this.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;
271
281
  this.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;
272
282
  this.onMove({
@@ -296,7 +306,6 @@ var PointerSensor = class {
296
306
  axis;
297
307
  active = false;
298
308
  rect = null;
299
- isPointerDown = false;
300
309
  constructor(el, onMove, onEnter, onLeave, axis, eventsEl = null) {
301
310
  this.eventsEl = eventsEl ?? el;
302
311
  this.onMove = onMove;
@@ -331,14 +340,10 @@ var PointerSensor = class {
331
340
  this.axis = axis;
332
341
  };
333
342
  handlePointerDown = (e) => {
334
- this.isPointerDown = true;
335
- this.eventsEl.setPointerCapture(e.pointerId);
336
343
  this.handlePointerEnter();
337
344
  this.handlePointerMove(e);
338
345
  };
339
- handlePointerUp = (e) => {
340
- this.isPointerDown = false;
341
- this.eventsEl.releasePointerCapture(e.pointerId);
346
+ handlePointerUp = () => {
342
347
  this.handlePointerLeave();
343
348
  };
344
349
  /**
@@ -347,13 +352,10 @@ var PointerSensor = class {
347
352
  */
348
353
  handlePointerMove = (e) => {
349
354
  if (!this.rect) this.rect = this.eventsEl.getBoundingClientRect();
350
- if (!this.rect) return;
351
- const rawX = (e.clientX - this.rect.left) / this.rect.width;
352
- const rawY = (e.clientY - this.rect.top) / this.rect.height;
353
- if ((rawX < 0 || rawX > 1 || rawY < 0 || rawY > 1) && !this.isPointerDown) {
354
- this.handlePointerLeave();
355
- return;
356
- }
355
+ const width = this.eventsEl.offsetWidth || this.rect.width;
356
+ const height = this.eventsEl.offsetHeight || this.rect.height;
357
+ const rawX = (e.clientX - this.rect.left) / width;
358
+ const rawY = (e.clientY - this.rect.top) / height;
357
359
  const clampedX = Math.max(0, Math.min(1, rawX));
358
360
  const clampedY = Math.max(0, Math.min(1, rawY));
359
361
  const x = this.axis === "y" ? 0 : clampedX * 2 - 1;
@@ -402,6 +404,7 @@ var Levita = class {
402
404
  listeners = /* @__PURE__ */ new Map();
403
405
  destroyed = false;
404
406
  gyroscopeRequested = false;
407
+ rafId = null;
405
408
  /**
406
409
  * @param el - The DOM element to apply the tilt effect to
407
410
  * @param options - Configuration options (all optional, sensible defaults)
@@ -437,6 +440,23 @@ var Levita = class {
437
440
  this.motionSensor.start();
438
441
  }
439
442
  };
443
+ /**
444
+ * Update options at runtime without destroying/recreating the instance.
445
+ *
446
+ * Only "lightweight" options are supported — those that don't require
447
+ * DOM mutations. Options like `glare`, `shadow`, `gyroscope`, `eventsEl`,
448
+ * `disabled`, and `maxGlare` require a full destroy/recreate cycle
449
+ * (or use `enable()`/`disable()` for toggling the effect).
450
+ */
451
+ update = (options) => {
452
+ if (this.destroyed) return;
453
+ Object.assign(this.options, options);
454
+ this.applyBaseProperties();
455
+ if (options.axis !== void 0) {
456
+ this.pointerSensor.setAxis(options.axis);
457
+ this.motionSensor?.setAxis(options.axis);
458
+ }
459
+ };
440
460
  /** Re-enable the tilt effect after a `disable()` call. */
441
461
  enable = () => {
442
462
  if (this.destroyed) return;
@@ -473,6 +493,7 @@ var Levita = class {
473
493
  destroy = () => {
474
494
  if (this.destroyed) return;
475
495
  this.destroyed = true;
496
+ if (this.rafId) cancelAnimationFrame(this.rafId);
476
497
  this.pointerSensor.stop();
477
498
  this.motionSensor?.stop();
478
499
  this.glareEffect?.destroy();
@@ -510,26 +531,34 @@ var Levita = class {
510
531
  /**
511
532
  * Process normalized sensor input and update CSS custom properties.
512
533
  * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
534
+ *
535
+ * Note: We use requestAnimationFrame to throttle updates. High-polling rate mice
536
+ * can fire hundreds of events per second, which would saturate the main thread
537
+ * with CSS variable updates and style recalculations. This ensures we only
538
+ * update the DOM once per browser frame.
513
539
  */
514
540
  handleSensorInput = (input) => {
515
541
  if (this.options.disabled) return;
516
542
  const multiplier = this.options.reverse ? -1 : 1;
517
543
  const x = input.y * this.options.max * multiplier;
518
544
  const y = input.x * this.options.max * multiplier * -1;
519
- this.el.style.setProperty("--levita-x", `${x}deg`);
520
- this.el.style.setProperty("--levita-y", `${y}deg`);
521
- this.el.style.setProperty("--levita-scale", String(this.options.scale));
522
- this.el.style.setProperty("--levita-percent-x", String(input.x));
523
- this.el.style.setProperty("--levita-percent-y", String(input.y));
524
- this.glareEffect?.update(input.x, input.y);
525
- this.shadowEffect?.update(input.x, input.y);
526
- const values = {
527
- x,
528
- y,
529
- percentX: input.x,
530
- percentY: input.y
531
- };
532
- this.emit("move", values);
545
+ if (this.rafId) cancelAnimationFrame(this.rafId);
546
+ this.rafId = requestAnimationFrame(() => {
547
+ this.el.style.setProperty("--levita-x", `${x}deg`);
548
+ this.el.style.setProperty("--levita-y", `${y}deg`);
549
+ this.el.style.setProperty("--levita-percent-x", String(input.x));
550
+ this.el.style.setProperty("--levita-percent-y", String(input.y));
551
+ this.glareEffect?.update(input.x, input.y);
552
+ this.shadowEffect?.update(input.x, input.y);
553
+ const values = {
554
+ x,
555
+ y,
556
+ percentX: input.x,
557
+ percentY: input.y
558
+ };
559
+ this.emit("move", values);
560
+ this.rafId = null;
561
+ });
533
562
  };
534
563
  handleEnter = () => {
535
564
  this.setTransition(false);
@@ -537,6 +566,10 @@ var Levita = class {
537
566
  this.emit("enter", void 0);
538
567
  };
539
568
  handleLeave = () => {
569
+ if (this.rafId) {
570
+ cancelAnimationFrame(this.rafId);
571
+ this.rafId = null;
572
+ }
540
573
  this.setTransition(true);
541
574
  if (this.options.reset) this.resetTransform();
542
575
  this.emit("leave", void 0);
@@ -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":"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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\"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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"}
package/dist/style.css CHANGED
@@ -11,7 +11,7 @@
11
11
  transform-style: preserve-3d;
12
12
  transition: transform var(--levita-speed) var(--levita-easing);
13
13
  will-change: transform;
14
- touch-action: none;
14
+ touch-action: pinch-zoom;
15
15
  }
16
16
 
17
17
  /* Glare overlay */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levita-js",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
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
  }