Update built-in view transitions (#8207)
* feat: rename morph => initial * feat: update slide, fade animations, add none * chore: add changeset * fix: bump compiler * feat: disable root transition by default * chore: update changeset * chore: fix build * feat(transitions): crossfade => fade * feat(transitions): remove opinionated default * chore: update changeset * feat(transitions): set root to fade * feat: remove opinionated root style * chore: remove unused easings * feat: refactor transition logic, ensure defaults are wrapped in @layer * Update .changeset/five-geese-crash.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/five-geese-crash.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/five-geese-crash.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update five-geese-crash.md --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
c37632a20d
commit
e45f302934
8 changed files with 100 additions and 89 deletions
11
.changeset/five-geese-crash.md
Normal file
11
.changeset/five-geese-crash.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
'astro': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Change the [View Transition built-in animation](https://docs.astro.build/en/guides/view-transitions/#built-in-animation-directives) options.
|
||||||
|
|
||||||
|
The `transition:animate` value `morph` has been renamed to `initial`. Also, this is no longer the default animation.
|
||||||
|
|
||||||
|
If no `transition:animate` directive is specified, your animations will now default to `fade`.
|
||||||
|
|
||||||
|
Astro also supports a new `transition:animate` value, `none`. This value can be used on a page's `<html>` element to disable animated full-page transitions on an entire page.
|
|
@ -32,7 +32,7 @@ const { link } = Astro.props as Props;
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header transition:animate="morph">
|
<header transition:animate="initial">
|
||||||
<h1>testing</h1>
|
<h1>testing</h1>
|
||||||
</header>
|
</header>
|
||||||
<main transition:animate="slide">
|
<main transition:animate="slide">
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
"test:e2e:match": "playwright test -g"
|
"test:e2e:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.0.0",
|
"@astrojs/compiler": "^2.0.1",
|
||||||
"@astrojs/internal-helpers": "workspace:*",
|
"@astrojs/internal-helpers": "workspace:*",
|
||||||
"@astrojs/markdown-remark": "workspace:*",
|
"@astrojs/markdown-remark": "workspace:*",
|
||||||
"@astrojs/telemetry": "workspace:*",
|
"@astrojs/telemetry": "workspace:*",
|
||||||
|
|
|
@ -77,7 +77,7 @@ export interface TransitionDirectionalAnimations {
|
||||||
backwards: TransitionAnimationPair;
|
backwards: TransitionAnimationPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TransitionAnimationValue = 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
|
export type TransitionAnimationValue = 'initial' | 'slide' | 'fade' | 'none' | TransitionDirectionalAnimations;
|
||||||
|
|
||||||
// Allow users to extend this for astro-jsx.d.ts
|
// Allow users to extend this for astro-jsx.d.ts
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
@ -93,7 +93,7 @@ export interface AstroBuiltinAttributes {
|
||||||
'set:html'?: any;
|
'set:html'?: any;
|
||||||
'set:text'?: any;
|
'set:text'?: any;
|
||||||
'is:raw'?: boolean;
|
'is:raw'?: boolean;
|
||||||
'transition:animate'?: 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
|
'transition:animate'?: TransitionAnimationValue;
|
||||||
'transition:name'?: string;
|
'transition:name'?: string;
|
||||||
'transition:persist'?: boolean | string;
|
'transition:persist'?: boolean | string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,6 @@ export async function compile({
|
||||||
astroGlobalArgs: JSON.stringify(astroConfig.site),
|
astroGlobalArgs: JSON.stringify(astroConfig.site),
|
||||||
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
|
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
|
||||||
resultScopedSlot: true,
|
resultScopedSlot: true,
|
||||||
experimentalTransitions: astroConfig.experimental.viewTransitions,
|
|
||||||
experimentalPersistence: astroConfig.experimental.viewTransitions,
|
|
||||||
transitionsAnimationURL: 'astro/components/viewtransitions.css',
|
transitionsAnimationURL: 'astro/components/viewtransitions.css',
|
||||||
preprocessStyle: createStylePreprocessor({
|
preprocessStyle: createStylePreprocessor({
|
||||||
filename,
|
filename,
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type {
|
||||||
SSRResult,
|
SSRResult,
|
||||||
TransitionAnimation,
|
TransitionAnimation,
|
||||||
TransitionAnimationValue,
|
TransitionAnimationValue,
|
||||||
TransitionDirectionalAnimations,
|
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import { fade, slide } from '../../transitions/index.js';
|
import { fade, slide } from '../../transitions/index.js';
|
||||||
import { markHTMLString } from './escape.js';
|
import { markHTMLString } from './escape.js';
|
||||||
|
@ -22,80 +21,82 @@ export function createTransitionScope(result: SSRResult, hash: string) {
|
||||||
return `astro-${hash}-${num}`;
|
return `astro-${hash}-${num}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure animationName is a valid CSS identifier
|
||||||
|
function toValidIdent(name: string): string {
|
||||||
|
return name.replace(/[^a-zA-Z0-9\-\_]/g, '_').replace(/^\_+|\_+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entries<T extends Record<string, any>> = Iterable<[keyof T, T[keyof T]]>
|
||||||
|
|
||||||
|
const getAnimations = (name: TransitionAnimationValue) => {
|
||||||
|
if (name === 'fade') return fade();
|
||||||
|
if (name === 'slide') return slide();
|
||||||
|
if (typeof name === 'object') return name;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderTransition(
|
export function renderTransition(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
hash: string,
|
hash: string,
|
||||||
animationName: TransitionAnimationValue | undefined,
|
animationName: TransitionAnimationValue | undefined,
|
||||||
transitionName: string
|
transitionName: string
|
||||||
) {
|
) {
|
||||||
let animations: TransitionDirectionalAnimations | null = null;
|
// Default to `fade` (similar to `initial`, but snappier)
|
||||||
switch (animationName) {
|
if (!animationName) animationName = 'fade';
|
||||||
case 'fade': {
|
const scope = createTransitionScope(result, hash);
|
||||||
animations = fade();
|
const name = transitionName ? toValidIdent(transitionName) : scope;
|
||||||
break;
|
const sheet = new ViewTransitionStyleSheet(scope, name);
|
||||||
}
|
|
||||||
case 'slide': {
|
const animations = getAnimations(animationName);
|
||||||
animations = slide();
|
if (animations) {
|
||||||
break;
|
for (const [direction, images] of Object.entries(animations) as Entries<typeof animations>) {
|
||||||
}
|
for (const [image, rules] of Object.entries(images) as Entries<typeof animations[typeof direction]>) {
|
||||||
default: {
|
sheet.addAnimationPair(direction, image, rules);
|
||||||
if (typeof animationName === 'object') {
|
|
||||||
animations = animationName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (animationName === 'none') {
|
||||||
|
sheet.addAnimationRaw('old', 'animation: none; opacity: 0; mix-blend-mode: normal;')
|
||||||
|
sheet.addAnimationRaw('new', 'animation: none; mix-blend-mode: normal;')
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = createTransitionScope(result, hash);
|
result._metadata.extraHead.push(markHTMLString(`<style>${sheet.toString()}</style>`));
|
||||||
|
|
||||||
// Default transition name is the scope of the element, ie HASH-1
|
|
||||||
if (!transitionName) {
|
|
||||||
transitionName = scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = markHTMLString(`<style>[data-astro-transition-scope="${scope}"] {
|
|
||||||
view-transition-name: ${transitionName};
|
|
||||||
}
|
|
||||||
${
|
|
||||||
!animations
|
|
||||||
? ``
|
|
||||||
: // Regular animations
|
|
||||||
`
|
|
||||||
::view-transition-old(${transitionName}) {
|
|
||||||
${stringifyAnimation(animations.forwards.old)}
|
|
||||||
}
|
|
||||||
[data-astro-transition-fallback=old] [data-astro-transition-scope="${scope}"] {
|
|
||||||
${stringifyAnimation(animations.forwards.old)}
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-new(${transitionName}) {
|
|
||||||
${stringifyAnimation(animations.forwards.new)}
|
|
||||||
}
|
|
||||||
[data-astro-transition-fallback=new] [data-astro-transition-scope="${scope}"] {
|
|
||||||
${stringifyAnimation(animations.forwards.new)}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-astro-transition=back]::view-transition-old(${transitionName}) {
|
|
||||||
${stringifyAnimation(animations.backwards.old)}
|
|
||||||
}
|
|
||||||
[data-astro-transition=back][data-astro-transition-fallback=old] [data-astro-transition-scope="${scope}"] {
|
|
||||||
${stringifyAnimation(animations.backwards.old)}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-astro-transition=back]::view-transition-new(${transitionName}) {
|
|
||||||
${stringifyAnimation(animations.backwards.new)}
|
|
||||||
}
|
|
||||||
[data-astro-transition=back][data-astro-transition-fallback=new] [data-astro-transition-scope="${scope}"] {
|
|
||||||
${stringifyAnimation(animations.backwards.new)}
|
|
||||||
}
|
|
||||||
`.trim()
|
|
||||||
}
|
|
||||||
</style>`);
|
|
||||||
|
|
||||||
result._metadata.extraHead.push(styles);
|
|
||||||
|
|
||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ViewTransitionStyleSheet {
|
||||||
|
private modern: string[] = []
|
||||||
|
private fallback: string[] = []
|
||||||
|
|
||||||
|
constructor(private scope: string, private name: string) {}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const { scope, name } = this;
|
||||||
|
const [modern, fallback] = [this.modern, this.fallback].map(rules => rules.join(''));
|
||||||
|
return [`[data-astro-transition-scope="${scope}"] { view-transition-name: ${name}; }`, this.layer(modern), fallback].join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
private layer(cssText: string) {
|
||||||
|
return cssText ? `@layer astro { ${cssText} }` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRule(target: 'modern' | 'fallback', cssText: string) {
|
||||||
|
this[target].push(cssText);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAnimationRaw(image: 'old' | 'new' | 'group', animation: string) {
|
||||||
|
const { scope, name } = this;
|
||||||
|
this.addRule('modern', `::view-transition-${image}(${name}) { ${animation} }`)
|
||||||
|
this.addRule('fallback', `[data-astro-transition-fallback="${image}"] [data-astro-transition-scope="${scope}"] { ${animation} }`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addAnimationPair(direction: 'forwards' | 'backwards', image: 'old' | 'new', rules: TransitionAnimation | TransitionAnimation[]) {
|
||||||
|
const { scope, name } = this;
|
||||||
|
const animation = stringifyAnimation(rules);
|
||||||
|
const prefix = direction === 'backwards' ? `[data-astro-transition=back]` : '';
|
||||||
|
this.addRule('modern', `${prefix}::view-transition-${image}(${name}) { ${animation} }`)
|
||||||
|
this.addRule('fallback', `${prefix}[data-astro-transition-fallback="${image}"] [data-astro-transition-scope="${scope}"] { ${animation} }`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AnimationBuilder = {
|
type AnimationBuilder = {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
[key: string]: string[] | ((k: string) => string);
|
[key: string]: string[] | ((k: string) => string);
|
||||||
|
@ -137,7 +138,6 @@ function stringifyAnimations(anims: TransitionAnimation[]): string {
|
||||||
const builder = animationBuilder();
|
const builder = animationBuilder();
|
||||||
|
|
||||||
for (const anim of anims) {
|
for (const anim of anims) {
|
||||||
/*300ms cubic-bezier(0.4, 0, 0.2, 1) both astroSlideFromRight;*/
|
|
||||||
if (anim.duration) {
|
if (anim.duration) {
|
||||||
addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
|
addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro';
|
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro';
|
||||||
|
|
||||||
|
const EASE_IN_OUT_QUART = 'cubic-bezier(0.76, 0, 0.24, 1)';
|
||||||
|
|
||||||
export function slide({
|
export function slide({
|
||||||
duration,
|
duration,
|
||||||
}: {
|
}: {
|
||||||
|
@ -11,13 +13,13 @@ export function slide({
|
||||||
{
|
{
|
||||||
name: 'astroFadeOut',
|
name: 'astroFadeOut',
|
||||||
duration: duration ?? '90ms',
|
duration: duration ?? '90ms',
|
||||||
easing: 'cubic-bezier(0.4, 0, 1, 1)',
|
easing: EASE_IN_OUT_QUART,
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'astroSlideToLeft',
|
name: 'astroSlideToLeft',
|
||||||
duration: duration ?? '300ms',
|
duration: duration ?? '220ms',
|
||||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
easing: EASE_IN_OUT_QUART,
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -25,14 +27,14 @@ export function slide({
|
||||||
{
|
{
|
||||||
name: 'astroFadeIn',
|
name: 'astroFadeIn',
|
||||||
duration: duration ?? '210ms',
|
duration: duration ?? '210ms',
|
||||||
easing: 'cubic-bezier(0, 0, 0.2, 1)',
|
easing: EASE_IN_OUT_QUART,
|
||||||
delay: '90ms',
|
delay: duration ? undefined : '30ms',
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'astroSlideFromRight',
|
name: 'astroSlideFromRight',
|
||||||
duration: duration ?? '300ms',
|
duration: duration ?? '220ms',
|
||||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
easing: EASE_IN_OUT_QUART,
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -45,22 +47,22 @@ export function slide({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fade({
|
export function fade({
|
||||||
duration,
|
duration
|
||||||
}: {
|
}: {
|
||||||
duration?: string | number;
|
duration?: string | number;
|
||||||
} = {}): TransitionDirectionalAnimations {
|
} = {}): TransitionDirectionalAnimations {
|
||||||
const anim = {
|
const anim = {
|
||||||
old: {
|
old: {
|
||||||
name: 'astroFadeInOut',
|
name: 'astroFadeOut',
|
||||||
duration: duration ?? '0.2s',
|
duration: duration ?? 180,
|
||||||
easing: 'linear',
|
easing: EASE_IN_OUT_QUART,
|
||||||
fillMode: 'forwards',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
new: {
|
new: {
|
||||||
name: 'astroFadeInOut',
|
name: 'astroFadeIn',
|
||||||
duration: duration ?? '0.3s',
|
duration: duration ?? 180,
|
||||||
easing: 'linear',
|
easing: EASE_IN_OUT_QUART,
|
||||||
fillMode: 'backwards',
|
fillMode: 'both',
|
||||||
},
|
},
|
||||||
} satisfies TransitionAnimationPair;
|
} satisfies TransitionAnimationPair;
|
||||||
|
|
||||||
|
|
|
@ -483,8 +483,8 @@ importers:
|
||||||
packages/astro:
|
packages/astro:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler':
|
'@astrojs/compiler':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.1
|
||||||
version: 2.0.0
|
version: 2.0.1
|
||||||
'@astrojs/internal-helpers':
|
'@astrojs/internal-helpers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../internal-helpers
|
version: link:../internal-helpers
|
||||||
|
@ -5161,8 +5161,8 @@ packages:
|
||||||
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
|
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@astrojs/compiler@2.0.0:
|
/@astrojs/compiler@2.0.1:
|
||||||
resolution: {integrity: sha512-SKVWorXpOHff+OuZCd5kdTc5HxVX7bVXVXYP0jANT4crz7y2PdthUxMnE21iuYt4+Bq3aV5MId4OdgwlJ2/d/Q==}
|
resolution: {integrity: sha512-DfBR7Cf+tOgQ4n7TIgTtU5x5SEA/08DNshpEPcT+91A0KbBlmUOYMBM/O6qAaHkmVo1KIoXQYhAmfdTT1zx9PQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@astrojs/language-server@2.3.0(prettier-plugin-astro@0.12.0)(prettier@3.0.2)(typescript@5.1.6):
|
/@astrojs/language-server@2.3.0(prettier-plugin-astro@0.12.0)(prettier@3.0.2)(typescript@5.1.6):
|
||||||
|
|
Loading…
Reference in a new issue