diff --git a/.eslintrc b/.eslintrc index 7588c1f286..52e9ebeec9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,8 +7,14 @@ }, "useJSXTextNode": true }, - "plugins": ["@typescript-eslint", "react", "react-hooks", "import"], - "extends": ["prettier"], + "plugins": [ + "@typescript-eslint", + "react", + "react-hooks", + "import", + "redos-detector" + ], + "extends": ["prettier", "plugin:regexp/recommended"], "rules": { "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/type-annotation-spacing": "error", @@ -31,6 +37,38 @@ // "react-this-binding-issue": 1, // "react-unused-props-and-state": 1, // "variable-name": [1, "ban-keywords"] - "use-isnan": "error" + "use-isnan": "error", + + // Regexp + // Regexp security + "redos-detector/no-unsafe-regex": ["error", { "ignoreError": true }], // Prevent DoS regexps + "regexp/no-super-linear-move": "error", + + // Auto optimize regexps + "regexp/no-non-standard-flag": "warn", + "regexp/no-control-character": "error", + "regexp/no-contradiction-with-assertion": "error", + "regexp/no-octal": "warn", + "regexp/no-standalone-backslash": "error", + "regexp/prefer-escape-replacement-dollar-char": "error", + "regexp/prefer-quantifier": "error", + "regexp/hexadecimal-escape": ["error", "always"], + "regexp/prefer-lookaround": "error", + "regexp/prefer-unicode-codepoint-escapes": "warn", + "regexp/grapheme-string-literal": "error", + "regexp/no-unused-capturing-group": [ + "error", + { + "fixable": true, + "allowNamed": true + } + ], + + // regex perf + "regexp/prefer-question-quantifier": "off", // `(?:a|b|)` is faster than (?:a|b)? + "regexp/no-empty-alternative": "off", // see above + "regexp/require-unicode-regexp": "warn", // /u flag is faster and enables regexp strict mode + "@typescript-eslint/prefer-regexp-exec": "off", + "regexp/prefer-regexp-exec": "error" // exec is faster than match } } diff --git a/.yarn/cache/@aashutoshrathi-word-wrap-npm-1.2.6-5b1d95e487-ada901b9e7.zip b/.yarn/cache/@aashutoshrathi-word-wrap-npm-1.2.6-5b1d95e487-ada901b9e7.zip new file mode 100644 index 0000000000..9334304c2a Binary files /dev/null and b/.yarn/cache/@aashutoshrathi-word-wrap-npm-1.2.6-5b1d95e487-ada901b9e7.zip differ diff --git a/.yarn/cache/@babel-code-frame-npm-7.18.6-25229a7e34-195e2be317.zip b/.yarn/cache/@babel-code-frame-npm-7.18.6-25229a7e34-195e2be317.zip deleted file mode 100644 index c03a5083da..0000000000 Binary files a/.yarn/cache/@babel-code-frame-npm-7.18.6-25229a7e34-195e2be317.zip and /dev/null differ diff --git a/.yarn/cache/@babel-generator-npm-7.18.12-280dfc3ba1-07dd71d255.zip b/.yarn/cache/@babel-generator-npm-7.18.12-280dfc3ba1-07dd71d255.zip deleted file mode 100644 index 9be6f619e3..0000000000 Binary files a/.yarn/cache/@babel-generator-npm-7.18.12-280dfc3ba1-07dd71d255.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-environment-visitor-npm-7.18.9-9f5b3635a1-b25101f616.zip b/.yarn/cache/@babel-helper-environment-visitor-npm-7.18.9-9f5b3635a1-b25101f616.zip deleted file mode 100644 index 0d38ae67ff..0000000000 Binary files a/.yarn/cache/@babel-helper-environment-visitor-npm-7.18.9-9f5b3635a1-b25101f616.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-function-name-npm-7.18.9-89df62ccc8-d04c44e027.zip b/.yarn/cache/@babel-helper-function-name-npm-7.18.9-89df62ccc8-d04c44e027.zip deleted file mode 100644 index b3d84e76ef..0000000000 Binary files a/.yarn/cache/@babel-helper-function-name-npm-7.18.9-89df62ccc8-d04c44e027.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-hoist-variables-npm-7.18.6-6eb061f405-fd9c35bb43.zip b/.yarn/cache/@babel-helper-hoist-variables-npm-7.18.6-6eb061f405-fd9c35bb43.zip deleted file mode 100644 index 888840b295..0000000000 Binary files a/.yarn/cache/@babel-helper-hoist-variables-npm-7.18.6-6eb061f405-fd9c35bb43.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-split-export-declaration-npm-7.18.6-53ebf8ad4c-c6d3dede53.zip b/.yarn/cache/@babel-helper-split-export-declaration-npm-7.18.6-53ebf8ad4c-c6d3dede53.zip deleted file mode 100644 index fc27cef392..0000000000 Binary files a/.yarn/cache/@babel-helper-split-export-declaration-npm-7.18.6-53ebf8ad4c-c6d3dede53.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-string-parser-npm-7.18.10-cf6fe67f9a-d554a43933.zip b/.yarn/cache/@babel-helper-string-parser-npm-7.18.10-cf6fe67f9a-d554a43933.zip deleted file mode 100644 index 11010bcd87..0000000000 Binary files a/.yarn/cache/@babel-helper-string-parser-npm-7.18.10-cf6fe67f9a-d554a43933.zip and /dev/null differ diff --git a/.yarn/cache/@babel-helper-validator-identifier-npm-7.18.6-357e4653ab-e295254d61.zip b/.yarn/cache/@babel-helper-validator-identifier-npm-7.18.6-357e4653ab-e295254d61.zip deleted file mode 100644 index 4165848d98..0000000000 Binary files a/.yarn/cache/@babel-helper-validator-identifier-npm-7.18.6-357e4653ab-e295254d61.zip and /dev/null differ diff --git a/.yarn/cache/@babel-highlight-npm-7.18.6-9d35ad2e27-92d8ee6154.zip b/.yarn/cache/@babel-highlight-npm-7.18.6-9d35ad2e27-92d8ee6154.zip deleted file mode 100644 index c3ee71dde6..0000000000 Binary files a/.yarn/cache/@babel-highlight-npm-7.18.6-9d35ad2e27-92d8ee6154.zip and /dev/null differ diff --git a/.yarn/cache/@babel-parser-npm-7.18.11-a2b80029aa-5ecc75b83e.zip b/.yarn/cache/@babel-parser-npm-7.18.11-a2b80029aa-5ecc75b83e.zip deleted file mode 100644 index 27fa6e61ea..0000000000 Binary files a/.yarn/cache/@babel-parser-npm-7.18.11-a2b80029aa-5ecc75b83e.zip and /dev/null differ diff --git a/.yarn/cache/@babel-template-npm-7.18.10-b6d6fdbaf8-93a6aa094a.zip b/.yarn/cache/@babel-template-npm-7.18.10-b6d6fdbaf8-93a6aa094a.zip deleted file mode 100644 index 08a1c38217..0000000000 Binary files a/.yarn/cache/@babel-template-npm-7.18.10-b6d6fdbaf8-93a6aa094a.zip and /dev/null differ diff --git a/.yarn/cache/@babel-types-npm-7.18.10-8502ea016c-11632c9b10.zip b/.yarn/cache/@babel-types-npm-7.18.10-8502ea016c-11632c9b10.zip deleted file mode 100644 index fd8134de85..0000000000 Binary files a/.yarn/cache/@babel-types-npm-7.18.10-8502ea016c-11632c9b10.zip and /dev/null differ diff --git a/.yarn/cache/@eslint-community-eslint-utils-npm-4.4.0-d1791bd5a3-cdfe3ae42b.zip b/.yarn/cache/@eslint-community-eslint-utils-npm-4.4.0-d1791bd5a3-cdfe3ae42b.zip new file mode 100644 index 0000000000..4e48357020 Binary files /dev/null and b/.yarn/cache/@eslint-community-eslint-utils-npm-4.4.0-d1791bd5a3-cdfe3ae42b.zip differ diff --git a/.yarn/cache/@eslint-community-regexpp-npm-4.10.0-6bfb984c81-2a6e345429.zip b/.yarn/cache/@eslint-community-regexpp-npm-4.10.0-6bfb984c81-2a6e345429.zip new file mode 100644 index 0000000000..7ef5a48973 Binary files /dev/null and b/.yarn/cache/@eslint-community-regexpp-npm-4.10.0-6bfb984c81-2a6e345429.zip differ diff --git a/.yarn/cache/@eslint-eslintrc-npm-1.3.0-1f3c51be25-a1e734ad31.zip b/.yarn/cache/@eslint-eslintrc-npm-1.3.0-1f3c51be25-a1e734ad31.zip deleted file mode 100644 index e9b7fa21a8..0000000000 Binary files a/.yarn/cache/@eslint-eslintrc-npm-1.3.0-1f3c51be25-a1e734ad31.zip and /dev/null differ diff --git a/.yarn/cache/@eslint-eslintrc-npm-2.1.4-1ff4b5f908-10957c7592.zip b/.yarn/cache/@eslint-eslintrc-npm-2.1.4-1ff4b5f908-10957c7592.zip new file mode 100644 index 0000000000..58788ff7a6 Binary files /dev/null and b/.yarn/cache/@eslint-eslintrc-npm-2.1.4-1ff4b5f908-10957c7592.zip differ diff --git a/.yarn/cache/@eslint-js-npm-8.57.0-00ead3710a-315dc65b0e.zip b/.yarn/cache/@eslint-js-npm-8.57.0-00ead3710a-315dc65b0e.zip new file mode 100644 index 0000000000..82eab16e7c Binary files /dev/null and b/.yarn/cache/@eslint-js-npm-8.57.0-00ead3710a-315dc65b0e.zip differ diff --git a/.yarn/cache/@humanwhocodes-config-array-npm-0.10.4-8334b3c6a2-d480e5d57e.zip b/.yarn/cache/@humanwhocodes-config-array-npm-0.10.4-8334b3c6a2-d480e5d57e.zip deleted file mode 100644 index 70b1644253..0000000000 Binary files a/.yarn/cache/@humanwhocodes-config-array-npm-0.10.4-8334b3c6a2-d480e5d57e.zip and /dev/null differ diff --git a/.yarn/cache/@humanwhocodes-config-array-npm-0.11.14-94a02fcc87-861ccce9ea.zip b/.yarn/cache/@humanwhocodes-config-array-npm-0.11.14-94a02fcc87-861ccce9ea.zip new file mode 100644 index 0000000000..166fee4b82 Binary files /dev/null and b/.yarn/cache/@humanwhocodes-config-array-npm-0.11.14-94a02fcc87-861ccce9ea.zip differ diff --git a/.yarn/cache/@humanwhocodes-gitignore-to-minimatch-npm-1.0.2-247ae8a408-aba5c40c9e.zip b/.yarn/cache/@humanwhocodes-gitignore-to-minimatch-npm-1.0.2-247ae8a408-aba5c40c9e.zip deleted file mode 100644 index 2636f8c631..0000000000 Binary files a/.yarn/cache/@humanwhocodes-gitignore-to-minimatch-npm-1.0.2-247ae8a408-aba5c40c9e.zip and /dev/null differ diff --git a/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip b/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip new file mode 100644 index 0000000000..7adb1e9f28 Binary files /dev/null and b/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip differ diff --git a/.yarn/cache/@humanwhocodes-object-schema-npm-1.2.1-eb622b5d0e-a824a1ec31.zip b/.yarn/cache/@humanwhocodes-object-schema-npm-1.2.1-eb622b5d0e-a824a1ec31.zip deleted file mode 100644 index 2b79104af5..0000000000 Binary files a/.yarn/cache/@humanwhocodes-object-schema-npm-1.2.1-eb622b5d0e-a824a1ec31.zip and /dev/null differ diff --git a/.yarn/cache/@humanwhocodes-object-schema-npm-2.0.2-77b42018f9-2fc1150336.zip b/.yarn/cache/@humanwhocodes-object-schema-npm-2.0.2-77b42018f9-2fc1150336.zip new file mode 100644 index 0000000000..cf6847cf44 Binary files /dev/null and b/.yarn/cache/@humanwhocodes-object-schema-npm-2.0.2-77b42018f9-2fc1150336.zip differ diff --git a/.yarn/cache/@jridgewell-resolve-uri-npm-3.1.0-6ff2351e61-b5ceaaf9a1.zip b/.yarn/cache/@jridgewell-resolve-uri-npm-3.1.0-6ff2351e61-b5ceaaf9a1.zip deleted file mode 100644 index 97e857d7d2..0000000000 Binary files a/.yarn/cache/@jridgewell-resolve-uri-npm-3.1.0-6ff2351e61-b5ceaaf9a1.zip and /dev/null differ diff --git a/.yarn/cache/@jridgewell-sourcemap-codec-npm-1.4.14-f5f0630788-61100637b6.zip b/.yarn/cache/@jridgewell-sourcemap-codec-npm-1.4.14-f5f0630788-61100637b6.zip deleted file mode 100644 index d8703c8967..0000000000 Binary files a/.yarn/cache/@jridgewell-sourcemap-codec-npm-1.4.14-f5f0630788-61100637b6.zip and /dev/null differ diff --git a/.yarn/cache/@jridgewell-trace-mapping-npm-0.3.15-7357dbf648-38917e9c2b.zip b/.yarn/cache/@jridgewell-trace-mapping-npm-0.3.15-7357dbf648-38917e9c2b.zip deleted file mode 100644 index 2ccfc7560e..0000000000 Binary files a/.yarn/cache/@jridgewell-trace-mapping-npm-0.3.15-7357dbf648-38917e9c2b.zip and /dev/null differ diff --git a/.yarn/cache/@ungap-structured-clone-npm-1.2.0-648f0b82e0-4f656b7b46.zip b/.yarn/cache/@ungap-structured-clone-npm-1.2.0-648f0b82e0-4f656b7b46.zip new file mode 100644 index 0000000000..598a36e085 Binary files /dev/null and b/.yarn/cache/@ungap-structured-clone-npm-1.2.0-648f0b82e0-4f656b7b46.zip differ diff --git a/.yarn/cache/acorn-npm-8.11.3-0d7ab48b38-76d8e7d559.zip b/.yarn/cache/acorn-npm-8.11.3-0d7ab48b38-76d8e7d559.zip new file mode 100644 index 0000000000..af75d2b2f2 Binary files /dev/null and b/.yarn/cache/acorn-npm-8.11.3-0d7ab48b38-76d8e7d559.zip differ diff --git a/.yarn/cache/acorn-npm-8.8.0-9ef399ab45-7270ca82b2.zip b/.yarn/cache/acorn-npm-8.8.0-9ef399ab45-7270ca82b2.zip deleted file mode 100644 index b5376b1392..0000000000 Binary files a/.yarn/cache/acorn-npm-8.8.0-9ef399ab45-7270ca82b2.zip and /dev/null differ diff --git a/.yarn/cache/comment-parser-npm-1.4.1-f416dc95e4-e0f6f60c51.zip b/.yarn/cache/comment-parser-npm-1.4.1-f416dc95e4-e0f6f60c51.zip new file mode 100644 index 0000000000..e57cfb3d83 Binary files /dev/null and b/.yarn/cache/comment-parser-npm-1.4.1-f416dc95e4-e0f6f60c51.zip differ diff --git a/.yarn/cache/eslint-npm-8.22.0-bb6eeb6c80-2d84a7a220.zip b/.yarn/cache/eslint-npm-8.22.0-bb6eeb6c80-2d84a7a220.zip deleted file mode 100644 index 0c34cffb83..0000000000 Binary files a/.yarn/cache/eslint-npm-8.22.0-bb6eeb6c80-2d84a7a220.zip and /dev/null differ diff --git a/.yarn/cache/eslint-npm-8.57.0-4286e12a3a-3a48d7ff85.zip b/.yarn/cache/eslint-npm-8.57.0-4286e12a3a-3a48d7ff85.zip new file mode 100644 index 0000000000..73f8f9dff6 Binary files /dev/null and b/.yarn/cache/eslint-npm-8.57.0-4286e12a3a-3a48d7ff85.zip differ diff --git a/.yarn/cache/eslint-plugin-redos-detector-npm-2.4.0-3dae8ce005-9b47a3af47.zip b/.yarn/cache/eslint-plugin-redos-detector-npm-2.4.0-3dae8ce005-9b47a3af47.zip new file mode 100644 index 0000000000..2c30c816de Binary files /dev/null and b/.yarn/cache/eslint-plugin-redos-detector-npm-2.4.0-3dae8ce005-9b47a3af47.zip differ diff --git a/.yarn/cache/eslint-plugin-regexp-npm-2.2.0-1d1ffb9937-cfe6870ebf.zip b/.yarn/cache/eslint-plugin-regexp-npm-2.2.0-1d1ffb9937-cfe6870ebf.zip new file mode 100644 index 0000000000..a1ee478621 Binary files /dev/null and b/.yarn/cache/eslint-plugin-regexp-npm-2.2.0-1d1ffb9937-cfe6870ebf.zip differ diff --git a/.yarn/cache/eslint-scope-npm-7.1.1-23935eb377-9f6e974ab2.zip b/.yarn/cache/eslint-scope-npm-7.1.1-23935eb377-9f6e974ab2.zip deleted file mode 100644 index fcad723df3..0000000000 Binary files a/.yarn/cache/eslint-scope-npm-7.1.1-23935eb377-9f6e974ab2.zip and /dev/null differ diff --git a/.yarn/cache/eslint-scope-npm-7.2.2-53cb0df8e8-ec97dbf5fb.zip b/.yarn/cache/eslint-scope-npm-7.2.2-53cb0df8e8-ec97dbf5fb.zip new file mode 100644 index 0000000000..29b002eb98 Binary files /dev/null and b/.yarn/cache/eslint-scope-npm-7.2.2-53cb0df8e8-ec97dbf5fb.zip differ diff --git a/.yarn/cache/eslint-visitor-keys-npm-3.3.0-d329af7c8c-d59e68a7c5.zip b/.yarn/cache/eslint-visitor-keys-npm-3.3.0-d329af7c8c-d59e68a7c5.zip deleted file mode 100644 index a46738b25f..0000000000 Binary files a/.yarn/cache/eslint-visitor-keys-npm-3.3.0-d329af7c8c-d59e68a7c5.zip and /dev/null differ diff --git a/.yarn/cache/eslint-visitor-keys-npm-3.4.3-a356ac7e46-36e9ef87fc.zip b/.yarn/cache/eslint-visitor-keys-npm-3.4.3-a356ac7e46-36e9ef87fc.zip new file mode 100644 index 0000000000..7c61b814bf Binary files /dev/null and b/.yarn/cache/eslint-visitor-keys-npm-3.4.3-a356ac7e46-36e9ef87fc.zip differ diff --git a/.yarn/cache/espree-npm-9.3.3-220189b4c1-33e8a36fc1.zip b/.yarn/cache/espree-npm-9.3.3-220189b4c1-33e8a36fc1.zip deleted file mode 100644 index 463c027cd9..0000000000 Binary files a/.yarn/cache/espree-npm-9.3.3-220189b4c1-33e8a36fc1.zip and /dev/null differ diff --git a/.yarn/cache/espree-npm-9.6.1-a50722a5a9-eb8c149c7a.zip b/.yarn/cache/espree-npm-9.6.1-a50722a5a9-eb8c149c7a.zip new file mode 100644 index 0000000000..0014c0574a Binary files /dev/null and b/.yarn/cache/espree-npm-9.6.1-a50722a5a9-eb8c149c7a.zip differ diff --git a/.yarn/cache/esquery-npm-1.4.0-f39408b1a7-a0807e17ab.zip b/.yarn/cache/esquery-npm-1.4.0-f39408b1a7-a0807e17ab.zip deleted file mode 100644 index abf91d4c4f..0000000000 Binary files a/.yarn/cache/esquery-npm-1.4.0-f39408b1a7-a0807e17ab.zip and /dev/null differ diff --git a/.yarn/cache/esquery-npm-1.5.0-d8f8a06879-aefb0d2596.zip b/.yarn/cache/esquery-npm-1.5.0-d8f8a06879-aefb0d2596.zip new file mode 100644 index 0000000000..6006b96052 Binary files /dev/null and b/.yarn/cache/esquery-npm-1.5.0-d8f8a06879-aefb0d2596.zip differ diff --git a/.yarn/cache/follow-redirects-npm-1.15.1-6b191885cd-6aa4e3e3cd.zip b/.yarn/cache/follow-redirects-npm-1.15.1-6b191885cd-6aa4e3e3cd.zip deleted file mode 100644 index f48c3fb384..0000000000 Binary files a/.yarn/cache/follow-redirects-npm-1.15.1-6b191885cd-6aa4e3e3cd.zip and /dev/null differ diff --git a/.yarn/cache/globals-npm-13.17.0-a6039e7d26-fbaf4112e5.zip b/.yarn/cache/globals-npm-13.17.0-a6039e7d26-fbaf4112e5.zip deleted file mode 100644 index 5af5a65426..0000000000 Binary files a/.yarn/cache/globals-npm-13.17.0-a6039e7d26-fbaf4112e5.zip and /dev/null differ diff --git a/.yarn/cache/globals-npm-13.24.0-cc7713139c-56066ef058.zip b/.yarn/cache/globals-npm-13.24.0-cc7713139c-56066ef058.zip new file mode 100644 index 0000000000..c8cb0244af Binary files /dev/null and b/.yarn/cache/globals-npm-13.24.0-cc7713139c-56066ef058.zip differ diff --git a/.yarn/cache/grapheme-splitter-npm-1.0.4-648f2bf509-0c22ec54de.zip b/.yarn/cache/grapheme-splitter-npm-1.0.4-648f2bf509-0c22ec54de.zip deleted file mode 100644 index 1eb26cc6a9..0000000000 Binary files a/.yarn/cache/grapheme-splitter-npm-1.0.4-648f2bf509-0c22ec54de.zip and /dev/null differ diff --git a/.yarn/cache/graphemer-npm-1.4.0-0627732d35-bab8f0be9b.zip b/.yarn/cache/graphemer-npm-1.4.0-0627732d35-bab8f0be9b.zip new file mode 100644 index 0000000000..e04f8d3724 Binary files /dev/null and b/.yarn/cache/graphemer-npm-1.4.0-0627732d35-bab8f0be9b.zip differ diff --git a/.yarn/cache/is-path-inside-npm-3.0.3-2ea0ef44fd-abd50f0618.zip b/.yarn/cache/is-path-inside-npm-3.0.3-2ea0ef44fd-abd50f0618.zip new file mode 100644 index 0000000000..27f29d70be Binary files /dev/null and b/.yarn/cache/is-path-inside-npm-3.0.3-2ea0ef44fd-abd50f0618.zip differ diff --git a/.yarn/cache/jsdoc-type-pratt-parser-npm-4.0.0-7b035921c4-af0629c951.zip b/.yarn/cache/jsdoc-type-pratt-parser-npm-4.0.0-7b035921c4-af0629c951.zip new file mode 100644 index 0000000000..975b3d2ec2 Binary files /dev/null and b/.yarn/cache/jsdoc-type-pratt-parser-npm-4.0.0-7b035921c4-af0629c951.zip differ diff --git a/.yarn/cache/minimist-npm-1.2.6-f4cee4b4af-d15428cd1e.zip b/.yarn/cache/minimist-npm-1.2.6-f4cee4b4af-d15428cd1e.zip deleted file mode 100644 index e7466c584e..0000000000 Binary files a/.yarn/cache/minimist-npm-1.2.6-f4cee4b4af-d15428cd1e.zip and /dev/null differ diff --git a/.yarn/cache/nan-npm-2.16.0-cac314a230-cb16937273.zip b/.yarn/cache/nan-npm-2.16.0-cac314a230-cb16937273.zip deleted file mode 100644 index d2907fbe77..0000000000 Binary files a/.yarn/cache/nan-npm-2.16.0-cac314a230-cb16937273.zip and /dev/null differ diff --git a/.yarn/cache/optionator-npm-0.9.1-577e397aae-dbc6fa0656.zip b/.yarn/cache/optionator-npm-0.9.1-577e397aae-dbc6fa0656.zip deleted file mode 100644 index 6e6efe345b..0000000000 Binary files a/.yarn/cache/optionator-npm-0.9.1-577e397aae-dbc6fa0656.zip and /dev/null differ diff --git a/.yarn/cache/optionator-npm-0.9.3-56c3a4bf80-0928199944.zip b/.yarn/cache/optionator-npm-0.9.3-56c3a4bf80-0928199944.zip new file mode 100644 index 0000000000..06266323c5 Binary files /dev/null and b/.yarn/cache/optionator-npm-0.9.3-56c3a4bf80-0928199944.zip differ diff --git a/.yarn/cache/readable-stream-npm-3.6.0-23a4a5eb56-d4ea81502d.zip b/.yarn/cache/readable-stream-npm-3.6.0-23a4a5eb56-d4ea81502d.zip deleted file mode 100644 index ede5b314bf..0000000000 Binary files a/.yarn/cache/readable-stream-npm-3.6.0-23a4a5eb56-d4ea81502d.zip and /dev/null differ diff --git a/.yarn/cache/refa-npm-0.12.1-44d59a9a85-845cef5478.zip b/.yarn/cache/refa-npm-0.12.1-44d59a9a85-845cef5478.zip new file mode 100644 index 0000000000..7316091c6f Binary files /dev/null and b/.yarn/cache/refa-npm-0.12.1-44d59a9a85-845cef5478.zip differ diff --git a/.yarn/cache/regexp-ast-analysis-npm-0.7.1-7d7723f5b9-c1c47fea63.zip b/.yarn/cache/regexp-ast-analysis-npm-0.7.1-7d7723f5b9-c1c47fea63.zip new file mode 100644 index 0000000000..82d31f0fda Binary files /dev/null and b/.yarn/cache/regexp-ast-analysis-npm-0.7.1-7d7723f5b9-c1c47fea63.zip differ diff --git a/.yarn/cache/scslre-npm-0.3.0-0df1ace320-a89d4fe5db.zip b/.yarn/cache/scslre-npm-0.3.0-0df1ace320-a89d4fe5db.zip new file mode 100644 index 0000000000..b50a0cd702 Binary files /dev/null and b/.yarn/cache/scslre-npm-0.3.0-0df1ace320-a89d4fe5db.zip differ diff --git a/.yarn/cache/v8-compile-cache-npm-2.3.0-961375f150-adb0a271ea.zip b/.yarn/cache/v8-compile-cache-npm-2.3.0-961375f150-adb0a271ea.zip deleted file mode 100644 index 0e04423cd8..0000000000 Binary files a/.yarn/cache/v8-compile-cache-npm-2.3.0-961375f150-adb0a271ea.zip and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index a3872ed560..a9eb6e91a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [11.0.11] 2024-03-12 + +### Changed + +- Keyframes now resolved asynchronously. +- External event handlers now fired synchronously. +- CSS variables and unit conversion now supported with >2 keyframe animations. +- Removed WAAPI animation of `background-color`. + +## [11.0.10] 2024-03-12 + +### Fixed + +- Improved speed and stability of regexes. + +## [11.0.9] 2024-03-12 + +### Added + +- Added support for Content Security Policy (CSP) nonces via `MotionConfig`. + ## [11.0.8] 2024-02-29 ### Fixed diff --git a/dev/benchmarks/cold-start-anime.html b/dev/benchmarks/cold-start-anime.html index 7bed6160b9..c8ca123ebf 100644 --- a/dev/benchmarks/cold-start-anime.html +++ b/dev/benchmarks/cold-start-anime.html @@ -28,7 +28,7 @@ } .box { - width: 10px; + width: 10%; height: 100px; background-color: #fff; } @@ -36,7 +36,7 @@
- + diff --git a/dev/benchmarks/cold-start-framer-motion.html b/dev/benchmarks/cold-start-framer-motion.html index 16f9923ca2..05c4f230c4 100644 --- a/dev/benchmarks/cold-start-framer-motion.html +++ b/dev/benchmarks/cold-start-framer-motion.html @@ -62,7 +62,7 @@ x: 5, }, { - easing: "linear", + ease: "linear", duration: 1, } ) @@ -78,7 +78,7 @@ x: "10%", }, { - easing: "linear", + ease: "linear", duration: 1, } ) diff --git a/dev/benchmarks/cold-start-waapi.html b/dev/benchmarks/cold-start-waapi.html index b0788d8427..865ba6d278 100644 --- a/dev/benchmarks/cold-start-waapi.html +++ b/dev/benchmarks/cold-start-waapi.html @@ -44,20 +44,51 @@ html += `
` } document.querySelector(".container").innerHTML = html + const boxes = document.querySelectorAll(".box") - boxes.forEach((box) => - box.animate( - { - rotate: "360deg", - backgroundColor: "#f00", - width: "100%", - }, - { - duration: 1000, - fill: "forwards", + setTimeout(() => { + boxes.forEach((box) => { + const animation = box.animate( + { + rotate: Math.random() * 360 + "deg", + backgroundColor: "#f00", + width: Math.random() * 100 + "%", + translate: "5px 0", + }, + { + duration: 1000, + fill: "both", + } + ) + animation.onfinish = () => { + requestAnimationFrame(() => { + animation.commitStyles() + animation.cancel() + }) } - ) - ) + }) + + setTimeout(() => { + boxes.forEach((box) => { + const animation = box.animate( + { + width: Math.random() * 100 + "px", + translate: "50% 0", + }, + { + duration: 1000, + fill: "both", + } + ) + animation.onfinish = () => { + requestAnimationFrame(() => { + animation.commitStyles() + animation.cancel() + }) + } + }) + }, 1500) + }, 1000) diff --git a/dev/benchmarks/warm-start-framer-motion.html b/dev/benchmarks/warm-start-framer-motion.html new file mode 100644 index 0000000000..adbabda428 --- /dev/null +++ b/dev/benchmarks/warm-start-framer-motion.html @@ -0,0 +1,73 @@ + + + + + + +
+ + + + + diff --git a/dev/benchmarks/warm-start-gsap.html b/dev/benchmarks/warm-start-gsap.html new file mode 100644 index 0000000000..114507e7dd --- /dev/null +++ b/dev/benchmarks/warm-start-gsap.html @@ -0,0 +1,84 @@ + + + + + + +
+ + + + diff --git a/dev/examples/MotionConfig-nonce.tsx b/dev/examples/MotionConfig-nonce.tsx new file mode 100644 index 0000000000..b1f5fb87d1 --- /dev/null +++ b/dev/examples/MotionConfig-nonce.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { motion, MotionConfig, AnimatePresence } from "framer-motion" + +/** + * An example of a nonce prop on MotionConfig + */ + +const styleA = { + width: 100, + height: 100, + background: "white", + borderRadius: 20, +} + +const styleB = { + width: 100, + height: 100, + background: "blue", + borderRadius: 20, +} + +export const App = () => { + const [toggle, setToggle] = React.useState(false) + + return ( + + + {toggle ? ( + + A + + ) : ( + + B + + )} + + + + ) +} diff --git a/dev/package.json b/dev/package.json index f80f6a549a..e7ea39920c 100644 --- a/dev/package.json +++ b/dev/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion--dev", - "version": "11.0.8", + "version": "11.0.11", "private": true, "scripts": { "dev": "webpack serve --config ./webpack/config.js --hot" @@ -8,8 +8,8 @@ "dependencies": { "@react-three/drei": "^7.27.3", "@react-three/fiber": "^8.2.2", - "framer-motion": "^11.0.8", - "framer-motion-3d": "^11.0.8", + "framer-motion": "^11.0.11", + "framer-motion-3d": "^11.0.11", "path-browserify": "^1.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/dev/tests/animate-presence-pop.tsx b/dev/tests/animate-presence-pop.tsx index 22ea2d83c8..165d252f2d 100644 --- a/dev/tests/animate-presence-pop.tsx +++ b/dev/tests/animate-presence-pop.tsx @@ -1,6 +1,6 @@ -import { AnimatePresence, motion } from "framer-motion" +import { AnimatePresence, motion, animate } from "framer-motion" import * as React from "react" -import { useState } from "react" +import { useState, useRef, useEffect } from "react" import styled from "styled-components" const Container = styled.section` @@ -23,6 +23,15 @@ export const App = () => { const itemStyle = position === "relative" ? { position, top: 100, left: 100 } : {} + const ref = useRef(null) + + useEffect(() => { + if (!ref.current) return + + animate(ref.current, { opacity: [0, 1] }, { duration: 1 }) + animate(ref.current, { opacity: [1, 0.5] }, { duration: 1 }) + }, []) + return ( setState(!state)}> @@ -54,6 +63,10 @@ export const App = () => { style={{ ...itemStyle, backgroundColor: "blue" }} /> +
) } diff --git a/dev/tests/drag-ref-constraints.tsx b/dev/tests/drag-ref-constraints.tsx index 9a62f7c0b6..74da2bb739 100644 --- a/dev/tests/drag-ref-constraints.tsx +++ b/dev/tests/drag-ref-constraints.tsx @@ -14,6 +14,7 @@ export const App = () => { window.scrollTo(0, 100) }, []) const x = useMotionValue("100%") + return (
{ useEffect(() => { /** - * Animate both background-color (WAAPI-driven) and color (sync) + * Animate both transform (WAAPI) and colors (JS) */ return scroll( animate( diff --git a/dev/tests/scroll-svg.tsx b/dev/tests/scroll-svg.tsx index ac20dd2756..87b6f8f822 100644 --- a/dev/tests/scroll-svg.tsx +++ b/dev/tests/scroll-svg.tsx @@ -18,7 +18,12 @@ export const App = () => { return ( <> -
+
{ />
- + {rectValues.scrollYProgress} - + {svgValues.scrollYProgress} diff --git a/dev/tests/waapi-cancel.tsx b/dev/tests/waapi-cancel.tsx index c24b12b25b..207446a2d8 100644 --- a/dev/tests/waapi-cancel.tsx +++ b/dev/tests/waapi-cancel.tsx @@ -19,7 +19,11 @@ const Container = styled.section` export const App = () => { useEffect(() => { - const controls = animate("#box", { opacity: [0, 1] }, { duration: 1 }) + const controls = animate( + "#box", + { x: [0, 100], opacity: [0, 1] }, + { duration: 1 } + ) controls.cancel() controls.complete() diff --git a/lerna.json b/lerna.json index 758fb9117f..15c0aefb32 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "11.0.8", + "version": "11.0.11", "packages": [ "packages/*" ], diff --git a/package.json b/package.json index 6c081f28e2..9dd591ce0d 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,13 @@ "concurrently": "^7.3.0", "convert-tsconfig-paths-to-webpack-aliases": "^0.9.2", "cypress": "^3.4.0", - "eslint": "^8.21.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-redos-detector": "^2.4.0", + "eslint-plugin-regexp": "^2.2.0", "gsap": "^3.12.5", "jest": "^28.0.0", "jest-environment-jsdom": "^28.1.3", diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 43944c1225..45473aba8d 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion-3d", - "version": "11.0.8", + "version": "11.0.11", "description": "A simple and powerful React animation library for @react-three/fiber", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -33,6 +33,7 @@ "framer" ], "scripts": { + "eslint": "yarn run lint", "lint": "yarn eslint src/**/*.ts", "test": "yarn test-unit", "test-ci": "yarn test-unit", @@ -46,7 +47,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^11.0.8", + "framer-motion": "^11.0.11", "react-merge-refs": "^2.0.1" }, "peerDependencies": { @@ -60,5 +61,5 @@ "@react-three/test-renderer": "^9.0.0", "@rollup/plugin-commonjs": "^22.0.1" }, - "gitHead": "8b983243dfb9654a7b22ad0e0d5840622fe50603" + "gitHead": "716f71e064c7c3810706c0d621809a62c8b20f34" } diff --git a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx index f3810f3b9e..917f5ef1e3 100644 --- a/packages/framer-motion-3d/src/render/__tests__/index.test.tsx +++ b/packages/framer-motion-3d/src/render/__tests__/index.test.tsx @@ -58,6 +58,7 @@ describe("motion for three", () => { ReactThreeTestRenderer.create() }) + expect(result.length).toBeGreaterThan(1) const lastFrame = result[result.length - 1] expect(lastFrame).toEqual({ scale: 5, @@ -230,8 +231,8 @@ describe("motion for three", () => { transition: { x: { type: false } }, }, off: { - x: 0, - y: 0, + x: 1, + y: 1, transition: { x: { type: false } }, }, }} @@ -240,10 +241,10 @@ describe("motion for three", () => { } ReactThreeTestRenderer.create() - frame.update(() => resolve([x.get(), y.get()])) + frame.postRender(() => resolve([x.get(), y.get()])) }) expect(result[0]).toEqual(100) - expect(result[1]).toEqual(0) + expect(result[1]).not.toEqual(100) }) }) diff --git a/packages/framer-motion-3d/src/render/create-visual-element.ts b/packages/framer-motion-3d/src/render/create-visual-element.ts index eada251e21..9076e1bc0f 100644 --- a/packages/framer-motion-3d/src/render/create-visual-element.ts +++ b/packages/framer-motion-3d/src/render/create-visual-element.ts @@ -1,15 +1,10 @@ import type { CreateVisualElement, - TargetAndTransition, ResolvedValues, MotionProps, } from "framer-motion" -import { - createBox, - checkTargetForNewValues, - VisualElement, -} from "framer-motion" +import { createBox, VisualElement } from "framer-motion" import { Object3DNode } from "@react-three/fiber" import { setThreeValue } from "./utils/set-value" @@ -41,15 +36,6 @@ export class ThreeVisualElement extends VisualElement< return a.id - b.id } - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - checkTargetForNewValues(this, target, {}) - return { ...target, transition, transitionEnd } - } - removeValueFromRenderState() {} measureInstanceViewportBox() { diff --git a/packages/framer-motion/cypress/integration/drag-to-reorder.ts b/packages/framer-motion/cypress/integration/drag-to-reorder.ts index d85f206e4f..8e74738860 100644 --- a/packages/framer-motion/cypress/integration/drag-to-reorder.ts +++ b/packages/framer-motion/cypress/integration/drag-to-reorder.ts @@ -201,7 +201,7 @@ describe("Drag to reorder", () => { const y = step > 0 ? delta : -delta chain = chain .trigger("pointermove", 360, baseY + y, { force: true }) - .wait(50) + .wait(100) }) }) return chain diff --git a/packages/framer-motion/cypress/integration/drag.ts b/packages/framer-motion/cypress/integration/drag.ts index 11ea5af826..2d68d7627b 100644 --- a/packages/framer-motion/cypress/integration/drag.ts +++ b/packages/framer-motion/cypress/integration/drag.ts @@ -218,7 +218,7 @@ describe("Drag", () => { expect(top).to.equal(-10) }) .trigger("pointerup", { force: true }) - .wait(50) + .wait(100) .should(($draggable: any) => { const draggable = $draggable[0] as HTMLDivElement const { left, top } = draggable.getBoundingClientRect() diff --git a/packages/framer-motion/cypress/integration/waapi.ts b/packages/framer-motion/cypress/integration/waapi.ts index 5800c60bf5..b3791477e7 100644 --- a/packages/framer-motion/cypress/integration/waapi.ts +++ b/packages/framer-motion/cypress/integration/waapi.ts @@ -4,7 +4,8 @@ describe("waapi", () => { .wait(100) .get("#box") .should(([$element]: any) => { - expect(getComputedStyle($element).opacity).to.equal("0") + expect(getComputedStyle($element).opacity).to.equal("1") + expect($element.getBoundingClientRect().left).to.equal(200) }) }) }) diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 49dc887cdf..8ac777d322 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "11.0.8", + "version": "11.0.11", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -40,6 +40,7 @@ "waapi" ], "scripts": { + "eslint": "yarn run lint", "lint": "yarn eslint src/**/*.ts", "build": "yarn clean && tsc -p . && rollup -c && node ./scripts/check-bundle.js", "dev": "yarn watch", @@ -86,7 +87,7 @@ "bundlesize": [ { "path": "./dist/size-rollup-motion.js", - "maxSize": "31.3 kB" + "maxSize": "32.1 kB" }, { "path": "./dist/size-rollup-m.js", @@ -94,15 +95,15 @@ }, { "path": "./dist/size-rollup-dom-animation.js", - "maxSize": "15.33 kB" + "maxSize": "16.1kB" }, { "path": "./dist/size-rollup-dom-max.js", - "maxSize": "26.8 kB" + "maxSize": "27.5 kB" }, { "path": "./dist/size-rollup-animate.js", - "maxSize": "16.6 kB" + "maxSize": "17.3 kB" }, { "path": "./dist/size-webpack-m.js", @@ -110,12 +111,12 @@ }, { "path": "./dist/size-webpack-dom-animation.js", - "maxSize": "19.92 kB" + "maxSize": "21 kB" }, { "path": "./dist/size-webpack-dom-max.js", - "maxSize": "32.2 kB" + "maxSize": "33 kB" } ], - "gitHead": "8b983243dfb9654a7b22ad0e0d5840622fe50603" + "gitHead": "716f71e064c7c3810706c0d621809a62c8b20f34" } diff --git a/packages/framer-motion/src/animation/GroupPlaybackControls.ts b/packages/framer-motion/src/animation/GroupPlaybackControls.ts index 06141d884f..ec0608ed14 100644 --- a/packages/framer-motion/src/animation/GroupPlaybackControls.ts +++ b/packages/framer-motion/src/animation/GroupPlaybackControls.ts @@ -36,6 +36,7 @@ export class GroupPlaybackControls implements AnimationPlaybackControls { animation.attachTimeline(timeline) } else { animation.pause() + return observeTimeline((progress) => { animation.time = animation.duration * progress }, timeline) @@ -76,7 +77,10 @@ export class GroupPlaybackControls implements AnimationPlaybackControls { } private runAll( - methodName: keyof Omit + methodName: keyof Omit< + AnimationPlaybackControls, + PropNames | "then" | "state" + > ) { this.animations.forEach((controls) => controls[methodName]()) } diff --git a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts similarity index 89% rename from packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx rename to packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts index 8beaf851bf..b9b5ed7ea3 100644 --- a/packages/framer-motion/src/animation/__tests__/animate-waapi.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate-waapi.test.ts @@ -1,3 +1,4 @@ +import { nextFrame } from "../../gestures/__tests__/utils" import { animate } from "../animate" import { defaultOptions } from "../animators/waapi/__tests__/setup" import { stagger } from "../utils/stagger" @@ -12,6 +13,8 @@ describe("animate() with WAAPI", () => { { duration: 1, transform: { duration: 2 } } ) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, { ...defaultOptions, duration: 1000 } @@ -65,6 +68,8 @@ describe("animate() with WAAPI", () => { animate(a, { opacity: [0.2, 0.5] }, { ease: "easeIn" }) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0.2, 0.5], offset: undefined }, { @@ -81,6 +86,8 @@ describe("animate() with WAAPI", () => { animate(b, { opacity: [0.2, 0.5] }, { ease: ["easeIn"] }) + await nextFrame() + expect(b.animate).toBeCalledWith( { opacity: [0.2, 0.5], offset: undefined, easing: ["ease-in"] }, { @@ -101,6 +108,8 @@ describe("animate() with WAAPI", () => { { times: [0.2, 0.3, 1], ease: [[0, 1, 2, 3], "linear"] } ) + await nextFrame() + expect(c.animate).toBeCalledWith( { opacity: [0.2, 0.5, 1], @@ -119,11 +128,30 @@ describe("animate() with WAAPI", () => { }) test("Returns duration correctly", async () => { + const a = document.createElement("div") + const animation = animate( - document.createElement("div"), + a, { opacity: 1 }, { duration: 2, opacity: { duration: 3 } } ) + + await nextFrame() + + expect(a.animate).toBeCalledWith( + { + opacity: [0, 1], + }, + { + delay: -0, + duration: 3000, + easing: "ease-out", + iterations: 1, + direction: "normal", + fill: "both", + } + ) + expect(animation.duration).toEqual(3) }) @@ -139,6 +167,8 @@ describe("animate() with WAAPI", () => { ], ]) + await nextFrame() + expect(a.animate).toBeCalledWith( { opacity: [0, 1, 1], diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx index 022d346379..a92fc73fbc 100644 --- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx @@ -5,7 +5,7 @@ import { motion, MotionGlobalConfig } from "../.." import { animate } from "../animate" import { useMotionValue } from "../../value/use-motion-value" import { motionValue, MotionValue } from "../../value" -import { syncDriver } from "../animators/js/__tests__/utils" +import { syncDriver } from "../animators/__tests__/utils" const duration = 0.001 diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx index 8a05c01c50..8e97587cd3 100644 --- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx @@ -21,7 +21,10 @@ const originalGetComputedStyle = window.getComputedStyle function getComputedStyleStub() { return { - getPropertyValue(variableName: "--from" | "--to" | "--a" | "--color") { + background: fromValue, + getPropertyValue( + variableName: "background" | "--from" | "--to" | "--a" | "--color" + ) { switch (variableName) { case fromName: return fromValue @@ -53,6 +56,7 @@ describe("css variables", () => { test("should animate css color variables", async () => { const promise = new Promise((resolve) => { let frameCount = 0 + const Component = () => ( { const results = await promise expect(results).toEqual([ { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, + { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" }, ]) }) diff --git a/packages/framer-motion/src/animation/animate.ts b/packages/framer-motion/src/animation/animate.ts index 28d6dbe5d2..05c2563d0b 100644 --- a/packages/framer-motion/src/animation/animate.ts +++ b/packages/framer-motion/src/animation/animate.ts @@ -141,7 +141,7 @@ export const createScopedAnimate = (scope?: AnimationScope) => { /** * Implementation */ - function scopedAnimate( + function scopedAnimate( valueOrElementOrSequence: | AnimationSequence | ElementOrSelector @@ -171,7 +171,7 @@ export const createScopedAnimate = (scope?: AnimationScope) => { ) } else { animation = animateSingleValue( - valueOrElementOrSequence, + valueOrElementOrSequence as MotionValue | V, keyframes as V | GenericKeyframesTarget, options as ValueAnimationTransition | undefined ) diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts new file mode 100644 index 0000000000..a80946168c --- /dev/null +++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts @@ -0,0 +1,338 @@ +import { EasingDefinition } from "../../easing/types" +import { time } from "../../frameloop/sync-time" +import { DOMKeyframesResolver } from "../../render/dom/DOMKeyframesResolver" +import { ResolvedKeyframes } from "../../render/utils/KeyframesResolver" +import { memo } from "../../utils/memo" +import { noop } from "../../utils/noop" +import { + millisecondsToSeconds, + secondsToMilliseconds, +} from "../../utils/time-conversion" +import { MotionValue } from "../../value" +import { ValueAnimationOptions } from "../types" +import { + BaseAnimation, + ValueAnimationOptionsWithDefaults, +} from "./BaseAnimation" +import { MainThreadAnimation } from "./MainThreadAnimation" +import { animateStyle } from "./waapi" +import { isWaapiSupportedEasing } from "./waapi/easing" +import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe" + +const supportsWaapi = memo(() => + Object.hasOwnProperty.call(Element.prototype, "animate") +) + +/** + * A list of values that can be hardware-accelerated. + */ +const acceleratedValues = new Set([ + "opacity", + "clipPath", + "filter", + "transform", + // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved + // or until we implement support for linear() easing. + // "background-color" +]) + +/** + * 10ms is chosen here as it strikes a balance between smooth + * results (more than one keyframe per frame at 60fps) and + * keyframe quantity. + */ +const sampleDelta = 10 //ms + +/** + * Implement a practical max duration for keyframe generation + * to prevent infinite loops + */ +const maxDuration = 20_000 + +/** + * Check if an animation can run natively via WAAPI or requires pregenerated keyframes. + * WAAPI doesn't support spring or function easings so we run these as JS animation before + * handing off. + */ +function requiresPregeneratedKeyframes( + options: ValueAnimationOptions +) { + return ( + options.type === "spring" || + options.name === "backgroundColor" || + !isWaapiSupportedEasing(options.ease) + ) +} + +function pregenerateKeyframes( + keyframes: ResolvedKeyframes, + options: ValueAnimationOptions +) { + /** + * Create a main-thread animation to pregenerate keyframes. + * We sample this at regular intervals to generate keyframes that we then + * linearly interpolate between. + */ + const sampleAnimation = new MainThreadAnimation({ + ...options, + keyframes, + repeat: 0, + delay: 0, + }) + + let state = { done: false, value: keyframes[0] } + const pregeneratedKeyframes: T[] = [] + + /** + * Bail after 20 seconds of pre-generated keyframes as it's likely + * we're heading for an infinite loop. + */ + let t = 0 + while (!state.done && t < maxDuration) { + state = sampleAnimation.sample(t) + pregeneratedKeyframes.push(state.value) + t += sampleDelta + } + + return { + times: undefined, + keyframes: pregeneratedKeyframes, + duration: t - sampleDelta, + ease: "linear" as EasingDefinition, + } +} + +export interface AcceleratedValueAnimationOptions< + T extends string | number = number +> extends ValueAnimationOptions { + name: string + motionValue: MotionValue +} + +interface ResolvedAcceleratedAnimation { + animation: Animation + duration: number + keyframes: string[] | number[] +} + +export class AcceleratedAnimation< + T extends string | number +> extends BaseAnimation { + protected options: ValueAnimationOptionsWithDefaults & { + name: string + motionValue: MotionValue + } + + constructor(options: ValueAnimationOptions) { + super(options) + + const { name, motionValue, keyframes } = this.options + this.resolver = new DOMKeyframesResolver( + keyframes, + (resolvedKeyframes: ResolvedKeyframes) => + this.onKeyframesResolved(resolvedKeyframes), + name, + motionValue + ) + + this.resolver.scheduleResolve() + } + + /** + * An AnimationTimline to attach to the WAAPI animation once it's created. + */ + private pendingTimeline: AnimationTimeline | undefined + + protected initPlayback( + keyframes: ResolvedKeyframes + ): ResolvedAcceleratedAnimation { + let duration = this.options.duration || 300 + + /** + * If this animation needs pre-generated keyframes then generate. + */ + if (requiresPregeneratedKeyframes(this.options)) { + const { onComplete, onUpdate, motionValue, ...options } = + this.options + const pregeneratedAnimation = pregenerateKeyframes( + keyframes, + options + ) + + keyframes = pregeneratedAnimation.keyframes + duration = pregeneratedAnimation.duration + this.options.times = pregeneratedAnimation.times + this.options.ease = pregeneratedAnimation.ease + } + + const { motionValue, name } = this.options + const animation = animateStyle( + motionValue.owner!.current as unknown as HTMLElement, + name, + keyframes as string[], + { ...this.options, duration } + ) + + // Override the browser calculated startTime with one synchronised to other JS + // and WAAPI animations starting this event loop. + animation.startTime = time.now() + + if (this.pendingTimeline) { + animation.timeline = this.pendingTimeline + this.pendingTimeline = undefined + } else { + /** + * Prefer the `onfinish` prop as it's more widely supported than + * the `finished` promise. + * + * Here, we synchronously set the provided MotionValue to the end + * keyframe. If we didn't, when the WAAPI animation is finished it would + * be removed from the element which would then revert to its old styles. + */ + animation.onfinish = () => { + const { onComplete } = this.options + motionValue.set(getFinalKeyframe(keyframes, this.options)) + onComplete && onComplete() + this.cancel() + this.resolveFinishedPromise() + this.updateFinishedPromise() + } + } + + return { + animation, + duration, + keyframes: keyframes as string[] | number[], + } + } + + get duration() { + const { duration } = this.resolved + return millisecondsToSeconds(duration) + } + + get time() { + const { animation } = this.resolved + return millisecondsToSeconds((animation.currentTime as number) || 0) + } + + set time(newTime: number) { + const { animation } = this.resolved + animation.currentTime = secondsToMilliseconds(newTime) + } + + get speed() { + const { animation } = this.resolved + return animation.playbackRate + } + + set speed(newSpeed: number) { + const { animation } = this.resolved + animation.playbackRate = newSpeed + } + + get state() { + const { animation } = this.resolved + return animation.playState + } + + /** + * Replace the default DocumentTimeline with another AnimationTimeline. + * Currently used for scroll animations. + */ + attachTimeline(timeline: any) { + if (!this._resolved) { + this.pendingTimeline = timeline + } else { + const { animation } = this.resolved + + animation.timeline = timeline + animation.onfinish = null + } + + return noop + } + + play() { + if (this.isStopped) return + + const { animation } = this.resolved + animation.play() + } + + pause() { + const { animation } = this.resolved + animation.pause() + } + + stop() { + this.isStopped = true + const { animation, keyframes } = this.resolved + + if ( + animation.playState === "idle" || + animation.playState === "finished" + ) { + return + } + + /** + * WAAPI doesn't natively have any interruption capabilities. + * + * Rather than read commited styles back out of the DOM, we can + * create a renderless JS animation and sample it twice to calculate + * its current value, "previous" value, and therefore allow + * Motion to calculate velocity for any subsequent animation. + */ + if (this.time) { + const { motionValue, onUpdate, onComplete, ...options } = + this.options + + const sampleAnimation = new MainThreadAnimation({ + ...options, + keyframes, + }) + + motionValue.setWithVelocity( + sampleAnimation.sample(this.time - sampleDelta).value, + sampleAnimation.sample(this.time).value, + sampleDelta + ) + } + + this.cancel() + } + + complete() { + this.resolved.animation.finish() + } + + cancel() { + this.resolved.animation.cancel() + } + + static supports( + options: ValueAnimationOptions + ): options is AcceleratedValueAnimationOptions { + const { motionValue, name, repeatDelay, repeatType, damping, type } = + options + + return ( + supportsWaapi() && + name && + acceleratedValues.has(name) && + motionValue && + motionValue.owner && + motionValue.owner.current instanceof HTMLElement && + /** + * If we're outputting values to onUpdate then we can't use WAAPI as there's + * no way to read the value from WAAPI every frame. + */ + !motionValue.owner.getProps().onUpdate && + !repeatDelay && + repeatType !== "mirror" && + damping !== 0 && + type !== "inertia" + ) + } +} diff --git a/packages/framer-motion/src/animation/animators/BaseAnimation.ts b/packages/framer-motion/src/animation/animators/BaseAnimation.ts new file mode 100644 index 0000000000..cabfdb8192 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/BaseAnimation.ts @@ -0,0 +1,149 @@ +import { + KeyframeResolver, + ResolvedKeyframes, + flushKeyframeResolvers, +} from "../../render/utils/KeyframesResolver" +import { instantAnimationState } from "../../utils/use-instant-transition-state" +import { + AnimationPlaybackControls, + RepeatType, + ValueAnimationOptions, +} from "../types" +import { canAnimate } from "./utils/can-animate" +import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe" + +export interface ValueAnimationOptionsWithDefaults + extends ValueAnimationOptions { + autoplay: boolean + delay: number + repeat: number + repeatDelay: number + repeatType: RepeatType +} + +export abstract class BaseAnimation + implements AnimationPlaybackControls +{ + // Persistent reference to the options used to create this animation + protected options: ValueAnimationOptionsWithDefaults + + // Resolve the current finished promise + protected resolveFinishedPromise: VoidFunction + + // A promise that resolves when the animation is complete + protected currentFinishedPromise: Promise + + // Track whether the animation has been stopped. Stopped animations won't restart. + protected isStopped = false + + // Internal reference to defered resolved keyframes and animation-specific data returned from initPlayback. + protected _resolved: Resolved & { keyframes: ResolvedKeyframes } + + // Reference to the active keyframes resolver. + protected resolver: KeyframeResolver + + constructor({ + autoplay = true, + delay = 0, + type = "keyframes", + repeat = 0, + repeatDelay = 0, + repeatType = "loop", + ...options + }: ValueAnimationOptions) { + this.options = { + autoplay, + delay, + type, + repeat, + repeatDelay, + repeatType, + ...options, + } + + this.updateFinishedPromise() + } + + protected abstract initPlayback(keyframes: ResolvedKeyframes): Resolved + + abstract play(): void + abstract pause(): void + abstract stop(): void + abstract cancel(): void + abstract complete(): void + abstract get speed(): number + abstract set speed(newSpeed: number) + abstract get time(): number + abstract set time(newTime: number) + abstract get duration(): number + abstract get state(): AnimationPlayState + + /** + * A getter for resolved data. If keyframes are not yet resolved, accessing + * this.resolved will synchronously flush all pending keyframe resolvers. + * This is a deoptimisation, but at its worst still batches read/writes. + */ + get resolved(): Resolved & { keyframes: ResolvedKeyframes } { + if (!this._resolved) flushKeyframeResolvers() + + return this._resolved + } + + /** + * A method to be called when the keyframes resolver completes. This method + * will check if its possible to run the animation and, if not, skip it. + * Otherwise, it will call initPlayback on the implementing class. + */ + protected onKeyframesResolved(keyframes: ResolvedKeyframes) { + const { name, type, velocity, delay, onComplete, onUpdate } = + this.options + + /** + * If we can't animate this value with the resolved keyframes + * then we should complete it immediately. + */ + if (!canAnimate(keyframes, name, type, velocity)) { + // Finish immediately + if (instantAnimationState.current || !delay) { + const finalKeyframe = getFinalKeyframe(keyframes, this.options) + onUpdate?.(finalKeyframe) + onComplete?.() + this.resolveFinishedPromise() + this.updateFinishedPromise() + + return + } + // Finish after a delay + else { + this.options.duration = 0 + } + } + + this._resolved = { + keyframes, + ...this.initPlayback(keyframes), + } + + this.onPostResolved() + } + + onPostResolved() {} + + /** + * Allows the returned animation to be awaited or promise-chained. Currently + * resolves when the animation finishes at all but in a future update could/should + * reject if its cancels. + */ + then(resolve: VoidFunction, reject?: VoidFunction) { + return this.currentFinishedPromise.then(resolve, reject) + } + + protected updateFinishedPromise() { + this.currentFinishedPromise = new Promise((resolve) => { + this.resolveFinishedPromise = () => { + resolve() + this.updateFinishedPromise() + } + }) + } +} diff --git a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts new file mode 100644 index 0000000000..a516b46213 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts @@ -0,0 +1,522 @@ +import { + KeyframeResolver as DefaultKeyframeResolver, + ResolvedKeyframes, +} from "../../render/utils/KeyframesResolver" +import { spring } from "../generators/spring/index" +import { inertia } from "../generators/inertia" +import { keyframes as keyframesGeneratorFactory } from "../generators/keyframes" +import { ValueAnimationOptions } from "../types" +import { BaseAnimation } from "./BaseAnimation" +import { AnimationState, KeyframeGenerator } from "../generators/types" +import { pipe } from "../../utils/pipe" +import { mix } from "../../utils/mix" +import { calcGeneratorDuration } from "../generators/utils/calc-duration" +import { DriverControls } from "./drivers/types" +import { + millisecondsToSeconds, + secondsToMilliseconds, +} from "../../utils/time-conversion" +import { clamp } from "../../utils/clamp" +import { invariant } from "../../utils/errors" +import { frameloopDriver } from "./drivers/driver-frameloop" + +type GeneratorFactory = ( + options: ValueAnimationOptions +) => KeyframeGenerator + +const generators: { [key: string]: GeneratorFactory } = { + decay: inertia, + inertia, + tween: keyframesGeneratorFactory, + keyframes: keyframesGeneratorFactory, + spring, +} + +const percentToProgress = (percent: number) => percent / 100 + +interface ResolvedData { + generator: KeyframeGenerator + mirroredGenerator: KeyframeGenerator | undefined + mapPercentToKeyframes: ((v: number) => T) | undefined + + /** + * Duration of the animation as calculated by the generator. + */ + calculatedDuration: number + + /** + * Duration of the animation plus repeatDelay. + */ + resolvedDuration: number + + /** + * Total duration of the animation including repeats. + */ + totalDuration: number +} + +/** + * Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of + * features we expose publically. Mostly the compatibility is to ensure visual identity + * between both WAAPI and main thread animations. + */ +export class MainThreadAnimation< + T extends string | number +> extends BaseAnimation> { + /** + * The driver that's controlling the animation loop. Normally this is a requestAnimationFrame loop + * but in tests we can pass in a synchronous loop. + */ + private driver?: DriverControls + + /** + * The time at which the animation was paused. + */ + private holdTime: number | null = null + + /** + * The time at which the animation was started. + */ + private startTime: number | null = null + + /** + * The time at which the animation was cancelled. + */ + private cancelTime: number | null = null + + /** + * The current time of the animation. + */ + private currentTime: number = 0 + + /** + * Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed. + */ + private playbackSpeed = 1 + + /** + * The state of the animation to apply when the animation is resolved. This + * allows calls to the public API to control the animation before it is resolved, + * without us having to resolve it first. + */ + private pendingPlayState: AnimationPlayState = "running" + + constructor({ + KeyframeResolver = DefaultKeyframeResolver, + ...options + }: ValueAnimationOptions) { + super(options) + + const { name, motionValue, keyframes } = this.options + const onResolved = (resolvedKeyframes: ResolvedKeyframes) => + this.onKeyframesResolved(resolvedKeyframes) + + if (name && motionValue && motionValue.owner) { + this.resolver = (motionValue.owner as any).resolveKeyframes( + keyframes, + onResolved, + name, + motionValue + ) + } else { + this.resolver = new KeyframeResolver( + keyframes, + onResolved, + name, + motionValue + ) + } + + this.resolver.scheduleResolve() + } + + protected initPlayback(keyframes: ResolvedKeyframes) { + const { + type = "keyframes", + repeat = 0, + repeatDelay = 0, + repeatType, + velocity = 0, + } = this.options + + const generatorFactory = generators[type] || keyframesGeneratorFactory + + /** + * If our generator doesn't support mixing numbers, we need to replace keyframes with + * [0, 100] and then make a function that maps that to the actual keyframes. + * + * 100 is chosen instead of 1 as it works nicer with spring animations. + */ + let mapPercentToKeyframes: ((v: number) => T) | undefined + let mirroredGenerator: KeyframeGenerator | undefined + + if ( + generatorFactory !== keyframesGeneratorFactory && + typeof keyframes[0] !== "number" + ) { + if (process.env.NODE_ENV !== "production") { + invariant( + keyframes.length === 2, + `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` + ) + } + + mapPercentToKeyframes = pipe( + percentToProgress, + mix(keyframes[0], keyframes[1]) + ) as (t: number) => T + + keyframes = [0 as T, 100 as T] + } + + const generator = generatorFactory({ ...this.options, keyframes }) + + /** + * If we have a mirror repeat type we need to create a second generator that outputs the + * mirrored (not reversed) animation and later ping pong between the two generators. + */ + if (repeatType === "mirror") { + mirroredGenerator = generatorFactory({ + ...this.options, + keyframes: [...keyframes].reverse(), + velocity: -velocity, + }) + } + + /** + * If duration is undefined and we have repeat options, + * we need to calculate a duration from the generator. + * + * We set it to the generator itself to cache the duration. + * Any timeline resolver will need to have already precalculated + * the duration by this step. + */ + if (generator.calculatedDuration === null) { + generator.calculatedDuration = calcGeneratorDuration(generator) + } + + const { calculatedDuration } = generator + const resolvedDuration = calculatedDuration + repeatDelay + const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay + + return { + generator, + mirroredGenerator, + mapPercentToKeyframes, + calculatedDuration, + resolvedDuration, + totalDuration, + } + } + + onPostResolved() { + const { autoplay = true } = this.options + + this.play() + + if (this.pendingPlayState === "paused" || !autoplay) { + this.pause() + } else { + this.state = this.pendingPlayState + } + } + + tick(timestamp: number, sample = false) { + const { + generator, + mirroredGenerator, + mapPercentToKeyframes, + keyframes, + calculatedDuration, + totalDuration, + resolvedDuration, + } = this.resolved + + if (this.startTime === null) return generator.next(0) + + const { delay, repeat, repeatType, repeatDelay, onUpdate } = + this.options + + /** + * requestAnimationFrame timestamps can come through as lower than + * the startTime as set by performance.now(). Here we prevent this, + * though in the future it could be possible to make setting startTime + * a pending operation that gets resolved here. + */ + if (this.speed > 0) { + this.startTime = Math.min(this.startTime, timestamp) + } else if (this.speed < 0) { + this.startTime = Math.min( + timestamp - totalDuration / this.speed, + this.startTime + ) + } + + // Update currentTime + if (sample) { + this.currentTime = timestamp + } else if (this.holdTime !== null) { + this.currentTime = this.holdTime + } else { + // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 = + // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for + // example. + this.currentTime = + Math.round(timestamp - this.startTime) * this.speed + } + + // Rebase on delay + const timeWithoutDelay = + this.currentTime - delay * (this.speed >= 0 ? 1 : -1) + const isInDelayPhase = + this.speed >= 0 + ? timeWithoutDelay < 0 + : timeWithoutDelay > totalDuration + this.currentTime = Math.max(timeWithoutDelay, 0) + + // If this animation has finished, set the current time to the total duration. + if (this.state === "finished" && this.holdTime === null) { + this.currentTime = totalDuration + } + + let elapsed = this.currentTime + + let frameGenerator = generator + + if (repeat) { + /** + * Get the current progress (0-1) of the animation. If t is > + * than duration we'll get values like 2.5 (midway through the + * third iteration) + */ + const progress = + Math.min(this.currentTime, totalDuration) / resolvedDuration + + /** + * Get the current iteration (0 indexed). For instance the floor of + * 2.5 is 2. + */ + let currentIteration = Math.floor(progress) + + /** + * Get the current progress of the iteration by taking the remainder + * so 2.5 is 0.5 through iteration 2 + */ + let iterationProgress = progress % 1.0 + + /** + * If iteration progress is 1 we count that as the end + * of the previous iteration. + */ + if (!iterationProgress && progress >= 1) { + iterationProgress = 1 + } + + iterationProgress === 1 && currentIteration-- + + currentIteration = Math.min(currentIteration, repeat + 1) + + /** + * Reverse progress if we're not running in "normal" direction + */ + + const isOddIteration = Boolean(currentIteration % 2) + if (isOddIteration) { + if (repeatType === "reverse") { + iterationProgress = 1 - iterationProgress + if (repeatDelay) { + iterationProgress -= repeatDelay / resolvedDuration + } + } else if (repeatType === "mirror") { + frameGenerator = mirroredGenerator! + } + } + + elapsed = clamp(0, 1, iterationProgress) * resolvedDuration + } + + /** + * If we're in negative time, set state as the initial keyframe. + * This prevents delay: x, duration: 0 animations from finishing + * instantly. + */ + const state = isInDelayPhase + ? { done: false, value: keyframes[0] } + : frameGenerator.next(elapsed) + + if (mapPercentToKeyframes) { + state.value = mapPercentToKeyframes(state.value as number) + } + + let { done } = state + + if (!isInDelayPhase && calculatedDuration !== null) { + done = + this.speed >= 0 + ? this.currentTime >= totalDuration + : this.currentTime <= 0 + } + + const isAnimationFinished = + this.holdTime === null && + (this.state === "finished" || (this.state === "running" && done)) + + if (onUpdate) { + onUpdate(state.value) + } + + if (isAnimationFinished) { + this.finish() + } + + return state + } + + state: AnimationPlayState = "idle" + + get duration() { + return millisecondsToSeconds(this.resolved.calculatedDuration) + } + + get time() { + return millisecondsToSeconds(this.currentTime) + } + + set time(newTime: number) { + newTime = secondsToMilliseconds(newTime) + this.currentTime = newTime + + if (this.holdTime !== null || this.speed === 0) { + this.holdTime = newTime + } else if (this.driver) { + this.startTime = this.driver.now() - newTime / this.speed + } + } + + get speed() { + return this.playbackSpeed + } + + set speed(newSpeed: number) { + const hasChanged = this.playbackSpeed !== newSpeed + this.playbackSpeed = newSpeed + if (hasChanged) { + this.time = millisecondsToSeconds(this.currentTime) + } + } + + play() { + if (!this.resolver.isScheduled) { + this.resolver.resume() + } + + if (!this._resolved) { + this.pendingPlayState = "running" + return + } + + if (this.isStopped) return + + const { driver = frameloopDriver, onPlay } = this.options + + if (!this.driver) { + this.driver = driver((timestamp) => this.tick(timestamp)) + } + + onPlay && onPlay() + + const now = this.driver.now() + + if (this.holdTime !== null) { + this.startTime = now - this.holdTime + } else if (!this.startTime || this.state === "finished") { + this.startTime = now + } + + if (this.state === "finished") { + this.updateFinishedPromise() + } + + this.cancelTime = this.startTime + this.holdTime = null + + /** + * Set playState to running only after we've used it in + * the previous logic. + */ + this.state = "running" + + this.driver.start() + } + + pause() { + if (!this._resolved) { + this.pendingPlayState = "paused" + return + } + + this.state = "paused" + this.holdTime = this.currentTime ?? 0 + } + + stop() { + this.isStopped = true + if (this.state === "idle") return + + this.state = "idle" + const { onStop } = this.options + onStop && onStop() + this.teardown() + } + + complete() { + if (this.state !== "running") { + this.play() + } + + this.pendingPlayState = this.state = "finished" + this.holdTime = null + } + + finish() { + this.teardown() + this.state = "finished" + + const { onComplete } = this.options + onComplete && onComplete() + } + + cancel() { + if (this.cancelTime !== null) { + this.tick(this.cancelTime) + } + this.teardown() + } + + private teardown() { + this.state = "idle" + this.stopDriver() + this.resolveFinishedPromise() + this.updateFinishedPromise() + this.startTime = this.cancelTime = null + this.resolver.cancel() + } + + private stopDriver() { + if (!this.driver) return + this.driver.stop() + this.driver = undefined + } + + sample(time: number): AnimationState { + this.startTime = 0 + return this.tick(time, true) + } +} + +// Legacy interface +export function animateValue( + options: ValueAnimationOptions +): MainThreadAnimation { + return new MainThreadAnimation(options) +} diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts similarity index 97% rename from packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts rename to packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts index b8f6ad48b6..2d0cafee13 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts +++ b/packages/framer-motion/src/animation/animators/__tests__/MainThreadAnimation.test.ts @@ -1,19 +1,26 @@ -import { animateValue } from "../" -import { reverseEasing } from "../../../../easing/modifiers/reverse" -import { nextFrame } from "../../../../gestures/__tests__/utils" -import { noop } from "../../../../utils/noop" -import { ValueAnimationOptions } from "../../../types" +import { MainThreadAnimation, animateValue } from "../MainThreadAnimation" +import { reverseEasing } from "../../../easing/modifiers/reverse" +import { nextFrame } from "../../../gestures/__tests__/utils" +import { noop } from "../../../utils/noop" +import { ValueAnimationOptions } from "../../types" import { syncDriver } from "./utils" +import { KeyframeResolver } from "../../../render/utils/KeyframesResolver" const linear = noop -function testAnimate( +class AsyncKeyframesResolver extends KeyframeResolver { + constructor(...args: [any, any, any, any, any]) { + super(...args, true) + } +} + +function testAnimate( options: ValueAnimationOptions, expected: V[], resolve: () => void ) { const output: V[] = [] - animateValue({ + new MainThreadAnimation({ driver: syncDriver(20), duration: 100, ease: linear, @@ -27,7 +34,7 @@ function testAnimate( }) } -describe("animate", () => { +describe("MainThreadAnimation", () => { test("Correctly performs an animation with default settings", async () => { return new Promise((resolve) => testAnimate( @@ -1231,6 +1238,25 @@ describe("animate", () => { expect(output).toEqual([0, 20, 40, 100]) }) + test("Correctly completes an animation with async resolver", async () => { + const output: number[] = [] + + const animation = animateValue({ + KeyframeResolver: AsyncKeyframesResolver as any, + keyframes: [0, 100], + driver: syncDriver(20), + duration: 100, + ease: linear, + onUpdate: (v) => output.push(v), + }) + + animation.complete() + + await animation + + expect(output).toEqual([100]) + }) + test("Updates speed to half speed", async () => { const output: number[] = [] diff --git a/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts b/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts deleted file mode 100644 index 3757511bf8..0000000000 --- a/packages/framer-motion/src/animation/animators/__tests__/instant.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { createInstantAnimation } from "../instant" - -describe("instantAnimation", () => { - test("Is instant, await", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - await createInstantAnimation({ - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Is instant, .then()", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - await new Promise((resolve) => { - animation.then(() => {}).then(() => resolve()) - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Can delay, await", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - delay: 0.1, - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - expect(onUpdate).not.toBeCalledWith(1) - expect(onComplete).not.toBeCalled() - - await animation - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Can delay, .then()", async () => { - const onUpdate = jest.fn() - const onComplete = jest.fn() - - const animation = createInstantAnimation({ - delay: 0.1, - keyframes: [0, 1], - onUpdate, - onComplete, - }) - - await new Promise((resolve) => { - animation.then(() => {}).then(() => resolve()) - - expect(onUpdate).not.toBeCalledWith(1) - expect(onComplete).not.toBeCalled() - }) - - expect(onUpdate).toBeCalledWith(1) - expect(onComplete).toBeCalled() - }) - - test("Returns duration: 0", async () => { - const animation = createInstantAnimation({ - delay: 0, - keyframes: [0, 1], - }) - expect(animation.duration).toEqual(0) - - const animationWithDelay = createInstantAnimation({ - delay: 0.2, - keyframes: [0, 1], - }) - expect(animationWithDelay.duration).toEqual(0) - }) -}) diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/utils.ts b/packages/framer-motion/src/animation/animators/__tests__/utils.ts similarity index 94% rename from packages/framer-motion/src/animation/animators/js/__tests__/utils.ts rename to packages/framer-motion/src/animation/animators/__tests__/utils.ts index 80f3efd986..193027dd44 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/utils.ts +++ b/packages/framer-motion/src/animation/animators/__tests__/utils.ts @@ -1,4 +1,4 @@ -import { KeyframeGenerator } from "../../../generators/types" +import { KeyframeGenerator } from "../../generators/types" export const syncDriver = (interval = 10) => { const driver = (update: (v: number) => void) => { diff --git a/packages/framer-motion/src/animation/animators/js/driver-frameloop.ts b/packages/framer-motion/src/animation/animators/drivers/driver-frameloop.ts similarity index 100% rename from packages/framer-motion/src/animation/animators/js/driver-frameloop.ts rename to packages/framer-motion/src/animation/animators/drivers/driver-frameloop.ts diff --git a/packages/framer-motion/src/animation/animators/js/types.ts b/packages/framer-motion/src/animation/animators/drivers/types.ts similarity index 100% rename from packages/framer-motion/src/animation/animators/js/types.ts rename to packages/framer-motion/src/animation/animators/drivers/types.ts diff --git a/packages/framer-motion/src/animation/animators/instant.ts b/packages/framer-motion/src/animation/animators/instant.ts deleted file mode 100644 index 80a61258b7..0000000000 --- a/packages/framer-motion/src/animation/animators/instant.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" -import { animateValue } from "./js" -import { noop } from "../../utils/noop" - -export function createInstantAnimation({ - keyframes, - delay, - onUpdate, - onComplete, -}: ValueAnimationOptions): AnimationPlaybackControls { - const setValue = (): AnimationPlaybackControls => { - onUpdate && onUpdate(keyframes[keyframes.length - 1]) - onComplete && onComplete() - - /** - * TODO: As this API grows it could make sense to always return - * animateValue. This will be a bigger project as animateValue - * is frame-locked whereas this function resolves instantly. - * This is a behavioural change and also has ramifications regarding - * assumptions within tests. - */ - return { - time: 0, - speed: 1, - duration: 0, - play: noop, - pause: noop, - stop: noop, - then: (resolve: VoidFunction) => { - resolve() - return Promise.resolve() - }, - cancel: noop, - complete: noop, - } - } - - return delay - ? animateValue({ - keyframes: [0, 1], - duration: 0, - delay, - onComplete: setValue, - }) - : setValue() -} diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts deleted file mode 100644 index fd3d02ac60..0000000000 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { AnimationPlaybackControls } from "../../types" -import { keyframes as keyframesGeneratorFactory } from "../../generators/keyframes" -import { spring } from "../../generators/spring/index" -import { inertia } from "../../generators/inertia" -import { AnimationState, KeyframeGenerator } from "../../generators/types" -import { DriverControls } from "./types" -import { ValueAnimationOptions } from "../../types" -import { frameloopDriver } from "./driver-frameloop" -import { clamp } from "../../../utils/clamp" -import { - millisecondsToSeconds, - secondsToMilliseconds, -} from "../../../utils/time-conversion" -import { calcGeneratorDuration } from "../../generators/utils/calc-duration" -import { invariant } from "../../../utils/errors" -import { mix } from "../../../utils/mix" -import { pipe } from "../../../utils/pipe" - -type GeneratorFactory = ( - options: ValueAnimationOptions -) => KeyframeGenerator - -const types: { [key: string]: GeneratorFactory } = { - decay: inertia, - inertia, - tween: keyframesGeneratorFactory, - keyframes: keyframesGeneratorFactory, - spring, -} - -export interface MainThreadAnimationControls - extends AnimationPlaybackControls { - sample: (t: number) => AnimationState -} - -const percentToProgress = (percent: number) => percent / 100 - -/** - * Animate a single value on the main thread. - * - * This function is written, where functionality overlaps, - * to be largely spec-compliant with WAAPI to allow fungibility - * between the two. - */ -export function animateValue({ - autoplay = true, - delay = 0, - driver = frameloopDriver, - keyframes, - type = "keyframes", - repeat = 0, - repeatDelay = 0, - repeatType = "loop", - onPlay, - onStop, - onComplete, - onUpdate, - ...options -}: ValueAnimationOptions): MainThreadAnimationControls { - let speed = 1 - - let hasStopped = false - let resolveFinishedPromise: VoidFunction - let currentFinishedPromise: Promise - - /** - * Resolve the current Promise every time we enter the - * finished state. This is WAAPI-compatible behaviour. - */ - const updateFinishedPromise = () => { - currentFinishedPromise = new Promise((resolve) => { - resolveFinishedPromise = resolve - }) - } - - // Create the first finished promise - updateFinishedPromise() - - let animationDriver: DriverControls | undefined - - const generatorFactory = types[type] || keyframesGeneratorFactory - - /** - * If this isn't the keyframes generator and we've been provided - * strings as keyframes, we need to interpolate these. - */ - let mapNumbersToKeyframes: undefined | ((t: number) => V) - if ( - generatorFactory !== keyframesGeneratorFactory && - typeof keyframes[0] !== "number" - ) { - if (process.env.NODE_ENV !== "production") { - invariant( - keyframes.length === 2, - `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` - ) - } - - mapNumbersToKeyframes = pipe( - percentToProgress, - mix(keyframes[0], keyframes[1]) - ) as (t: number) => V - - keyframes = [0, 100] as any - } - - const generator = generatorFactory({ ...options, keyframes }) - - let mirroredGenerator: KeyframeGenerator | undefined - if (repeatType === "mirror") { - mirroredGenerator = generatorFactory({ - ...options, - keyframes: [...keyframes].reverse(), - velocity: -(options.velocity || 0), - }) - } - - let playState: AnimationPlayState = "idle" - let holdTime: number | null = null - let startTime: number | null = null - let cancelTime: number | null = null - - /** - * If duration is undefined and we have repeat options, - * we need to calculate a duration from the generator. - * - * We set it to the generator itself to cache the duration. - * Any timeline resolver will need to have already precalculated - * the duration by this step. - */ - if (generator.calculatedDuration === null && repeat) { - generator.calculatedDuration = calcGeneratorDuration(generator) - } - - const { calculatedDuration } = generator - - let resolvedDuration = Infinity - let totalDuration = Infinity - - if (calculatedDuration !== null) { - resolvedDuration = calculatedDuration + repeatDelay - totalDuration = resolvedDuration * (repeat + 1) - repeatDelay - } - - let currentTime = 0 - const tick = (timestamp: number) => { - if (startTime === null) return - - /** - * requestAnimationFrame timestamps can come through as lower than - * the startTime as set by performance.now(). Here we prevent this, - * though in the future it could be possible to make setting startTime - * a pending operation that gets resolved here. - */ - if (speed > 0) startTime = Math.min(startTime, timestamp) - if (speed < 0) - startTime = Math.min(timestamp - totalDuration / speed, startTime) - - if (holdTime !== null) { - currentTime = holdTime - } else { - // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 = - // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for - // example. - currentTime = Math.round(timestamp - startTime) * speed - } - - // Rebase on delay - const timeWithoutDelay = currentTime - delay * (speed >= 0 ? 1 : -1) - const isInDelayPhase = - speed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration - currentTime = Math.max(timeWithoutDelay, 0) - - /** - * If this animation has finished, set the current time - * to the total duration. - */ - if (playState === "finished" && holdTime === null) { - currentTime = totalDuration - } - - let elapsed = currentTime - - let frameGenerator = generator - - if (repeat) { - /** - * Get the current progress (0-1) of the animation. If t is > - * than duration we'll get values like 2.5 (midway through the - * third iteration) - */ - const progress = - Math.min(currentTime, totalDuration) / resolvedDuration - - /** - * Get the current iteration (0 indexed). For instance the floor of - * 2.5 is 2. - */ - let currentIteration = Math.floor(progress) - - /** - * Get the current progress of the iteration by taking the remainder - * so 2.5 is 0.5 through iteration 2 - */ - let iterationProgress = progress % 1.0 - - /** - * If iteration progress is 1 we count that as the end - * of the previous iteration. - */ - if (!iterationProgress && progress >= 1) { - iterationProgress = 1 - } - - iterationProgress === 1 && currentIteration-- - - currentIteration = Math.min(currentIteration, repeat + 1) - - /** - * Reverse progress if we're not running in "normal" direction - */ - - const isOddIteration = Boolean(currentIteration % 2) - if (isOddIteration) { - if (repeatType === "reverse") { - iterationProgress = 1 - iterationProgress - if (repeatDelay) { - iterationProgress -= repeatDelay / resolvedDuration - } - } else if (repeatType === "mirror") { - frameGenerator = mirroredGenerator! - } - } - - elapsed = clamp(0, 1, iterationProgress) * resolvedDuration - } - - /** - * If we're in negative time, set state as the initial keyframe. - * This prevents delay: x, duration: 0 animations from finishing - * instantly. - */ - const state = isInDelayPhase - ? { done: false, value: keyframes[0] } - : frameGenerator.next(elapsed) - - if (mapNumbersToKeyframes) { - state.value = mapNumbersToKeyframes(state.value) - } - - let { done } = state - - if (!isInDelayPhase && calculatedDuration !== null) { - done = speed >= 0 ? currentTime >= totalDuration : currentTime <= 0 - } - - const isAnimationFinished = - holdTime === null && - (playState === "finished" || (playState === "running" && done)) - - if (onUpdate) { - onUpdate(state.value) - } - - if (isAnimationFinished) { - finish() - } - - return state - } - - const stopAnimationDriver = () => { - animationDriver && animationDriver.stop() - animationDriver = undefined - } - - const cancel = () => { - playState = "idle" - stopAnimationDriver() - resolveFinishedPromise() - updateFinishedPromise() - startTime = cancelTime = null - } - - const finish = () => { - playState = "finished" - onComplete && onComplete() - stopAnimationDriver() - resolveFinishedPromise() - } - - const play = () => { - if (hasStopped) return - - if (!animationDriver) animationDriver = driver(tick) - - const now = animationDriver.now() - - onPlay && onPlay() - - if (holdTime !== null) { - startTime = now - holdTime - } else if (!startTime || playState === "finished") { - startTime = now - } - - if (playState === "finished") { - updateFinishedPromise() - } - - cancelTime = startTime - holdTime = null - - /** - * Set playState to running only after we've used it in - * the previous logic. - */ - playState = "running" - - animationDriver.start() - } - - if (autoplay) { - play() - } - - const controls = { - then(resolve: VoidFunction, reject?: VoidFunction) { - return currentFinishedPromise.then(resolve, reject) - }, - get time() { - return millisecondsToSeconds(currentTime) - }, - set time(newTime: number) { - newTime = secondsToMilliseconds(newTime) - - currentTime = newTime - if (holdTime !== null || !animationDriver || speed === 0) { - holdTime = newTime - } else { - startTime = animationDriver.now() - newTime / speed - } - }, - get duration() { - const duration = - generator.calculatedDuration === null - ? calcGeneratorDuration(generator) - : generator.calculatedDuration - - return millisecondsToSeconds(duration) - }, - get speed() { - return speed - }, - set speed(newSpeed: number) { - if (newSpeed === speed || !animationDriver) return - speed = newSpeed - controls.time = millisecondsToSeconds(currentTime) - }, - get state() { - return playState - }, - play, - pause: () => { - playState = "paused" - holdTime = currentTime - }, - stop: () => { - hasStopped = true - if (playState === "idle") return - playState = "idle" - onStop && onStop() - cancel() - }, - cancel: () => { - if (cancelTime !== null) tick(cancelTime) - cancel() - }, - complete: () => { - playState = "finished" - holdTime === null - }, - sample: (elapsed: number) => { - startTime = 0 - return tick(elapsed)! - }, - } - - return controls -} diff --git a/packages/framer-motion/src/animation/animators/utils/can-animate.ts b/packages/framer-motion/src/animation/animators/utils/can-animate.ts new file mode 100644 index 0000000000..b722740b89 --- /dev/null +++ b/packages/framer-motion/src/animation/animators/utils/can-animate.ts @@ -0,0 +1,42 @@ +import { ResolvedKeyframes } from "../../../render/utils/KeyframesResolver" +import { warning } from "../../../utils/errors" +import { isAnimatable } from "../../utils/is-animatable" + +function hasKeyframesChanged(keyframes: ResolvedKeyframes) { + const current = keyframes[0] + if (keyframes.length === 1) return true + for (let i = 0; i < keyframes.length; i++) { + if (keyframes[i] !== current) return true + } +} + +export function canAnimate( + keyframes: ResolvedKeyframes, + name?: string, + type?: string, + velocity?: number +) { + /** + * Check if we're able to animate between the start and end keyframes, + * and throw a warning if we're attempting to animate between one that's + * animatable and another that isn't. + */ + const originKeyframe = keyframes[0] + if (originKeyframe === null) return false + + const targetKeyframe = keyframes[keyframes.length - 1] + const isOriginAnimatable = isAnimatable(originKeyframe, name) + const isTargetAnimatable = isAnimatable(targetKeyframe, name) + + warning( + isOriginAnimatable === isTargetAnimatable, + `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` + ) + + // Always skip if any of these are true + if (!isOriginAnimatable || !isTargetAnimatable) { + return false + } + + return hasKeyframesChanged(keyframes) || (type === "spring" && velocity) +} diff --git a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts deleted file mode 100644 index 9fe664932e..0000000000 --- a/packages/framer-motion/src/animation/animators/waapi/create-accelerated-animation.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { EasingDefinition } from "../../../easing/types" -import { frame, cancelFrame } from "../../../frameloop" -import type { VisualElement } from "../../../render/VisualElement" -import type { MotionValue } from "../../../value" -import { AnimationPlaybackControls, ValueAnimationOptions } from "../../types" -import { animateStyle } from "." -import { isWaapiSupportedEasing } from "./easing" -import { getFinalKeyframe } from "./utils/get-final-keyframe" -import { animateValue } from "../js" -import { - millisecondsToSeconds, - secondsToMilliseconds, -} from "../../../utils/time-conversion" -import { memo } from "../../../utils/memo" -import { noop } from "../../../utils/noop" - -const supportsWaapi = memo(() => - Object.hasOwnProperty.call(Element.prototype, "animate") -) - -/** - * A list of values that can be hardware-accelerated. - */ -const acceleratedValues = new Set([ - "opacity", - "clipPath", - "filter", - "transform", -]) - -/** - * 10ms is chosen here as it strikes a balance between smooth - * results (more than one keyframe per frame at 60fps) and - * keyframe quantity. - */ -const sampleDelta = 10 //ms - -/** - * Implement a practical max duration for keyframe generation - * to prevent infinite loops - */ -const maxDuration = 20_000 - -const requiresPregeneratedKeyframes = ( - valueName: string, - options: ValueAnimationOptions -) => - options.type === "spring" || - valueName === "backgroundColor" || - !isWaapiSupportedEasing(options.ease) - -export function createAcceleratedAnimation( - value: MotionValue, - valueName: string, - { onUpdate, onComplete, ...options }: ValueAnimationOptions -): AnimationPlaybackControls | false { - const canAccelerateAnimation = - supportsWaapi() && - acceleratedValues.has(valueName) && - !options.repeatDelay && - options.repeatType !== "mirror" && - options.damping !== 0 && - options.type !== "inertia" - - if (!canAccelerateAnimation) return false - - /** - * TODO: Unify with js/index - */ - let hasStopped = false - let resolveFinishedPromise: VoidFunction - let currentFinishedPromise: Promise - - /** - * Cancelling an animation will write to the DOM. For safety we want to defer - * this until the next `update` frame lifecycle. This flag tracks whether we - * have a pending cancel, if so we shouldn't allow animations to finish. - */ - let pendingCancel = false - - /** - * Resolve the current Promise every time we enter the - * finished state. This is WAAPI-compatible behaviour. - */ - const updateFinishedPromise = () => { - currentFinishedPromise = new Promise((resolve) => { - resolveFinishedPromise = resolve - }) - } - - // Create the first finished promise - updateFinishedPromise() - - let { keyframes, duration = 300, ease, times } = options - - /** - * If this animation needs pre-generated keyframes then generate. - */ - if (requiresPregeneratedKeyframes(valueName, options)) { - const sampleAnimation = animateValue({ - ...options, - repeat: 0, - delay: 0, - }) - let state = { done: false, value: keyframes[0] } - const pregeneratedKeyframes: number[] = [] - - /** - * Bail after 20 seconds of pre-generated keyframes as it's likely - * we're heading for an infinite loop. - */ - let t = 0 - while (!state.done && t < maxDuration) { - state = sampleAnimation.sample(t) - pregeneratedKeyframes.push(state.value) - t += sampleDelta - } - - times = undefined - keyframes = pregeneratedKeyframes - duration = t - sampleDelta - ease = "linear" - } - - const animation = animateStyle( - (value.owner as VisualElement).current!, - valueName, - keyframes, - { - ...options, - duration, - /** - * This function is currently not called if ease is provided - * as a function so the cast is safe. - * - * However it would be possible for a future refinement to port - * in easing pregeneration from Motion One for browsers that - * support the upcoming `linear()` easing function. - */ - ease: ease as EasingDefinition, - times, - } - ) - - const cancelAnimation = () => { - pendingCancel = false - animation.cancel() - } - - const safeCancel = () => { - pendingCancel = true - frame.update(cancelAnimation) - resolveFinishedPromise() - updateFinishedPromise() - } - - /** - * Prefer the `onfinish` prop as it's more widely supported than - * the `finished` promise. - * - * Here, we synchronously set the provided MotionValue to the end - * keyframe. If we didn't, when the WAAPI animation is finished it would - * be removed from the element which would then revert to its old styles. - */ - animation.onfinish = () => { - if (pendingCancel) return - value.set(getFinalKeyframe(keyframes, options)) - onComplete && onComplete() - safeCancel() - } - - /** - * Animation interrupt callback. - */ - const controls = { - then(resolve: VoidFunction, reject?: VoidFunction) { - return currentFinishedPromise.then(resolve, reject) - }, - attachTimeline(timeline: any) { - animation.timeline = timeline - animation.onfinish = null - return noop - }, - get time() { - return millisecondsToSeconds(animation.currentTime || 0) - }, - set time(newTime: number) { - animation.currentTime = secondsToMilliseconds(newTime) - }, - get speed() { - return animation.playbackRate - }, - set speed(newSpeed: number) { - animation.playbackRate = newSpeed - }, - get duration() { - return millisecondsToSeconds(duration) - }, - play: () => { - if (hasStopped) return - animation.play() - - /** - * Cancel any pending cancel tasks - */ - cancelFrame(cancelAnimation) - }, - pause: () => animation.pause(), - stop: () => { - hasStopped = true - if (animation.playState === "idle") return - - /** - * WAAPI doesn't natively have any interruption capabilities. - * - * Rather than read commited styles back out of the DOM, we can - * create a renderless JS animation and sample it twice to calculate - * its current value, "previous" value, and therefore allow - * Motion to calculate velocity for any subsequent animation. - */ - const { currentTime } = animation - - if (currentTime) { - const sampleAnimation = animateValue({ - ...options, - autoplay: false, - }) - - value.setWithVelocity( - sampleAnimation.sample(currentTime - sampleDelta).value, - sampleAnimation.sample(currentTime).value, - sampleDelta - ) - } - safeCancel() - }, - complete: () => { - if (pendingCancel) return - animation.finish() - }, - cancel: safeCancel, - } - - return controls -} diff --git a/packages/framer-motion/src/animation/animators/waapi/index.ts b/packages/framer-motion/src/animation/animators/waapi/index.ts index dc92602762..5557a31cae 100644 --- a/packages/framer-motion/src/animation/animators/waapi/index.ts +++ b/packages/framer-motion/src/animation/animators/waapi/index.ts @@ -7,7 +7,7 @@ export function animateStyle( keyframes: string[] | number[], { delay = 0, - duration, + duration = 300, repeat = 0, repeatType = "loop", ease, diff --git a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts b/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts index 1aa5a140e1..9909939832 100644 --- a/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts +++ b/packages/framer-motion/src/animation/animators/waapi/utils/get-final-keyframe.ts @@ -1,12 +1,16 @@ import { Repeat } from "../../../../types" +const isNotNull = (value: unknown) => value !== null + export function getFinalKeyframe( keyframes: T[], { repeat, repeatType = "loop" }: Repeat ): T { + const resolvedKeyframes = keyframes.filter(isNotNull) const index = repeat && repeatType !== "loop" && repeat % 2 === 1 ? 0 - : keyframes.length - 1 - return keyframes[index] + : resolvedKeyframes.length - 1 + + return resolvedKeyframes[index] } diff --git a/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts b/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts index 1e950374b8..0a46a60444 100644 --- a/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts +++ b/packages/framer-motion/src/animation/generators/__tests__/keyframes.test.ts @@ -3,7 +3,7 @@ import { easeInOut } from "../../../easing/ease" import { defaultOffset } from "../../../utils/offsets/default" import { convertOffsetToTimes } from "../../../utils/offsets/time" import { defaultEasing, keyframes } from "../keyframes" -import { animateSync } from "../../animators/js/__tests__/utils" +import { animateSync } from "../../animators/__tests__/utils" const linear = noop diff --git a/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts b/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts index 106dc47a84..6a5ffc883f 100644 --- a/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts +++ b/packages/framer-motion/src/animation/generators/__tests__/spring.test.ts @@ -1,6 +1,6 @@ import { ValueAnimationOptions } from "../../types" import { spring } from "../spring" -import { animateSync } from "../../animators/js/__tests__/utils" +import { animateSync } from "../../animators/__tests__/utils" describe("spring", () => { test("Runs animations with default values ", () => { diff --git a/packages/framer-motion/src/animation/generators/inertia.ts b/packages/framer-motion/src/animation/generators/inertia.ts index ea36ad493d..e3c8cc9765 100644 --- a/packages/framer-motion/src/animation/generators/inertia.ts +++ b/packages/framer-motion/src/animation/generators/inertia.ts @@ -100,7 +100,7 @@ export function inertia({ * If we have a spring and the provided t is beyond the moment the friction * animation crossed the min/max boundary, use the spring. */ - if (timeReachedBoundary !== undefined && t > timeReachedBoundary) { + if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) { return spring!.next(t - timeReachedBoundary) } else { !hasUpdatedFrame && applyFriction(t) diff --git a/packages/framer-motion/src/animation/generators/keyframes.ts b/packages/framer-motion/src/animation/generators/keyframes.ts index 0670896f57..c7ec15a5b1 100644 --- a/packages/framer-motion/src/animation/generators/keyframes.ts +++ b/packages/framer-motion/src/animation/generators/keyframes.ts @@ -15,7 +15,7 @@ export function defaultEasing( return values.map(() => easing || easeInOut).splice(0, values.length - 1) } -export function keyframes({ +export function keyframes({ duration = 300, keyframes: keyframeValues, times, diff --git a/packages/framer-motion/src/animation/hooks/animation-controls.ts b/packages/framer-motion/src/animation/hooks/animation-controls.ts index 93745b8061..4526d38df6 100644 --- a/packages/framer-motion/src/animation/hooks/animation-controls.ts +++ b/packages/framer-motion/src/animation/hooks/animation-controls.ts @@ -1,13 +1,41 @@ import { invariant } from "../../utils/errors" -import { setValues } from "../../render/utils/setters" +import { setTarget } from "../../render/utils/setters" import type { VisualElement } from "../../render/VisualElement" -import { AnimationControls } from "../types" +import { AnimationControls, AnimationDefinition } from "../types" import { animateVisualElement } from "../interfaces/visual-element" function stopAnimation(visualElement: VisualElement) { visualElement.values.forEach((value) => value.stop()) } +function setVariants(visualElement: VisualElement, variantLabels: string[]) { + const reversedLabels = [...variantLabels].reverse() + + reversedLabels.forEach((key) => { + const variant = visualElement.getVariant(key) + variant && setTarget(visualElement, variant) + + if (visualElement.variantChildren) { + visualElement.variantChildren.forEach((child) => { + setVariants(child, variantLabels) + }) + } + }) +} + +export function setValues( + visualElement: VisualElement, + definition: AnimationDefinition +) { + if (Array.isArray(definition)) { + return setVariants(visualElement, definition) + } else if (typeof definition === "string") { + return setVariants(visualElement, [definition]) + } else { + setTarget(visualElement, definition as any) + } +} + /** * @public */ diff --git a/packages/framer-motion/src/animation/hooks/use-animated-state.ts b/packages/framer-motion/src/animation/hooks/use-animated-state.ts index 60ca87b4ec..f0f8ba8a7f 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from "react" import { useConstant } from "../../utils/use-constant" -import { checkTargetForNewValues, getOrigin } from "../../render/utils/setters" import { TargetAndTransition } from "../../types" import { ResolvedValues } from "../../render/types" import { makeUseVisualState } from "../../motion/utils/use-visual-state" @@ -46,16 +45,6 @@ class StateVisualElement extends VisualElement< sortInstanceNodePosition() { return 0 } - - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - const origin = getOrigin(target as any, transition || {}, this) - checkTargetForNewValues(this, target, origin as any) - return { transition, transitionEnd, ...target } - } } const useVisualState = makeUseVisualState({ diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts index c2afc603f5..569fd2f22e 100644 --- a/packages/framer-motion/src/animation/interfaces/motion-value.ts +++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts @@ -1,26 +1,29 @@ -import { warning } from "../../utils/errors" -import { ResolvedValueTarget, Transition } from "../../types" +import { Transition } from "../../types" import { secondsToMilliseconds } from "../../utils/time-conversion" -import { instantAnimationState } from "../../utils/use-instant-transition-state" import type { MotionValue, StartAnimation } from "../../value" -import { createAcceleratedAnimation } from "../animators/waapi/create-accelerated-animation" -import { createInstantAnimation } from "../animators/instant" import { getDefaultTransition } from "../utils/default-transitions" -import { isAnimatable } from "../utils/is-animatable" -import { getKeyframes } from "../utils/keyframes" import { getValueTransition, isTransitionDefined } from "../utils/transitions" -import { animateValue } from "../animators/js" -import { AnimationPlaybackControls, ValueAnimationOptions } from "../types" +import { ValueAnimationOptions } from "../types" +import type { UnresolvedKeyframes } from "../../render/utils/KeyframesResolver" import { MotionGlobalConfig } from "../../utils/GlobalConfig" +import { instantAnimationState } from "../../utils/use-instant-transition-state" +import type { VisualElement } from "../../render/VisualElement" +import { getFinalKeyframe } from "../animators/waapi/utils/get-final-keyframe" +import { frame } from "../../frameloop/frame" +import { AcceleratedAnimation } from "../animators/AcceleratedAnimation" +import { MainThreadAnimation } from "../animators/MainThreadAnimation" -export const animateMotionValue = ( - valueName: string, - value: MotionValue, - target: ResolvedValueTarget, - transition: Transition & { elapsed?: number; isHandoff?: boolean } = {} -): StartAnimation => { - return (onComplete: VoidFunction): AnimationPlaybackControls => { - const valueTransition = getValueTransition(transition, valueName) || {} +export const animateMotionValue = + ( + name: string, + value: MotionValue, + target: V | UnresolvedKeyframes, + transition: Transition & { elapsed?: number } = {}, + element?: VisualElement, + isHandoff?: boolean + ): StartAnimation => + (onComplete) => { + const valueTransition = getValueTransition(transition, name) || {} /** * Most transition values are currently completely overwritten by value-specific @@ -36,32 +39,10 @@ export const animateMotionValue = ( let { elapsed = 0 } = transition elapsed = elapsed - secondsToMilliseconds(delay) - const keyframes = getKeyframes( - value, - valueName, - target, - valueTransition - ) - - /** - * Check if we're able to animate between the start and end keyframes, - * and throw a warning if we're attempting to animate between one that's - * animatable and another that isn't. - */ - const originKeyframe = keyframes[0] - const targetKeyframe = keyframes[keyframes.length - 1] - const isOriginAnimatable = isAnimatable(valueName, originKeyframe) - const isTargetAnimatable = isAnimatable(valueName, targetKeyframe) - - warning( - isOriginAnimatable === isTargetAnimatable, - `You are trying to animate ${valueName} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.` - ) - let options: ValueAnimationOptions = { - keyframes, - velocity: value.getVelocity(), + keyframes: Array.isArray(target) ? target : [null, target], ease: "easeOut", + velocity: value.getVelocity(), ...valueTransition, delay: -elapsed, onUpdate: (v) => { @@ -72,6 +53,9 @@ export const animateMotionValue = ( onComplete() valueTransition.onComplete && valueTransition.onComplete() }, + name, + motionValue: value, + element: isHandoff ? undefined : element, } /** @@ -81,7 +65,7 @@ export const animateMotionValue = ( if (!isTransitionDefined(valueTransition)) { options = { ...options, - ...getDefaultTransition(valueName, options), + ...getDefaultTransition(name, options), } } @@ -93,59 +77,61 @@ export const animateMotionValue = ( if (options.duration) { options.duration = secondsToMilliseconds(options.duration) } - if (options.repeatDelay) { options.repeatDelay = secondsToMilliseconds(options.repeatDelay) } + if (options.from !== undefined) { + options.keyframes[0] = options.from + } + + let shouldSkip = false + + if ((options as any).type === false) { + options.duration = 0 + if (options.delay === 0) { + shouldSkip = true + } + } + if ( - !isOriginAnimatable || - !isTargetAnimatable || instantAnimationState.current || - valueTransition.type === false || MotionGlobalConfig.skipAnimations ) { - /** - * If we can't animate this value, or the global instant animation flag is set, - * or this is simply defined as an instant transition, return an instant transition. - */ - return createInstantAnimation( - instantAnimationState.current - ? { ...options, delay: 0 } - : options - ) + shouldSkip = true + options.duration = 0 + options.delay = 0 } /** - * Animate via WAAPI if possible. + * If we can or must skip creating the animation, and apply only + * the final keyframe, do so. We also check once keyframes are resolved but + * this early check prevents the need to create an animation at all. */ - if ( - /** - * If this is a handoff animation, the optimised animation will be running via - * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the - * optimised animation. - */ - !transition.isHandoff && - value.owner && - value.owner.current instanceof HTMLElement && - /** - * If we're outputting values to onUpdate then we can't use WAAPI as there's - * no way to read the value from WAAPI every frame. - */ - !value.owner.getProps().onUpdate - ) { - const acceleratedAnimation = createAcceleratedAnimation( - value, - valueName, - options + if (shouldSkip && !isHandoff && value.get() !== undefined) { + const finalKeyframe = getFinalKeyframe( + options.keyframes as V[], + valueTransition ) - if (acceleratedAnimation) return acceleratedAnimation + if (finalKeyframe !== undefined) { + frame.update(() => { + options.onUpdate!(finalKeyframe) + options.onComplete!() + }) + + return + } } /** - * If we didn't create an accelerated animation, create a JS animation + * Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via + * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the + * optimised animation. */ - return animateValue(options) + if (!isHandoff && AcceleratedAnimation.supports(options)) { + return new AcceleratedAnimation(options) + } else { + return new MainThreadAnimation(options) + } } -} diff --git a/packages/framer-motion/src/animation/interfaces/single-value.ts b/packages/framer-motion/src/animation/interfaces/single-value.ts index 43533a6f8c..ccd18a64b5 100644 --- a/packages/framer-motion/src/animation/interfaces/single-value.ts +++ b/packages/framer-motion/src/animation/interfaces/single-value.ts @@ -4,16 +4,14 @@ import { isMotionValue } from "../../value/utils/is-motion-value" import { GenericKeyframesTarget } from "../../types" import { AnimationPlaybackControls, ValueAnimationTransition } from "../types" -export function animateSingleValue( +export function animateSingleValue( value: MotionValue | V, keyframes: V | GenericKeyframesTarget, options?: ValueAnimationTransition ): AnimationPlaybackControls { const motionValue = isMotionValue(value) ? value : createMotionValue(value) - motionValue.start( - animateMotionValue("", motionValue, keyframes as any, options) - ) + motionValue.start(animateMotionValue("", motionValue, keyframes, options)) return motionValue.animation! } diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts index 3a1d803c41..f8b4b19d58 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts @@ -1,7 +1,7 @@ import { transformProps } from "../../render/html/utils/transform" import type { AnimationTypeState } from "../../render/utils/animation-state" import type { VisualElement } from "../../render/VisualElement" -import type { Target, TargetAndTransition } from "../../types" +import type { TargetAndTransition } from "../../types" import { optimizedAppearDataAttribute } from "../optimized-appear/data-id" import type { VisualElementAnimationOptions } from "./types" import { animateMotionValue } from "./motion-value" @@ -9,7 +9,6 @@ import { isWillChangeMotionValue } from "../../value/use-will-change/is" import { setTarget } from "../../render/utils/setters" import { AnimationPlaybackControls } from "../types" import { getValueTransition } from "../utils/transitions" -import { MotionValue } from "../../value" import { frame } from "../../frameloop" /** @@ -29,28 +28,16 @@ function shouldBlockAnimation( return shouldBlock } -function hasKeyframesChanged(value: MotionValue, target: Target) { - const current = value.get() - - if (Array.isArray(target)) { - for (let i = 0; i < target.length; i++) { - if (target[i] !== current) return true - } - } else { - return current !== target - } -} - export function animateTarget( visualElement: VisualElement, - definition: TargetAndTransition, + targetAndTransition: TargetAndTransition, { delay = 0, transitionOverride, type }: VisualElementAnimationOptions = {} ): AnimationPlaybackControls[] { let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target - } = visualElement.makeTargetAnimatable(definition) + } = targetAndTransition const willChange = visualElement.getValue("willChange") @@ -64,11 +51,13 @@ export function animateTarget( visualElement.animationState.getState()[type] for (const key in target) { - const value = visualElement.getValue(key) + const value = visualElement.getValue( + key, + visualElement.latestValues[key] ?? null + ) const valueTarget = target[key] if ( - !value || valueTarget === undefined || (animationTypeState && shouldBlockAnimation(animationTypeState, key)) @@ -86,47 +75,21 @@ export function animateTarget( * If this is the first time a value is being animated, check * to see if we're handling off from an existing animation. */ + let isHandoff = false if (window.HandoffAppearAnimations) { const appearId = visualElement.getProps()[optimizedAppearDataAttribute] if (appearId) { - const elapsed = window.HandoffAppearAnimations( - appearId, - key, - value, - frame - ) + const elapsed = window.HandoffAppearAnimations(appearId, key) if (elapsed !== null) { valueTransition.elapsed = elapsed - valueTransition.isHandoff = true + isHandoff = true } } } - let canSkip = - !valueTransition.isHandoff && - !hasKeyframesChanged(value, valueTarget) - - if ( - valueTransition.type === "spring" && - (value.getVelocity() || valueTransition.velocity) - ) { - canSkip = false - } - - /** - * Temporarily disable skipping animations if there's an animation in - * progress. Better would be to track the current target of a value - * and compare that against valueTarget. - */ - if (value.animation) { - canSkip = false - } - - if (canSkip) continue - value.start( animateMotionValue( key, @@ -134,23 +97,29 @@ export function animateTarget( valueTarget, visualElement.shouldReduceMotion && transformProps.has(key) ? { type: false } - : valueTransition + : valueTransition, + visualElement, + isHandoff ) ) - const animation = value.animation! + const animation = value.animation - if (isWillChangeMotionValue(willChange)) { - willChange.add(key) - animation.then(() => willChange.remove(key)) - } + if (animation) { + if (isWillChangeMotionValue(willChange)) { + willChange.add(key) + animation.then(() => willChange.remove(key)) + } - animations.push(animation) + animations.push(animation) + } } if (transitionEnd) { Promise.all(animations).then(() => { - transitionEnd && setTarget(visualElement, transitionEnd) + frame.update(() => { + transitionEnd && setTarget(visualElement, transitionEnd) + }) }) } diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts b/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts index 153732d19d..06dec1bb09 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element-variant.ts @@ -8,7 +8,14 @@ export function animateVariant( variant: string, options: VisualElementAnimationOptions = {} ) { - const resolved = resolveVariant(visualElement, variant, options.custom) + const resolved = resolveVariant( + visualElement, + variant, + options.type === "exit" + ? visualElement.presenceContext?.custom + : undefined + ) + let { transition = visualElement.getDefaultTransition() || {} } = resolved || {} diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/framer-motion/src/animation/interfaces/visual-element.ts index 2e3bc89f48..591a8ad351 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element.ts @@ -1,3 +1,4 @@ +import { frame } from "../../frameloop" import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" import { VisualElement } from "../../render/VisualElement" import { AnimationDefinition } from "../types" @@ -31,7 +32,9 @@ export function animateVisualElement( ) } - return animation.then(() => - visualElement.notify("AnimationComplete", definition) - ) + return animation.then(() => { + frame.postRender(() => { + visualElement.notify("AnimationComplete", definition) + }) + }) } diff --git a/packages/framer-motion/src/animation/optimized-appear/types.ts b/packages/framer-motion/src/animation/optimized-appear/types.ts index 17c07b791a..aa1686aaac 100644 --- a/packages/framer-motion/src/animation/optimized-appear/types.ts +++ b/packages/framer-motion/src/animation/optimized-appear/types.ts @@ -4,14 +4,8 @@ import type { MotionValue } from "../../value" export type HandoffFunction = ( storeId: string, valueName: string, - /** - * Legacy arguments. This function is inlined as part of SSG so it can be there's - * a version mismatch between the main included Motion and the inlined script. - * - * Remove in early 2024. - */ - _value: MotionValue, - _frame: Batcher + _value?: MotionValue, + _frame?: Batcher ) => null | number /** diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts index 659dbd26bc..eec9d71b83 100644 --- a/packages/framer-motion/src/animation/types.ts +++ b/packages/framer-motion/src/animation/types.ts @@ -1,10 +1,15 @@ import { TargetAndTransition, TargetResolver } from "../types" import type { VisualElement } from "../render/VisualElement" import { Easing } from "../easing/types" -import { Driver } from "./animators/js/types" +import { Driver } from "./animators/drivers/types" import { SVGPathProperties, VariantLabels } from "../motion/types" import { SVGAttributes } from "../render/svg/types-attributes" import { ProgressTimeline } from "../render/dom/scroll/observe" +import { MotionValue } from "../value" +import { + KeyframeResolver, + OnKeyframesResolved, +} from "../render/utils/KeyframesResolver" export interface AnimationPlaybackLifecycles { onUpdate?: (latest: V) => void @@ -29,13 +34,22 @@ export interface Transition export interface ValueAnimationTransition extends Transition, - AnimationPlaybackLifecycles { - isHandoff?: boolean -} + AnimationPlaybackLifecycles {} + +export type ResolveKeyframes = ( + keyframes: V[], + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: any +) => KeyframeResolver -export interface ValueAnimationOptions +export interface ValueAnimationOptions extends ValueAnimationTransition { keyframes: V[] + KeyframeResolver?: typeof KeyframeResolver + name?: string + motionValue?: MotionValue + from?: V } export interface AnimationScope { @@ -153,9 +167,11 @@ export interface VelocityOptions { restDelta?: number } +export type RepeatType = "loop" | "reverse" | "mirror" + export interface AnimationPlaybackOptions { repeat?: number - repeatType?: "loop" | "reverse" | "mirror" + repeatType?: RepeatType repeatDelay?: number } diff --git a/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts b/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts deleted file mode 100644 index 66b2380c71..0000000000 --- a/packages/framer-motion/src/animation/utils/__tests__/keyframes.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { motionValue } from "../../../value" -import { getKeyframes } from "../keyframes" - -describe("getKeyframes", () => { - test("Makes animatable 'none' from string target", () => { - const keyframes = getKeyframes( - motionValue("none"), - "transform", - "translateX(100px)", - {} - ) - expect(keyframes).toEqual(["translateX(0px)", "translateX(100px)"]) - }) - - test("Makes animatable 'none' from keyframes target", () => { - const a = getKeyframes( - motionValue("none"), - "transform", - [null, "translateX(100px)"], - {} - ) - expect(a).toEqual(["translateX(0px)", "translateX(100px)"]) - - const b = getKeyframes( - motionValue("none"), - "transform", - [null, "translateX(100px)", null], - {} - ) - expect(b).toEqual([ - "translateX(0px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("Replaces 'none' within keyframes", () => { - const keyframes = getKeyframes( - motionValue("translateX(200px)"), - "transform", - ["none", "translateX(100px)"], - {} - ) - expect(keyframes).toEqual(["translateX(0px)", "translateX(100px)"]) - }) - - test("Fills wildcard keyframes", () => { - const keyframes = getKeyframes( - motionValue("none"), - "transform", - [null, null, "translateX(100px)", null], - {} - ) - expect(keyframes).toEqual([ - "translateX(0px)", - "translateX(0px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("from overrides current motion value", () => { - const keyframes = getKeyframes( - motionValue("translateX(1px)"), - "transform", - [null, null, "translateX(100px)", null], - { from: "translateX(2px)" } - ) - expect(keyframes).toEqual([ - "translateX(2px)", - "translateX(2px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("initial keyframe overrides from, if not null", () => { - const keyframes = getKeyframes( - motionValue("translateX(1px)"), - "transform", - ["translateX(3px)", null, "translateX(100px)", null], - { from: "translateX(2px)" } - ) - expect(keyframes).toEqual([ - "translateX(3px)", - "translateX(3px)", - "translateX(100px)", - "translateX(100px)", - ]) - }) - - test("Matches value type of origin keyframe if zero/none", () => { - const a = getKeyframes(motionValue(0), "transform", "50px", {}) - expect(a).toEqual(["0px", "50px"]) - const b = getKeyframes(motionValue("0"), "transform", "50px", {}) - expect(b).toEqual(["0px", "50px"]) - const c = getKeyframes(motionValue(2), "transform", [0, "50px"], {}) - expect(c).toEqual(["0px", "50px"]) - const d = getKeyframes(motionValue(2), "transform", ["0", "50px"], {}) - expect(d).toEqual(["0px", "50px"]) - const e = getKeyframes( - motionValue(2), - "transform", - ["none", "50px"], - {} - ) - expect(e).toEqual(["0px", "50px"]) - - const f = getKeyframes(motionValue("0px"), "transform", "50%", {}) - expect(f).toEqual(["0%", "50%"]) - }) - - test("Matches value type of subsequent keyframes if zero/none", () => { - const a = getKeyframes(motionValue("50px"), "transform", 0, {}) - expect(a).toEqual(["50px", "0px"]) - const b = getKeyframes(motionValue("50px"), "transform", "0", {}) - expect(b).toEqual(["50px", "0px"]) - const c = getKeyframes( - motionValue(""), - "transform", - [0, "50px", null], - {} - ) - expect(c).toEqual(["0px", "50px", "50px"]) - const d = getKeyframes( - motionValue(2), - "transform", - ["0", "50px", "none"], - {} - ) - expect(d).toEqual(["0px", "50px", "0px"]) - const e = getKeyframes( - motionValue(2), - "transform", - ["none", "50px"], - {} - ) - expect(e).toEqual(["0px", "50px"]) - }) - - test("Makes 0 motion value animatable to string", () => { - const keyframes = getKeyframes(motionValue(0), "transform", "0%", {}) - expect(keyframes).toEqual(["0%", "0%"]) - }) - - test("Makes 0 keyframe animatable to string", () => { - const keyframes = getKeyframes( - motionValue(0), - "transform", - [0, "0%"], - {} - ) - expect(keyframes).toEqual(["0%", "0%"]) - }) -}) diff --git a/packages/framer-motion/src/animation/utils/is-animatable.ts b/packages/framer-motion/src/animation/utils/is-animatable.ts index 27febc13f2..72ffe8a4c3 100644 --- a/packages/framer-motion/src/animation/utils/is-animatable.ts +++ b/packages/framer-motion/src/animation/utils/is-animatable.ts @@ -10,9 +10,12 @@ import { ValueKeyframesDefinition } from "../types" * * @internal */ -export const isAnimatable = (key: string, value: ValueKeyframesDefinition) => { +export const isAnimatable = ( + value: ValueKeyframesDefinition, + name?: string +) => { // If the list of keys tat might be non-animatable grows, replace with Set - if (key === "zIndex") return false + if (name === "zIndex") return false // If it's a number or a keyframes array, we can animate it. We might at some point // need to do a deep isAnimatable check of keyframes, or let Popmotion handle this, diff --git a/packages/framer-motion/src/animation/utils/is-none.ts b/packages/framer-motion/src/animation/utils/is-none.ts index 9eb0c3c0bd..39de76960a 100644 --- a/packages/framer-motion/src/animation/utils/is-none.ts +++ b/packages/framer-motion/src/animation/utils/is-none.ts @@ -5,5 +5,7 @@ export function isNone(value: string | number | null) { return value === 0 } else if (value !== null) { return value === "none" || value === "0" || isZeroValueString(value) + } else { + return true } } diff --git a/packages/framer-motion/src/animation/utils/keyframes.ts b/packages/framer-motion/src/animation/utils/keyframes.ts deleted file mode 100644 index c1dfe3e1ec..0000000000 --- a/packages/framer-motion/src/animation/utils/keyframes.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { getAnimatableNone } from "../../render/dom/value-types/animatable-none" -import { Transition } from "../../types" -import { MotionValue } from "../../value" -import { ValueKeyframesDefinition } from "../types" -import { isAnimatable } from "./is-animatable" -import { isNone } from "./is-none" - -export function getKeyframes( - value: MotionValue, - valueName: string, - target: ValueKeyframesDefinition, - transition: Transition -): string[] | number[] { - const isTargetAnimatable = isAnimatable(valueName, target) - let keyframes: ValueKeyframesDefinition - - if (Array.isArray(target)) { - keyframes = [...target] - } else { - keyframes = [null, target] - } - - const defaultOrigin = - transition.from !== undefined ? transition.from : value.get() - - let animatableTemplateValue: string | undefined = undefined - const noneKeyframeIndexes: number[] = [] - - for (let i = 0; i < keyframes.length; i++) { - /** - * Fill null/wildcard keyframes - */ - if (keyframes[i] === null) { - keyframes[i] = i === 0 ? defaultOrigin : keyframes[i - 1] - } - - if (isNone(keyframes[i])) { - noneKeyframeIndexes.push(i) - } - - // TODO: Clean this conditional, it works for now - if ( - typeof keyframes[i] === "string" && - keyframes[i] !== "none" && - keyframes[i] !== "0" - ) { - animatableTemplateValue = keyframes[i] as string - } - } - - if ( - isTargetAnimatable && - noneKeyframeIndexes.length && - animatableTemplateValue - ) { - for (let i = 0; i < noneKeyframeIndexes.length; i++) { - const index = noneKeyframeIndexes[i] - keyframes[index] = getAnimatableNone( - valueName, - animatableTemplateValue - ) - } - } - - return keyframes as string[] | number[] -} diff --git a/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx b/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx index 69e4573bc8..f168461a6c 100644 --- a/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx @@ -1,5 +1,7 @@ import * as React from "react" -import { useRef, useInsertionEffect, useId } from "react" +import { useRef, useInsertionEffect, useId, useContext } from "react" + +import { MotionConfigContext } from "../../context/MotionConfigContext" interface Size { width: number @@ -55,6 +57,7 @@ export function PopChild({ children, isPresent }: Props) { top: 0, left: 0, }) + const { nonce } = useContext(MotionConfigContext) /** * We create and inject a style block so we can apply this explicit @@ -72,6 +75,7 @@ export function PopChild({ children, isPresent }: Props) { ref.current.dataset.motionPopId = id const style = document.createElement("style") + if (nonce) style.nonce = nonce document.head.appendChild(style) if (style.sheet) { style.sheet.insertRule(` diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index eb39373019..d8e699e48f 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -11,6 +11,7 @@ import { } from "../../.." import { motionValue } from "../../../value" import { ResolvedValues } from "../../../render/types" +import { nextFrame } from "../../../gestures/__tests__/utils" describe("AnimatePresence", () => { test("Allows initial animation if no `initial` prop defined", async () => { @@ -23,9 +24,13 @@ describe("AnimatePresence", () => { animate={{ x: 100 }} style={{ x }} exit={{ x: 0 }} - onAnimationStart={() => - frame.postRender(() => resolve(x.get())) - } + onAnimationStart={() => { + frame.postRender(() => { + frame.postRender(() => { + resolve(x.get()) + }) + }) + }} /> ) @@ -112,7 +117,7 @@ describe("AnimatePresence", () => { }) test("Allows nested exit animations", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(0) const Component = ({ isOpen }: any) => { return ( @@ -133,10 +138,16 @@ describe("AnimatePresence", () => { const { rerender } = render() rerender() + + await nextFrame() + expect(opacity.get()).toBe(0.9) rerender() rerender() - setTimeout(() => resolve(opacity.get()), 50) + + await nextFrame() + + resolve(opacity.get()) }) const opacity = await promise @@ -494,6 +505,8 @@ describe("AnimatePresence", () => { rerender() }) + await nextFrame() + expect(x.get()).toBe(200) }) @@ -503,7 +516,7 @@ describe("AnimatePresence", () => { exit: { opacity: 0, transition: { type: false } }, } - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const Component = ({ isVisible }: { isVisible: boolean }) => { return ( @@ -531,6 +544,8 @@ describe("AnimatePresence", () => { rerender() + await nextFrame() + resolve(opacity.get()) }) @@ -776,6 +791,8 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + expect(x.get()).toBe(200) }) @@ -845,10 +862,14 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + await act(async () => { rerender() }) + await nextFrame() + expect([xParent.get(), xChild.get()]).toEqual([200, 200]) }) @@ -887,11 +908,14 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + expect(opacity.get()).toBe(0) }) test("Sibling AnimatePresence wrapped in LayoutGroup remove exiting elements", async () => { - const opacity = motionValue(1) + const opacityA = motionValue(1) + const opacityB = motionValue(1) const Component = ({ isVisible }: { isVisible: boolean }) => { return ( @@ -902,7 +926,7 @@ describe("AnimatePresence with custom components", () => { data-testid="a" exit={{ opacity: 0 }} transition={{ type: false }} - style={{ opacity }} + style={{ opacity: opacityA }} /> )} @@ -912,7 +936,7 @@ describe("AnimatePresence with custom components", () => { data-testid="b" exit={{ opacity: 0 }} transition={{ type: false }} - style={{ opacity }} + style={{ opacity: opacityB }} /> )} @@ -934,8 +958,12 @@ describe("AnimatePresence with custom components", () => { rerender() }) + await nextFrame() + await new Promise((resolve) => { setTimeout(() => { + expect(opacityA.get()).toBe(0) + expect(opacityB.get()).toBe(0) expect(queryByTestId("a")).toBe(null) expect(queryByTestId("b")).toBe(null) resolve() diff --git a/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx b/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx index c199eb35e2..fd670a6429 100644 --- a/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx +++ b/packages/framer-motion/src/components/MotionConfig/__tests__/index.test.tsx @@ -3,6 +3,7 @@ import { motion } from "../../../render/dom/motion" import { MotionConfig } from "../" import * as React from "react" import { motionValue } from "../../../value" +import { nextFrame } from "../../../gestures/__tests__/utils" describe("custom properties", () => { test("renders", () => { @@ -48,7 +49,7 @@ describe("reducedMotion", () => { }) test("reducedMotion makes transforms animate instantly", async () => { - const result = await new Promise<[number, number]>((resolve) => { + const result = await new Promise<[number, number]>(async (resolve) => { const x = motionValue(0) const opacity = motionValue(0) const Component = () => { @@ -65,6 +66,9 @@ describe("reducedMotion", () => { const { rerender } = render() rerender() + + await nextFrame() + resolve([x.get(), opacity.get()]) }) diff --git a/packages/framer-motion/src/context/MotionConfigContext.tsx b/packages/framer-motion/src/context/MotionConfigContext.tsx index 1a9ede4431..21fa6ad4ab 100644 --- a/packages/framer-motion/src/context/MotionConfigContext.tsx +++ b/packages/framer-motion/src/context/MotionConfigContext.tsx @@ -33,6 +33,15 @@ export interface MotionConfigContext { * @public */ reducedMotion?: ReducedMotionConfig + + /** + * A custom `nonce` attribute used when wanting to enforce a Content Security Policy (CSP). + * For more details see: + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#unsafe_inline_styles + * + * @public + */ + nonce?: string } /** diff --git a/packages/framer-motion/src/frameloop/batcher.ts b/packages/framer-motion/src/frameloop/batcher.ts index 75e99aa5ef..07bdcc347f 100644 --- a/packages/framer-motion/src/frameloop/batcher.ts +++ b/packages/framer-motion/src/frameloop/batcher.ts @@ -3,12 +3,12 @@ import { createRenderStep } from "./render-step" import { Batcher, Process, StepId, Steps, FrameData } from "./types" export const stepsOrder: StepId[] = [ - "prepare", - "read", - "update", - "preRender", - "render", - "postRender", + "read", // Read + "resolveKeyframes", // Write/Read/Write/Read + "update", // Compute + "preRender", // Compute + "render", // Write + "postRender", // Compute ] const maxElapsed = 40 @@ -69,6 +69,7 @@ export function createRenderBatcher( const step = steps[key] acc[key] = (process: Process, keepAlive = false, immediate = false) => { if (!runNextFrame) wake() + return step.schedule(process, keepAlive, immediate) } return acc diff --git a/packages/framer-motion/src/frameloop/types.ts b/packages/framer-motion/src/frameloop/types.ts index 4781b4aa4c..cb3b075b14 100644 --- a/packages/framer-motion/src/frameloop/types.ts +++ b/packages/framer-motion/src/frameloop/types.ts @@ -13,8 +13,8 @@ export interface Step { } export type StepId = - | "prepare" | "read" + | "resolveKeyframes" | "update" | "preRender" | "render" diff --git a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx index 13069f22bc..2ad026aae3 100644 --- a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx @@ -1,10 +1,11 @@ import { focus, blur, render } from "../../../jest.setup" import * as React from "react" import { motion, motionValue } from "../../" +import { nextFrame } from "./utils" describe("focus", () => { test("whileFocus applied", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const ref = React.createRef() const Component = () => ( @@ -25,6 +26,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) @@ -60,7 +63,7 @@ describe("focus", () => { }) test("whileFocus applied if focus-visible selector throws unsupported", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const ref = React.createRef() const Component = () => ( @@ -87,6 +90,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) @@ -95,7 +100,7 @@ describe("focus", () => { test("whileFocus applied as variant", async () => { const target = 0.5 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: target }, } @@ -120,6 +125,8 @@ describe("focus", () => { focus(container, "myAnchorElement") + await nextFrame() + resolve(opacity.get()) }) @@ -127,7 +134,7 @@ describe("focus", () => { }) test("whileFocus is unapplied when blur", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const ref = React.createRef() const variant = { hidden: { opacity: 0.5, transitionEnd: { opacity: 0.75 } }, @@ -157,6 +164,7 @@ describe("focus", () => { ref.current!.matches = () => true focus(container, "myAnchorElement") + await nextFrame() setTimeout(() => { blurred = true blur(container, "myAnchorElement") diff --git a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx index 7d5a398428..ec777408d6 100644 --- a/packages/framer-motion/src/gestures/__tests__/hover.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/hover.test.tsx @@ -8,6 +8,7 @@ import * as React from "react" import { motion } from "../../" import { motionValue } from "../../value" import { frame } from "../../frameloop" +import { nextFrame } from "./utils" describe("hover", () => { test("hover event listeners fire", async () => { @@ -51,7 +52,7 @@ describe("hover", () => { }) test("whileHover applied", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacity = motionValue(1) const Component = () => ( { pointerEnter(container.firstChild as Element) + await nextFrame() + resolve(opacity.get()) }) @@ -74,7 +77,7 @@ describe("hover", () => { test("whileHover applied as variant", async () => { const target = 0.5 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: target }, } @@ -93,6 +96,8 @@ describe("hover", () => { pointerEnter(container.firstChild as Element) + await nextFrame() + resolve(opacity.get()) }) @@ -101,7 +106,7 @@ describe("hover", () => { test("whileHover propagates to children", async () => { const target = 0.2 - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const parent = { hidden: { opacity: 0.8 }, } @@ -128,6 +133,8 @@ describe("hover", () => { const { container } = render() pointerEnter(container.firstChild as Element) + + await nextFrame() resolve(opacity.get()) }) @@ -135,7 +142,7 @@ describe("hover", () => { }) test("whileHover is unapplied when hover ends", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hidden: { opacity: 0.5, transitionEnd: { opacity: 0.75 } }, } @@ -159,6 +166,8 @@ describe("hover", () => { ) pointerEnter(container.firstChild as Element) + + await nextFrame() setTimeout(() => { hasMousedOut = true pointerLeave(container.firstChild as Element) @@ -169,7 +178,7 @@ describe("hover", () => { }) test("whileHover only animates values that arent being controlled by a higher-priority gesture ", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const variant = { hovering: { opacity: 0.5, scale: 0.5 }, tapping: { scale: 2 }, @@ -189,9 +198,14 @@ describe("hover", () => { const { container, rerender } = render() rerender() + await nextFrame() pointerDown(container.firstChild as Element) + + await nextFrame() pointerEnter(container.firstChild as Element) + await nextFrame() + resolve([opacity.get(), scale.get()]) }) diff --git a/packages/framer-motion/src/gestures/__tests__/press.test.tsx b/packages/framer-motion/src/gestures/__tests__/press.test.tsx index 77ebe730b2..81adba85e9 100644 --- a/packages/framer-motion/src/gestures/__tests__/press.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/press.test.tsx @@ -341,7 +341,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -355,16 +355,18 @@ describe("press", () => { ) const { container } = render() - + await nextFrame() logOpacity() // 0.5 // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -374,7 +376,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies via keyboard", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -389,14 +391,17 @@ describe("press", () => { const { container } = render() + await nextFrame() logOpacity() // 0.5 fireEvent.focus(container.firstChild as Element) fireEvent.keyDown(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 1 fireEvent.keyUp(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -406,7 +411,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies via blur cancel", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -421,14 +426,17 @@ describe("press", () => { const { container } = render() + await nextFrame() logOpacity() // 0.5 fireEvent.focus(container.firstChild as Element) fireEvent.keyDown(container.firstChild as Element, enterKey) + await nextFrame() logOpacity() // 1 fireEvent.blur(container.firstChild as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) @@ -438,7 +446,7 @@ describe("press", () => { }) test("press gesture variant unapplies children", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -455,15 +463,17 @@ describe("press", () => { const { getByTestId } = render() + await nextFrame() logOpacity() // 0.5 // Trigger mouse down pointerDown(getByTestId("child") as Element) - + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(getByTestId("child") as Element) + await nextFrame() logOpacity() // 0.5 resolve(opacityHistory) }) @@ -472,7 +482,7 @@ describe("press", () => { }) test("press gesture on children returns to parent-defined variant", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -494,15 +504,18 @@ describe("press", () => { const { rerender, getByTestId } = render() rerender() + await nextFrame() logOpacity() // 1 // Trigger mouse down pointerDown(getByTestId("child") as Element) + await nextFrame() logOpacity() // 0.5 // Trigger mouse up pointerUp(getByTestId("child") as Element) + await nextFrame() logOpacity() // 1 resolve(opacityHistory) }) @@ -556,7 +569,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies with whileHover", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -573,38 +586,47 @@ describe("press", () => { const { container, rerender } = render() rerender() + await nextFrame() logOpacity() // 0.5 // Trigger hover pointerEnter(container.firstChild as Element) + await nextFrame() logOpacity() // 0.75 // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() // 0.75 // Trigger hover end pointerLeave(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger hover pointerEnter(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger mouse down pointerDown(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger hover end pointerLeave(container.firstChild as Element) + await nextFrame() logOpacity() // Trigger mouse up pointerUp(container.firstChild as Element) + await nextFrame() logOpacity() resolve(opacityHistory) @@ -616,7 +638,7 @@ describe("press", () => { }) test("press gesture variant applies and unapplies as state changes", () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const opacityHistory: number[] = [] const opacity = motionValue(0.5) const logOpacity = () => opacityHistory.push(opacity.get()) @@ -638,40 +660,58 @@ describe("press", () => { ) rerender() + await nextFrame() + logOpacity() // 0.5 // Trigger hover pointerEnter(container.firstChild as Element) + + await nextFrame() logOpacity() // 0.75 // Trigger mouse down pointerDown(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 rerender() rerender() // Trigger mouse up pointerUp(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover end pointerLeave(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover pointerEnter(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger mouse down pointerDown(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger hover end pointerLeave(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 // Trigger mouse up pointerUp(container.firstChild as Element) + + await nextFrame() logOpacity() // 1 resolve(opacityHistory) diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index b0f69416d0..66d8c9e8ca 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -31,7 +31,6 @@ import { calcLength } from "../../projection/geometry/delta-calc" import { mixNumber } from "../../utils/mix/number" import { percent } from "../../value/types/numbers/units" import { animateMotionValue } from "../../animation/interfaces/motion-value" -import { frame } from "../../frameloop" import { getContextWindow } from "../../utils/get-context-window" export const elementDragControls = new WeakMap< @@ -153,9 +152,7 @@ export class VisualElementDragControls { }) // Fire onDragStart event - if (onDragStart) { - frame.update(() => onDragStart(event, info), false, true) - } + if (onDragStart) onDragStart(event, info) const { animationState } = this.visualElement animationState && animationState.setActive("whileDrag", true) @@ -243,9 +240,7 @@ export class VisualElementDragControls { this.startAnimation(velocity) const { onDragEnd } = this.getProps() - if (onDragEnd) { - frame.update(() => onDragEnd(event, info)) - } + if (onDragEnd) onDragEnd(event, info) } private cancel() { @@ -441,7 +436,13 @@ export class VisualElementDragControls { ) { const axisValue = this.getAxisMotionValue(axis) return axisValue.start( - animateMotionValue(axis, axisValue, 0, transition) + animateMotionValue( + axis, + axisValue, + 0, + transition, + this.visualElement + ) ) } diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx index f5826f416c..751a1d8192 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx @@ -496,6 +496,8 @@ describe("dragging", () => { expect(opacity.get()).toBe(0.5) await pointer.to(10, 200) pointer.end() + + await nextFrame() expect(opacity.get()).toBe(0) }) diff --git a/packages/framer-motion/src/gestures/hover.ts b/packages/framer-motion/src/gestures/hover.ts index d2a5f5aa17..4d153f9193 100644 --- a/packages/framer-motion/src/gestures/hover.ts +++ b/packages/framer-motion/src/gestures/hover.ts @@ -4,7 +4,6 @@ import { isDragActive } from "./drag/utils/lock" import { EventInfo } from "../events/types" import type { VisualElement } from "../render/VisualElement" import { Feature } from "../motion/features/Feature" -import { frame } from "../frameloop" function addHoverEvent(node: VisualElement, isActive: boolean) { const eventName = "pointer" + (isActive ? "enter" : "leave") @@ -20,7 +19,7 @@ function addHoverEvent(node: VisualElement, isActive: boolean) { } if (props[callbackName]) { - frame.update(() => props[callbackName](event, info)) + props[callbackName](event, info) } } diff --git a/packages/framer-motion/src/gestures/pan/index.ts b/packages/framer-motion/src/gestures/pan/index.ts index 68e79dd904..8a75c563bf 100644 --- a/packages/framer-motion/src/gestures/pan/index.ts +++ b/packages/framer-motion/src/gestures/pan/index.ts @@ -2,14 +2,13 @@ import { PanInfo, PanSession } from "./PanSession" import { addPointerEvent } from "../../events/add-pointer-event" import { Feature } from "../../motion/features/Feature" import { noop } from "../../utils/noop" -import { frame } from "../../frameloop" import { getContextWindow } from "../../utils/get-context-window" type PanEventHandler = (event: PointerEvent, info: PanInfo) => void const asyncHandler = (handler?: PanEventHandler) => (event: PointerEvent, info: PanInfo) => { if (handler) { - frame.update(() => handler(event, info)) + handler(event, info) } } @@ -39,9 +38,7 @@ export class PanGesture extends Feature { onMove: onPan, onEnd: (event: PointerEvent, info: PanInfo) => { delete this.session - if (onPanEnd) { - frame.update(() => onPanEnd(event, info)) - } + if (onPanEnd) onPanEnd(event, info) }, } } diff --git a/packages/framer-motion/src/gestures/press.ts b/packages/framer-motion/src/gestures/press.ts index 5561fb0743..f0e86b9276 100644 --- a/packages/framer-motion/src/gestures/press.ts +++ b/packages/framer-motion/src/gestures/press.ts @@ -10,7 +10,6 @@ import { pipe } from "../utils/pipe" import { isDragActive } from "./drag/utils/lock" import { isNodeOrChild } from "./utils/is-node-or-child" import { noop } from "../utils/noop" -import { frame } from "../frameloop" function fireSyntheticPointerEvent( name: string, @@ -41,7 +40,7 @@ export class PressGesture extends Feature { } if (onTapStart) { - frame.update(() => onTapStart(event, info)) + onTapStart(event, info) } } @@ -76,16 +75,14 @@ export class PressGesture extends Feature { const { onTap, onTapCancel, globalTapTarget } = this.node.getProps() - frame.update(() => { - /** - * We only count this as a tap gesture if the event.target is the same - * as, or a child of, this component's element - */ - !globalTapTarget && - !isNodeOrChild(this.node.current, endEvent.target as Element) - ? onTapCancel && onTapCancel(endEvent, endInfo) - : onTap && onTap(endEvent, endInfo) - }) + /** + * We only count this as a tap gesture if the event.target is the same + * as, or a child of, this component's element + */ + !globalTapTarget && + !isNodeOrChild(this.node.current, endEvent.target as Element) + ? onTapCancel && onTapCancel(endEvent, endInfo) + : onTap && onTap(endEvent, endInfo) } const removePointerUpListener = addPointerEvent( @@ -115,9 +112,7 @@ export class PressGesture extends Feature { if (!this.checkPressEnd()) return const { onTapCancel } = this.node.getProps() - if (onTapCancel) { - frame.update(() => onTapCancel(event, info)) - } + if (onTapCancel) onTapCancel(event, info) } private startAccessiblePress = () => { @@ -129,9 +124,7 @@ export class PressGesture extends Feature { fireSyntheticPointerEvent("up", (event, info) => { const { onTap } = this.node.getProps() - if (onTap) { - frame.update(() => onTap(event, info)) - } + if (onTap) onTap(event, info) }) } diff --git a/packages/framer-motion/src/index.ts b/packages/framer-motion/src/index.ts index f5045810a4..291ee5ae93 100644 --- a/packages/framer-motion/src/index.ts +++ b/packages/framer-motion/src/index.ts @@ -86,7 +86,7 @@ export { useInstantLayoutTransition } from "./projection/use-instant-layout-tran export { useResetProjection } from "./projection/use-reset-projection" export { buildTransform } from "./render/html/utils/build-transform" export { visualElementStore } from "./render/store" -export { animateValue } from "./animation/animators/js" +export { animateValue } from "./animation/animators/MainThreadAnimation" export { color } from "./value/types/color" export { complex } from "./value/types/complex" export { px } from "./value/types/numbers/units" diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index 47db2497bd..1f00b1d8b2 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -8,7 +8,7 @@ import { } from "../../" import * as React from "react" import { createRef } from "react" -import { nextFrame, nextMicrotask } from "../../gestures/__tests__/utils" +import { nextFrame } from "../../gestures/__tests__/utils" describe("animate prop as object", () => { test("animates to set prop", async () => { @@ -101,7 +101,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(true) }) test("uses transitionEnd on subsequent renders", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const x = motionValue(0) const Component = ({ animate }: any) => ( @@ -133,7 +133,11 @@ describe("animate prop as object", () => { }} /> ) - requestAnimationFrame(() => resolve(x.get())) + + await nextFrame() + await nextFrame() + + resolve(x.get()) }) return expect(promise).resolves.toBe(300) }) @@ -262,7 +266,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toHaveStyle("font-weight: 100") }) test("doesn't animate no-op values", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(false) }) test("doesn't animate no-op keyframes", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(false) }) - test("doe animate different keyframes", async () => { - const promise = new Promise((resolve) => { + test("does animate different keyframes", async () => { + const promise = new Promise(async (resolve) => { let isAnimating = false const Component = () => ( { const { rerender } = render() rerender() - frame.postRender(() => { - frame.postRender(() => resolve(isAnimating)) - }) + await nextFrame() + await nextFrame() + + resolve(isAnimating) }) return expect(promise).resolves.toBe(true) @@ -377,13 +384,13 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(true) }) test("doesn't animate zIndex", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const Component = () => const { container, rerender } = render() rerender() - requestAnimationFrame(() => - resolve(container.firstChild as Element) - ) + + await nextFrame() + resolve(container.firstChild as Element) }) return expect(promise).resolves.toHaveStyle("z-index: 100") }) @@ -410,7 +417,7 @@ describe("animate prop as object", () => { rerender() rerender() - await nextMicrotask() + await nextFrame() expect(ref.current).toHaveStyle("opacity: 0") @@ -452,7 +459,7 @@ describe("animate prop as object", () => { /> ) - await nextMicrotask() + await nextFrame() expect(ref.current).toHaveStyle("opacity: 0.5") @@ -762,7 +769,7 @@ describe("animate prop as object", () => { return expect(promise).resolves.toBe(20) }) - test("animates previously unseen properties", async () => { + test("animates previously unseen properties, instant animation", async () => { const Component = ({ animate }: any) => ( ) @@ -770,10 +777,31 @@ describe("animate prop as object", () => { ) rerender() + + rerender() + rerender() + + await nextFrame() + + return expect(container.firstChild as Element).toHaveStyle( + "transform: translateX(0px) translateY(100px) translateZ(0)" + ) + }) + + test("animates previously unseen properties", async () => { + const Component = ({ animate }: any) => ( + + ) + const { container, rerender } = render( + + ) + rerender() + rerender() rerender() - await nextMicrotask() + await nextFrame() + await nextFrame() return expect(container.firstChild as Element).toHaveStyle( "transform: translateX(0px) translateY(100px) translateZ(0)" @@ -801,11 +829,17 @@ describe("animate prop as object", () => { test("animates previously unseen CSS variables", async () => { const promise = new Promise((resolve) => { + let latestColor = "" const Component = () => ( resolve(latest["--foo"] as string)} + onUpdate={(latest) => { + latestColor = latest["--foo"] as string + }} + onAnimationComplete={() => { + resolve(latestColor) + }} transition={{ type: false }} /> ) @@ -817,7 +851,7 @@ describe("animate prop as object", () => { }) test("forces an animation to fallback if has been set to `null`", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const complete = () => resolve(true) const Component = ({ animate, onAnimationComplete }: any) => ( { const { container, rerender } = render( ) + await nextFrame() rerender() rerender() + await nextFrame() expect(container.firstChild as Element).toHaveStyle( "transform: none" ) @@ -941,9 +977,9 @@ describe("animate prop as object", () => { ref={ref} initial={{ backgroundColor: "#0088ff" }} animate={{ backgroundColor: "hsl(345, 100%, 60%)" }} - onAnimationComplete={() => + onAnimationComplete={() => { ref.current && resolve(ref.current) - } + }} transition={{ duration: 0.01 }} /> ) diff --git a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx index 7e6b975fde..24fbff1734 100644 --- a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx @@ -1,7 +1,7 @@ import { render } from "../../../jest.setup" import { motion, motionValue, useMotionValue, useTransform } from "../../" import * as React from "react" -import { nextMicrotask } from "../../gestures/__tests__/utils" +import { nextFrame } from "../../gestures/__tests__/utils" describe("SVG", () => { test("doesn't add translateZ", () => { @@ -24,7 +24,7 @@ describe("SVG", () => { render() }) - test("recognises MotionValues in attributes", () => { + test("recognises MotionValues in attributes", async () => { let r = motionValue(0) let fill = motionValue("#000") @@ -49,6 +49,8 @@ describe("SVG", () => { const { rerender } = render() rerender() + await nextFrame() + expect(r.get()).toBe(100) expect(fill.get()).toBe("rgba(255, 0, 0, 1)") }) @@ -65,11 +67,14 @@ describe("SVG", () => { render() }) - test("doesn't calculate transformOrigin for elements", () => { + test("doesn't calculate transformOrigin for elements", async () => { const Component = () => { return } const { container } = render() + + await nextFrame() + expect(container.firstChild as Element).not.toHaveStyle( "transform-origin: 0px 0px" ) @@ -93,7 +98,7 @@ describe("SVG", () => { render() }) - test("doesn't read viewBox as '0 0 0 0'", () => { + test("doesn't read viewBox as '0 0 0 0'", async () => { const Component = () => { return ( { ) } const { container } = render() + + await nextFrame() + expect(container.firstChild as Element).toHaveAttribute( "viewBox", "0 0 100 100" @@ -121,7 +129,9 @@ describe("SVG", () => { ) } const { container } = render() - await nextMicrotask() + + await nextFrame() + expect(container.firstChild as Element).toHaveAttribute( "viewBox", "100 100 200 200" diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx index 78fcc1b925..58d2b379bf 100644 --- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx @@ -508,7 +508,7 @@ describe("animate prop as variant", () => { }) test("nested controlled variants switch correctly", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise(async (resolve) => { const parentOpacity = motionValue(0.2) const childOpacity = motionValue(0.1) @@ -539,16 +539,17 @@ describe("animate prop as variant", () => { } const { rerender } = render() - setTimeout(() => { - expect(parentOpacity.get()).toBe(0.4) - expect(childOpacity.get()).toBe(0.6) - rerender() + await nextFrame() + + expect(parentOpacity.get()).toBe(0.4) + expect(childOpacity.get()).toBe(0.6) + + rerender() - setTimeout(() => { - resolve([parentOpacity.get(), childOpacity.get()]) - }, 0) - }, 0) + await nextFrame() + + resolve([parentOpacity.get(), childOpacity.get()]) }) return expect(promise).resolves.toEqual([0.3, 0.5]) @@ -847,7 +848,7 @@ describe("animate prop as variant", () => { }).not.toThrowError() }) - test("new child items animate from initial to animate", () => { + test("new child items animate from initial to animate", async () => { const x = motionValue(0) const Component = ({ length }: { length: number }) => { const variants: Variants = { @@ -878,6 +879,8 @@ describe("animate prop as variant", () => { rerender() rerender() + await nextFrame() + expect(x.get()).toBe(100) }) @@ -1026,6 +1029,7 @@ describe("animate prop as variant", () => { rerender() await nextFrame() + expect(element).toHaveStyle("transform: none") }) diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index 53b099ff8c..c00f3fb7a9 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -10,9 +10,10 @@ import * as React from "react" import { createRef } from "react" import { nextFrame } from "../../gestures/__tests__/utils" import "../../animation/animators/waapi/__tests__/setup" +import { act } from "react-dom/test-utils" describe("WAAPI animations", () => { - test("opacity animates with WAAPI at default settings", () => { + test("opacity animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -38,7 +41,7 @@ describe("WAAPI animations", () => { ) }) - test("filter animates with WAAPI at default settings", () => { + test("filter animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -67,7 +72,7 @@ describe("WAAPI animations", () => { ) }) - test("clipPath animates with WAAPI at default settings", () => { + test("clipPath animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -96,7 +103,7 @@ describe("WAAPI animations", () => { ) }) - test("Complex string type animates with WAAPI spring", () => { + test("Complex string type animates with WAAPI spring", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -126,7 +135,7 @@ describe("WAAPI animations", () => { ) }) - test("transform animates with WAAPI at default settings", () => { + test("transform animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -155,7 +166,8 @@ describe("WAAPI animations", () => { ) }) - test.skip("backgroundColor animates with WAAPI at default settings", () => { + // backgroundColor currently disabled for performance reasons + test.skip("backgroundColor animates with WAAPI at default settings", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -216,7 +230,7 @@ describe("WAAPI animations", () => { ) }) - test("opacity animates with WAAPI when no value is originally provided via initial", () => { + test("opacity animates with WAAPI when no value is originally provided via initial", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) - test("opacity animates with WAAPI at default settings with no initial value set", () => { + test("opacity animates with WAAPI at default settings with no initial value set", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) - test("opacity animates with WAAPI at default settings when layout is enabled", () => { + test("opacity animates with WAAPI at default settings when layout is enabled", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() }) @@ -272,13 +292,14 @@ describe("WAAPI animations", () => { setIsHovered(true)} - onHoverEnd={() => setIsHovered(false)} + onHoverStart={() => act(() => setIsHovered(true))} + onHoverEnd={() => act(() => setIsHovered(false))} > ) @@ -286,10 +307,12 @@ describe("WAAPI animations", () => { const { container, rerender } = render() pointerEnter(container.firstChild as Element) + await nextFrame() await nextFrame() pointerLeave(container.firstChild as Element) await nextFrame() rerender() + await nextFrame() expect(ref.current!.animate).toBeCalledTimes(2) }) @@ -317,6 +340,7 @@ describe("WAAPI animations", () => { const { container, rerender } = render() pointerDown(container.firstChild as Element) + await nextFrame() await nextFrame() pointerUp(container.firstChild as Element) @@ -324,10 +348,12 @@ describe("WAAPI animations", () => { rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalledTimes(2) }) - test("WAAPI is called with expected arguments", () => { + test("WAAPI is called with expected arguments", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: [0.2, 0.9] }, @@ -361,7 +389,7 @@ describe("WAAPI animations", () => { ) }) - test("WAAPI is called with expected arguments with pre-generated keyframes", () => { + test("WAAPI is called with expected arguments with pre-generated keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], offset: undefined }, @@ -393,7 +423,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeIn' to 'ease-in'", () => { + test("Maps 'easeIn' to 'ease-in'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -422,7 +454,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeOut' to 'ease-out'", () => { + test("Maps 'easeOut' to 'ease-out'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -451,7 +485,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'easeInOut' to 'ease-in-out'", () => { + test("Maps 'easeInOut' to 'ease-in-out'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -480,7 +516,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'circIn' to 'cubic-bezier(0, 0.65, 0.55, 1)'", () => { + test("Maps 'circIn' to 'cubic-bezier(0, 0.65, 0.55, 1)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -509,7 +547,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'circOut' to 'cubic-bezier(0.55, 0, 1, 0.45)'", () => { + test("Maps 'circOut' to 'cubic-bezier(0.55, 0, 1, 0.45)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -538,7 +578,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'backIn' to 'cubic-bezier(0.31, 0.01, 0.66, -0.59)'", () => { + test("Maps 'backIn' to 'cubic-bezier(0.31, 0.01, 0.66, -0.59)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -567,7 +608,7 @@ describe("WAAPI animations", () => { ) }) - test("Maps 'backOut' to 'cubic-bezier(0.33, 1.53, 0.69, 0.99)'", () => { + test("Maps 'backOut' to 'cubic-bezier(0.33, 1.53, 0.69, 0.99)'", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, @@ -596,7 +639,7 @@ describe("WAAPI animations", () => { ) }) - test("WAAPI is called with pre-generated spring keyframes", () => { + test("WAAPI is called with pre-generated spring keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -638,7 +683,7 @@ describe("WAAPI animations", () => { /** * TODO: We could not accelerate but scrub WAAPI animation if repeatDelay is defined */ - test("Doesn't animate with WAAPI if repeatDelay is defined", () => { + test("Doesn't animate with WAAPI if repeatDelay is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Pregenerates keyframes if ease is function", () => { + test("Pregenerates keyframes if ease is function", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -684,7 +733,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is anticipate", () => { + test("Pregenerates keyframes if ease is anticipate", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -717,7 +768,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is backInOut", () => { + test("Pregenerates keyframes if ease is backInOut", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -750,7 +803,7 @@ describe("WAAPI animations", () => { ) }) - test("Pregenerates keyframes if ease is circInOut", () => { + test("Pregenerates keyframes if ease is circInOut", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -783,7 +838,7 @@ describe("WAAPI animations", () => { ) }) - test("Doesn't animate with WAAPI if repeatType is defined as mirror", () => { + test("Doesn't animate with WAAPI if repeatType is defined as mirror", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() expect(ref.current!.animate).not.toBeCalled() }) - test("Doesn't animate with WAAPI if onUpdate is defined", () => { + test("Doesn't animate with WAAPI if onUpdate is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Doesn't animate with WAAPI if external motion value is defined", () => { + test("Doesn't animate with WAAPI if external motion value is defined", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).not.toBeCalled() }) - test("Animates with WAAPI if repeat is defined and we need to generate keyframes", () => { + test("Animates with WAAPI if repeat is defined and we need to generate keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { @@ -868,7 +930,7 @@ describe("WAAPI animations", () => { ) }) - test("Animates with WAAPI if repeat is Infinity and we need to generate keyframes", () => { + test("Animates with WAAPI if repeat is Infinity and we need to generate keyframes", async () => { const ref = createRef() const Component = () => ( { const { rerender } = render() rerender() + await nextFrame() + expect(ref.current!.animate).toBeCalled() expect(ref.current!.animate).toBeCalledWith( { diff --git a/packages/framer-motion/src/motion/features/animation/exit.ts b/packages/framer-motion/src/motion/features/animation/exit.ts index 12c99e340a..b7970ce5c8 100644 --- a/packages/framer-motion/src/motion/features/animation/exit.ts +++ b/packages/framer-motion/src/motion/features/animation/exit.ts @@ -8,7 +8,7 @@ export class ExitAnimationFeature extends Feature { update() { if (!this.node.presenceContext) return - const { isPresent, onExitComplete, custom } = this.node.presenceContext + const { isPresent, onExitComplete } = this.node.presenceContext const { isPresent: prevIsPresent } = this.node.prevPresenceContext || {} if (!this.node.animationState || isPresent === prevIsPresent) { @@ -17,8 +17,7 @@ export class ExitAnimationFeature extends Feature { const exitAnimation = this.node.animationState.setActive( "exit", - !isPresent, - { custom: custom ?? this.node.getProps().custom } + !isPresent ) if (onExitComplete && !isPresent) { diff --git a/packages/framer-motion/src/projection/index.ts b/packages/framer-motion/src/projection/index.ts index 807dd6db5b..46640c2b56 100644 --- a/packages/framer-motion/src/projection/index.ts +++ b/packages/framer-motion/src/projection/index.ts @@ -7,7 +7,7 @@ export { calcBoxDelta } from "./geometry/delta-calc" */ import { frame, frameData } from "../frameloop" import { mix } from "../utils/mix" -import { animateValue } from "../animation/animators/js" +import { animateValue } from "../animation/animators/MainThreadAnimation" export { frame, animateValue as animate, mix, frameData } export { buildTransform } from "../render/html/utils/build-transform" export { addScaleCorrector } from "./styles/scale-correction" diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 34051371dd..838d82186c 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -10,7 +10,6 @@ import { MotionProps, MotionStyle } from "../motion/types" import { createBox } from "../projection/geometry/models" import { Box } from "../projection/geometry/types" import { IProjectionNode } from "../projection/node/types" -import { TargetAndTransition } from "../types" import { isRefObject } from "../utils/is-ref-object" import { initPrefersReducedMotion } from "../utils/reduced-motion" import { @@ -42,6 +41,16 @@ import { Feature } from "../motion/features/Feature" import type { PresenceContextProps } from "../context/PresenceContext" import { variantProps } from "./utils/variant-props" import { visualElementStore } from "./store" +import { + KeyframeResolver, + ResolvedKeyframes, + UnresolvedKeyframes, +} from "./utils/KeyframesResolver" +import { isNumericalString } from "../utils/is-numerical-string" +import { isZeroValueString } from "../utils/is-zero-value-string" +import { findValueType } from "./dom/value-types/find" +import { complex } from "../value/types/complex" +import { getAnimatableNone } from "./dom/value-types/animatable-none" const featureNames = Object.keys(featureDefinitions) const numFeatures = featureNames.length @@ -80,15 +89,6 @@ export abstract class VisualElement< */ abstract sortInstanceNodePosition(a: Instance, b: Instance): number - /** - * Take a target and make it animatable. For instance if provided - * height: "auto" we need to measure height in pixels and animate that instead. - */ - abstract makeTargetAnimatableFromInstance( - target: TargetAndTransition, - isLive: boolean - ): TargetAndTransition - /** * Measure the viewport-relative bounding box of the Instance. */ @@ -151,6 +151,24 @@ export abstract class VisualElement< projection?: IProjectionNode ): void + resolveKeyframes = ( + keyframes: UnresolvedKeyframes, + // We use an onComplete callback here rather than a Promise as a Promise + // resolution is a microtask and we want to retain the ability to force + // the resolution of keyframes synchronously. + onComplete: (resolvedKeyframes: ResolvedKeyframes) => void, + name: string, + value: MotionValue + ): KeyframeResolver => { + return new this.KeyframeResolver( + keyframes, + onComplete, + name, + value, + this + ) + } + /** * If the component child is provided as a motion value, handle subscriptions * with the renderer-specific VisualElement. @@ -260,6 +278,8 @@ export abstract class VisualElement< */ animationState?: AnimationState + KeyframeResolver = KeyframeResolver + /** * The options used to create this VisualElement. The Options type is defined * by the inheriting VisualElement and is passed straight through to the render functions. @@ -339,6 +359,7 @@ export abstract class VisualElement< props, presenceContext, reducedMotionConfig, + blockInitialAnimation, visualState, }: VisualElementOptions, options: Options = {} as any @@ -354,6 +375,7 @@ export abstract class VisualElement< this.depth = parent ? parent.depth + 1 : 0 this.reducedMotionConfig = reducedMotionConfig this.options = options + this.blockInitialAnimation = Boolean(blockInitialAnimation) this.isControllingVariants = checkIsControllingVariants(props) this.isVariantNode = checkIsVariantNode(props) @@ -450,8 +472,8 @@ export abstract class VisualElement< "change", (latestValue: string | number) => { this.latestValues[key] = latestValue - this.props.onUpdate && - frame.update(this.notifyUpdate, false, true) + + this.props.onUpdate && frame.preRender(this.notifyUpdate) if (valueIsTransform && this.projection) { this.projection.isTransformDirty = true @@ -633,20 +655,6 @@ export abstract class VisualElement< this.latestValues[key] = value } - /** - * Make a target animatable by Popmotion. For instance, if we're - * trying to animate width from 100px to 100vw we need to measure 100vw - * in pixels to determine what we really need to animate to. This is also - * pluggable to support Framer's custom value types like Color, - * and CSS variables. - */ - makeTargetAnimatable( - target: TargetAndTransition, - canMutate = true - ): TargetAndTransition { - return this.makeTargetAnimatableFromInstance(target, canMutate) - } - /** * Update the provided props. Ensure any newly-added motion values are * added to our map, old ones removed, and listeners updated. @@ -799,10 +807,10 @@ export abstract class VisualElement< * value, we'll create one if none exists. */ getValue(key: string): MotionValue | undefined - getValue(key: string, defaultValue: string | number): MotionValue + getValue(key: string, defaultValue: string | number | null): MotionValue getValue( key: string, - defaultValue?: string | number + defaultValue?: string | number | null ): MotionValue | undefined { if (this.props.values && this.props.values[key]) { return this.props.values[key] @@ -811,7 +819,10 @@ export abstract class VisualElement< let value = this.values.get(key) if (value === undefined && defaultValue !== undefined) { - value = motionValue(defaultValue, { owner: this }) + value = motionValue( + defaultValue === null ? undefined : defaultValue, + { owner: this } + ) this.addValue(key, value) } @@ -823,11 +834,28 @@ export abstract class VisualElement< * we need to check for it in our state and as a last resort read it * directly from the instance (which might have performance implications). */ - readValue(key: string) { - return this.latestValues[key] !== undefined || !this.current - ? this.latestValues[key] - : this.getBaseTargetFromProps(this.props, key) ?? + readValue(key: string, target?: string | number | null) { + let value = + this.latestValues[key] !== undefined || !this.current + ? this.latestValues[key] + : this.getBaseTargetFromProps(this.props, key) ?? this.readValueFromInstance(this.current, key, this.options) + + if (value !== undefined && value !== null) { + if ( + typeof value === "string" && + (isNumericalString(value) || isZeroValueString(value)) + ) { + // If this is a number read as a string, ie "0" or "200", convert it to a number + value = parseFloat(value) + } else if (!findValueType(value) && complex.test(target)) { + value = getAnimatableNone(key, target as string) + } + + this.setBaseTarget(key, isMotionValue(value) ? value.get() : value) + } + + return isMotionValue(value) ? value.get() : value } /** @@ -846,7 +874,11 @@ export abstract class VisualElement< const { initial } = this.props const valueFromInitial = typeof initial === "string" || typeof initial === "object" - ? resolveVariantFromProps(this.props, initial as any)?.[key] + ? resolveVariantFromProps( + this.props, + initial as any, + this.presenceContext?.custom + )?.[key] : undefined /** diff --git a/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts new file mode 100644 index 0000000000..0f37cfd9f6 --- /dev/null +++ b/packages/framer-motion/src/render/dom/DOMKeyframesResolver.ts @@ -0,0 +1,183 @@ +import { isNone } from "../../animation/utils/is-none" +import { getVariableValue } from "./utils/css-variables-conversion" +import { isCSSVariableToken } from "./utils/is-css-variable" +import { + isNumOrPxType, + positionalKeys, + positionalValues, + removeNonTranslationalTransform, +} from "./utils/unit-conversion" +import { findDimensionValueType } from "./value-types/dimensions" +import { + KeyframeResolver, + OnKeyframesResolved, + UnresolvedKeyframes, +} from "../utils/KeyframesResolver" +import { makeNoneKeyframesAnimatable } from "../html/utils/make-none-animatable" +import { VisualElement } from "../VisualElement" +import { MotionValue } from "../../value" + +export class DOMKeyframesResolver< + T extends string | number +> extends KeyframeResolver { + name: string + protected element: VisualElement + + private removedTransforms?: [string, string | number][] + private measuredOrigin?: string | number + private suspendedScrollY?: number + + constructor( + unresolvedKeyframes: UnresolvedKeyframes, + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: MotionValue + ) { + super( + unresolvedKeyframes, + onComplete, + name, + motionValue, + motionValue?.owner as VisualElement, + true + ) + } + + readKeyframes() { + const { unresolvedKeyframes, element, name } = this + + if (!element.current) return + + const noneKeyframeIndexes: number[] = [] + + super.readKeyframes() + + /** + * If any keyframe is a CSS variable, we need to find its value by sampling the element + */ + for (let i = 0; i < unresolvedKeyframes.length; i++) { + const keyframe = unresolvedKeyframes[i] + if (typeof keyframe === "string" && isCSSVariableToken(keyframe)) { + const resolved = getVariableValue(keyframe, element.current) + + if (resolved !== undefined) { + unresolvedKeyframes[i] = resolved as T + } + } + + if (isNone(unresolvedKeyframes[i])) { + noneKeyframeIndexes.push(i) + } + } + + if (noneKeyframeIndexes.length) { + makeNoneKeyframesAnimatable( + unresolvedKeyframes, + noneKeyframeIndexes, + name + ) + } + + /** + * Check to see if unit type has changed. If so schedule jobs that will + * temporarily set styles to the destination keyframes. + * Skip if we have more than two keyframes or this isn't a positional value. + * TODO: We can throw if there are multiple keyframes and the value type changes. + */ + if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) { + return + } + + const [origin, target] = unresolvedKeyframes + const originType = findDimensionValueType(origin) + const targetType = findDimensionValueType(target) + + /** + * Either we don't recognise these value types or we can animate between them. + */ + if (!originType || !targetType || originType === targetType) return + + /** + * If both values are numbers or pixels, we can animate between them by + * converting them to numbers. + */ + if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { + for (let i = 0; i < unresolvedKeyframes.length; i++) { + const value = unresolvedKeyframes[i] + if (typeof value === "string") { + unresolvedKeyframes[i] = parseFloat(value as string) + } + } + } else { + /** + * Else, the only way to resolve this is by measuring the element. + */ + this.needsMeasurement = true + } + } + + unsetTransforms() { + const { element, name, unresolvedKeyframes } = this + + if (!element.current) return + + this.removedTransforms = removeNonTranslationalTransform(element) + + const finalKeyframe = + unresolvedKeyframes[unresolvedKeyframes.length - 1] + + element.getValue(name, finalKeyframe).jump(finalKeyframe, false) + } + + measureInitialState() { + const { element, unresolvedKeyframes, name } = this + + if (!element.current) return + + if (name === "height") { + this.suspendedScrollY = window.pageYOffset + } + + this.measuredOrigin = positionalValues[name]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) + + unresolvedKeyframes[0] = this.measuredOrigin + } + + renderEndStyles() { + this.element.render() + } + + measureEndState() { + const { element, name, unresolvedKeyframes } = this + + if (!element.current) return + + const value = element.getValue(name) + value && value.jump(this.measuredOrigin, false) + + unresolvedKeyframes[unresolvedKeyframes.length - 1] = positionalValues[ + name + ]( + element.measureViewportBox(), + window.getComputedStyle(element.current) + ) as any + + if (name === "height" && this.suspendedScrollY !== undefined) { + window.scrollTo(0, this.suspendedScrollY) + } + + // If we removed transform values, reapply them before the next render + if (this.removedTransforms?.length) { + this.removedTransforms.forEach( + ([unsetTransformName, unsetTransformValue]) => { + element + .getValue(unsetTransformName)! + .set(unsetTransformValue) + } + ) + } + } +} diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts index 4b772373ac..e7c486bd99 100644 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ b/packages/framer-motion/src/render/dom/DOMVisualElement.ts @@ -1,11 +1,9 @@ -import { checkTargetForNewValues, getOrigin } from "../utils/setters" import { DOMVisualElementOptions } from "../dom/types" -import { parseDomVariant } from "../dom/utils/parse-dom-variant" import { VisualElement } from "../VisualElement" import { MotionProps } from "../../motion/types" import { MotionValue } from "../../value" -import { TargetAndTransition } from "../.." import { HTMLRenderState } from "../html/types" +import { DOMKeyframesResolver } from "./DOMKeyframesResolver" export abstract class DOMVisualElement< Instance extends HTMLElement | SVGElement = HTMLElement, @@ -36,24 +34,5 @@ export abstract class DOMVisualElement< delete style[key] } - makeTargetAnimatableFromInstance( - { transition, transitionEnd, ...target }: TargetAndTransition, - isMounted: boolean - ): TargetAndTransition { - const origin = getOrigin(target as any, transition || {}, this) - - if (isMounted) { - checkTargetForNewValues(this, target, origin as any) - - const parsed = parseDomVariant(this, target, origin, transitionEnd) - transitionEnd = parsed.transitionEnd - target = parsed.target - } - - return { - transition, - transitionEnd, - ...target, - } - } + KeyframeResolver = DOMKeyframesResolver } diff --git a/packages/framer-motion/src/render/dom/scroll/offsets/index.ts b/packages/framer-motion/src/render/dom/scroll/offsets/index.ts index 8d25efb853..d2b254179e 100644 --- a/packages/framer-motion/src/render/dom/scroll/offsets/index.ts +++ b/packages/framer-motion/src/render/dom/scroll/offsets/index.ts @@ -19,7 +19,7 @@ export function resolveOffsets( info: ScrollInfo, options: ScrollInfoOptions ) { - let { offset: offsetDefinition = ScrollOffset.All } = options + const { offset: offsetDefinition = ScrollOffset.All } = options const { target = container, axis = "y" } = options const lengthLabel = axis === "y" ? "height" : "width" diff --git a/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts b/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts index 9f1c2878e0..8219a3f8f1 100644 --- a/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts +++ b/packages/framer-motion/src/render/dom/utils/camel-to-dash.ts @@ -2,4 +2,4 @@ * Convert camelCase to dash-case properties. */ export const camelToDash = (str: string) => - str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() + str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase() diff --git a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts index 2e56bea526..37e2cf0c08 100644 --- a/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts +++ b/packages/framer-motion/src/render/dom/utils/css-variables-conversion.ts @@ -1,7 +1,5 @@ -import { Target, TargetWithKeyframes } from "../../../types" import { invariant } from "../../../utils/errors" import { isNumericalString } from "../../../utils/is-numerical-string" -import type { VisualElement } from "../../VisualElement" import { isCSSVariableToken, CSSVariableToken } from "./is-css-variable" /** @@ -13,18 +11,20 @@ import { isCSSVariableToken, CSSVariableToken } from "./is-css-variable" * * @param current */ + const splitCSSVariableRegex = - /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/ + // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words + /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u export function parseCSSVariable(current: string) { const match = splitCSSVariableRegex.exec(current) if (!match) return [,] - const [, token, fallback] = match - return [token, fallback] + const [, token1, token2, fallback] = match + return [`--${token1 ?? token2}`, fallback] } const maxDepth = 4 -function getVariableValue( +export function getVariableValue( current: CSSVariableToken, element: Element, depth = 1 @@ -45,64 +45,9 @@ function getVariableValue( if (resolved) { const trimmed = resolved.trim() return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed - } else if (isCSSVariableToken(fallback)) { - // The fallback might itself be a CSS variable, in which case we attempt to resolve it too. - return getVariableValue(fallback, element, depth + 1) - } else { - return fallback - } -} - -/** - * Resolve CSS variables from - * - * @internal - */ -export function resolveCSSVariables( - visualElement: VisualElement, - { ...target }: TargetWithKeyframes, - transitionEnd: Target | undefined -): { target: TargetWithKeyframes; transitionEnd?: Target } { - const element = visualElement.current - if (!(element instanceof Element)) return { target, transitionEnd } - - // If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd` - // only if they change but I think this reads clearer and this isn't a performance-critical path. - if (transitionEnd) { - transitionEnd = { ...transitionEnd } - } - - // Go through existing `MotionValue`s and ensure any existing CSS variables are resolved - visualElement.values.forEach((value) => { - const current = value.get() - if (!isCSSVariableToken(current)) return - - const resolved = getVariableValue(current, element) - if (resolved) value.set(resolved) - }) - - // Cycle through every target property and resolve CSS variables. Currently - // we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)` - for (const key in target) { - const current = target[key] - if (!isCSSVariableToken(current)) continue - - const resolved = getVariableValue(current, element) - - if (!resolved) continue - - // Clone target if it hasn't already been - target[key] = resolved - - if (!transitionEnd) transitionEnd = {} - - // If the user hasn't already set this key on `transitionEnd`, set it to the unresolved - // CSS variable. This will ensure that after the animation the component will reflect - // changes in the value of the CSS variable. - if (transitionEnd[key] === undefined) { - transitionEnd[key] = current - } } - return { target, transitionEnd } + return isCSSVariableToken(fallback) + ? getVariableValue(fallback, element, depth + 1) + : fallback } diff --git a/packages/framer-motion/src/render/dom/utils/is-css-variable.ts b/packages/framer-motion/src/render/dom/utils/is-css-variable.ts index c040d044c4..1995bd92ed 100644 --- a/packages/framer-motion/src/render/dom/utils/is-css-variable.ts +++ b/packages/framer-motion/src/render/dom/utils/is-css-variable.ts @@ -4,7 +4,7 @@ export type CSSVariableToken = `var(${CSSVariableName})` const checkStringStartsWith = (token: string) => - (key?: string): key is T => + (key?: string | number | null): key is T => typeof key === "string" && key.startsWith(token) export const isCSSVariableName = checkStringStartsWith("--") @@ -22,4 +22,4 @@ export const isCSSVariableToken = ( } const singleCssVariableRegex = - /var\s*\(\s*--[\w-]+(\s*,\s*(?:(?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)+)?\s*\)$/i + /var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu diff --git a/packages/framer-motion/src/render/dom/utils/is-svg-component.ts b/packages/framer-motion/src/render/dom/utils/is-svg-component.ts index 17eee34df1..7bb76df849 100644 --- a/packages/framer-motion/src/render/dom/utils/is-svg-component.ts +++ b/packages/framer-motion/src/render/dom/utils/is-svg-component.ts @@ -22,7 +22,7 @@ export function isSVGComponent(Component: string | ComponentType = ( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin?: Target, - transitionEnd?: Target -) => { - target: TargetWithKeyframes - transitionEnd?: Target -} - -/** - * Parse a DOM variant to make it animatable. This involves resolving CSS variables - * and ensuring animations like "20%" => "calc(50vw)" are performed in pixels. - */ -export const parseDomVariant: MakeTargetAnimatable = ( - visualElement, - target, - origin, - transitionEnd -) => { - const resolved = resolveCSSVariables(visualElement, target, transitionEnd) - target = resolved.target - transitionEnd = resolved.transitionEnd - return unitConversion(visualElement, target, origin, transitionEnd) -} diff --git a/packages/framer-motion/src/render/dom/utils/unit-conversion.ts b/packages/framer-motion/src/render/dom/utils/unit-conversion.ts index edb792a522..4a77e5b233 100644 --- a/packages/framer-motion/src/render/dom/utils/unit-conversion.ts +++ b/packages/framer-motion/src/render/dom/utils/unit-conversion.ts @@ -1,18 +1,12 @@ -import { Target, TargetWithKeyframes } from "../../../types" -import { isKeyframesTarget } from "../../../animation/utils/is-keyframes-target" -import { invariant } from "../../../utils/errors" import { MotionValue } from "../../../value" import { transformPropOrder } from "../../html/utils/transform" -import { ResolvedValues } from "../../types" -import { findDimensionValueType } from "../value-types/dimensions" import { Box } from "../../../projection/geometry/types" -import { isBrowser } from "../../../utils/is-browser" import type { VisualElement } from "../../VisualElement" import { ValueType } from "../../../value/types/types" import { number } from "../../../value/types/numbers" import { px } from "../../../value/types/numbers/units" -const positionalKeys = new Set([ +export const positionalKeys = new Set([ "width", "height", "top", @@ -24,12 +18,8 @@ const positionalKeys = new Set([ "translateX", "translateY", ]) -const isPositionalKey = (key: string) => positionalKeys.has(key) -const hasPositionalKey = (target: TargetWithKeyframes) => { - return Object.keys(target).some(isPositionalKey) -} -const isNumOrPxType = (v?: ValueType): v is ValueType => +export const isNumOrPxType = (v?: ValueType): v is ValueType => v === number || v === px type GetActualMeasurementInPixels = ( @@ -45,12 +35,12 @@ const getTranslateFromMatrix = (_bbox, { transform }) => { if (transform === "none" || !transform) return 0 - const matrix3d = transform.match(/^matrix3d\((.+)\)$/) + const matrix3d = transform.match(/^matrix3d\((.+)\)$/u) if (matrix3d) { return getPosFromMatrix(matrix3d[1], pos3) } else { - const matrix = transform.match(/^matrix\((.+)\)$/) as string[] + const matrix = transform.match(/^matrix\((.+)\)$/u) as string[] if (matrix) { return getPosFromMatrix(matrix[1], pos2) } else { @@ -65,7 +55,7 @@ const nonTranslationalTransformKeys = transformPropOrder.filter( ) type RemovedTransforms = [string, string | number][] -function removeNonTranslationalTransform(visualElement: VisualElement) { +export function removeNonTranslationalTransform(visualElement: VisualElement) { const removedTransforms: RemovedTransforms = [] nonTranslationalTransformKeys.forEach((key) => { @@ -105,213 +95,3 @@ export const positionalValues: { [key: string]: GetActualMeasurementInPixels } = // Alias translate longform names positionalValues.translateX = positionalValues.x positionalValues.translateY = positionalValues.y - -const convertChangedValueTypes = ( - target: TargetWithKeyframes, - visualElement: VisualElement, - changedKeys: string[] -) => { - const originBbox = visualElement.measureViewportBox() - const element = visualElement.current - const elementComputedStyle = getComputedStyle(element!) - const { display } = elementComputedStyle - const origin: ResolvedValues = {} - - // If the element is currently set to display: "none", make it visible before - // measuring the target bounding box - if (display === "none") { - visualElement.setStaticValue( - "display", - (target.display as string) || "block" - ) - } - - /** - * Record origins before we render and update styles - */ - changedKeys.forEach((key) => { - origin[key] = positionalValues[key](originBbox, elementComputedStyle) - }) - - // Apply the latest values (as set in checkAndConvertChangedValueTypes) - visualElement.render() - - const targetBbox = visualElement.measureViewportBox() - - changedKeys.forEach((key) => { - // Restore styles to their **calculated computed style**, not their actual - // originally set style. This allows us to animate between equivalent pixel units. - const value = visualElement.getValue(key) - - value && value.jump(origin[key]) - target[key] = positionalValues[key](targetBbox, elementComputedStyle) - }) - - return target -} - -const checkAndConvertChangedValueTypes = ( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin: Target = {}, - transitionEnd: Target = {} -): { target: TargetWithKeyframes; transitionEnd: Target } => { - target = { ...target } - transitionEnd = { ...transitionEnd } - - const targetPositionalKeys = Object.keys(target).filter(isPositionalKey) - - // We want to remove any transform values that could affect the element's bounding box before - // it's measured. We'll reapply these later. - let removedTransformValues: RemovedTransforms = [] - let hasAttemptedToRemoveTransformValues = false - - const changedValueTypeKeys: string[] = [] - - targetPositionalKeys.forEach((key) => { - const value = visualElement.getValue(key) as MotionValue< - number | string - > - if (!visualElement.hasValue(key)) return - - let from = origin[key] - let fromType = findDimensionValueType(from) - const to = target[key] - let toType - - // TODO: The current implementation of this basically throws an error - // if you try and do value conversion via keyframes. There's probably - // a way of doing this but the performance implications would need greater scrutiny, - // as it'd be doing multiple resize-remeasure operations. - if (isKeyframesTarget(to)) { - const numKeyframes = to.length - const fromIndex = to[0] === null ? 1 : 0 - from = to[fromIndex] - fromType = findDimensionValueType(from) - - for (let i = fromIndex; i < numKeyframes; i++) { - /** - * Don't allow wildcard keyframes to be used to detect - * a difference in value types. - */ - if (to[i] === null) break - - if (!toType) { - toType = findDimensionValueType(to[i]) - - invariant( - toType === fromType || - (isNumOrPxType(fromType) && isNumOrPxType(toType)), - "Keyframes must be of the same dimension as the current value" - ) - } else { - invariant( - findDimensionValueType(to[i]) === toType, - "All keyframes must be of the same type" - ) - } - } - } else { - toType = findDimensionValueType(to) - } - - if (fromType !== toType) { - // If they're both just number or px, convert them both to numbers rather than - // relying on resize/remeasure to convert (which is wasteful in this situation) - if (isNumOrPxType(fromType) && isNumOrPxType(toType)) { - const current = value.get() - if (typeof current === "string") { - value.set(parseFloat(current)) - } - if (typeof to === "string") { - target[key] = parseFloat(to) - } else if (Array.isArray(to) && toType === px) { - target[key] = to.map(parseFloat) - } - } else if ( - fromType?.transform && - toType?.transform && - (from === 0 || to === 0) - ) { - // If one or the other value is 0, it's safe to coerce it to the - // type of the other without measurement - if (from === 0) { - value.set((toType as any).transform(from)) - } else { - target[key] = (fromType as any).transform(to) - } - } else { - // If we're going to do value conversion via DOM measurements, we first - // need to remove non-positional transform values that could affect the bbox measurements. - if (!hasAttemptedToRemoveTransformValues) { - removedTransformValues = - removeNonTranslationalTransform(visualElement) - hasAttemptedToRemoveTransformValues = true - } - - changedValueTypeKeys.push(key) - transitionEnd[key] = - transitionEnd[key] !== undefined - ? transitionEnd[key] - : target[key] - - value.jump(to) - } - } - }) - - if (changedValueTypeKeys.length) { - const scrollY = - changedValueTypeKeys.indexOf("height") >= 0 - ? window.pageYOffset - : null - - const convertedTarget = convertChangedValueTypes( - target, - visualElement, - changedValueTypeKeys - ) - - // If we removed transform values, reapply them before the next render - if (removedTransformValues.length) { - removedTransformValues.forEach(([key, value]) => { - visualElement.getValue(key)!.set(value) - }) - } - - // Reapply original values - visualElement.render() - - // Restore scroll position - if (isBrowser && scrollY !== null) { - window.scrollTo({ top: scrollY }) - } - - return { target: convertedTarget, transitionEnd } - } else { - return { target, transitionEnd } - } -} - -/** - * Convert value types for x/y/width/height/top/left/bottom/right - * - * Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'` - * - * @internal - */ -export function unitConversion( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin?: Target, - transitionEnd?: Target -): { target: TargetWithKeyframes; transitionEnd?: Target } { - return hasPositionalKey(target) - ? checkAndConvertChangedValueTypes( - visualElement, - target, - origin, - transitionEnd - ) - : { target, transitionEnd } -} diff --git a/packages/framer-motion/src/render/html/utils/make-none-animatable.ts b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts new file mode 100644 index 0000000000..d730c066d7 --- /dev/null +++ b/packages/framer-motion/src/render/html/utils/make-none-animatable.ts @@ -0,0 +1,36 @@ +import { getAnimatableNone } from "../../dom/value-types/animatable-none" +import { UnresolvedKeyframes } from "../../utils/KeyframesResolver" + +/** + * If we encounter keyframes like "none" or "0" and we also have keyframes like + * "#fff" or "200px 200px" we want to find a keyframe to serve as a template for + * the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into + * zero equivalents, i.e. "#fff0" or "0px 0px". + */ +export function makeNoneKeyframesAnimatable( + unresolvedKeyframes: UnresolvedKeyframes, + noneKeyframeIndexes: number[], + name?: string +) { + let i = 0 + let animatableTemplate: string | undefined = undefined + while (i < unresolvedKeyframes.length && !animatableTemplate) { + if ( + typeof unresolvedKeyframes[i] === "string" && + unresolvedKeyframes[i] !== "none" && + unresolvedKeyframes[i] !== "0" + ) { + animatableTemplate = unresolvedKeyframes[i] as string + } + i++ + } + + if (animatableTemplate && name) { + for (const noneIndex of noneKeyframeIndexes) { + unresolvedKeyframes[noneIndex] = getAnimatableNone( + name, + animatableTemplate + ) + } + } +} diff --git a/packages/framer-motion/src/render/utils/KeyframesResolver.ts b/packages/framer-motion/src/render/utils/KeyframesResolver.ts new file mode 100644 index 0000000000..20d928e8c7 --- /dev/null +++ b/packages/framer-motion/src/render/utils/KeyframesResolver.ts @@ -0,0 +1,198 @@ +import { cancelFrame, frame } from "../../frameloop" +import { MotionValue } from "../../value" +import type { VisualElement } from "../VisualElement" + +export type UnresolvedKeyframes = Array + +export type ResolvedKeyframes = Array + +const toResolve = new Set() +let isScheduled = false +let anyNeedsMeasurement = false + +function measureAllKeyframes() { + if (anyNeedsMeasurement) { + // Write + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.unsetTransforms() + }) + + // Read + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.measureInitialState() + }) + + // Write + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.renderEndStyles() + }) + + // Read + toResolve.forEach((resolver) => { + resolver.needsMeasurement && resolver.measureEndState() + }) + } + + anyNeedsMeasurement = false + isScheduled = false + + toResolve.forEach((resolver) => resolver.complete()) + + toResolve.clear() +} + +function readAllKeyframes() { + toResolve.forEach((resolver) => { + resolver.readKeyframes() + + if (resolver.needsMeasurement) { + anyNeedsMeasurement = true + } + }) + + frame.resolveKeyframes(measureAllKeyframes) +} + +export function flushKeyframeResolvers() { + readAllKeyframes() + measureAllKeyframes() + + cancelFrame(readAllKeyframes) + cancelFrame(measureAllKeyframes) +} + +export type OnKeyframesResolved = ( + resolvedKeyframes: ResolvedKeyframes +) => void + +export class KeyframeResolver { + protected element?: VisualElement + protected unresolvedKeyframes: UnresolvedKeyframes + name?: string + + private motionValue?: MotionValue + private onComplete: OnKeyframesResolved + + /** + * Track whether this resolver has completed. Once complete, it never + * needs to attempt keyframe resolution again. + */ + private isComplete = false + + /** + * Track whether this resolver is async. If it is, it'll be added to the + * resolver queue and flushed in the next frame. Resolvers that aren't going + * to trigger read/write thrashing don't need to be async. + */ + private isAsync = false + + /** + * Track whether this resolver needs to perform a measurement + * to resolve its keyframes. + */ + needsMeasurement = false + + /** + * Track whether this resolver is currently scheduled to resolve + * to allow it to be cancelled and resumed externally. + */ + isScheduled = false + + constructor( + unresolvedKeyframes: UnresolvedKeyframes, + onComplete: OnKeyframesResolved, + name?: string, + motionValue?: MotionValue, + element?: VisualElement, + isAsync = false + ) { + this.unresolvedKeyframes = [...unresolvedKeyframes] + this.onComplete = onComplete + this.name = name + this.motionValue = motionValue + this.element = element + this.isAsync = isAsync + } + + scheduleResolve() { + this.isScheduled = true + if (this.isAsync) { + toResolve.add(this) + + if (!isScheduled) { + isScheduled = true + frame.read(readAllKeyframes) + } + } else { + this.readKeyframes() + this.complete() + } + } + + readKeyframes() { + const { unresolvedKeyframes, name, element, motionValue } = this + + /** + * If a keyframe is null, we hydrate it either by reading it from + * the instance, or propagating from previous keyframes. + */ + for (let i = 0; i < unresolvedKeyframes.length; i++) { + if (unresolvedKeyframes[i] === null) { + /** + * If the first keyframe is null, we need to find its value by sampling the element + */ + if (i === 0) { + const currentValue = motionValue?.get() + + const finalKeyframe = + unresolvedKeyframes[unresolvedKeyframes.length - 1] + + if (currentValue !== undefined) { + unresolvedKeyframes[0] = currentValue + } else if (element && name) { + const valueAsRead = element.readValue( + name, + finalKeyframe + ) + + if (valueAsRead !== undefined && valueAsRead !== null) { + unresolvedKeyframes[0] = valueAsRead + } + } + + if (unresolvedKeyframes[0] === undefined) { + unresolvedKeyframes[0] = finalKeyframe + } + + if (motionValue && currentValue === undefined) { + motionValue.set(unresolvedKeyframes[0] as T) + } + } else { + unresolvedKeyframes[i] = unresolvedKeyframes[i - 1] + } + } + } + } + + unsetTransforms() {} + measureInitialState() {} + renderEndStyles() {} + measureEndState() {} + + complete() { + this.isComplete = true + this.onComplete(this.unresolvedKeyframes as ResolvedKeyframes) + toResolve.delete(this) + } + + cancel() { + if (!this.isComplete) { + this.isScheduled = false + toResolve.delete(this) + } + } + + resume() { + if (!this.isComplete) this.scheduleResolve() + } +} diff --git a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts index 4d8850d84c..a9013348f4 100644 --- a/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts +++ b/packages/framer-motion/src/render/utils/__tests__/StateVisualElement.ts @@ -1,9 +1,7 @@ import { ResolvedValues } from "../../types" -import { checkTargetForNewValues, getOrigin } from "../setters" import { MotionProps } from "../../../motion/types" import { createBox } from "../../../projection/geometry/models" import { VisualElement } from "../../VisualElement" -import { TargetAndTransition } from "../../.." export class StateVisualElement extends VisualElement< ResolvedValues, @@ -34,14 +32,4 @@ export class StateVisualElement extends VisualElement< ) { return options.initialState[key] || 0 } - - makeTargetAnimatableFromInstance({ - transition, - transitionEnd, - ...target - }: TargetAndTransition) { - const origin = getOrigin(target as any, transition || {}, this) - checkTargetForNewValues(this, target, origin as any) - return { transition, transitionEnd, ...target } - } } diff --git a/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts b/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts index ab2d6b5784..9848ac69c3 100644 --- a/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts +++ b/packages/framer-motion/src/render/utils/__tests__/animation-state.test.ts @@ -30,15 +30,10 @@ function createTest( element: element, state: { ...element.animationState, - update( - newProps: any, - options: any, - type: any, - animateChanges = true - ): any { + update(newProps: any, type: any, animateChanges = true): any { element.update(newProps, null) return animateChanges === true - ? element.animationState?.animateChanges(options, type) + ? element.animationState?.animateChanges(type) : undefined }, }, diff --git a/packages/framer-motion/src/render/utils/animation-state.ts b/packages/framer-motion/src/render/utils/animation-state.ts index ff59550865..43cc531204 100644 --- a/packages/framer-motion/src/render/utils/animation-state.ts +++ b/packages/framer-motion/src/render/utils/animation-state.ts @@ -13,10 +13,7 @@ import { AnimationDefinition } from "../../animation/types" import { animateVisualElement } from "../../animation/interfaces/visual-element" export interface AnimationState { - animateChanges: ( - options?: VisualElementAnimationOptions, - type?: AnimationType - ) => Promise + animateChanges: (type?: AnimationType) => Promise setActive: ( type: AnimationType, isActive: boolean, @@ -56,19 +53,27 @@ export function createAnimationState( * This function will be used to reduce the animation definitions for * each active animation type into an object of resolved values for it. */ - const buildResolvedTypeValues = ( - acc: { [key: string]: any }, - definition: string | TargetAndTransition | undefined - ) => { - const resolved = resolveVariant(visualElement, definition) - - if (resolved) { - const { transition, transitionEnd, ...target } = resolved - acc = { ...acc, ...target, ...transitionEnd } - } + const buildResolvedTypeValues = + (type: AnimationType) => + ( + acc: { [key: string]: any }, + definition: string | TargetAndTransition | undefined + ) => { + const resolved = resolveVariant( + visualElement, + definition, + type === "exit" + ? visualElement.presenceContext?.custom + : undefined + ) - return acc - } + if (resolved) { + const { transition, transitionEnd, ...target } = resolved + acc = { ...acc, ...target, ...transitionEnd } + } + + return acc + } /** * This just allows us to inject mocked animation functions @@ -88,10 +93,7 @@ export function createAnimationState( * 3. Determine if any values have been removed from a type and figure out * what to animate those to. */ - function animateChanges( - options?: VisualElementAnimationOptions, - changedActiveType?: AnimationType - ) { + function animateChanges(changedActiveType?: AnimationType) { const props = visualElement.getProps() const context = visualElement.getVariantContext(true) || {} @@ -213,7 +215,7 @@ export function createAnimationState( * animateChanges calls to determine whether a value has changed. */ let resolvedValues = definitionList.reduce( - buildResolvedTypeValues, + buildResolvedTypeValues(type), {} ) @@ -261,7 +263,7 @@ export function createAnimationState( } if (valueHasChanged) { - if (next !== undefined) { + if (next !== undefined && next !== null) { // If next is defined and doesn't equal prev, it needs animating markToAnimate(key) } else { @@ -308,7 +310,7 @@ export function createAnimationState( animations.push( ...definitionList.map((animation) => ({ animation: animation as AnimationDefinition, - options: { type, ...options }, + options: { type }, })) ) } @@ -323,10 +325,8 @@ export function createAnimationState( const fallbackAnimation = {} removedKeys.forEach((key) => { const fallbackTarget = visualElement.getBaseTarget(key) - - if (fallbackTarget !== undefined) { - fallbackAnimation[key] = fallbackTarget - } + fallbackAnimation[key] = + fallbackTarget === undefined ? null : fallbackTarget }) animations.push({ animation: fallbackAnimation }) @@ -349,11 +349,7 @@ export function createAnimationState( /** * Change whether a certain animation type is active. */ - function setActive( - type: AnimationType, - isActive: boolean, - options?: VisualElementAnimationOptions - ) { + function setActive(type: AnimationType, isActive: boolean) { // If the active state hasn't changed, we can safely do nothing here if (state[type].isActive === isActive) return Promise.resolve() @@ -364,7 +360,7 @@ export function createAnimationState( state[type].isActive = isActive - const animations = animateChanges(options, type) + const animations = animateChanges(type) for (const key in state) { state[key].protectedKeys = {} diff --git a/packages/framer-motion/src/render/utils/setters.ts b/packages/framer-motion/src/render/utils/setters.ts index 74f58c2543..cd4904dc67 100644 --- a/packages/framer-motion/src/render/utils/setters.ts +++ b/packages/framer-motion/src/render/utils/setters.ts @@ -1,19 +1,6 @@ -import { AnimationDefinition } from "../../animation/types" -import { - Target, - TargetAndTransition, - TargetResolver, - TargetWithKeyframes, - Transition, -} from "../../types" -import { isNumericalString } from "../../utils/is-numerical-string" -import { isZeroValueString } from "../../utils/is-zero-value-string" +import { TargetAndTransition, TargetResolver } from "../../types" import { resolveFinalValueInKeyframes } from "../../utils/resolve-value" import { motionValue } from "../../value" -import { complex } from "../../value/types/complex" -import { getAnimatableNone } from "../dom/value-types/animatable-none" -import { findValueType } from "../dom/value-types/find" -import { ResolvedValues } from "../types" import type { VisualElement } from "../VisualElement" import { resolveVariant } from "./resolve-dynamic-variants" @@ -38,11 +25,7 @@ export function setTarget( definition: string | TargetAndTransition | TargetResolver ) { const resolved = resolveVariant(visualElement, definition) - let { - transitionEnd = {}, - transition = {}, - ...target - } = resolved ? visualElement.makeTargetAnimatable(resolved, false) : {} + let { transitionEnd = {}, transition = {}, ...target } = resolved || {} target = { ...target, ...transitionEnd } @@ -51,123 +34,3 @@ export function setTarget( setMotionValue(visualElement, key, value as string | number) } } - -function setVariants(visualElement: VisualElement, variantLabels: string[]) { - const reversedLabels = [...variantLabels].reverse() - - reversedLabels.forEach((key) => { - const variant = visualElement.getVariant(key) - variant && setTarget(visualElement, variant) - - if (visualElement.variantChildren) { - visualElement.variantChildren.forEach((child) => { - setVariants(child, variantLabels) - }) - } - }) -} - -export function setValues( - visualElement: VisualElement, - definition: AnimationDefinition -) { - if (Array.isArray(definition)) { - return setVariants(visualElement, definition) - } else if (typeof definition === "string") { - return setVariants(visualElement, [definition]) - } else { - setTarget(visualElement, definition as any) - } -} - -export function checkTargetForNewValues( - visualElement: VisualElement, - target: TargetWithKeyframes, - origin: ResolvedValues -) { - const newValueKeys = Object.keys(target).filter( - (key) => !visualElement.hasValue(key) - ) - - const numNewValues = newValueKeys.length - - if (!numNewValues) return - - for (let i = 0; i < numNewValues; i++) { - const key = newValueKeys[i] - const targetValue = target[key] - let value: string | number | null = null - - /** - * If the target is a series of keyframes, we can use the first value - * in the array. If this first value is null, we'll still need to read from the DOM. - */ - if (Array.isArray(targetValue)) { - value = targetValue[0] - } - - /** - * If the target isn't keyframes, or the first keyframe was null, we need to - * first check if an origin value was explicitly defined in the transition as "from", - * if not read the value from the DOM. As an absolute fallback, take the defined target value. - */ - if (value === null) { - value = origin[key] ?? visualElement.readValue(key) ?? target[key] - } - - /** - * If value is still undefined or null, ignore it. Preferably this would throw, - * but this was causing issues in Framer. - */ - if (value === undefined || value === null) continue - - if ( - typeof value === "string" && - (isNumericalString(value) || isZeroValueString(value)) - ) { - // If this is a number read as a string, ie "0" or "200", convert it to a number - value = parseFloat(value) - } else if (!findValueType(value) && complex.test(targetValue)) { - value = getAnimatableNone(key, targetValue) - } - - visualElement.addValue( - key, - motionValue(value, { owner: visualElement }) - ) - if (origin[key] === undefined) { - origin[key] = value as number | string - } - if (value !== null) visualElement.setBaseTarget(key, value) - } -} - -export function getOriginFromTransition(key: string, transition: Transition) { - if (!transition) return - const valueTransition = - transition[key] || transition["default"] || transition - return valueTransition.from -} - -export function getOrigin( - target: Target, - transition: Transition, - visualElement: VisualElement -) { - const origin: Target = {} - - for (const key in target) { - const transitionOrigin = getOriginFromTransition(key, transition) - - if (transitionOrigin !== undefined) { - origin[key] = transitionOrigin - } else { - const value = visualElement.getValue(key) - if (value) { - origin[key] = value.get() - } - } - } - - return origin -} diff --git a/packages/framer-motion/src/three-entry.ts b/packages/framer-motion/src/three-entry.ts index 06280a950b..3c5aa560b8 100644 --- a/packages/framer-motion/src/three-entry.ts +++ b/packages/framer-motion/src/three-entry.ts @@ -6,7 +6,6 @@ export type { export { AnimationType } from "./render/utils/types" export { animations } from "./motion/features/animations" export { MotionContext } from "./context/MotionContext" -export { checkTargetForNewValues } from "./render/utils/setters" export { createBox } from "./projection/geometry/models" export { calcLength } from "./projection/geometry/delta-calc" export { filterProps } from "./render/dom/utils/filter-props" diff --git a/packages/framer-motion/src/utils/interpolate.ts b/packages/framer-motion/src/utils/interpolate.ts index 1b47a58d95..54b48dbce6 100644 --- a/packages/framer-motion/src/utils/interpolate.ts +++ b/packages/framer-motion/src/utils/interpolate.ts @@ -74,6 +74,7 @@ export function interpolate( * that returns the output. */ if (inputLength === 1) return () => output[0] + if (inputLength === 2 && input[0] === input[1]) return () => output[1] // If input runs highest -> lowest, reverse both arrays if (input[0] > input[inputLength - 1]) { diff --git a/packages/framer-motion/src/utils/is-numerical-string.ts b/packages/framer-motion/src/utils/is-numerical-string.ts index a00f763763..cee510e18e 100644 --- a/packages/framer-motion/src/utils/is-numerical-string.ts +++ b/packages/framer-motion/src/utils/is-numerical-string.ts @@ -1,4 +1,4 @@ /** * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1" */ -export const isNumericalString = (v: string) => /^\-?\d*\.?\d+$/.test(v) +export const isNumericalString = (v: string) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v) diff --git a/packages/framer-motion/src/utils/is-zero-value-string.ts b/packages/framer-motion/src/utils/is-zero-value-string.ts index 415baeb55b..5354720bb3 100644 --- a/packages/framer-motion/src/utils/is-zero-value-string.ts +++ b/packages/framer-motion/src/utils/is-zero-value-string.ts @@ -1,4 +1,4 @@ /** * Check if the value is a zero value string like "0px" or "0%" */ -export const isZeroValueString = (v: string) => /^0[^.\s]+$/.test(v) +export const isZeroValueString = (v: string) => /^0[^.\s]+$/u.test(v) diff --git a/packages/framer-motion/src/value/__tests__/use-spring.test.tsx b/packages/framer-motion/src/value/__tests__/use-spring.test.tsx index a18cc07607..024b7b4ba6 100644 --- a/packages/framer-motion/src/value/__tests__/use-spring.test.tsx +++ b/packages/framer-motion/src/value/__tests__/use-spring.test.tsx @@ -4,7 +4,7 @@ import { useSpring } from "../use-spring" import { useMotionValue } from "../use-motion-value" import { motionValue, MotionValue } from ".." import { motion } from "../../" -import { syncDriver } from "../../animation/animators/js/__tests__/utils" +import { syncDriver } from "../../animation/animators/__tests__/utils" describe("useSpring", () => { test("can create a motion value from a number", async () => { diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts index d003dec0cd..335866bf10 100644 --- a/packages/framer-motion/src/value/index.ts +++ b/packages/framer-motion/src/value/index.ts @@ -77,7 +77,7 @@ export class MotionValue { /** * The current state of the `MotionValue`. */ - private current: V + private current: V | undefined /** * The previous state of the `MotionValue`. @@ -112,9 +112,7 @@ export class MotionValue { private stopPassiveEffect?: VoidFunction /** - * A reference to the currently-controlling Popmotion animation - * - * + * A reference to the currently-controlling animation. */ animation?: AnimationPlaybackControls @@ -287,11 +285,11 @@ export class MotionValue { * Set the state of the `MotionValue`, stopping any active animations, * effects, and resets velocity to `0`. */ - jump(v: V) { + jump(v: V, endAnimation = true) { this.updateAndNotify(v) this.prev = v this.prevUpdatedAt = this.prevFrameValue = undefined - this.stop() + endAnimation && this.stop() if (this.stopPassiveEffect) this.stopPassiveEffect() } @@ -334,7 +332,7 @@ export class MotionValue { collectMotionValues.current.push(this) } - return this.current + return this.current! } /** diff --git a/packages/framer-motion/src/value/types/complex/filter.ts b/packages/framer-motion/src/value/types/complex/filter.ts index 1350abe335..a49985fbd8 100644 --- a/packages/framer-motion/src/value/types/complex/filter.ts +++ b/packages/framer-motion/src/value/types/complex/filter.ts @@ -21,7 +21,7 @@ function applyDefaultFilter(v: string) { return name + "(" + defaultValue + unit + ")" } -const functionRegex = /([a-z-]*)\(.*?\)/g +const functionRegex = /\b([a-z-]*)\(.*?\)/gu export const filter = { ...complex, diff --git a/packages/framer-motion/src/value/types/complex/index.ts b/packages/framer-motion/src/value/types/complex/index.ts index bfb3cd6bea..4f0cc3fbc0 100644 --- a/packages/framer-motion/src/value/types/complex/index.ts +++ b/packages/framer-motion/src/value/types/complex/index.ts @@ -34,13 +34,13 @@ export interface ComplexValueInfo { types: Array } +// this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex` const complexRegex = - /(var\s*\(\s*--[\w-]+(\s*,\s*(?:(?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)+)?\s*\))|(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))|((-)?([\d]*\.?[\d])+)/gi + /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu export function analyseComplexValue(value: string | number): ComplexValueInfo { const originalValue = value.toString() - const matchedValues = originalValue.match(complexRegex) || [] const values: ComplexValues = [] const indexes: ValueIndexes = { color: [], @@ -49,9 +49,8 @@ export function analyseComplexValue(value: string | number): ComplexValueInfo { } const types: Array = [] - for (let i = 0; i < matchedValues.length; i++) { - const parsedValue: string | number = matchedValues[i] - + let i = 0 + const tokenised = originalValue.replace(complexRegex, (parsedValue) => { if (color.test(parsedValue)) { indexes.color.push(i) types.push(COLOR_TOKEN) @@ -65,9 +64,9 @@ export function analyseComplexValue(value: string | number): ComplexValueInfo { types.push(NUMBER_TOKEN) values.push(parseFloat(parsedValue)) } - } - - const tokenised = originalValue.replace(complexRegex, SPLIT_TOKEN) + ++i + return SPLIT_TOKEN + }) const split = tokenised.split(SPLIT_TOKEN) return { values, split, indexes, types } diff --git a/packages/framer-motion/src/value/types/utils.ts b/packages/framer-motion/src/value/types/utils.ts index 3e9add17be..ff92b6206c 100644 --- a/packages/framer-motion/src/value/types/utils.ts +++ b/packages/framer-motion/src/value/types/utils.ts @@ -7,11 +7,11 @@ // to avoid exponents export const sanitize = (v: number) => Math.round(v * 100000) / 100000 -export const floatRegex = /(-)?([\d]*\.?[\d])+/g +export const floatRegex = /-?(?:\d+(?:\.\d+)?|\.\d+)/gu export const colorRegex = - /(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))/gi + /(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu export const singleColorRegex = - /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i + /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu export function isString(v: any): v is string { return typeof v === "string" diff --git a/packages/framer-motion/src/value/use-spring.ts b/packages/framer-motion/src/value/use-spring.ts index b379da718a..b967bd8b90 100644 --- a/packages/framer-motion/src/value/use-spring.ts +++ b/packages/framer-motion/src/value/use-spring.ts @@ -5,11 +5,11 @@ import { useMotionValue } from "./use-motion-value" import { MotionConfigContext } from "../context/MotionConfigContext" import { SpringOptions } from "../animation/types" import { useIsomorphicLayoutEffect } from "../utils/use-isomorphic-effect" +import { frameData } from "../frameloop" import { - MainThreadAnimationControls, + MainThreadAnimation, animateValue, -} from "../animation/animators/js" -import { frameData } from "../frameloop" +} from "../animation/animators/MainThreadAnimation" /** * Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state. @@ -35,8 +35,9 @@ export function useSpring( config: SpringOptions = {} ) { const { isStatic } = useContext(MotionConfigContext) - const activeSpringAnimation = - useRef | null>(null) + const activeSpringAnimation = useRef | null>( + null + ) const value = useMotionValue(isMotionValue(source) ? source.get() : source) const stopAnimation = () => { diff --git a/yarn.lock b/yarn.lock index 61551e5ecf..21c654bceb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 5 cacheKey: 8 +"@aashutoshrathi/word-wrap@npm:^1.2.3": + version: 1.2.6 + resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" + checksum: ada901b9e7c680d190f1d012c84217ce0063d8f5c5a7725bb91ec3c5ed99bb7572680eb2d2938a531ccbaec39a95422fcd8a6b4a13110c7d98dd75402f66a0cd + languageName: node + linkType: hard + "@adobe/css-tools@npm:^4.0.1": version: 4.3.2 resolution: "@adobe/css-tools@npm:4.3.2" @@ -22,16 +29,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": - version: 7.18.6 - resolution: "@babel/code-frame@npm:7.18.6" - dependencies: - "@babel/highlight": ^7.18.6 - checksum: 195e2be3172d7684bf95cff69ae3b7a15a9841ea9d27d3c843662d50cdd7d6470fd9c8e64be84d031117e4a4083486effba39f9aef6bbb2c89f7f21bcfba33ba - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.22.13": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": version: 7.22.13 resolution: "@babel/code-frame@npm:7.22.13" dependencies: @@ -71,18 +69,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.18.10, @babel/generator@npm:^7.7.2": - version: 7.18.12 - resolution: "@babel/generator@npm:7.18.12" - dependencies: - "@babel/types": ^7.18.10 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: 07dd71d255144bb703a80ab0156c35d64172ce81ddfb70ff24e2be687b052080233840c9a28d92fa2c33f7ecb8a8b30aef03b807518afc53b74c7908bf8859b1 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.23.3": +"@babel/generator@npm:^7.18.10, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.7.2": version: 7.23.3 resolution: "@babel/generator@npm:7.23.3" dependencies: @@ -172,14 +159,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-environment-visitor@npm:7.18.9" - checksum: b25101f6162ddca2d12da73942c08ad203d7668e06663df685634a8fde54a98bc015f6f62938e8554457a592a024108d45b8f3e651fd6dcdb877275b73cc4420 - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.22.20": +"@babel/helper-environment-visitor@npm:^7.18.9, @babel/helper-environment-visitor@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-environment-visitor@npm:7.22.20" checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 @@ -195,17 +175,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-function-name@npm:7.18.9" - dependencies: - "@babel/template": ^7.18.6 - "@babel/types": ^7.18.9 - checksum: d04c44e0272f887c0c868651be7fc3c5690531bea10936f00d4cca3f6d5db65e76dfb49e8d553c42ae1fe1eba61ccce9f3d93ba2df50a66408c8d4c3cc61cf0c - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.23.0": +"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-function-name@npm:7.23.0" dependencies: @@ -215,16 +185,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-hoist-variables@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: fd9c35bb435fda802bf9ff7b6f2df06308a21277c6dec2120a35b09f9de68f68a33972e2c15505c1a1a04b36ec64c9ace97d4a9e26d6097b76b4396b7c5fa20f - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.22.5": +"@babel/helper-hoist-variables@npm:^7.18.6, @babel/helper-hoist-variables@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-hoist-variables@npm:7.22.5" dependencies: @@ -328,16 +289,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-split-export-declaration@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: c6d3dede53878f6be1d869e03e9ffbbb36f4897c7cc1527dc96c56d127d834ffe4520a6f7e467f5b6f3c2843ea0e81a7819d66ae02f707f6ac057f3d57943a2b - languageName: node - linkType: hard - -"@babel/helper-split-export-declaration@npm:^7.22.6": +"@babel/helper-split-export-declaration@npm:^7.18.6, @babel/helper-split-export-declaration@npm:^7.22.6": version: 7.22.6 resolution: "@babel/helper-split-export-declaration@npm:7.22.6" dependencies: @@ -346,13 +298,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/helper-string-parser@npm:7.18.10" - checksum: d554a4393365b624916b5c00a4cc21c990c6617e7f3fe30be7d9731f107f12c33229a7a3db9d829bfa110d2eb9f04790745d421640e3bd245bb412dc0ea123c1 - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-string-parser@npm:7.22.5" @@ -360,14 +305,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-validator-identifier@npm:7.18.6" - checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.22.20": +"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc @@ -404,17 +342,6 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/highlight@npm:7.18.6" - dependencies: - "@babel/helper-validator-identifier": ^7.18.6 - chalk: ^2.0.0 - js-tokens: ^4.0.0 - checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789 - languageName: node - linkType: hard - "@babel/highlight@npm:^7.22.13": version: 7.22.20 resolution: "@babel/highlight@npm:7.22.20" @@ -426,16 +353,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.10": - version: 7.18.11 - resolution: "@babel/parser@npm:7.18.11" - bin: - parser: ./bin/babel-parser.js - checksum: 5ecc75b83e62ec53a947b1635a6ca75d6210d4a4f962f9f16f4239a6783f98e57f9662b598fa2fb1b8e12c0ad5c2bd86846ed0b97b85eb73dd7498b3a6d71a4b - languageName: node - linkType: hard - -"@babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3": version: 7.23.3 resolution: "@babel/parser@npm:7.23.3" bin: @@ -1344,18 +1262,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.18.10, @babel/template@npm:^7.18.6, @babel/template@npm:^7.3.3": - version: 7.18.10 - resolution: "@babel/template@npm:7.18.10" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/parser": ^7.18.10 - "@babel/types": ^7.18.10 - checksum: 93a6aa094af5f355a72bd55f67fa1828a046c70e46f01b1606e6118fa1802b6df535ca06be83cc5a5e834022be95c7b714f0a268b5f20af984465a71e28f1473 - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.15": +"@babel/template@npm:^7.18.10, @babel/template@npm:^7.18.6, @babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" dependencies: @@ -1384,18 +1291,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.18.10 - resolution: "@babel/types@npm:7.18.10" - dependencies: - "@babel/helper-string-parser": ^7.18.10 - "@babel/helper-validator-identifier": ^7.18.6 - to-fast-properties: ^2.0.0 - checksum: 11632c9b106e54021937a6498138014ebc9ad6c327a07b2af3ba8700773945aba4055fd136431cbe3a500d0f363cbf9c68eb4d6d38229897c5de9d06e14c85e8 - languageName: node - linkType: hard - -"@babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.3 resolution: "@babel/types@npm:7.23.3" dependencies: @@ -1522,20 +1418,45 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.3.0": - version: 1.3.0 - resolution: "@eslint/eslintrc@npm:1.3.0" +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.6.1, @eslint-community/regexpp@npm:^4.8.0, @eslint-community/regexpp@npm:^4.9.1": + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 2a6e345429ea8382aaaf3a61f865cae16ed44d31ca917910033c02dc00d505d939f10b81e079fa14d43b51499c640138e153b7e40743c4c094d9df97d4e56f7b + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" dependencies: ajv: ^6.12.4 debug: ^4.3.2 - espree: ^9.3.2 - globals: ^13.15.0 + espree: ^9.6.0 + globals: ^13.19.0 ignore: ^5.2.0 import-fresh: ^3.2.1 js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: a1e734ad31a8b5328dce9f479f185fd4fc83dd7f06c538e1fa457fd8226b89602a55cc6458cd52b29573b01cdfaf42331be8cfc1fec732570086b591f4ed6515 + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 + languageName: node + linkType: hard + +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 315dc65b0e9893e2bff139bddace7ea601ad77ed47b4550e73da8c9c2d2766c7a575c3cddf17ef85b8fd6a36ff34f91729d0dcca56e73ca887c10df91a41b0bb languageName: node linkType: hard @@ -1572,28 +1493,28 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.10.4": - version: 0.10.4 - resolution: "@humanwhocodes/config-array@npm:0.10.4" +"@humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 - minimatch: ^3.0.4 - checksum: d480e5d57e6d787565b6cff78e27c3d1b380692d4ffb0ada7d7f5957a56c9032f034da05a3e443065dbd0671ebf4d859036ced34e96b325bbc1badbae3c05300 + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 + minimatch: ^3.0.5 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 languageName: node linkType: hard -"@humanwhocodes/gitignore-to-minimatch@npm:^1.0.2": - version: 1.0.2 - resolution: "@humanwhocodes/gitignore-to-minimatch@npm:1.0.2" - checksum: aba5c40c9e3770ed73a558b0bfb53323842abfc2ce58c91d7e8b1073995598e6374456d38767be24ab6176915f0a8d8b23eaae5c85e2b488c0dccca6d795e2ad +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee languageName: node linkType: hard @@ -1876,13 +1797,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3": - version: 3.1.0 - resolution: "@jridgewell/resolve-uri@npm:3.1.0" - checksum: b5ceaaf9a110fcb2780d1d8f8d4a0bfd216702f31c988d8042e5f8fbe353c55d9b0f55a1733afdc64806f8e79c485d2464680ac48a0d9fcadb9548ee6b81d267 - languageName: node - linkType: hard - "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -1907,31 +1821,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10": - version: 1.4.14 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" - checksum: 61100637b6d173d3ba786a5dff019e1a74b1f394f323c1fee337ff390239f053b87266c7a948777f4b1ee68c01a8ad0ab61e5ff4abb5a012a0b091bec391ab97 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.14": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.13, @jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.15 - resolution: "@jridgewell/trace-mapping@npm:0.3.15" - dependencies: - "@jridgewell/resolve-uri": ^3.0.3 - "@jridgewell/sourcemap-codec": ^1.4.10 - checksum: 38917e9c2b014d469a9f51c016ed506acbe44dd16ec2f6f99b553ebf3764d22abadbf992f2367b6d2b3511f3eae8ed3a8963f6c1030093fda23efd35ecab2bae - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.17": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.13, @jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.20 resolution: "@jridgewell/trace-mapping@npm:0.3.20" dependencies: @@ -2747,7 +2644,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -3851,6 +3748,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + "@use-gesture/core@npm:10.2.18": version: 10.2.18 resolution: "@use-gesture/core@npm:10.2.18" @@ -4161,12 +4065,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0": - version: 8.8.0 - resolution: "acorn@npm:8.8.0" +"acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.9.0": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" bin: acorn: bin/acorn - checksum: 7270ca82b242eafe5687a11fea6e088c960af712683756abf0791b68855ea9cace3057bd5e998ffcef50c944810c1e0ca1da526d02b32110e13c722aa959afdc + checksum: 76d8e7d559512566b43ab4aadc374f11f563f0a9e21626dd59cb2888444e9445923ae9f3699972767f18af61df89cd89f5eaaf772d1327b055b45cb829b4a88c languageName: node linkType: hard @@ -4232,7 +4136,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.1.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -5424,7 +5328,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:2.4.2, chalk@npm:^2.0.0, chalk@npm:^2.0.1, chalk@npm:^2.3.1, chalk@npm:^2.4.1, chalk@npm:^2.4.2": +"chalk@npm:2.4.2, chalk@npm:^2.0.1, chalk@npm:^2.3.1, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -5846,6 +5750,13 @@ __metadata: languageName: node linkType: hard +"comment-parser@npm:^1.4.0": + version: 1.4.1 + resolution: "comment-parser@npm:1.4.1" + checksum: e0f6f60c5139689c4b1b208ea63e0730d9195a778e90dd909205f74f00b39eb0ead05374701ec5e5c29d6f28eb778cd7bc41c1366ab1d271907f1def132d6bf1 + languageName: node + linkType: hard + "common-tags@npm:1.8.0": version: 1.8.0 resolution: "common-tags@npm:1.8.0" @@ -6387,7 +6298,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -7184,6 +7095,32 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-redos-detector@npm:^2.4.0": + version: 2.4.0 + resolution: "eslint-plugin-redos-detector@npm:2.4.0" + peerDependencies: + eslint: ">=6" + checksum: 9b47a3af4780d3c8bb655801361d1093d77210ec62b9cb10604bafe8c666e4658bdfbb813722da8b25f002be6aace71be145930ba1e35391220e504d0817607c + languageName: node + linkType: hard + +"eslint-plugin-regexp@npm:^2.2.0": + version: 2.2.0 + resolution: "eslint-plugin-regexp@npm:2.2.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.9.1 + comment-parser: ^1.4.0 + jsdoc-type-pratt-parser: ^4.0.0 + refa: ^0.12.1 + regexp-ast-analysis: ^0.7.1 + scslre: ^0.3.0 + peerDependencies: + eslint: ">=8.44.0" + checksum: cfe6870ebfb2501c2f5c8f33fe81128fbb140c9455b22ed7aa25af3b7ea76a7d031df621e20faad220a29a642c633c3a3d8abd44cff7780cd4e828d45ffaee97 + languageName: node + linkType: hard + "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -7194,13 +7131,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.1.1": - version: 7.1.1 - resolution: "eslint-scope@npm:7.1.1" +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" dependencies: esrecurse: ^4.3.0 estraverse: ^5.2.0 - checksum: 9f6e974ab2db641ca8ab13508c405b7b859e72afe9f254e8131ff154d2f40c99ad4545ce326fd9fde3212ff29707102562a4834f1c48617b35d98c71a97fbf3e + checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e languageName: node linkType: hard @@ -7222,70 +7159,69 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": - version: 3.3.0 - resolution: "eslint-visitor-keys@npm:3.3.0" - checksum: d59e68a7c5a6d0146526b0eec16ce87fbf97fe46b8281e0d41384224375c4e52f5ffb9e16d48f4ea50785cde93f766b0c898e31ab89978d88b0e1720fbfb7808 +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 languageName: node linkType: hard -"eslint@npm:^8.21.0": - version: 8.22.0 - resolution: "eslint@npm:8.22.0" +"eslint@npm:^8.57.0": + version: 8.57.0 + resolution: "eslint@npm:8.57.0" dependencies: - "@eslint/eslintrc": ^1.3.0 - "@humanwhocodes/config-array": ^0.10.4 - "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 - ajv: ^6.10.0 + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.6.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.57.0 + "@humanwhocodes/config-array": ^0.11.14 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + "@ungap/structured-clone": ^1.2.0 + ajv: ^6.12.4 chalk: ^4.0.0 cross-spawn: ^7.0.2 debug: ^4.3.2 doctrine: ^3.0.0 escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.1 - eslint-utils: ^3.0.0 - eslint-visitor-keys: ^3.3.0 - espree: ^9.3.3 - esquery: ^1.4.0 + eslint-scope: ^7.2.2 + eslint-visitor-keys: ^3.4.3 + espree: ^9.6.1 + esquery: ^1.4.2 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 file-entry-cache: ^6.0.1 find-up: ^5.0.0 - functional-red-black-tree: ^1.0.1 - glob-parent: ^6.0.1 - globals: ^13.15.0 - globby: ^11.1.0 - grapheme-splitter: ^1.0.4 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 ignore: ^5.2.0 - import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 + is-path-inside: ^3.0.3 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 lodash.merge: ^4.6.2 minimatch: ^3.1.2 natural-compare: ^1.4.0 - optionator: ^0.9.1 - regexpp: ^3.2.0 + optionator: ^0.9.3 strip-ansi: ^6.0.1 - strip-json-comments: ^3.1.0 text-table: ^0.2.0 - v8-compile-cache: ^2.0.3 bin: eslint: bin/eslint.js - checksum: 2d84a7a2207138cdb250759b047fdb05a57fede7f87b7a039d9370edba7f26e23a873a208becfd4b2c9e4b5499029f3fc3b9318da3290e693d25c39084119c80 + checksum: 3a48d7ff85ab420a8447e9810d8087aea5b1df9ef68c9151732b478de698389ee656fd895635b5f2871c89ee5a2652b3f343d11e9db6f8486880374ebc74a2d9 languageName: node linkType: hard -"espree@npm:^9.3.2, espree@npm:^9.3.3": - version: 9.3.3 - resolution: "espree@npm:9.3.3" +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" dependencies: - acorn: ^8.8.0 + acorn: ^8.9.0 acorn-jsx: ^5.3.2 - eslint-visitor-keys: ^3.3.0 - checksum: 33e8a36fc15d082e68672e322e22a53856b564d60aad8f291a667bfc21b2c900c42412d37dd3c7a0f18b9d0d8f8858dabe8776dbd4b4c2f72c5cf4d6afeabf65 + eslint-visitor-keys: ^3.4.1 + checksum: eb8c149c7a2a77b3f33a5af80c10875c3abd65450f60b8af6db1bfcfa8f101e21c1e56a561c6dc13b848e18148d43469e7cd208506238554fb5395a9ea5a1ab9 languageName: node linkType: hard @@ -7299,12 +7235,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.0": - version: 1.4.0 - resolution: "esquery@npm:1.4.0" +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" dependencies: estraverse: ^5.1.0 - checksum: a0807e17abd7fbe5fbd4fab673038d6d8a50675cdae6b04fbaa520c34581be0c5fa24582990e8acd8854f671dd291c78bb2efb9e0ed5b62f33bac4f9cf820210 + checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 languageName: node linkType: hard @@ -7894,17 +7830,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0": - version: 1.15.1 - resolution: "follow-redirects@npm:1.15.1" - peerDependenciesMeta: - debug: - optional: true - checksum: 6aa4e3e3cdfa3b9314801a1cd192ba756a53479d9d8cca65bf4db3a3e8834e62139245cd2f9566147c8dfe2efff1700d3e6aefd103de4004a7b99985e71dd533 - languageName: node - linkType: hard - -"follow-redirects@npm:^1.15.0": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.0": version: 1.15.4 resolution: "follow-redirects@npm:1.15.4" peerDependenciesMeta: @@ -8010,8 +7936,8 @@ __metadata: cache-loader: ^1.2.5 convert-tsconfig-paths-to-webpack-aliases: ^0.9.2 fork-ts-checker-webpack-plugin: ^6.2.0 - framer-motion: ^11.0.8 - framer-motion-3d: ^11.0.8 + framer-motion: ^11.0.11 + framer-motion-3d: ^11.0.11 path-browserify: ^1.0.1 react: ^18.2.0 react-dom: ^18.2.0 @@ -8049,11 +7975,13 @@ __metadata: concurrently: ^7.3.0 convert-tsconfig-paths-to-webpack-aliases: ^0.9.2 cypress: ^3.4.0 - eslint: ^8.21.0 + eslint: ^8.57.0 eslint-config-prettier: ^8.5.0 eslint-plugin-import: ^2.26.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-redos-detector: ^2.4.0 + eslint-plugin-regexp: ^2.2.0 gsap: ^3.12.5 jest: ^28.0.0 jest-environment-jsdom: ^28.1.3 @@ -8080,14 +8008,14 @@ __metadata: languageName: unknown linkType: soft -"framer-motion-3d@^11.0.8, framer-motion-3d@workspace:packages/framer-motion-3d": +"framer-motion-3d@^11.0.11, framer-motion-3d@workspace:packages/framer-motion-3d": version: 0.0.0-use.local resolution: "framer-motion-3d@workspace:packages/framer-motion-3d" dependencies: "@react-three/fiber": ^8.2.2 "@react-three/test-renderer": ^9.0.0 "@rollup/plugin-commonjs": ^22.0.1 - framer-motion: ^11.0.8 + framer-motion: ^11.0.11 react-merge-refs: ^2.0.1 peerDependencies: "@react-three/fiber": ^8.2.2 @@ -8097,7 +8025,7 @@ __metadata: languageName: unknown linkType: soft -"framer-motion@^11.0.8, framer-motion@workspace:packages/framer-motion": +"framer-motion@^11.0.11, framer-motion@workspace:packages/framer-motion": version: 0.0.0-use.local resolution: "framer-motion@workspace:packages/framer-motion" dependencies: @@ -8526,7 +8454,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^6.0.1": +"glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" dependencies: @@ -8605,12 +8533,12 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.15.0": - version: 13.17.0 - resolution: "globals@npm:13.17.0" +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" dependencies: type-fest: ^0.20.2 - checksum: fbaf4112e59b92c9f5575e85ce65e9e17c0b82711196ec5f58beb08599bbd92fd72703d6dfc9b080381fd35b644e1b11dcf25b38cc2341ec21df942594cbc8ce + checksum: 56066ef058f6867c04ff203b8a44c15b038346a62efbc3060052a1016be9f56f4cf0b2cd45b74b22b81e521a889fc7786c73691b0549c2f3a6e825b3d394f43c languageName: node linkType: hard @@ -8655,10 +8583,10 @@ __metadata: languageName: node linkType: hard -"grapheme-splitter@npm:^1.0.4": - version: 1.0.4 - resolution: "grapheme-splitter@npm:1.0.4" - checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620 +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 languageName: node linkType: hard @@ -9114,7 +9042,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.0.0, import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1": +"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -9713,6 +9641,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + "is-plain-obj@npm:^1.0.0, is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -10527,6 +10462,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:^4.0.0": + version: 4.0.0 + resolution: "jsdoc-type-pratt-parser@npm:4.0.0" + checksum: af0629c9517e484be778d8564440fec8de5b7610e0c9c88a3ba4554321364faf72b46689c8d8845faa12c0718437a9ed97e231977efc0f2d50e8a2dbad807eb3 + languageName: node + linkType: hard + "jsdom@npm:^19.0.0": version: 19.0.0 resolution: "jsdom@npm:19.0.0" @@ -11625,7 +11567,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -11668,14 +11610,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": - version: 1.2.6 - resolution: "minimist@npm:1.2.6" - checksum: d15428cd1e11eb14e1233bcfb88ae07ed7a147de251441d61158619dfb32c4d7e9061d09cab4825fdee18ecd6fce323228c8c47b5ba7cd20af378ca4048fb3fb - languageName: node - linkType: hard - -"minimist@npm:^1.2.3": +"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 @@ -11936,16 +11871,7 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.12.1": - version: 2.16.0 - resolution: "nan@npm:2.16.0" - dependencies: - node-gyp: latest - checksum: cb16937273ea55b01ea47df244094c12297ce6b29b36e845d349f1f7c268b8d7c5abd126a102c5678a1e1afd0d36bba35ea0cc959e364928ce60561c9306064a - languageName: node - linkType: hard - -"nan@npm:^2.14.0": +"nan@npm:^2.12.1, nan@npm:^2.14.0": version: 2.18.0 resolution: "nan@npm:2.18.0" dependencies: @@ -12633,17 +12559,17 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.1": - version: 0.9.1 - resolution: "optionator@npm:0.9.1" +"optionator@npm:^0.9.3": + version: 0.9.3 + resolution: "optionator@npm:0.9.3" dependencies: + "@aashutoshrathi/word-wrap": ^1.2.3 deep-is: ^0.1.3 fast-levenshtein: ^2.0.6 levn: ^0.4.1 prelude-ls: ^1.2.1 type-check: ^0.4.0 - word-wrap: ^1.2.3 - checksum: dbc6fa065604b24ea57d734261914e697bd73b69eff7f18e967e8912aa2a40a19a9f599a507fa805be6c13c24c4eae8c71306c239d517d42d4c041c942f508a0 + checksum: 09281999441f2fe9c33a5eeab76700795365a061563d66b098923eb719251a42bdbe432790d35064d0816ead9296dbeb1ad51a733edf4167c96bd5d0882e428a languageName: node linkType: hard @@ -13823,14 +13749,14 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" dependencies: inherits: ^2.0.3 string_decoder: ^1.1.1 util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d languageName: node linkType: hard @@ -13849,17 +13775,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": - version: 3.6.2 - resolution: "readable-stream@npm:3.6.2" - dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d - languageName: node - linkType: hard - "readdir-scoped-modules@npm:^1.0.0": version: 1.1.0 resolution: "readdir-scoped-modules@npm:1.1.0" @@ -13920,6 +13835,15 @@ __metadata: languageName: node linkType: hard +"refa@npm:^0.12.0, refa@npm:^0.12.1": + version: 0.12.1 + resolution: "refa@npm:0.12.1" + dependencies: + "@eslint-community/regexpp": ^4.8.0 + checksum: 845cef54786884d5f09558dd600cec20ee354ebbcabd206503d5cc5d63d22bb69dee8b66bf8411de622693fc5c2b9d42b9cf1e5e6900c538ee333eefb5cc1b41 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.0.1": version: 10.0.1 resolution: "regenerate-unicode-properties@npm:10.0.1" @@ -13962,6 +13886,16 @@ __metadata: languageName: node linkType: hard +"regexp-ast-analysis@npm:^0.7.0, regexp-ast-analysis@npm:^0.7.1": + version: 0.7.1 + resolution: "regexp-ast-analysis@npm:0.7.1" + dependencies: + "@eslint-community/regexpp": ^4.8.0 + refa: ^0.12.1 + checksum: c1c47fea637412d8362a9358b1b2952a6ab159daaede2244c05e79c175844960259c88e30072e4f81a06e5ac1c80f590b14038b17bc8cff09293f752cf843b1c + languageName: node + linkType: hard + "regexp-to-ast@npm:0.5.0": version: 0.5.0 resolution: "regexp-to-ast@npm:0.5.0" @@ -14551,6 +14485,17 @@ __metadata: languageName: node linkType: hard +"scslre@npm:^0.3.0": + version: 0.3.0 + resolution: "scslre@npm:0.3.0" + dependencies: + "@eslint-community/regexpp": ^4.8.0 + refa: ^0.12.0 + regexp-ast-analysis: ^0.7.0 + checksum: a89d4fe5dbf632cae14cc1e53c9d18012924cc88d6615406ad90190d2b9957fc8db16994c2023235af1b6a6c25290b089eb4c26e47d21b05073b933be5ca9d33 + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -15516,7 +15461,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 @@ -16774,13 +16719,6 @@ __metadata: languageName: node linkType: hard -"v8-compile-cache@npm:^2.0.3": - version: 2.3.0 - resolution: "v8-compile-cache@npm:2.3.0" - checksum: adb0a271eaa2297f2f4c536acbfee872d0dd26ec2d76f66921aa7fc437319132773483344207bdbeee169225f4739016d8d2dbf0553913a52bb34da6d0334f8e - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.1": version: 9.0.1 resolution: "v8-to-istanbul@npm:9.0.1" @@ -17227,7 +17165,7 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3": +"word-wrap@npm:~1.2.3": version: 1.2.4 resolution: "word-wrap@npm:1.2.4" checksum: 8f1f2e0a397c0e074ca225ba9f67baa23f99293bc064e31355d426ae91b8b3f6b5f6c1fc9ae5e9141178bb362d563f55e62fd8d5c31f2a77e3ade56cb3e35bd1