让cursor用CSS 动画实现电子书翻页效果

基于 Vue 2 和 CSS 动画实现电子书翻页效果

在本文中,我将介绍如何使用 Vue 2 和 CSS 动画创建一个具有翻页效果的电子书展示组件。

这个组件模拟了实体书的翻页体验,适用于展示宫崎骏作品集等书籍内容。

核心实现思路

该电子书组件主要利用 CSS 3D 变换和动画来实现翻页效果,并通过 Vue 的生命周期和 DOM 操作功能来动态构建书籍内容。

首先先让cursor进行分析

然后对其关键技术点进行解析

  1. CSS 3D 变换 :通过 perspectiverotateY 实现 3D 翻页效果,使页面翻转时具有空间感。

  2. CSS 动画 :使用 @keyframes 定义翻页动画,包括 turnForward(向前翻页)和 turnBackward(向后翻页),并配合 animation 属性应用到页面元素上。

/* 淡出动画 */ @keyframes fadeOut { 0% { visibility: visible; } 49% { visibility: visible; } 50% { visibility: hidden; } 100% { visibility: hidden; } } /* 淡入动画 */ @keyframes fadeIn { 0% { visibility: hidden; } 50% { visibility: hidden; } 51% { visibility: visible; } 100% { visibility: visible; } } @keyframes turnForward { 0% { z-index: 999; } 40% { z-index: 999; } 49% { transform: rotateY(-90deg); } 50% { transform: rotateY(-90deg); } 100% { transform: rotateY(-180deg); z-index: null; } } @keyframes turnBackward { 0% { z-index: 999; } 40% { z-index: 999; } 49% { transform: rotateY(-90deg); } 50% { transform: rotateY(-90deg); } 100% { transform: rotateY(0deg); z-index: null; } }

  1. Vue 生命周期 :在 mounted 阶段初始化书籍内容,此时 DOM 已经渲染完成,可以安全地进行 DOM 操作。

  2. DOM 操作封装 :将创建页面元素的逻辑抽取为独立方法(如 createPage),提高代码复用性和可读性。

  3. 事件处理 :为每个页面添加点击事件监听器,根据当前状态(isTurnIngshowPageNumber)决定是否执行翻页操作。

  4. z-index 动态计算:在翻页过程中动态调整页面的 z-index 值,确保翻页顺序正确。

代码实现

以下是完整的组件代码实现:

html 复制代码
<template>  
    <div class="book" ref="book"></div>  
</template>

