UNPKG

@fiddle-digital/string-tune

Version:

StringTune is a cutting-edge JavaScript library designed to deliver high-performance, modular web effects. Whether you're looking to add smooth parallax scrolling, dynamic cursor interactions, progress tracking, or autoplay videos, StringTune empowers dev

1 lines 316 kB
{"version":3,"sources":["../src/core/controllers/CursorController.ts","../src/core/managers/EventManager.ts","../src/core/managers/ModuleManager.ts","../src/objects/StringObject.ts","../src/core/managers/ObjectManager.ts","../src/core/controllers/ScrollController.ts","../src/core/controllers/StringScrollDefault.ts","../src/core/controllers/StringScrollDisable.ts","../src/core/controllers/StringScrollSmooth.ts","../src/core/managers/ScrollManager.ts","../src/states/CursorState.ts","../src/states/RenderState.ts","../src/states/ScrollState.ts","../src/states/TimeState.ts","../src/states/ViewportState.ts","../src/core/StringData.ts","../src/core/StringModule.ts","../src/tools/BoundingClientRectTool.ts","../src/tools/DOMAttributeTool.ts","../src/tools/RecordAttributeTool.ts","../src/tools/TransformNullifyTool.ts","../src/tools/RelativePositionTool.ts","../src/tools/LerpTool.ts","../src/tools/UnitParserTool.ts","../src/tools/AdaptiveLerpTool.ts","../src/tools/OriginParserTool.ts","../src/tools/ColorParserTool.ts","../src/tools/ValidationTool.ts","../src/tools/EasingFunctionTool.ts","../src/tools/MagneticPullTool.ts","../src/tools/LerpColorTool.ts","../src/tools/LerpVector2Tool.ts","../src/tools/TransformScaleParserTool.ts","../src/tools/CharIndexerTool.ts","../src/tools/LayoutLineSplitterTool.ts","../src/tools/SplitDomBuilderTool.ts","../src/tools/WordIndexerTool.ts","../src/tools/SplitOptionsParserTool.ts","../src/core/StringToolsContainer.ts","../src/modules/cursor/StringCursor.ts","../src/modules/cursor/StringMagnetic.ts","../src/modules/loading/StringLazy.ts","../src/modules/loading/StringLoading.ts","../src/modules/screen/StringInview.ts","../src/modules/screen/StringResponsive.ts","../src/modules/scroll/StringAnchor.ts","../src/modules/scroll/StringGlide.ts","../src/modules/scroll/StringLerp.ts","../src/modules/scroll/StringProgress.ts","../src/modules/scroll/StringParallax.ts","../src/modules/scrollbar/StringScrollbarHorizontal.ts","../src/modules/scrollbar/StringScrollbarVertical.ts","../src/modules/scrollbar/StringScrollbar.ts","../src/modules/text/StringSplit.ts","../src/modules/tracker/StringDelayLerpTracker.ts","../src/modules/tracker/StringFPSTracker.ts","../src/modules/tracker/StringLerpTracker.ts","../src/modules/tracker/StringPositionTracker.ts","../src/utils/Debounce.ts","../src/utils/StringFPS.ts","../src/modules/loading/StringVideoAutoplay.ts","../src/index.ts"],"sourcesContent":["import { CursorState } from \"../../states/CursorState\"\r\nimport { EventManager } from \"../managers/EventManager\"\r\nimport { StringContext } from \"../StringContext\"\r\nimport { StringData } from \"../StringData\"\r\nimport { StringToolsContainer } from \"../StringToolsContainer\"\r\n\r\n/**\r\n * Manages virtual cursor logic: smoothing, updating, and syncing with mouse events.\r\n * \r\n * This controller handles cursor position tracking with smoothing (lerp) logic.\r\n * Useful for animated cursor effects and interaction modules.\r\n */\r\nexport class CursorController {\r\n /** Context providing access to shared data, tools, and settings. */\r\n protected context: StringContext\r\n\r\n /** Threshold below which cursor is considered settled (no movement). */\r\n private readonly SETTLE_THRESHOLD = 0.1\r\n\r\n /** Smoothing factor used to interpolate cursor movement. */\r\n private smoothingFactor: number\r\n\r\n /**\r\n * Constructs a new `CursorController` instance.\r\n * @param smoothing The initial lerp smoothing factor (0 to 1).\r\n * @param context The shared context containing state and tools.\r\n */\r\n constructor(smoothing: number = 0.1, context: StringContext) {\r\n this.smoothingFactor = smoothing\r\n this.context = context\r\n this.onSettingsChange()\r\n }\r\n\r\n /**\r\n * Updates the target cursor position from a mouse event.\r\n * This is the raw position that smoothing will interpolate toward.\r\n * @param e MouseEvent with current cursor position.\r\n */\r\n public onMouseMove(e: MouseEvent): void {\r\n this.context.data.cursor.targetX = e.clientX\r\n this.context.data.cursor.targetY = e.clientY\r\n }\r\n\r\n /**\r\n * Updates smoothed cursor position using linear interpolation (lerp).\r\n * Should be called on every animation frame.\r\n * Handles snapping when movement is below threshold.\r\n */\r\n public onFrame(): void {\r\n const { targetX, targetY, smoothedX, smoothedY } = this.context.data.cursor\r\n\r\n const stepX = this.context.tools.lerp.process({ from: smoothedX, to: targetX, progress: this.smoothingFactor })\r\n const stepY = this.context.tools.lerp.process({ from: smoothedY, to: targetY, progress: this.smoothingFactor })\r\n\r\n const distance = this.getStepDistance(stepX, stepY)\r\n\r\n if (this.isSettled(distance)) {\r\n this.snapToTarget()\r\n } else {\r\n this.applyStep(stepX, stepY)\r\n }\r\n }\r\n\r\n /**\r\n * Called when global settings change.\r\n * Updates the internal lerp factor from context settings.\r\n */\r\n public onSettingsChange(): void {\r\n let lerp = Number(this.context.settings['lerp'])\r\n this.setLerpFactor(lerp)\r\n }\r\n\r\n /**\r\n * Dynamically adjusts the smoothing factor using adaptive mapping.\r\n * @param t The raw input lerp value (usually from 0 to 1).\r\n */\r\n public setLerpFactor(t: number): void {\r\n this.smoothingFactor = this.context.tools.adaptiveLerp.process({ \r\n value: t,\r\n inMin: 0.1,\r\n inMax: 1.0,\r\n outMin: 0.05,\r\n outMax: 0.65\r\n })\r\n }\r\n\r\n /**\r\n * Calculates the Euclidean distance from the cursor step.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n * @returns The length of the movement vector.\r\n */\r\n private getStepDistance(x: number, y: number): number {\r\n return Math.hypot(x, y)\r\n }\r\n\r\n /**\r\n * Determines whether the movement is below the settle threshold.\r\n * @param distance Distance between smoothed and target positions.\r\n * @returns Whether the cursor should snap to target.\r\n */\r\n private isSettled(distance: number): boolean {\r\n return distance < this.SETTLE_THRESHOLD\r\n }\r\n\r\n /**\r\n * Immediately sets smoothed position to the target and zeroes deltas.\r\n */\r\n private snapToTarget(): void {\r\n this.context.data.cursor.smoothedX = this.context.data.cursor.targetX\r\n this.context.data.cursor.smoothedY = this.context.data.cursor.targetY\r\n this.context.data.cursor.stepX = 0\r\n this.context.data.cursor.stepY = 0\r\n }\r\n\r\n /**\r\n * Applies lerped movement step to smoothed position and stores delta.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n */\r\n private applyStep(x: number, y: number): void {\r\n this.context.data.cursor.smoothedX += x\r\n this.context.data.cursor.smoothedY += y\r\n this.context.data.cursor.stepX = x\r\n this.context.data.cursor.stepY = y\r\n }\r\n}\r\n","import { EventCallback } from \"../../models/event/EventCallback\";\r\n\r\n/**\r\n * Manages custom event subscriptions and dispatching.\r\n * Allows multiple listeners per event and supports optional `id` suffixing.\r\n */\r\nexport class EventManager {\r\n private listeners: Record<string, Set<EventCallback<any>>> = {};\r\n\r\n /**\r\n * Subscribes to an event.\r\n * Optionally appends an `id` to the event name for namespacing.\r\n *\r\n * @param eventName The base event name (e.g. \"scroll\", \"update\").\r\n * @param callback The function to call when the event is emitted.\r\n * @param id Optional unique identifier to scope the event (e.g. element ID).\r\n */\r\n on<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string\r\n ): void {\r\n const fullEvent = id ? `${eventName}_${id}` : eventName;\r\n\r\n if (!this.listeners[fullEvent]) {\r\n this.listeners[fullEvent] = new Set();\r\n }\r\n this.listeners[fullEvent].add(callback);\r\n }\r\n\r\n /**\r\n * Unsubscribes from a specific event listener.\r\n * Must match the original `eventName`, `callback`, and optional `id`.\r\n *\r\n * @param eventName The base event name to unsubscribe from.\r\n * @param callback The callback function to remove.\r\n * @param id Optional identifier used when subscribing.\r\n */\r\n off<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string\r\n ): void {\r\n const fullEvent = id ? `${eventName}_${id}` : eventName;\r\n\r\n if (this.listeners[fullEvent]) {\r\n this.listeners[fullEvent].delete(callback);\r\n }\r\n }\r\n\r\n /**\r\n * Emits an event with an optional payload.\r\n * All matching listeners will be called.\r\n *\r\n * @param eventName The full event name (must include `id` if used).\r\n * @param payload Optional data passed to event listeners.\r\n */\r\n emit<T = any>(eventName: string, payload?: T): void {\r\n const set = this.listeners[eventName];\r\n if (!set) return;\r\n\r\n for (const callback of set) {\r\n callback(payload as T);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object progress event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle progress value.\r\n */\r\n onProgress(id: string, callback: EventCallback<number>): void {\r\n this.on(`progress:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object progress event.\r\n * @param id The object ID.\r\n * @param value The progress value.\r\n */\r\n emitProgress(id: string, value: number): void {\r\n this.emit(`progress:${id}`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object in-view event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle visibility.\r\n */\r\n onInview(id: string, callback: EventCallback<boolean>): void {\r\n this.on(`object:inview:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object in-view event.\r\n * @param id The object ID.\r\n * @param visible Whether the object is visible.\r\n */\r\n emitInview(id: string, visible: boolean): void {\r\n this.emit(`object:inview${id}`, visible);\r\n }\r\n\r\n /**\r\n * Subscribes to the global scroll event.\r\n * @param callback The callback to handle scroll value.\r\n */\r\n onScroll(callback: EventCallback<number>): void {\r\n this.on(`scroll`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global scroll event.\r\n * @param value The scroll value.\r\n */\r\n emitScroll(value: number): void {\r\n this.emit(`scroll`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to the global update event.\r\n * @param callback The callback to handle update.\r\n */\r\n onUpdate(callback: EventCallback<void>): void {\r\n this.on(`update`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global update event.\r\n */\r\n emitUpdate(): void {\r\n this.emit(`update`);\r\n }\r\n\r\n /**\r\n * Clears all listeners for a specific event.\r\n *\r\n * @param eventName The full event name (including optional `id`).\r\n */\r\n clear(eventName: string): void {\r\n delete this.listeners[eventName];\r\n }\r\n\r\n /**\r\n * Clears all registered events.\r\n */\r\n clearAll(): void {\r\n this.listeners = {};\r\n }\r\n}\r\n","import { StringData } from \"../..\";\r\nimport { IStringModule } from \"../IStringModule\";\r\nimport { StringModule } from \"../StringModule\";\r\n\r\n/**\r\n * Central manager for registering, tracking and delegating lifecycle events \r\n * to core and UI modules in the system.\r\n * \r\n * Handles scroll, resize, animation frame updates and DOM events.\r\n */\r\nexport class ModuleManager {\r\n /** All core logic modules (e.g., scroll, in-view, parallax). */\r\n private modules: StringModule[] = [];\r\n\r\n /** All UI or visual/interaction-based modules (e.g., cursor, split text). */\r\n private uiModules: StringModule[] = [];\r\n\r\n /**\r\n * @param data Shared state container for scroll, viewport, etc.\r\n */\r\n constructor(private data: StringData) {}\r\n\r\n /**\r\n * Registers a module into the appropriate group based on its type.\r\n * @param module The module instance to register.\r\n */\r\n register(module: StringModule): void {\r\n if (module.type === 1) this.modules.push(module);\r\n if (module.type === 2) this.uiModules.push(module);\r\n }\r\n\r\n /**\r\n * Finds the first registered module of the given class/type.\r\n * @param type The module class constructor.\r\n * @returns The instance, if found.\r\n */\r\n find<T>(type: new (...args: any[]) => T): T | undefined {\r\n return this.modules.find(m => m instanceof type) as T | undefined;\r\n }\r\n\r\n /** Invokes `onInit` on all modules. */\r\n onInit(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onInit());\r\n }\r\n\r\n /** Invokes `onFrame` on all modules, passing shared state. */\r\n onFrame(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onFrame(this.data));\r\n }\r\n\r\n /** Invokes `onScroll` on all modules with current scroll state. */\r\n onScroll(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScroll(this.data));\r\n }\r\n\r\n /** Invokes `onResize` on all modules. */\r\n onResize(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onResize());\r\n }\r\n\r\n /**\r\n * Delegates mouse movement events to modules.\r\n * @param e The mousemove event.\r\n */\r\n onMouseMove(e: MouseEvent): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onMouseMove(e));\r\n }\r\n\r\n /**\r\n * Delegates wheel events to modules.\r\n * @param e The wheel event.\r\n */\r\n onWheel(e: WheelEvent): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onWheel(e));\r\n }\r\n\r\n /** Notifies all modules that scroll has changed diraction. */\r\n onDirectionChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDirectionChange());\r\n }\r\n\r\n /** Notifies all modules that scroll has started. */\r\n onScrollStart(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollStart());\r\n }\r\n\r\n /** Notifies all modules that scroll has stopped. */\r\n onScrollStop(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollStop());\r\n }\r\n\r\n /** Notifies all modules that scroll axis (horizontal/vertical) has changed. */\r\n onAxisChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onAxisChange());\r\n }\r\n\r\n /** Notifies all modules that device type (desktop/mobile) has changed. */\r\n onDeviceChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDeviceChange());\r\n }\r\n\r\n /** Notifies modules of updates to scroll-related configuration. */\r\n onScrollConfigChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollConfigChange());\r\n }\r\n\r\n /** Notifies modules of updated global or module-specific settings. */\r\n onSettingsChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onSettingsChange());\r\n }\r\n\r\n /**\r\n * Called when DOM is mutated — e.g. new elements added/removed.\r\n * @param added Newly added DOM nodes.\r\n * @param removed Removed DOM nodes.\r\n */\r\n onDOMMutate(added: NodeList, removed: NodeList): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDOMMutate(added, removed));\r\n }\r\n\r\n /** Cleans up all modules and clears internal lists. */\r\n destroy(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.destroy());\r\n this.modules = [];\r\n this.uiModules = [];\r\n }\r\n\r\n /**\r\n * Returns all modules (core + UI) as a flat array.\r\n */\r\n get all(): IStringModule[] {\r\n return [...this.modules, ...this.uiModules];\r\n }\r\n\r\n /**\r\n * Returns only core modules (type === 1).\r\n */\r\n get core(): IStringModule[] {\r\n return this.modules;\r\n }\r\n\r\n /**\r\n * Returns only UI modules (type === 2).\r\n */\r\n get ui(): IStringModule[] {\r\n return this.uiModules;\r\n }\r\n}\r\n","import { IStringModule } from \"../core/IStringModule\";\r\nimport { EventManager } from \"../core/managers/EventManager\";\r\n\r\n/**\r\n * Internal class representing a DOM-bound interactive object.\r\n * Connected to modules and holds its own internal state.\r\n */\r\nexport class StringObject {\r\n /**\r\n * The DOM element this object wraps.\r\n */\r\n public htmlElement: HTMLElement;\r\n\r\n /**\r\n * Unique global ID assigned by the system.\r\n */\r\n public id: string = \"\";\r\n\r\n /**\r\n * Space-separated list of all attribute keys associated with this object.\r\n */\r\n public keys: string[] = [];\r\n\r\n /**\r\n * A list of elements that should be affected in sync with this one.\r\n */\r\n public connects: HTMLElement[] = [];\r\n\r\n /**\r\n * Internal key-value store of dynamic object properties (like offsets, progress, etc.).\r\n */\r\n private properties: Map<string, any> = new Map();\r\n\r\n /**\r\n * Modules currently connected to this object.\r\n */\r\n private modules: IStringModule[] = [];\r\n\r\n /**\r\n * Manages and handles events for the object.\r\n * Provides functionality to register, trigger, and manage event listeners.\r\n */\r\n events: EventManager = new EventManager();\r\n\r\n constructor(id: string, element: HTMLElement) {\r\n this.htmlElement = element;\r\n this.id = id;\r\n }\r\n\r\n /**\r\n * Stores a property value for this object.\r\n * @param key - Property name\r\n * @param value - Value to store\r\n */\r\n public setProperty<T>(key: string, value: T): void {\r\n this.properties.set(key, value);\r\n }\r\n\r\n /**\r\n * Retrieves a previously stored property value.\r\n * @param key - Property name\r\n * @returns The value or null if not set\r\n */\r\n public getProperty<T>(key: string): T {\r\n return this.properties.get(key) ?? null;\r\n }\r\n\r\n /**\r\n * Marks this object as \"active\" (usually on intersection/scroll enter).\r\n */\r\n public enter(): void {\r\n this.events.emit(\"enter\", this);\r\n this.setProperty(\"active\", true);\r\n this.modules.forEach((module) => {\r\n module.enterObject(this.id, this);\r\n });\r\n }\r\n\r\n /**\r\n * Marks this object as \"inactive\" (usually on intersection/scroll leave).\r\n */\r\n public leave(): void {\r\n this.events.emit(\"leave\", this);\r\n this.setProperty(\"active\", false);\r\n this.modules.forEach((module) => {\r\n module.exitObject(this.id);\r\n });\r\n }\r\n\r\n /**\r\n * Shows the object, applies visual class and notifies connected modules.\r\n */\r\n public show(): void {\r\n this.htmlElement.classList.add(\"-inview\");\r\n }\r\n\r\n /**\r\n * Hides the object, removes visual class (if repeat is enabled), and notifies modules.\r\n */\r\n public hide(): void {\r\n const shouldRepeat = this.getProperty<boolean>(\"repeat\");\r\n if (shouldRepeat) {\r\n this.htmlElement.classList.remove(\"-inview\");\r\n }\r\n }\r\n\r\n /**\r\n * Connects a module to this object if not already connected.\r\n * @param module - The module to connect\r\n */\r\n public connect(module: IStringModule): void {\r\n if (!this.modules.includes(module)) {\r\n this.modules.push(module);\r\n }\r\n }\r\n}\r\n","import { ModuleManager } from \"./ModuleManager\";\r\nimport { StringData } from \"../StringData\";\r\nimport { StringObject } from \"../../objects/StringObject\";\r\nimport { EventManager } from \"./EventManager\";\r\n\r\nexport class ObjectManager {\r\n private objects = new Map<string, StringObject>();\r\n private connectQueue: { id: string; element: HTMLElement }[] = [];\r\n private globalId = 1;\r\n\r\n constructor(\r\n private data: StringData,\r\n private modules: ModuleManager,\r\n private events: EventManager\r\n ) {}\r\n\r\n /**\r\n * Returns the object map (read-only).\r\n */\r\n get all(): ReadonlyMap<string, StringObject> {\r\n return this.objects;\r\n }\r\n\r\n /**\r\n * Adds a new object from an element.\r\n */\r\n public add(el: HTMLElement) {\r\n const idAttr = el.getAttribute(\"string-id\") ?? `string-${this.globalId++}`;\r\n\r\n const object =\r\n idAttr && this.objects.has(idAttr)\r\n ? this.objects.get(idAttr)!\r\n : new StringObject(idAttr, el);\r\n\r\n el.setAttribute(\"string-id\", object.id);\r\n\r\n const keysAttr =\r\n el.getAttribute(\"string\") ?? el.getAttribute(\"data-string\");\r\n\r\n if (keysAttr) {\r\n object.keys = (keysAttr ?? \"\").split(\"|\");\r\n }\r\n\r\n el.setAttribute(\"string-inited\", \"\");\r\n this.objects.set(object.id, object);\r\n\r\n const attributes = this.getAllAttributes(el);\r\n\r\n // Delegate core setup (dimensions, offsets, key, start/end, etc.)\r\n this.modules.core.forEach((m) => {\r\n if (\r\n \"setupCoreProperties\" in m &&\r\n typeof m[\"setupCoreProperties\"] === \"function\"\r\n ) {\r\n (m as any).setupCoreProperties(object, el, attributes);\r\n }\r\n });\r\n\r\n // Try connecting to modules\r\n this.modules.core.forEach((m) => {\r\n if (m.canConnect(object)) {\r\n m.initializeObject(this.globalId, object, el, attributes);\r\n m.calculatePositions(object, this.data.viewport.windowHeight);\r\n m.connectObject(object);\r\n }\r\n });\r\n\r\n // Restore connect-from\r\n const queueItems = this.connectQueue.filter((q) => q.id === object.id);\r\n queueItems.forEach((item) => object.connects.push(item.element));\r\n this.connectQueue = this.connectQueue.filter((q) => q.id !== object.id);\r\n\r\n // Set up observers\r\n this.initObservers(object, el);\r\n }\r\n\r\n /**\r\n * Removes an object by its id.\r\n */\r\n public remove(id: string) {\r\n const obj = this.objects.get(id);\r\n if (!obj) return;\r\n\r\n obj.events.clearAll();\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n obj.getProperty<IntersectionObserver>(\"observer-inview\")?.disconnect();\r\n\r\n obj.htmlElement.removeAttribute(\"string-inited\");\r\n obj.leave();\r\n\r\n this.objects.delete(id);\r\n }\r\n\r\n /**\r\n * Add an element that will connect later.\r\n */\r\n public enqueueConnection(id: string, element: HTMLElement) {\r\n this.connectQueue.push({ id, element });\r\n }\r\n\r\n private getAllAttributes(el: HTMLElement): Record<string, any> {\r\n const attributes: Record<string, any> = {};\r\n Array.from(el.attributes).forEach((attr) => {\r\n attributes[attr.name] = attr.value;\r\n });\r\n return attributes;\r\n }\r\n\r\n private initObservers(obj: StringObject, el: HTMLElement) {\r\n const start = obj.getProperty<number>(\"offset-top\") ?? 0;\r\n const end = obj.getProperty<number>(\"offset-bottom\") ?? 0;\r\n const inviewTop = obj.getProperty<number>(\"inview-top\") ?? 0;\r\n const inviewBottom = obj.getProperty<number>(\"inview-bottom\") ?? 0;\r\n\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n obj.getProperty<IntersectionObserver>(\"observer-inview\")?.disconnect();\r\n\r\n const progressCallback = (entries: IntersectionObserverEntry[]) => {\r\n entries.forEach((e) => {\r\n this.events.emit(`object:activate:${obj.id}`, e.isIntersecting);\r\n e.isIntersecting ? obj.enter() : obj.leave();\r\n });\r\n };\r\n\r\n const inviewCallback = (entries: IntersectionObserverEntry[]) => {\r\n entries.forEach((e) => {\r\n this.events.emit(`object:inview:${obj.id}`, e.isIntersecting);\r\n e.isIntersecting ? obj.show() : obj.hide();\r\n });\r\n };\r\n\r\n const progressObserver = new IntersectionObserver(progressCallback, {\r\n root: null,\r\n rootMargin: `${end + this.data.viewport.windowHeight}px 0px ${\r\n start + this.data.viewport.windowHeight\r\n }px 0px`,\r\n threshold: 0.001,\r\n });\r\n\r\n const inviewObserver = new IntersectionObserver(inviewCallback, {\r\n root: null,\r\n rootMargin: `${end + inviewTop}px 0px ${start + inviewBottom}px 0px`,\r\n threshold: 0.001,\r\n });\r\n\r\n progressObserver.observe(el);\r\n inviewObserver.observe(el);\r\n\r\n obj.setProperty(\"observer-progress\", progressObserver);\r\n obj.setProperty(\"observer-inview\", inviewObserver);\r\n }\r\n\r\n /**\r\n * Observes DOM mutations to auto-add/remove elements with [string] attribute.\r\n * Should be called once after DOM is ready.\r\n */\r\n public observeDOM(): void {\r\n const observer = new MutationObserver((mutations) => {\r\n mutations.forEach((mutation) => {\r\n if (mutation.type === \"childList\") {\r\n // Removed elements\r\n mutation.removedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (element.hasAttribute(\"string\")) {\r\n this.handleRemoved(element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\"[string],[data-string]\")\r\n .forEach((child) => {\r\n if (this.isFixed(child as HTMLElement)) return;\r\n this.handleRemoved(child as HTMLElement);\r\n });\r\n });\r\n\r\n // Added elements\r\n mutation.addedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (\r\n element.hasAttribute(\"string\") &&\r\n !element.hasAttribute(\"string-inited\")\r\n ) {\r\n this.add(element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\r\n \"[string]:not([string-inited]),[data-string]:not([string-inited])\"\r\n )\r\n .forEach((child) => this.add(child as HTMLElement));\r\n\r\n // Check for connect-from logic\r\n const copyFrom =\r\n element.getAttribute(\"string-copy-from\") ??\r\n element.getAttribute(\"data-string-copy-from\");\r\n if (copyFrom) {\r\n if (this.objects.has(copyFrom)) {\r\n this.objects.get(copyFrom)!.connects.push(element);\r\n } else {\r\n this.enqueueConnection(copyFrom, element);\r\n }\r\n }\r\n });\r\n\r\n // Let modules know about DOM rebuild\r\n this.modules.all.forEach((m) => m.onDOMRebuild());\r\n }\r\n });\r\n });\r\n\r\n observer.observe(document.body, {\r\n childList: true,\r\n subtree: true,\r\n });\r\n }\r\n\r\n /**\r\n * Removes an object and its observers.\r\n */\r\n private handleRemoved(el: HTMLElement): void {\r\n const id = el.getAttribute(\"string-id\");\r\n if (!id) return;\r\n\r\n const copyFrom =\r\n el.getAttribute(\"string-copy-from\") ??\r\n el.getAttribute(\"data-string-copy-from\");\r\n if (copyFrom) {\r\n this.connectQueue = this.connectQueue.filter((q) => q.id !== copyFrom);\r\n }\r\n\r\n this.remove(id);\r\n }\r\n\r\n /**\r\n * Re-applies module initialization logic to all managed objects after settings change.\r\n *\r\n * This method should be called when `StringSettings` are updated at runtime,\r\n * especially if the new settings affect how modules calculate offsets,\r\n * easing, origins, or custom configuration.\r\n *\r\n * Internally, it re-runs `initializeObject`, `calculatePositions`, and `connectObject`\r\n * for each core module that can connect to the object.\r\n *\r\n * This is useful for supporting dynamic configuration updates without requiring\r\n * a full DOM rebuild or reinitialization.\r\n */\r\n public onSettingsChange() {\r\n this.objects.forEach((object) => {\r\n this.modules.core.forEach((m) => {\r\n if (m.canConnect(object)) {\r\n const attributes = this.getAllAttributes(object.htmlElement);\r\n m.initializeObject(\r\n this.globalId,\r\n object,\r\n object.htmlElement,\r\n attributes\r\n );\r\n m.calculatePositions(object, this.data.viewport.windowHeight);\r\n m.connectObject(object);\r\n }\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Checks whether the element is marked as fixed (not managed).\r\n */\r\n private isFixed(el: HTMLElement): boolean {\r\n return el.hasAttribute(\"string-fixed\");\r\n }\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\n\r\n/**\r\n * Base class for managing scroll behavior in the system.\r\n * Handles abstract scroll state and updates, intended for extension.\r\n */\r\nexport class ScrollController {\r\n /** Shared context containing data and tools */\r\n protected context: StringContext;\r\n\r\n /** Reference to the document object */\r\n protected document: Document;\r\n\r\n /** Name of the scroll mode (e.g. 'default', 'smooth', etc.) */\r\n public name: string = \"\";\r\n\r\n /** Whether the system is in programmatic scroll mode */\r\n public isProg: boolean = false;\r\n\r\n /** Whether parallax-related logic should be active */\r\n public isParallaxEnabled: boolean = false;\r\n\r\n /** Scroll direction: vertical or horizontal */\r\n protected _scrollDirection: \"vertical\" | \"horizontal\" = \"vertical\";\r\n\r\n /**\r\n * Sets scroll direction and updates internal scroll logic.\r\n * @param scrollDirection Either 'vertical' or 'horizontal'.\r\n */\r\n public set scrollDirection(scrollDirection: \"vertical\" | \"horizontal\") {\r\n this._scrollDirection = scrollDirection;\r\n\r\n if (this._scrollDirection === \"vertical\") {\r\n this.onCalcUpdate = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(\r\n 0,\r\n this.context.data.scroll.current\r\n );\r\n // this.scrollContainer.scrollTo(0, this.context.data.scroll.current);\r\n };\r\n } else if (this._scrollDirection === \"horizontal\") {\r\n this.onCalcUpdate = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(\r\n this.context.data.scroll.current,\r\n 0\r\n );\r\n // this.scrollContainer.scrollTo(this.context.data.scroll.current, 0);\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Creates a new ScrollController instance.\r\n * @param context Shared context containing data and settings.\r\n */\r\n constructor(context: StringContext) {\r\n this.document = document;\r\n this.context = context;\r\n }\r\n\r\n /**\r\n * Called when scroll direction changes (up ↔ down).\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onChangeDirection = () => {};\r\n\r\n /**\r\n * Called when scroll starts (user input).\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onScrollStart = () => {};\r\n\r\n /**\r\n * Called when scroll ends.\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onScrollStop = () => {};\r\n\r\n /**\r\n * Scroll-to function called on each frame.\r\n * This will be reassigned depending on scroll direction.\r\n */\r\n public onCalcUpdate: () => void = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(\r\n 0,\r\n this.context.data.scroll.current\r\n );\r\n };\r\n\r\n /**\r\n * Called every animation frame.\r\n * Intended to be overridden in subclasses.\r\n */\r\n public onFrame(): void {}\r\n\r\n /**\r\n * Called when wheel event is fired.\r\n * Override to implement custom scroll interaction.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: any): void {}\r\n\r\n /**\r\n * Called when native scroll event is fired.\r\n * Override to track native scroll position.\r\n * @param e Scroll event.\r\n */\r\n public onScroll(e: any): void {}\r\n\r\n public disableScrollEvents(): void {}\r\n\r\n public enableScrollEvents(): void {}\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\nimport { ScrollController } from \"./ScrollController\";\r\n\r\n/**\r\n * Default scroll controller using native browser scrolling behavior.\r\n * Handles `scrollTop`, easing delta over time for smooth lerped animations.\r\n */\r\nexport class StringScrollDefault extends ScrollController {\r\n /** Unique name identifier for this scroll mode. */\r\n public readonly name: string = 'default';\r\n\r\n /**\r\n * Constructs a new instance of the default scroll controller.\r\n * @param context Shared string system context.\r\n */\r\n constructor(context: StringContext) {\r\n super(context);\r\n }\r\n\r\n /**\r\n * Called every animation frame.\r\n * Applies easing to scroll delta and updates lerped value.\r\n * Fires `onScrollStop` once movement has settled.\r\n */\r\n public onFrame(): void {\r\n if (this.context.data.scroll.delta !== 0) {\r\n const delta = this.context.data.scroll.delta * this.context.data.scroll.speedAccelerate;\r\n this.context.data.scroll.delta -= delta;\r\n this.context.data.scroll.lerped = delta;\r\n\r\n if (Math.abs(this.context.data.scroll.lerped) < 0.1) {\r\n this.context.data.scroll.delta = 0;\r\n this.context.data.scroll.lerped = 0;\r\n this.onScrollStop();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Called on native scroll event.\r\n * Syncs the internal scroll state with the current page scroll.\r\n * @param e Scroll event object (unused).\r\n */\r\n public onScroll(e: any): void {\r\n const scrollTop = this.context.data.scroll.elementContainer.scrollTop;\r\n this.context.data.scroll.current = scrollTop;\r\n this.context.data.scroll.target = scrollTop;\r\n this.context.data.scroll.transformedCurrent = scrollTop\r\n }\r\n\r\n /**\r\n * Handles wheel input by updating scroll delta for easing.\r\n * Triggers `onScrollStart` if delta begins from zero.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: any): void {\r\n if (e.deltaY !== 0) {\r\n if (this.context.data.scroll.delta === 0) {\r\n this.onScrollStart();\r\n }\r\n\r\n const plusDelta = e.deltaY;\r\n\r\n // Prevent negative delta from triggering at top of scroll\r\n if (this.context.data.scroll.target === 0) {\r\n this.context.data.scroll.delta += Math.max(0, e.deltaY);\r\n }\r\n\r\n this.context.data.scroll.delta += plusDelta;\r\n }\r\n }\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\nimport { ScrollController } from \"./ScrollController\";\r\n\r\n/**\r\n * Scroll controller that disables all user-initiated scrolling.\r\n * Prevents native scroll and wheel behavior, effectively locking the page.\r\n */\r\nexport class StringScrollDisable extends ScrollController {\r\n /** Unique name identifier for this scroll mode. */\r\n public readonly name: string = \"disable\";\r\n\r\n /**\r\n * Constructs the scroll disabling controller.\r\n * @param context The shared string system context.\r\n */\r\n\r\n private preventScroll = (e: Event) => {\r\n e.preventDefault();\r\n };\r\n\r\n private preventKeyScroll = (e: KeyboardEvent) => {\r\n const keysThatScroll = [\r\n \"ArrowUp\",\r\n \"ArrowDown\",\r\n \"PageUp\",\r\n \"PageDown\",\r\n \" \",\r\n \"Home\",\r\n \"End\",\r\n ];\r\n if (keysThatScroll.includes(e.key)) {\r\n e.preventDefault();\r\n }\r\n };\r\n\r\n private onPreventScroll = this.preventScroll.bind(this);\r\n private onPreventKeyScroll = this.preventKeyScroll.bind(this);\r\n\r\n constructor(context: StringContext) {\r\n super(context);\r\n }\r\n\r\n disableScrollEvents() {\r\n window.addEventListener(\"touchmove\", this.onPreventScroll, {\r\n passive: false,\r\n });\r\n window.addEventListener(\"keydown\", this.onPreventKeyScroll);\r\n }\r\n\r\n enableScrollEvents() {\r\n window.removeEventListener(\"touchmove\", this.onPreventScroll);\r\n window.removeEventListener(\"keydown\", this.onPreventKeyScroll);\r\n }\r\n\r\n /**\r\n * Called on each animation frame.\r\n * Not used in this controller since scrolling is disabled.\r\n */\r\n public onFrame(): void {}\r\n\r\n /**\r\n * Prevents scroll via mouse wheel.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: any): void {\r\n e.preventDefault();\r\n }\r\n\r\n /**\r\n * Prevents scroll via native scroll interaction (e.g. touch).\r\n * @param e Scroll event.\r\n */\r\n public onScroll(e: any): void {\r\n e.preventDefault();\r\n }\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\nimport { ScrollController } from \"./ScrollController\";\r\n\r\n/**\r\n * CSS class names applied to `<html>` element based on scroll direction.\r\n */\r\nconst CLASS_NAMES = {\r\n SCROLL_FORWARD: '-scroll-forward',\r\n SCROLL_BACK: '-scroll-back',\r\n} as const;\r\n\r\n/**\r\n * Smooth scroll controller with delta-based acceleration and direction tracking.\r\n * Handles wheel events and scroll position updates with smooth interpolation.\r\n */\r\nexport class StringScrollSmooth extends ScrollController {\r\n /** \r\n * Unique identifier for this scroll controller type.\r\n * Used for selection and debug purposes.\r\n */\r\npublic readonly name: string = 'smooth';\r\n\r\n/** \r\n * Whether the user has manually scrolled using the native scrollbar.\r\n * When true, the scroll position is directly synced from DOM on next frame.\r\n */\r\nprivate isScrollbarManipulation = false;\r\n\r\n/** \r\n * Current internal force applied to the scroll target.\r\n * Calculated from accumulated scroll impulse and acceleration.\r\n */\r\nprivate scrollForce: number = 0;\r\n\r\n/** \r\n * Latest raw scroll impulse received from the wheel event (`deltaY`).\r\n * Temporarily stored for direction and boundary checks.\r\n */\r\nprivate wheelImpulse: number = 0;\r\n\r\n/** \r\n * Scroll position from the previous frame.\r\n * Used to detect actual movement and trigger updates only when needed.\r\n */\r\nprivate previousCurrent: number = 0;\r\n\r\n/** \r\n * Current scroll direction.\r\n * - `true` — scrolling down\r\n * - `false` — scrolling up\r\n * - `null` — unknown (initial state)\r\n */\r\nprivate isBottomScrollDirection: boolean | null = null;\r\n\r\n/** \r\n * Minimum velocity threshold below which scrolling is considered stopped.\r\n * Prevents micro-movements from continuing infinite smooth scroll.\r\n */\r\nprivate readonly velocityThreshold = 0.1;\r\n\r\n\r\n constructor(context: StringContext) {\r\n super(context);\r\n }\r\n\r\n /**\r\n * Handles scroll direction changes and toggles CSS classes accordingly.\r\n * @param newDirection `true` if scrolling down, `false` if up.\r\n */\r\n private updateScrollDirection(newDirection: boolean) {\r\n if (this.isBottomScrollDirection === null) {\r\n this.isBottomScrollDirection = newDirection;\r\n return;\r\n }\r\n this.context.data.scroll.isScrollingDown = newDirection;\r\n this.onChangeDirection();\r\n\r\n document.documentElement.classList.toggle(CLASS_NAMES.SCROLL_FORWARD, newDirection);\r\n document.documentElement.classList.toggle(CLASS_NAMES.SCROLL_BACK, !newDirection);\r\n }\r\n\r\n /**\r\n * Immediately stops scrolling and resets all deltas and directions.\r\n */\r\n public stopScroll(): void {\r\n this.context.data.scroll.lerped = 0;\r\n this.context.data.scroll.delta = 0;\r\n this.context.data.scroll.target = this.context.data.scroll.current;\r\n this.isProg = false;\r\n this.onCalcUpdate();\r\n document.documentElement.classList.remove(CLASS_NAMES.SCROLL_BACK, CLASS_NAMES.SCROLL_FORWARD);\r\n this.isBottomScrollDirection = null;\r\n }\r\n\r\n /**\r\n * Called on each animation frame to apply lerped scroll logic.\r\n */\r\n public onFrame(): void {\r\n if (this.isScrollbarManipulation) {\r\n this.isScrollbarManipulation = false;\r\n this.context.data.scroll.current = this.context.data.scroll.elementContainer.scrollTop\r\n this.context.data.scroll.target = this.context.data.scroll.elementContainer.scrollTop\r\n this.context.data.scroll.transformedCurrent = this.context.data.scroll.current * this.context.data.viewport.transformScale\r\n return;\r\n }\r\n\r\n \r\n if (this.context.data.scroll.delta !== 0) {\r\n this.scrollForce = this.context.data.scroll.delta * this.context.data.scroll.speedAccelerate;\r\n\r\n this.context.data.scroll.target = Math.min(\r\n Math.max(0, this.context.data.scroll.target + this.scrollForce),\r\n this.context.data.scroll.bottomPosition\r\n );\r\n this.context.data.scroll.delta -= this.scrollForce;\r\n\r\n this.context.data.scroll.lerped = \r\n (this.context.data.scroll.target - this.context.data.scroll.current) * \r\n this.context.data.scroll.speed;\r\n\r\n const absVelocity = Math.abs(this.context.data.scroll.lerped);\r\n if (this.context.data.scroll.lerped > 0) {\r\n this.context.data.scroll.current = Math.ceil(this.context.data.scroll.current + this.context.data.scroll.lerped);\r\n } else {\r\n this.context.data.scroll.current = Math.floor(this.context.data.scroll.current + this.context.data.scroll.lerped);\r\n }\r\n \r\n this.context.data.scroll.transformedCurrent = this.context.data.scroll.current * this.context.data.viewport.transformScale\r\n this.updateScrollDirection(this.context.data.scroll.lerped > 0);\r\n\r\n if (absVelocity < this.velocityThreshold) {\r\n this.stopScroll();\r\n this.onScrollStop();\r\n } else {\r\n this.isProg = true;\r\n if (this.previousCurrent !== this.context.data.scroll.current) {\r\n this.previousCurrent = this.context.data.scroll.current;\r\n this.onCalcUpdate();\r\n }\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Handles user wheel input to accumulate scroll delta.\r\n * @param e The WheelEvent from the browser.\r\n */\r\n public onWheel(e: WheelEvent): void {\r\n if (e.deltaY !== 0) {\r\n e.preventDefault();\r\n }\r\n\r\n this.wheelImpulse = e.deltaY;\r\n if (this.wheelImpulse === 0) return;\r\n\r\n if (this.context.data.scroll.delta === 0) {\r\n this.onScrollStart();\r\n }\r\n \r\n const scrollDirection = Math.sign(this.wheelImpulse);\r\n const atTop = this.context.data.scroll.target === 0 && scrollDirection < 0;\r\n const atBottom = this.context.data.scroll.target === this.context.data.scroll.bottomPosition && scrollDirection > 0;\r\n if (atTop || atBottom) return;\r\n this.context.data.scroll.delta += this.wheelImpulse;\r\n }\r\n\r\n /**\r\n * Detects native scrollbar manipulation when smooth scroll is idle.\r\n * @param e Scroll event.\r\n */\r\n public onScroll(e: Event): void {\r\n if (!this.isProg) {\r\n this.isScrollbarManipulation = true;\r\n }\r\n }\r\n}\r\n","import { ScrollMode } from \"../../states/ScrollState\";\r\nimport { ScrollController } from \"../controllers/ScrollController\";\r\nimport { StringScrollDefault } from \"../controllers/StringScrollDefault\";\r\nimport { StringScrollDisable } from \"../controllers/StringScrollDisable\";\r\nimport { StringScrollSmooth } from \"../controllers/StringScrollSmooth\";\r\nimport { StringContext } from \"../StringContext\";\r\n\r\n/**\r\n * Handles scroll engine setup and switching logic.\r\n * Synchronizes scroll modes with the centralized ScrollState inside StringContext.\r\n */\r\nexport class ScrollManager {\r\n private modes: Map<ScrollMode, ScrollController> = new Map();\r\n\r\n constructor(private context: StringContext) {\r\n this.modes.set(\"smooth\", new StringScrollSmooth(context));\r\n this.modes.set(\"default\", new StringScrollDefault(context));\r\n this.modes.set(\"disable\", new StringScrollDisable(context));\r\n\r\n // Initial mode based on screen width\r\n this.updateResponsiveMode();\r\n }\r\n\r\n /**\r\n * Manually sets the scroll mode for mobile devices.\r\n * @param mode The scroll mode: 'smooth', 'default', or 'disable'.\r\n */\r\n public setMobileMode(mode: ScrollMode): void {\r\n this.context.data.scroll.modeMobile = mode;\r\n this.updateResponsiveMode();\r\n }\r\n\r\n /**\r\n * Manually sets the scroll mode for desktop devices.\r\n * @param mode The scroll mode: 'smooth', 'default', or 'disable'.\r\n */\r\n public setDesktopMode(mode: ScrollMode): void {\r\n this.context.data.scroll.modeDesktop = mode;\r\n this.updateResponsiveMode();\r\n }\r\n\r\n /**\r\n * Automatically switches scroll mode based on screen width.\r\n * Call this inside your resize handler.\r\n */\r\n public updateResponsiveMode(): void {\r\n const isMobile = window.innerWidth < 1080;\r\n const newMode = isMobile\r\n ? this.context.data.scroll.modeMobile\r\n : this.context.data.scroll.modeDesktop;\r\n\r\n this.setMode(newMode);\r\n }\r\n\r\n public updatePosition(): void {\r\n this.modes.forEach((engine) => {\r\n engine.onCalcUpdate();\r\n });\r\n }\r\n\r\n /**\r\n * Sets the current scroll mode and updates the active scroll engine.\r\n * @param mode The scroll mode to activate.\r\n */\r\n public setMode(mode: ScrollMode): void {\r\n if (!this.modes.has(mode)) {\r\n console.warn(`[ScrollManager] Unknown scroll mode: ${mode}`);\r\n return;\r\n }\r\n this.get().enableScrollEvents();\r\n this.context.data.scroll.mode = mode;\r\n this.get().disableScrollEvents();\r\n }\r\n\r\n /**\r\n * Returns the currently active scroll engine based on state.\r\n */\r\n public get(): ScrollController {\r\n return this.modes.get(this.context.data.scroll.mode)!;\r\n }\r\n\r\n /**\r\n * Returns all available scroll engine instances.\r\n */\r\n public getEngines(): Map<ScrollMode, ScrollController> {\r\n return this.modes;\r\n }\r\n\r\n /**\r\n * Calls `onFrame()` on the current scroll engine.\r\n */\r\n public onFrame(): void {\r\n this.get().onFrame();\r\n }\r\n\r\n /**\r\n * Forwards native scroll event to the current scroll engine.\r\n * @param e The scroll event.\r\n */\r\n public onScroll(e: Event): void {\r\n this.get().onScroll(e);\r\n }\r\n\r\n /**\r\n * Forwards wheel event to the current scroll engine.\r\n * @param e The wheel event.\r\n */\r\n public onWheel(e: WheelEvent): void {\r\n this.get().onWheel(e);\r\n }\r\n\r\n /**\r\n * Subscribes lifecycle event handlers to all scroll engines.\r\n * @param events Scroll lifecycle callbacks.\r\n */\r\n public bindEvents(events: {\r\n onScrollStart: () => void;\r\n onScrollStop: () => void;\r\n onDirectionChange: () => void;\r\n }) {\r\n this.modes.forEach((engine) => {\r\n engine.onScrollStart = events.onScrollStart;\r\n engine.onScrollStop = events.onScrollStop;\r\n engine.onChangeDirection = events.onDirectionChange;\r\n });\r\n }\r\n}\r\n","/**\r\n * Reactive cursor data for raw, target, smoothed and step deltas.\r\n */\r\nexport class CursorState {\r\n /**\r\n * Target X position of the cursor (e.g., from `mousemove`)\r\n */\r\n targetX: number = 0\r\n\r\n /**\r\n * Target Y position of the cursor.\r\n */\r\n targetY: number = 0\r\n\r\n /**\r\n * Smoothed X position after applying lerp.\r\n */\r\n smoothedX: number = 0\r\n\r\n /**\r\n * Smoothed Y position after applying lerp.\r\n */\r\n smoothedY: number = 0\r\n\r\n /**\r\n * Delta step between current and target X (used internally for lerp).\r\n */\r\n stepX: number = 0\r\n\r\n /**\r\n * Delta step between current and target Y.\r\n */\r\n stepY: number = 0\r\n}\r\n","/**\r\n * Global Three.js or rendering context reference.\r\n */\r\nexport class RenderState {\r\n /** Instance of Three.js or another render context */\r\n threeInstance: any = null\r\n}\r\n","export type ScrollDirection = 'vertical' | 'horizontal'\r\nexport type ScrollMode = 'smooth' | 'disable' | 'default'\r\n\r\n/**\r\n * Describes current scroll-related state for all calculations and modules.\r\n */\r\nexport class ScrollState {\r\n /** Target scroll value — where we want to scroll to (used in smooth scroll) */\r\n target: number = 0\r\n\r\n /** Current scroll value (actual scroll position) */\r\n current: number = 0\r\n\r\n /** Transformed current scroll value (with transform by scroll container) */\r\n transformedCurrent: number = 0\r\n\r\n /** Delta between frames (used for animation / velocity) */\r\n delta: number = 0\r\n\r\n /** Interpolated scroll value for smooth transitions */\r\n lerped: number = 0\r\n\r\n /** Displacement value (similar to lerped, but used for other animations) */\r\n displacement: number = 0\r\n\r\n /** Whether scroll direction is downward */\r\n isScrollingDown: boolean = false\r\n\r\n /** Top screen scroll position */\r\n topPosition: number = 0\r\n\r\n /** Bottom screen scroll position */\r\n bottomPosition: number = 0\r\n\r\n /** Scroll direction (vertical / horizontal) */\r\n direction: ScrollDirection = 'vertical'\r\n\r\n /** Scroll container element */\r\n elementContainer: HTMLElement = document.documentElement\r\n\r\n /** Scroll container element */\r\n scrollContainer: HTMLElement | Window = wind