让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 的组件化思想,实现了既美观又实用的交互效果。

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

相关推荐
子昕17 小时前
突发!Cursor推出Ultra版:200刀一个月,Pro用户开始慌了
cursor
轻语呢喃17 小时前
用AI编程助手打造小游戏:从《谁是卧底》看Trae和Cursor的实战应用
cursor·trae
AryaNimbus19 小时前
“我 Cursor Pro 怎么用三天就没了?”——500 次额度的真相是这样
前端·cursor
Baihai_IDP1 天前
深度解析 Cursor(逐行解析系统提示词、分享高效制定 Cursor Rules 的技巧...)
人工智能·ai编程·cursor
OliverZ1 天前
使用 MCP Feedback Enhanced 减少 Cursor 请求次数
ai编程·cursor·mcp
前端日常开发1 天前
什么?cursor帮我完成微信公众号登录的接口开发全流程
cursor
飞哥数智坊1 天前
AI编程实战:Cursor+Claude4助力15分钟完成大屏开发
人工智能·claude·cursor
大飞码农2 天前
Cursor+千问3:教你如何利用提示词打造自己的AI教育卡片
ai编程·cursor
anyup2 天前
10000+ 个点位轻松展示,使用 Leaflet 实现地图海量标记点聚类
前端·数据可视化·cursor