- 这些 Canvas 小技巧,保证你新年用得上
来自「微信开发者」公众号,作者为微信小程序技术研发工程师binnie。 本文主要介绍了3个隐藏的 Canvas 小技巧: - 绘制并生成图片 - Video 绘制 Canvas / webgl - 视频解码并绘制到 webgl - 录制并导出 webgl 视频 一键加滤镜 快速合成音视频 轻松挑选视频封面 …… Canvas 能够做这些? 作为资深的开发者,相信大家对 Canvas 都不陌生。这项能力在绘制图形方面发挥着极大的作用,高效支持图片编辑、数据可视化等应用场景。但是只局限于一般能力应用,那格局就小了。 Canvas 的应用场景非常丰富!赶紧往下看看这些隐藏的 Canvas 小技巧,保证你新年用得上!还有手把手教程以及文末彩蛋哟。 -- • 绘制并生成图片 • -- [图片] 示例:新年模板长按保存祝福 适用场景:图片分享海报 相关 API:RenderingContext/Canvas/wx.canvasToTempFilePath Step 1: 创建实例获取对象 创建 Canvas 实例,获取 CanvasRenderingContext2D 对象(Canvas 绘图上下文)来绘制形状、文本、图像等。 const query = wx.createSelectorQuery() let canvas = null query.select('#myCanvas') .fields({ node: true, size: true }) .exec((res) => { // 通过 wx.createSelectorQuery 获取到 canvas 实例 canvas = res[0].node // 通过 canvas.getContext('2d') 获取 CanvasRenderingContext2D 对象 const ctx = canvas.getContext('2d') }) Step 2: 设置宽高调整图片 获取 Canvas 绘图上下文后,将 Canvas 的宽高设置为节点宽高 * 设备像素比,绘制出来的图片更清晰 // 获取设备像素比 const dpr = wx.getSystemInfoSync().pixelRatio // 将 canvas 宽高设置为 canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr Step 3: 绘制内容 使用 CanvasRenderingContext2D 绘制,根据业务需要在画布中绘制头像、文字、背景等 // 矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, canvas.width , canvas.height ) // 图片 var image = canvas.createImage() himage.src = 'https://example.com/example.jpg' headImage.onload = (res) => { ctx.drawImage(himage 0, 0, 32, 32; } // 文本 ctx.font = "18px SimHei"; ctx.textAlgin = "left" ctx.fillStyle = "#07c160"; ctx.fillText("这是我的名字", 0, 0); Step 4: 生成并保存本地 使用 wx.canvasToTempFilePath 将画布生成图片,wx.saveImageToPhotosAlbum 将图片保存到本地。 wx.canvasToTempFilePath({ canvas: canvas, // canvas 实例 success(res) { // canvas 生成图片成功 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res) { // 保存成功 } }) } }) -- • Video 绘制 Canvas / webgl • -- [图片] 示例:视频文件绘制 Canvas 适用场景:制作 Video 滤镜、挑选 Video 封面等 相关 API:RenderingContext/Canvas Step 1: 获取实例 通过 wx.createSelectorQuery 获取 VideoContext 实例 let video = null wx.createSelectorQuery().select('#video').context(res => { // 通过 wx.createSelectorQuery 获取 VideoContext 实例 video = res.context; }) Step 2: 绘制内容 获取 VideoContext 实例后,将 VideoContext 传递给 Canvas 进行绘制。开发者根据业务需求选择绘制类型: Canvas 2d 写法:canvas.drawImage(video, ...)webgl 写法:gl.texImage2D(..., video) wx.createSelectorQuery().selectAll('#myCanvas,#webglCanvas').node(res => { const ctx = res[0].node.getContext('2d') const gl = res[1].node.getContext('webgl') setInterval(() => { // canvas 2d // 将 video 纹理对象传入 drawImage 进行绘制 ctx1.drawImage(video, 0, 0, w * dpr, h * dpr); // 添加一个蒙层 ctx1.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx1.fillRect(0, 0, w * dpr, h * dpr); // webgl const render = createRenderer(res[1].node, w, h) render(new Uint8Array(ctx1.getImageData(0, 0, w * dpr, h * dpr).data), w * dpr, h * dpr) }, 1000 / 24) }).exec() function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") ... return (arrayBuffer, width, height) => { ... // 指定二维纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } -- • 视频解码并绘制到 webgl • -- [图片] 示例:视频一键解码并绘制到 webgl 适用场景:添加特效、贴图等视频编辑场景 相关 API:wx.createVideoDecoder/VideoDecoder/RenderingContext/Canvas.requestAnimationFrame/wx.createMediaAudioPlayer/MediaAudioPlayer Step 1: 创建视频解码器进行解码 1. 调用 createVideoDecoder 对视频进行解码 2. 使用 videodecoder.start 启动解码,视频源文件不限制本地或远程路径 3. 通过 videodecoder.on('start', res => {}) 监听解码,通过 videodecoder.getFrameData() 获取到解码数据 // 获取视频解码器 getVideoDecoder(source, abortAudio) { return new Promise((resolve, reject) => { // 创建视频解码器 videodecoder = wx.createVideoDecoder() // 开始解码 videodecoder.start({ abortAudio: abortAudio, source: source, // 视频源文件,支持本地路径&远程路径 mode: 0 // 按pts解码,保证音画同步 }) // 监听解码 开始 videodecoder.on('start', res => { console.log('videodecoder start', res) // 状态初始化 isStop = false resolve(videodecoder) }) // 监听解码 结束 videodecoder.on('ended', res => { // 状态设置为结束,停止画面录制器 isStop = true }) }) }, Step 2: 解码数据绘制到 webgl 1. 通过 gl.texImage2D(..., image) 将解码数据绘制到 webgl 2. 使用 webgl.requestAnimationFrame 继续绘制,效果更加流畅 // 将解码数据绘制到 webgl 中 const query = wx.createSelectorQuery() query.select('#webglCanvas').node().exec((res) => { const webgl = res[0].node const requestAnimationFrame = webgl.requestAnimationFrame; // 初始化webgl let render = null if (!render) { render = createRenderer(webgl, 600, 400) } /** * 绘制视频帧到 canvas */ let i = 1 let loop = () => { // 解码结束,停止循环 if (isStop) { return } // 获取解码数据,绘制到 webgl 中 const imageData = videodecoder.getFrameData() if (imageData) { // render 的高宽需要设置为图片的宽高才可以绘制出来 render(new Uint8Array(imageData.data), imageData.width, imageData.height) } // 继续绘制 console.log('绘制帧数:', i++) requestAnimationFrame(loop) } // 启动录制循环 requestAnimationFrame(loop) }) Step 3: 添加音频播放器同步播放音频 完成 Step2 后,webgl 只有视频播放,缺少音频。因此使用 wx.createMediaAudioPlayer(),支持 addAudioSource 传入 videodecoder,保证视频帧渲染音画同步 /** * 创建媒体音频播放器 */ let mediaAudioPlayer = null let addAudio = () => { if (mediaAudioPlayer) return mediaAudioPlayer = wx.createMediaAudioPlayer() mediaAudioPlayer.start().then(() => { // 添加播放器音频来源 mediaAudioPlayer.addAudioSource(videodecoder).then(res => { console.log('add mediaAudioPlayer: ',) }) }) } // render 绘制视频同时添加音频 render(new Uint8Array(imageData.data), imageData.width, imageData.height) addAudio() -- • 录制并导出 webgl 视频 • -- [图片] 示例:录制并一键导出 webgl 视频 适用场景:将动画、编辑过的视频导出视频文件保存 相关 API:wx.createMediaRecorder/MediaRecorder/wx.createMediaContainer/MediaContainer/MediaTrack Step 1: 创建 webgl 画面录制器进行录制 通过 createMediaRecorder 创建页面录制器,并且绑定 webgl(建议离屏状态,效果更好)进行录制 /** * 获取画面录制器 */ getRecorder() { let canvas = this.getMainCanvasNode() let recorder = wx.createMediaRecorder(canvas, { fps: choosedVideoInfo.fps, // 实际视频的 fps videoBitsPerSecond: choosedVideoInfo.bitrate, // 实际视频的 bitrate gop: 12 }) // 监听录制事件 recorder.on("timeupdate", (res) => { console.log('recorder 录制中,当前时间:', res.currentTime) }) recorder.on("stop", (res) => { console.log('recorder停止') this.saveMedia(res.tempFilePath) }) // 开始录制 recorder.start() this.recorder = recorder return recorder }, // 初始化 画面录制器 并进行录制 await this.initRenderer() this.getDecoder().then((decoder) => { let recorder = this.getRecorder() var self = this function loop() { if (self.stopped) { return } let frameData = decoder.getFrameData() if (!frameData) { console.log('没取到帧') setTimeout(() => { loop() }, 1000/60) } else { self.renderFrame(frameData) recorder.requestFrame(() => { console.log('录制帧数:', i++) loop() }) } } loop() }) Step 2: 添加音频合成音视频 1. 通过 createMediaContainer 创建音视频处理容器来合成音视频 2. 通过 MediaContainer.extractDataSource 将视频源分离出视频轨道和音频轨道,将需要的轨道通过 MediaContainer.addTrack 添加到容器中 3. 通过 MediaContainer.export 导出即可获得合成后的视频文件 /** * 将视频和音频合到一起并保存到本地 * @param {*} videoTempFilePath */ saveMedia(videoTempFilePath) { const self = this let choosedFile = this.choosedFile const MediaContainer = wx.createMediaContainer() // webgl的取视频 MediaContainer.extractDataSource({ source: videoTempFilePath, success(res) { MediaContainer.addTrack(res.tracks[0]) // 源视频取音频 MediaContainer.extractDataSource({ source: choosedFile, success(res) { // 拿到音频轨道并加入到容器 res.tracks[0].kind == 'audio' && MediaContainer.addTrack(res.tracks[0]) res.tracks[1].kind == 'audio' && MediaContainer.addTrack(res.tracks[1]) // 合成视频并导出视频文件 MediaContainer.export({ success(res) { // 保存视频到本地 wx.saveVideoToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: '导出成功', icon: 'success', duration: 2000 }) self.destroy() } }) } }) } }) } }) }, -- •高效图像处理彩蛋 • -- 学会以上这些 Canvas 小技巧,还担心新年的美图美照美视频处理不过来?赶紧码下这个 Canvas 代码包,保证你就是家里最闪耀的靓女靓仔。 预祝大家新的一年 Canvas 在手,红包一直有!
2022-03-24 - Lottie-前端实现AE动效
项目背景 在海外项目中,为了优化用户体验加入了几处微交互动画,实现方式是设计输出合成的雪碧图,前端通过序列帧实现动画效果: [图片] 序列帧: [图片] 动画效果: [图片] 序列帧: [图片] 帧动画的缺点和局限性比较明显,合成的雪碧图文件大,且在不同屏幕分辨率下可能会失真。经调研发现,Lottie是个简单、高效且性能高的动画方案。 Lottie是可应用于Android, iOS, Web和Windows的库,通过Bodymovin解析AE动画,并导出可在移动端和web端渲染动画的json文件。换言之,设计师用AE把动画效果做出来,再用Bodymovin导出相应地json文件给到前端,前端使用Lottie库就可以实现动画效果。 [图片] Bodymovin插件的安装与使用 关闭AE 下载并安装ZXP installer https://aescripts.com/learn/zxp-installer/ 下载最新版bodymovin插件 https://github.com/airbnb/lottie-web/blob/master/build/extension/bodymovin.zxp 把下载好的bodymovin.zxp拖到ZXP installer [图片] 打开AE,在菜单首选项->常规中勾选☑️允许脚本写入文件和访问网络(否则输出JSON文件时会失败) [图片] 在AE中制作动画,打开菜单窗口->拓展->Bodymovin,勾选要输出的动画,并设置输出文件目录,点击render [图片] 打开输出目录会看到生成的JSON文件,若动画里导入了外部图片,则会在images中存放JSON中引用的图片 前端使用lottie 静态URL https://cdnjs.com/libraries/lottie-web NPM [代码]npm install lottie-web [代码] 调用loadAnimation [代码]lottie.loadAnimation({ container: element, // 容器节点 renderer: 'svg', loop: true, autoplay: true, path: 'data.json' // JSON文件路径 }); [代码] vue-lottie 也可以在vue中使用lottie [代码] import lottie from '../lib/lottie'; import * as favAnmData from '../../raw/fav.json'; export default { props: { options: { type: Object, required: true }, height: Number, width: Number, }, data () { return { style: { width: this.width ? `${this.width}px` : '100%', height: this.height ? `${this.height}px` : '100%', overflow: 'hidden', margin: '0 auto' } } }, mounted () { this.anim = lottie.loadAnimation({ container: this.$refs.lavContainer, renderer: 'svg', loop: this.options.loop !== false, autoplay: this.options.autoplay !== false, animationData: favAnmData, assetsPath: this.options.assetsPath, rendererSettings: this.options.rendererSettings } ); this.$emit('animCreated', this.anim) } } [代码] loadAnimation参数 参数名 描述 container 用于渲染动画的HTML元素,需确保在调用loadAnimation时该元素已存在 renderer 渲染器,可选值为’svg’(默认值)/‘canvas’/‘html’。svg支持的功能最多,但html的性能更好且支持3d图层。各选项值支持的功能列表在此 loop 默认值为true。可传递需要循环的特定次数 autoplay 自动播放 path JSON文件路径 animationData JSON数据,与path互斥 name 传递该参数后,可在之后通过lottie命令引用该动画实例 rendererSettings 可传递给renderer实例的特定设置,具体可看 Lottie动画监听 Lottie提供了用于监听动画执行情况的事件: complete loopComplete enterFrame segmentStart config_ready(初始配置完成) data_ready(所有动画数据加载完成) DOMLoaded(元素已添加到DOM节点) destroy 可使用addEventListener监听事件 [代码]// 动画播放完成触发 anm.addEventListener('complete', anmLoaded); // 当前循环播放完成触发 anm.addEventListener('loopComplete', anmComplete); // 播放一帧动画的时候触发 anm.addEventListener('enterFrame', enterFrame); [代码] 控制动画播放速度和进度 可使用anm.pause和anm.play暂停和播放动画,调用anm.stop则会停止动画播放并回到动画第一帧的画面。 使用anm.setSpeed(speed)可调节动画速度,而anm.goToAndStop(value, isFrame)和anm.goToAndPlay可控制播放特定帧数,也可结合anm.totalFrames控制进度百分比,比如可传anm.totalFrames - 1跳到最后一帧。 [代码]anm.goToAndStop(anm.totalFrames - 1, 1); [代码] 这样的好处是可以把相关联的JSON文件合并,通过anm.goToAndPlay控制动画状态的切换,如下图例中一个JSON文件包含了2个动画状态的数据: [图片] 图片资源 JSON文件里assets设置了对图片的引用: [图片] 若想统一修改静态资源路径或者设置成绝对路径,可在调用loadAnimation时传入assetsPath参数: [代码]lottie.loadAnimation({ container: element, renderer: 'svg', path: 'data.json', assetsPath: 'URL' // 静态资源绝对路径 }); [代码] 功能支持列表 即使用bodymovin成功输出了JSON文件(没有报错),也会出现动效不如预期的情况,比如这是在AE中构建的形象图: [图片] 但在页面中渲染效果是这样的: [图片] 这是因为使用了不支持的Merge Paths功能 [图片] 因此对设计师而言,创建Lottie动画和往常制作AE动画有所不同,此文档记录了Bodymovin支持输出的AE功能列表,动画制作前需跟设计师沟通好,根据动画加载平台来确认可使用的AE功能。 除此之外,尽量遵循官方文档里对设计过程的指导和建议: 动画简单化。创建动画时需时刻记着保持JSON文件的精简,比如尽可能地绑定父子关系,在相似的图层上复制相同的关键帧会增加额外的代码,尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得JSON文件变得非常大且耗性能。 建立形状图层。将AI、EPS、SVG和PDF等资源转换成形状图层否则无法在Lottie中正常使用,转换好后注意删除该资源以防被导出到JSON文件。 设置尺寸。在AE中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。 不使用表达式和特效。Lottie暂不支持。 注意遮罩尺寸。若使用alpha遮罩,遮照的大小会对性能产生很大的影响。尽可能地把遮罩尺寸维持到最小。 动画调试。若输出动画破损,通过每次导出特定图层来调试出哪些图层出了问题。然后在github中附上该图层文件提交问题,选择用其他方式重构该图层。 不使用混合模式和亮度蒙版。 不添加图层样式。 全屏动画。设置比想要支持的最宽屏幕更宽的导出尺寸。 设置空白对象。若使用空白对象,需确保勾选可见并设置透明度为0%否则不会被导出到JSON文件。 预览效果 由于以上所说的功能支持问题会导致输出动画效果不确定性,设计师和前端之间有个动画效果联调的过程,为了提高联调效率,设计师可先进行初步的效果预览,再把文件交付给前端。 方法1:输出预览HTML文件 渲染前设置所要渲染的文件 [图片] 勾选☑️Demo选项 [图片] 在输出的文件目录中就可找到可预览的demo.html文件 方法2:LottieFiles分享平台 把生成的JSON文件传到LottieFiles平台,可播放、暂停生成文件的动画效果,可设置图层颜色、动画速度,也可以下载lottie preview客户端在iOS或Android机子上预览。 [图片] LottieFiles平台还提供了很多线上公开的Lottie动画效果,可直接下载JSON文件使用 [图片] 交互hack Lottie的不足之处是没有对应的API操纵动画层,若想做更细化的动画处理,只能直接操作节点来实现。比如当播放完左图动画进入惊讶状态后,若想实现右图随鼠标移动而控制动画层的简单效果: [图片][图片] 开启调试面板可以看到,lottie-web通过使用<g>标签的transform属性来控制动画: [图片] 当元素已添加到DOM节点,找到想要控制的<g>标签,提取其transform属性的矩阵值,并使用rematrix解析矩阵值。 [代码]onIntroDone() { const Gs = this.refs.svg.querySelectorAll('svg > g > g > g'); Gs.forEach((node, i) => { // 过滤需要修改的节点 ... // 获取transform属性值 const styleArr = node.getAttribute('transform').split(','); styleArr[0] = styleArr[0].replace('matrix(', ''); styleArr[5] = styleArr[5].replace(')', ''); const style = `matrix(${styleArr[0]}, ${styleArr[1]}, ${styleArr[2]}, ${styleArr[3]}, ${styleArr[4]}, ${styleArr[5]})`; // 使用Rematrix解析 const transform = Rematrix.parse(style); this.matrices.push({ node, transform, prevTransform: transform }); } } [代码] 监听鼠标移动,设置新的transform属性值。 [代码]onMouseMove = (e) => { this.mouseCoords.x = e.clientX || e.pageX; this.mouseCoords.y = e.clientY || e.pageY; let x = this.mouseCoords.x - (this.props.browser.width / 2); let y = this.mouseCoords.y - (this.props.browser.height / 2); const diffX = (this.mouseCoords.prevX - x); const diffY = (this.mouseCoords.prevY - y); this.mouseCoords.prevX = x; this.mouseCoords.prevY = y; this.matrices.forEach((matrix, i) => { let translate = Rematrix.translate(diffX, diffY); const product = [matrix.prevTransform, translate].reduce(Rematrix.multiply); const css = `matrix(${product[0]}, ${product[1]}, ${product[4]}, ${product[5]}, ${product[12]}, ${product[13]})`; matrix.prevTransform = product; matrix.node.setAttribute('transform', css); }) } [代码] 进一步优化 看到一个方法,在AE中将图层命名为[代码]#id[代码]格式,生成的SVG相应的图层id会被设置为id,命名为[代码].class[代码]格式,相应的图层class会被设置为class [图片] 试了下的确可以,如下图,因此可通过这个方法快速找到需要操作的动画层,进一步简化代码: [图片] 小结 Lottie的缺点在于若在AE动画制作的过程不注意规范,会导致数据文件大、耗内存和性能的问题;Lottie-web的官方文档不够详尽,例如assetsPath参数是在看源码的时候发现的;开放的API不够齐全,无法很灵活地控制动画层。 而优点也很明显,Lottie能帮助提高开发效率,精简代码,易于调试和维护;资源文件小,输出动画效果保真;跨平台——Android, iOS, Web和Windows通用。 总的来说,Lottie的引用可以替代传统的GIF和帧动画,灵活利用好提供的属性和方法可以控制动画的播放,但需注意规范设计和开发的流程,才可以更高效地完成动画的制作与调试。
2019-03-25 - 每日出击运签小程序“踩坑”总结
最近刚好接手支持了每日出击运签的小程序,在小程序完成以后,整理了一下初次接手小程序的一些体验,所以有了现在这篇小总结。在小程序需求的进行期间,十分感谢泽贤,小苏,俞焕,花花的指导~ 话不多说,立马送上小程序码,大家可以扫码体验一下 [图片] 每日出击运签小程序主要划分为几个功能模块: 摇一摇 抽签并显示运签结果 保存图片 预约功能 签到功能 积分功能 评论功能 下面重点介绍一下摇一摇、抽签并显示运签结果、保存图片三个功能。 关于摇一摇功能 花叔早前已经写过文章(链接如下:http://www.ifanr.com/minapp/880378)介绍过摇一摇的实现思路了,这里简单归纳一下摇一摇功能的几个主要的思路。 准备: 需要设置一组变量,保存摇一摇x,y,z三轴的数值,需要设置一个变量来记录摇一摇的时间。 实现注意事项: 我们需要定一个阈值,作为摇一摇的判断基准。 需要通过公式来计算单位时间内运动的路程,也就是我们想要的速度。如果这个速度超过了我们定下来的阈值,那么就算作用户已经摇一摇 [代码]//计算 公式的意思是 单位时间内运动的路程,即为我们想要的速度[代码][代码]var[代码] [代码]speed = Math.abs(x + y + z - lastX - lastY - lastZ) / diffTime * 10000;[代码] 另外我们需要注意两个小程序提供的API: wx.onAccelerometerChange(CALLBACK):用于监听加速度数据,在发生有加速度的动作的时候,执行摇一摇的判断逻辑 wx.stopAccelerometer:在摇一摇的逻辑执行期间,需要停止监听加速度数据,避免多次触发摇一摇 为了给予用户更好的摇一摇反馈,建议在执行摇一摇以后设置振动反馈,这个小程序API里面也带有相关的接口:wx.vibrateShort(OBJECT) 关于抽签并显示运签结果的功能 每日出击运签的小程序其中有一个需求是抽签并显示运签结果。在用户每天进入小程序的时候,通过摇一摇,得到一个抽签结果,如图所示: [图片] 关于这块功能,这里需要着重介绍vine在实现小程序的过程中比较关注的两个点,一个是随机显示抽签结果,一个是用于显示签纸的动画效果。 随机显示抽签结果 这个小程序中,随机显示抽签结果的需求具体如下: 随机出现五个运签结果,大吉、吉、平、凶、大凶;不同结果对应下面不同文案;每个用户每天只能获取同一个结果。随机文案,每个用户在用完库存前不重复 这里我们可以提炼出两个关键点:一个是每个用户每天获取的结果不变,一个是,在库存用完前不重复出现文案 也就是说,我们需要把用户的抽签结果和用户id关联起来。另外我们需要准备储存抽签结果的数组,每天从数组里面取出一个值作为抽签结果,从而保证在库存用完前不重复出现文案。贴代码: [代码]// 打乱数组顺序[代码][代码] [代码][代码]function[代码] [代码]shuffle(a) {[代码][代码] [代码][代码]var[代码] [代码]j, x, i;[代码][代码] [代码][代码]for[代码] [代码](i = a.length - 1; i > 0; i--) {[代码][代码] [代码][代码]j = Math.floor(random() * (i + 1));[代码][代码] [代码][代码]x = a[i];[代码][代码] [代码][代码]a[i] = a[j];[代码][代码] [代码][代码]a[j] = x;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]m_w = 123456789;[代码][代码] [代码][代码]var[代码] [代码]m_z = 987654321;[代码][代码] [代码][代码]var[代码] [代码]mask = 0xffffffff;[代码] [代码] [代码][代码]function[代码] [代码]seed(i) {[代码][代码] [代码][代码]m_w = i;[代码][代码] [代码][代码]m_z = 987654321;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]random() {[代码][代码] [代码][代码]m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;[代码][代码] [代码][代码]m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;[代码][代码] [代码][代码]var[代码] [代码]result = ((m_z << 16) + m_w) & mask;[代码][代码] [代码][代码]result /= 4294967296;[代码][代码] [代码][代码]return[代码] [代码]result + 0.5;[代码][代码] [代码][代码]}[代码] [代码]//获取用户id [代码][代码] [代码][代码]var[代码] [代码]userid = parseInt(gbConfig.user_id, 16);[代码][代码] [代码][代码]seed(userid);[代码][代码] [代码][代码]// 随机生成抽签描述[代码][代码] [代码][代码]var[代码] [代码]qian = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30];[代码][代码] [代码][代码]var[代码] [代码]result = [];[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < 10; i++) {[代码][代码] [代码][代码]shuffle(qian);[代码][代码] [代码][代码]result = result.concat(qian);[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]// 随机生成抽签结果[代码][代码] [代码][代码]var[代码] [代码]luck = [1, 2, 3, 4, 5];[代码][代码] [代码][代码]var[代码] [代码]luckarr = [];[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < 60; i++) {[代码][代码] [代码][代码]shuffle(luck);[代码][代码] [代码][代码]luckarr = luckarr.concat(luck);[代码][代码] [代码][代码]}[代码] 2.签纸显示的动画效果 这次小程序,在上线前添加了签纸显示的动画效果,以加强“抽签”的用户感受。签纸以滚轴的形式向下展开,慢慢呈现出签纸的结果。 关于动画效果,最容易被使用的实现方式应该就是css3动画以及序列帧。这里尝试了三套方案,最终选择了最后一套。下面分别来介绍一下这三套方案。 第一:使用传统的序列帧方式。尝试使用传统的序列帧,通过逐帧动画,慢慢铺开签纸,逐帧动画如下: [图片] 在通过减帧以后,得到了8张序列帧图,vine发现,8张序列帧图通过拼接得到的雪碧图会非常大(达到了1M左右的大小),所以再次进行减帧得到由4张序列帧图拼接而成的雪碧图(也有500+kb左右)。但是在实际放到页面上面的时候发现,4张序列帧图帧数太少,造成了动画卡顿不流畅的情况,而且在低端安卓机上面,这组序列帧动画完全无法流畅进行。所以第一套方案放弃。 第二:把8张序列帧图分开加载,4张为一组,拼接成一张雪碧图,一共两张雪碧图,如下: [图片] [图片] vine的处理方式是:先加载第一张雪碧图,在第一张雪碧图逐帧动画结束后,切换到第二张雪碧图,无缝连接播放第二组逐帧动画。结果在低端安卓机上面出现了同样的情况,动画播放非常卡顿,而且在图片切换的过程中出现了空白。于是第二套方案失败。 第三:在vine百思不得其解的时候,突然发现,这套序列帧有一个特点:它的签纸和卷轴并不是立体的!也就是说,这里其实可以不需要序列帧来实现签纸打开的效果,只需要最后一张图就可以了: [图片] 关于保存图片的功能 保存图片的功能是这次小程序中的一个大难点,这里非常感谢泽贤和小苏提供的建议。下面来介绍一下保存图片的功能到底是什么: [图片][图片] 通过点击分享按钮,可以生成一张和签纸类似的图片,用户可以长按保存这张图片到手机本地。从而达到转发分享的目的。 而这个保存,所需要的就是canvas。小程序API提供了canvas的接口: [图片] 通过这个接口,我们可以把当前画布指定区域的内容导出成指定大小的图片,然后再调用小程序的预览接口进行图片预览以及保存: [图片] 但是在这里,vine遇到了几个问题: 多次点击保存按钮触发canvas,导致手机滑动的时候非常卡顿 生成预览图片时间非常慢 三星note5保存图片尺寸的问题 下面来介绍一下这两个问题相应的解决措施: 多次点击保存按钮触发canvas,导致手机滑动的时候非常卡顿 canvas只在需要触发的时候渲染,在用户没有点击保存按钮的时候,默认不渲染canvas。代码如下:由于小程序和MVVM框架类似,无法直接操作dom,所以vine采取了在样式上面添加状态,通过修改showcanvas变量的值,来控制canvas的显示和隐藏。在执行画图操作的时候使变量值为false,显示canvas,在绘制完成的时候更改变量值,隐藏canvas [代码]<[代码][代码]canvas[代码] [代码]class[代码][代码]=[代码][代码]'{{showcanvas ? "hidcanvas" : "showcanvas"}}'[代码] [代码]style[代码][代码]=[代码][代码]"width: 750px; height: 1334px;position:fixed;left:100000rpx;top:-100000rpx;"[代码] [代码]canvas-id[代码][代码]=[代码][代码]"shareQRcode"[代码][代码]></[代码][代码]canvas[代码][代码]>[代码] 生成预览图片时间非常慢 生成预览图片时间非常慢,目前判断的原因是,直接预览canvas临时路径的图片耗时比较久。那么应该如何优化这个过程呢? 小程序的API提供了几个关于文件的接口: [图片] [图片] [图片] 我们可以通过这几个接口优化目前的预览方式。在这其中vine做了两种不同的尝试。 第一,在执行wx.previewImage的success的回调的时候,把previewImage生成的临时路径保存下来,下次再点击预览的时候,直接获取本地已保存的文件列表wx.getSavedFileList,取得最近保存下来的文件的本地路径,进行预览。这么做的想法主要是为了解决canvas渲染的问题,每天只渲染一次,后续都是通过预览本地图片链接达到生成图片的目的。梳理一下流程: 初次渲染:canvasToTempFilePath -> previewImage -> saveFile 二次渲染:getSavedFileList -> previewImage 可惜理想始终是丰满的,在实际操作的过程中vine发现,通过这样的方式预览图片的时候,一直处于loading的状态,无法生成初次渲染的图片。vine非常苦恼,至今没能找到原因。 于是vine想了另一种办法,调整了预览的顺序如下: 初次渲染:canvasToTempFilePath -> saveFile -> previewImage 二次渲染:getSavedFileList -> previewImage 这时候vine发现,先把canvas生成的临时路径保存到本地,再预览,这种方法是可行的!而且在二次渲染的过程中,由于只是读取小程序本地的图片路径,无需再次调用canvas绘图,二次预览的时间大大减少。 在这里我们需要注意官方文档中提到的一点:小程序本地文件存储的大小限制为10M。所以我们需要调用wx.removeSavedFiled的方法删除我们不需要的图片(当天之前存下来的图片都是我们无需保存到本地的图片,可以删除) 三星note5保存图片尺寸的问题 在数量庞大版本不一的安卓手机上,vine收到反馈,三星note5在保存图片的时候会遇到图片底部被裁减的情况。在通过多次修改以及对比类似的小程序以后发现,note5用canvas绘制的图片有一个最大的范围是750*1150,超出这个范围的图片,底部就会被裁减。为什么会有这个最大范围vine目前还没探索出结论,希望遇到过这个问题的大神可以和vine交流一下心得,手动比心!~ 最后梳理一下小程序项目的一些注意点: 小程序需要申请小程序公司主体的账号,拿到appid,给到后台同学配置接口 小程序的体验需要上传体验版本,获取体验者的微信号,到小程序管理后台开通体验者权限 小程序开发过程中需要的单位为rpx,兼容问题较少 在小程序的需求中,需要仔细查看文档中提到的版本以及兼容问题,做好降级处理:https://mp.weixin.qq.com/debug/wxadoc/dev/framework/compatibility.html
2018-01-29