@fiddle-digital/string-tune
Version:
StringTune is a cutting-edge JavaScript library designed to deliver high-performance, modular web effects. Whether you're looking to add smooth parallax scrolling, dynamic cursor interactions, progress tracking, or autoplay videos, StringTune empowers dev
1 lines • 316 kB
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/StringObject.ts","../src/core/managers/ObjectManager.ts","../src/core/controllers/ScrollController.ts","../src/core/controllers/StringScrollDefault.ts","../src/core/controllers/StringScrollDisable.ts","../src/core/controllers/StringScrollSmooth.ts","../src/core/managers/ScrollManager.ts","../src/states/CursorState.ts","../src/states/RenderState.ts","../src/states/ScrollState.ts","../src/states/TimeState.ts","../src/states/ViewportState.ts","../src/core/StringData.ts","../src/core/StringModule.ts","../src/tools/BoundingClientRectTool.ts","../src/tools/DOMAttributeTool.ts","../src/tools/RecordAttributeTool.ts","../src/tools/TransformNullifyTool.ts","../src/tools/RelativePositionTool.ts","../src/tools/LerpTool.ts","../src/tools/UnitParserTool.ts","../src/tools/AdaptiveLerpTool.ts","../src/tools/OriginParserTool.ts","../src/tools/ColorParserTool.ts","../src/tools/ValidationTool.ts","../src/tools/EasingFunctionTool.ts","../src/tools/MagneticPullTool.ts","../src/tools/LerpColorTool.ts","../src/tools/LerpVector2Tool.ts","../src/tools/TransformScaleParserTool.ts","../src/tools/CharIndexerTool.ts","../src/tools/LayoutLineSplitterTool.ts","../src/tools/SplitDomBuilderTool.ts","../src/tools/WordIndexerTool.ts","../src/tools/SplitOptionsParserTool.ts","../src/core/StringToolsContainer.ts","../src/modules/cursor/StringCursor.ts","../src/modules/cursor/StringMagnetic.ts","../src/modules/loading/StringLazy.ts","../src/modules/loading/StringLoading.ts","../src/modules/screen/StringInview.ts","../src/modules/screen/StringResponsive.ts","../src/modules/scroll/StringAnchor.ts","../src/modules/scroll/StringGlide.ts","../src/modules/scroll/StringLerp.ts","../src/modules/scroll/StringProgress.ts","../src/modules/scroll/StringParallax.ts","../src/modules/scrollbar/StringScrollbarHorizontal.ts","../src/modules/scrollbar/StringScrollbarVertical.ts","../src/modules/scrollbar/StringScrollbar.ts","../src/modules/text/StringSplit.ts","../src/modules/tracker/StringDelayLerpTracker.ts","../src/modules/tracker/StringFPSTracker.ts","../src/modules/tracker/StringLerpTracker.ts","../src/modules/tracker/StringPositionTracker.ts","../src/utils/Debounce.ts","../src/utils/StringFPS.ts","../src/modules/loading/StringVideoAutoplay.ts"],"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 { StringMagnetic } from \"./modules/cursor/StringMagnetic\";\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 { 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\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 {\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 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 /** 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 /**\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.delta = 0;\r\n this.data.scroll.lerped = 0;\r\n this.scrollManager.updatePosition();\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 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; // Assigns the original (potentially non-standard) container\r\n }\r\n this.debouncedResize();\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 speed() {\r\n return this.data.scroll.current;\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 private debouncedResize = Debounce(this.onResize, 30);\r\n\r\n private constructor() {\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.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 );\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 };\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 \"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 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\r\n this.onWheelBind = this.onWheelEvent.bind(this);\r\n this.onScrollBind = this.onScrollEvent.bind(this);\r\n this.onResizeBind = this.onResize.bind(this);\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\r\n this.scrollManager.bindEvents({\r\n onScrollStart: this.onScrollStartBind,\r\n onScrollStop: this.onScrollStopBind,\r\n onDirectionChange: this.onDirectionChangeBind,\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 });\r\n this.on(\"image:load:all\", () => {\r\n this.onResize();\r\n });\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 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 });\r\n this.moduleManager.register(module);\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\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 * 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) {\r\n // window.addEventListener('scroll', this.onScrollBind);\r\n // this.root.addEventListener('wheel', this.onWheelBind, { passive: false });\r\n\r\n this.data.scroll.scrollContainer?.addEventListener(\r\n \"scroll\",\r\n this.onScrollBind\r\n );\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\r\n const observerContainerResize = new ResizeObserver(() => {\r\n this.debouncedResize();\r\n });\r\n observerContainerResize.observe(this.context.data.scroll.container);\r\n\r\n const observerContainerMutation = new MutationObserver(\r\n (mutationsList: MutationRecord[], observer: MutationObserver) => {\r\n for (const mutation of mutationsList) {\r\n if (\r\n mutation.type === \"attributes\" &&\r\n (mutation.attributeName === \"style\" ||\r\n mutation.attributeName === \"class\")\r\n ) {\r\n this.onResize();\r\n }\r\n }\r\n }\r\n );\r\n const config: MutationObserverInit = {\r\n attributes: true,\r\n attributeFilter: [\"style\", \"class\"],\r\n };\r\n observerContainerMutation.observe(\r\n this.context.data.scroll.container,\r\n config\r\n );\r\n\r\n this.use(StringInview);\r\n\r\n const htmlFontSize = window.getComputedStyle(\r\n document.documentElement\r\n ).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.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\r\n .querySelectorAll(\"[string-copy-from],[data-string-copy-from]\")\r\n .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 if (connectTargetId && connectTargetId.length > 0) {\r\n this.objectManager.enqueueConnection(\r\n connectTargetId,\r\n 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 this.onSettingsChange();\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 }\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 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 }\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 }\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 /**\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() {\r\n this.cursorController.onSettingsChange();\r\n this.objectManager.onSettingsChange();\r\n this.moduleManager.onSettingsChange();\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 this.scrollManager.get().onScroll(e);\r\n this.moduleManager.onScroll();\r\n this.eventManager.emit(`lerp`, this.data.scroll.lerped);\r\n this.eventManager.emit(`scroll`, this.data.scroll.current);\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 this.eventManager.emit(`update`, null);\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(): void {\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 = window.innerWidth;\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 = 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 = transformScale;\r\n\r\n const isDesktop = width > 1080;\r\n\r\n const widthChanged = this.prevWidth !== width;\r\n const heightChanged = this.prevHeight !== height;\r\n const scrollHeightChanged =\r\n this.context.data.viewport.contentHeight !== newScrollHeight;\r\n\r\n const shouldRebuild =\r\n widthChanged || (isDesktop && heightChanged) || scrollHeightChanged;\r\n\r\n this.context.data.scroll.topPosition = 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(\r\n document.documentElement\r\n ).fontSize;\r\n const fontSizeNumber = parseFloat(htmlFontSize);\r\n this.context.data.viewport.baseRem = fontSizeNumber * transformScale;\r\n\r\n scroll.bottomPosition = this.context.data.viewport.contentHeight - height;\r\n\r\n if (shouldRebuild) {\r\n this.context.data.scroll.current =\r\n this.context.data.scroll.container.scrollTop;\r\n this.context.data.scroll.target =\r\n this.context.data.scroll.container.scrollTop;\r\n this.context.data.scroll.transformedCurrent =\r\n this.context.data.scroll.current *\r\n this.context.data.viewport.transformScale;\r\n this.scrollManager.updateResponsiveMode();\r\n this.moduleManager.onResize();\r\n this.onSettingsChange();\r\n this.moduleManager.onScroll();\r\n this.moduleManager.onFrame();\r\n }\r\n }\r\n\r\n /**\r\n * Cleans up the system, removes all event listeners, stops the loop,\r\n * and destroys modules and event subscriptions.\r\n */\r\n public destroy() {\r\n this.data.scroll.scrollContainer?.removeEventListener(\r\n \"scroll\",\r\n this.onScrollBind\r\n );\r\n this.data.scroll.container?.removeEventListener(\"wheel\", this.onScrollBind);\r\n\r\n this.window.removeEventListener(\"resize\", this.onResizeBind);\r\n this.root.removeEventListener(\"mousemove\", this.onMouseMoveBind);\r\n this.loop.stop();\r\n this.moduleManager.destroy();\r\n this.eventManager.clearAll();\r\n }\r\n}\r\n\r\nexport {\r\n StringTune as default,\r\n StringCursor,\r\n StringDelayLerpTracker,\r\n StringFPSTracker,\r\n StringGlide,\r\n StringLazy,\r\n StringLerp,\r\n StringLerpTracker,\r\n StringLoading,\r\n StringMagnetic,\r\n StringParallax,\r\n StringPositionTracker,\r\n StringProgress,\r\n StringResponsive,\r\n StringScrollbar,\r\n StringSplit,\r\n StringAnchor,\r\n StringTune as StringTune,\r\n StringVideoAutoplay,\r\n StringModule,\r\n StringObject,\r\n StringData,\r\n type StringContext,\r\n};\r\n","import { CursorState } from \"../../states/CursorState\"\r\nimport { EventManager } from \"../managers/EventManager\"\r\nimport { StringContext } from \"../StringContext\"\r\nimport { StringData } from \"../StringData\"\r\nimport { StringToolsContainer } from \"../StringToolsContainer\"\r\n\r\n/**\r\n * Manages virtual cursor logic: smoothing, updating, and syncing with mouse events.\r\n * \r\n * This controller handles cursor position tracking with smoothing (lerp) logic.\r\n * Useful for animated cursor effects and interaction modules.\r\n */\r\nexport class CursorController {\r\n /** Context providing access to shared data, tools, and settings. */\r\n protected context: StringContext\r\n\r\n /** Threshold below which cursor is considered settled (no movement). */\r\n private readonly SETTLE_THRESHOLD = 0.1\r\n\r\n /** Smoothing factor used to interpolate cursor movement. */\r\n private smoothingFactor: number\r\n\r\n /**\r\n * Constructs a new `CursorController` instance.\r\n * @param smoothing The initial lerp smoothing factor (0 to 1).\r\n * @param context The shared context containing state and tools.\r\n */\r\n constructor(smoothing: number = 0.1, context: StringContext) {\r\n this.smoothingFactor = smoothing\r\n this.context = context\r\n this.onSettingsChange()\r\n }\r\n\r\n /**\r\n * Updates the target cursor position from a mouse event.\r\n * This is the raw position that smoothing will interpolate toward.\r\n * @param e MouseEvent with current cursor position.\r\n */\r\n public onMouseMove(e: MouseEvent): void {\r\n this.context.data.cursor.targetX = e.clientX\r\n this.context.data.cursor.targetY = e.clientY\r\n }\r\n\r\n /**\r\n * Updates smoothed cursor position using linear interpolation (lerp).\r\n * Should be called on every animation frame.\r\n * Handles snapping when movement is below threshold.\r\n */\r\n public onFrame(): void {\r\n const { targetX, targetY, smoothedX, smoothedY } = this.context.data.cursor\r\n\r\n const stepX = this.context.tools.lerp.process({ from: smoothedX, to: targetX, progress: this.smoothingFactor })\r\n const stepY = this.context.tools.lerp.process({ from: smoothedY, to: targetY, progress: this.smoothingFactor })\r\n\r\n const distance = this.getStepDistance(stepX, stepY)\r\n\r\n if (this.isSettled(distance)) {\r\n this.snapToTarget()\r\n } else {\r\n this.applyStep(stepX, stepY)\r\n }\r\n }\r\n\r\n /**\r\n * Called when global settings change.\r\n * Updates the internal lerp factor from context settings.\r\n */\r\n public onSettingsChange(): void {\r\n let lerp = Number(this.context.settings['lerp'])\r\n this.setLerpFactor(lerp)\r\n }\r\n\r\n /**\r\n * Dynamically adjusts the smoothing factor using adaptive mapping.\r\n * @param t The raw input lerp value (usually from 0 to 1).\r\n */\r\n public setLerpFactor(t: number): void {\r\n this.smoothingFactor = this.context.tools.adaptiveLerp.process({ \r\n value: t,\r\n inMin: 0.1,\r\n inMax: 1.0,\r\n outMin: 0.05,\r\n outMax: 0.65\r\n })\r\n }\r\n\r\n /**\r\n * Calculates the Euclidean distance from the cursor step.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n * @returns The length of the movement vector.\r\n */\r\n private getStepDistance(x: number, y: number): number {\r\n return Math.hypot(x, y)\r\n }\r\n\r\n /**\r\n * Determines whether the movement is below the settle threshold.\r\n * @param distance Distance between smoothed and target positions.\r\n * @returns Whether the cursor should snap to target.\r\n */\r\n private isSettled(distance: number): boolean {\r\n return distance < this.SETTLE_THRESHOLD\r\n }\r\n\r\n /**\r\n * Immediately sets smoothed position to the target and zeroes deltas.\r\n */\r\n private snapToTarget(): void {\r\n this.context.data.cursor.smoothedX = this.context.data.cursor.targetX\r\n this.context.data.cursor.smoothedY = this.context.data.cursor.targetY\r\n this.context.data.cursor.stepX = 0\r\n this.context.data.cursor.stepY = 0\r\n }\r\n\r\n /**\r\n * Applies lerped movement step to smoothed position and stores delta.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n */\r\n private applyStep(x: number, y: number): void {\r\n this.context.data.cursor.smoothedX += x\r\n this.context.data.cursor.smoothedY += y\r\n this.context.data.cursor.stepX = x\r\n this.context.data.cursor.stepY = y\r\n }\r\n}\r\n","import { EventCallback } from \"../../models/event/EventCallback\";\r\n\r\n/**\r\n * Manages custom event subscriptions and dispatching.\r\n * Allows multiple listeners per event and supports optional `id` suffixing.\r\n */\r\nexport class EventManager {\r\n private listeners: Record<string, Set<EventCallback<any>>> = {};\r\n\r\n /**\r\n * Subscribes to an event.\r\n * Optionally appends an `id` to the event name for namespacing.\r\n *\r\n * @param eventName The base event name (e.g. \"scroll\", \"update\").\r\n * @param callback The function to call when the event is emitted.\r\n * @param id Optional unique identifier to scope the event (e.g. element ID).\r\n */\r\n on<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string\r\n ): void {\r\n const fullEvent = id ? `${eventName}_${id}` : eventName;\r\n\r\n if (!this.listeners[fullEvent]) {\r\n this.listeners[fullEvent] = new Set();\r\n }\r\n this.listeners[fullEvent].add(callback);\r\n }\r\n\r\n /**\r\n * Unsubscribes from a specific event listener.\r\n * Must match the original `eventName`, `callback`, and optional `id`.\r\n *\r\n * @param eventName The base event name to unsubscribe from.\r\n * @param callback The callback function to remove.\r\n * @param id Optional identifier used when subscribing.\r\n */\r\n off<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string\r\n ): void {\r\n const fullEvent = id ? `${eventName}_${id}` : eventName;\r\n\r\n if (this.listeners[fullEvent]) {\r\n this.listeners[fullEvent].delete(callback);\r\n }\r\n }\r\n\r\n /**\r\n * Emits an event with an optional payload.\r\n * All matching listeners will be called.\r\n *\r\n * @param eventName The full event name (must include `id` if used).\r\n * @param payload Optional data passed to event listeners.\r\n */\r\n emit<T = any>(eventName: string, payload?: T): void {\r\n const set = this.listeners[eventName];\r\n if (!set) return;\r\n\r\n for (const callback of set) {\r\n callback(payload as T);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object progress event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle progress value.\r\n */\r\n onProgress(id: string, callback: EventCallback<number>): void {\r\n this.on(`progress:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object progress event.\r\n * @param id The object ID.\r\n * @param value The progress value.\r\n */\r\n emitProgress(id: string, value: number): void {\r\n this.emit(`progress:${id}`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object in-view event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle visibility.\r\n */\r\n onInview(id: string, callback: EventCallback<boolean>): void {\r\n this.on(`object:inview:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object in-view event.\r\n * @param id The object ID.\r\n * @param visible Whether the object is visible.\r\n */\r\n emitInview(id: string, visible: boolean): void {\r\n this.emit(`object:inview${id}`, visible);\r\n }\r\n\r\n /**\r\n * Subscribes to the global scroll event.\r\n * @param callback The callback to handle scroll value.\r\n */\r\n onScroll(callback: EventCallback<number>): void {\r\n this.on(`scroll`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global scroll event.\r\n * @param value The scroll value.\r\n */\r\n emitScroll(value: number): void {\r\n this.emit(`scroll`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to the global update event.\r\n * @param callback The callback to handle update.\r\n */\r\n onUpdate(callback: EventCallback<void>): void {\r\n this.on(`update`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global update event.\r\n */\r\n emitUpdate(): void {\r\n this.emit(`update`);\r\n }\r\n\r\n /**\r\n * Clears all listeners for a specific event.\r\n *\r\n * @param eventName The full event name (including optional `id`).\r\n */\r\n clear(eventName: string): void {\r\n delete this.listeners[eventName];\r\n }\r\n\r\n /**\r\n * Clears all registered events.\r\n */\r\n clearAll(): void {\r\n this.listeners = {};\r\n }\r\n}\r\n","import { StringData } from \"../..\";\r\nimport { IStringModule } from \"../IStringModule\";\r\nimport { StringModule } from \"../StringModule\";\r\n\r\n/**\r\n * Central manager for registering, tracking and delegating lifecycle events \r\n * to core and UI modules in the system.\r\n * \r\n * Handles scroll, resize, animation frame updates and DOM events.\r\n */\r\nexport class ModuleManager {\r\n /** All core logic modules (e.g., scroll, in-view, parallax). */\r\n private modules: StringModule[] = [];\r\n\r\n /** All UI or visual/interaction-based modules (e.g., cursor, split text). */\r\n private uiModules: StringModule[] = [];\r\n\r\n /**\r\n * @param data Shared state container for scroll, viewport, etc.\r\n */\r\n constructor(private data: StringData) {}\r\n\r\n /**\r\n * Registers a module into the appropriate group based on its type.\r\n * @param module The module instance to register.\r\n */\r\n register(module: StringModule): void {\r\n if (module.type === 1) this.modules.push(module);\r\n if (module.type === 2) this.uiModules.push(module);\r\n }\r\n\r\n /**\r\n * Finds the first registered module of the given class/type.\r\n * @param type The module class constructor.\r\n * @returns The instance, if found.\r\n */\r\n find<T>(type: new (...args: any[]) => T): T | undefined {\r\n return this.modules.find(m => m instanceof type) as T | undefined;\r\n }\r\n\r\n /** Invokes `onInit` on all modules. */\r\n onInit(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onInit());\r\n }\r\n\r\n /** Invokes `onFrame` on all modules, passing shared state. */\r\n onFrame(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onFrame(this.data));\r\n }\r\n\r\n /** Invokes `onScroll` on all modules with current scroll state. */\r\n onScroll(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScroll(this.data));\r\n }\r\n\r\n /** Invokes `onResize` on all modules. */\r\n onResize(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onResize());\r\n }\r\n\r\n /**\r\n * Delegates mouse movement events to modules.\r\n * @param e The mousemove event.\r\n */\r\n onMouseMove(e: MouseEvent): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onMouseMove(e));\r\n }\r\n\r\n /**\r\n * Delegates wheel events to modules.\r\n * @param e The wheel event.\r\n */\r\n onWheel(e: WheelEvent): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onWheel(e));\r\n }\r\n\r\n /** Notifies all modules that scroll has changed diraction. */\r\n onDirectionChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDirectionChange());\r\n }\r\n\r\n /** Notifies all modules that scroll has started. */\r\n onScrollStart(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollStart());\r\n }\r\n\r\n /** Notifies all modules that scroll has stopped. */\r\n onScrollStop(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollStop());\r\n }\r\n\r\n /** Notifies all modules that scroll axis (horizontal/vertical) has changed. */\r\n onAxisChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onAxisChange());\r\n }\r\n\r\n /** Notifies all modules that device type (desktop/mobile) has changed. */\r\n onDeviceChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDeviceChange());\r\n }\r\n\r\n /** Notifies modules of updates to scroll-related configuration. */\r\n onScrollConfigChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onScrollConfigChange());\r\n }\r\n\r\n /** Notifies modules of updated global or module-specific settings. */\r\n onSettingsChange(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onSettingsChange());\r\n }\r\n\r\n /**\r\n * Called when DOM is mutated — e.g. new elements added/removed.\r\n * @param added Newly added DOM nodes.\r\n * @param removed Removed DOM nodes.\r\n */\r\n onDOMMutate(added: NodeList, removed: NodeList): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.onDOMMutate(added, removed));\r\n }\r\n\r\n /** Cleans up all modules and clears internal lists. */\r\n destroy(): void {\r\n [...this.modules, ...this.uiModules].forEach(m => m.destroy());\r\n this.modules = [];\r\n this.uiModules = [];\r\n }\r\n\r\n /**\r\n * Returns all modules (core + UI) as a flat array.\r\n */\r\n get all(): IStringModule[] {\r\n return [...this.modules, ...this.uiModules];\r\n }\r\n\r\n /**\r\n * Returns only core modules (type === 1).\r\n */\r\n get core(): IStringModule[] {\r\n return this.modules;\r\n }\r\n\r\n /**\r\n * Returns only UI modules (type === 2).\r\n */\r\n get ui(): IStringModule[] {\r\n return this.uiModules;\r\n }\r\n}\r\n","import { IStringModule } from \"../core/IStringModule\";\r\nimport { EventManager } from \"../core/managers/EventManager\";\r\n\r\n/**\r\n * Internal class representing a DOM-bound interactive object.\r\n * Connected to modules and holds its own internal state.\r\n */\r\nexport class StringObject {\r\n /**\r\n * The DOM element this object wraps.\r\n */\r\n public htmlElement: HTMLElement;\r\n\r\n /**\r\n * Unique global ID assigned by the system.\r\n */\r\n public id: string = \"\";\r\n\r\n /**\r\n * Space-separated list of all attribute keys associated with this object.\r\n */\r\n public keys: string[] = [];\r\n\r\n /**\r\n * A list of elements that should be affected in sync with this one.\r\n */\r\n public connects: HTMLElement[] = [];\r\n\r\n /**\r\n * Internal key-value store of dynamic object properties (like offsets, progress, etc.).\r\n */\r\n private properties: Map<string, any> = new Map();\r\n\r\n /**\r\n * Modules currently connected to this object.\r\n */\r\n private modules: IStringModule[] = [];\r\n\r\n /**\r\n * Manages and handles events for the object.\r\n * Provides functionality to register, trigger, and manage event listeners.\r\n */\r\n events: EventManager = new EventManager();\r\n\r\n constructor(id: string, element: HTMLElement) {\r\n this.htmlElement = element;\r\n this.id = id;\r\n }\r\n\r\n /**\r\n * Stores a property value for this object.\r\n * @param key - Property name\r\n * @param value - Value to store\r\n */\r\n public setProperty<T>(key: string, value: T): void {\r\n this.properties.set(key, value);\r\n }\r\n\r\n /**\r\n * Retrieves a previously stored property value.\r\n * @param key - Property name\r\n * @returns The value or null if not set\r\n */\r\n public getProperty<T>(key: string): T {\r\n return this.properties.get(key) ?? null;\r\n }\r\n\r\n /**\r\n * Marks this object as \"active\" (usually on intersection/scroll enter).\r\n */\r\n public enter(): void {\r\n this.events.emit(\"enter\", this);\r\n this.setProperty(\"active\", true);\r\n this.modules.forEach((module) => {\r\n module.enterObject(this.id, this);\r\n });\r\n }\r\n\r\n /**\r\n * Marks this object as \"inactive\" (usually on intersection/scroll leave).\r\n */\r\n public leave(): void {\r\n this.events.emit(\"leave\", this);\r\n this.setProperty(\"active\", false);\r\n this.modules.forEach((module) => {\r\n module.exitObject(this.id);\r\n });\r\n }\r\n\r\n /**\r\n * Shows the object, applies visual class and notifies connected modules.\r\n */\r\n public show(): void {\r\n this.htmlElement.classList.add(\"-inview\");\r\n }\r\n\r\n /**\r\n * Hides the object, removes visual class (if repeat is enabled), and notifies modules.\r\n */\r\n public hide(): void {\r\n const shouldRepeat = this.getProperty<boolean>(\"repeat\");\r\n if (shouldRepeat) {\r\n this.htmlElement.classList.remove(\"-inview\");\r\n }\r\n }\r\n\r\n /**\r\n * Connects a module to this object if not already connected.\r\n * @param module - The module to connect\r\n */\r\n public connect(module: IStringModule): void {\r\n if (!this.modules.includes(module)) {\r\n this.modules.push(module);\r\n }\r\n }\r\n}\r\n","import { ModuleManager } from \"./ModuleManager\";\r\nimport { StringData } from \"../StringData\";\r\nimport { StringObject } from \"../../objects/StringObject\";\r\nimport { EventManager } from \"./EventManager\";\r\n\r\nexport class ObjectManager {\r\n private objects = new Map<string, StringObject>();\r\n private connectQueue: { id: string; element: HTMLElement }[] = [];\r\n private globalId = 1;\r\n\r\n constructor(\r\n private data: StringData,\r\n private modules: ModuleManager,\r\n private events: EventManager\r\n ) {}\r\n\r\n /**\r\n * Returns the object map (read-only).\r\n */\r\n get all(): ReadonlyMap<string, StringObject> {\r\n return this.objects;\r\n }\r\n\r\n /**\r\n * Adds a new object from an element.\r\n */\r\n public add(el: HTMLElement) {\r\n const idAttr = el.getAttribute(\"string-id\") ?? `string-${this.globalId++}`;\r\n\r\n const object =\r\n idAttr && this.objects.has(idAttr)\r\n ? this.objects.get(idAttr)!\r\n : new StringObject(idAttr, el);\r\n\r\n el.setAttribute(\"string-id\", object.id);\r\n\r\n const keysAttr =\r\n el.getAttribute(\"string\") ?? el.getAttribute(\"data-string\");\r\n\r\n if (keysAttr) {\r\n object.keys = (keysAttr ?? \"\").split(\"|\");\r\n }\r\n\r\n el.setAttribute(\"string-inited\", \"\");\r\n this.objects.set(object.id, object);\r\n\r\n const attributes = this.getAllAttributes(el);\r\n\r\n // Delegate core setup (dimensions, offsets, key, start/end, etc.)\r\n this.modules.core.forEach((m) => {\r\n if (\r\n \"setupCoreProperties\" in m &&\r\n typeof m[\"setupCoreProperties\"] === \"function\"\r\n ) {\r\n (m as any).setupCoreProperties(object, el, attributes);\r\n }\r\n });\r\n\r\n // Try connecting to modules\r\n this.modules.core.forEach((m) => {\r\n if (m.canConnect(object)) {\r\n m.initializeObject(this.globalId, object, el, attributes);\r\n m.calculatePositions(object, this.data.viewport.windowHeight);\r\n m.connectObject(object);\r\n }\r\n });\r\n\r\n // Restore connect-from\r\n const queueItems = this.connectQueue.filter((q) => q.id === object.id);\r\n queueItems.forEach((item) => object.connects.push(item.element));\r\n this.connectQueue = this.connectQueue.filter((q) => q.id !== object.id);\r\n\r\n // Set up observers\r\n this.initObservers(object, el);\r\n }\r\n\r\n /**\r\n * Removes an object by its id.\r\n */\r\n public remove(id: string) {\r\n const obj = this.objects.get(id);\r\n if (!obj) return;\r\n\r\n obj.events.clearAll();\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n obj.getProperty<IntersectionObserver>(\"observer-inview\")?.disconnect();\r\n\r\n obj.htmlElement.removeAttribute(\"string-inited\");\r\n obj.leave();\r\n\r\n this.objects.delete(id);\r\n }\r\n\r\n /**\r\n * Add an element that will connect later.\r\n */\r\n public enqueueConnection(id: string, element: HTMLElement) {\r\n this.connectQueue.push({ id, element });\r\n }\r\n\r\n private getAllAttributes(el: HTMLElement): Record<string, any> {\r\n const attributes: Record<string, any> = {};\r\n Array.from(el.attributes).forEach((attr) => {\r\n attributes[attr.name] = attr.value;\r\n });\r\n return attributes;\r\n }\r\n\r\n private initObservers(obj: StringObject, el: HTMLElement) {\r\n const start = obj.getProperty<number>(\"offset-top\") ?? 0;\r\n const end = obj.getProperty<number>(\"offset-bottom\") ?? 0;\r\n const inviewTop = obj.getProperty<number>(\"inview-top\") ?? 0;\r\n const inviewBottom = obj.getProperty<number>(\"inview-bottom\") ?? 0;\r\n\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n obj.getProperty<IntersectionObserver>(\"observer-inview\")?.disconnect();\r\n\r\n const progressCallback = (entries: IntersectionObserverEntry[]) => {\r\n entries.forEach((e) => {\r\n this.events.emit(`object:activate:${obj.id}`, e.isIntersecting);\r\n e.isIntersecting ? obj.enter() : obj.leave();\r\n });\r\n };\r\n\r\n const inviewCallback = (entries: IntersectionObserverEntry[]) => {\r\n entries.forEach((e) => {\r\n this.events.emit(`object:inview:${obj.id}`, e.isIntersecting);\r\n e.isIntersecting ? obj.show() : obj.hide();\r\n });\r\n };\r\n\r\n const progressObserver = new IntersectionObserver(progressCallback, {\r\n root: null,\r\n rootMargin: `${end + this.data.viewport.windowHeight}px 0px ${\r\n start + this.data.viewport.windowHeight\r\n }px 0px`,\r\n threshold: 0.001,\r\n });\r\n\r\n const inviewObserver = new IntersectionObserver(inviewCallback, {\r\n root: null,\r\n rootMargin: `${end + inviewTop}px 0px ${start + inviewBottom}px 0px`,\r\n threshold: 0.001,\r\n });\r\n\r\n progressObserver.observe(el);\r\n inviewObserver.observe(el);\r\n\r\n obj.setProperty(\"observer-progress\", progressObserver);\r\n obj.setProperty(\"observer-inview\", inviewObserver);\r\n }\r\n\r\n /**\r\n * Observes DOM mutations to auto-add/remove elements with [string] attribute.\r\n * Should be called once after DOM is ready.\r\n */\r\n public observeDOM(): void {\r\n const observer = new MutationObserver((mutations) => {\r\n mutations.forEach((mutation) => {\r\n if (mutation.type === \"childList\") {\r\n // Removed elements\r\n mutation.removedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (element.hasAttribute(\"string\")) {\r\n this.handleRemoved(element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\"[string],[data-string]\")\r\n .forEach((child) => {\r\n if (this.isFixed(child as HTMLElement)) return;\r\n this.handleRemoved(child as HTMLElement);\r\n });\r\n });\r\n\r\n // Added elements\r\n mutation.addedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (\r\n element.hasAttribute(\"string\") &&\r\n !element.hasAttribute(\"string-inited\")\r\n ) {\r\n this.add(element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\r\n \"[string]:not([string-inited]),[data-string]:not([string-inited])\"\r\n )\r\n .forEach((child) => this.add(child as HTMLElement));\r\n\r\n // Check for connect-from logic\r\n const copyFrom =\r\n element.getAttribute(\"string-copy-from\") ??\r\n element.getAttribute(\"data-string-copy-from\");\r\n if (copyFrom) {\r\n if (this.objects.has(copyFrom)) {\r\n this.objects.get(copyFrom)!.connects.push(element);\r\n } else {\r\n this.enqueueConnection(copyFrom, element);\r\n }\r\n }\r\n });\r\n\r\n // Let modules know about DOM rebuild\r\n this.modules.all.for