From dba4e9812d6f16c2bd1c06133e2f6574fd546b66 Mon Sep 17 00:00:00 2001 From: boston343 Date: Wed, 23 Jul 2025 11:13:11 -0400 Subject: [PATCH 01/10] docs updates --- apps/docs/src/content/docs/getting-started/installation.mdx | 4 ++-- apps/docs/src/content/docs/reference/api.mdx | 2 +- apps/docs/src/content/docs/reference/presets.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/content/docs/getting-started/installation.mdx b/apps/docs/src/content/docs/getting-started/installation.mdx index e9ce254..844eccd 100644 --- a/apps/docs/src/content/docs/getting-started/installation.mdx +++ b/apps/docs/src/content/docs/getting-started/installation.mdx @@ -30,7 +30,7 @@ yarn add motion-on-scroll ## Minimal bootstrapping @@ -86,7 +86,7 @@ That’s it! Every element declaring a `data-mos` value is now animated when it ### CSS file -MOS ships with a small stylesheet that ensures elements are hidden upon initial page load. Import it from `motion-on-scroll/dist/mos.css` or copy the file into your own pipeline. The contents of the file are copied below for convenience. +MOS ships with a small stylesheet that ensures elements are hidden upon initial page load. Import it from `motion-on-scroll/dist/mos.css` or copy the file into your own pipeline. The contents of the file are below for convenience. ```css /* no pointer events until animation starts */ diff --git a/apps/docs/src/content/docs/reference/api.mdx b/apps/docs/src/content/docs/reference/api.mdx index 90519e3..a74c48a 100644 --- a/apps/docs/src/content/docs/reference/api.mdx +++ b/apps/docs/src/content/docs/reference/api.mdx @@ -29,7 +29,7 @@ MOS.init({ *Type*: `number` (pixels) *Default*: `120` -Additional distance (in pixels) before an element is considered *in view* (applied via `rootMargin`). +Additional distance (in pixels) before an element is considered *in view*. #### duration diff --git a/apps/docs/src/content/docs/reference/presets.mdx b/apps/docs/src/content/docs/reference/presets.mdx index 5bd348d..8e82b6c 100644 --- a/apps/docs/src/content/docs/reference/presets.mdx +++ b/apps/docs/src/content/docs/reference/presets.mdx @@ -5,7 +5,7 @@ description: Animation presets shipped with Motion-On-Scroll and how to extend t ## Available presets -All of the standard AOS presets are supported. +All of the standard AOS presets are supported. You can see them in action on the [homepage](/)! ### Fades From 28381b00bbb423efc82f9abaa6b6b538f56ae0d1 Mon Sep 17 00:00:00 2001 From: boston343 Date: Wed, 23 Jul 2025 11:24:09 -0400 Subject: [PATCH 02/10] add tests --- .../src/__tests__/index.spec.ts | 535 ++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 packages/motion-on-scroll/src/__tests__/index.spec.ts diff --git a/packages/motion-on-scroll/src/__tests__/index.spec.ts b/packages/motion-on-scroll/src/__tests__/index.spec.ts new file mode 100644 index 0000000..d5b4cbe --- /dev/null +++ b/packages/motion-on-scroll/src/__tests__/index.spec.ts @@ -0,0 +1,535 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { DEFAULT_OPTIONS } from "../helpers/constants.js"; + +// Mock all dependencies +vi.mock("../helpers/animations.js", () => ({ + registerAnimation: vi.fn(), +})); + +vi.mock("../helpers/easing.js", () => ({ + registerEasing: vi.fn(), +})); + +vi.mock("../helpers/elements.js", () => ({ + clearAllElements: vi.fn(), + getMosElements: vi.fn(() => []), + prepareElements: vi.fn(), +})); + +vi.mock("../helpers/keyframes.js", () => ({ + registerKeyframes: vi.fn(), +})); + +vi.mock("../helpers/observer.js", () => ({ + startDomObserver: vi.fn(), +})); + +vi.mock("../helpers/scroll-handler.js", () => ({ + cleanupScrollHandler: vi.fn(), + ensureScrollHandlerActive: vi.fn(), + evaluateElementPositions: vi.fn(), + updateScrollHandlerDelays: vi.fn(), +})); + +vi.mock("../helpers/utils.js", () => ({ + debounce: vi.fn((fn) => fn), + isDisabled: vi.fn(() => false), + removeMosAttributes: vi.fn(), +})); + +// Import mocked functions +import { registerAnimation } from "../helpers/animations.js"; +import { registerEasing } from "../helpers/easing.js"; +import { + clearAllElements, + getMosElements, + prepareElements, +} from "../helpers/elements.js"; +import { registerKeyframes } from "../helpers/keyframes.js"; +import { startDomObserver } from "../helpers/observer.js"; +import { + cleanupScrollHandler, + ensureScrollHandlerActive, + evaluateElementPositions, + updateScrollHandlerDelays, +} from "../helpers/scroll-handler.js"; +import { debounce, isDisabled, removeMosAttributes } from "../helpers/utils.js"; + +describe("index.ts - Main Entry Point", () => { + let mockElement1: HTMLElement; + let mockElement2: HTMLElement; + let MOS: any; + let init: any; + let refresh: any; + let refreshHard: any; + let handleLayoutChange: any; + let setupStartEventListener: any; + + beforeEach(async () => { + // Reset all mocks + vi.clearAllMocks(); + + // Create mock DOM elements + mockElement1 = document.createElement("div"); + mockElement1.setAttribute("data-mos", "fade"); + mockElement1.setAttribute("id", "element1"); + + mockElement2 = document.createElement("div"); + mockElement2.setAttribute("data-mos", "slide-up"); + mockElement2.setAttribute("id", "element2"); + + // Mock getMosElements to return our test elements + vi.mocked(getMosElements).mockReturnValue([mockElement1, mockElement2]); + + // Mock document.readyState since it's read-only + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'loading' + }); + + // Mock global MutationObserver + global.MutationObserver = vi.fn(() => ({ + observe: vi.fn(), + disconnect: vi.fn(), + takeRecords: vi.fn() + })) as any; + + // Reset modules and re-import to get fresh state + vi.resetModules(); + const indexModule = await import("../index.js"); + MOS = indexModule.MOS; + init = indexModule.init; + refresh = indexModule.refresh; + refreshHard = indexModule.refreshHard; + handleLayoutChange = indexModule.handleLayoutChange; + setupStartEventListener = indexModule.setupStartEventListener; + }); + + describe("MOS object export", () => { + it("should export MOS object with all required methods", () => { + expect(MOS).toBeDefined(); + expect(MOS.init).toBeDefined(); + expect(MOS.refresh).toBeDefined(); + expect(MOS.refreshHard).toBeDefined(); + expect(MOS.registerKeyframes).toBeDefined(); + expect(MOS.registerEasing).toBeDefined(); + expect(MOS.registerAnimation).toBeDefined(); + }); + + it("should have function exports matching MOS object", () => { + expect(typeof init).toBe("function"); + expect(typeof refresh).toBe("function"); + expect(typeof refreshHard).toBe("function"); + }); + }); + + describe("init()", () => { + it("should initialize with default options when no options provided", () => { + const result = init(); + + expect(getMosElements).toHaveBeenCalled(); + expect(result).toEqual([mockElement1, mockElement2]); + }); + + it("should merge provided options with defaults", () => { + const customOptions = { + duration: 800, + easing: "ease-in-out", + delay: 100, + }; + + init(customOptions); + + // Should still find elements + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should handle time unit conversion on first init", () => { + const optionsWithSeconds = { + timeUnits: "s" as const, + }; + + init(optionsWithSeconds); + + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should handle global disable by cleaning up elements", () => { + vi.mocked(isDisabled).mockReturnValue(true); + + const result = init(); + + // removeMosAttributes is called with forEach, so it gets (element, index, array) + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [mockElement1, mockElement2]); + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [mockElement1, mockElement2]); + expect(result).toEqual([]); + }); + + it("should start DOM observer when not disabled and MutationObserver exists", () => { + // Mock isDisabled to return false so we don't exit early + vi.mocked(isDisabled).mockReturnValue(false); + + init({ disableMutationObserver: false }); + + expect(startDomObserver).toHaveBeenCalled(); + }); + + it("should not start DOM observer when disabled", () => { + init({ disableMutationObserver: true }); + + expect(startDomObserver).not.toHaveBeenCalled(); + }); + + it("should not start DOM observer when MutationObserver is not supported", () => { + // Remove MutationObserver + const originalMutationObserver = global.MutationObserver; + delete (global as any).MutationObserver; + + init(); + + expect(startDomObserver).not.toHaveBeenCalled(); + + // Restore MutationObserver + global.MutationObserver = originalMutationObserver; + }); + + it("should refresh when called multiple times", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // First init + init(); + expect(getMosElements).toHaveBeenCalledTimes(1); + + // Second init should trigger refresh + init({ duration: 600 }); + expect(getMosElements).toHaveBeenCalledTimes(2); + }); + }); + + describe("refresh()", () => { + it("should not activate library when shouldActivate is false and library is inactive", () => { + refresh(false); + + // Should not call any scroll handler functions when library is inactive + expect(updateScrollHandlerDelays).not.toHaveBeenCalled(); + expect(prepareElements).not.toHaveBeenCalled(); + expect(ensureScrollHandlerActive).not.toHaveBeenCalled(); + expect(evaluateElementPositions).not.toHaveBeenCalled(); + }); + + it("should activate library when shouldActivate is true", () => { + refresh(true); + + expect(updateScrollHandlerDelays).toHaveBeenCalled(); + expect(getMosElements).toHaveBeenCalled(); + expect(prepareElements).toHaveBeenCalledWith([mockElement1, mockElement2], DEFAULT_OPTIONS); + expect(ensureScrollHandlerActive).toHaveBeenCalled(); + expect(evaluateElementPositions).toHaveBeenCalled(); + }); + + it("should update scroll handler delays with library config", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // First init to set library as active + init({ throttleDelay: 150 }); + + // Call refresh with shouldActivate=true to ensure library is active + refresh(true); + + expect(updateScrollHandlerDelays).toHaveBeenCalledWith(150); + }); + + it("should prepare elements with current library config", () => { + const customOptions = { duration: 800, delay: 200 }; + + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // Init with custom options to set library config + init(customOptions); + + // Call refresh with shouldActivate=true to ensure library is active + refresh(true); + + expect(prepareElements).toHaveBeenCalledWith( + [mockElement1, mockElement2], + expect.objectContaining(customOptions) + ); + }); + }); + + describe("refreshHard()", () => { + it("should re-find elements and clear existing state", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // Set up library as active first + init(); + + refreshHard(); + + expect(getMosElements).toHaveBeenCalledWith(true); // Force refresh + expect(clearAllElements).toHaveBeenCalled(); + expect(cleanupScrollHandler).toHaveBeenCalled(); + }); + + it("should handle global disable during hard refresh", () => { + vi.mocked(isDisabled).mockReturnValue(true); + + refreshHard(); + + // removeMosAttributes is called with forEach, so it gets (element, index, array) + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [mockElement1, mockElement2]); + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [mockElement1, mockElement2]); + + // When disabled, function returns early - cleanup functions are NOT called + expect(clearAllElements).not.toHaveBeenCalled(); + expect(cleanupScrollHandler).not.toHaveBeenCalled(); + }); + + it("should call refresh after cleanup when not disabled", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + refreshHard(); + + expect(clearAllElements).toHaveBeenCalled(); + expect(cleanupScrollHandler).toHaveBeenCalled(); + + // refresh() is called, but updateScrollHandlerDelays only happens if library is active + // Since we haven't activated the library, updateScrollHandlerDelays won't be called + expect(getMosElements).toHaveBeenCalled(); // This should be called by refresh + }); + }); + + describe("handleLayoutChange()", () => { + it("should evaluate element positions when library is active", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // Activate library first + init(); + refresh(true); + + handleLayoutChange(); + + expect(evaluateElementPositions).toHaveBeenCalled(); + }); + + it("should not evaluate positions when library is inactive", () => { + handleLayoutChange(); + + expect(evaluateElementPositions).not.toHaveBeenCalled(); + }); + }); + + describe("setupStartEventListener()", () => { + beforeEach(() => { + // Mock addEventListener + vi.spyOn(window, "addEventListener"); + vi.spyOn(document, "addEventListener"); + }); + + it("should call refresh immediately when DOMContentLoaded already fired", async () => { + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'interactive' + }); + + // Re-import to get fresh module state with new readyState + vi.resetModules(); + const { setupStartEventListener } = await import("../index.js"); + + setupStartEventListener(); + + // Should not add event listener since DOM is already ready + expect(document.addEventListener).not.toHaveBeenCalledWith("DOMContentLoaded", expect.any(Function), expect.any(Object)); + }); + + it("should call refresh immediately when load already fired", async () => { + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'complete' + }); + + vi.resetModules(); + const { setupStartEventListener } = await import("../index.js"); + + setupStartEventListener(); + + // Should not add event listener since page is already loaded + expect(window.addEventListener).not.toHaveBeenCalledWith("load", expect.any(Function), expect.any(Object)); + }); + + it("should add window event listener for load event", () => { + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'loading' + }); + + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // Initialize with load event + init({ startEvent: "load" }); + + expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function), { once: true }); + }); + + it("should add document event listener for DOMContentLoaded", () => { + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'loading' + }); + + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + // Initialize with DOMContentLoaded (default) + init({ startEvent: "DOMContentLoaded" }); + + expect(document.addEventListener).toHaveBeenCalledWith("DOMContentLoaded", expect.any(Function), { once: true }); + }); + + it("should add document event listener for custom events", () => { + Object.defineProperty(document, 'readyState', { + writable: true, + value: 'loading' + }); + + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + init({ startEvent: "custom-event" }); + + expect(document.addEventListener).toHaveBeenCalledWith("custom-event", expect.any(Function), { once: true }); + }); + }); + + describe("Layout change event listeners", () => { + beforeEach(() => { + vi.spyOn(window, "addEventListener"); + }); + + it("should set up resize and orientation change listeners", () => { + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + init(); + + expect(window.addEventListener).toHaveBeenCalledWith("resize", expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith("orientationchange", expect.any(Function)); + }); + + it("should use debounced handler with configured delay", () => { + const customDebounceDelay = 200; + + // Mock isDisabled to return false + vi.mocked(isDisabled).mockReturnValue(false); + + init({ debounceDelay: customDebounceDelay }); + + expect(debounce).toHaveBeenCalledWith(expect.any(Function), customDebounceDelay); + }); + }); + + describe("Registration functions", () => { + it("should expose registerKeyframes function", () => { + const mockKeyframes = { "custom-fade": { from: { opacity: 0 }, to: { opacity: 1 } } }; + + MOS.registerKeyframes(mockKeyframes); + + expect(registerKeyframes).toHaveBeenCalledWith(mockKeyframes); + }); + + it("should expose registerEasing function", () => { + const mockEasing = { "custom-ease": "cubic-bezier(0.25, 0.46, 0.45, 0.94)" }; + + MOS.registerEasing(mockEasing); + + expect(registerEasing).toHaveBeenCalledWith(mockEasing); + }); + + it("should expose registerAnimation function", () => { + const mockAnimation = { + name: "custom-animation", + factory: () => ({ opacity: [0, 1] }), + }; + + MOS.registerAnimation(mockAnimation); + + expect(registerAnimation).toHaveBeenCalledWith(mockAnimation); + }); + }); + + describe("Time units adjustment", () => { + it("should convert default duration from ms to seconds when timeUnits is 's'", () => { + const options = { + timeUnits: "s" as const, + // Don't specify duration - should use converted default + }; + + init(options); + + // The conversion should happen internally + // Default duration (400ms) should become 0.4s + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should convert default delay from ms to seconds when timeUnits is 's'", () => { + const options = { + timeUnits: "s" as const, + // Don't specify delay - should use converted default + }; + + init(options); + + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should not convert when timeUnits is 'ms'", () => { + const options = { + timeUnits: "ms" as const, + }; + + init(options); + + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should not convert on subsequent inits", () => { + // First init with seconds + init({ timeUnits: "s" }); + + // Second init should not trigger conversion again + init({ timeUnits: "s" }); + + expect(getMosElements).toHaveBeenCalledTimes(2); + }); + + it("should not convert when explicit duration is provided", () => { + const options = { + timeUnits: "s" as const, + duration: 2, // Explicit duration should not be converted + }; + + init(options); + + expect(getMosElements).toHaveBeenCalled(); + }); + + it("should not convert when explicit delay is provided", () => { + const options = { + timeUnits: "s" as const, + delay: 1, // Explicit delay should not be converted + }; + + init(options); + + expect(getMosElements).toHaveBeenCalled(); + }); + }); +}); From 7258152cf11be172e8fbf303992078c79c528205 Mon Sep 17 00:00:00 2001 From: boston343 Date: Wed, 23 Jul 2025 21:49:13 -0400 Subject: [PATCH 03/10] fix setting units in init function without explicitly setting duration and delay, and add additional tests for verification. --- apps/docs/src/components/MosDemo.astro | 1 + .../src/__tests__/index.spec.ts | 56 +++++++++++++++++++ packages/motion-on-scroll/src/index.ts | 8 +-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/components/MosDemo.astro b/apps/docs/src/components/MosDemo.astro index a6dfd2e..cb35c9b 100644 --- a/apps/docs/src/components/MosDemo.astro +++ b/apps/docs/src/components/MosDemo.astro @@ -169,6 +169,7 @@ const animationSections = [ duration: 800, offset: 50, once: false, + timeUnits: "ms", mirror: false, }); diff --git a/packages/motion-on-scroll/src/__tests__/index.spec.ts b/packages/motion-on-scroll/src/__tests__/index.spec.ts index d5b4cbe..7215e5c 100644 --- a/packages/motion-on-scroll/src/__tests__/index.spec.ts +++ b/packages/motion-on-scroll/src/__tests__/index.spec.ts @@ -531,5 +531,61 @@ describe("index.ts - Main Entry Point", () => { expect(getMosElements).toHaveBeenCalled(); }); + + it("should convert default duration and delay when timeUnits is 's' and no explicit values provided", () => { + // Mock isDisabled to return false so init completes + vi.mocked(isDisabled).mockReturnValue(false); + + // Mock prepareElements to capture the config that gets passed + let capturedConfig: any; + vi.mocked(prepareElements).mockImplementation((elements, config) => { + capturedConfig = config; + return []; // Return empty array to match MosElement[] return type + }); + + // Call init with timeUnits: "s" but no explicit duration or delay + init({ timeUnits: "s" }); + + // Trigger refresh to ensure prepareElements gets called with the config + refresh(true); + + // Verify that the config passed to prepareElements has converted defaults + expect(capturedConfig).toBeDefined(); + expect(capturedConfig.timeUnits).toBe("s"); + + // Default duration should be converted: 400ms -> 0.4s + expect(capturedConfig.duration).toBe(0.4); + + // Default delay should be converted: 0ms -> 0s + expect(capturedConfig.delay).toBe(0); + }); + + it("should not convert defaults when explicit duration and delay are provided with timeUnits 's'", () => { + // Mock isDisabled to return false so init completes + vi.mocked(isDisabled).mockReturnValue(false); + + // Mock prepareElements to capture the config that gets passed + let capturedConfig: any; + vi.mocked(prepareElements).mockImplementation((elements, config) => { + capturedConfig = config; + return []; // Return empty array to match MosElement[] return type + }); + + // Call init with timeUnits: "s" AND explicit duration/delay + init({ + timeUnits: "s", + duration: 1.5, // Explicit value should not be converted + delay: 0.2 // Explicit value should not be converted + }); + + // Trigger refresh to ensure prepareElements gets called with the config + refresh(true); + + // Verify that the explicit values are preserved (not converted) + expect(capturedConfig).toBeDefined(); + expect(capturedConfig.timeUnits).toBe("s"); + expect(capturedConfig.duration).toBe(1.5); // Should remain as provided + expect(capturedConfig.delay).toBe(0.2); // Should remain as provided + }); }); }); diff --git a/packages/motion-on-scroll/src/index.ts b/packages/motion-on-scroll/src/index.ts index 1cf9d38..f5bf45b 100644 --- a/packages/motion-on-scroll/src/index.ts +++ b/packages/motion-on-scroll/src/index.ts @@ -42,16 +42,16 @@ let isLibraryActive = false; * Only applied on first initialization when timeUnits is set to "s" * @param config - The configuration object to potentially modify */ -function adjustTimeUnitsOnFirstInit(config: MosOptions): void { +function adjustTimeUnitsOnFirstInit(config: MosOptions, newOptions: PartialMosOptions): void { if (isLibraryActive || config.timeUnits !== "s") return; // Convert default duration from ms to seconds if not explicitly set - if (config.duration == null) { + if (newOptions.duration == null) { config.duration = DEFAULT_OPTIONS.duration / 1000; } // Convert default delay from ms to seconds if not explicitly set - if (config.delay == null) { + if (newOptions.delay == null) { config.delay = DEFAULT_OPTIONS.delay / 1000; } } @@ -116,7 +116,7 @@ function init(options: PartialMosOptions = {}): HTMLElement[] { libraryConfig = { ...DEFAULT_OPTIONS, ...options }; // Handle time unit conversion on first initialization - adjustTimeUnitsOnFirstInit(libraryConfig); + adjustTimeUnitsOnFirstInit(libraryConfig, options); // If already initialized, just refresh with new options if (isLibraryActive) { From d2614ecf390f58ad4b7e53708d7a3f50e6308269 Mon Sep 17 00:00:00 2001 From: boston343 Date: Thu, 24 Jul 2025 21:38:12 -0400 Subject: [PATCH 04/10] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ddd5fdc..9af8836 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![NPM version](https://img.shields.io/npm/v/motion-on-scroll.svg?style=flat)](https://npmjs.org/package/motion-on-scroll) [![NPM downloads](https://img.shields.io/npm/dm/motion-on-scroll.svg?style=flat)](https://npmjs.org/package/motion-on-scroll) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/webreaper.svg?style=social)](https://twitter.com/webreaper) +Looking for the main package? Go to [motion-on-scroll](/packages/motion-on-scroll/). + ## Note: this is currently in beta - there are still various items to be ironed out. Feel free to try it though :) Effortless, AOS-compatible scroll animations powered by [Motion](https://motion.dev). From 4fdb42dba7a545a54d8df22405c4c799fbf8e474 Mon Sep 17 00:00:00 2001 From: boston343 Date: Thu, 24 Jul 2025 21:38:44 -0400 Subject: [PATCH 05/10] update README --- README.md | 2 +- packages/motion-on-scroll/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9af8836..d0fd261 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Looking for the main package? Go to [motion-on-scroll](/packages/motion-on-scroll/). -## Note: this is currently in beta - there are still various items to be ironed out. Feel free to try it though :) +## Note: this is currently in beta. Feel free to try it though :) Effortless, AOS-compatible scroll animations powered by [Motion](https://motion.dev). diff --git a/packages/motion-on-scroll/README.md b/packages/motion-on-scroll/README.md index ddd5fdc..57d5aa7 100644 --- a/packages/motion-on-scroll/README.md +++ b/packages/motion-on-scroll/README.md @@ -2,7 +2,7 @@ [![NPM version](https://img.shields.io/npm/v/motion-on-scroll.svg?style=flat)](https://npmjs.org/package/motion-on-scroll) [![NPM downloads](https://img.shields.io/npm/dm/motion-on-scroll.svg?style=flat)](https://npmjs.org/package/motion-on-scroll) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/webreaper.svg?style=social)](https://twitter.com/webreaper) -## Note: this is currently in beta - there are still various items to be ironed out. Feel free to try it though :) +## Note: this is currently in beta. Feel free to try it though :) Effortless, AOS-compatible scroll animations powered by [Motion](https://motion.dev). From ae9e4c504a7b15081acdb71edc2c78985a28e39c Mon Sep 17 00:00:00 2001 From: boston343 Date: Thu, 24 Jul 2025 21:40:43 -0400 Subject: [PATCH 06/10] docs(changeset): Fix setting units in init function when not explicitly setting duration and delay. Also add additional tests for verification. --- .changeset/sad-lights-repeat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sad-lights-repeat.md diff --git a/.changeset/sad-lights-repeat.md b/.changeset/sad-lights-repeat.md new file mode 100644 index 0000000..b3f4bf7 --- /dev/null +++ b/.changeset/sad-lights-repeat.md @@ -0,0 +1,5 @@ +--- +"motion-on-scroll": patch +--- + +Fix setting units in init function when not explicitly setting duration and delay. Also add additional tests for verification. From 39a154bf74879ec52a722cf5bd15fecbd06336ca Mon Sep 17 00:00:00 2001 From: boston343 Date: Fri, 25 Jul 2025 23:20:30 -0400 Subject: [PATCH 07/10] fix resize element flicker issue --- .../src/__tests__/scroll-handler.spec.ts | 28 +++++++++++++++++++ .../src/helpers/scroll-handler.ts | 8 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts index 9f6be70..61f0d92 100644 --- a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts +++ b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts @@ -287,6 +287,34 @@ describe("scroll-handler.ts", () => { // Should trigger play since scrollY (150) >= position.in (100) expect(play).toHaveBeenCalledWith(mockMosElement1); }); + + it("should preserve animated element states during resize (flicker fix)", () => { + // Setup: Create elements with different animation states + const animatedElement = { + ...mockMosElement1, + animated: true, // Already animated + }; + const nonAnimatedElement = { + ...mockMosElement2, + animated: false, // Not yet animated + }; + + vi.mocked(getPreparedElements).mockReturnValue([animatedElement, nonAnimatedElement]); + vi.mocked(isElementAboveViewport).mockReturnValue(false); + + evaluateElementPositions(); + + // Should recalculate positions for both elements + expect(getPositionIn).toHaveBeenCalledWith(animatedElement.element, animatedElement.options); + expect(getPositionIn).toHaveBeenCalledWith(nonAnimatedElement.element, nonAnimatedElement.options); + + // Should NOT reset initial state for already-animated element (prevents flicker) + expect(setInitialState).not.toHaveBeenCalledWith(animatedElement); + expect(setFinalState).not.toHaveBeenCalledWith(animatedElement); + + // Should still set initial state for non-animated element + expect(setInitialState).toHaveBeenCalledWith(nonAnimatedElement); + }); }); describe("scroll event processing", () => { diff --git a/packages/motion-on-scroll/src/helpers/scroll-handler.ts b/packages/motion-on-scroll/src/helpers/scroll-handler.ts index 4d93671..22684d9 100644 --- a/packages/motion-on-scroll/src/helpers/scroll-handler.ts +++ b/packages/motion-on-scroll/src/helpers/scroll-handler.ts @@ -131,11 +131,17 @@ function setElementInitialState(elementData: MosElement): void { /** * Refreshes all tracked elements by recalculating positions and states * Called when the library needs to update after configuration changes + * Preserves current animation states to prevent flicker during resize */ export function evaluateElementPositions(): void { getPreparedElements().forEach((elementData) => { calculateElementTriggerPositions(elementData); - setElementInitialState(elementData); + + // Only reset initial state for elements that haven't been animated yet + // This prevents flicker during resize for already-animated elements + if (!elementData.animated) { + setElementInitialState(elementData); + } }); // Process current scroll position to animate elements already in viewport From 40902e3302e1b865b7bb37adf91fabc9fe926442 Mon Sep 17 00:00:00 2001 From: boston343 Date: Fri, 25 Jul 2025 23:25:02 -0400 Subject: [PATCH 08/10] improve demo page --- apps/docs/src/components/MosDemo.astro | 139 +++++++++++++++++++------ 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/apps/docs/src/components/MosDemo.astro b/apps/docs/src/components/MosDemo.astro index cb35c9b..115d777 100644 --- a/apps/docs/src/components/MosDemo.astro +++ b/apps/docs/src/components/MosDemo.astro @@ -1,4 +1,45 @@ --- +import { Code } from "@astrojs/starlight/components"; + +// Code examples for custom animations +const keyframesCode = `MOS.registerKeyframes("my-custom-keyframes", { + opacity: [0, 1], + rotate: [0, 360], +});`; + +const keyframesHtml = `
+ my-custom-keyframes +
`; + +const animationCode = `MOS.registerAnimation("bouncy", (el, opts) => + animate( + el, + { opacity: [0, 1], scale: [0.4, 1] }, + { + type: "spring", + bounce: 0.5, + duration: opts.timeUnits === "s" ? opts.duration : opts.duration / 1000, + delay: opts.timeUnits === "s" ? opts.delay : opts.delay / 1000, + }, + ), +);`; + +const animationHtml = `
+ bouncy animation +
`; + +const bouncyEasingCode = `MOS.registerEasing("bouncy", [0.68, -0.55, 0.265, 1.55]);`; + +const bouncyEasingHtml = `
+ fade-up w/ bouncy easing +
`; + +const dramaticEasingCode = `MOS.registerEasing("dramatic", "cubic-bezier(0.25, 0.46, 0.45, 0.94)");`; + +const dramaticEasingHtml = `
+ fade-up w/ dramatic easing +
`; + const animationSections = [ { title: "Fade Animations", @@ -67,10 +108,10 @@ const animationSections = [ }`} >
- {animation} + data-mos="{animation}"
); @@ -82,7 +123,7 @@ const animationSections = [ } -
+

