发布于 2026-01-06 3 阅读
0

我们如何在 Appwrite Cloud Public Beta DEV's Worldwide Show and Tell Challenge Presented by Mux 中实现卡片动画:Pitch Your Projects!

我们如何在 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眼镜!

https://media3.giphy.com/media/Gjy7TJTRD3dixQNNB7/giphy.gif?cid=ecf05e471wtvkifjomcdhcyk6tmnz9yh28ep7zleqe3cj0cq&ep=v1_gifs_search&rid=giphy.gif&ct=g

这段标记代码的运行结果如下:

<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>
Enter fullscreen mode Exit fullscreen mode

您可以在这里预览

弹出卡片

我们希望用户点击卡片时,卡片能够弹出显示,方便他们仔细查看。弹出时,卡片还能旋转,这样更有趣🕺

我们将使用 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>
Enter fullscreen mode Exit fullscreen mode

您可以在这里预览结果

用光标旋转卡片

现在到了我最喜欢的部分,用光标旋转卡片!我们希望创造一种愉悦的体验,让用户不仅能弹出卡片,还能自由移动卡片,从而更好地控制卡片。

我们也会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>
Enter fullscreen mode Exit fullscreen mode

您可以在这里看到结果

强光

最后,我们还可以给卡片增加一些光泽,增强 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>
Enter fullscreen mode Exit fullscreen mode

您可以在这里查看结果

最终结果

最终呈现出的卡片动画流畅且视觉效果出色,为我们的仪表盘增添了一丝互动性。

我们希望这份详细的实现流程分解能对其他希望在 Web 应用中添加类似效果的开发者有所帮助。同时,我们也希望它能让大家感受到前端开发的魅力🪄

感谢您选择 Appwrite Cloud Beta 来满足您的云计算需求!

文章来源:https://dev.to/appwrite/how-we-implemented-the-card-animation-in-appwrite-cloud-public-beta-4npb