Skip to content

Commit 99f7b20

Browse files
committed
New Animation demo, components and utility functions
1 parent 42b417e commit 99f7b20

File tree

12 files changed

+689
-2
lines changed

12 files changed

+689
-2
lines changed

.changeset/big-frogs-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"astro-vtbot": minor
3+
---
4+
5+
Provides tech demo and reusable components for new animation types

animations/AnimationStyle.astro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
import { getKeyframes, getStyles } from './animation-style';
3+
export interface Props {
4+
name: string;
5+
}
6+
const { name = '' } = Astro.props;
7+
const css = getKeyframes(name) + getStyles(name);
8+
---
9+
10+
<style set:text={css}></style>

animations/CreateAnimationScope.astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
export { createAnimationScope } from 'astro:transitions';
3+
---

animations/animation-style.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { TransitionDirectionalAnimations } from "astro";
2+
3+
4+
type Kebab<T extends string, A extends string = ''> = T extends `${infer F}${infer R}`
5+
? Kebab<R, `${A}${F extends Lowercase<F> ? '' : '-'}${Lowercase<F>}`>
6+
: A;
7+
type KebabKeys<T> = { [K in keyof T as K extends string ? Kebab<K> : K]: T[K] };
8+
type AnimationCSS = KebabKeys<Partial<CSSStyleDeclaration>>;
9+
export type NamedAnimationPairs = Record<string, { new?: AnimationCSS; old?: AnimationCSS; }>;
10+
export type ScopeAndStyles = {
11+
scope: string;
12+
styles: string;
13+
};
14+
15+
export const styleSheet = (transitionName: string, animations: NamedAnimationPairs): ScopeAndStyles => maybeScopedStyleSheet(transitionName, undefined, animations);
16+
17+
export const scopedStyleSheet = (transitionName: string, scope: string, animations: NamedAnimationPairs) => maybeScopedStyleSheet(transitionName, scope, animations).styles;
18+
19+
export function maybeScopedStyleSheet(transitionName: string, scope: string | undefined, animations: NamedAnimationPairs): ScopeAndStyles {
20+
scope ??= 'astro-' + Math.random().toString(36).slice(2, 8);
21+
const header = `[data-astro-transition-scope=${scope}] {view-transition-name: ${transitionName};}@layer astro {`;
22+
const closeLayer = '}';
23+
24+
const the = (dir: string) =>
25+
dir === 'forwards'
26+
? ''
27+
: dir === 'backwards'
28+
? "[data-astro-transition=back]"
29+
: `[data-astro-transition=${dir}`;
30+
31+
const pseudo = (image: string) => `::view-transition-${image}(${transitionName})`;
32+
33+
const style = (dir: string, image: string) =>
34+
Object.entries(animations[dir][image])
35+
.map(([k, v]) => `${k}: ${v}`)
36+
.join('; ');
37+
38+
const fallback = (dir: string, image: string, scope: string) =>
39+
`${the(
40+
dir
41+
)}[data-astro-transition-fallback=${image}][data-astro-transition-scope=${scope}], ${the(
42+
dir
43+
)}[data-astro-transition-fallback=${image}] [data-astro-transition-scope=${scope}]`;
44+
45+
let styles = header;
46+
const dirs = Object.keys(animations);
47+
styles = dirs.reduce((acc, dir) => acc + `${the(dir)}${pseudo('old')} {${style(dir, 'old')}}`, styles);
48+
styles = dirs.reduce((acc, dir) => acc + `${the(dir)}${pseudo('new')} {${style(dir, 'new')}}`, styles);
49+
styles += closeLayer;
50+
51+
styles = dirs.reduce((acc, dir) => acc + `${fallback(dir, 'old', scope)} {${style(dir, 'old')}}`, styles);
52+
styles = dirs.reduce((acc, dir) => acc + `${fallback(dir, 'new', scope)} {${style(dir, 'new')}}`, styles);
53+
54+
return { scope, styles };
55+
}
56+
const map = {};
57+
map["name"] = "animation-name";
58+
map["delay"] = "animation-delay";
59+
map["duration"] = "animation-duration";
60+
map["easing"] = "animation-timing-function";
61+
map["fillMode"] = "animation-fill-mode";
62+
map["direction"] = "animation-direction";
63+
const timeString = (value: number | string) => typeof value === 'number' ? value + 'ms' : value;
64+
65+
export const extend = (base: TransitionDirectionalAnimations, extension?: NamedAnimationPairs) => ({
66+
forwards: {
67+
old: Object.fromEntries([
68+
...Object.entries(base.forwards.old).map(([key, value]) => [map[key], timeString(value)]),
69+
...Object.entries(extension?.forwards?.old ?? {}),
70+
]),
71+
new: Object.fromEntries([
72+
...Object.entries(base.forwards.new).map(([key, value]) => [map[key], timeString(value)]),
73+
...Object.entries(extension?.forwards?.new ?? {}),
74+
]),
75+
},
76+
backwards: {
77+
old: Object.fromEntries([
78+
...Object.entries(base.backwards.old).map(([key, value]) => [map[key], value]),
79+
...Object.entries(extension?.backwards?.old ?? {}),
80+
]),
81+
new: Object.fromEntries([
82+
...Object.entries(base.backwards.new).map(([key, value]) => [map[key], value]),
83+
...Object.entries(extension?.backwards?.new ?? {}),
84+
]),
85+
},
86+
});
87+
88+
const framesMap = {};
89+
export function setKeyframes(name: string, css: string) {
90+
framesMap[name] = css;
91+
}
92+
export function getKeyframes(name: string) {
93+
return framesMap[name];
94+
}
95+
const stylesMap = {};
96+
export function setStyles(name: string, css: string) {
97+
stylesMap[name] = css;
98+
}
99+
export function getStyles(name: string) {
100+
return stylesMap[name];
101+
}

