levita-js 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,558 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/constants.ts
4
+ /** All keys of `LevitaOptions`, derived from `DEFAULT_OPTIONS`. */
5
+ const OPTION_KEYS = [
6
+ "max",
7
+ "perspective",
8
+ "scale",
9
+ "speed",
10
+ "easing",
11
+ "reverse",
12
+ "axis",
13
+ "reset",
14
+ "glare",
15
+ "maxGlare",
16
+ "shadow",
17
+ "gyroscope",
18
+ "disabled",
19
+ "eventsEl"
20
+ ];
21
+ /**
22
+ * Build a partial `LevitaOptions` object from a source,
23
+ * including only keys that are explicitly defined.
24
+ */
25
+ const buildOptions = (source) => {
26
+ const options = {};
27
+ for (const key of OPTION_KEYS) if (source[key] !== void 0) options[key] = source[key];
28
+ return options;
29
+ };
30
+ const DEFAULT_OPTIONS = {
31
+ max: 15,
32
+ perspective: 1e3,
33
+ scale: 1.05,
34
+ speed: 200,
35
+ easing: "ease-out",
36
+ reverse: false,
37
+ axis: null,
38
+ reset: true,
39
+ glare: false,
40
+ maxGlare: .5,
41
+ shadow: false,
42
+ gyroscope: "auto",
43
+ disabled: false,
44
+ eventsEl: null
45
+ };
46
+
47
+ //#endregion
48
+ //#region src/effects/glare.ts
49
+ /**
50
+ * Creates a radial gradient overlay that follows the tilt position,
51
+ * simulating light reflection on the surface.
52
+ *
53
+ * Injects two DOM elements (.levita-glare > .levita-glare-inner) into
54
+ * the target element. Position and opacity are driven by CSS custom
55
+ * properties — no JS runs per animation frame.
56
+ */
57
+ var GlareEffect = class {
58
+ container;
59
+ inner;
60
+ maxOpacity;
61
+ /**
62
+ * @param el - The element to attach the glare overlay to
63
+ * @param maxOpacity - Maximum glare opacity (0-1)
64
+ */
65
+ constructor(el, maxOpacity) {
66
+ this.maxOpacity = maxOpacity;
67
+ this.container = document.createElement("div");
68
+ this.container.classList.add("levita-glare");
69
+ this.inner = document.createElement("div");
70
+ this.inner.classList.add("levita-glare-inner");
71
+ this.container.appendChild(this.inner);
72
+ el.appendChild(this.container);
73
+ if (!el.style.position || el.style.position === "static") el.style.position = "relative";
74
+ }
75
+ /**
76
+ * Update glare position and intensity based on normalized tilt values.
77
+ * Sets CSS custom properties that the stylesheet uses for rendering.
78
+ *
79
+ * @param normalizedX - Horizontal position [-1, 1]
80
+ * @param normalizedY - Vertical position [-1, 1]
81
+ */
82
+ update = (normalizedX, normalizedY) => {
83
+ const glareX = (normalizedX + 1) / 2 * 100;
84
+ const glareY = (normalizedY + 1) / 2 * 100;
85
+ const intensity = Math.sqrt(normalizedX ** 2 + normalizedY ** 2) / Math.SQRT2;
86
+ this.inner.style.setProperty("--levita-glare-x", `${glareX}%`);
87
+ this.inner.style.setProperty("--levita-glare-y", `${glareY}%`);
88
+ this.inner.style.setProperty("--levita-glare-opacity", `${intensity * this.maxOpacity}`);
89
+ };
90
+ /** Remove the glare DOM elements from the parent. */
91
+ destroy = () => {
92
+ this.container.remove();
93
+ };
94
+ };
95
+
96
+ //#endregion
97
+ //#region src/effects/shadow.ts
98
+ /**
99
+ * Adds a dynamic drop shadow that shifts based on the tilt angle,
100
+ * reinforcing the 3D depth illusion.
101
+ *
102
+ * Uses CSS custom properties (--levita-shadow-x, --levita-shadow-y)
103
+ * combined with `filter: drop-shadow()` — no JS runs per animation frame.
104
+ */
105
+ var ShadowEffect = class {
106
+ el;
107
+ maxOffset;
108
+ /**
109
+ * @param el - The element to apply the shadow to
110
+ * @param maxOffset - Maximum shadow offset in pixels (default: 20)
111
+ */
112
+ constructor(el, maxOffset = 20) {
113
+ this.el = el;
114
+ this.maxOffset = maxOffset;
115
+ this.el.classList.add("levita-shadow");
116
+ }
117
+ /**
118
+ * Update shadow offset based on normalized tilt values.
119
+ *
120
+ * @param normalizedX - Horizontal position [-1, 1]
121
+ * @param normalizedY - Vertical position [-1, 1]
122
+ */
123
+ update = (normalizedX, normalizedY) => {
124
+ const shadowX = normalizedX * this.maxOffset;
125
+ const shadowY = normalizedY * this.maxOffset;
126
+ this.el.style.setProperty("--levita-shadow-x", `${shadowX}px`);
127
+ this.el.style.setProperty("--levita-shadow-y", `${shadowY}px`);
128
+ };
129
+ /** Remove the shadow class and clean up CSS custom properties. */
130
+ destroy = () => {
131
+ this.el.classList.remove("levita-shadow");
132
+ this.el.style.removeProperty("--levita-shadow-x");
133
+ this.el.style.removeProperty("--levita-shadow-y");
134
+ };
135
+ };
136
+
137
+ //#endregion
138
+ //#region src/layers.ts
139
+ /**
140
+ * Scan a container for children with `data-levita-offset` attributes
141
+ * and set the `--levita-offset` CSS custom property on each one.
142
+ *
143
+ * The CSS stylesheet uses this variable with `translateZ()` to position
144
+ * layers at different depths — no JS runs per animation frame.
145
+ *
146
+ * @param container - The parent element to scan for layer children
147
+ * @returns Array of discovered layers with their elements and offsets
148
+ */
149
+ const scanLayers = (container) => {
150
+ const elements = container.querySelectorAll("[data-levita-offset]");
151
+ const layers = [];
152
+ for (const el of elements) {
153
+ const raw = el.dataset.levitaOffset;
154
+ const offset = Number.parseFloat(raw ?? "0");
155
+ if (!Number.isNaN(offset)) {
156
+ el.style.setProperty("--levita-offset", String(offset));
157
+ layers.push({
158
+ el,
159
+ offset
160
+ });
161
+ }
162
+ }
163
+ return layers;
164
+ };
165
+ /**
166
+ * Remove the `--levita-offset` CSS custom property from all layer elements.
167
+ *
168
+ * @param layers - The layers to clean up
169
+ */
170
+ const cleanupLayers = (layers) => {
171
+ for (const layer of layers) layer.el.style.removeProperty("--levita-offset");
172
+ };
173
+
174
+ //#endregion
175
+ //#region src/sensors/motion.ts
176
+ /**
177
+ * Reads device orientation (accelerometer/gyroscope) and normalizes
178
+ * the tilt angles to a [-1, 1] range.
179
+ *
180
+ * Handles iOS 13+ permission flow via async `requestPermission()`.
181
+ * On Android, permission is granted automatically.
182
+ *
183
+ * Uses exponential moving average for smoothing raw sensor data.
184
+ */
185
+ var MotionSensor = class MotionSensor {
186
+ onMove;
187
+ axis;
188
+ active = false;
189
+ permitted = false;
190
+ minAngle;
191
+ maxAngle;
192
+ smoothing;
193
+ lastX = 0;
194
+ lastY = 0;
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) {
203
+ this.onMove = onMove;
204
+ this.axis = axis;
205
+ this.minAngle = minAngle;
206
+ this.maxAngle = maxAngle;
207
+ this.smoothing = smoothing;
208
+ }
209
+ /** Check if the DeviceOrientationEvent API is available in this environment. */
210
+ static isSupported = () => {
211
+ return typeof window !== "undefined" && "DeviceOrientationEvent" in window;
212
+ };
213
+ /**
214
+ * Check if explicit permission is required (iOS 13+).
215
+ * On Android and desktop, this returns false.
216
+ */
217
+ static needsPermission = () => {
218
+ return typeof DeviceOrientationEvent !== "undefined" && "requestPermission" in DeviceOrientationEvent;
219
+ };
220
+ /**
221
+ * Request permission to access device orientation.
222
+ * On Android, resolves immediately to true.
223
+ * On iOS 13+, triggers the native permission dialog.
224
+ * Must be called from a user gesture on iOS.
225
+ *
226
+ * @returns Whether permission was granted
227
+ */
228
+ requestPermission = async () => {
229
+ if (!MotionSensor.isSupported()) return false;
230
+ if (!MotionSensor.needsPermission()) {
231
+ this.permitted = true;
232
+ return true;
233
+ }
234
+ try {
235
+ this.permitted = await DeviceOrientationEvent.requestPermission() === "granted";
236
+ return this.permitted;
237
+ } catch {
238
+ this.permitted = false;
239
+ return false;
240
+ }
241
+ };
242
+ /** Start listening to deviceorientation events. Requires prior permission. */
243
+ start = () => {
244
+ if (this.active || !this.permitted) return;
245
+ this.active = true;
246
+ window.addEventListener("deviceorientation", this.handleOrientation);
247
+ };
248
+ /** Stop listening and remove the deviceorientation event listener. */
249
+ stop = () => {
250
+ if (!this.active) return;
251
+ this.active = false;
252
+ window.removeEventListener("deviceorientation", this.handleOrientation);
253
+ };
254
+ /** Update the axis lock at runtime. */
255
+ setAxis = (axis) => {
256
+ this.axis = axis;
257
+ };
258
+ /**
259
+ * Normalize device orientation angles to [-1, 1] and apply
260
+ * exponential moving average smoothing.
261
+ *
262
+ * beta = X-axis rotation [-180, 180] (front-back tilt)
263
+ * gamma = Y-axis rotation [-90, 90] (left-right tilt)
264
+ */
265
+ handleOrientation = (e) => {
266
+ const evt = e;
267
+ const beta = evt.beta ?? 0;
268
+ const gamma = evt.gamma ?? 0;
269
+ 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);
272
+ this.lastX = this.lastX + (rawX - this.lastX) * this.smoothing;
273
+ this.lastY = this.lastY + (rawY - this.lastY) * this.smoothing;
274
+ this.onMove({
275
+ x: this.lastX,
276
+ y: this.lastY
277
+ });
278
+ };
279
+ /** Clamp a value to the [-1, 1] range. */
280
+ clamp = (value) => {
281
+ return Math.max(-1, Math.min(1, value));
282
+ };
283
+ };
284
+
285
+ //#endregion
286
+ //#region src/sensors/pointer.ts
287
+ /**
288
+ * Tracks pointer (mouse/touch) position over an element and normalizes
289
+ * it to a [-1, 1] range relative to the element's bounds.
290
+ *
291
+ * Uses PointerEvent for unified mouse + touch input.
292
+ */
293
+ var PointerSensor = class {
294
+ eventsEl;
295
+ onMove;
296
+ onEnter;
297
+ onLeave;
298
+ axis;
299
+ active = false;
300
+ rect = null;
301
+ constructor(el, onMove, onEnter, onLeave, axis, eventsEl = null) {
302
+ this.eventsEl = eventsEl ?? el;
303
+ this.onMove = onMove;
304
+ this.onEnter = onEnter;
305
+ this.onLeave = onLeave;
306
+ this.axis = axis;
307
+ }
308
+ /** Start listening to pointer events. */
309
+ start = () => {
310
+ if (this.active) return;
311
+ this.active = true;
312
+ this.eventsEl.addEventListener("pointermove", this.handlePointerMove);
313
+ this.eventsEl.addEventListener("pointerenter", this.handlePointerEnter);
314
+ this.eventsEl.addEventListener("pointerleave", this.handlePointerLeave);
315
+ };
316
+ /** Stop listening and remove all pointer event listeners. */
317
+ stop = () => {
318
+ if (!this.active) return;
319
+ this.active = false;
320
+ this.eventsEl.removeEventListener("pointermove", this.handlePointerMove);
321
+ this.eventsEl.removeEventListener("pointerenter", this.handlePointerEnter);
322
+ this.eventsEl.removeEventListener("pointerleave", this.handlePointerLeave);
323
+ };
324
+ /** Update the axis lock at runtime. */
325
+ setAxis = (axis) => {
326
+ this.axis = axis;
327
+ };
328
+ /**
329
+ * Compute normalized [-1, 1] position from pointer coordinates
330
+ * relative to the element's bounding rect. Respects axis locking.
331
+ */
332
+ handlePointerMove = (e) => {
333
+ if (!this.rect) return;
334
+ const rawX = (e.clientX - this.rect.left) / this.rect.width;
335
+ const rawY = (e.clientY - this.rect.top) / this.rect.height;
336
+ const x = this.axis === "y" ? 0 : rawX * 2 - 1;
337
+ const y = this.axis === "x" ? 0 : rawY * 2 - 1;
338
+ this.onMove({
339
+ x,
340
+ y
341
+ });
342
+ };
343
+ handlePointerEnter = () => {
344
+ this.rect = this.eventsEl.getBoundingClientRect();
345
+ this.onEnter?.();
346
+ };
347
+ handlePointerLeave = () => {
348
+ this.rect = null;
349
+ this.onLeave?.();
350
+ };
351
+ };
352
+
353
+ //#endregion
354
+ //#region src/levita.ts
355
+ /**
356
+ * Main entry point for the Levita 3D tilt effect.
357
+ *
358
+ * Orchestrates sensors (pointer, accelerometer), visual effects (glare, shadow),
359
+ * and multi-layer parallax. All rendering is driven by CSS custom properties —
360
+ * no requestAnimationFrame loop runs during interaction.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * import { Levita } from 'levita-js';
365
+ * import 'levita-js/style.css';
366
+ *
367
+ * const tilt = new Levita(element, { glare: true, shadow: true });
368
+ * tilt.on('move', ({ x, y }) => console.log(x, y));
369
+ * ```
370
+ */
371
+ var Levita = class {
372
+ el;
373
+ options;
374
+ pointerSensor;
375
+ motionSensor = null;
376
+ glareEffect = null;
377
+ shadowEffect = null;
378
+ layers = [];
379
+ listeners = /* @__PURE__ */ new Map();
380
+ destroyed = false;
381
+ gyroscopeRequested = false;
382
+ /**
383
+ * @param el - The DOM element to apply the tilt effect to
384
+ * @param options - Configuration options (all optional, sensible defaults)
385
+ */
386
+ constructor(el, options = {}) {
387
+ this.el = el;
388
+ this.options = {
389
+ ...DEFAULT_OPTIONS,
390
+ ...options
391
+ };
392
+ this.el.classList.add("levita");
393
+ this.applyBaseProperties();
394
+ this.layers = scanLayers(this.el);
395
+ if (this.options.glare) this.glareEffect = new GlareEffect(this.el, this.options.maxGlare);
396
+ if (this.options.shadow) this.shadowEffect = new ShadowEffect(this.el);
397
+ this.pointerSensor = new PointerSensor(this.el, (values) => this.handleSensorInput(values), () => this.handleEnter(), () => this.handleLeave(), this.options.axis, this.options.eventsEl);
398
+ if (this.options.gyroscope !== false && MotionSensor.isSupported()) {
399
+ this.motionSensor = new MotionSensor((values) => this.handleSensorInput(values), this.options.axis);
400
+ if (this.options.gyroscope === "auto") this.el.addEventListener("click", this.handleFirstTouch, { once: true });
401
+ }
402
+ if (!this.options.disabled) this.enable();
403
+ }
404
+ /**
405
+ * On first touch (iOS auto mode), request accelerometer permission
406
+ * and switch from pointer to motion sensor if granted.
407
+ */
408
+ handleFirstTouch = async () => {
409
+ if (this.gyroscopeRequested || !this.motionSensor) return;
410
+ this.gyroscopeRequested = true;
411
+ if (await this.motionSensor.requestPermission()) {
412
+ this.pointerSensor.stop();
413
+ this.setTransition(false);
414
+ this.motionSensor.start();
415
+ }
416
+ };
417
+ /** Re-enable the tilt effect after a `disable()` call. */
418
+ enable = () => {
419
+ if (this.destroyed) return;
420
+ this.options.disabled = false;
421
+ this.pointerSensor.start();
422
+ };
423
+ /** Pause the tilt effect and reset the element to its neutral position. */
424
+ disable = () => {
425
+ this.options.disabled = true;
426
+ this.pointerSensor.stop();
427
+ this.motionSensor?.stop();
428
+ this.resetTransform();
429
+ };
430
+ /**
431
+ * Manually request accelerometer permission (for `gyroscope: true` mode).
432
+ * Must be called from a user gesture on iOS 13+.
433
+ *
434
+ * @returns Whether permission was granted
435
+ */
436
+ requestPermission = async () => {
437
+ if (!this.motionSensor) return false;
438
+ const granted = await this.motionSensor.requestPermission();
439
+ if (granted) {
440
+ this.pointerSensor.stop();
441
+ this.setTransition(false);
442
+ this.motionSensor.start();
443
+ }
444
+ return granted;
445
+ };
446
+ /**
447
+ * Fully clean up: stop sensors, remove effects, restore the element
448
+ * to its original state. The instance cannot be reused after this.
449
+ */
450
+ destroy = () => {
451
+ if (this.destroyed) return;
452
+ this.destroyed = true;
453
+ this.pointerSensor.stop();
454
+ this.motionSensor?.stop();
455
+ this.glareEffect?.destroy();
456
+ this.shadowEffect?.destroy();
457
+ cleanupLayers(this.layers);
458
+ this.el.classList.remove("levita");
459
+ this.removeBaseProperties();
460
+ this.listeners.clear();
461
+ this.el.removeEventListener("click", this.handleFirstTouch);
462
+ };
463
+ /**
464
+ * Register an event listener.
465
+ *
466
+ * @param event - Event name: 'move', 'enter', or 'leave'
467
+ * @param callback - Handler function
468
+ */
469
+ on = (event, callback) => {
470
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
471
+ this.listeners.get(event)?.add(callback);
472
+ };
473
+ /**
474
+ * Remove a previously registered event listener.
475
+ *
476
+ * @param event - Event name
477
+ * @param callback - The exact handler reference passed to `on()`
478
+ */
479
+ off = (event, callback) => {
480
+ this.listeners.get(event)?.delete(callback);
481
+ };
482
+ /** Emit an event to all registered listeners. */
483
+ emit = (event, data) => {
484
+ const callbacks = this.listeners.get(event);
485
+ if (callbacks) for (const cb of callbacks) cb(data);
486
+ };
487
+ /**
488
+ * Process normalized sensor input and update CSS custom properties.
489
+ * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
490
+ */
491
+ handleSensorInput = (input) => {
492
+ if (this.options.disabled) return;
493
+ const multiplier = this.options.reverse ? -1 : 1;
494
+ const x = input.y * this.options.max * multiplier;
495
+ const y = input.x * this.options.max * multiplier * -1;
496
+ this.el.style.setProperty("--levita-x", `${x}deg`);
497
+ this.el.style.setProperty("--levita-y", `${y}deg`);
498
+ this.el.style.setProperty("--levita-scale", String(this.options.scale));
499
+ this.el.style.setProperty("--levita-percent-x", String(input.x));
500
+ this.el.style.setProperty("--levita-percent-y", String(input.y));
501
+ this.glareEffect?.update(input.x, input.y);
502
+ this.shadowEffect?.update(input.x, input.y);
503
+ const values = {
504
+ x,
505
+ y,
506
+ percentX: input.x,
507
+ percentY: input.y
508
+ };
509
+ this.emit("move", values);
510
+ };
511
+ handleEnter = () => {
512
+ this.setTransition(false);
513
+ this.el.style.setProperty("--levita-scale", String(this.options.scale));
514
+ this.emit("enter", void 0);
515
+ };
516
+ handleLeave = () => {
517
+ this.setTransition(true);
518
+ if (this.options.reset) this.resetTransform();
519
+ this.emit("leave", void 0);
520
+ };
521
+ /** Toggle the CSS transition on or off. */
522
+ setTransition = (on) => {
523
+ this.el.style.setProperty("--levita-speed", on ? `${this.options.speed}ms` : "0ms");
524
+ };
525
+ /** Reset the element to its neutral (non-tilted) position. */
526
+ resetTransform = () => {
527
+ this.el.style.setProperty("--levita-x", "0deg");
528
+ this.el.style.setProperty("--levita-y", "0deg");
529
+ this.el.style.setProperty("--levita-scale", "1");
530
+ this.el.style.setProperty("--levita-percent-x", "0");
531
+ this.el.style.setProperty("--levita-percent-y", "0");
532
+ this.glareEffect?.update(0, 0);
533
+ this.shadowEffect?.update(0, 0);
534
+ };
535
+ /** Apply initial CSS custom properties from options. */
536
+ applyBaseProperties = () => {
537
+ this.el.style.setProperty("--levita-perspective", `${this.options.perspective}px`);
538
+ this.el.style.setProperty("--levita-speed", `${this.options.speed}ms`);
539
+ this.el.style.setProperty("--levita-easing", this.options.easing);
540
+ };
541
+ /** Remove all Levita CSS custom properties from the element. */
542
+ removeBaseProperties = () => {
543
+ this.el.style.removeProperty("--levita-perspective");
544
+ this.el.style.removeProperty("--levita-speed");
545
+ this.el.style.removeProperty("--levita-easing");
546
+ this.el.style.removeProperty("--levita-x");
547
+ this.el.style.removeProperty("--levita-y");
548
+ this.el.style.removeProperty("--levita-scale");
549
+ this.el.style.removeProperty("--levita-percent-x");
550
+ this.el.style.removeProperty("--levita-percent-y");
551
+ };
552
+ };
553
+
554
+ //#endregion
555
+ exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
556
+ exports.Levita = Levita;
557
+ exports.OPTION_KEYS = OPTION_KEYS;
558
+ exports.buildOptions = buildOptions;
@@ -0,0 +1,149 @@
1
+ //#region src/types.d.ts
2
+ type Axis = "x" | "y" | null;
3
+ type GyroscopeMode = "auto" | boolean;
4
+ interface LevitaOptions {
5
+ /** Max tilt angle in degrees. Default: 15 */
6
+ max: number;
7
+ /** CSS perspective in px. Default: 1000 */
8
+ perspective: number;
9
+ /** Scale factor on hover. Default: 1.05 */
10
+ scale: number;
11
+ /** Transition duration in ms. Default: 200 */
12
+ speed: number;
13
+ /** CSS transition easing. Default: 'ease-out' */
14
+ easing: string;
15
+ /** Invert tilt direction. Default: false */
16
+ reverse: boolean;
17
+ /** Restrict tilt to a single axis. Default: null (both) */
18
+ axis: Axis;
19
+ /** Reset tilt on pointer leave. Default: true */
20
+ reset: boolean;
21
+ /** Enable glare effect. Default: false */
22
+ glare: boolean;
23
+ /** Max glare opacity (0-1). Default: 0.5 */
24
+ maxGlare: number;
25
+ /** Enable dynamic shadow. Default: false */
26
+ shadow: boolean;
27
+ /** Gyroscope mode. Default: 'auto' */
28
+ gyroscope: GyroscopeMode;
29
+ /** Disable the effect. Default: false */
30
+ disabled: boolean;
31
+ /** Element to listen for pointer events on. Default: the element itself */
32
+ eventsEl: HTMLElement | null;
33
+ }
34
+ interface TiltValues {
35
+ /** Tilt angle on X axis in degrees */
36
+ x: number;
37
+ /** Tilt angle on Y axis in degrees */
38
+ y: number;
39
+ /** Normalized X position [-1, 1] */
40
+ percentX: number;
41
+ /** Normalized Y position [-1, 1] */
42
+ percentY: number;
43
+ }
44
+ interface LevitaEventMap {
45
+ move: TiltValues;
46
+ enter: undefined;
47
+ leave: undefined;
48
+ }
49
+ type EventCallback<T> = (data: T) => void;
50
+ //#endregion
51
+ //#region src/constants.d.ts
52
+ /** All keys of `LevitaOptions`, derived from `DEFAULT_OPTIONS`. */
53
+ declare const OPTION_KEYS: readonly (keyof LevitaOptions)[];
54
+ /**
55
+ * Build a partial `LevitaOptions` object from a source,
56
+ * including only keys that are explicitly defined.
57
+ */
58
+ declare const buildOptions: (source: Partial<LevitaOptions>) => Partial<LevitaOptions>;
59
+ declare const DEFAULT_OPTIONS: LevitaOptions;
60
+ //#endregion
61
+ //#region src/levita.d.ts
62
+ /**
63
+ * Main entry point for the Levita 3D tilt effect.
64
+ *
65
+ * Orchestrates sensors (pointer, accelerometer), visual effects (glare, shadow),
66
+ * and multi-layer parallax. All rendering is driven by CSS custom properties —
67
+ * no requestAnimationFrame loop runs during interaction.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { Levita } from 'levita-js';
72
+ * import 'levita-js/style.css';
73
+ *
74
+ * const tilt = new Levita(element, { glare: true, shadow: true });
75
+ * tilt.on('move', ({ x, y }) => console.log(x, y));
76
+ * ```
77
+ */
78
+ declare class Levita {
79
+ private el;
80
+ private options;
81
+ private pointerSensor;
82
+ private motionSensor;
83
+ private glareEffect;
84
+ private shadowEffect;
85
+ private layers;
86
+ private listeners;
87
+ private destroyed;
88
+ private gyroscopeRequested;
89
+ /**
90
+ * @param el - The DOM element to apply the tilt effect to
91
+ * @param options - Configuration options (all optional, sensible defaults)
92
+ */
93
+ constructor(el: HTMLElement, options?: Partial<LevitaOptions>);
94
+ /**
95
+ * On first touch (iOS auto mode), request accelerometer permission
96
+ * and switch from pointer to motion sensor if granted.
97
+ */
98
+ private handleFirstTouch;
99
+ /** Re-enable the tilt effect after a `disable()` call. */
100
+ enable: () => void;
101
+ /** Pause the tilt effect and reset the element to its neutral position. */
102
+ disable: () => void;
103
+ /**
104
+ * Manually request accelerometer permission (for `gyroscope: true` mode).
105
+ * Must be called from a user gesture on iOS 13+.
106
+ *
107
+ * @returns Whether permission was granted
108
+ */
109
+ requestPermission: () => Promise<boolean>;
110
+ /**
111
+ * Fully clean up: stop sensors, remove effects, restore the element
112
+ * to its original state. The instance cannot be reused after this.
113
+ */
114
+ destroy: () => void;
115
+ /**
116
+ * Register an event listener.
117
+ *
118
+ * @param event - Event name: 'move', 'enter', or 'leave'
119
+ * @param callback - Handler function
120
+ */
121
+ on: <K extends keyof LevitaEventMap>(event: K, callback: EventCallback<LevitaEventMap[K]>) => void;
122
+ /**
123
+ * Remove a previously registered event listener.
124
+ *
125
+ * @param event - Event name
126
+ * @param callback - The exact handler reference passed to `on()`
127
+ */
128
+ off: <K extends keyof LevitaEventMap>(event: K, callback: EventCallback<LevitaEventMap[K]>) => void;
129
+ /** Emit an event to all registered listeners. */
130
+ private emit;
131
+ /**
132
+ * Process normalized sensor input and update CSS custom properties.
133
+ * Maps sensor X/Y to rotateY/rotateX (swapped, rotateX = vertical tilt).
134
+ */
135
+ private handleSensorInput;
136
+ private handleEnter;
137
+ private handleLeave;
138
+ /** Toggle the CSS transition on or off. */
139
+ private setTransition;
140
+ /** Reset the element to its neutral (non-tilted) position. */
141
+ private resetTransform;
142
+ /** Apply initial CSS custom properties from options. */
143
+ private applyBaseProperties;
144
+ /** Remove all Levita CSS custom properties from the element. */
145
+ private removeBaseProperties;
146
+ }
147
+ //#endregion
148
+ export { type Axis, DEFAULT_OPTIONS, type EventCallback, type GyroscopeMode, Levita, type LevitaEventMap, type LevitaOptions, OPTION_KEYS, type TiltValues, buildOptions };
149
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +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"}