Skip to content

Commit

Permalink
Update built-in view transitions (#8207)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
natemoo-re and sarah11918 committed Aug 24, 2023
1 parent c37632a commit e45f302
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 82 deletions.
11 changes: 11 additions & 0 deletions .changeset/five-geese-crash.md
@@ -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.
Expand Up @@ -32,7 +32,7 @@ const { link } = Astro.props as Props;
</script>
</head>
<body>
<header transition:animate="morph">
<header transition:animate="initial">
<h1>testing</h1>
</header>
<main transition:animate="slide">
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Expand Up @@ -119,7 +119,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^2.0.0",
"@astrojs/compiler": "^2.0.1",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/@types/astro.ts
Expand Up @@ -77,7 +77,7 @@ export interface TransitionDirectionalAnimations {
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
// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand All @@ -93,7 +93,7 @@ export interface AstroBuiltinAttributes {
'set:html'?: any;
'set:text'?: any;
'is:raw'?: boolean;
'transition:animate'?: 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
'transition:animate'?: TransitionAnimationValue;
'transition:name'?: string;
'transition:persist'?: boolean | string;
}
Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/compile/compile.ts
Expand Up @@ -45,8 +45,6 @@ export async function compile({
astroGlobalArgs: JSON.stringify(astroConfig.site),
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
experimentalTransitions: astroConfig.experimental.viewTransitions,
experimentalPersistence: astroConfig.experimental.viewTransitions,
transitionsAnimationURL: 'astro/components/viewtransitions.css',
preprocessStyle: createStylePreprocessor({
filename,
Expand Down
112 changes: 56 additions & 56 deletions packages/astro/src/runtime/server/transition.ts
Expand Up @@ -2,7 +2,6 @@ import type {
SSRResult,
TransitionAnimation,
TransitionAnimationValue,
TransitionDirectionalAnimations,
} from '../../@types/astro';
import { fade, slide } from '../../transitions/index.js';
import { markHTMLString } from './escape.js';
Expand All @@ -22,78 +21,80 @@ export function createTransitionScope(result: SSRResult, hash: string) {
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(
result: SSRResult,
hash: string,
animationName: TransitionAnimationValue | undefined,
transitionName: string
) {
let animations: TransitionDirectionalAnimations | null = null;
switch (animationName) {
case 'fade': {
animations = fade();
break;
}
case 'slide': {
animations = slide();
break;
}
default: {
if (typeof animationName === 'object') {
animations = animationName;
// Default to `fade` (similar to `initial`, but snappier)
if (!animationName) animationName = 'fade';
const scope = createTransitionScope(result, hash);
const name = transitionName ? toValidIdent(transitionName) : scope;
const sheet = new ViewTransitionStyleSheet(scope, name);

const animations = getAnimations(animationName);
if (animations) {
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]>) {
sheet.addAnimationPair(direction, image, rules);
}
}
} 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>`));
return scope;
}

// Default transition name is the scope of the element, ie HASH-1
if (!transitionName) {
transitionName = scope;
}
class ViewTransitionStyleSheet {
private modern: string[] = []
private fallback: string[] = []

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)}
}
constructor(private scope: string, private name: string) {}

::view-transition-new(${transitionName}) {
${stringifyAnimation(animations.forwards.new)}
}
[data-astro-transition-fallback=new] [data-astro-transition-scope="${scope}"] {
${stringifyAnimation(animations.forwards.new)}
}
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('')
}

[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)}
}
private layer(cssText: string) {
return cssText ? `@layer astro { ${cssText} }` : '';
}

[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()
private addRule(target: 'modern' | 'fallback', cssText: string) {
this[target].push(cssText);
}
</style>`);

result._metadata.extraHead.push(styles);
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} }`)
}

return scope;
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 = {
Expand Down Expand Up @@ -137,7 +138,6 @@ function stringifyAnimations(anims: TransitionAnimation[]): string {
const builder = animationBuilder();

for (const anim of anims) {
/*300ms cubic-bezier(0.4, 0, 0.2, 1) both astroSlideFromRight;*/
if (anim.duration) {
addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
}
Expand Down
34 changes: 18 additions & 16 deletions packages/astro/src/transitions/index.ts
@@ -1,5 +1,7 @@
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro';

const EASE_IN_OUT_QUART = 'cubic-bezier(0.76, 0, 0.24, 1)';

export function slide({
duration,
}: {
Expand All @@ -11,28 +13,28 @@ export function slide({
{
name: 'astroFadeOut',
duration: duration ?? '90ms',
easing: 'cubic-bezier(0.4, 0, 1, 1)',
easing: EASE_IN_OUT_QUART,
fillMode: 'both',
},
{
name: 'astroSlideToLeft',
duration: duration ?? '300ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
duration: duration ?? '220ms',
easing: EASE_IN_OUT_QUART,
fillMode: 'both',
},
],
new: [
{
name: 'astroFadeIn',
duration: duration ?? '210ms',
easing: 'cubic-bezier(0, 0, 0.2, 1)',
delay: '90ms',
easing: EASE_IN_OUT_QUART,
delay: duration ? undefined : '30ms',
fillMode: 'both',
},
{
name: 'astroSlideFromRight',
duration: duration ?? '300ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
duration: duration ?? '220ms',
easing: EASE_IN_OUT_QUART,
fillMode: 'both',
},
],
Expand All @@ -45,22 +47,22 @@ export function slide({
}

export function fade({
duration,
duration
}: {
duration?: string | number;
} = {}): TransitionDirectionalAnimations {
const anim = {
old: {
name: 'astroFadeInOut',
duration: duration ?? '0.2s',
easing: 'linear',
fillMode: 'forwards',
name: 'astroFadeOut',
duration: duration ?? 180,
easing: EASE_IN_OUT_QUART,
fillMode: 'both',
},
new: {
name: 'astroFadeInOut',
duration: duration ?? '0.3s',
easing: 'linear',
fillMode: 'backwards',
name: 'astroFadeIn',
duration: duration ?? 180,
easing: EASE_IN_OUT_QUART,
fillMode: 'both',
},
} satisfies TransitionAnimationPair;

Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e45f302

Please sign in to comment.