<script>  
export default {  
    data() {  
        return {  
            isTurnIng: false, // 是否正在翻页  
            showPageNumber: 0, // 当前显示的页码  
            timer: null, // 动画定时器  
            pages: [ // 页面内容数据  
                {  
                    front: { title: "《千与千寻》", text: "内容1", background: "<https://i04piccdn.sogoucdn.com/6e02ef629da4a23c>" },  
                    back: { title: "《天空之城》", text: "内容2", background: "<https://i02piccdn.sogoucdn.com/a454ac48b1e33aec>" }  
                },  
                {  
                    front: { title: "《悬崖上的金鱼姬》", text: "内容3", background: "<https://i04piccdn.sogoucdn.com/c93cbdfb64b1e1e7>" },  
                    back: { title: "《魔女宅急便》", text: "内容4", background: "<https://i02piccdn.sogoucdn.com/7c13d38533902d8b>" }  
                }  
            ],  
            covers: [ // 封面和封底数据  
                { front: { title: "『宫崎骏作品集』", background: "<http://5b0988e595225.cdn.sohucs.com/images/20190602/4de5e4720e294714adf3f0f782c4f277.jpeg>" } },  
                { back: { title: "", background: "<https://i04piccdn.sogoucdn.com/a608bd742dbf3629>" } }  
            ]  
        }  
    },  
    mounted() {  
        this.initBook()  
    },  
    methods: {  
        // 初始化电子书  
        initBook() {  
            this.createCoverEnd()  
            this.createPages()  
            this.createCover()  
            this.bindPageEvents()  
        },

        // 创建封底  
        createCoverEnd() {  
            const coverEnd = document.createElement("span")  
            coverEnd.classList.add("cover", "coverEnd")  
            coverEnd.setAttribute("data-index", this.pages.length + 1)

            const backPage = this.createPage(this.covers[1].back, true)  
            backPage.classList.add("back-page")

            coverEnd.appendChild(backPage)  
            this.$[refs.book](http://refs.book).appendChild(coverEnd)  
        },

        // 创建书页  
        createPages() {  
            this.pages.reverse().forEach((page, index) => {  
                const pageElement = document.createElement("span")  
                pageElement.classList.add("page", "turn-page")  
                pageElement.setAttribute("data-index", this.pages.length - index)

                const frontPage = this.createPage(page.front)  
                frontPage.classList.add("front-page")

                const backPage = this.createPage(page.back)  
                backPage.classList.add("back-page")

                pageElement.appendChild(frontPage)  
                pageElement.appendChild(backPage)  
                this.$[refs.book](http://refs.book).appendChild(pageElement)  
            })  
        },

        // 创建封面  
        createCover() {  
            const cover = document.createElement("span")  
            cover.classList.add("cover")  
            cover.setAttribute("data-index", "0")

            const frontPage = this.createPage(this.covers[0].front, true)  
            frontPage.classList.add("front-page")

            cover.appendChild(frontPage)  
            this.$[refs.book](http://refs.book).appendChild(cover)  
        },

        // 创建页面元素  
        createPage(info = {}, isCover = false) {  
            const pageElement = document.createElement("div")  
            const prefix = isCover ? "cover" : "page"

            if (info.background) {  
                const img = document.createElement("img")  
                img.classList.add(`${prefix}-image`)  
                img.src = info.background  
                pageElement.appendChild(img)  
            }

            if (info.title) {  
                const titleElement = document.createElement("div")  
                titleElement.innerText = info.title  
                titleElement.classList.add(`${prefix}-title`)  
                pageElement.appendChild(titleElement)  
            }

            const textElement = document.createElement("div")  
            textElement.innerText = info.text || ""  
            textElement.classList.add(`${prefix}-text`)  
            pageElement.appendChild(textElement)

            return pageElement  
        },

        // 绑定页面点击事件  
        bindPageEvents() {  
            const root = document.documentElement  
            const duration = getComputedStyle(root).getPropertyValue("--animation-duration")

            const coverElements = this.$[refs.book](http://refs.book).querySelectorAll(".cover")  
            const pageElements = this.$[refs.book](http://refs.book).querySelectorAll(".page")

                ;[...coverElements, ...pageElements].forEach(page => {  
                    page.addEventListener("click", (e) => {  
                        this.pageClick(e, page)  
                    })  
                })  
        },

        // 计算 z-index  
        calZIndex(selector = ".turn") {  
            const turnedPages = document.querySelectorAll(selector)  
            return turnedPages.length + 1  
        },

        // 页面点击处理  
        pageClick(e, target) {  
            const index = parseInt(target.getAttribute("data-index"))

            if (![this.showPageNumber, this.showPageNumber - 1].includes(index)) {  
                return  
            }

            if (target.classList.contains("turn")) {  
                if (this.isTurnIng === "turnBack") return

                this.showPageNumber = index  
                this.isTurnIng = "turn"  
                [target.style](http://target.style).transform = "rotateY(-180deg)"  
                target.classList.remove("turn")  
                target.classList.add("turnBack")  
                [target.style](http://target.style).zIndex = null  
            } else {  
                if (this.isTurnIng === "turn") return

                this.showPageNumber = index + 1  
                this.isTurnIng = "turnBack"  
                [target.style](http://target.style).transform = "rotateY(0deg)"  
                target.classList.remove("turnBack")  
                target.classList.add("turn")  
                [target.style](http://target.style).zIndex = this.calZIndex()  
            }

            clearTimeout(this.timer)  
            this.timer = setTimeout(() => {  
                this.isTurnIng = false  
            }, parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--animation-duration")) * 1000)  
        }  
    }  
}  
</script>  
<style>  
:root {  
    --width: 32rem;  
    --height: 20rem;  
    --font-size: 1.2rem;  
    --animation-duration: 2s;  
}

body {  
    background-color: #4f4e68;  
    height: 100%;  
    overflow: hidden;  
}

.book {  
    position: absolute;  
    top: 0;  
    bottom: 0;  
    left: 0;  
    right: 0;  
    margin: auto;  
    width: var(--width);  
    height: var(--height);  
    perspective: 70rem;  
}

.coverEnd {  
    right: -0.5rem !important;  
}

.cover {  
    background-color: #36354e;  
    transform: rotateY(0deg);  
    width: calc(var(--width) / 2);  
    height: var(--height);  
    overflow: hidden;  
}

.cover-image {  
    width: 100%;  
    height: 100%;  
    object-fit: cover;  
    pointer-events: none;  
    position: absolute;  
    top: 0;  
    left: 0;  
    z-index: 0;  
    opacity: 0.8;  
}

.cover-title {  
    position: absolute;  
    top: 50%;  
    left: 50%;  
    transform: translate(-50%, -50%);  
    pointer-events: none;  
    background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00);  
    background-clip: text;  
    -webkit-background-clip: text;  
    color: transparent;  
    font-size: 2rem;  
    font-weight: bold;  
    text-align: center;  
    z-index: 1;  
    width: 100%;  
}

.page {  
    width: calc(var(--width) / 2 - 0.5rem);  
    height: calc(var(--height) - 0.5rem);  
    background-color: #e9e6c4;  
    transform: rotateY(0deg);  
    text-align: right;  
    font-size: var(--font-size);  
    color: #777;  
    font-family: monospace;  
    overflow: hidden;  
    text-indent: 2em;  
}

.page::after {  
    display: block;  
    content: "";  
    position: absolute;  
    top: calc(var(--font-size) * 2);  
    left: 1rem;  
    right: 1rem;  
    bottom: 1rem;  
    background-image: linear-gradient(to bottom,  
            currentColor 1px,  
            transparent 1px);  
    background-size: 100% calc(var(--font-size) + 0.5rem);  
    /* 调整行高 */  
    opacity: 1;  
    /* 调整透明度 */  
    pointer-events: none;  
    /* 让点击事件穿透伪元素 */  
}

.cover,  
.page {  
    position: absolute;  
    padding-top: 1rem;  
    transform-origin: 0 0;  
    border-radius: 5px 0 0 5px;  
    box-shadow: inset 3px 0px 20px rgba(0, 0, 0, 0.2),  
        0px 0px 15px rgba(0, 0, 0, 0.1);  
    box-sizing: border-box;  
    text-align: left;  
    right: 0;  
}

.page-title {  
    transform: translate(0, 0);  
    pointer-events: none;  
    background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00);  
    background-clip: text;  
    -webkit-background-clip: text;  
    color: transparent;  
    font-weight: bold;  
    text-align: center;  
    text-indent: 0;  
}

.page-text {  
    mix-blend-mode: hard-light;  
    color: #000000;  
    padding-left: 1em;  
    padding-right: 1em;  
    font-size: calc(var(--font-size) * 0.9);  
    line-height: calc(var(--font-size) + 0.5rem);  
    font-weight: bold;  
}

.page-image {  
    position: absolute;  
    top: 0;  
    left: 0;  
    width: 100%;  
    height: 100%;  
    object-fit: cover;  
    pointer-events: none;  
    opacity: 0.8;  
    z-index: 0;  
}

.turn {  
    animation: turnForward var(--animation-duration) forwards;  
}

.turnBack {  
    animation: turnBackward var(--animation-duration) forwards;  
}

.front-page,  
.back-page {  
    overflow: hidden;  
    position: absolute;  
    top: 0;  
    left: 0;  
    height: 100%;  
    width: 100%;  
    pointer-events: none;  
    right: 0;  
    bottom: 0;  
    margin: 0;  
    padding-top: 1rem;  
    z-index: 1;  
}

.back-page {  
    transform: rotateY(180deg);  
    visibility: hidden;  
}

.turn-page.turn>div.front-page {  
    animation: fadeOut var(--animation-duration) forwards;  
}

.turn-page.turnBack>div.front-page {  
    animation: fadeIn var(--animation-duration) forwards;  
}

.turn-page.turnBack>div.back-page {  
    animation: fadeOut var(--animation-duration) forwards;  
}

.turn-page.turn>div.back-page {  
    animation: fadeIn var(--animation-duration) forwards;  
}

/* 淡出动画 */  
@keyframes fadeOut {  
    0% {  
        visibility: visible;  
    }

    49% {  
        visibility: visible;  
    }

    50% {  
        visibility: hidden;  
    }

    100% {  
        visibility: hidden;  
    }  
}

/* 淡入动画 */  
@keyframes fadeIn {  
    0% {  
        visibility: hidden;  
    }

    50% {  
        visibility: hidden;  
    }

    51% {  
        visibility: visible;  
    }

    100% {  
        visibility: visible;  
    }  
}

@keyframes turnForward {  
    0% {  
        z-index: 999;  
    }

    40% {  
        z-index: 999;  
    }

    49% {  
        transform: rotateY(-90deg);  
    }

    50% {  
        transform: rotateY(-90deg);  
    }

    100% {  
        transform: rotateY(-180deg);  
        z-index: null;  
    }  
}

@keyframes turnBackward {  
    0% {  
        z-index: 999;  
    }

    40% {  
        z-index: 999;  
    }

    49% {  
        transform: rotateY(-90deg);  
    }

    50% {  
        transform: rotateY(-90deg);  
    }

    100% {  
        transform: rotateY(0deg);  
        z-index: null;  
    }  
}  
</style>  

使用场景

让cursor帮我们将其封装成组件

这个电子书组件就可以适用于多种场景,包括但不限于以下场景:

  • 在线阅读应用:为用户提供一个沉浸式的阅读体验
  • 作品展示:展示宫崎骏等著名导演的作品集
  • 教育平台:作为互动教材的一部分
  • 产品演示:展示产品特性或使用指南

总结

最终的效果

通过上述实现,我们成功创建了一个具有翻页效果的电子书组件。这个组件充分利用了 CSS 3D 变换和动画功能,同时结合 Vue 的组件化思想,实现了既美观又实用的交互效果。

代码结构清晰,易于维护和扩展,可以方便地应用于各种需要展示书籍内容的场景。

相关推荐
Sam_Deep_Thinking3 天前
在Windows 11上配置Cursor IDE进行Java开发
ai编程·cursor
SamDeepThinking5 天前
在Cursor里安装极其好用的Mysql Database Client 插件
ai编程·cursor
卡尔特斯5 天前
Cursor 自用习惯快速调整基础布局与配置
cursor
Sam_Deep_Thinking6 天前
在 Cursor IDE 中配置 SQLTools 连接 MySQL 数据库指南(Windows 11)
ai编程·cursor
SamDeepThinking6 天前
彻底让Cursor不要格式化Java代码
ai编程·cursor
SamDeepThinking6 天前
使用Cursor生成【财务对账系统】前后端代码
后端·ai编程·cursor
SamDeepThinking6 天前
在Windows 11上配置Cursor IDE进行Java开发
后端·ai编程·cursor
陈佬昔没带相机6 天前
告别Token焦虑!我是如何用最低消费玩转AI编程的
claude·cursor·trae
yaocheng的ai分身7 天前
Browser MCP扩展
cursor·mcp
转转技术团队7 天前
让AI成为你的编程助手:如何高效使用Cursor
后端·cursor