- 如何使用painter实现一个海报编辑工具——以taro为例
文章开始前先做个简单的声明:这篇文章主要面向刚了解到 painter 的开发者,文中使用的框架、实现的方式只作为一种参考,并不一定是最佳实践。而使用 painter 能够做的扩展不止下文提到的这些能力,只是以下样例更为方便理解。欢迎各位酌情阅读。 自动态模版功能发布后,陆续有开发者开始尝试使用动态模版能力,我们也收集到了大家反馈的一些问题。这一系列文章的主要内容是从头开始实现一个简单的、基于 painter 动态模版能力的海报编辑工具。希望能通过这一过程,让大家了解为什么我们推出了动态模版能力,以及如何快速上手。同时在文中,也会统一回答一下关于动态模版使用的一些问题。文章中实现的编辑器代码,可以在https://github.com/Kujiale-Mobile/Taro-Painter-Demo/tree/2.x获取 先期准备 本次我们使用 2.2.15 版本的 taro 创建一个空项目 [代码]$ taro init [代码] painter 组件是使用了 mina-painter 包(https://www.npmjs.com/package/mina-painter),这是我们封装的 taro 风格组件,供 taro 1.x/2.x 版本使用,支持 base64 图片与 canvas2d 模式。 [代码]$ yarn add mina-painter [代码] 创建空页面,并引入 painter 组件。 [代码]// pages/index/index.tsx import Painter from 'mina-painter'; ... render() { return ( ... <Painter customStyle={`margin-top:5vh;`} customActionStyle={customActionStyle} dancePalette={danceTemplate} palette={outputTemplate} action={action} clearActionBox={clearActionBox} onImgOK={this.handleImgOk} onDidShow={this.handleDidShow} onTouchEnd={this.handleTouchEnd} onViewClicked={this.handleViewClick} onViewUpdate={this.handleViewUpdate} /> ... ) } [代码] 写一个简单的海报模版,包含 painter 内各种 view 类型。 [代码]// palette/index.ts const template = { width: '750rpx', height: '1334rpx', background: '#FFFFFF', views: [ { id: 'rect_10', type: 'rect', css: { scalable: true, color: '#F5F2EC', height: '348rpx', width: '750rpx', bottom: '0rpx', left: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_9', type: 'rect', css: { scalable: true, color: '#CBBD9F', height: '646rpx', width: '388rpx', left: '0rpx', top: '456rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_8', type: 'rect', css: { scalable: true, color: '#EBE5D7', height: '160rpx', width: '360rpx', top: '222rpx', right: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'qrcode', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053391518/3EF9BB7ABE024959EB2A0E81078B40FA.jpeg', css: { width: '202rpx', height: '202rpx', bottom: '76rpx', right: '40rpx', borderRadius: '8rpx', borderColor: '#FFFFFF', borderWidth: '4rpx', }, }, { id: 'worker_type', type: 'text', text: '门店店长', css: { scalable: true, deletable: true, left: '156rpx', bottom: '76rpx', fontSize: '24rpx', color: '#656c75', lineHeight: '34rpx', }, }, { id: 'worker_name', type: 'text', text: 'tester', css: { scalable: true, deletable: true, fontSize: '30rpx', fontWeight: 'bold', color: '#333', left: '156rpx', bottom: '114rpx', width: '280rpx', lineHeight: '42rpx', maxLines: 1, }, }, { id: 'avatar', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/png/1623053110600/BDA064C5ECDCB7DD50DEB466C70E2EB0.png', css: { width: '80rpx', height: '80rpx', borderRadius: '40rpx', left: '52rpx', bottom: '76rpx', }, }, { type: 'image', id: 'image-main', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053489433/54EE335A9C385A3D99D8664CB9135F84.jpg', css: { width: '672rpx', height: '672rpx', mode: 'aspectFill', right: '0rpx', top: '314rpx', scalable: true, minWidth: '120rpx', }, }, { type: 'rect', css: { width: '666rpx', height: '2rpx', top: '144rpx', right: '0rpx', color: '#EBEFF5', }, }, { id: 'name', type: 'text', text: '一个蒙着红色布的——球?', css: { scalable: true, deletable: true, fontSize: '32rpx', color: '#383c42', maxLines: 1, width: '480rpx', left: '76rpx', top: '74rpx', lineHeight: '44rpx', }, }, { id: 'product', type: 'text', text: '¥9999', css: { scalable: true, deletable: true, fontSize: '80rpx', lineHeight: '90rpx', fontWeight: 'bold', color: '#383C42', textAlign: 'center', left: '76rpx', top: '170rpx', }, }, ], } [代码] 以上种种准备好之后,我们就能得到这样的一个页面: [图片] 这个页面有最基础的点选、拖动能力,通过配置 view.css 的 scalable 和 deletable 属性,可以使用 painter 内置提供的缩放功能。 怎么样,是不是已经功能很完备,好像可以满足需求了啊~好,今天的分享就到此为止(并不是) [图片] 接下来,我们主要会为 text 、image 提供一些能力拓展,并实现基本的撤销、恢复功能。能力拓展的方式是相似的,相信在看完文章后,你就可以熟练地为任意 view 类型拓展能力了。 通过刷新整个 palette 方式进行的操作 [代码]// pages/index/index.tsx refreshPalette = (palette?: IPalette) => { this.setState({ dancePalette: palette || { ...this.currentPalette }, }); }; [代码] 最简单的刷新海报的方式就是直接刷新整个 palette 了,这种操作即便是不使用动态模版也一样可以用。这种方式开销大,速度相对慢,但是可以完全改变海报的结构 删除 View [图片] 虽然 painter 提供了自定义删除 icon 的方法,但是点击删除按钮,你会发现 view 并没有被删除。这是因为我们希望这种修改 palette 的操作能够让外部主动操作,而不是将删除操作也内置——那可能会导致你对自己写的海报模版失去掌控。要想实现删除逻辑非常简单,当用户点击删除按钮时,我们可以从 onTouchEnd 处监听到一个 type = ‘delete’ 的事件。 [图片] 从 palette 中找出对应的 view 并删除,然后更新 palette 就能完成删除操作了。 [代码]// pages/index/index.tsx this.currentPalette.views.splice(detail.index, 1); this.refreshPalette(); [代码] 修改背景 修改背景更为简单——直接改 palette 的 background 属性,然后刷新模版即可 [图片] [代码]// pages/index/index.tsx this.currentPalette.background = color; this.refreshPalette(); [代码] 添加新 View —— 以 text 为例 [图片][图片] 准备一个预先定义好样式的 text 类型的 view ,将输入内容填充后塞入模版的 views 中,最后刷新模版即可 [代码]// common/index.ts export function getBlankTextView(text?: string): IView { return { type: 'text', text: text || '', id: `text_${new Date().getTime()}${Math.ceil(Math.random() * 10)}`, css: { scalable: true, deletable: true, width: '384rpx', fontSize: '36rpx', color: '#000', textAlign: 'center', padding: '0 8rpx 8rpx 8rpx', top: '50%', left: '50%', align: 'center', verticalAlign: 'center', }, }; } // pages/index/index.tsx this.currentPalette.views.push(getBlankTextView(inputValue)); this.refreshPalette(); [代码] 通过刷新 action 方式进行的操作 [代码]// pages/index/index.tsx refreshSelectView = (view?: IView) => { this.setState({ action: { view: view || this.currentView }, }); }; [代码] painter 动态模版功能的一大改动就是增加了 action 属性。当我们向 action 传入一个 view ,painter 会去寻找与其匹配的 view 并刷新状态。通过这种方式,我们最小化了需要修改的内容,从而减少了 painter 所需要的渲染时间。 刷新选中view的样式——以 text 为例 通过监听 onViewClick 事件,我们能够获取当前点击的 view ,在确定当前 view 后,我们就可以通过修改改 view 的 css ,然后刷新 action 来修改样式了。具体表现如下: [图片][图片][图片][图片][图片][图片] [代码]// pages/index/index.tsx this.currentView.css = newCss; this.refreshSelectView(); [代码] 除了 text ,其他各类 view 也都可以做类似操作,比如修改 rect 的尺寸、修改图片链接、基于替换图片链接实现图片裁剪等等。这里只是抛砖引玉,欢迎大家向我们分享你做出了什么炫酷的功能。 同时使用上述两种方法实现撤销与恢复操作 上面介绍了两种刷新海报的方式,而接下来,我们实现一个简单的撤销与恢复功能。这个功能的核心没有什么特殊的,就是同时维持撤销栈和恢复栈两个栈,通过记录之前所做的操作,做反向操作。 [图片] [代码]// pages/index/index.tsx interface ITimeStackItem { view?: IView; palette?: IPalette; index?: number; type?: string; } pushToHistory = (item: ITimeStackItem) => { this.future.length = 0; while (this.history.length > 19) { this.history.shift(); } this.history.push(item); this.refreshTop(); }; handleTimeMachine = (type: 'revert' | 'recover') => { let popStack: ITimeStackItem[]; let pushStack: ITimeStackItem[]; if (type === 'revert') { popStack = this.history; pushStack = this.future; } else { pushStack = this.history; popStack = this.future; } const pre = popStack.pop(); if (!pre) { return; } if (pre.type === 'delete') { this.currentView = undefined; if (this.currentPalette.views[pre.index!] && this.currentPalette.views[pre.index!].id === pre.view!.id) { this.currentPalette.views.splice(pre.index!, 1); } else { this.currentPalette.views.splice(pre.index!, 0, pre.view!); } pushStack.push(pre); this.refreshPalette(); } else if (pre.palette) { pushStack.push({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); this.currentPalette = pre.palette; this.currentView = undefined; this.refreshPalette(); } else { for (let i = 0; i < this.currentPalette.views.length; i++) { if (this.currentPalette.views[i].id === pre.view!.id) { pushStack.push({ view: JSON.parse(JSON.stringify(this.currentPalette.views[i])), }); this.currentPalette.views[i] = pre.view!; this.currentView = this.currentPalette.views[i]; this.refreshSelectView(pre.view); break; } } } this.setState({ editState: this.currentView && this.currentView.type === 'text' ? EditState.TEXT : EditState.NORMAL, }); this.refreshTop(); }; [代码] 保存生成的海报 在操作动态模版时,是不会触发 onImgOk 的,因为动态模版的内容渲染在四个不同层级的 canvas 上,无法实时生成完善的海报图片,所以需要手动设置 palette 使用静态模版生成对应的海报 [代码]// pages/index/index.tsx this.setState({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); handleImgOk = path => { ... }; [代码] 总结 经过上述的一个流程,是不是对如何使用 painter 的动态模版有一些新的想法了呢?欢迎大家基于 painter 开发出更多有趣的功能并在评论区与我们分享。
2021-06-16 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26