@@ -91,43 +132,83 @@ const animationSections = [
-
-
- my-custom-keyframes +
+
+
+ +
+
+ +
+
+
+
+ my-custom-keyframes +
-
-
- bouncy animation +
+
+
+ bouncy animation +
+
+
+
+ +
+
+ +
-
-
- fade-up w/ bouncy easing +
+
+
+ +
+
+ +
+
+
+
+ fade-up w/ bouncy easing +
-
-
- fade-up w/ dramatic easing +
+
+
+ fade-up w/ dramatic easing +
+
+
+
+ +
+
+ +
From 083a02c47f4a513c4ae2f5afa9dd63cb256ceb7c Mon Sep 17 00:00:00 2001 From: boston343 Date: Fri, 25 Jul 2025 23:29:48 -0400 Subject: [PATCH 09/10] docs(changeset): Fix potential element flash on page resize. Also add additional tests for verification. --- .changeset/fancy-toys-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fancy-toys-mate.md diff --git a/.changeset/fancy-toys-mate.md b/.changeset/fancy-toys-mate.md new file mode 100644 index 0000000..5c842b3 --- /dev/null +++ b/.changeset/fancy-toys-mate.md @@ -0,0 +1,5 @@ +--- +"motion-on-scroll": patch +--- + +Fix potential element flash on page resize. Also add additional tests for verification. From 638f132e3fd2f8b0f09e64150522194dfc4db0e2 Mon Sep 17 00:00:00 2001 From: boston343 Date: Fri, 25 Jul 2025 23:32:51 -0400 Subject: [PATCH 10/10] chore: formatting --- .../src/__tests__/index.spec.ts | 185 ++++++++++-------- .../src/__tests__/scroll-handler.spec.ts | 7 +- .../src/helpers/scroll-handler.ts | 2 +- 3 files changed, 112 insertions(+), 82 deletions(-) diff --git a/packages/motion-on-scroll/src/__tests__/index.spec.ts b/packages/motion-on-scroll/src/__tests__/index.spec.ts index 7215e5c..f77fa85 100644 --- a/packages/motion-on-scroll/src/__tests__/index.spec.ts +++ b/packages/motion-on-scroll/src/__tests__/index.spec.ts @@ -41,11 +41,7 @@ vi.mock("../helpers/utils.js", () => ({ // Import mocked functions import { registerAnimation } from "../helpers/animations.js"; import { registerEasing } from "../helpers/easing.js"; -import { - clearAllElements, - getMosElements, - prepareElements, -} from "../helpers/elements.js"; +import { clearAllElements, getMosElements, prepareElements } from "../helpers/elements.js"; import { registerKeyframes } from "../helpers/keyframes.js"; import { startDomObserver } from "../helpers/observer.js"; import { @@ -83,16 +79,16 @@ describe("index.ts - Main Entry Point", () => { vi.mocked(getMosElements).mockReturnValue([mockElement1, mockElement2]); // Mock document.readyState since it's read-only - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'loading' + value: "loading", }); // Mock global MutationObserver global.MutationObserver = vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn(), - takeRecords: vi.fn() + takeRecords: vi.fn(), })) as any; // Reset modules and re-import to get fresh state @@ -161,15 +157,21 @@ describe("index.ts - Main Entry Point", () => { const result = init(); // removeMosAttributes is called with forEach, so it gets (element, index, array) - expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [mockElement1, mockElement2]); - expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [mockElement1, mockElement2]); + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [ + mockElement1, + mockElement2, + ]); + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [ + mockElement1, + mockElement2, + ]); expect(result).toEqual([]); }); it("should start DOM observer when not disabled and MutationObserver exists", () => { // Mock isDisabled to return false so we don't exit early vi.mocked(isDisabled).mockReturnValue(false); - + init({ disableMutationObserver: false }); expect(startDomObserver).toHaveBeenCalled(); @@ -189,7 +191,7 @@ describe("index.ts - Main Entry Point", () => { init(); expect(startDomObserver).not.toHaveBeenCalled(); - + // Restore MutationObserver global.MutationObserver = originalMutationObserver; }); @@ -197,7 +199,7 @@ describe("index.ts - Main Entry Point", () => { it("should refresh when called multiple times", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // First init init(); expect(getMosElements).toHaveBeenCalledTimes(1); @@ -232,7 +234,7 @@ describe("index.ts - Main Entry Point", () => { it("should update scroll handler delays with library config", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // First init to set library as active init({ throttleDelay: 150 }); @@ -244,19 +246,19 @@ describe("index.ts - Main Entry Point", () => { it("should prepare elements with current library config", () => { const customOptions = { duration: 800, delay: 200 }; - + // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // Init with custom options to set library config init(customOptions); - + // Call refresh with shouldActivate=true to ensure library is active refresh(true); expect(prepareElements).toHaveBeenCalledWith( [mockElement1, mockElement2], - expect.objectContaining(customOptions) + expect.objectContaining(customOptions), ); }); }); @@ -265,7 +267,7 @@ describe("index.ts - Main Entry Point", () => { it("should re-find elements and clear existing state", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // Set up library as active first init(); @@ -282,9 +284,15 @@ describe("index.ts - Main Entry Point", () => { refreshHard(); // removeMosAttributes is called with forEach, so it gets (element, index, array) - expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [mockElement1, mockElement2]); - expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [mockElement1, mockElement2]); - + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [ + mockElement1, + mockElement2, + ]); + expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [ + mockElement1, + mockElement2, + ]); + // When disabled, function returns early - cleanup functions are NOT called expect(clearAllElements).not.toHaveBeenCalled(); expect(cleanupScrollHandler).not.toHaveBeenCalled(); @@ -293,12 +301,12 @@ describe("index.ts - Main Entry Point", () => { it("should call refresh after cleanup when not disabled", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + refreshHard(); expect(clearAllElements).toHaveBeenCalled(); expect(cleanupScrollHandler).toHaveBeenCalled(); - + // refresh() is called, but updateScrollHandlerDelays only happens if library is active // Since we haven't activated the library, updateScrollHandlerDelays won't be called expect(getMosElements).toHaveBeenCalled(); // This should be called by refresh @@ -309,7 +317,7 @@ describe("index.ts - Main Entry Point", () => { it("should evaluate element positions when library is active", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // Activate library first init(); refresh(true); @@ -334,78 +342,94 @@ describe("index.ts - Main Entry Point", () => { }); it("should call refresh immediately when DOMContentLoaded already fired", async () => { - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'interactive' + value: "interactive", }); - + // Re-import to get fresh module state with new readyState vi.resetModules(); const { setupStartEventListener } = await import("../index.js"); - + setupStartEventListener(); // Should not add event listener since DOM is already ready - expect(document.addEventListener).not.toHaveBeenCalledWith("DOMContentLoaded", expect.any(Function), expect.any(Object)); + expect(document.addEventListener).not.toHaveBeenCalledWith( + "DOMContentLoaded", + expect.any(Function), + expect.any(Object), + ); }); it("should call refresh immediately when load already fired", async () => { - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'complete' + value: "complete", }); - + vi.resetModules(); const { setupStartEventListener } = await import("../index.js"); - + setupStartEventListener(); // Should not add event listener since page is already loaded - expect(window.addEventListener).not.toHaveBeenCalledWith("load", expect.any(Function), expect.any(Object)); + expect(window.addEventListener).not.toHaveBeenCalledWith( + "load", + expect.any(Function), + expect.any(Object), + ); }); it("should add window event listener for load event", () => { - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'loading' + value: "loading", }); - + // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // Initialize with load event init({ startEvent: "load" }); - - expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function), { once: true }); + + expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function), { + once: true, + }); }); it("should add document event listener for DOMContentLoaded", () => { - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'loading' + value: "loading", }); - + // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + // Initialize with DOMContentLoaded (default) init({ startEvent: "DOMContentLoaded" }); - - expect(document.addEventListener).toHaveBeenCalledWith("DOMContentLoaded", expect.any(Function), { once: true }); + + expect(document.addEventListener).toHaveBeenCalledWith( + "DOMContentLoaded", + expect.any(Function), + { once: true }, + ); }); it("should add document event listener for custom events", () => { - Object.defineProperty(document, 'readyState', { + Object.defineProperty(document, "readyState", { writable: true, - value: 'loading' + value: "loading", }); - + // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + init({ startEvent: "custom-event" }); - - expect(document.addEventListener).toHaveBeenCalledWith("custom-event", expect.any(Function), { once: true }); + + expect(document.addEventListener).toHaveBeenCalledWith("custom-event", expect.any(Function), { + once: true, + }); }); }); @@ -417,19 +441,22 @@ describe("index.ts - Main Entry Point", () => { it("should set up resize and orientation change listeners", () => { // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + init(); expect(window.addEventListener).toHaveBeenCalledWith("resize", expect.any(Function)); - expect(window.addEventListener).toHaveBeenCalledWith("orientationchange", expect.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith( + "orientationchange", + expect.any(Function), + ); }); it("should use debounced handler with configured delay", () => { const customDebounceDelay = 200; - + // Mock isDisabled to return false vi.mocked(isDisabled).mockReturnValue(false); - + init({ debounceDelay: customDebounceDelay }); expect(debounce).toHaveBeenCalledWith(expect.any(Function), customDebounceDelay); @@ -439,17 +466,17 @@ describe("index.ts - Main Entry Point", () => { describe("Registration functions", () => { it("should expose registerKeyframes function", () => { const mockKeyframes = { "custom-fade": { from: { opacity: 0 }, to: { opacity: 1 } } }; - + MOS.registerKeyframes(mockKeyframes); - + expect(registerKeyframes).toHaveBeenCalledWith(mockKeyframes); }); it("should expose registerEasing function", () => { const mockEasing = { "custom-ease": "cubic-bezier(0.25, 0.46, 0.45, 0.94)" }; - + MOS.registerEasing(mockEasing); - + expect(registerEasing).toHaveBeenCalledWith(mockEasing); }); @@ -458,9 +485,9 @@ describe("index.ts - Main Entry Point", () => { name: "custom-animation", factory: () => ({ opacity: [0, 1] }), }; - + MOS.registerAnimation(mockAnimation); - + expect(registerAnimation).toHaveBeenCalledWith(mockAnimation); }); }); @@ -503,7 +530,7 @@ describe("index.ts - Main Entry Point", () => { it("should not convert on subsequent inits", () => { // First init with seconds init({ timeUnits: "s" }); - + // Second init should not trigger conversion again init({ timeUnits: "s" }); @@ -535,27 +562,27 @@ describe("index.ts - Main Entry Point", () => { it("should convert default duration and delay when timeUnits is 's' and no explicit values provided", () => { // Mock isDisabled to return false so init completes vi.mocked(isDisabled).mockReturnValue(false); - + // Mock prepareElements to capture the config that gets passed let capturedConfig: any; vi.mocked(prepareElements).mockImplementation((elements, config) => { capturedConfig = config; return []; // Return empty array to match MosElement[] return type }); - + // Call init with timeUnits: "s" but no explicit duration or delay init({ timeUnits: "s" }); - + // Trigger refresh to ensure prepareElements gets called with the config refresh(true); - + // Verify that the config passed to prepareElements has converted defaults expect(capturedConfig).toBeDefined(); expect(capturedConfig.timeUnits).toBe("s"); - + // Default duration should be converted: 400ms -> 0.4s expect(capturedConfig.duration).toBe(0.4); - + // Default delay should be converted: 0ms -> 0s expect(capturedConfig.delay).toBe(0); }); @@ -563,29 +590,29 @@ describe("index.ts - Main Entry Point", () => { it("should not convert defaults when explicit duration and delay are provided with timeUnits 's'", () => { // Mock isDisabled to return false so init completes vi.mocked(isDisabled).mockReturnValue(false); - + // Mock prepareElements to capture the config that gets passed let capturedConfig: any; vi.mocked(prepareElements).mockImplementation((elements, config) => { capturedConfig = config; return []; // Return empty array to match MosElement[] return type }); - + // Call init with timeUnits: "s" AND explicit duration/delay - init({ + init({ timeUnits: "s", - duration: 1.5, // Explicit value should not be converted - delay: 0.2 // Explicit value should not be converted + duration: 1.5, // Explicit value should not be converted + delay: 0.2, // Explicit value should not be converted }); - + // Trigger refresh to ensure prepareElements gets called with the config refresh(true); - + // Verify that the explicit values are preserved (not converted) expect(capturedConfig).toBeDefined(); expect(capturedConfig.timeUnits).toBe("s"); - expect(capturedConfig.duration).toBe(1.5); // Should remain as provided - expect(capturedConfig.delay).toBe(0.2); // Should remain as provided + expect(capturedConfig.duration).toBe(1.5); // Should remain as provided + expect(capturedConfig.delay).toBe(0.2); // Should remain as provided }); }); }); diff --git a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts index 61f0d92..9770435 100644 --- a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts +++ b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts @@ -298,7 +298,7 @@ describe("scroll-handler.ts", () => { ...mockMosElement2, animated: false, // Not yet animated }; - + vi.mocked(getPreparedElements).mockReturnValue([animatedElement, nonAnimatedElement]); vi.mocked(isElementAboveViewport).mockReturnValue(false); @@ -306,7 +306,10 @@ describe("scroll-handler.ts", () => { // Should recalculate positions for both elements expect(getPositionIn).toHaveBeenCalledWith(animatedElement.element, animatedElement.options); - expect(getPositionIn).toHaveBeenCalledWith(nonAnimatedElement.element, nonAnimatedElement.options); + expect(getPositionIn).toHaveBeenCalledWith( + nonAnimatedElement.element, + nonAnimatedElement.options, + ); // Should NOT reset initial state for already-animated element (prevents flicker) expect(setInitialState).not.toHaveBeenCalledWith(animatedElement); diff --git a/packages/motion-on-scroll/src/helpers/scroll-handler.ts b/packages/motion-on-scroll/src/helpers/scroll-handler.ts index 22684d9..fde0e8d 100644 --- a/packages/motion-on-scroll/src/helpers/scroll-handler.ts +++ b/packages/motion-on-scroll/src/helpers/scroll-handler.ts @@ -136,7 +136,7 @@ function setElementInitialState(elementData: MosElement): void { export function evaluateElementPositions(): void { getPreparedElements().forEach((elementData) => { calculateElementTriggerPositions(elementData); - + // Only reset initial state for elements that haven't been animated yet // This prevents flicker during resize for already-animated elements if (!elementData.animated) {