From 740fe212ad776f1c11fce327a6d7fe8c2cd2f2f7 Mon Sep 17 00:00:00 2001 From: Patrick Kettner Date: Sun, 10 May 2026 03:06:06 -0400 Subject: [PATCH 1/2] add linear-easing guidance --- .../user-experience/linear-easing/demo.html | 57 ++++++++ .../linear-easing/expectations.md | 8 ++ .../user-experience/linear-easing/grader.ts | 134 ++++++++++++++++++ guides/user-experience/linear-easing/guide.md | 53 +++++++ .../linear-easing/negative-demo.html | 38 +++++ .../linear-easing/tasks/task.md | 6 + .../physics-based-easing/expectations.md | 5 +- .../physics-based-easing/guide.md | 29 ++-- 8 files changed, 314 insertions(+), 16 deletions(-) create mode 100644 guides/user-experience/linear-easing/demo.html create mode 100644 guides/user-experience/linear-easing/expectations.md create mode 100644 guides/user-experience/linear-easing/grader.ts create mode 100644 guides/user-experience/linear-easing/guide.md create mode 100644 guides/user-experience/linear-easing/negative-demo.html create mode 100644 guides/user-experience/linear-easing/tasks/task.md diff --git a/guides/user-experience/linear-easing/demo.html b/guides/user-experience/linear-easing/demo.html new file mode 100644 index 000000000..940f80968 --- /dev/null +++ b/guides/user-experience/linear-easing/demo.html @@ -0,0 +1,57 @@ + + + + + +linear-easing Demo + + + +
+
+
+ + diff --git a/guides/user-experience/linear-easing/expectations.md b/guides/user-experience/linear-easing/expectations.md new file mode 100644 index 000000000..36f9765f9 --- /dev/null +++ b/guides/user-experience/linear-easing/expectations.md @@ -0,0 +1,8 @@ +- The agent has defined an animation or transition that uses the `linear(...)` easing function inline. +- The agent MUST NOT abstract the `linear(...)` function into a CSS variable (e.g. `--easing: linear(...)` is not allowed). +- The `linear(...)` function contains at least three comma-separated progress points. +- The agent has provided a fallback easing function (e.g., `ease`, `ease-in-out`, `cubic-bezier()`) as a completely separate, full property declaration (e.g., `transition: transform 0.3s ease;`) immediately before the declaration containing the `linear(...)` function. + +## Grader Implementation Notes +- For the CSS variable rule, only fail if `linear(` is assigned to a custom property. Do not fail if standard unrelated variables like `var(--primary)` are used for other properties. +- For the fallback rule, when checking the raw CSS, remember that `linear(` might not be the first token after the colon, especially in shorthand properties like `transition: transform 0.5s linear(...)`. diff --git a/guides/user-experience/linear-easing/grader.ts b/guides/user-experience/linear-easing/grader.ts new file mode 100644 index 000000000..f22a33bbc --- /dev/null +++ b/guides/user-experience/linear-easing/grader.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; +import * as path from 'path'; +import * as fs from 'fs'; + +const targetFile = process.env.TARGET_FILE || ''; + +test.describe('Linear Easing Grader', () => { + test.beforeEach(async ({ page }) => { + if (!targetFile) { + throw new Error('TARGET_FILE environment variable is required'); + } + const absolutePath = path.isAbsolute(targetFile) + ? `file://${targetFile}` + : `file://${path.join(process.cwd(), targetFile)}`; + await page.goto(absolutePath); + }); + + test('The agent has defined an animation or transition that uses the linear() easing function inline.', async ({ page }) => { + const linearUsedInline = await page.evaluate(() => { + const styles = Array.from(document.styleSheets); + for (const sheet of styles) { + try { + const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; + for (const rule of rules) { + if (!rule.style) continue; + const props = ['animation-timing-function', 'transition-timing-function', 'animation', 'transition']; + for (const prop of props) { + const val = rule.style.getPropertyValue(prop); + if (val.includes('linear(')) { + return true; + } + } + } + } catch (e) {} + } + return false; + }); + expect(linearUsedInline).toBe(true); + }); + + test('The linear() function contains at least three comma-separated progress points.', async ({ page }) => { + const pointCount = await page.evaluate(() => { + let maxPoints = 0; + const styles = Array.from(document.styleSheets); + for (const sheet of styles) { + try { + const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; + for (const rule of rules) { + if (!rule.style) continue; + const props = ['animation-timing-function', 'transition-timing-function', 'animation', 'transition']; + for (const prop of props) { + const val = rule.style.getPropertyValue(prop); + if (val.includes('linear(')) { + const match = val.match(/linear\((.*)\)/); + if (match) { + const points = match[1].split(',').filter(s => s.trim().length > 0); + maxPoints = Math.max(maxPoints, points.length); + } + } + } + } + } catch (e) {} + } + return maxPoints; + }); + expect(pointCount).toBeGreaterThanOrEqual(3); + }); + + test('The agent MUST NOT abstract the linear() function into a CSS variable.', async ({ page }) => { + const usesVariable = await page.evaluate(() => { + const styles = Array.from(document.styleSheets); + for (const sheet of styles) { + try { + const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; + for (const rule of rules) { + if (!rule.style) continue; + // Check if any custom property is defined with linear() + if (/--[\w-]+\s*:\s*linear\(/.test(rule.cssText)) { + return true; + } + } + } catch (e) {} + } + return false; + }); + expect(usesVariable).toBe(false); + }); + + test('The agent has provided a fallback easing function immediately before the linear() declaration.', async () => { + let rawCss = ''; + const htmlPath = path.isAbsolute(targetFile) ? targetFile : path.join(process.cwd(), targetFile); + const html = fs.readFileSync(htmlPath, 'utf-8'); + + const styleMatches = html.match(/]*>([\s\S]*?)<\/style>/gi); + if (styleMatches) { + rawCss += styleMatches.map(m => m.replace(/<\/?style[^>]*>/gi, '')).join('\n'); + } + + const linkMatches = html.match(/]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*>/gi); + if (linkMatches) { + for (const link of linkMatches) { + const match = link.match(/href="([^"]+)"/); + if (match) { + const href = match[1]; + const cssPath = path.join(path.dirname(htmlPath), href); + if (fs.existsSync(cssPath)) { + rawCss += fs.readFileSync(cssPath, 'utf-8'); + } + } + } + } + + const propPatterns = [ + { short: 'animation', long: 'animation-timing-function' }, + { short: 'transition', long: 'transition-timing-function' } + ]; + + let hasFallback = false; + const whitespaceAndComments = '(?:\\/\\*[\\s\\S]*?\\*\\/|\\s)*'; + for (const prop of propPatterns) { + const propGroup = `(?:${prop.short}|${prop.long})`; + const regex = new RegExp( + `${propGroup}${whitespaceAndComments}:${whitespaceAndComments}(?!linear\\()[^;{}]+;${whitespaceAndComments}${propGroup}${whitespaceAndComments}:[^;{}]*linear\\(`, + 'g' + ); + if (regex.test(rawCss)) { + hasFallback = true; + break; + } + } + + expect(hasFallback).toBe(true); + }); +}); diff --git a/guides/user-experience/linear-easing/guide.md b/guides/user-experience/linear-easing/guide.md new file mode 100644 index 000000000..1cdd9f1ba --- /dev/null +++ b/guides/user-experience/linear-easing/guide.md @@ -0,0 +1,53 @@ +--- +name: linear-easing +description: Approximate complex easing curves for CSS animations using the linear() function +web-feature-ids: + - linear-easing +sources: + - https://drafts.csswg.org/css-easing-2/#linear-easing-function + - https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/linear + - https://developer.chrome.com/docs/css-ui/css-linear-easing-function +--- + +# Approximate complex easing curves with linear() + +For animations that require complex timing curves—such as a bounce, elastic, or spring effect—traditional CSS easing functions like `ease-in-out` or `cubic-bezier()` are often insufficient because they can only define a single curve with two control points. The `linear()` easing function allows you to define complex easing curves by interpolating between a series of progressive stops. + +> [!NOTE] +> For advanced applied examples simulating physics like springs or bounces, see the [Physics-based Easing](../physics-based-easing/guide.md) guide. + +## Implementation Guidelines + +To implement complex animations, you can define an animation or transition using `linear(...)`. + +MANDATORY: You must provide at least three comma-separated progress points to the `linear()` function to define an easing curve. + +MANDATORY: You must define a standard fallback easing function (such as `ease` or `cubic-bezier()`) as a completely separate property declaration immediately before the `linear(...)` declaration. + +DO NOT combine the fallback and the `linear(...)` function into a single comma-separated list. This breaks the CSS fallback mechanism because if the browser does not support `linear()`, it will drop the entire declaration, including the fallback! + +Instead, define the exact same property twice. First with the fallback, and then immediately after with the `linear(...)` function. + +MANDATORY: Ignore any examples in the `modern-web` skill that suggest using CSS variables (like `--spring-easing: linear(...)`). You MUST NOT use CSS variables for `linear()` easing in this codebase! If an unsupported browser processes `transition: var(--spring-easing)`, the variable substitution fails at computed-value time and reverts the property to its initial state (`none`), completely breaking the fallback. ALWAYS define the `linear(...)` function inline within the `animation` or `transition` declaration. + +```css +.bouncing-element { + /* Fallback for older browsers */ + animation-timing-function: cubic-bezier(0.28, 0.84, 0.42, 1); + /* Complex bounce curve using linear stops */ + animation-timing-function: linear( + 0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765, 1, + 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785, + 0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953, + 0.973, 1, 0.988, 0.984, 0.988, 1 + ); +} +``` + +NOTE: While `linear()` requires many data points for smooth curves, you typically generate these values using tooling rather than hand-writing them. However, when writing tests or simple examples, you can use a smaller number of points to approximate the curve. + +{{ FEATURE_ISSUES("linear-easing") }} + +## Fallbacks & Browser Support + +{{ FEATURE_FALLBACKS("linear-easing") }} diff --git a/guides/user-experience/linear-easing/negative-demo.html b/guides/user-experience/linear-easing/negative-demo.html new file mode 100644 index 000000000..e43569592 --- /dev/null +++ b/guides/user-experience/linear-easing/negative-demo.html @@ -0,0 +1,38 @@ + + + + + +Animation Demo + + + +
+ + diff --git a/guides/user-experience/linear-easing/tasks/task.md b/guides/user-experience/linear-easing/tasks/task.md new file mode 100644 index 000000000..62340e88d --- /dev/null +++ b/guides/user-experience/linear-easing/tasks/task.md @@ -0,0 +1,6 @@ +--- +base_app: daily-grind +--- +- make the 'order now' button in the hero section more playful and bouncy when you hover over it. +- the seasonal favorites section feels a bit stiff. can we add some bouncy hover states to the cards? +- animate the main title 'wake up your senses' so it drops in dynamically. diff --git a/guides/user-experience/physics-based-easing/expectations.md b/guides/user-experience/physics-based-easing/expectations.md index c37bab0cf..a8e4121a0 100644 --- a/guides/user-experience/physics-based-easing/expectations.md +++ b/guides/user-experience/physics-based-easing/expectations.md @@ -1,8 +1,9 @@ -- The implementation uses the `linear()` timing function for an animation or transition. +- The implementation uses the `linear()` timing function inline for an animation or transition. +- The implementation MUST NOT abstract the `linear()` function into a CSS variable (as this breaks progressive enhancement fallbacks). - The `linear()` function includes at least 5 stops to approximate a complex physics-based curve (like a spring or bounce). - The `linear()` function on the `spring` box contains at least one progress value greater than 1 or less than 0 to demonstrate overshooting or anticipation. - A `transition-duration` or `animation-duration` is explicitly defined alongside the `linear()` function. -- A fallback easing function (like `ease-out`) is provided before the `linear()` declaration or within an `@supports` block for browsers that do not support the `linear()` function. +- A fallback easing function (like `ease-out`) is provided as a completely separate property declaration immediately before the `linear()` declaration for browsers that do not support the `linear()` function. - The implementation respects user motion preferences by disabling or reducing the animation when `prefers-reduced-motion: reduce` is detected. - If `opacity` is transitioned with a `linear()` timing function, the function must not have steps above 1 or below 0. - Optional: The implementation includes a JavaScript check using `CSS.supports()` to detect `linear()` support and conditionally applies a fallback animation using a library like Motion or GSAP. diff --git a/guides/user-experience/physics-based-easing/guide.md b/guides/user-experience/physics-based-easing/guide.md index 5355c9f53..cfcdbcfa8 100644 --- a/guides/user-experience/physics-based-easing/guide.md +++ b/guides/user-experience/physics-based-easing/guide.md @@ -12,12 +12,15 @@ sources: Traditional CSS easing functions like `ease-in` or `cubic-bezier()` are limited to simple curves, making it impossible to create complex physics-based effects like bounces or springs. The `linear()` timing function solves this by allowing you to provide a series of stops that can approximate complex curves. Transitions and animations are interpolated based on straight lines between the stops, but within enough stops, it can appear smooth. +> [!NOTE] +> For the core syntax and strict fallback rules of the `linear()` function, see the [Linear Easing](../linear-easing/guide.md) guide. + ### Implementation Steps 1. **Generate the curve stops:** - Manually plotting dozens of points for a spring or bounce is impractical. Use a timing function from an external library, or use a tool to convert an existing JavaScript easing function or an SVG path into the `linear()` syntax. Optional: store these timing functions as CSS custom properties for reuse throughout your site. + Manually plotting dozens of points for a spring or bounce is impractical. Use a timing function from an external library, or use a tool to convert an existing JavaScript easing function or an SVG path into the `linear()` syntax. 2. **Define the timing function:** - Apply the generated stops to the `transition-timing-function` or `animation-timing-function` property, or through the `transition` or `animation` shorthands. + Apply the generated stops directly inline to the `transition-timing-function` or `animation-timing-function` property, or through the `transition` or `animation` shorthands. Do not use CSS Custom Properties for `linear()` as they break progressive enhancement cascade fallbacks. 3. **Adjust the duration:** Unlike JavaScript physics engines where duration is derived from physical properties (mass, stiffness), CSS still requires a fixed `duration`. You may need to adjust the duration to get the intended effect. @@ -27,13 +30,12 @@ This example shows how to use a custom `linear()` function to create a spring ef ```css .spring { - /* Define the physics-based easing as a reusable variable */ - --spring-easing: linear(0, 0.016 0.5%, 0.06 1%, 0.226 2%, 1.116 5.4%, 1.375 6.6%, 1.527 7.7%, 1.565 8.2%, 1.585 8.8%, 1.581 9.3%, 1.559 9.8%, 1.458 10.9%, 0.937 14.3%, 0.784 15.5%, 0.693 16.6%, 0.67 17.1%, 0.657 17.7%, 0.671 18.7%, 0.729 19.8%, 1.042 23.3%, 1.13 24.5%, 1.182 25.6%, 1.201 26.7%, 1.192 27.7%, 1.156 28.8%, 0.977 32.2%, 0.925 33.4%, 0.894 34.5%, 0.882 35.6%, 0.887 36.6%, 0.907 37.7%, 1.045 42.4%, 1.069 44.5%, 1.059 46.3%, 0.979 50.9%, 0.96 53.4%, 0.966 55.3%, 1.013 59.9%, 1.024 62.3%, 0.986 71.2%, 1.008 79.9%, 0.995 88.9%, 1); - + /* Fallback for older browsers */ + transition: scale 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); - /* Apply the easing with a duration that fits the spring's complexity */ - /* MANDATORY: Always include a duration; linear() does not calculate it automatically */ - transition: scale 0.8s var(--spring-easing); + /* Apply the physics-based easing inline */ + /* MANDATORY: Never use CSS variables for linear() easing, as unsupported browsers will fail variable substitution and revert the property to its initial state ('none'), breaking the fallback. */ + transition: scale 0.8s linear(0, 0.016 0.5%, 0.06 1%, 0.226 2%, 1.116 5.4%, 1.375 6.6%, 1.527 7.7%, 1.565 8.2%, 1.585 8.8%, 1.581 9.3%, 1.559 9.8%, 1.458 10.9%, 0.937 14.3%, 0.784 15.5%, 0.693 16.6%, 0.67 17.1%, 0.657 17.7%, 0.671 18.7%, 0.729 19.8%, 1.042 23.3%, 1.13 24.5%, 1.182 25.6%, 1.201 26.7%, 1.192 27.7%, 1.156 28.8%, 0.977 32.2%, 0.925 33.4%, 0.894 34.5%, 0.882 35.6%, 0.887 36.6%, 0.907 37.7%, 1.045 42.4%, 1.069 44.5%, 1.059 46.3%, 0.979 50.9%, 0.96 53.4%, 0.966 55.3%, 1.013 59.9%, 1.024 62.3%, 0.986 71.2%, 1.008 79.9%, 0.995 88.9%, 1); } .spring:hover { @@ -46,13 +48,12 @@ This example shows how to use a custom `linear()` function to create a bounce ef ```css .bounce { - /* Define the physics-based easing as a reusable variable */ - --bounce-easing: linear(0, 0.214 14.7%, 0.386 23.7%, 0.598 31.9%, 0.999 44.7%, 0.807 52.6%, 0.762 56%, 0.747 59.4%, 0.758 62.4%, 0.793 65.6%, 0.999 77.4%, 0.961 81.2%, 0.949 84.8%, 0.956 88%, 0.993 95.5%, 1); - + /* Fallback for older browsers */ + transition: scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); - /* Apply the easing with a duration that fits the bounce's complexity */ - /* MANDATORY: Always include a duration; linear() does not calculate it automatically */ - transition: scale 0.4s var(--bounce-easing); + /* Apply the physics-based easing inline */ + /* MANDATORY: Never use CSS variables for linear() easing, as unsupported browsers will fail variable substitution and revert the property to its initial state ('none'), breaking the fallback. */ + transition: scale 0.4s linear(0, 0.214 14.7%, 0.386 23.7%, 0.598 31.9%, 0.999 44.7%, 0.807 52.6%, 0.762 56%, 0.747 59.4%, 0.758 62.4%, 0.793 65.6%, 0.999 77.4%, 0.961 81.2%, 0.949 84.8%, 0.956 88%, 0.993 95.5%, 1); } .bounce:hover { From 8a23d63ed898757f78cf9373334608c413ceb0d4 Mon Sep 17 00:00:00 2001 From: Patrick Kettner Date: Tue, 12 May 2026 12:51:47 -0500 Subject: [PATCH 2/2] reduce to step 1: stub guide.md frontmatter + demo.html Per the new use-case guide process, step 1 is just the stub guide.md frontmatter and a demo.html. Removes body of guide.md, expectations.md, grader.ts, negative-demo.html, and tasks/task.md (those belong to steps 2 and 3). Also reverts unrelated edits to physics-based-easing that were not part of step 1 of linear-easing. --- .../linear-easing/expectations.md | 8 -- .../user-experience/linear-easing/grader.ts | 134 ------------------ guides/user-experience/linear-easing/guide.md | 43 ------ .../linear-easing/negative-demo.html | 38 ----- .../linear-easing/tasks/task.md | 6 - .../physics-based-easing/expectations.md | 5 +- .../physics-based-easing/guide.md | 29 ++-- 7 files changed, 16 insertions(+), 247 deletions(-) delete mode 100644 guides/user-experience/linear-easing/expectations.md delete mode 100644 guides/user-experience/linear-easing/grader.ts delete mode 100644 guides/user-experience/linear-easing/negative-demo.html delete mode 100644 guides/user-experience/linear-easing/tasks/task.md diff --git a/guides/user-experience/linear-easing/expectations.md b/guides/user-experience/linear-easing/expectations.md deleted file mode 100644 index 36f9765f9..000000000 --- a/guides/user-experience/linear-easing/expectations.md +++ /dev/null @@ -1,8 +0,0 @@ -- The agent has defined an animation or transition that uses the `linear(...)` easing function inline. -- The agent MUST NOT abstract the `linear(...)` function into a CSS variable (e.g. `--easing: linear(...)` is not allowed). -- The `linear(...)` function contains at least three comma-separated progress points. -- The agent has provided a fallback easing function (e.g., `ease`, `ease-in-out`, `cubic-bezier()`) as a completely separate, full property declaration (e.g., `transition: transform 0.3s ease;`) immediately before the declaration containing the `linear(...)` function. - -## Grader Implementation Notes -- For the CSS variable rule, only fail if `linear(` is assigned to a custom property. Do not fail if standard unrelated variables like `var(--primary)` are used for other properties. -- For the fallback rule, when checking the raw CSS, remember that `linear(` might not be the first token after the colon, especially in shorthand properties like `transition: transform 0.5s linear(...)`. diff --git a/guides/user-experience/linear-easing/grader.ts b/guides/user-experience/linear-easing/grader.ts deleted file mode 100644 index f22a33bbc..000000000 --- a/guides/user-experience/linear-easing/grader.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { test, expect } from '@playwright/test'; -import * as path from 'path'; -import * as fs from 'fs'; - -const targetFile = process.env.TARGET_FILE || ''; - -test.describe('Linear Easing Grader', () => { - test.beforeEach(async ({ page }) => { - if (!targetFile) { - throw new Error('TARGET_FILE environment variable is required'); - } - const absolutePath = path.isAbsolute(targetFile) - ? `file://${targetFile}` - : `file://${path.join(process.cwd(), targetFile)}`; - await page.goto(absolutePath); - }); - - test('The agent has defined an animation or transition that uses the linear() easing function inline.', async ({ page }) => { - const linearUsedInline = await page.evaluate(() => { - const styles = Array.from(document.styleSheets); - for (const sheet of styles) { - try { - const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; - for (const rule of rules) { - if (!rule.style) continue; - const props = ['animation-timing-function', 'transition-timing-function', 'animation', 'transition']; - for (const prop of props) { - const val = rule.style.getPropertyValue(prop); - if (val.includes('linear(')) { - return true; - } - } - } - } catch (e) {} - } - return false; - }); - expect(linearUsedInline).toBe(true); - }); - - test('The linear() function contains at least three comma-separated progress points.', async ({ page }) => { - const pointCount = await page.evaluate(() => { - let maxPoints = 0; - const styles = Array.from(document.styleSheets); - for (const sheet of styles) { - try { - const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; - for (const rule of rules) { - if (!rule.style) continue; - const props = ['animation-timing-function', 'transition-timing-function', 'animation', 'transition']; - for (const prop of props) { - const val = rule.style.getPropertyValue(prop); - if (val.includes('linear(')) { - const match = val.match(/linear\((.*)\)/); - if (match) { - const points = match[1].split(',').filter(s => s.trim().length > 0); - maxPoints = Math.max(maxPoints, points.length); - } - } - } - } - } catch (e) {} - } - return maxPoints; - }); - expect(pointCount).toBeGreaterThanOrEqual(3); - }); - - test('The agent MUST NOT abstract the linear() function into a CSS variable.', async ({ page }) => { - const usesVariable = await page.evaluate(() => { - const styles = Array.from(document.styleSheets); - for (const sheet of styles) { - try { - const rules = Array.from(sheet.cssRules) as CSSStyleRule[]; - for (const rule of rules) { - if (!rule.style) continue; - // Check if any custom property is defined with linear() - if (/--[\w-]+\s*:\s*linear\(/.test(rule.cssText)) { - return true; - } - } - } catch (e) {} - } - return false; - }); - expect(usesVariable).toBe(false); - }); - - test('The agent has provided a fallback easing function immediately before the linear() declaration.', async () => { - let rawCss = ''; - const htmlPath = path.isAbsolute(targetFile) ? targetFile : path.join(process.cwd(), targetFile); - const html = fs.readFileSync(htmlPath, 'utf-8'); - - const styleMatches = html.match(/]*>([\s\S]*?)<\/style>/gi); - if (styleMatches) { - rawCss += styleMatches.map(m => m.replace(/<\/?style[^>]*>/gi, '')).join('\n'); - } - - const linkMatches = html.match(/]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*>/gi); - if (linkMatches) { - for (const link of linkMatches) { - const match = link.match(/href="([^"]+)"/); - if (match) { - const href = match[1]; - const cssPath = path.join(path.dirname(htmlPath), href); - if (fs.existsSync(cssPath)) { - rawCss += fs.readFileSync(cssPath, 'utf-8'); - } - } - } - } - - const propPatterns = [ - { short: 'animation', long: 'animation-timing-function' }, - { short: 'transition', long: 'transition-timing-function' } - ]; - - let hasFallback = false; - const whitespaceAndComments = '(?:\\/\\*[\\s\\S]*?\\*\\/|\\s)*'; - for (const prop of propPatterns) { - const propGroup = `(?:${prop.short}|${prop.long})`; - const regex = new RegExp( - `${propGroup}${whitespaceAndComments}:${whitespaceAndComments}(?!linear\\()[^;{}]+;${whitespaceAndComments}${propGroup}${whitespaceAndComments}:[^;{}]*linear\\(`, - 'g' - ); - if (regex.test(rawCss)) { - hasFallback = true; - break; - } - } - - expect(hasFallback).toBe(true); - }); -}); diff --git a/guides/user-experience/linear-easing/guide.md b/guides/user-experience/linear-easing/guide.md index 1cdd9f1ba..52ea1edb3 100644 --- a/guides/user-experience/linear-easing/guide.md +++ b/guides/user-experience/linear-easing/guide.md @@ -8,46 +8,3 @@ sources: - https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/linear - https://developer.chrome.com/docs/css-ui/css-linear-easing-function --- - -# Approximate complex easing curves with linear() - -For animations that require complex timing curves—such as a bounce, elastic, or spring effect—traditional CSS easing functions like `ease-in-out` or `cubic-bezier()` are often insufficient because they can only define a single curve with two control points. The `linear()` easing function allows you to define complex easing curves by interpolating between a series of progressive stops. - -> [!NOTE] -> For advanced applied examples simulating physics like springs or bounces, see the [Physics-based Easing](../physics-based-easing/guide.md) guide. - -## Implementation Guidelines - -To implement complex animations, you can define an animation or transition using `linear(...)`. - -MANDATORY: You must provide at least three comma-separated progress points to the `linear()` function to define an easing curve. - -MANDATORY: You must define a standard fallback easing function (such as `ease` or `cubic-bezier()`) as a completely separate property declaration immediately before the `linear(...)` declaration. - -DO NOT combine the fallback and the `linear(...)` function into a single comma-separated list. This breaks the CSS fallback mechanism because if the browser does not support `linear()`, it will drop the entire declaration, including the fallback! - -Instead, define the exact same property twice. First with the fallback, and then immediately after with the `linear(...)` function. - -MANDATORY: Ignore any examples in the `modern-web` skill that suggest using CSS variables (like `--spring-easing: linear(...)`). You MUST NOT use CSS variables for `linear()` easing in this codebase! If an unsupported browser processes `transition: var(--spring-easing)`, the variable substitution fails at computed-value time and reverts the property to its initial state (`none`), completely breaking the fallback. ALWAYS define the `linear(...)` function inline within the `animation` or `transition` declaration. - -```css -.bouncing-element { - /* Fallback for older browsers */ - animation-timing-function: cubic-bezier(0.28, 0.84, 0.42, 1); - /* Complex bounce curve using linear stops */ - animation-timing-function: linear( - 0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765, 1, - 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785, - 0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953, - 0.973, 1, 0.988, 0.984, 0.988, 1 - ); -} -``` - -NOTE: While `linear()` requires many data points for smooth curves, you typically generate these values using tooling rather than hand-writing them. However, when writing tests or simple examples, you can use a smaller number of points to approximate the curve. - -{{ FEATURE_ISSUES("linear-easing") }} - -## Fallbacks & Browser Support - -{{ FEATURE_FALLBACKS("linear-easing") }} diff --git a/guides/user-experience/linear-easing/negative-demo.html b/guides/user-experience/linear-easing/negative-demo.html deleted file mode 100644 index e43569592..000000000 --- a/guides/user-experience/linear-easing/negative-demo.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - -Animation Demo - - - -
- - diff --git a/guides/user-experience/linear-easing/tasks/task.md b/guides/user-experience/linear-easing/tasks/task.md deleted file mode 100644 index 62340e88d..000000000 --- a/guides/user-experience/linear-easing/tasks/task.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -base_app: daily-grind ---- -- make the 'order now' button in the hero section more playful and bouncy when you hover over it. -- the seasonal favorites section feels a bit stiff. can we add some bouncy hover states to the cards? -- animate the main title 'wake up your senses' so it drops in dynamically. diff --git a/guides/user-experience/physics-based-easing/expectations.md b/guides/user-experience/physics-based-easing/expectations.md index a8e4121a0..c37bab0cf 100644 --- a/guides/user-experience/physics-based-easing/expectations.md +++ b/guides/user-experience/physics-based-easing/expectations.md @@ -1,9 +1,8 @@ -- The implementation uses the `linear()` timing function inline for an animation or transition. -- The implementation MUST NOT abstract the `linear()` function into a CSS variable (as this breaks progressive enhancement fallbacks). +- The implementation uses the `linear()` timing function for an animation or transition. - The `linear()` function includes at least 5 stops to approximate a complex physics-based curve (like a spring or bounce). - The `linear()` function on the `spring` box contains at least one progress value greater than 1 or less than 0 to demonstrate overshooting or anticipation. - A `transition-duration` or `animation-duration` is explicitly defined alongside the `linear()` function. -- A fallback easing function (like `ease-out`) is provided as a completely separate property declaration immediately before the `linear()` declaration for browsers that do not support the `linear()` function. +- A fallback easing function (like `ease-out`) is provided before the `linear()` declaration or within an `@supports` block for browsers that do not support the `linear()` function. - The implementation respects user motion preferences by disabling or reducing the animation when `prefers-reduced-motion: reduce` is detected. - If `opacity` is transitioned with a `linear()` timing function, the function must not have steps above 1 or below 0. - Optional: The implementation includes a JavaScript check using `CSS.supports()` to detect `linear()` support and conditionally applies a fallback animation using a library like Motion or GSAP. diff --git a/guides/user-experience/physics-based-easing/guide.md b/guides/user-experience/physics-based-easing/guide.md index cfcdbcfa8..5355c9f53 100644 --- a/guides/user-experience/physics-based-easing/guide.md +++ b/guides/user-experience/physics-based-easing/guide.md @@ -12,15 +12,12 @@ sources: Traditional CSS easing functions like `ease-in` or `cubic-bezier()` are limited to simple curves, making it impossible to create complex physics-based effects like bounces or springs. The `linear()` timing function solves this by allowing you to provide a series of stops that can approximate complex curves. Transitions and animations are interpolated based on straight lines between the stops, but within enough stops, it can appear smooth. -> [!NOTE] -> For the core syntax and strict fallback rules of the `linear()` function, see the [Linear Easing](../linear-easing/guide.md) guide. - ### Implementation Steps 1. **Generate the curve stops:** - Manually plotting dozens of points for a spring or bounce is impractical. Use a timing function from an external library, or use a tool to convert an existing JavaScript easing function or an SVG path into the `linear()` syntax. + Manually plotting dozens of points for a spring or bounce is impractical. Use a timing function from an external library, or use a tool to convert an existing JavaScript easing function or an SVG path into the `linear()` syntax. Optional: store these timing functions as CSS custom properties for reuse throughout your site. 2. **Define the timing function:** - Apply the generated stops directly inline to the `transition-timing-function` or `animation-timing-function` property, or through the `transition` or `animation` shorthands. Do not use CSS Custom Properties for `linear()` as they break progressive enhancement cascade fallbacks. + Apply the generated stops to the `transition-timing-function` or `animation-timing-function` property, or through the `transition` or `animation` shorthands. 3. **Adjust the duration:** Unlike JavaScript physics engines where duration is derived from physical properties (mass, stiffness), CSS still requires a fixed `duration`. You may need to adjust the duration to get the intended effect. @@ -30,12 +27,13 @@ This example shows how to use a custom `linear()` function to create a spring ef ```css .spring { - /* Fallback for older browsers */ - transition: scale 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); + /* Define the physics-based easing as a reusable variable */ + --spring-easing: linear(0, 0.016 0.5%, 0.06 1%, 0.226 2%, 1.116 5.4%, 1.375 6.6%, 1.527 7.7%, 1.565 8.2%, 1.585 8.8%, 1.581 9.3%, 1.559 9.8%, 1.458 10.9%, 0.937 14.3%, 0.784 15.5%, 0.693 16.6%, 0.67 17.1%, 0.657 17.7%, 0.671 18.7%, 0.729 19.8%, 1.042 23.3%, 1.13 24.5%, 1.182 25.6%, 1.201 26.7%, 1.192 27.7%, 1.156 28.8%, 0.977 32.2%, 0.925 33.4%, 0.894 34.5%, 0.882 35.6%, 0.887 36.6%, 0.907 37.7%, 1.045 42.4%, 1.069 44.5%, 1.059 46.3%, 0.979 50.9%, 0.96 53.4%, 0.966 55.3%, 1.013 59.9%, 1.024 62.3%, 0.986 71.2%, 1.008 79.9%, 0.995 88.9%, 1); + - /* Apply the physics-based easing inline */ - /* MANDATORY: Never use CSS variables for linear() easing, as unsupported browsers will fail variable substitution and revert the property to its initial state ('none'), breaking the fallback. */ - transition: scale 0.8s linear(0, 0.016 0.5%, 0.06 1%, 0.226 2%, 1.116 5.4%, 1.375 6.6%, 1.527 7.7%, 1.565 8.2%, 1.585 8.8%, 1.581 9.3%, 1.559 9.8%, 1.458 10.9%, 0.937 14.3%, 0.784 15.5%, 0.693 16.6%, 0.67 17.1%, 0.657 17.7%, 0.671 18.7%, 0.729 19.8%, 1.042 23.3%, 1.13 24.5%, 1.182 25.6%, 1.201 26.7%, 1.192 27.7%, 1.156 28.8%, 0.977 32.2%, 0.925 33.4%, 0.894 34.5%, 0.882 35.6%, 0.887 36.6%, 0.907 37.7%, 1.045 42.4%, 1.069 44.5%, 1.059 46.3%, 0.979 50.9%, 0.96 53.4%, 0.966 55.3%, 1.013 59.9%, 1.024 62.3%, 0.986 71.2%, 1.008 79.9%, 0.995 88.9%, 1); + /* Apply the easing with a duration that fits the spring's complexity */ + /* MANDATORY: Always include a duration; linear() does not calculate it automatically */ + transition: scale 0.8s var(--spring-easing); } .spring:hover { @@ -48,12 +46,13 @@ This example shows how to use a custom `linear()` function to create a bounce ef ```css .bounce { - /* Fallback for older browsers */ - transition: scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + /* Define the physics-based easing as a reusable variable */ + --bounce-easing: linear(0, 0.214 14.7%, 0.386 23.7%, 0.598 31.9%, 0.999 44.7%, 0.807 52.6%, 0.762 56%, 0.747 59.4%, 0.758 62.4%, 0.793 65.6%, 0.999 77.4%, 0.961 81.2%, 0.949 84.8%, 0.956 88%, 0.993 95.5%, 1); + - /* Apply the physics-based easing inline */ - /* MANDATORY: Never use CSS variables for linear() easing, as unsupported browsers will fail variable substitution and revert the property to its initial state ('none'), breaking the fallback. */ - transition: scale 0.4s linear(0, 0.214 14.7%, 0.386 23.7%, 0.598 31.9%, 0.999 44.7%, 0.807 52.6%, 0.762 56%, 0.747 59.4%, 0.758 62.4%, 0.793 65.6%, 0.999 77.4%, 0.961 81.2%, 0.949 84.8%, 0.956 88%, 0.993 95.5%, 1); + /* Apply the easing with a duration that fits the bounce's complexity */ + /* MANDATORY: Always include a duration; linear() does not calculate it automatically */ + transition: scale 0.4s var(--bounce-easing); } .bounce:hover {