我们如何在 Appwrite Cloud 公测版中实现卡片动画
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
为了庆祝 Cloud 公开测试版的发布,我们希望打造一些独一无二的专属体验。因此,我们决定制作一张个性化贺卡,并融入动画和互动元素,力求为整个体验增添一份“特别”的感受。
Appwrite 控制台中的 Cloud Beta 卡片页面
当我们的设计团队向我们展示设计稿时,我们非常喜欢。但很快,现实就摆在了眼前,我意识到我必须实现这个设计。它需要美观、兼容多种浏览器,并且性能出色。这真是一个不小的挑战!
不过,凭借 Svelte 和 CSS 的强大功能,我们最终成功了,我很高兴能与大家分享我的经验。
灵感
我们这项功能的灵感来源于https://poke-holo.simey.me/,它为宝可梦卡牌提供了类似的动画效果。这个项目和 Appwrite 一样都是开源的,而且和我们的游戏主机一样,都是用 Svelte 框架构建的!这意味着我们可以从中学习到很多东西。
实施过程
最终的卡牌动画可以分解成多个部分。我们将逐一讲解其中的主要部分:
- 点击时“弹出”卡片
- 旋转卡片
- 卡片光泽
您可以在这里预览代码和输出:https://appwrite-card-snippets.vercel.app/
如果你感兴趣,还可以查看卡片元素的源代码,我们在那里整合了所有这些元素以及一些额外的细节✨
设置基本 HTML 结构
卡片需要正反两面。由于我们要制作3D动画,所以也会用到perspectiveCSS属性。记得带上你的3D眼镜!
这段标记代码的运行结果如下:
<div class="card">
<div class="card-inner">
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transition: transform 0.8s;
transform-style: preserve-3d;
}
/* Do an horizontal flip when you move the mouse over the flip box container */
.card:hover .card-inner {
transform: rotateY(180deg);
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
您可以在这里预览。
弹出卡片
我们希望用户点击卡片时,卡片能够弹出显示,方便他们仔细查看。弹出时,卡片还能旋转,这样更有趣🕺
我们将使用 Svelte 的springstore 来实现这个效果。每当我们将 store 的值设置为新值时,卡片会平滑过渡到新值,而不是立即改变。我们需要两个 store,scale分别用于控制卡片大小和rotateDelta控制旋转。
<script>
import { spring } from 'svelte/motion';
let active = true;
const smooth = { stiffness: 0.03, damping: 0.45 };
const scale = spring(1, smooth);
const rotateDelta = spring(0, smooth);
function popup() {
scale.set(1.45);
rotateDelta.set(360);
}
function retreat() {
scale.set(1);
rotateDelta.set(0);
}
$: if (active) {
popup();
} else {
retreat();
}
$: style = [`--scale: ${$scale}`, `--rotateDelta: ${$rotateDelta}deg`].join(';');
</script>
<div class="card" {style}>
<button class="card-inner" on:click={() => (active = !active)}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</button>
</div>
<style>
/* Button reset */
button {
background: none;
border: none;
padding: 0;
cursor: pointer;
outline: inherit;
}
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform: scale(var(--scale)) rotateY(var(--rotateDelta));
transform-style: preserve-3d;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
您可以在这里预览结果。
用光标旋转卡片
现在到了我最喜欢的部分,用光标旋转卡片!我们希望创造一种愉悦的体验,让用户不仅能弹出卡片,还能自由移动卡片,从而更好地控制卡片。
我们也会spring在这里使用商店,但这次我们只需要一个,rotate它有 x 轴和 y 轴。
<script lang="ts">
import { spring } from 'svelte/motion';
const smooth = { stiffness: 0.066, damping: 0.25 };
const rotate = spring({ x: 0, y: 0 }, smooth);
const round = (num: number, fix = 3) => parseFloat(num.toFixed(fix));
function getMousePosition(e: MouseEvent | TouchEvent) {
if ('touches' in e) {
return {
x: e?.touches?.[0]?.clientX,
y: e?.touches?.[0]?.clientY,
};
} else {
return {
x: e.clientX,
y: e.clientY,
};
}
}
const interact = (e: MouseEvent | TouchEvent) => {
const { x: clientX, y: clientY } = getMousePosition(e);
const el = e.target as HTMLElement;
const rect = el.getBoundingClientRect(); // get element's current size/position
const absolute = {
x: clientX - rect.left, // get mouse position from left
y: clientY - rect.top, // get mouse position from right
};
const center = {
x: round((100 / rect.width) * absolute.x) - 50,
y: round((100 / rect.height) * absolute.y) - 50,
};
rotate.set({
x: round(-(center.x / 3.5)),
y: round(center.y / 2),
});
};
const interactEnd = () => {
setTimeout(() => {
rotate.set({ x: 0, y: 0 });
}, 500);
};
$: style = [`--rotateX: ${$rotate.x}deg`, `--rotateY: ${$rotate.y}deg`].join(';');
</script>
<div class="card" {style}>
<div class="card-inner" on:pointermove={interact} on:mouseout={interactEnd} on:blur={interactEnd}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform-style: preserve-3d;
transform: rotateY(var(--rotateX)) rotateX(var(--rotateY));
transform-origin: center;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
</style>
您可以在这里看到结果。
强光
最后,我们还可以给卡片增加一些光泽,增强 3D 立体感。
<script lang="ts">
import { spring } from 'svelte/motion';
const smooth = { stiffness: 0.066, damping: 0.25 };
const glare = spring({ x: 0, y: 0, o: 0 }, smooth);
const round = (num: number, fix = 3) => parseFloat(num.toFixed(fix));
function getMousePosition(e: MouseEvent | TouchEvent) {
if ('touches' in e) {
return {
x: e?.touches?.[0]?.clientX,
y: e?.touches?.[0]?.clientY,
};
} else {
return {
x: e.clientX,
y: e.clientY,
};
}
}
const interact = (e: MouseEvent | TouchEvent) => {
const { x: clientX, y: clientY } = getMousePosition(e);
const el = e.target as HTMLElement;
const rect = el.getBoundingClientRect(); // get element's current size/position
const absolute = {
x: clientX - rect.left, // get mouse position from left
y: clientY - rect.top, // get mouse position from right
};
glare.set({
x: round((100 / rect.width) * absolute.x),
y: round((100 / rect.height) * absolute.y),
o: 1,
});
console.log(absolute, round((100 / rect.width) * absolute.x));
};
const interactEnd = () => {
setTimeout(() => {
glare.update((old) => ({ ...old, o: 0 }));
}, 500);
};
$: style = [`--glareX: ${$glare.x}%`, `--glareY: ${$glare.y}%`, `--glareO: ${$glare.o}`].join(
';'
);
</script>
<div class="card" {style}>
<div class="card-inner" on:pointermove={interact} on:mouseout={interactEnd} on:blur={interactEnd}>
<div class="card-back">
<img
src="https://cloud.appwrite.io/v1/cards/cloud-back?mock=normal"
alt="The back of the Card"
loading="lazy"
width="450"
height="274"
/>
</div>
<div class="card-front">
<img
src="https://cloud.appwrite.io/v1/cards/cloud?mock=normal"
alt="The front of the card"
width="450"
height="274"
/>
<div class="card-glare" />
</div>
</div>
</div>
<style>
.card {
perspective: 1000px;
}
.card-inner {
display: grid;
transform-style: preserve-3d;
transform: rotateY(var(--rotateX)) rotateX(var(--rotateY));
transform-origin: center;
}
/* Position the front and back side */
.card-front,
.card-back {
grid-area: 1/1;
backface-visibility: hidden;
}
.card-front {
display: grid;
}
.card-front > * {
grid-area: 1/1;
}
/* Rotate the back side 180 degrees */
.card-back {
transform: rotateY(180deg);
}
.card-glare {
border-radius: 14px;
transform: translateZ(1px);
z-index: 4;
background: radial-gradient(
farthest-corner circle at var(--glareX) var(--glareY),
rgba(255, 255, 255, 0.8) 10%,
rgba(255, 255, 255, 0.65) 20%,
rgba(0, 0, 0, 0.5) 90%
);
mix-blend-mode: overlay;
opacity: calc(var(--glareO) * 0.5);
}
</style>
您可以在这里查看结果。
最终结果
最终呈现出的卡片动画流畅且视觉效果出色,为我们的仪表盘增添了一丝互动性。
我们希望这份详细的实现流程分解能对其他希望在 Web 应用中添加类似效果的开发者有所帮助。同时,我们也希望它能让大家感受到前端开发的魅力🪄
感谢您选择 Appwrite Cloud Beta 来满足您的云计算需求!
文章来源:https://dev.to/appwrite/how-we-implemented-the-card-animation-in-appwrite-cloud-public-beta-4npb

