@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 • 1.72 MB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/core/controllers/CursorController.ts","../src/core/managers/EventManager.ts","../src/core/managers/ModuleManager.ts","../src/objects/StringMirrorObject.ts","../src/objects/StringObject.ts","../src/core/managers/DOMBatcher.ts","../src/models/IModuleLifecyclePermissions.ts","../src/core/StringModule.ts","../src/core/managers/ObjectManager.ts","../src/models/scroll/ScrollHTMLClass.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/SystemState.ts","../src/states/TimeState.ts","../src/states/ViewportState.ts","../src/core/StringData.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/EasingFunctionTool.ts","../src/tools/MagneticPullTool.ts","../src/tools/LerpColorTool.ts","../src/tools/LerpVector2Tool.ts","../src/tools/TransformScaleParserTool.ts","../src/tools/SplitOptionsParserTool.ts","../src/tools/RuleParserTool.ts","../src/tools/ValidationTool.ts","../src/utils/style-txn.ts","../src/core/StringToolsContainer.ts","../src/utils/isCoarsePointer.ts","../src/modules/cursor/StringCursor.ts","../src/core/physics/PhysicsBody.ts","../src/core/physics/forces/SpringForce.ts","../src/modules/cursor/StringImpulse.ts","../src/modules/cursor/StringAttractor.ts","../src/utils/frame-dom.ts","../src/utils/pointerDrag.ts","../src/modules/layout/StringMarquee.ts","../src/modules/layout/StringMasonry.ts","../src/modules/cursor/StringMagnetic.ts","../src/modules/cursor/CursorReactiveModule.ts","../src/modules/cursor/StringSpotlight.ts","../src/modules/cursor/StringTilt.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/models/text/SplitElementClass.ts","../src/utils/text/BuildDOMTree.ts","../src/utils/text/BuildTokens.ts","../src/utils/text/CanvasKerningApplier.ts","../src/utils/text/layout-measure/FlexMeasureAdapter.ts","../src/utils/text/layout-measure/InlineFlowMeasureAdapter.ts","../src/utils/text/layout-measure/LayoutMeasureRouter.ts","../src/utils/text/TextSegmentation.ts","../src/utils/text/layout-measure/measureTokens.ts","../src/utils/text/LayoutMeasurer.ts","../src/utils/text/SplitMeasuredTokens.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/models/slider/SequenceState.ts","../src/modules/slider/StringSequence.ts","../src/modules/input/StringForm.ts","../src/core/managers/ScrollScopeRegistry.ts","../src/core/managers/CenterCache.ts","../src/core/managers/HoverTracker.ts","../src/core/managers/StringSignalHub.ts","../src/modules/dev-tools/core/StringDevtoolsIcons.ts","../src/modules/dev-tools/core/StringDevIconRegistry.ts","../src/modules/dev-tools/core/StringDevElements.ts","../src/modules/dev-tools/core/StringDevCoreStyles.css.ts","../src/modules/dev-tools/core/StringDevStyleSystem.ts","../src/modules/dev-tools/core/StringDevViewportPolicy.ts","../src/modules/dev-tools/core/StringDevStorageScope.ts","../src/modules/dev-tools/core/StringDevtoolsDock.ts","../src/core/managers/DevtoolsManager.ts","../src/modules/scroll/StringScroller.ts","../src/modules/scroll/StringScrollContainer.ts","../src/utils/ParsePartOf.ts","../src/modules/scroll/StringProgressPart.ts","../src/utils/ParseSignalRules.ts","../src/modules/signal/StringSignal.ts","../src/modules/random/StringRandom.ts","../src/modules/dev-tools/core/StringDevViewportLayer.ts","../src/modules/dev-tools/core/StringDevOverlayRegistry.ts","../src/modules/dev-tools/core/StringDevModule.ts","../src/modules/dev-tools/layout/models/GridInstance.ts","../src/modules/dev-tools/layout/GridManager.ts","../src/modules/dev-tools/layout/GridOverlay.ts","../src/modules/dev-tools/core/ui/fields/StringDevUIField.ts","../src/modules/dev-tools/core/ui/fields/StringDevFieldNumber.ts","../src/modules/dev-tools/core/ui/fields/StringDevFieldRange.ts","../src/modules/dev-tools/core/ui/fields/StringDevFieldColor.ts","../src/modules/dev-tools/core/ui/fields/StringDevFieldSelect.ts","../src/modules/dev-tools/core/ui/fields/StringDevFieldToggle.ts","../src/modules/dev-tools/core/ui/StringDevUIBuilder.ts","../src/modules/dev-tools/layout/GridHUD.ts","../src/modules/dev-tools/layout/GridSerializer.ts","../src/modules/dev-tools/layout/adapters/GridAdapter.ts","../src/modules/dev-tools/layout/adapters/ColumnsAdapter.ts","../src/modules/dev-tools/layout/adapters/RowsAdapter.ts","../src/modules/dev-tools/layout/adapters/CenterAdapter.ts","../src/modules/dev-tools/layout/adapters/RuleOfThirdsAdapter.ts","../src/modules/dev-tools/layout/adapters/GoldenRectangleAdapter.ts","../src/modules/dev-tools/layout/adapters/DotGridAdapter.ts","../src/modules/dev-tools/core/StringDevtoolsOverlayLayout.ts","../src/modules/dev-tools/layout/StringGrid.css.ts","../src/modules/dev-tools/layout/StringGrid.ts","../src/modules/dev-tools/rulers/StringRulers.css.ts","../src/modules/dev-tools/rulers/models/RulerLine.ts","../src/modules/dev-tools/rulers/RulersManager.ts","../src/modules/dev-tools/rulers/SnapEngine.ts","../src/modules/dev-tools/rulers/models/RulerMode.ts","../src/utils/interaction-lock.ts","../src/modules/dev-tools/rulers/RulersOverlay.ts","../src/modules/dev-tools/core/StringDevPersistedState.ts","../src/modules/dev-tools/rulers/StringRulers.ts","../src/modules/dev-tools/core/StringDevOverlayModule.ts","../src/modules/dev-tools/core/StringDevBadgeOverlayModule.ts","../src/modules/dev-tools/inview/StringDevInview.css.ts","../src/modules/dev-tools/inview/StringDevInview.ts","../src/modules/dev-tools/core/bindOutsideClick.ts","../src/modules/dev-tools/progress/StringDevProgress.css.ts","../src/modules/dev-tools/progress/StringDevProgress.ts","../src/models/devtools/StringDevtool.ts"],"sourcesContent":["import { CursorController } from \"./core/controllers/CursorController\";\r\nimport { IStringModule } from \"./core/IStringModule\";\r\nimport { EventManager } from \"./core/managers/EventManager\";\r\nimport { ModuleManager } from \"./core/managers/ModuleManager\";\r\nimport { ObjectManager } from \"./core/managers/ObjectManager\";\r\nimport { ScrollManager } from \"./core/managers/ScrollManager\";\r\nimport { StringContext } from \"./core/StringContext\";\r\nimport { StringData } from \"./core/StringData\";\r\nimport { StringModule } from \"./core/StringModule\";\r\nimport { DefaultToolsContainer } from \"./core/StringToolsContainer\";\r\nimport { StringCursor } from \"./modules/cursor/StringCursor\";\r\nimport { StringImpulse } from \"./modules/cursor/StringImpulse\";\r\nimport { StringAttractor } from \"./modules/cursor/StringAttractor\";\r\nimport { StringMarquee } from \"./modules/layout/StringMarquee\";\r\nimport { StringMasonry } from \"./modules/layout/StringMasonry\";\r\nimport { StringMagnetic } from \"./modules/cursor/StringMagnetic\";\r\nimport { StringSpotlight } from \"./modules/cursor/StringSpotlight\";\r\nimport { StringTilt } from \"./modules/cursor/StringTilt\";\r\nimport { StringVelocity } from \"./modules/cursor/StringVelocity\";\r\nimport { StringLazy } from \"./modules/loading/StringLazy\";\r\nimport { StringLoading } from \"./modules/loading/StringLoading\";\r\nimport { StringInview } from \"./modules/screen/StringInview\";\r\nimport { StringResponsive } from \"./modules/screen/StringResponsive\";\r\nimport { StringAnchor } from \"./modules/scroll/StringAnchor\";\r\nimport { StringGlide } from \"./modules/scroll/StringGlide\";\r\nimport { StringLerp } from \"./modules/scroll/StringLerp\";\r\nimport { StringParallax } from \"./modules/scroll/StringParallax\";\r\nimport { StringProgress } from \"./modules/scroll/StringProgress\";\r\nimport { StringScrollbar } from \"./modules/scrollbar/StringScrollbar\";\r\nimport { StringCircularText } from \"./modules/text/StringCircularText\";\r\nimport { StringSplit } from \"./modules/text/StringSplit\";\r\nimport { StringDelayLerpTracker } from \"./modules/tracker/StringDelayLerpTracker\";\r\nimport { StringFPSTracker } from \"./modules/tracker/StringFPSTracker\";\r\nimport { StringLerpTracker } from \"./modules/tracker/StringLerpTracker\";\r\nimport { StringPositionTracker } from \"./modules/tracker/StringPositionTracker\";\r\nimport { StringObject } from \"./objects/StringObject\";\r\nimport { ScrollMode } from \"./states/ScrollState\";\r\nimport { Debounce } from \"./utils/Debounce\";\r\nimport { EventCallback } from \"./models/event/EventCallback\";\r\nimport { StringFPS } from \"./utils/StringFPS\";\r\nimport { StringSettings } from \"./utils/StringSettings\";\r\nimport { StringVideoAutoplay } from \"./modules/loading/StringVideoAutoplay\";\r\nimport { ScrollMarkRule } from \"./models/scroll/ScrollTriggerRule\";\r\nimport { StringSequence } from \"./modules/slider/StringSequence\";\r\nimport { StringForm } from \"./modules/input/StringForm\";\r\nimport { ISettingsChangeData } from \"./models/event/ISettingsChangeData\";\r\nimport { CenterCache } from \"./core/managers/CenterCache\";\r\nimport { HoverTracker } from \"./core/managers/HoverTracker\";\r\nimport { ScrollScopeRegistry } from \"./core/managers/ScrollScopeRegistry\";\r\nimport { StringSignalHub } from \"./core/managers/StringSignalHub\";\r\nimport { DevtoolsManager } from \"./core/managers/DevtoolsManager\";\r\nimport { CursorReactiveModule } from \"./modules/cursor/CursorReactiveModule\";\r\nimport { StringScroller } from \"./modules/scroll/StringScroller\";\r\nimport { StringScrollContainer } from \"./modules/scroll/StringScrollContainer\";\r\nimport { parsePartOf } from \"./utils/ParsePartOf\";\r\nimport { StringProgressPart } from \"./modules/scroll/StringProgressPart\";\r\nimport { StringSignal } from \"./modules/signal/StringSignal\";\r\nimport { frameDOM } from \"./utils/frame-dom\";\r\nimport { styleTxn } from \"./utils/style-txn\";\r\nimport { startPointerDrag, type PointerDragHandlers, type PointerDragDelta } from \"./utils/pointerDrag\";\r\nimport { StringRandom } from \"./modules/random/StringRandom\";\r\nimport { ScrollController } from \"./core/controllers/ScrollController\";\r\n// import { logScrollDebug } from \"./utils/scroll-debug\";\r\nimport { StringDevLayout } from \"./modules/dev-tools/layout/StringGrid\";\r\nimport { GridAdapter } from \"./modules/dev-tools/layout/adapters/GridAdapter\";\r\nimport { StringDevRulers } from \"./modules/dev-tools/rulers/StringRulers\";\r\nimport { StringDevInview } from \"./modules/dev-tools/inview/StringDevInview\";\r\nimport { StringDevProgress } from \"./modules/dev-tools/progress/StringDevProgress\";\r\nimport { StringDevModule } from \"./modules/dev-tools/core/StringDevModule\";\r\nimport { StringDevOverlayRegistry } from \"./modules/dev-tools/core/StringDevOverlayRegistry\";\r\nimport { setStringDevStorageScopeToken } from \"./modules/dev-tools/core/StringDevStorageScope\";\r\nimport {\r\n StringDevIconRegistry,\r\n resolveDevtoolsIcon,\r\n} from \"./modules/dev-tools/core/StringDevIconRegistry\";\r\nimport {\r\n buildDevtoolsThemeBlock,\r\n ensureStringDevtoolsSharedStyles,\r\n type StringDevStyleTokens,\r\n} from \"./modules/dev-tools/core/StringDevStyleSystem\";\r\n// import {\r\n// StringInfiniteVirtual,\r\n// StringInfiniteVirtualOptions,\r\n// StringInfiniteVirtualReachEvent,\r\n// StringInfiniteVirtualSnapshot,\r\n// StringVirtualAlign,\r\n// StringVirtualAxis,\r\n// StringVirtualItem,\r\n// StringVirtualKey,\r\n// StringVirtualRange,\r\n// } from \"./modules/virtual-scroll/StringInfiniteVirtual\";\r\n// import {\r\n// StringInfiniteVirtualDOM,\r\n// StringInfiniteVirtualDOMOptions,\r\n// } from \"./modules/virtual-scroll/StringInfiniteVirtualDOM\";\r\n// import {\r\n// StringVirtualScroll,\r\n// StringVirtualScrollItemEventPayload,\r\n// StringVirtualScrollSetDataPayload,\r\n// StringVirtualScrollSetFilterPayload,\r\n// } from \"./modules/virtual-scroll/StringVirtualScroll\";\r\n// import {\r\n// StringFilterEngine,\r\n// StringFilterExpression,\r\n// StringFilterResult,\r\n// } from \"./modules/virtual-scroll/filter/StringFilterEngine\";\r\n// import {\r\n// createFilterSchema,\r\n// StringFilterFieldDefinition,\r\n// StringFilterFieldKind,\r\n// StringFilterIndexKind,\r\n// StringFilterPrimitive,\r\n// StringFilterSchema,\r\n// } from \"./modules/virtual-scroll/filter/StringFilterSchema\";\r\n// import {\r\n// createFilteredVirtualDataset,\r\n// StringFilteredVirtualDataset,\r\n// StringFilteredVirtualDatasetItem,\r\n// StringFilteredVirtualDatasetOptions,\r\n// } from \"./modules/virtual-scroll/filter/StringFilteredVirtualDataset\";\r\n// import { StringBitset } from \"./modules/virtual-scroll/filter/StringBitset\";\r\nimport type {\r\n StringRulersTrigger,\r\n RulersTriggerAction,\r\n} from \"./modules/dev-tools/rulers/models/StringRulersTrigger\";\r\nimport type { RulersLayoutGrid } from \"./modules/dev-tools/rulers/models/RulersLayoutGrid\";\r\nimport { isStringDevtoolProvider } from \"./models/devtools/StringDevtool\";\r\nimport type {\r\n StringDevtoolDefinition,\r\n StringDevtoolProvider,\r\n StringDevtoolState,\r\n} from \"./models/devtools/StringDevtool\";\r\n\r\nfunction isTouchDevice() {\r\n return \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\r\n}\r\nfunction isSafari(): boolean {\r\n let ua = navigator.userAgent.toLowerCase();\r\n if (ua.indexOf(\"safari\") != -1) {\r\n if (ua.indexOf(\"chrome\") > -1) {\r\n return false;\r\n } else {\r\n return true;\r\n }\r\n } else {\r\n return false;\r\n }\r\n}\r\n\r\ntype ScrollIntent =\r\n | { kind: \"position\"; position: number; immediate: boolean }\r\n | { kind: \"selector\"; selector: string; offset: number; immediate: boolean }\r\n | { kind: \"element\"; element: HTMLElement; offset: number; immediate: boolean };\r\n\r\nexport interface ScrollSettingsOptions {\r\n /**\r\n * Controls how quickly the visible scroll position follows the internal target.\r\n *\r\n * This is the main \"follow\" value. Higher values make the viewport catch the target\r\n * faster and feel more direct. Lower values add softness and visual weight because\r\n * the target can move ahead while the rendered position eases toward it.\r\n *\r\n * Typical range: `0.08` to `0.18`.\r\n * Default: `0.09`.\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Controls how strongly wheel input moves the scroll target at the start of a gesture.\r\n *\r\n * This is the main \"start\" value. Higher values make scroll pick up speed quickly\r\n * from small wheel deltas. Lower values make the start heavier and more gradual.\r\n * The public value is normalized from `0` to `1` and mapped internally to a safe\r\n * scroll-force range.\r\n *\r\n * Typical range: `0.35` to `0.85`.\r\n * Default: `0.8`.\r\n */\r\n acceleration?: number;\r\n\r\n /**\r\n * Controls how long the wheel impulse keeps fading after input stops.\r\n *\r\n * This is the main \"stop\" or inertia value. `0` gives a short, direct tail;\r\n * `0.5` keeps the legacy decay feel; `1` creates a longer and softer glide.\r\n * It does not expose raw deceleration directly. Instead, StringTune derives a\r\n * stable decay curve from the current acceleration so the start and stop remain\r\n * related.\r\n *\r\n * Range: `0` to `1`.\r\n * Default: `0.5`.\r\n */\r\n smoothness?: number;\r\n\r\n /**\r\n * Scales raw wheel input before acceleration is applied.\r\n *\r\n * Use this for device/input sensitivity. It changes how much scroll energy enters\r\n * the system without changing the easing curve itself. Raise it when the page feels\r\n * underpowered on a mouse wheel; lower it when trackpads feel too aggressive.\r\n *\r\n * Typical range: `0.75` to `1.25`.\r\n * Default: `1.1`.\r\n */\r\n multiplier?: number;\r\n\r\n /**\r\n * Maximum accumulated wheel impulse used in one smooth-scroll step.\r\n *\r\n * This prevents high-resolution wheels, trackpads, or bursty browser input from\r\n * pushing the target too far in a single frame. Lower values feel more controlled;\r\n * higher values preserve stronger flicks and long fast gestures.\r\n *\r\n * Typical range: `1200` to `3200`.\r\n * Default: `2400`.\r\n */\r\n maxDelta?: number;\r\n\r\n /**\r\n * Minimum visible velocity before smooth scroll snaps to target and stops.\r\n *\r\n * Lower values keep the final tail alive longer and make the stop more delicate.\r\n * Higher values stop earlier, which can feel cleaner on UI-heavy pages where tiny\r\n * sub-pixel motion is not useful.\r\n *\r\n * Typical range: `0.004` to `0.03`.\r\n * Default: `0.08`.\r\n */\r\n stopThreshold?: number;\r\n}\r\n\r\n/**\r\n * Public smooth-scroll tuning facade.\r\n *\r\n * Prefer this grouped API for new projects:\r\n *\r\n * ```ts\r\n * stringTune.scroll.configure({\r\n * speed: 0.09,\r\n * acceleration: 0.8,\r\n * smoothness: 0.5,\r\n * multiplier: 1.1,\r\n * });\r\n * ```\r\n *\r\n * The parameters are intentionally named by scroll feel:\r\n * `acceleration` shapes how motion starts, `speed` shapes how the viewport follows\r\n * the target, and `smoothness` shapes how motion fades and stops.\r\n */\r\nexport class StringTuneScrollSettings {\r\n constructor(private readonly data: StringData) {}\r\n\r\n /**\r\n * Controls how quickly the visible scroll position follows the internal target.\r\n *\r\n * This is the visible interpolation factor from current position to target.\r\n * Higher values feel tighter and more responsive because the viewport catches up\r\n * quickly. Lower values feel softer, heavier, and more cinematic because the target\r\n * can move ahead while the visible position eases toward it.\r\n *\r\n * This parameter does not change wheel sensitivity. If the scroll starts too fast\r\n * or too slow, tune `acceleration` or `multiplier` first.\r\n *\r\n * Typical range: `0.08` to `0.18`.\r\n * Default: `0.09`.\r\n */\r\n public get speed(): number {\r\n return this.data.scroll.speed;\r\n }\r\n\r\n public set speed(value: number) {\r\n this.data.scroll.speed = Math.max(0.0, value);\r\n }\r\n\r\n /**\r\n * Controls how strongly wheel input moves the scroll target at the start of a gesture.\r\n *\r\n * This is the primary control for the beginning of a scroll. Higher values make\r\n * the target react immediately to small wheel deltas. Lower values make the start\r\n * feel heavier and more gradual.\r\n *\r\n * The value is normalized from `0` to `1`. Internally it is mapped to a stable\r\n * scroll-force range, so consumers do not need to tune raw physics constants.\r\n *\r\n * Typical range: `0.35` to `0.85`.\r\n * Default: `0.8`.\r\n */\r\n public get acceleration(): number {\r\n return this.data.scroll.acceleration;\r\n }\r\n\r\n public set acceleration(value: number) {\r\n const normalized = this.clamp01(value);\r\n\r\n this.data.scroll.acceleration = normalized;\r\n this.data.scroll.speedAccelerate = this.mapNormalizedAcceleration(normalized);\r\n }\r\n\r\n /**\r\n * Controls how long the wheel impulse keeps fading after input stops.\r\n *\r\n * This is the primary control for the end of a scroll. `0` creates a short,\r\n * direct tail. `0.5` keeps the legacy decay feel. `1` creates a longer, softer\r\n * glide after the user stops scrolling.\r\n *\r\n * This parameter is not raw deceleration. StringTune derives the internal decay\r\n * curve from `smoothness` and the current acceleration so tuning remains stable\r\n * and predictable.\r\n *\r\n * Range: `0` to `1`.\r\n * Default: `0.5`.\r\n */\r\n public get smoothness(): number {\r\n return this.data.scroll.smoothness;\r\n }\r\n\r\n public set smoothness(value: number) {\r\n this.data.scroll.smoothness = this.clamp01(value);\r\n this.data.scroll.decelerationCoefficient = this.getDecayPowerFromSmoothness(value);\r\n }\r\n\r\n /**\r\n * Scales raw wheel input before acceleration is applied.\r\n *\r\n * Use this when the same scroll curve feels too weak or too strong for the target\r\n * input device. It changes the amount of wheel energy entering the system, while\r\n * `acceleration`, `speed`, and `smoothness` keep the same shape.\r\n *\r\n * Typical range: `0.75` to `1.25`.\r\n * Default: `1.1`.\r\n */\r\n public get multiplier(): number {\r\n return this.data.scroll.multiplier;\r\n }\r\n\r\n public set multiplier(value: number) {\r\n this.data.scroll.multiplier = Math.max(0.0, value);\r\n }\r\n\r\n /**\r\n * Limits a single accumulated wheel impulse before it moves the target.\r\n *\r\n * This protects the smooth-scroll engine from large input bursts produced by\r\n * high-resolution wheels, fast trackpad gestures, or browser event batching.\r\n * Lower values feel more controlled and restrained. Higher values allow stronger\r\n * flick gestures to travel farther.\r\n *\r\n * Typical range: `1200` to `3200`.\r\n * Default: `2400`.\r\n */\r\n public get maxDelta(): number {\r\n return this.data.scroll.maxDelta;\r\n }\r\n\r\n public set maxDelta(value: number) {\r\n this.data.scroll.maxDelta = Math.max(1.0, value);\r\n }\r\n\r\n /**\r\n * Minimum visible velocity before smooth scroll snaps to target and stops.\r\n *\r\n * This controls the final settle point. Lower values preserve tiny end-of-motion\r\n * movement and make the stop more delicate. Higher values stop earlier and can\r\n * feel cleaner on dense interfaces where long sub-pixel motion adds noise.\r\n *\r\n * Typical range: `0.004` to `0.03`.\r\n * Default: `0.08`.\r\n */\r\n public get stopThreshold(): number {\r\n return this.data.scroll.stopThreshold;\r\n }\r\n\r\n public set stopThreshold(value: number) {\r\n this.data.scroll.stopThreshold = Math.max(0.0, value);\r\n }\r\n\r\n /**\r\n * Updates several smooth-scroll settings at once.\r\n *\r\n * Omitted properties keep their current values, so this can be used for presets\r\n * or partial runtime tuning without resetting the whole scroll feel.\r\n *\r\n * ```ts\r\n * stringTune.scroll.configure({\r\n * speed: 0.105,\r\n * acceleration: 0.55,\r\n * smoothness: 0.72,\r\n * });\r\n * ```\r\n */\r\n public configure(options: ScrollSettingsOptions): void {\r\n if (options.speed !== undefined) this.speed = options.speed;\r\n if (options.acceleration !== undefined) this.acceleration = options.acceleration;\r\n if (options.smoothness !== undefined) this.smoothness = options.smoothness;\r\n if (options.multiplier !== undefined) this.multiplier = options.multiplier;\r\n if (options.maxDelta !== undefined) this.maxDelta = options.maxDelta;\r\n if (options.stopThreshold !== undefined) this.stopThreshold = options.stopThreshold;\r\n }\r\n\r\n /**\r\n * Legacy bridge for `stringTune.decelerationCoefficient`.\r\n * Prefer `stringTune.scroll.smoothness` for new code.\r\n */\r\n public setLegacyDecelerationCoefficient(value: number): void {\r\n const coefficient = Math.max(0.0, value);\r\n\r\n this.data.scroll.decelerationCoefficient = coefficient;\r\n this.data.scroll.smoothness = this.getSmoothnessFromDecayPower(coefficient);\r\n }\r\n\r\n private mapNormalizedAcceleration(value: number): number {\r\n const min = 0.1;\r\n const max = 0.5;\r\n\r\n return min + (max - min) * value;\r\n }\r\n\r\n private getDecayPowerFromSmoothness(value: number): number {\r\n return 1.0 + (0.5 - this.clamp01(value)) * 1.2;\r\n }\r\n\r\n private getSmoothnessFromDecayPower(value: number): number {\r\n return this.clamp01(0.5 - (value - 1.0) / 1.2);\r\n }\r\n\r\n private clamp01(value: number): number {\r\n return Math.min(Math.max(0.0, value), 1.0);\r\n }\r\n}\r\n\r\nclass StringTune {\r\n private static readonly DEVTOOLS_ACCESS_URL = \"https://access.fiddle.digital/\";\r\n private static readonly DEVTOOLS_LOG_PREFIX = \"[StringTune Devtools]\";\r\n private static readonly DEVTOOLS_ARTIFACT_SELECTORS = [\r\n \"[data-string-dev-viewport-layer]\",\r\n \"[data-string-dev-viewport-world]\",\r\n \"[data-stdg-dock]\",\r\n \"[data-stdg-dock-sub-badges]\",\r\n ];\r\n\r\n /** Bound handler for the scroll start event */\r\n private onScrollStartBind: any;\r\n\r\n /** Bound handler for the scroll stop event */\r\n private onScrollStopBind: any;\r\n\r\n /** Bound handler for the scroll direction change event */\r\n private onDirectionChangeBind: any;\r\n\r\n /** Bound handler for scroll mode/config changes */\r\n private onScrollConfigChangeBind: any;\r\n\r\n /** Bound wheel event handler */\r\n private onWheelBind: any;\r\n\r\n /** Bound scroll event handler */\r\n private onScrollBind: any;\r\n\r\n /** Bound resize event handler */\r\n private onResizeBind: any;\r\n\r\n /** Bound mouse move handler */\r\n private onMouseMoveBind: any;\r\n\r\n /** Bound scroll to handler */\r\n private onScrollToBind: (value: any) => void;\r\n private onDOMChangedBind: (data?: { stringElementChanged?: boolean }) => void;\r\n\r\n private onContainerTransitionEndBind: any;\r\n private onResizeObserverBind: any;\r\n private pendingScroll: boolean = false;\r\n private lastScrollEmitted: number = NaN;\r\n private observerContainerMutation: MutationObserver | null = null;\r\n private pendingResizeRaf: number | null = null;\r\n private pendingResizeForce: boolean = false;\r\n private pendingRebuildRaf: number | null = null;\r\n private activeScrollIntent: ScrollIntent | null = null;\r\n\r\n /** Singleton instance of StringTune */\r\n private static i: StringTune;\r\n\r\n /** Root scrollable element (typically <body>) */\r\n private root: any;\r\n\r\n /** Window object (used for event bindings and dimensions) */\r\n private window: any;\r\n\r\n /** Previous window width for resize diff check */\r\n private prevWidth: number = 0;\r\n\r\n /** Previous window height for resize diff check */\r\n private prevHeight: number = 0;\r\n\r\n /** Manages all modules registered in the system */\r\n private moduleManager: ModuleManager;\r\n\r\n /** Manages scroll modes and active scroll engine */\r\n private scrollManager: ScrollManager;\r\n\r\n /** Manages all interactive objects (elements with `string-*` attributes) */\r\n private objectManager: ObjectManager;\r\n\r\n /** Central event manager for internal pub-sub logic */\r\n private eventManager: EventManager;\r\n private signalHub: StringSignalHub;\r\n\r\n /** Handles custom cursor logic (if enabled) */\r\n private cursorController: CursorController;\r\n\r\n /** Provides default utility tools (parsers, interpolation, etc.) */\r\n private tools: DefaultToolsContainer;\r\n\r\n /** Main loop used for frame updates (with fixed FPS) */\r\n private loop: StringFPS = new StringFPS();\r\n\r\n /** Global reactive data store (scroll, viewport, etc.) */\r\n private data: StringData;\r\n\r\n private scrollSettings: StringTuneScrollSettings;\r\n\r\n /** Context shared across all modules (events, data, tools, settings) */\r\n private context: StringContext;\r\n\r\n /** Caches the center positions of string objects. */\r\n private centers: CenterCache;\r\n\r\n /** Registry of scroll scopes (root page scroll + nested StringScrollContainer instances). */\r\n private scrollScopes: ScrollScopeRegistry;\r\n\r\n /** Tracks hover states of string objects. */\r\n private hoverManager: HoverTracker;\r\n private devtools: DevtoolsManager;\r\n private devtoolsFpsLastSampleTime: number = 0;\r\n private devtoolsFpsFrameCount: number = 0;\r\n\r\n private observerContainerResize: ResizeObserver | null = null;\r\n private devtoolsAccessToken: string = \"\";\r\n private devtoolsAccessState: \"unknown\" | \"pending\" | \"granted\" | \"denied\" = \"unknown\";\r\n private devtoolsAccessRequestId: number = 0;\r\n private pendingDevtoolUses: Array<{ objectClass: typeof StringModule; settings: any }> = [];\r\n private hasStarted: boolean = false;\r\n private devtoolsAccessLastMessage: \"none\" | \"granted\" | \"denied\" | \"error\" = \"none\";\r\n\r\n public canRebuild: boolean = true;\r\n\r\n /**\r\n * Sets the scroll position manually.\r\n * This overrides all internal scroll states including target and lerped values.\r\n * Useful for programmatic jumps or syncing scroll externally.\r\n *\r\n * @param value The new scroll position in pixels.\r\n */\r\n public set scrollPosition(value: number) {\r\n this.data.scroll.current = value;\r\n this.data.scroll.target = value;\r\n this.data.scroll.transformedCurrent =\r\n this.data.scroll.current * this.data.viewport.transformScale;\r\n this.data.scroll.delta = 0;\r\n this.data.scroll.lerped = 0;\r\n this.scrollManager.updatePosition();\r\n this.moduleManager.onScroll();\r\n this.objectManager.checkInview();\r\n }\r\n\r\n public set accessDevtoolToken(value: string) {\r\n const normalized = value.trim();\r\n const isSameToken = normalized === this.devtoolsAccessToken;\r\n\r\n if (\r\n isSameToken &&\r\n (this.devtoolsAccessState === \"granted\" || this.devtoolsAccessState === \"pending\")\r\n ) {\r\n return;\r\n }\r\n\r\n this.devtoolsAccessToken = normalized;\r\n\r\n if (normalized.length === 0) {\r\n this.devtoolsAccessState = \"unknown\";\r\n return;\r\n }\r\n\r\n this.validateDevtoolsAccess(normalized);\r\n }\r\n\r\n /**\r\n * Configures the container element(s) used for scroll tracking.\r\n * Accepts either the `Window` object or an `HTMLElement`.\r\n * Determines the appropriate internal element references based on the input type\r\n * and triggers a resize calculation.\r\n *\r\n * @param {Window | HTMLElement | any} container The target window or HTML element to associate with scrolling.\r\n * Handles `Window`, `HTMLElement`, and potentially other types via fallback.\r\n */\r\n public set scrollContainer(container: any) {\r\n const previousScrollContainer = this.data.scroll.scrollContainer;\r\n const previousWheelContainer = this.data.scroll.container;\r\n\r\n if (this.hasStarted) {\r\n previousScrollContainer?.removeEventListener(\"scroll\", this.onScrollBind);\r\n previousWheelContainer?.removeEventListener(\"wheel\", this.onWheelBind);\r\n }\r\n\r\n this.observerContainerResize?.unobserve(this.context.data.scroll.container);\r\n\r\n this.data.scroll.elementContainer.removeEventListener(\r\n \"transitionend\",\r\n this.onContainerTransitionEndBind,\r\n );\r\n if (container instanceof Window) {\r\n this.data.scroll.container = document.body;\r\n this.data.scroll.elementContainer = document.documentElement;\r\n this.data.scroll.scrollContainer = container;\r\n } else if (container instanceof HTMLElement) {\r\n this.data.scroll.container = container;\r\n this.data.scroll.elementContainer = container;\r\n this.data.scroll.scrollContainer = container;\r\n } else {\r\n // Fallback case\r\n this.data.scroll.container = document.body;\r\n this.data.scroll.elementContainer = document.documentElement;\r\n this.data.scroll.scrollContainer = container;\r\n }\r\n this.data.scroll.elementContainer.addEventListener(\r\n \"transitionend\",\r\n this.onContainerTransitionEndBind,\r\n );\r\n this.observerContainerResize?.observe(this.context.data.scroll.container);\r\n this.observeContainerMutations();\r\n\r\n if (this.hasStarted) {\r\n this.data.scroll.scrollContainer?.addEventListener(\"scroll\", this.onScrollBind);\r\n this.data.scroll.container?.addEventListener(\"wheel\", this.onWheelBind, {\r\n passive: false,\r\n });\r\n }\r\n\r\n this.queueResize(true, \"scrollContainer\");\r\n }\r\n\r\n /**\r\n * Gets the current scroll position in pixels.\r\n * This is typically updated every frame.\r\n */\r\n public get scrollPosition() {\r\n return this.data.scroll.current;\r\n }\r\n\r\n public get scrollHeight() {\r\n return this.data.viewport.contentHeight;\r\n }\r\n\r\n public get containerHeight() {\r\n return this.data.viewport.windowHeight;\r\n }\r\n\r\n /**\r\n * Smooth scroll tuning API.\r\n * Use this instead of legacy top-level scroll setters for new code.\r\n */\r\n public get scroll(): StringTuneScrollSettings {\r\n return this.scrollSettings;\r\n }\r\n\r\n /**\r\n * Sets the base scroll speed for smooth scrolling.\r\n * Typically a value between 0 and 1.\r\n */\r\n public set speed(value: number) {\r\n this.scroll.speed = value;\r\n }\r\n\r\n /**\r\n * Sets the scroll acceleration using a normalized value from 0 to 1.\r\n * Internally maps it to a real acceleration value between 0.1 and 0.5.\r\n *\r\n * @param speed A normalized acceleration factor (0 to 1).\r\n */\r\n public set speedAccelerate(speed: number) {\r\n this.scroll.acceleration = speed;\r\n }\r\n\r\n /**\r\n * @deprecated Use `stringTune.scroll.smoothness` instead.\r\n */\r\n public set decelerationCoefficient(coefficient: number) {\r\n this.scroll.setLegacyDecelerationCoefficient(coefficient);\r\n }\r\n\r\n /**\r\n * Sets the scroll mode for desktop devices.\r\n * Can be 'smooth', 'default', or 'disable'.\r\n */\r\n public set scrollDesktopMode(mode: ScrollMode) {\r\n this.scrollManager.setDesktopMode(mode);\r\n }\r\n\r\n /**\r\n * Sets the scroll mode for mobile devices.\r\n * Can be 'smooth', 'default', or 'disable'.\r\n */\r\n public set scrollMobileMode(mode: ScrollMode) {\r\n this.scrollManager.setMobileMode(mode);\r\n }\r\n\r\n /**\r\n * Locks page scroll without changing desktop/mobile mode preferences.\r\n * Use when opening modals — scroll containers remain functional.\r\n */\r\n public lockPageScroll(): void {\r\n this.scrollManager.lockPageScroll();\r\n }\r\n\r\n /**\r\n * Unlocks page scroll, restoring the mode based on current device (desktop/mobile).\r\n */\r\n public unlockPageScroll(): void {\r\n this.scrollManager.unlockPageScroll();\r\n }\r\n\r\n public set FPSTrackerVisible(visible: boolean) {\r\n this.data.system.fpsTracker = visible;\r\n this.eventManager.emit(\"tracker:fps:visible\", visible);\r\n }\r\n\r\n public set PositionTrackerVisible(visible: boolean) {\r\n this.data.system.positionTracker = visible;\r\n this.eventManager.emit(\"tracker:position:visible\", visible);\r\n }\r\n\r\n public set domBatcherEnabled(enabled: boolean) {\r\n this.objectManager.setDOMBatcherEnabled(enabled);\r\n }\r\n\r\n public set intersectionObserverEnabled(enabled: boolean) {\r\n this.objectManager.setIntersectionObserverEnabled(enabled);\r\n }\r\n\r\n private debouncedResize = Debounce((source: string) => {\r\n this.queueResize(false, source);\r\n }, 30);\r\n\r\n private debouncedRebuild = Debounce(() => {\r\n this.queueRebuild();\r\n }, 30);\r\n\r\n private constructor() {\r\n this.cleanupExistingDevtoolsArtifacts();\r\n this.root = document.body;\r\n this.window = window;\r\n\r\n this.tools = new DefaultToolsContainer();\r\n this.data = new StringData();\r\n this.scrollSettings = new StringTuneScrollSettings(this.data);\r\n this.eventManager = new EventManager();\r\n this.signalHub = new StringSignalHub((eventName, payload) =>\r\n this.eventManager.emit(eventName, payload),\r\n );\r\n this.moduleManager = new ModuleManager(this.data);\r\n this.objectManager = new ObjectManager(\r\n this.data,\r\n this.moduleManager,\r\n this.eventManager,\r\n this.tools,\r\n );\r\n\r\n this.scrollScopes = new ScrollScopeRegistry(window);\r\n this.centers = new CenterCache(this.scrollScopes);\r\n this.hoverManager = new HoverTracker();\r\n this.devtools = new DevtoolsManager();\r\n\r\n this.context = {\r\n events: this.eventManager,\r\n data: this.data,\r\n tools: this.tools,\r\n settings: {},\r\n centers: this.centers,\r\n hover: this.hoverManager,\r\n objectManager: this.objectManager,\r\n signals: this.signalHub,\r\n scrollScopes: this.scrollScopes,\r\n };\r\n\r\n this.cursorController = new CursorController(1, this.context);\r\n this.scrollManager = new ScrollManager(this.context);\r\n\r\n this.setupSettings({\r\n \"global-class\": false,\r\n \"offset-top\": \"0%\",\r\n \"offset-bottom\": \"0%\",\r\n key: \"--progress\",\r\n \"inview-top\": \"0%\",\r\n \"inview-bottom\": \"0%\",\r\n \"enter-el\": \"top\",\r\n \"enter-vp\": \"bottom\",\r\n \"exit-el\": \"bottom\",\r\n \"exit-vp\": \"top\",\r\n \"parallax-bias\": \"0.0\",\r\n parallax: \"0.2\",\r\n lerp: \"0.2\",\r\n \"cursor-lerp\": \"0.75\",\r\n radius: \"150\",\r\n strength: \"0.3\",\r\n glide: \"1\",\r\n anchor: \"center center\",\r\n timeout: 900,\r\n alignment: \"center\",\r\n \"target-disable\": \"false\",\r\n \"target-style-disable\": \"false\",\r\n \"target-class\": \"\",\r\n active: \"false\",\r\n fixed: \"false\",\r\n repeat: \"false\",\r\n \"self-disable\": \"false\",\r\n abs: \"false\",\r\n easing: \"cubic-bezier(0.25, 0.25, 0.25, 0.25)\",\r\n \"glide-base-velocity\": 0.00125,\r\n \"glide-reduce-velocity\": 0.0000625,\r\n \"glide-negative-velocity\": -0.0001,\r\n\r\n \"position-strength\": 3,\r\n \"position-tension\": 0.05,\r\n \"position-friction\": 0.15,\r\n \"position-max-velocity\": 10,\r\n \"position-update-threshold\": 0.1,\r\n \"rotation-strength\": 0.75,\r\n \"rotation-tension\": 0.06,\r\n \"rotation-friction\": 0.18,\r\n \"rotation-max-angular-velocity\": 6,\r\n \"rotation-max-angle\": 18,\r\n \"rotation-update-threshold\": 0.15,\r\n \"max-offset\": 220,\r\n \"sleep-epsilon\": 0.01,\r\n \"continuous-push\": true,\r\n });\r\n\r\n this.onContainerTransitionEndBind = this.onContainerTransitionEnd.bind(this);\r\n\r\n this.onResizeObserverBind = this.onResizeObserverEvent.bind(this);\r\n this.observerContainerResize = new ResizeObserver(this.onResizeObserverBind);\r\n this.observerContainerResize.observe(this.context.data.scroll.container);\r\n\r\n this.onWheelBind = this.onWheelEvent.bind(this);\r\n this.onScrollBind = this.onScrollEvent.bind(this);\r\n this.onResizeBind = () => {\r\n this.queueResize(false, \"resizeBind\");\r\n };\r\n this.onMouseMoveBind = this.onMouseMoveEvent.bind(this);\r\n\r\n this.onScrollStartBind = this.onScrollStart.bind(this);\r\n this.onScrollStopBind = this.onScrollStop.bind(this);\r\n this.onDirectionChangeBind = this.onDirectionChange.bind(this);\r\n this.onScrollConfigChangeBind = this.onScrollConfigChange.bind(this);\r\n this.onScrollToBind = this.scrollTo.bind(this);\r\n this.onDOMChangedBind = this.onDOMChanged.bind(this);\r\n\r\n this.eventManager.on(`wheel`, this.onWheelBind);\r\n this.eventManager.on(`resize`, this.onResizeBind);\r\n this.eventManager.on(`scrollTo`, this.onScrollToBind);\r\n this.eventManager.on(`dom:changed`, this.onDOMChangedBind);\r\n\r\n this.scrollManager.bindEvents({\r\n onScrollStart: this.onScrollStartBind,\r\n onScrollStop: this.onScrollStopBind,\r\n onDirectionChange: this.onDirectionChangeBind,\r\n onModeChange: this.onScrollConfigChangeBind,\r\n });\r\n\r\n this.loop.setOnFrame((time: number) => {\r\n this.data.time.delta = time - this.data.time.now;\r\n this.data.time.previous = this.data.time.now;\r\n this.data.time.now = time;\r\n this.data.time.elapsed += this.data.time.delta;\r\n this.onUpdateEvent();\r\n this.updateDevtoolsFPS(time);\r\n });\r\n this.on(\"image:load:all\", () => {\r\n this.onRebuild();\r\n });\r\n this.scrollContainer = window;\r\n }\r\n\r\n /**\r\n * Returns the singleton instance of StringTune.\r\n * If not already created, initializes it.\r\n */\r\n public static getInstance(): StringTune {\r\n if (!StringTune.i) {\r\n StringTune.i = new StringTune();\r\n }\r\n return StringTune.i;\r\n }\r\n\r\n /**\r\n * Finds and returns an existing module by its class.\r\n * Useful for reusing a module instance without re-registering.\r\n *\r\n * @template T The type of the module to retrieve.\r\n * @param type The module class constructor.\r\n * @returns The module instance if found, otherwise undefined.\r\n */\r\n public reuse<T>(type: new (...args: any[]) => T): T | undefined {\r\n return this.moduleManager.find(type);\r\n }\r\n\r\n /**\r\n * Instantiates and registers a new module.\r\n * Accepts optional per-instance settings that override global settings.\r\n *\r\n * @param objectClass The module class to instantiate.\r\n * @param settings Optional settings specific to this module.\r\n */\r\n public use(objectClass: typeof StringModule, settings: any = null) {\r\n const existing = this.moduleManager.find(objectClass);\r\n if (existing) {\r\n return;\r\n }\r\n if (this.shouldDeferDevtoolModule(objectClass, settings)) {\r\n return;\r\n }\r\n this.instantiateModule(objectClass, settings);\r\n }\r\n\r\n private cleanupExistingDevtoolsArtifacts(): void {\r\n for (const selector of StringTune.DEVTOOLS_ARTIFACT_SELECTORS) {\r\n document.querySelectorAll(selector).forEach((node) => node.remove());\r\n }\r\n }\r\n\r\n private instantiateModule(objectClass: typeof StringModule, settings: any = null): void {\r\n const effectiveSettings = {\r\n ...this.context.settings,\r\n ...settings,\r\n };\r\n const module = new objectClass({\r\n events: this.eventManager,\r\n data: this.data,\r\n tools: this.tools,\r\n settings: effectiveSettings,\r\n centers: this.centers,\r\n hover: this.hoverManager,\r\n objectManager: this.objectManager,\r\n signals: this.signalHub,\r\n scrollScopes: this.scrollScopes,\r\n });\r\n this.moduleManager.register(module);\r\n if (isStringDevtoolProvider(module)) {\r\n this.devtools.register(module.getDevtoolDefinition());\r\n }\r\n\r\n if (this.hasStarted) {\r\n this.objectManager.attachModule(module);\r\n module.onInit();\r\n module.onResize();\r\n module.onScroll(this.data);\r\n module.onFrame(this.data);\r\n }\r\n }\r\n\r\n private shouldDeferDevtoolModule(objectClass: typeof StringModule, settings: any): boolean {\r\n const isDevtoolModule =\r\n objectClass === StringDevModule || objectClass.prototype instanceof StringDevModule;\r\n if (!isDevtoolModule) {\r\n return false;\r\n }\r\n\r\n if (this.devtoolsAccessState === \"granted\") {\r\n return false;\r\n }\r\n\r\n this.pendingDevtoolUses.push({ objectClass, settings });\r\n if (this.devtoolsAccessToken.length > 0 && this.devtoolsAccessState !== \"pending\") {\r\n this.validateDevtoolsAccess(this.devtoolsAccessToken);\r\n }\r\n return true;\r\n }\r\n\r\n private async validateDevtoolsAccess(token: string): Promise<void> {\r\n const requestId = ++this.devtoolsAccessRequestId;\r\n this.devtoolsAccessState = \"pending\";\r\n\r\n try {\r\n const response = await fetch(\r\n `${StringTune.DEVTOOLS_ACCESS_URL}?token=${encodeURIComponent(token)}`,\r\n );\r\n const allowed = await this.resolveDevtoolsAccessResponse(response);\r\n if (requestId !== this.devtoolsAccessRequestId || token !== this.devtoolsAccessToken) {\r\n return;\r\n }\r\n\r\n this.devtoolsAccessState = allowed ? \"granted\" : \"denied\";\r\n if (!allowed) {\r\n this.logDevtoolsAccess(\"denied\");\r\n this.pendingDevtoolUses = [];\r\n return;\r\n }\r\n\r\n this.logDevtoolsAccess(\"granted\");\r\n const pending = [...this.pendingDevtoolUses];\r\n this.pendingDevtoolUses = [];\r\n pending.forEach(({ objectClass, settings }) => {\r\n this.instantiateModule(objectClass, settings);\r\n });\r\n } catch {\r\n if (requestId !== this.devtoolsAccessRequestId || token !== this.devtoolsAccessToken) {\r\n return;\r\n }\r\n if (this.devtoolsAccessState === \"granted\") {\r\n return;\r\n }\r\n this.devtoolsAccessState = \"denied\";\r\n this.logDevtoolsAccess(\"error\");\r\n this.pendingDevtoolUses = [];\r\n }\r\n }\r\n\r\n private logDevtoolsAccess(type: \"granted\" | \"denied\" | \"error\"): void {\r\n if (this.devtoolsAccessLastMessage === type) {\r\n return;\r\n }\r\n this.devtoolsAccessLastMessage = type;\r\n\r\n if (type === \"granted\") {\r\n console.info(\r\n `${StringTune.DEVTOOLS_LOG_PREFIX} Access granted. Devtools modules are enabled.`,\r\n );\r\n return;\r\n }\r\n\r\n if (type === \"denied\") {\r\n console.warn(\r\n `${StringTune.DEVTOOLS_LOG_PREFIX} Access denied. Devtools modules were not enabled. Check accessDevtoolToken.`,\r\n );\r\n return;\r\n }\r\n\r\n console.warn(\r\n `${StringTune.DEVTOOLS_LOG_PREFIX} Access check failed. Devtools modules were not enabled.`,\r\n );\r\n }\r\n\r\n private async resolveDevtoolsAccessResponse(response: Response): Promise<boolean> {\r\n if (!response.ok) {\r\n return false;\r\n }\r\n\r\n const contentType = response.headers.get(\"content-type\")?.toLowerCase() ?? \"\";\r\n if (contentType.includes(\"application/json\")) {\r\n const data = await response.json();\r\n if (typeof data === \"boolean\") {\r\n return data;\r\n }\r\n if (data && typeof data === \"object\" && \"allowed\" in data) {\r\n return data.allowed === true;\r\n }\r\n return false;\r\n }\r\n\r\n const text = (await response.text()).trim().toLowerCase();\r\n return text === \"true\";\r\n }\r\n\r\n /**\r\n * Registers a new scroll mode (provider) to the system.\r\n * Allows integrating custom scroll implementations (e.g. Lenis, Locomotive).\r\n *\r\n * @param name The unique name for this scroll mode (e.g. 'smooth', 'lenis').\r\n * @param factory A function that receives the StringContext and returns a ScrollController instance, OR a ScrollController class constructor.\r\n *\r\n * Example:\r\n * ```ts\r\n * stringTune.registerScrollMode(\"custom\", (context) => new CustomAdapter(context));\r\n * ```\r\n */\r\n public registerScrollMode(\r\n name: string,\r\n factory:\r\n | ((context: StringContext) => ScrollController)\r\n | (new (context: StringContext) => ScrollController),\r\n ) {\r\n let controller: ScrollController;\r\n\r\n // Check if it's a class constructor (handles class fields too)\r\n if (typeof factory === \"function\" && factory.prototype instanceof ScrollController) {\r\n const Constructor = factory as new (context: StringContext) => ScrollController;\r\n controller = new Constructor(this.context);\r\n } else {\r\n const func = factory as (context: StringContext) => ScrollController;\r\n controller = func(this.context);\r\n }\r\n\r\n if (!controller.name) {\r\n controller.name = name;\r\n }\r\n\r\n this.scrollManager.registerMode(name, controller);\r\n }\r\n\r\n /**\r\n * Subscribes to a global event within the system.\r\n *\r\n * @param eventName The name of the event to listen for.\r\n * @param callback The function to call when the event is triggered.\r\n * @param id Optional subscription ID (for easier management).\r\n */\r\n public on(eventName: string, callback: EventCallback<any>, id: string = \"\") {\r\n this.eventManager.on(eventName, callback, id);\r\n }\r\n public emit(eventName: string, data: any) {\r\n this.eventManager.emit(eventName, data);\r\n }\r\n\r\n /**\r\n * Unsubscribes from a global event.\r\n *\r\n * @param eventName The name of the event.\r\n * @param callback The previously registered callback.\r\n * @param id Optional ID used during subscription.\r\n */\r\n public off(eventName: string, callback: EventCallback<any>, id: string = \"\") {\r\n this.eventManager.off(eventName, callback, id);\r\n }\r\n\r\n /**\r\n * Adds a scroll trigger rule that activates when the user scrolls past a defined offset\r\n * in a specific direction. This can be used to toggle CSS classes or execute callbacks\r\n * when elements come into view or go out of view.\r\n *\r\n * @param rule - The scroll trigger configuration object.\r\n * - `id`: A unique identifier for th