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 1.58 MB
{"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/modules/cursor/StringImpulse.ts","../src/utils/frame-dom.ts","../src/modules/layout/StringMasonry.ts","../src/modules/cursor/StringMagnetic.ts","../src/modules/cursor/CursorReactiveModule.ts","../src/modules/cursor/StringSpotlight.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/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/CenterCache.ts","../src/core/managers/HoverTracker.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/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/modules/dev-tools/core/startPointerDrag.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 { 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 { frameDOM } from \"./utils/frame-dom\";\r\nimport { styleTxn } from \"./utils/style-txn\";\r\nimport { StringRandom } from \"./modules/random/StringRandom\";\r\nimport { ScrollController } from \"./core/controllers/ScrollController\";\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\nclass StringTune {\n private static readonly DEVTOOLS_ACCESS_URL = \"https://access.fiddle.digital/\";\n private static readonly DEVTOOLS_LOG_PREFIX = \"[StringTune Devtools]\";\n private static readonly DEVTOOLS_ARTIFACT_SELECTORS = [\n \"[data-string-dev-viewport-layer]\",\n \"[data-string-dev-viewport-world]\",\n \"[data-stdg-dock]\",\n \"[data-stdg-dock-sub-badges]\",\n ];\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: () => 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\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\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 /** 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 /** 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 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 this.queueResize(true);\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 * 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.data.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 const min = 0.1;\r\n const max = 0.5;\r\n this.data.scroll.speedAccelerate = min + (max - min) * speed;\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 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(() => {\r\n this.queueResize(false);\r\n }, 30);\r\n\r\n private constructor() {\n this.cleanupExistingDevtoolsArtifacts();\n this.root = document.body;\n this.window = window;\n\r\n this.tools = new DefaultToolsContainer();\r\n this.data = new StringData();\r\n this.eventManager = new EventManager();\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.centers = new CenterCache();\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 };\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({\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);\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.onResize();\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) {\n const existing = this.moduleManager.find(objectClass);\n if (existing) {\n return;\n }\n if (this.shouldDeferDevtoolModule(objectClass, settings)) {\n return;\n }\n this.instantiateModule(objectClass, settings);\n }\n\n private cleanupExistingDevtoolsArtifacts(): void {\n for (const selector of StringTune.DEVTOOLS_ARTIFACT_SELECTORS) {\n document.querySelectorAll(selector).forEach((node) => node.remove());\n }\n }\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 });\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 this rule.\r\n * - `offset`: The vertical scroll offset (in pixels) where the rule should activate.\r\n * - `direction`: Defines the scroll direction required to activate the rule.\r\n * Can be `\"forward\"`, `\"backward\"`, or `\"any\"`.\r\n * - `onEnter`: (Optional) A function that will be called when the scroll position enters the trigger zone\r\n * in the specified direction.\r\n * - `onLeave`: (Optional) A function that will be called when the scroll position leaves the trigger zone\r\n * or scrolls in the opposite direction.\r\n * - `toggleClass`: (Optional) An object defining a class toggle behavior.\r\n * It contains a target element and a class name to be added when the trigger is active\r\n * and removed when it's not.\r\n */\r\n public addScrollMark(rule: ScrollMarkRule) {\r\n this.scrollManager.addScrollMark(rule);\r\n }\r\n\r\n /**\r\n * Removes a scroll trigger by its unique identifier.\r\n *\r\n * @param id - The unique identifier of the scroll trigger to be removed.\r\n */\r\n public removeScrollMark(id: string) {\r\n this.scrollManager.removeScrollMark(id);\r\n }\r\n\r\n /**\r\n * Starts the scroll engine and initializes all listeners, observers, and modules.\r\n *\r\n * @param fps Desired frames per second for the update loop.\r\n */\r\n public start(fps: number) {\n if (this.hasStarted) {\n return;\n }\n this.hasStarted = true;\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 window.addEventListener(\"resize\", this.onResizeBind);\r\n this.root.addEventListener(\"mousemove\", this.onMouseMoveBind);\r\n this.observeContainerMutations();\r\n\r\n this.use(StringInview);\r\n\r\n const htmlFontSize = window.getComputedStyle(document.documentElement).fontSize;\r\n const fontSizeNumber = parseFloat(htmlFontSize);\r\n this.context.data.viewport.baseRem = fontSizeNumber;\r\n\r\n document.documentElement.classList.add(\"-string\");\r\n this.syncDebugScrollState();\r\n this.moduleManager.onInit();\r\n this.onResize();\r\n this.initObjects();\r\n this.objectManager.observeDOM();\r\n\r\n this.loop.start(fps);\r\n this.eventManager.emit(`start`, null);\r\n }\r\n\r\n /**\r\n * Initializes all DOM elements with `string` or `string-copy-from` attributes.\r\n * Registers them with the object manager and triggers resize/scroll/frame hooks.\r\n */\r\n private initObjects() {\r\n document.querySelectorAll(\"[string],[data-string]\").forEach((element) => {\r\n this.objectManager.add(element as HTMLElement);\r\n });\r\n document.querySelectorAll(\"[string-copy-from],[data-string-copy-from]\").forEach((element) => {\r\n let connectTargetId = this.tools.domAttribute.process({\r\n element: element as HTMLElement,\r\n key: \"copy-from\",\r\n fallback: \"\",\r\n });\r\n\r\n if (connectTargetId && connectTargetId.length > 0) {\r\n this.objectManager.linkMirror(connectTargetId, element as HTMLElement);\r\n }\r\n });\r\n\r\n this.moduleManager.onResize();\r\n this.moduleManager.onScroll();\r\n this.moduleManager.onFrame();\r\n }\r\n\r\n /**\r\n * Sets global fallback settings for all modules.\r\n * These can be overridden by module-specific settings during `use(...)`.\r\n *\r\n * @param settings A key-value map of default settings (e.g. 'offset-top': '-10%').\r\n */\r\n public setupSettings(settings: StringSettings): void {\r\n this.context.settings = {\r\n ...this.context.settings,\r\n ...settings,\r\n };\r\n if (typeof settings[\"storageToken\"] === \"string\") {\r\n setStringDevStorageScopeToken(settings[\"storageToken\"]);\r\n } else if (typeof settings[\"storage-token\"] === \"string\") {\r\n setStringDevStorageScopeToken(settings[\"storage-token\"]);\r\n }\r\n if (typeof settings[\"accessDevtoolToken\"] === \"string\") {\r\n this.accessDevtoolToken = settings[\"accessDevtoolToken\"];\r\n }\r\n this.onSettingsChange({\r\n isDesktop: this.data.viewport.windowWidth > 1024,\r\n widthChanged: true,\r\n heightChanged: true,\r\n scrollHeightChanged: true,\r\n isForceRebuild: false,\r\n });\r\n }\r\n\r\n private onResizeObserverEvent() {\r\n this.debouncedResize();\r\n }\r\n\r\n private onContainerTransitionEnd(event: TransitionEvent) {\r\n if (event.target === this.context.data.scroll.container) {\r\n this.queueResize(true);\r\n }\r\n }\r\n\r\n private onDOMChanged() {\r\n this.queueResize(false);\r\n this.debouncedResize();\r\n }\r\n\r\n private observeContainerMutations() {\r\n this.observerContainerMutation?.disconnect();\r\n const container = this.context.data.scroll.container;\r\n if (!container) return;\r\n\r\n this.observerContainerMutation = new MutationObserver((mutationsList: MutationRecord[]) => {\r\n for (let i = 0; i < mutationsList.length; i++) {\r\n const mutation = mutationsList[i];\r\n if (\r\n mutation.type === \"attributes\" &&\r\n (mutation.attributeName === \"style\" || mutation.attributeName === \"class\")\r\n ) {\r\n this.queueResize(false);\r\n this.debouncedResize();\r\n break;\r\n }\r\n }\r\n });\r\n\r\n this.observerContainerMutation.observe(container, {\r\n attributes: true,\r\n attributeFilter: [\"style\", \"class\"],\r\n });\r\n }\r\n\r\n private queueResize(force: boolean = false) {\r\n if (force) {\r\n this.pendingResizeForce = true;\r\n }\r\n\r\n if (this.pendingResizeRaf != null) {\r\n return;\r\n }\r\n\r\n this.pendingResizeRaf = requestAnimationFrame(() => {\r\n this.pendingResizeRaf = null;\r\n const shouldForce = this.pendingResizeForce;\r\n this.pendingResizeForce = false;\r\n this.onResize(shouldForce);\r\n });\r\n }\r\n\r\n /**\r\n * Handles mouse move event and dispatches it to cursor and modules.\r\n * @param e Native mouse move event.\r\n */\r\n private onMouseMoveEvent(e: MouseEvent) {\r\n this.cursorController.onMouseMove(e);\r\n this.moduleManager.onMouseMove(e);\r\n frameDOM.measure(() => {\r\n this.moduleManager.onMouseMoveMeasure();\r\n });\r\n }\r\n\r\n /**\r\n * Handles wheel scroll event and passes it to the scroll engine and modules.\r\n * @param e Native wheel event.\r\n */\r\n private onWheelEvent(e: WheelEvent) {\r\n const target = e.target as HTMLElement;\r\n const isModal = target.closest(\"[string-isolation],[data-string-isolation]\");\r\n if (isModal != null) return;\r\n this.scrollManager.get().onWheel(e);\r\n this.moduleManager.onWheel(e);\r\n }\r\n\r\n /**\r\n * Called when scrolling begins.\r\n * Triggers module scroll start lifecycle hook.\r\n */\r\n private onScrollStart() {\r\n this.moduleManager.onScrollStart();\r\n this.eventManager.emit(`scroll:start`, null);\r\n }\r\n\r\n /**\r\n * Called when scrolling ends.\r\n * Triggers module scroll stop lifecycle hook.\r\n */\r\n private onScrollStop() {\r\n this.moduleManager.onScrollStop();\r\n this.eventManager.emit(`scroll:stop`, null);\r\n }\r\n\r\n /**\r\n * Called when scrolling ends.\r\n * Triggers module scroll stop lifecycle hook.\r\n */\r\n private onDirectionChange() {\r\n this.moduleManager.onDirectionChange();\r\n }\r\n\r\n private onScrollConfigChange() {\r\n this.moduleManager.onScrollConfigChange();\r\n this.syncDebugScrollState();\r\n this.moduleManager.onScroll();\r\n this.moduleManager.onScrollMeasure();\r\n this.moduleManager.onFrame();\r\n styleTxn.run(() => {\r\n this.moduleManager.onMutate();\r\n });\r\n }\r\n\r\n private syncDebugScrollState(): void {\r\n const html = document.documentElement;\r\n const isMobileBucket = window.innerWidth < 1024;\r\n html.setAttribute(\"data-string-scroll-mode\", String(this.data.scroll.mode));\r\n html.setAttribute(\"data-string-scroll-device\", isMobileBucket ? \"mobile\" : \"desktop\");\r\n }\r\n\r\n /**\r\n * Called when global or module settings are updated.\r\n * Notifies all managers and modules to re-read new settings.\r\n */\r\n private onSettingsChange(data: ISettingsChangeData) {\r\n this.cursorController.onSettingsChange(data);\r\n this.objectManager.onSettingsChange(data);\r\n this.moduleManager.onSettingsChange(data);\r\n }\r\n\r\n /**\r\n * Handles native scroll event.\r\n * Prevents default behavior and triggers internal scroll logic and event emissions.\r\n *\r\n * @param e The native scroll event.\r\n */\r\n private onScrollEvent(e: Event) {\r\n e.preventDefault();\r\n\r\n this.context.centers.invalidateAll();\r\n\r\n this.scrollManager.get().onScroll(e);\r\n this.pendingScroll = true;\r\n return false;\r\n }\r\n\r\n /**\r\n * Called every frame by the update loop.\r\n * Triggers scroll engine, modules, and global `update` event.\r\n */\r\n private onUpdateEvent() {\r\n this.cursorController.onFrame();\r\n this.scrollManager.get().onFrame();\r\n this.moduleManager.onFrame();\r\n\r\n if (this.pendingScroll || this.data.scroll.current !== this.lastScrollEmitted) {\r\n this.pendingScroll = false;\r\n this.moduleManager.onScroll();\r\n this.objectManager.checkInview();\r\n this.eventManager.emit(`lerp`, this.data.scroll.lerped);\r\n this.eventManager.emit(`scroll`, this.data.scroll.current);\r\n frameDOM.measure(() => {\r\n this.moduleManager.onScrollMeasure();\r\n });\r\n this.lastScrollEmitted = this.data.scroll.current;\r\n }\r\n\r\n frameDOM.mutate(() => {\r\n styleTxn.begin();\r\n this.moduleManager.onMutate();\r\n styleTxn.commit();\r\n });\r\n\r\n this.eventManager.emit(`update`, null);\r\n\r\n frameDOM.flush();\r\n }\r\n\r\n private updateDevtoolsFPS(time: number): void {\r\n if (this.devtoolsFpsLastSampleTime === 0) {\r\n this.devtoolsFpsLastSampleTime = time;\r\n }\r\n\r\n this.devtoolsFpsFrameCount += 1;\r\n const elapsed = time - this.devtoolsFpsLastSampleTime;\r\n\r\n if (elapsed < 1000) {\r\n return;\r\n }\r\n\r\n const fps = (this.devtoolsFpsFrameCount * 1000) / elapsed;\r\n this.devtools.setFPS(fps);\r\n this.devtoolsFpsFrameCount = 0;\r\n this.devtoolsFpsLastSampleTime = time;\r\n }\r\n\r\n /**\r\n * Handles resize events from scroll container or window.\r\n * Ignores height-only changes on mobile to prevent layout jumps.\r\n * Rebuilds layout and triggers module resize if size really changed.\r\n */\r\n public onResize(force: boolean = false): void {\r\n if (this.canRebuild == false) {\r\n return;\r\n }\r\n const container = this.data.scroll.container;\r\n const scroll = this.context.data.scroll;\r\n let width = 0;\r\n let height = 0;\r\n var newScrollHeight;\r\n var newContainerTopPosition = 0;\r\n const rect = container.getBoundingClientRect();\r\n\r\n if (container.tagName == \"BODY\") {\r\n width = document.documentElement.clientWidth || window.innerWidth || rect.width;\r\n height = window.innerHeight;\r\n } else {\r\n width = rect.width;\r\n height = rect.height;\r\n }\r\n\r\n newContainerTopPosition = container.tagName === \"BODY\" ? 0 : rect.top;\r\n newScrollHeight = scroll.container.scrollHeight;\r\n const transformScale = this.tools.transformScaleParser.process({\r\n value: window.getComputedStyle(container).transform,\r\n });\r\n this.context.data.viewport.transformScale =\r\n window.getComputedStyle(container).scale == \"none\"\r\n ? transformScale\r\n : Number(window.getComputedStyle(container).scale);\r\n this.context.data.scroll.transformedCurrent =\r\n this.context.data.scroll.current * this.context.data.viewport.transformScale;\r\n const isTouch = isTouchDevice();\r\n const isDesktop = width > 1024;\r\n\r\n const widthChanged = this.prevWidth !== width;\r\n const heightChanged = this.prevHeight !== height;\r\n const heightDiff = Math.abs(this.prevHeight - height);\r\n const scrollHeightChanged = this.context.data.viewport.contentHeight !== newScrollHeight;\r\n\r\n const shouldRebuild =\r\n widthChanged ||\r\n (!isTouch && heightChanged) ||\r\n (isTouch && heightDiff > 150) ||\r\n scrollHeightChanged;\r\n\r\n this.context.data.scroll.topPosition = Math.floor(newContainerTopPosition);\r\n this.context.data.viewport.contentWidth = width;\r\n this.context.data.viewport.contentHeight = newScrollHeight;\r\n\r\n this.prevWidth = width;\r\n this.prevHeight = height;\r\n\r\n this.context.data.viewport.windowWidth = width;\r\n this.context.data.viewport.windowHeight = height;\r\n\r\n const htmlFontSize = window.getComputedStyle(document.documentElement).fontSize;\r\n const fontSizeNumber = parseFloat(htmlFontSize);\r\n this.context.data.viewport.baseRem = fontSizeNumber * transformScale;\r\n this.syncDebugScrollState();\r\n\r\n scroll.bottomPosition = this.context.data.viewport.contentHeight - height;\r\n\r\n if (widthChanged || (typeof force === \"boolean\" && force)) {\r\n this.moduleManager.onResizeWidth();\r\n