animations/swing.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@keyframes FwdSwingOut {
2+
from {
3+
transform: rotate3d(1, 0, 0, 0);
4+
opacity: 1;
5+
}
6+
to {
7+
transform: rotate3d(1, 0, 0, 90deg);
8+
opacity: 1;
9+
}
10+
}
11+
@keyframes FwdSwingIn {
12+
from {
13+
transform: rotate3d(1, 0, 0, -90deg);
14+
opacity: 1;
15+
}
16+
to {
17+
transform: rotate3d(1, 0, 0, 0);
18+
opacity: 1;
19+
}
20+
}
21+
@keyframes BwdSwingOut {
22+
from {
23+
transform: rotate3d(1, 0, 0, 0);
24+
opacity: 1;
25+
}
26+
to {
27+
transform: rotate3d(1, 0, 0, -90deg);
28+
opacity: 1;
29+
}
30+
}
31+
@keyframes BwdSwingIn {
32+
from {
33+
transform: rotate3d(1, 0, 0, 90deg);
34+
opacity: 1;
35+
}
36+
to {
37+
transform: rotate3d(1, 0, 0, 0);
38+
opacity: 1;
39+
}
40+
}

animations/swing.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import './swing.css';
2+
import { NamedAnimationPairs, extend, maybeScopedStyleSheet, setKeyframes, setStyles } from './animation-style';
3+
import type { TransitionAnimation, TransitionDirectionalAnimations } from 'astro';
4+
5+
type SwingKeyframeParameter = {
6+
axis?: { x?: number; y?: number; z?: number; };
7+
angle?: { grow?: string; shrink?: string; };
8+
midOpacity?: number;
9+
toOpacity?: number;
10+
};
11+
12+
type CustomSwingOptions = {
13+
keyframes?: SwingKeyframeParameter;
14+
base?: AnimationProperties;
15+
animations?: NamedAnimationPairs;
16+
};
17+
18+
19+
export const genKeyframes = (
20+
name: string,
21+
x: number = 0,
22+
y: number = 0,
23+
z: number = 0,
24+
growAngle = "90deg",
25+
shrinkAngle = "-90deg",
26+
midOpacity: number = 1,
27+
toOpacity: number = 0
28+
) =>
29+
`
30+
@keyframes ${name}FwdSwingOut {
31+
from {
32+
transform: rotate3d(${x}, ${y}, ${z}, 0);
33+
opacity: 1;
34+
}
35+
50% {
36+
opacity: ${midOpacity};
37+
}
38+
to {
39+
transform: rotate3d(${x}, ${y}, ${z}, ${growAngle});
40+
opacity: ${toOpacity};
41+
}
42+
}
43+
@keyframes ${name}FwdSwingIn {
44+
from {
45+
transform: rotate3d(${x}, ${y}, ${z}, ${shrinkAngle});
46+
opacity: ${toOpacity};
47+
}
48+
50% {
49+
opacity: ${midOpacity};
50+
}
51+
to {
52+
transform: rotate3d(${x}, ${y}, ${z}, 0);
53+
opacity: 1;
54+
}
55+
}
56+
@keyframes ${name}BwdSwingOut {
57+
from {
58+
transform: rotate3d(${x}, ${y}, ${z}, 0);
59+
opacity: 1;
60+
}
61+
50% {
62+
opacity: ${midOpacity};
63+
}
64+
to {
65+
transform: rotate3d(${x}, ${y}, ${z}, ${shrinkAngle});
66+
opacity: ${toOpacity};
67+
}
68+
}
69+
@keyframes ${name}BwdSwingIn {
70+
from {
71+
transform: rotate3d(${x}, ${y}, ${z}, ${growAngle});
72+
opacity: ${toOpacity};
73+
}
74+
50% {
75+
opacity: ${midOpacity};
76+
}
77+
to {
78+
transform: rotate3d(${x}, ${y}, ${z}, 0);
79+
opacity: 1;
80+
}
81+
}`;
82+
83+
export type AnimationProperties = Omit<TransitionAnimation, "name">;
84+
85+
export const swing = (animation?: AnimationProperties) => namedSwing('', animation);
86+
export const namedSwing = (name: string, animation?: AnimationProperties) => {
87+
const common = {
88+
easing: 'ease-in-out',
89+
fillMode: 'both',
90+
duration: '0.15s',
91+
...animation,
92+
};
93+
94+
const forwards = {
95+
old: { ...common, name: `${name}FwdSwingOut` },
96+
new: { delay: common.duration, ...common, name: `${name}FwdSwingIn` },
97+
};
98+
99+
const backwards = {
100+
old: { ...common, name: `${name}BwdSwingOut` },
101+
new: { delay: common.duration, ...common, name: `${name}BwdSwingIn` },
102+
};
103+
return { forwards, backwards } as TransitionDirectionalAnimations;
104+
};
105+
106+
107+
export const customSwing = (
108+
name: string,
109+
options: CustomSwingOptions,
110+
scope?: string
111+
) => {
112+
const { keyframes, base, animations: extensions } = options;
113+
const animations = extend(namedSwing(name, base), extensions ?? {});
114+
const axis = keyframes?.axis ?? { y: 1 };
115+
116+
setKeyframes(
117+
name,
118+
genKeyframes(name, axis?.x, axis?.y, axis?.z, keyframes?.angle?.grow, keyframes?.angle?.shrink, keyframes?.midOpacity, keyframes?.toOpacity)
119+
);
120+
121+
let { scope: finalScope, styles } = maybeScopedStyleSheet(name, scope, animations);
122+
setStyles(name, styles);
123+
return finalScope;
124+
};
125+

animations/zoom.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@keyframes FwdZoomOut {
2+
from {
3+
transform: scale(1);
4+
opacity: 1;
5+
}
6+
to {
7+
transform: scale(5);
8+
opacity: 0;
9+
}
10+
}
11+
@keyframes FwdZoomIn {
12+
from {
13+
transform: scale(0);
14+
opacity: 0;
15+
}
16+
to {
17+
transform: scale(1);
18+
opacity: 1;
19+
}
20+
}
21+
@keyframes BwdZoomOut {
22+
from {
23+
transform: scale(1);
24+
opacity: 1;
25+
}
26+
to {
27+
transform: scale(0);
28+
opacity: 0;
29+
}
30+
}
31+
@keyframes BwdZoomIn {
32+
from {
33+
transform: scale(5);
34+
opacity: 0;
35+
}
36+
to {
37+
transform: scale(1);
38+
opacity: 1;
39+
}
40+
}

0 commit comments

Comments
 (0)