看完这篇你也可以实现一个360度全景插件

导读本文从绘图基础开始讲起,详细介绍了如何使用Three.js开发一个功能齐全的全景插件。 我们先来看一下插件的效果: 如果你对Three.js已经很熟悉了,或者你想跳过基础理论,那么你可以直接从全景预览开始看起。 本项目的github地址:https://github.com/ConardLi/t... 一、理清关系1.1 OpenGL OpenGL是用于渲染2D、3D量图形的跨语言、跨平台的应用程序编程接口(API)。 这个接口由近350个不同的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。 OpenGL ES 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。 基于OpenGL,一般使用C或Cpp开发,对前端开发者来说不是很友好。 1.2 WebGLWebGL把JavaScript和OpenGL ES 2.0结合在一起,从而为前端开发者提供了使用JavaScript编写3D效果的能力。 WebGL为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。 1.3 CanvasCanvas是一个可以自由制定大小的矩形区域,可以通过JavaScript可以对矩形区域进行操作,可以自由的绘制图形,文字等。 一般使用Canvas都是使用它的2d的context功能,进行2d绘图,这是其本身的能力。 和这个相对的,WebGL是三维,可以描画3D图形,WebGL,想要在浏览器上进行呈现,它必须需要一个载体,这个载体就是Canvas,区别于之前的2dcontext,还可以从Canvas中获取webglcontext。 1.4 Three.js 我们先来从字面意思理解下:Three代表3D,js代表JavaScript,即使用JavaScript来开发3D效果。 Three.js是使用JavaScript 对 WebGL接口进行封装与简化而形成的一个易用的3D库。 直接使用WebGL进行开发对于开发者来说成本相对来说是比较高的,它需要你掌握较多的计算机图形学知识。 Three.js在一定程度上简化了一些规范和难以理解的概念,对很多API进行了简化,这大大降低了学习和开发三维效果成本。 下面我们来具体看一下使用Three.js必须要知道的知识。 二、Three.js基础知识使用Three.js绘制一个三维效果,至少需要以下几个步骤: 创建一个容纳三维空间的场景 — Sence将需要绘制的元素加入到场景中,对元素的形状、材料、阴影等进行设置给定一个观察场景的位置,以及观察角度,我们用相机对象(Camera)来控制将绘制好的元素使用渲染器(Renderer)进行渲染,最终呈现在浏览器上拿电影来类比的话,场景对应于整个布景空间,相机是拍摄镜头,渲染器用来把拍摄好的场景转换成胶卷。 2.1 场景场景允许你设置哪些对象被three.js渲染以及渲染在哪里。 我们在场景中放置对象、灯光和相机。 很简单,直接创建一个Scene的实例即可。 _scene = new Scene();2.2 元素有了场景,我们接下来就需要场景里应该展示哪些东西。 一个复杂的三维场景往往就是由非常多的元素搭建起来的,这些元素可能是一些自定义的几何体(Geometry),或者外部导入的复杂模型。 Three.js 为我们提供了非常多的Geometry,例如SphereGeometry(球体)、 TetrahedronGeometry(四面体)、TorusGeometry(圆环体)等等。 在Three.js中,材质(Material)决定了几何图形具体是以什么形式展现的。它包括了一个几何体如何形状以外的其他属性,例如色彩、纹理、透明度等等,Material和Geometry是相辅相成的,必须结合使用。 下面的代码我们创建了一个长方体体,赋予它基础网孔材料(MeshBasicMaterial) var geometry = new THREE.BoxGeometry(200, 100, 100); var material = new THREE.MeshBasicMaterial({ color: 0x645d50 }); var mesh = new THREE.Mesh(geometry, material); _scene.add(mesh); ...

May 5, 2019 · 6 min · jiezi

Vue使用Canvas绘制图片矩形线条文字下载图片

1 前言1.1 业务场景图片储存在后台中,根据图片的地址,在vue页面中,查看图片,并根据坐标标注指定区域。 由于浏览器的机制,使用window.location.href下载图片时,并不会保存到本地,会在浏览器打开。 2 实现原理2.1 绘制画布<el-dialog title="查看图片" :visible.sync="dialogJPG" append-to-body> <canvas id="mycanvas" width="940" height="570"></canvas></el-dialog>这里为了交互体验,使用了element-ui的弹窗方式。将canvas画布放到了弹窗中。 为了突出画布效果可以在css中设置一个边框。 #mycanvas { border: 1px solid rgb(199, 198, 198);}2.2 绘制图片// imageUrl为后台提供图片地址doDraw(imageUrl){ // 获取canvas var canvas = document.getElementById("mycanvas") // 由于弹窗,确保已获取到 var a = setInterval(() =>{ // 重复获取 canvas = document.getElementById("mycanvas") if(!canvas){ return false } else { clearInterval(a) // 可以理解为一个画笔,可画路径、矩形、文字、图像 var context = canvas.getContext('2d') var img = new Image() img.src = imageUrl // 加载图片 img.onload = function(){ if(img.complete){ // 根据图像重新设定了canvas的长宽 canvas.setAttribute("width",img.width) canvas.setAttribute("height",img.height) // 绘制图片 context.drawImage(img,0,0,img.width,img.height) } } } },1)},context.drawImage()方法的参数介绍,可参照 W3school ...

April 26, 2019 · 1 min · jiezi

Canvas实现贝赛尔曲线轨迹动画

最近实现的下图的效果,跟大家分享一下 假如我们要画下图曲线的动画 如果每次都画一条短线连接起来,如下图被分成五段 再看十段要是被分的段数足够多时每次画一段就很像曲线轨迹了 二次贝赛尔曲线/** * 二次贝塞尔曲线动画 * @param {Array<number>} start 起点坐标 * @param {Array<number>} 曲度点坐标(也就是转弯的点,不是准确的坐标,只是大致的方向) * @param {Array<number>} end 终点坐标 * @param {number} percent 绘制百分比(0-100) */ function drawCurvePath(start, point, end, percent){ ctx.beginPath(); //开始画线 ctx.moveTo(start[0], start[1]); //画笔移动到起点 for (var t = 0; t <= percent / 100; t += 0.005) { //获取每个时间点的坐标 var x = quadraticBezier(start[0], point[0], end[0], t); var y = quadraticBezier(start[1], point[1], end[1], t); ctx.lineTo(x, y); //画出上个时间点到当前时间点的直线 } ctx.stroke(); //描边 } /** * 二次贝塞尔曲线方程 * @param {Array<number>} start 起点 * @param {Array<number>} 曲度点 * @param {Array<number>} end 终点 * @param {number} 绘制进度(0-1) */ function quadraticBezier(p0, p1, p2, t) { var k = 1 - t; return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2; }更加详细的贝塞尔曲线内容请参考这篇博客 ...

April 25, 2019 · 3 min · jiezi

用canvas实现手写签名功能

最近开发网站有一个需求,要求页面上有一块区域,用户能用鼠标在上面写字,并能保存成图片 base64 码放在服务器。这样的需求用 canvas 实现是最好的。需要用到 canvas 的以下几个属性: beginPath 创建一个新的路径globalAlpha 设置图形和图片透明度的属性lineWidth 设置线段厚度的属性(即线段的宽度)strokeStyle 描述画笔(绘制图形)颜色或者样式的属性,默认值是 #000 (black)moveTo(x, y) 将一个新的子路径的起始点移动到(x,y)坐标的方法lineTo(x, y) 使用直线连接子路径的终点到x,y坐标的方法(并不会真正地绘制)closePath 它尝试从当前点到起始点绘制一条直线stroke 它会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径,默认颜色是黑色除了用到这些属性外,还需要监听鼠标点击和鼠标移动事件。 废话就不多说了,直接上代码和 DEMO。 我对代码做了扩展,除了支持画笔,还支持喷枪、刷子、橡皮擦功能。 canvas 转成图片将 canvas 转在图片,需要用到以下属性: toDataURLcanvas.toDataURL() 方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。 const image = new Image() // canvas.toDataURL 返回的是一串Base64编码的URL image.src = canvas.toDataURL("image/png")

April 23, 2019 · 1 min · jiezi

篮球比分几比几——纯js实现的数字轮盘转动动效

篮球比分几比几——纯js实现的数字轮盘转动动效原谅我这次标题党了哈,这其实就是一个数字的翻牌器的动画效果,只不过,我们可以自己完全去用js来实现而不需要用到其他东西。先来看看效果: (ps:1.为了响应题目特意做了红黑的篮球比分牌的样子;2.gif是循环的哦~~) 这种效果让我做的话,我一开始是用会四列的<ul> 和 <li>来实现。转动的列表嘛,最直观的就是这样了。这种方法最麻烦就是要做两三层的包裹,然后各种设置overflow,特别是有些时候还有恶心的滚动条出现,滚动条不仅不美观,还会让我们算出来的每个元素宽度有误差,其实不算是很好的方法。 但是这是过去的事情了。以前我没得选,现在我想做个好人用background-position。 (っ°°;)っwhat?! background-position能写这玩意? 没错,就是能写。如果想不到怎么写的朋友可以先去回忆一下雪碧图这个知识点。 ()诶,雪碧图?这个吗? 不对,我们这里的雪碧图是指Image sprites技术,也可以称之为 CSS 贴图定位、图像精灵(sprite,意为精灵),被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。详情可以查看MDN的介绍 这里的雪碧图能够实现的原因就是css上有background-position的属性,在一张图片上取不同的位置作为背景,就可以得到不同背景。 o( ̄▽ ̄)d 好,这个我懂了,那它和我们今天要讲的动效有什么关系呢? 想一下~~如果background-position动起来了,会有什么样子呢? ⊙(・◇・)?会上下左右动...这有什么特别的吗... 没错,只能上下左右动。但是这样动起来的话,不就可以实现我们今天的动效了吗? 如果,设计狮大大给到这样一张图片: 再结合我们刚讨论的background-position,是不是就知道要怎么做了? (゜-゜)上下移动的话... 没错,我们可以通过一帧帧向上或者向下来改变background-position的值来达到数字轮盘的效果! (`⌒´メ)哼! 嗯? 怎么了? (`⌒´メ)说好的纯js实现呢?所以你还不是要有图才能做?如果设计狮大大不给图你还不是什么都做不了! 你这还真的说到点上了,要图的每做一款动效都要画图,这岂不是很麻烦? 别急,我们可是前端er,图这东西只要是不复杂,我们还能自己画嘛~~ 接下来,让我们掏出canvas,画画儿~~: const canvas = document.createElement('CANVAS'); // 首先我们先创建一个canvas canvas.height = (30 + 5) * 10; // 然后设定canvas的长宽,这个跟我们要设定的字体有关 canvas.width = 40; // 我们假设需要做一个30px的字,距离上下左右的间距为5px的图 const ctx = canvas.getContext('2d'); ctx.font = '30px Impact'; // 设置字体大小和字体样式 ctx.fillStyle = 'red'; // 设置字体样式 for (let i = 0; i < 10; i++) { // 开始循环写数字 ctx.fillText(i, 5, 30 + (i * 35)); }这小段代码就可以给我们画出用于做动画的简单数字图: ...

April 22, 2019 · 2 min · jiezi

认识canvas(画扇形 动态画圆弧(requestAnimationFrame结合settimeout做的动画)、画表盘)

最近做的两个项目都是关于canvas的,做完整理一下,方便下一次使用,在vue里写的小demo,功能:画扇形 动态画圆弧(requestAnimationFrame结合settimeout做的动画)、画表盘1、创建一个ctx对象 2、begain()方法开始画笔 3、fillStyple设置填充颜色 [strokeStyle] 4、arc(x,y,r,startAngle,endAngle,direction) true是顺时针 false是逆时针 默认是逆时针 5、closePath()结束画笔 开始填充fill() [没有closePah直接stroke()]mounted () { this.$nextTick(() => { /* 1、创建一个ctx对象 2、begain()方法开始画笔 3、fillStyple设置填充颜色 [strokeStyle] 4、arc(x,y,r,startAngle,endAngle,direction) true是顺时针 false是逆时针 默认是逆时针 5、closePath()结束画笔 开始填充fill() [没有closePah直接stroke()] */ // 封装画扇形 let ctx = this.$refs.can01.getContext(‘2d’) this.drawFanShapes(ctx, 400, 400, 0, 0, 150, ‘red’, false) this.drawFanShapes(ctx, 400, 400, 0, 120, 200, ‘green’, false) // 动态画圆弧 let ctx02 = this.$refs.can02.getContext(‘2d’) this.drawArc(ctx02, 400, 400, 100, 0, 360, ‘#ddd’, 10, ‘round’, false) let globalAni = null let endAngle = 0 let _self = this function animate () { let timer = setTimeout(() => { globalAni = requestAnimationFrame(animate) if (endAngle < 270) { endAngle += 10 _self.drawArc(ctx02, 400, 400, 100, 0, endAngle, ‘green’, 10, ‘round’, false) } else { clearTimeout(timer) cancelAnimationFrame(globalAni) } }, 20) } globalAni = requestAnimationFrame(animate) // 画时钟表盘 let ctx03 = this.$refs.can03.getContext(‘2d’) this.drawClock(ctx03, 200, 200, 60, -180 - 160, 1, ‘red’) }) }, methods: { // 画表刻度(ctx,x,y,刻度数,startX, endY,lineWidth, strokeColor) drawClock (ctx, x, y, num, startX, endY, lineWidth, strokeColor) { for (let i = 0; i < 60; i++) { ctx.save() ctx.lineWidth = 1 ctx.strokeStyle = ‘red’ ctx.translate(200, 200) ctx.rotate(6 * i * Math.PI / 180) ctx.beginPath() ctx.moveTo(0, -180) ctx.lineTo(0, -160) ctx.stroke() ctx.restore() } }, // 画扇形(ctx,width,height,半径[0自动算半径],开始角度,结束角度,填充颜色,方向) drawArc (ctx, width, height, radius, startAngle, endAngle, strokeColor, lineWidth, round, direction) { ctx.save() let basic = { x: width / 2, y: height / 2, r: (!radius) ? (width - lineWidth) / 2 : radius, startAngle: (startAngle / 180) * Math.PI, endAngle: (endAngle / 180) * Math.PI, direction: direction || false } ctx.beginPath() ctx.strokeStyle = strokeColor ctx.lineWidth = lineWidth ctx.arc(basic.x, basic.y, basic.r, basic.startAngle, basic.endAngle, direction) ctx.lineCap = round ctx.stroke() ctx.restore() }, // 画圆弧(ctx,x,y,半径[0自动算半径],开始角度,结束角度,画的颜色,是否圆角,方向) drawFanShapes (ctx, width, height, radius, startAngle, endAngle, fillColor, direction) { let basic = { x: width / 2, y: height / 2, r: (!radius) ? width / 2 : radius, startAngle: (startAngle / 180) * Math.PI, endAngle: (endAngle / 180) * Math.PI, direction: direction || false } ctx.beginPath() ctx.fillStyle = fillColor ctx.moveTo(basic.x, basic.y) ctx.arc(basic.x, basic.y, basic.r, basic.startAngle, basic.endAngle, direction) ctx.closePath() ctx.fill() } } ...

April 15, 2019 · 2 min · jiezi

微信小程序内使用canvas绘制自定义折线图表

话不多说,最终实现效果如下:图中难点:圆角矩形绘制;转载他人帖子:看此处:https://www.jb51.net/article/…最左或者最右边的气泡需要做动态偏移本项目是由mpvue写的小程序:所以用的是vue的书写格式(微信小程序可以自行修改):使用方法:将下列代码新建linechart.vue文件再项目中调用本组件的drawAll方法传入日期和值即可代码中有少量注解请不懂的给我留言<template> <div class=“linechart”> <canvas class=“circle” canvas-id=“canvasline” style=“width: 750rpx;height: 280rpx;"> </canvas></div></template><script> export default { data() { return { canvasWidth: 375, canvasHeight: 123, date: [’-/-’,’-/-’,’-/-’,’-/-’,’-/-’,’-/-’,’-/-’], value: [0,0,8,10,6,0,0,], len: 4, xcoords: [] } }, onLoad() { this.drawAll() }, methods: { drawAll(date, value) { this.date = date || this.date this.value = value || this.value var ctx = wx.createCanvasContext(‘canvasline’) this.roundRect(ctx, this.px2PX(10), 0, this.px2PX(this.canvasWidth) - this.px2PX(20), this.px2PX(this.canvasHeight), this.px2PX(8), ‘#F5F3ED’); this.drawYLine(ctx, this.px2PX(20), 0, this.px2PX(20), this.px2PX(this.canvasHeight),this.px2PX(55), this.px2PX(1), ‘white’) this.drawXLine(ctx, this.len, this.px2PX(1), ‘white’); this.drawLine(ctx, this.px2PX(1.5), this.px2PX(3)) ctx.draw() }, px2PX(px) { // px (Int) 375为设计稿宽度,根据屏幕动态设置像素大小解决模糊问题和适配 return (wx.getSystemInfoSync().screenWidth / 375) * Number(px) }, /** * * @param {CanvasContext} ctx canvas上下文 * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {color} fillColor 填充的颜色 / // 绘制矩形 roundRect(ctx, x, y, w, h, r, fillColor) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; // 开始绘制 ctx.beginPath() // 因为边缘描边存在锯齿,最好指定使用 transparent 填充 // 这里是使用 fill 还是 stroke都可以,二选一即可 // ctx.setFillStyle(’transparent’) // ctx.setStrokeStyle(’transparent’) // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // border-top ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // border-right ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // border-bottom ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // border-left ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) ctx.setFillStyle(fillColor); // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应 ctx.fill() // ctx.stroke() ctx.closePath() // 剪切 // ctx.clip() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number, number, number, number} x1, y1, x2, y2 第一条线的起始坐标和结束坐标 * @param {number} spacing 线条直接的间隔 * @param {number} lineWidth 线条宽度 * @param {color} color线条的颜色 / // 绘制竖线网格和底部文字 drawYLine(ctx, x1, y1, x2, y2, spacing, lineWidth, color) { ctx.beginPath(); let width = this.px2PX(this.canvasWidth) - (x1 * 2) let len = Math.floor(width /spacing) for (let i = 0; i <= len; i++) { let spaced = spacing * i + i; this.xcoords.push(x1 + spaced) ctx.setLineWidth(lineWidth) ctx.setStrokeStyle(color) ctx.moveTo(x1 + spaced, y1); ctx.lineTo(x2 + spaced, y2); / — 底部标尺文字 – / ctx.setFontSize(this.px2PX(12)); ctx.setTextAlign(‘center’); ctx.setFillStyle(’#DFDACD’); ctx.fillText(this.date[i], x1 + spaced, y2 + this.px2PX(14)) / —- 底部标尺文字 — / } ctx.stroke() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number} len 绘制多少条横线 * @param {number} lineWidth 线条宽度 * @param {color} color线条的颜色 / // 绘制横线网格 drawXLine(ctx, len, lineWidth, color) { ctx.beginPath(); let spaced = this.px2PX(this.canvasHeight) / len let x = this.px2PX(this.canvasWidth) for (let i = 0; i < len; i++) { let hei = spaced * i + i ctx.moveTo(0, hei); ctx.lineTo(x, hei); } ctx.setLineWidth(lineWidth) ctx.setStrokeStyle(color) ctx.stroke() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number} width 折线的线条宽度 * @param {number} r 折线拐角的圆的半径 / // 绘制折线,折线区域,气泡,气泡文字 drawLine(ctx, width,r) { let arrMax = Math.max.apply({},this.value) let height = this.px2PX(this.canvasHeight) let hei = this.px2PX(this.canvasHeight) - this.px2PX(24) let average = arrMax <= 0 ? 0 : hei / arrMax let len = this.value.length - 1 ctx.beginPath(); / 折线 / for (let i = 0; i < len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average, x2 = this.xcoords[i+1], y2 = height - this.value[i + 1] * average ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) } ctx.setStrokeStyle(’#F9B213’); ctx.setLineWidth(width); ctx.stroke() / 折线 / / 折线区域 / ctx.beginPath(); for (let i = 0; i < len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average, x2 = this.xcoords[i+1], y2 = height - this.value[i + 1] * average ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.lineTo(x2, height) ctx.lineTo(x1, height) } / 折线区域 */ ctx.setFillStyle(‘rgba(249,178,19,0.08)’); ctx.fill(); for (let i = 0; i <= len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average ctx.beginPath(); ctx.arc(x1, y1, r, 0, 2 * Math.PI) ctx.setStrokeStyle(’#F9B213’); ctx.setLineWidth(width); ctx.setFillStyle(‘white’); ctx.fill(); ctx.stroke() } for (let i = 0; i <= len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average let defaultWidth = this.px2PX(24), defaultHeight = this.px2PX(16) let fontsize = this.px2PX(10) let lense = this.value[i].toString().length if (lense > 1) { defaultWidth = defaultWidth + lense * fontsize / 2.5 } let x = x1 - defaultWidth / 2 let y = y1 - defaultHeight - r * 2 if (i === 0) { // 第一个文字tip向右 x = x1 - fontsize / 2 ctx.setTextAlign(’left’); } else if (i === len) { // 最后一个文字tip向左 x = x - defaultWidth / 2 + fontsize / 2 ctx.setTextAlign(‘right’); } else { ctx.setTextAlign(‘center’); } this.roundRect(ctx, x, y, defaultWidth, defaultHeight, this.px2PX(8), ‘white’) ctx.beginPath(); ctx.setFontSize(fontsize); ctx.setFillStyle(’#F9B213’); ctx.fillText(’+’+this.value[i], x1, y1 - this.px2PX(10)) ctx.closePath() } } } }</script><style lang=“scss”> .linechart { width: 750upx; height: 280upx; }</style>以上列子如有疑问,请给我留言。 ...

April 10, 2019 · 4 min · jiezi

【Copy攻城狮日志】踩坑小程序之canvas的显示层级问题

Created 2019-4-3 18:29:53 by huqiUpdated 2019-4-3 19:12:22 by huqi↑开局一张图,故事全靠编↑从一个需求说起狼叔@i5ting 曾说过:“单纯讲技术进阶点意义不大,脱离场景都是耍流氓”。今天,依旧从一个需求说起。什么需求呢?一个二维码,一个二次确认弹窗。这里的二维码是前端生成的,二维码下边有个button,点击button调起自定义的弹窗组件。依旧是很简单的需求,但是对于“资深”的Copy攻城狮来说,除了布局,其他的就只能去Copy了。分析了一下可能需要的代码,就开始’刷刷刷’一顿CP(Copy&Paste)操作猛如虎,结果跑下代码发现error二百五。特别是真机跑的时候,问题特别多。像这次的问题,开发者工具上压根就发现不了,幸好习惯性真机预览,不然一通push就等着失业了。还是坑在基础不牢固,文档看得不深入,对小程序原生组件应该注意的事项把握不准,才会掉入这个非常基础的坑。(图片来源于网络)canvas生成二维码通常来说,遇到这种类似的需要,我都会先找找被人造的轮子,尝试一下,有合适的就直接拿过来用了。这次用的是@yingye 大佬开源的weapp-qrcode,这个js应该是借鉴了jquery-qrcode 和 node-qrcode,有兴趣的同学可以研究研究,生码的逻辑应该是类似的,只是小程序中没有DOM操作,都是利用canvas来实现的。具体怎么实现,各位看客可以直接看相关的源码或文档。我的实现:wxml<canvas style=“width: 140px; height: 140px;” canvas-id=“myQrcode”></canvas>wxsscanvas{ display: block; margin: 0rpx auto; /** 居中 /}js drawQrcode({ width: 140, // 必须,二维码宽度,与canvas的width保持一致 height: 140, // 必须,二维码高度,与canvas的height保持一致 x: 0, // 非必须,二维码绘制的 x 轴起始位置,默认值0 y: 0, // 非必须,二维码绘制的 y 轴起始位置,默认值0 canvasId: ‘myQrcode’, // 非必须,绘制的canvasId typeNumber: 10, // 非必须,二维码的计算模式,默认值-1 text: ‘您的二维码内容’, // 必须,二维码内容 callback(e) { // 非必须,绘制完成后的回调函数 console.log(’e: ‘, e) } })二维码效果:canvas使用限制当我页面如上图一样。底部有个按钮。点击唤起自定义的弹窗组件,在开发者工具上呈现的效果十分正常。但是在真机上就会出现文字开头的不和谐现象。canvas直接覆盖住了自定义组件。通过翻阅文档,您会发现官方特别写出了Bug&Tip:3.tip:请注意原生组件使用限制。4.bug: 避免设置过大的宽高,在安卓下会有crash的问题然后点开原生组件使用限制,就会发现本B.U.G的根本原因了:原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。也就是说canvas会覆盖自定义的dialog组件。那么怎么解决呢?我的思路是“曲线救国”–将canvas转成image。一不做二不休,撸起袖子,开干!将canvas转换成image既然原生组件(camera、canvas、focus时的input、live-player、live-pusher、map、textarea、video)这么牛逼,那就打压一下,去掉他们高贵的身份,豁免他们享有的特权,彻底ge他们的命,恢复他们的平民身份。按照这个思路,开始一步一步来实现wxml <canvas wx:if="{{!renderImg}}" style=“width: 140px; height: 140px;” canvas-id=“myQrcode”></canvas> <image wx:else mode=“scaleToFill” class=“image” style=“width: 140px; height: 140px;” src="{{renderImg}}"></image>js data: { renderImg: ’’ }, onLoad: function(){ drawQrcode({ width: 140, // 必须,二维码宽度,与canvas的width保持一致 height: 140, // 必须,二维码高度,与canvas的height保持一致 x: 0, // 非必须,二维码绘制的 x 轴起始位置,默认值0 y: 0, // 非必须,二维码绘制的 y 轴起始位置,默认值0 canvasId: ‘myQrcode’, // 非必须,绘制的canvasId typeNumber: 10, // 非必须,二维码的计算模式,默认值-1 text: ‘您的二维码内容’, // 必须,二维码内容 callback(e) { // 非必须,绘制完成后的回调函数 console.log(’e: ‘, e) if(e.errMsg == ‘drawCanvas:ok’) { // 新增转图片 wx.canvasToTempFilePath({ x: 0, y: 0, width: 140, height: 140, canvasId: ‘myQrcode’, success: function(res) { me.setData({ renderImg: res.tempFilePath}); } }); } } }) }以上将canvas替换成image,不过遇到闪烁的问题,这是wx:if特有的,这里通过取巧的办法,只改了canvas的样式:wxsscanvas{ display: block; margin: 0rpx -9999px; / 占位解决二维码闪屏 /}image{ display: block; margin: 0rpx auto; / 居中 **/}至此,已填了这个canvas显示层级过高的坑。如您有更好的方案,欢迎提出指正!如您觉得文章解决了您的问题,欢迎打赏 ...

April 3, 2019 · 1 min · jiezi

黑洞效果的粒子背景效果

简介html canvas实现的粒子效果背景,鼠标点击粒子有弹动效果,设置区块产生黑洞回收粒子效果预览:http://www.jquery66.com/demos…下载地址:https://u18725144.ctfile.com/…

March 29, 2019 · 1 min · jiezi

网页html生成图片的常用方案

如果您有一个需求是将网页生成一个快照的图片,然后需要用到该图片上传或者发送给他人的这样的需求,那么你会怎么做呢?聪明的你可能会想到canvas是否可以生成一个这样的图片呢?没错,今天就给大家推荐一个简单又好用的工具html2canvas。准备文件进入该官方网站点击此处,在官网首页开始下载资源文件,html2canvas.js或者html2canvas.min.js皆可。将该资源文件引入您需要使用该功能的页面中。开始使用给您想转换成图片的盒子标签上添加一个唯一id,这样便于找到该DOM节点。按照官方文档相关参数设置并添加代码在合适的地方来进行图片转换。html2canvas(document.querySelector("#app")).then(canvas => { document.body.appendChild(canvas)});详细使用相关参数设置可参考该官方文档,根据需要设置即可。兼容性介绍Firefox 3.5+Google ChromeOpera 12+IE9+EdgeSafari 6+关于vue-cli中使用最新版本应该可以直接通过yarn或者npm引入了,可参照官网首页介绍npm install –save html2canvas或者yarn add html2canvas如果有相关问题,还可参考另一篇文章点此查看

March 16, 2019 · 1 min · jiezi

如何理解并应用贝塞尔曲线

贝塞尔曲线又叫贝兹曲线,在大学高数中一度让我非常头疼。前阵子练手写动画的时候,发现贝塞尔曲线可以应用于轨迹的绘制以及定义动画曲线。本文就来探究一下,贝塞尔曲线到底是个什么样的存在。贝塞尔曲线原理贝塞尔曲线由n个点来决定,其曲线轨迹可以由一个公式来得出:其中n就代表了贝塞尔曲线是几阶曲线,该公式描述了曲线运动的路径。以下我们来讨论一下,贝塞尔公式如何推导。一阶贝塞尔曲线设定图中运动的点为Pt,t为运动时间,t∈(0,1),可得如下公式二阶贝塞尔曲线在二阶贝塞尔曲线中,已知三点恒定(P0,P1,P2),设定在P0P1中的点为Pa,在P1P2中的点为Pb,Pt在PaPb上的点,这三点都在相同时间t内做匀速运动。由公式(1)可知将公式(2)(3)代入公式(4)中,可得三阶贝塞尔曲线同理,根据以上的推导过程可得由此可以推导n阶贝塞尔曲线放上一个网址,随意感受一下贝塞尔曲线的绘制过程:http://myst729.github.io/bezi…实际应用贝塞尔曲线在前端中主要有两方面的应用,一方面可以作为动画曲线应用于CSS3动画中;另一方面可以通过canvas来绘制曲线达到需要的效果。CSS3中贝塞尔曲线的应用在CSS3中,有两属性经常被用到:transition-timing-function和animation-timing-function,这两个分别代表了过渡的速度和动画的速度。CSS3为我们提供了一个新的工具——cubic-bezier(x1,y1,x2,y2)。这个工具能够生成一个速度曲线,使我们的元素按照该曲线来调节速度。在上面的推导中,我们知道在贝塞尔公式中,有两个点的位置恒定——P0和P1,cubic-bezier中定义了两个控制点的位置,所以该曲线为三阶贝塞尔曲线。有个网站可以方便我们快速建立一个贝塞尔曲线:cubic-bezier贝塞尔曲线与动画曲线的关联先来一波动图简单粗暴的感受一下:例一:例二:例三:左边的是贝塞尔曲线,横轴代表了事件,竖轴代表了进度,无法直观得感受出速度的变化。右边的曲线是控制面板中的动画曲线,横轴是时间,竖轴是速度,可以方面地看出速度的变化。上述例子中,以前进反向为速度正方向,后退方向为速度反方向。如何得知速度的变化推导例一中,贝塞尔曲线为一条直线,当时间均匀变化时,进度也在均匀变大,由此可知速度恒定不变,时间和进度之间的关系可以用一个线性方程来表示:y=ax+b (a=1,b=0)其中x为时间,y为进度,a即为速度。推导案例一从上面结论中启发,去观察其他贝塞尔曲线,图中是一段变化的曲线,我们取其中一小段,将其看作稳定不变的一段直线,通过下面的线性方程来表示,并通过红线标注在图中:y=ax+b根据初中数学的内容,我们知道,当a>1时,与x轴的夹角∈(45°,90°);当a∈(0,1)时,与x轴的夹角在(0,45°)之间。相同的时间内,与x轴的夹角越大,a越大,速度越快。观察上图的夹角变化趋势,夹角逐渐变小趋向于0,而后逐渐变大,趋向于90°,对应速度应是速度逐渐变慢趋向于0,之后逐渐变快。放上动画曲线以及动图来验证一下我们的推测:推导案例二下图中的曲线部分在第四象限,部分在第一象限,这时对应的动画曲线该如何推导呢。同样将该曲线视为由n段平滑的直线构成,由线性方程来表示直线的趋势,可知速度a方向一开始为负,之后慢慢向正方靠近,a的速率也在由大变小,当为0时,再向正方慢慢变大。即该曲线表示元素一开始在朝反方向减速运动,当速度为0后,向正方向作加速运动。通过动画曲线及动图来验证上述推导:验证用两个曲线来验证一下上面的结论:曲线一:曲线二:从结果可以判断,用上述推导方法可以正确得出贝塞尔曲线与动画曲线之间的关系。动画曲线的应用了解了如何用贝塞尔曲线来指定动画曲线后,很多动画涉及到速度方面的效果就可以实现了,例如小车加速刹车,弹簧动画等速度轨迹都可以根据自己的需要来进行定制。放上一个缓动函数速查网址,可以让自己的动效更加真实:缓动函数放一个小例子:该动画模拟了小球落下回弹的过程代码如下: <div class=“ground”> <div class=“ball” id=“ball”></div> </div> .ball { width: 30px; height: 30px; background: #000000; border-radius: 50%; position: absolute; top: 0; left: 50%; animation: move 4s cubic-bezier(0.36, 1.7, 0.26, 0.61) 1s infinite; } @keyframes move { 0% { top: 0; } 100% { top: 90%; } }这类动画可以参考网上大大们的案例:贝塞尔曲线与CSS3动画、SVG和canvas的应用理解与运用贝塞尔曲线利用canvas绘制贝塞尔曲线canvas中提供了api可以快速绘制一条贝塞尔曲线,来达到需要的效果:二阶贝塞尔曲线quadraticCurveTo(x1,y1,x2,y2)var c=document.getElementById(“myCanvas”);var ctx=c.getContext(“2d”);ctx.beginPath();ctx.moveTo(20,20);ctx.quadraticCurveTo(40,200,200,20);ctx.stroke();其中moveTo定义了起始点,quadraticCurveTo(x1,y1,x2,y2)中的(x1,y1)为控制点,(x2,y2)为终点三阶贝塞尔曲线bezierCurveTo(x1,y1,x2,y2,x3,y3)var c=document.getElementById(“myCanvas”);var ctx=c.getContext(“2d”);ctx.beginPath();ctx.moveTo(20,20);ctx.bezierCurveTo(40,100,200,150,200,20);ctx.stroke();其中moveTo定义了起始点,bezierCurveTo(x1,y1,x2,y2)中的(x1,y1),(x2,y2)为控制点,(x3,y3)为终点总结为了弄清贝塞尔曲线是个什么东西,和动画曲线、速度又有什么关联,作者跑去复习了一下那些早扔给老师的东西,有说错的请轻拍/(ㄒoㄒ)/~~广而告之本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。欢迎讨论,点个赞再走吧 。◕‿◕。 ~

March 14, 2019 · 1 min · jiezi

Canvas 点线动画案例

Canvas 点线动画案例画圆:arc(x, y, r, start, stop)画线:moveTo(x, y) 定义线条开始坐标lineTo(x, y) 定义线条结束坐标填充:fill()绘制:stroke()1、画一个点初始化<canvas id=“canvas” width=“700” height=“600”> 浏览器不支持canvas!</canvas>找到 <canvas> 元素let canvas = document.getElementById(“canvas”);创建 context 对象let ctx = canvas.getContext(“2d”);画圆// 坐标(x, y)、半径、开始角度、结束角度、顺时针(逆时针)ctx.arc(70, 80, 30, 0, Math.PI * 2, false);2、画很多点//生成点for (let i = 0; i < dotsNum; i ++) { x = Math.random() * canvas.width; y = Math.random() * canvas.height; r = Math.random() * 4; // 随机生成 <4 的半径值 ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fillStyle = “rgba(0,0,0,.8)”; ctx.fill(); ctx.closePath();}3、画两点一线确定两个点的坐标 + lineTo 、moveTofor (let i = 0; i < 2; i++) { ctx.beginPath() // 设置原点位置为(100,100),半径为10 ctx.arc(100 + i * 150, 100 + i * 250, 10, 0, Math.PI * 2, false) // 两个点进行画线 ctx.moveTo(100, 100) ctx.lineTo(100 + i * 150, 100 + i * 250) // 设置线的宽度,单位是像素 ctx.lineWidth = 2 ctx.stroke() // 实心圆 - 填充颜色,默认是黑色 // 实心圆 - 画实心圆 ctx.fill() ctx.closePath()}4、画多点多线当点很多、元素很多的时候再进行画线操作会很繁琐,对于多元素的情况,创建实例对象,把变量存储在实例对象上。定义一个Dots函数。var Dots = function () { // 画布 this.canvas; this.ctx; // 画点 this.x; this.y; this.r;};添加一个用于点的生成的初始化方法。Dots.prototype = { // 初始化点的方法 init: function (canvas) { this.canvas = canvas; this.ctx = this.canvas.getContext(‘2d’); this.x = Math.random() * this.canvas.width; this.y = Math.random() * this.canvas.height; this.r = Math.random() * 4; // 随机生成 <4 的半径值 this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI); this.ctx.fillStyle = “black”; this.ctx.fill(); this.ctx.closePath(); }};在点与点之间进行画线,每两个点之间就有一条线,总共有C(n,2)条线。// 绘制连线 for (var i = 0; i < dotsNum; i ++) { for (var j = i + 1; j < dotsNum; j ++) { var tx = dotsArr[i].x - dotsArr[j].x, ty = dotsArr[i].y - dotsArr[j].y, // 三角形斜边长 s = Math.sqrt(Math.pow(tx, 2) + Math.pow(ty, 2)); if (s < dotsDistance) { ctx.beginPath(); ctx.moveTo(dotsArr[i].x, dotsArr[i].y); ctx.lineTo(dotsArr[j].x, dotsArr[j].y); ctx.strokeStyle = ‘rgba(0,0,0,’+(dotsDistance-s)/dotsDistance+’)’; ctx.strokeWidth = 1; ctx.stroke(); ctx.closePath(); } } }点与点之间连线优化点:限定点与点的连线距离(优化:根据点之间的距离添加连线颜色透明度)5、requestAnimationFrameCanvas 画布的工作原理和显示器工作原理一样,都是通过不断的刷新绘制。浏览器的刷新是实时的,而 Canvas 的刷新是手动触发的,如果我们只想在 Canvas 上实现静态的效果,就没必不断刷新。requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。requestAnimationFrame不是自己指定回调函数运行的时间,而是跟着浏览器内建的刷新频率来执行回调。优势:浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果,一旦页面不处于浏览器的当前标签,就会自动停止刷新。使用方式:持续调用 requestAnimFrame清除动画调用 cancelAnimationFrame动效绘制大致路数:var canvas = document.querySelector(‘canvas’);var context = canvas.getContext(‘2d’);// 画布渲染var render = function () { // 清除画布 context.clearRect(0, 0, canvas.width, canvas.height); // 绘制(在canvas画布上绘制图形的代码) draw(); // 继续渲染 requestAnimationFrame(render);};render();上面的draw()就是在 canvas 画布上绘制图形的代码,但是如果仅仅有上面代码还不够,如果是同一个位置不断刷新,我们看到的还是静止不动的效果,所以还需要一个运动变量。运动坐标变量:var canvas = document.querySelector(‘canvas’);var context = canvas.getContext(‘2d’);// 坐标变量var x = 0;// 绘制方法var draw = function () { ball.x = x;};// 画布渲染var render = function () { // 清除画布 context.clearRect(0, 0, canvas.width, canvas.height); // 位置变化 x++; // 绘制 draw(); // 继续渲染 requestAnimationFrame(render);};render();6、动起来的多点多线动的是点,画的是线给 Dots 对象添加运动变量,sx 和 sy 两个值表示点在x轴和y轴的运动量,此处为在[-2, 2)之间运动。let Dots = function () { // 画布 this.canvas; this.ctx; // 画点 this.x; this.y; this.r; // 移动 // 随机确定点的移动速度与方向 速度值在 [-2, 2) 之间 提高数值可加快速度 //(Math.random() 随机返回[0,1)的数) this.sx = Math.random() * 4 - 2; this.sy = Math.random() * 4 - 2;};添加更新点的方法update()// 更新点位置 update: function () { this.x = this.x + this.sx; this.y = this.y + this.sy; // 点超出 canvas 范围时重新初始化 if (this.x < 0 || this.x > this.canvas.width) { this.init(this.canvas); } if (this.y < 0 || this.y > this.canvas.height) { this.init(this.canvas); } this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI); this.ctx.fillStyle = “rgba(0,0,0,.6)”; this.ctx.fill(); this.ctx.closePath(); }动画及连线兼容 requestAnimationFrame let requestAnimFrame = requestAnimationFrame || webkitRequestAnimationFrame || oRequestAnimationFrame || msRequestAnimationFrame; requestAnimFrame(animateUpdate); // 兼容不同浏览器的 requestAnimationFrame或者使用 setTimeout 向下兼容:// requestAnimationFrame的向下兼容处理if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(fn) { setTimeout(fn, 17); };}由于点的位置不断变换,因此需要将画线的操作放在动画内执行,点的位置 update 一次就执行一次连线。 function animateUpdate() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空canvas中原有的内容 for (let i = 0; i < dotsNum; i ++) { dotsArr[i].update(); } // 绘制连线 for (let i = 0; i < dotsNum; i ++) { for (let j = i + 1; j < dotsNum; j ++) { let tx = dotsArr[i].x - dotsArr[j].x, ty = dotsArr[i].y - dotsArr[j].y, // 三角形斜边长 s = Math.sqrt(Math.pow(tx, 2) + Math.pow(ty, 2)); if (s < dotsDistance) { ctx.beginPath(); ctx.moveTo(dotsArr[i].x, dotsArr[i].y); ctx.lineTo(dotsArr[j].x, dotsArr[j].y); ctx.strokeStyle = ‘rgba(0,0,0,’+(dotsDistance-s)/dotsDistance+’)’; ctx.strokeWidth = 1; ctx.stroke(); ctx.closePath(); } } } // 继续渲染 requestAnimFrame(animateUpdate); }类似的例子星空效果、下雨效果等你可能不知道的点1、canvas 画的圆不是圆,是椭圆不要在style里指定 Canvas 的宽度,Canvas 画布的尺寸的大小和显示的大小是有很大的区别的,在 canvas 里面设置的是才是 Canvas 本身的大小。如果不给<canvas>设置 width、height 属性时,则默认 width 为 300、height 为 150, 单位都是 px。也可以使用 css 属性来设置宽高,但是如宽高属性和初始比例不一致,他会出现扭曲。所以,建议永远不要使用css属性来设置<canvas>的宽高。2、不要企图通过闭合现有路径来开始一条新路径画新元素前记得要 beginPath()不管用 moveTo 把画笔移动到哪里,只要不调用beginPath(),一直都是在画一条路径fillRect 与 strokeRect 这种直接画出独立区域的函数,也不会打断当前的path ...

March 13, 2019 · 3 min · jiezi

用canvas画一个微笑的表情

实习期间让我用canvas画一个表情,比较简单,话不多说直接上代码:<body><div id=“canvas-warp”> <canvas id=“canvas” style=“display: block; margin: 200px auto;"> 你的浏览器居然不支持Canvas! </canvas></div><script> window.onload = function () { var canvas = document.getElementById(“canvas”); canvas.width = 400; canvas.height = 400; //获取上下文 var context = canvas.getContext(“2d”); //用于画有填充色圆的函数 参数分别为圆心坐标 ,半径,起始与终止位置,线颜色,填充颜色 function drawCircle(x2, y2, r2, a2, b2, lineColor, FillColor) { context.beginPath(); context.arc(x2, y2, r2, a2, b2 * Math.PI); context.strokeStyle = lineColor; context.fillStyle = FillColor; context.fill(); //确认填充 context.stroke(); }; //用于画圆弧函数 默认线条为黑色 无填充 参数分别为:圆心x坐标,圆心y坐标,半径,开始位置,终止位置 function drawsArc(x, y, r, l1, l2) { context.beginPath(); context.arc(x, y, r, l1 * Math.PI, l2 * Math.PI); context.strokeStyle = “black”; context.stroke(); }; //用于画眼睛的函数 function darwEyes(x1, y1, a1, b1) { //参数分别为椭圆圆心位置 长轴 短轴 context.strokeStyle = “#754924” ParamEllipse(context, x1, y1, a1, b1); //椭圆 function ParamEllipse(context, x, y, a, b) { //使每次循环所绘制的路径(弧线)接近1像素 var step = (a > b) ? 1 / a : 1 / b; context.beginPath(); context.moveTo(x + a, y); //从椭圆的左端点开始绘制 for (var i = 0; i < 2 * Math.PI; i += step) { //参数为i,表示度数(弧度) context.lineTo(x + a * Math.cos(i), y + b * Math.sin(i)); } context.closePath(); context.fillStyle = “#754924”; context.fill(); context.stroke(); }; }; //脸 drawCircle(200, 200, 200, 0, 2, “#EEE685”, “#FCF200”); //左眼 context.strokeStyle = “#754924” darwEyes(116, 130, 18, 25); drawCircle(110, 127, 12, 0, 2, “#754924”, “#F5F5F5”); //右眼 darwEyes(296, 130, 18, 25); drawCircle(290, 127, 12, 0, 2, “#754924”, “#F5F5F5”); //左眉毛 drawsArc(100, 100, 50, 1.3, 1.7); //右眉毛 drawsArc(300, 100, 50, 1.3, 1.7); //嘴巴 drawsArc(200, 120, 180, 0.3, 0.7); }</script></body>效果图 ...

March 12, 2019 · 2 min · jiezi

Canvas绘制出一个时钟

参考视频资料:Canvas 绘制时钟最近复习到Canvas,先准备来段有趣的代码,用Canvas绘制出一个动态的时钟。然后后续再对Canvas进行进一步学习。以下代码均来自以上链接所属的视频教程。 【侵删】完整代码:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title> <style type=“text/css”> div { text-align: center; margin-top: 250px; } </style></head><body> <div> <canvas id=“clock” height=“200px” width=“200px”>你的浏览器不支持canvas</canvas> </div> <script> var dom = document.getElementById(‘clock’); var ctx = dom.getContext(‘2d’); var width = ctx.canvas.width; var height = ctx.canvas.height; var r = width / 2; //绘制表盘 function drawBackground() { ctx.save(); ctx.translate(r, r); ctx.beginPath(); ctx.lineWidth = 10; ctx.arc(0, 0, r - 5, 0, 2 * Math.PI, false); ctx.stroke(); var hourNumbers = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2]; ctx.font = ‘18px Arial’; ctx.textAlign = ‘center’; ctx.textBaseline = ‘middle’; //小时数字 hourNumbers.forEach(function (number, i) { var rad = 2 * Math.PI / 12 * i; var x = Math.cos(rad) * (r - 30); var y = Math.sin(rad) * (r - 30); ctx.fillText(number, x, y); // console.log(x) }) //绘制分刻度 for (var i = 0; i < 60; i++) { var rad = 2 * Math.PI / 60 * i; var x = Math.cos(rad) * (r - 18); var y = Math.sin(rad) * (r - 18); ctx.beginPath(); if (i % 5 == 0) { ctx.fillStyle = ‘#000’; ctx.arc(x, y, 2, 0, 2 * Math.PI, false); } else { ctx.fillStyle = ‘#ccc’; ctx.arc(x, y, 2, 0, 2 * Math.PI, false); } ctx.fill(); } } //绘制时针 function drawHour(hour, minute) { ctx.save(); ctx.beginPath(); var rad = 2 * Math.PI / 12 * hour; var mrad = 2 * Math.PI / 12 / 60 * minute; ctx.rotate(rad + mrad); ctx.lineWidth = 6; ctx.lineCap = ‘round’; ctx.moveTo(0, 10); ctx.lineTo(0, -r / 2); ctx.stroke(); ctx.restore(); } //绘制分针 function drawMinute(minute) { ctx.save(); ctx.beginPath(); var rad = 2 * Math.PI / 60 * minute; ctx.rotate(rad); ctx.lineWidth = 3; ctx.lineCap = ‘round’; ctx.moveTo(0, 10); ctx.lineTo(0, -r + 30); ctx.stroke(); ctx.restore(); } //绘制秒针 function drawSecond(second) { ctx.save(); ctx.beginPath(); ctx.fillStyle = ‘red’ var rad = 2 * Math.PI / 60 * second; ctx.rotate(rad); ctx.moveTo(-2, 20); ctx.lineTo(2, 20); ctx.lineTo(1, -r + 18); ctx.lineTo(-1, -r + 18); ctx.fill(); ctx.restore(); } //绘制指针的端点 function drawDot() { ctx.beginPath(); ctx.fillStyle = ‘white’; ctx.arc(0, 0, 3, 0, 2 * Math.PI, false); ctx.fill(); } //动起来 function draw() { //清除之前所绘制的 ctx.clearRect(0, 0, width, height); var now = new Date(); var hour = now.getHours(); var minute = now.getMinutes(); var second = now.getSeconds(); drawBackground(); drawHour(hour, minute); drawMinute(minute); drawSecond(second) drawDot(); ctx.restore(); } //draw(); setInterval(draw, 1000); </script></body></html> ...

March 11, 2019 · 2 min · jiezi

canvas-深入与应用秘籍

前言去年在公司内部做了一次canvas的分享,或者说canvas总结会更为贴切,但由于一直都因为公事或者私事,一直没有把东西总结成文章分享给大家,实在抱歉分享这篇文章的目的是为了让同学们对canvas有一个全面的认识,废话不多说,开拔!原文出处《canvas-深入与应用秘籍》介绍Canvas是一个可以使用脚本(通常为Javascript,其它比如 Java Applets or JavaFX/JavaFX Script)来绘制图形,默认大小为300像素×150像素的HTML元素。<canvas style=“background: purple;"></canvas>小试牛刀<!– canvas –><canvas id=“canvas”></canvas><!– javascript –><script> const canvas = document.getElementById(‘canvas’) const ctx = canvas.getContext(‘2d’) ctx.fillStyle = ‘purple’ ctx.fillRect(0, 0, 300, 150)</script>经过了以上地狱般的学习,我相信同学们现在已精通canvas。接下来,我将介绍很多案例,把自己能想到的都列举出来,并且,结合其原理,为同学们一一介绍。应用案例案例如下:动画游戏视频(因为生产环境还不成熟,略)截图合成图分享网页截图滤镜抠图旋转、缩放、位移、形变粒子动画API介绍requestAnimationFrame该方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。requestAnimationFrame 优点1.避免掉帧完全依赖浏览器的绘制频率,从而避免过度绘制,影响电池寿命。2.提升性能当Tab或隐藏的iframe里,暂停调用。Demo方块移动<!– canvas –><canvas id=“canvas” width=“600” height=“600”></canvas><!– javascript –><script> const canvas = document.getElementById(‘canvas’) const ctx = canvas.getContext(‘2d’) ctx.fillStyle = ‘purple’ const step = 1 // 每步的长度 let xPosition = 0 // x坐标 move() // call move function move() { ctx.clearRect(0, 0, 600, 600) ctx.fillRect(xPosition, 0, 300, 150) xPosition += step if (xPosition <= 300) { requestAnimationFrame(() => { move() }) } }</script>游戏三要素个人做游戏总结的三要素:对象抽象requestAnimationFrame缓动函数对象抽象:即对游戏中角色的抽象,面向对象的思维在游戏中非常地普遍。举个例子,我们来抽象一个《勇者斗恶龙》里的史莱姆:class Slime { constructor(hp, mp, level, attack, defence) { this.hp = hp this.mp = mp this.level = level this.attack = attack this.defence = defence } bite() { return this.attack } fire() { return this.attack * 2 }}requestAnimationFrame:之前我们已经接触过这个API了,结合上面动画的例子,我们很容易自然的就能想到,游戏动起来的原理了。缓动函数:我们知道,匀速运动的动画会显得非常不自然,要变得自然就得时而加速,时而减速,那样动画就会变得更加灵活,不再生硬。Demo有兴趣的同学可以看我以前写的小游戏。项目地址:(github.com/CodeLittlePrince/FishHeart)[https://github.com/CodeLittle…]截图API介绍drawImage(image, sx, sy [, sWidth, sHeight [, dx, dy, dWidth, dHeight]])绘制图像方法。toDataURL(type, encoderOptions)方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。注意:该方法必须在http服务下非同源的图片需要CORS支持,图片设置crossOrigin =“”(只要crossOrigin的属性值不是use-credentials,全部都会解析为anonymous,包括空字符串,包括类似’abc’这样的字符)canvas.style.width 和 canvas.width 的区别把canvas元素比作画框:canvas.width则是控制画框尺寸的方式。canvas.style.width则是控制在画框中的画尺寸的方式。Demo核心代码const captureResultBox = document.getElementById(‘captureResultBox’)const captureRect = document.getElementById(‘captureRect’)const style = window.getComputedStyle(captureRect)// 设置canvas画布大小canvas.width = parseInt(style.width)canvas.height = parseInt(style.height)// 画图const x = parseInt(style.left)const y = parseInt(style.top)const w = parseInt(img.width)const h = parseInt(img.height)ctx.drawImage(img, x, y, w, h, 0, 0, w, h)// 将图片append到html中const resultImg = document.createElement(‘img’)// toDataURL必须在http服务中resultImg.src = canvas.toDataURL(‘image/png’, 0.92)合成图原理回看之前的例子,我们知道了drawImage可以自己画图画,也可以画图片。canvas完全就是个画板,可任由我们发挥。合成的思路其实就是把多张图片都画在同一个画布(cavans)里。是不是一下子就知道接下来怎么做啦?Demo核心代码// 设置画布大小 canvas.width = bg.width canvas.height = bg.height // 画背景 ctx.drawImage(bg, 0, 0) // 画第一个角色 ctx.drawImage( character1, 100, 200, character1.width / 2, character1.height / 2 ) // 画第二个角色 ctx.drawImage( character2, 500, 200, character2.width / 2, character2.height / 2 )如图,背景是一深夜无人后院,然后去网上搜两张背景透明的角色图片,再将两张图一次画到画布上就成了合成图啦。分享网页截图原理拿比较出名的html2canvas为例,实现方式就是遍历整个dom,然后挨个拉取样式,在canvas上一个个地画出来。Demo滤镜API介绍getImageData(sx, sy, sw, sh)返回一个ImageData对象,用来描述canvas区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。看段代码:const img = document.createElement(‘img’)img.src = ‘./filter.jpg’img.addEventListener(’load’, () => { canvas.width = img.width canvas.height = img.height ctx.drawImage(img, 0, 0) console.log(ctx.getImageData(0, 0, canvas.width, canvas.height))})它会打印出如下数据:有点迷?不慌,接下去看。数据类型介绍Uint8ClampedArray8位无符号整型固定数组) 类型化数组表示一个由值固定在0-255区间的8位无符号整型组成的数组;如果你指定一个在 [0,255] 区间外的值,它将被替换为0或255;如果你指定一个非整数,那么它将被设置为最接近它的整数。(数组)内容被初始化为0。一旦(数组)被创建,你可以使用对象的方法引用数组里的元素,或使用标准的数组索引语法(即使用方括号标记)。回看这张图:data里其实就是像素,按每4个为一组成为一个像素。4个一组,难道是rgba?(o゜▽゜)o☆[BINGO!]这样的话,图片的宽x高x4(w h 4 )就是所有像素的总和,刚好就死data的length。数学推导已知:924160 = 640 x 316 x 4可知:数组的长度为length = canvas.width x canvas.height x 4知道了这种关系,我们不妨把这个一维数组想象成二维数组,想象它是一个平面图,如图:一个格子代表一个像素w = 图像宽度h = 图像高度这样,我们可以很容易得到点(x, y)在一维数组中对应的位置。我们想一想,点(1, 1)坐标对应的是数组下标为0,点(2, 1)对应的是数组下标4,假设图像宽度为22,那么点(1,2)对应下标就是index=((2 - 1)w + (1 - 1))*4 = 8。推导出公式:index = [(y - 1) w + (x - 1) ] 4继续API介绍createImageData(width, height)createImageData是在canvas在取渲染上下文为2D(即canvas.getContext(‘2d’))的时候提供的接口。作用是创建一个新的、空的、特定尺寸的ImageData对象。其中所有的像素点初始都为黑色透明。并返回该ImageData对象。putImageDataputImageData方法作为canvas 2D API 以给定的ImageData对象绘制数据进位图。如果提供了脏矩形,将只有矩形的像素会被绘制。这个方法不会影响canvas的形变矩阵。这小节我们学了好几个新API,然后重新理了理数学知识。同学们好好消化完以后,就进Demo阶段吧。Demo核心代码:最终效果:抠图对于纯背景抠图,其实还是比较简单的。上面我们已经说过,我们可以拿到整个canvas的每个像素点的值了。所以,只需要把纯色的色值转为透明就好了。但这种场景不多,因为,背景很少有纯色的情况,而且即使背景纯色,不保证被扣对象的身上没有和背景同色值的情况。所以,如果要处理复杂的情况,还是建议后端来做比较好,后端早已有了成熟的图像处理解决方案,比如opencv等。像美图的话,有专门的图像算法团队,天天研究这方面。接下来,我将介绍下美图人像抠图的思路。属性介绍globalCompositeOperation控制drawImage的绘制图层先后顺序。思路我们将使用souce-in这个属性。如上图所示,这个属性的作用是,两图叠加,只取叠加的部分。为什么这样搞?不是说好了,美图是让后端算法大佬们处理吗?因为,为了人像抠图适应更多的场景,算法大佬们只会把人物图像处理成一个蒙版图并返给前端,之后让前端自己处理。我们看下原图:再看下后端返给的蒙版图:得到以上的蒙版图以后,先把黑色处理成透明;先在canvas上draw原图;再把globalCompositeOperation 设置为 ‘source-in’;然后再draw处理后的蒙版图;得到的就是最后的抠图啦!这个方案是咨询前美图大佬@xd-tayde的,感谢Demo处理结果:旋转、缩放、位移、形变对于旋转、缩放、位移、形变,canvas的上下文ctx有对应的API可以调用,也可以用martrix方式做更高级的变化。因为涉及的内容很多,如果全写这的话,篇幅太大。所以,我这里直接推荐一篇文章给同学们学习 ——《canvas 图像旋转与翻转姿势解锁》粒子抽象之前我们就知道了,我们可以获取canvas上的每个像素点。所谓的粒子,其实算是对一个像素的抽象。它具有自己坐标,自己的色值,可以通过改变自身的属性“动”起来。因此我们不妨将粒子作为一个对象来看待,它有坐标和色值,如:let particle = { x: 0, y: 0, rgba: ‘(1, 1, 1, 1)’}Demo - 小试牛刀我将把一张网易支付的logo图,用散落的粒子重新画出来。核心代码:// 获取像素颜色信息 const originImageData = ctx.getImageData(0, 0, canvas.width, canvas.height) const originImageDataValue = originImageData.data const w = canvas.width const h = canvas.height let colors = [] let index = 0 for (let y = 1; y <= h; y++) { for (let x = 1; x <= w ; x++) { const r = originImageDataValue[index] const g = originImageDataValue[index + 1] const b = originImageDataValue[index + 2] const a = originImageDataValue[index + 3] index += 4 // 将像素位置打乱,保存进返回数据中 colors.push({ x: x + getRandomArbitrary(-OFFSET, OFFSET), y: y + getRandomArbitrary(-OFFSET, OFFSET), color: rgba(${r}, ${g}, ${b}, ${a}) }) }效果:Demo - 粒子动画三要素粒子对象化缓动函数性能粒子对象化已经介绍过了。缓动函数,在之前的游戏也提及过,是为了让动画更加的自然生动。性能是一个很需要关注的问题。因为比如一张500x500的图片,那数据量就是500x500x4=1000000。动画借助了requestAnimationFrame,正常的情况下一般刷新频率在60HZ,能展现非常流畅的动画。但现在要处理这么大的数据量,浏览器抗不过来了,自然造成了降频,导致动画卡帧严重。为了性能,粒子动画往往采用选择性的选取像素用来绘制。比如,只绘制原图x坐标为偶数,或能被4等整除的像素。比如,只绘制原图对应像素r色值为155以上的像素。结合上面的思路,就可以做出各种强大的例子动画啦。Demo所有Demo项目地址github.com/CodeLittlePrince/canvas-tutorial参考文章《打造高大上的 Canvas 粒子动画 - 腾讯 ISUX》 ...

March 8, 2019 · 2 min · jiezi

H5海报制作实践

引言年后一直处于秣马厉兵的状态,上周接到了一个紧急需求,为38妇女节做一个活动页,主要功能是生成海报,第一次做这种需求,我也是个半桶水前端,这里将碰到的问题、踩的坑,如何解决的分享给大家,讲的不到位的地方还望斧正。效果展示目前活动还是在线状态,这里是最后生成海报的效果,扫描二维码就可以进入页面。实现方案起初实现的方案是展示的时候直接使用canvas,计算手机屏幕大小,让canvas充满整个屏幕,用户编辑完之后直接用展示的canvas生成图片,最后发现这种形式很麻烦,碰到适配问题,canvas计算起来比较麻烦。最终方案,展示的时候使用html、css,这样用户看到的展示、编辑页面适配起来容易。最后生成图片的时候使用canvas,这个canvas是隐藏的,用户不可见,这样还有一个优点,最终生成的海报大小是固定的,跟手机屏幕大小无关。方案看着很简单,实现的时候各种细节问题。资源预加载H5海报活动,就像一个小型的APP,体验一定要好,最主要的就是资源预加载了,整个应用大小有30个图片,还有字体文件,一个字体文件就有3MB多,如何做好资源预加载很大程度上影响了这次活动的体验。图片预加载图片预加载的原理就是使用http协议中的缓存,这里主要指的是强缓存(协商缓存还要去服务器,有网络交互)。在活动首页之前加个loading页面,将所有用到的图片加载一遍,等到后面加载的时候就只有几ms。图片预加载,使用let image = new Image()创建一个图片标签,在image.src中加入图片链接,加载成功调用image.onload事件。一张图片还好,大量图片的话如何优雅的做出进度条呢?还好有Promise这个银弹,我们可以轻松的实现进度条效果。class Preloadedr { /** * * @param images array 要加载的图片,数组 * @param processCb function 回调函数,加载中进度有变化就调用 * @param completeCb function 回调函数,加载完成调用 / constructor(images, processCb, completeCb) { this.imagesElement = [] this.loaded = 0 this.images = images this.total = images.length this.processCb = processCb this.completeCb = completeCb } /* * 开始预加载缓存图片 * * @returns {Promise<any[]>} Promise 包含所有图片的promise / preloadImage() { let me = this let promises = [] me.loadedAction() me.images.forEach((img) => { let p = new Promise((resolve, reject) => { let image = new Image() image.src = img this.imagesElement.push(image) image.onload = () => { me.loadedAction(img) resolve(image) } image.onerror = () => { resolve(“error”) } }) promises.push(p) }) return Promise.all(promises) } /* * 进度变化的时候回调,private * * @param key string 加载成功的图片地址 / loadedAction(key) { if (key) { this.loaded++ } this.processCb(this.total, this.loaded) if (this.total == this.loaded) { this.completeCb() } }}每个要加载的图片都是一个Promise,将所有图片Promise包装为一个大的Promise,当这个大的Promise状态为fulfilled的时候,表明图片加载完成。要注意,包装图片Promise的时候onerror也是返回成功,这是因为Promise.all会包装出一个新Promise,这个Promise只要出现一个失败,就直接返回报错了,所以失败了也返回成功(resolve),就算有少数图片未加载成功也影响不大。用起来也很简单:(async () => { let imgLoader = new Preloadedr([ “//avatar-static.segmentfault.com/606/114/606114310-5c10a92c87f07_huge256”, “//image-static.segmentfault.com/203/994/2039943300-5c515b79c91f1_articlex”, ], (total, loaded) => { console.log(“process: 图片” + Math.floor(100 * loaded / total) + “%”) }, () => { console.log(“complete: 图片” + 100 + “%”) }) await imgLoader.preloadImage() console.log(“加载完成”)})()可以看到输出如下:process: 图片0%Promise {<pending>}process: 图片50%process: 图片100%complete: 图片100%加载完成至此,图片预加载就实现了。接下来我们看看字体的预加载,字体也是一种http静态资源,也可以使用缓存,但在实现预加载上却远没有图片这么简单。字体预加载字体预加载,没有像Image那么方便的函数回调使用,查了下资料,有个document.fonts实验性的属性,试了下基本支持,但在ios上可能会出现一点儿小问题,加载过一次有缓存了,第二次加载时候onloadingdone事件可能不会触发,另外这个属性、事件还是一个实验性的属性,浏览器支持程度未知,可能很差。查了很多资料,无意中看到有人说webfontloader这个项目通过一种比较trick的方法实现了,原理就是下面这两句话:不同字体,在将 fontSize 设置到很大的时候(比如300px),同一段文字,他展示的宽度是不一样的。给两个div,同样的文字内容,第一段设置两种字体,待加载字体首选,默认字体备选,第二种只设置默认字体,定时器去扫描,当两段文字长度不同的时候就说明新字体加载成功可使用。大概看了下webfontloader,代码写的比较凌乱,命名奇怪,注释少、没翻译(????,可能是我能力还不够),但考虑的情况比较完善,实现字体实现除了trick的方法外,也用了上面提到的document.fonts,有兴趣的可以详细阅读下。下面看看我实现的简易代码:class Fontloader { constructor(fontFamily) { this.fontFamily = fontFamily } /* * 返回Promise,监测字体 * * @returns {Promise<any>} / watcher() { if (“object” == typeof document.fonts) { // 使用默认的document.fonts,兼容性可能有问题,我做的过程中发现ios上可能会出现问题 return this.defaultWatcher() } else { // 使用trick法监测 return this.trickWatcher() } } /* * 返回trick法监测的Promise * * @returns {Promise<any>} / trickWatcher() { let me = this /* * 生成一个获取字体展示宽度的span元素 * @param font * @returns {HTMLSpanElement} / let genSpanWithFont = (font) => { let span = document.createElement(“span”) span.style.cssText = display:block; position:absolute; top:-9999px; left:-9999px; font-size:500px; width:auto; height:auto; line-height:normal; margin:0; padding:0; font-variant:normal; white-space:nowrap; font-family:${font} span.innerHTML = “BESbswy” if (typeof document.body.append == “function”) { document.body.append(span) } else if (typeof document.body.appendChild == “function”) { document.body.appendChild(span) } return span } /* * 用来比较的字体 * @type {string[]} / let fontDefault = [“serif”, “sans_serif”] let defaultWidth = [] let fontWidth = [] fontDefault.forEach(font => { let spanDefault = genSpanWithFont(font) defaultWidth.push(spanDefault) let spanFont = genSpanWithFont(me.fontFamily + ,${font}) fontWidth.push(spanFont) }) let clearUp = () => { defaultWidth.forEach(e => { document.body.removeChild(e) }) fontWidth.forEach(e => { document.body.removeChild(e) }) } return new Promise((resolve, reject) => { let check = () => { for (let i = 0; i < fontDefault.length; i++) { console.log(defaultWidth[i].offsetWidth, fontWidth[i].offsetWidth) if (defaultWidth[i].offsetWidth !== fontWidth[i].offsetWidth) { return true } } return false } let times = 1 let maxTimes = 10000 let loop = () => { if (times > maxTimes) { clearUp() reject(“load fonts error”) } times++ if (check()) { clearUp() resolve([me.fontFamily]) } else { window.setTimeout(loop, 1000) } } loop() }) } /* * 支持原生方法的使用原生方法 * @returns {Promise<any>} */ defaultWatcher() { return new Promise((resolve, reject) => { let loadedFamily = [] document.fonts.onloadingdone = (e) => { e.target.forEach((font) => { if (font.status == “loaded”) { loadedFamily.push(font.family) } }) resolve(loadedFamily) } document.fonts.onloadingerror = (e) => { reject(“load fonts error”) } }) }}封装之后,两种形式都统一返回Promise,在调用方通过异步函数await watcher(),等待字体加在完成之后在继续流程。这里唯一有个缺点就是,字体可能要好几MB,加载很慢,进度条很不均匀,这里我将加载分为2段,一段是图片,一段是字体,进度条分开展示,各位看官有更好的方法,不妨一起讨论。canvas绘制绘制canvas的时候我是用了pixi.js类库,实际使用的时候并不一定方便很多o(╯□╰)o,如果是简单的绘制,原生的也是很好用的。如果用了某些类库,碰到问题因为文档少,翻译更少,解决起来可能更麻烦。跨域图片如何解决绘制这张海报的时候,大部分图片都是自己的,设置允许跨域,只有用户图像这个图片,是拿的其他部门获取的实时用户头像,不让跨域,这可把我整惨了,试了很多办法都不行,最后使用服务器中转解决了这个问题,步骤如下:得到图片链接。将图片链接通过接口传递给我们自己的服务器,服务器上获取图片base64,成功后返回给web。将base64绘制到canvas。这样就解决了来自别人服务器不让跨域图片的绘制toDataURL导出图片不全海报由10个sprite组成,绘制完之后,马上调用toDataURL,发现生成的图片没内容,或者图片缺失某些sprite,这是因为绘制还没完成我就导出了,何以见得呢?当我延时几秒之后导出就没问题了。为了保险起见,图片我一张张的绘制,每次绘制都是一个Promise,等待状态为fullfield之后在进行下一张图片的绘制,最后一张绘制完之后,等待几百毫秒之后在进行导出,实际效果挺好,没再出现过导出图片不全或者空白的问题,下面是对绘图的封装: async drawImage(sprite) { return new Promise((resolve, reject) => { let img = new Image() img.setAttribute(“crossOrigin”,‘Anonymous’) img.onload = () => { console.log(“yes”) let item = new PIXI.Sprite.from(new PIXI.BaseTexture(img)) item.x = sprite.x item.y = sprite.y item.width = sprite.width item.height = sprite.height this.app.stage.addChild(item) resolve(“0”) } img.src = sprite.image }) }我这里使用的是pixi.js,sprite 表示一个精灵,里面包含了图片地址、坐标、宽高信息。onload之后进行绘制,然后resolve。汉字折行问题用的这个类库不支持汉字折行,汉字折行问题需要自己去计算,这里使用canvas的measureText方法,这个方法会根据字体大小样式计算字体正常渲染需要多少宽度,我只需要根据这个宽度一行行渲染汉字就行了,需要自己控计算控制绘制起点。ios键盘相关问题作为一个后端,半桶水前端,每次碰到这种奇葩问题都很头疼,但作为后端又有一丝庆幸,不用经常面对这些问题,哈哈哈哈。这次碰到的问题是ios上键盘弹起不正常、收起键盘卡顿的问题,具体就是用户点击按钮之后展示输入框,软键盘不弹起,和点击ios软键盘确定按钮之后卡顿,需要滑动一下才能继续触摸的问题。碰到这问题真是老虎吃天,没处下爪。最后各种查资料、各种尝试,解决方案如下:弹起问题,我用的是vue,输入框展示之后马上聚焦有问题,需要用$nextTick()包一层,下个渲染回合在进行渲染。卡顿问题,每当输入框失去焦点的时候,将滚动条滚动到顶部document.body.scrollTop = 0即可。弹起遮盖问题,有些情况会出现键盘弹起会遮盖输入框,类似的,这种情况发生后执行document.body.scrollTop = 1000,将滚动条滚到底部即可。碰到类似问题的可以沿着这个思路去解决,延时触发了、下个周期执行了、滚动之类的。总结经过这次开发,对海报这种活动算是有了完整的了解,学习、巩固了很多知识。相信读着朋友们看完之后,也可以轻松实现海报制作了。最后请大家玩儿玩儿这个活动,不妨关注下我的微博,哈哈哈。 ...

March 7, 2019 · 3 min · jiezi

学习 PixiJS — 交互工具

说明Pixi 内置一组功能有限的用于鼠标交互和触摸交互的方法,但是对于游戏和应用程序所需的丰富交互性,建议使用第三方库来简化操作,这篇文章介绍的是 Tink 库,它有通用的指针对象、拖放精灵、按钮对象、键盘控制 等一些有用的功能。使用 Tink 库要开始使用 Tink ,首先直接用 script 标签,引入 js 文件。<script src=“https://www.kkkk1000.com/js/tink.js"></script>然后创建它的实例,它的构造函数需要两个参数,一个是 PIXI,另一个是渲染器的 view 属性,也就是用作视图的 canvas 元素。let t = new Tink(PIXI, renderer.view);变量 t 现在代表 Tink 实例,可以使用它来访问 Tink 的所有方法。接下来,在游戏循环中调用 Tink 的 update 方法,来更新交互的对象,如下所示:function gameLoop(){ requestAnimationFrame(gameLoop); state(); t.update(); renderer.render(stage);}scaleToWindow 函数这里提供一个 scaleToWindow 函数,它可以将画布缩放到浏览器窗口的最大大小。scaleToWindow 函数的源码在这,使用方法如下所示:let scale = scaleToWindow(renderer.view, borderColor);它需要两个参数,一个是需要缩放的 canvas 元素,另一个参数是可选的,表示与画布相邻的浏览器背景的颜色,它可以是任何RGB,HSLA 或十六进制颜色值,以及任何 HTML 颜色字符串,例如 blue 或者 red 。scaleToWindow 函数还返回画布缩放到的缩放值。设置缩放比例Tink 的构造函数还可以传入第三个参数,这个可选的参数用来确保 Tink 使用的坐标将匹配画布的缩放像素坐标。在创建实例的时候可以直接使用 scaleToWindow 函数的返回值,作为第三个参数。let scale = scaleToWindow(renderer.view);let t = new Tink(PIXI, renderer.view, scale);指针对象使用 Tink 的 makePointer 方法可以创建指针对象,它可以自动确定用户是鼠标交互还是通过触摸进行交互。let pointer = t.makePointer();通常,一个指针对象足以满足大多数游戏或应用程序的需求,但你也可以根据需要制作多个指针对象。但是如果你的游戏或应用程序需要进行复杂的多点触控交互,可以考虑使用 Hammer 库。指针对象有三种事件:press:按下鼠标左键或用户将手指按到设备屏幕时触发release:释放鼠标按键时或者用户将手指从屏幕上抬起时触发tap:单击鼠标左键,或者用户点击屏幕时触发用法:pointer.press = () => console.log(“触发 pressed 事件”);pointer.release = () => console.log(“触发 released 事件”);pointer.tap = () => console.log(“触发 tapped 事件”);指针对象还有 x 和 y 属性,表示它在画布上的位置。pointer.xpointer.y它还有三个 Boolean 属性,用于指示指针的当前状态:isUp,isDown 和 tapped 。pointer.isUppointer.isDownpointer.tapped查看示例指针对象与精灵的交互指针对象有一个 hitTestSprite 方法,可以使用它来检测指针是否正在接触精灵。pointer.hitTestSprite(anySprite);如果指针位于精灵的矩形区域内,则 hitTestSprite 将返回 true 。查看示例hitTestSprite 方法也适用于圆形精灵。只需将精灵的 circular 属性设置为 true 即可。anyCircularSprite.circular = true;这样 hitTestSprite 方法就使用圆形碰撞检测算法,而不是默认的矩形碰撞检测算法。查看示例如果需要指针位于精灵上时显示手形图标,可以将指针的 cursor 属性设置为 pointer。当指针离开精灵区域时将其设置为 auto 将显示默认箭头图标。示例:if (pointer.hitTestSprite(anySprite)) { //当指针在精灵上时显示一个手形图标 pointer.cursor = “pointer”;} else { //当指针移出精灵区域时显示默认箭头图标 pointer.cursor = “auto”;}pointer.cursor 只是引用 canvas 元素的 style.cursor 属性来实现这一点。你也可以手动设置任何你喜欢的光标样式值。方法如下:renderer.view.style.cursor = “cursorStyle"不过,这些光标样式仅适用于基于鼠标的界面,在触摸界面上,不会起作用。示例: 在示例中可以看到将指针移到方形和圆形精灵上,光标是变化的。文本还会根据指针接触的内容显示 矩形! 或 圆形! 或 没有接触到精灵!。因为圆形精灵的 circular 属性设置为 true,你能看到圆形的形状会被准确检测到。以下是实现效果的关键代码:if (pointer.hitTestSprite(rectangle)) { message.text = “矩形!”; pointer.cursor = “pointer”;} else if (pointer.hitTestSprite(circle)) { message.text = “圆形!”; pointer.cursor = “pointer”;} else { message.text = “没有接触到精灵!”; pointer.cursor = “auto”;}查看示例拖放精灵你可以使用 Tink 的 makeDraggable 方法向精灵添加拖放功能,它的参数是一个想要可以拖动的精灵或精灵列表。示例:t.makeDraggable(sprite1, sprite2, sprite3);选择可拖动的精灵时,其堆叠顺序会发生变化,拖动的精灵会显示在其他精灵上方。鼠标箭头图标在可拖动的精灵上时也会变为手形。查看示例可拖动的精灵有一个名为 draggable 的 Boolean 属性,默认值为 true 。要禁用拖动,将draggable 设置为 false 即可。anySprite.draggable = false;将其设置为 true 将再次启用拖动。要从拖放系统中完全删除精灵或精灵列表,需要使用 makeUndraggable 方法,如下所示:t.makeUndraggable(sprite1, sprite2, sprite3);按钮按钮是一个重要的用户界面(UI)组件。Tink 有一个 button 方法,用来创建按钮。在这之前让我们先来了解下什么是按钮。什么是按钮?你可以将按钮理解为可点击或者可触摸的精灵。它们具有状态和动作。状态定义按钮的外观,动作定义它的作用。大多数按钮具有以下三种状态:up:指针未触摸按钮时的状态over:当指针在按钮上时的状态down:当指针按下按钮时的状态如下图所示基于触摸的界面的按钮只有两种状态: up 和 down。你可以通过按钮的 state 属性访问这些状态,如下所示:playButton.statestate 属性可能有 up,over 或 down 这三个字符串值,你可以在游戏逻辑中使用它。按钮的动作,如下所示:press:当指针按下按钮时release:指针从按钮释放时over:当指针移动到按钮区域时out:当指针移出按钮区域时tap:点击按钮时你可以为这些动作定义一个函数,当执行了相应操作时,会触发这个函数,如下所示。playButton.press = () => console.log(“pressed”);playButton.release = () => console.log(“released”);playButton.over = () => console.log(“over”);playButton.out = () => console.log(“out”);playButton.tap = () => console.log(“tapped”);在按钮对象中,使用 action 属性可以知道当前是 pressed 操作还是 released 操作。playButton.action制作按钮首先,从定义三个按钮状态的三个图像开始。三个图像分别是 up.png,over.png 和 down.png 。然后将这三个图像做成纹理贴图集 ,你可以使用 Texture Packer 这个工具来制作。接下来,加载纹理图集到程序中。//加载纹理贴图集,加载完后执行 setup 函数loader.add(“images/button.json”).load(setup);然后,在初始化精灵的 setup 函数中,创建一个数组,该数组有个三个成员,按顺序分别对应按钮的 up, over, 和 down 的状态。let id = PIXI.loader.resources[“images/button.json”].textures;let buttonFrames = [ id[“up.png”], id[“over.png”], id[“down.png”]];数组中的成员其实不必非要是纹理贴图集中的帧,如果你愿意,也可以使用任何单个图像纹理。最后,使用 Tink 的 button 方法创建按钮。使用 buttonFrames 数组作为第一个参数。第二个和第三个参数是按钮的 x 和 y 坐标,默认值都是0 。let playButton = t.button(buttonFrames, 32, 96);千万不要忘记将按钮添加到舞台上!stage.addChild(playButton);示例:在示例中可以看到将指针移到按钮上时,光标变为手形图标。而且在视图中还会根据按钮状态和动作显示相应的文本。查看示例从本质上讲,按钮只是一个普通的 Pixi 动画精灵,因此你可以像对待其他动画精灵一样对待它。制作交互式精灵Tink 有另一个名为 makeInteractive 的方法,它允许你向任何普通精灵添加按钮属性和方法。t.makeInteractive(anySprite);这可以将任何精灵转换为类似按钮的对象,然后你可以为精灵添加 press 或 release 事件方法。并且可以访问它的 state 和 action 属性,如下所示:anySprite.press = () => { //当指针按下精灵时执行某些操作};anySprite.release = () => { //按下精灵后释放指针时执行某些操作};function play() { stateMessage.text = State: ${anySprite.state}; actionMessage.text = Action: ${anySprite.action};}查看示例键盘控制keyboard 是一种监听和捕获键盘事件的方法。它实际上只是将原生的 keyup 和 keydown 事件封装起来而已,以下是如何使用 keyboard 方法。创建一个新的键盘对象(keyObject ):let keyObject = t.keyboard(asciiKeyCodeNumber);它的参数是你要监听的键盘键编码,你可以在这里查看每个键对应的编码。然后你就可以为返回值(keyObject)定义 press 和 release 方法,如下所示:keyObject.press = () => { //按键按下时执行某些操作};keyObject.release = () => { //按键释放时执行某些操作};keyObject 还具有 isDown 和 isUp 布尔属性,你可以使用它们来检查每个键的状态。Tink 还有另一个方便的方法 arrowControl ,可以让你使用键盘方向键快速为精灵创建一个4个方向的控制器。这个方法需要两个参数,第一个是需要控制的精灵,第二个是移动速度。示例:t.arrowControl(anySprite, 5);anySprite.vx = 0;anySprite.vy = 0;因为 arrowControl 方法能让精灵移动,用到了精灵的速度属性(vx,vy),所以需要给这两个属性一个初始值,然后在游戏循环中需要更新精灵的位置,如下所示:function play() { anySprite.x += anySprite.vx; anySprite.y += anySprite.vy;}最后,就可以使用箭头键在四个方向上移动精灵了。查看示例注意: 使用高于 4.2.1 版本的 Pixi 时,需要将 tink.js 文件中的 extras.MovieClip 改为 extras.AnimatedSprite 。上一篇 学习 PixiJS — 碰撞检测 ...

March 4, 2019 · 2 min · jiezi

【前端优化】动画几种实现方式总结和性能分析

备注:没整理格式,抱歉动画实现的几种方式:性能排序js < requestAnimationFrame <css3< Canvasjs实现方式:1.setTimeout 自身调用 eg12.setInterval 调用 eg2setTimeout的定时器值推荐最小使用16.7ms的原因(16.7 = 1000 / 60, 即每秒60帧)为什么倒计时动画一定要用setTimeout而避免使用setInterval——-两者区别及setTimeout引发的js线程讨论1.js线程讨论1.1 为什么:单线程是JavaScript的一大特性。JavaScript是浏览器用来与用户进行交互、进行DOM操作的,这也使得了它必须是单线程这一特性。比如你去修改一个元素的DOM,同时又去删除这个元素,那么浏览器应该听谁的?1.2 js单线程工作机制是:当线程中没有执行任何同步代码的前提下才会执行异步代码var t = true;window.setTimeout(function (){ t = false;},1000);while (t){}alert(’end’)JavaScript引擎是单线程运行的,浏览器只有一个线程在运行JavaScript程序1.3 浏览器工作基本原理一、浏览器的内核是多线程的,内核制控下保持同步,至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程(http请求线程等)javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。异步事件:如setTimeOut、浏览器内核的其他线程如鼠标点击、AJAX异步请求等,(当线程中没有执行任何同步代码的前提下才会执行异步代码)也就是说即使setTimeout为0,他也是等js引擎的代码执行完之后才会插入到js引擎线程的最后执行。1.4 JavaScript中任务,一种是同步任务,一种是异步任务。同步任务:各个任务按照文档定义的顺序一一推入"执行栈"中,当前一个任务执行完毕,才会开始执行下一个任务。异步任务:各个任务推入"任务队列"中,只有在当前的所有同步任务执行完毕,才会将队列中的任务"出队"执行。(注:这里的异步任务并不一定是按照文档定义的顺序推入队列中)//只有用户触发点击事件才会被推入队列中(如果点击时间小于定时器指定的时间,则先于定时器推入,否则反之)1.5 “任务队列是什么?异步任务通常包括哪些?“任务队列(event loop):你可理解为用于存放事件的队列,当执行一个异步任务时,就相当于执行任务的回调函数。通常io(ajax获取服务器数据)、用户/浏览器自执行事件(onclick、onload、onkeyup等等)以及定时器(setTimeout、setInterval)都可以算作异步操作。先来看一段代码来理解一下console.log(“1”);setTimeout(function(){console.log(“2”);},1000);console.log(“3”);setTimeout(function(){console.log(“4”);},0);输出结果: 1->3->4->2.那么在来看你这段代码。var t = true;window.setTimeout(function (){t = false},1000);while (t){}alert(’end’);1.6 setTimeOut的讨论参数描述code必需。要调用的函数后要执行的 JavaScript 代码串。millisec必需。在执行代码前需等待的毫秒数。提示:setTimeout() 只执行 code 一次。如果要多次调用,请使用 setInterval() 或者让 code 自身再次原理:setTimeout调用的时候,JavaScript引擎会启动定时器timer,当定时器时间到,就把该事件放到主事件队列等待处理。注意:浏览器JavaScript线程空闲的时候才会真正执行 ep3millisec参数有什么用?那么问题来了。setTimeout(handler,0)和setTimeout(handler,100)在单独使用时,好像并没有区别。(中间执行的代码处理时间超过100ms时)millisec一般在多个setTimeout一起使用的时,需要区分哪个先加入到队列的时候才有用,否则都可以设置成setTimeout(handler,0)1.7 SetTimeout 与 setInterval的区别setTimeout(function(){/* 代码块… */setTimeout(arguments.callee, 10);}, 10);setInterval(function(){/*代码块… */}, 10);setTimeout递归执行的代码必须是上一次执行完了并间格一定时间才再次执行比仿说: setTimeout延迟时间为1秒执行, 要执行的代码需要2秒来执行,那这段代码上一次与下一次的执行时间为3秒. 而不是我们想象的每1秒执行一次.setInterval是排队执行的比仿说: setInterval每次执行时间为1秒,而执行的代码需要2秒执行, 那它还是每次去执行这段代码, 上次还没执行完的代码会排队, 上一次执行完下一次的就立即执行, 这样实际执行的间隔时间为2秒这样的话在我看来, 如果setInterval执行的代码时间长度比每次执行的间隔段的话,就没有意义,并且队伍越来越长,内存就被吃光了.如果某一次执行被卡住了,那程序就会被堵死巨坑无比的setInterval定时器的代码可能在代码还没有执行完成再次被添加到队列,结果导致循环内的判断条件不准确,代码多执行几次,之间没有停顿。JavaScript已经解决这个问题,当使用setInterval()时,仅当没有该定时器的其他代码实例时才将定时器代码插入队列。这样确保了定时器代码加入到队列的最小时间间隔为指定间隔某些间隔会被跳过2.多个定时器的代码执行之间的间隔可能比预期要小大前端团队 > 前端动画实现 > image2017-11-28 14:24:25.png5处,创建一个定时器205处,添加一个定时器,但是onclick代码没执行完成,等待300处,onclick代码执行完毕,执行第一个定时器405处,添加第二个定时器,但前一个定时器没有执行完成,等待605处,本来是要添加第三个定时器,但是此时发现,队列中有了一个定时器,被跳过等到第一个定时器代码执行完毕,马上执行第二个定时器,所以间隔会比预期的小。二 CSS3动画1.tansitiontransition-property 要运动的样式 (all || [attr] || none)transition-duration 运动时间transition-delay 延迟时间transition-timing-function 运动形式ease:(逐渐变慢)默认值linear:(匀速)ease-in:(加速)ease-out:(减速)ease-in-out:(先加速后减速)cubic-bezier 贝塞尔曲线( x1, y1, x2, y2 ) http://matthewlein.com/ceaser/transition的完整写法如下img { transition: 1s 1s height ease;}单独定义成各个属性。img{ transition-property: height; transition-duration: 1s; transition-delay: 1s; transition-timing-function: ease;}/可以多个动画同时运动/用逗号隔开transition:1s width,2s height,3s background;/可以在动画完成时间之后添加动画延迟执行的时间/transition:1s width,2s 1s height,3s 3s background;过渡完成事件// Webkit内核: obj.addEventListener(‘webkitTransitionEnd’,function(){},false);// firefox: obj.addEventListener(’transitionend’,function(){},false);/tansition动画发生在样式改变的时候/function addEnd(obj,fn) —封装适应与各个浏览器的动画结束{ //动画执行完执行该函数 obj.addEventListener(‘WebkitTransitionEnd’,fn,false); obj.addEventListener(’transitionend’,fn,false); //标准}addEnd(oBox,function(){ alert(“end”); });// 面临两个bug:1.tansition中有多个动画时,每个执行完,都会有一个结束弹出 // 2.发生重复调用的情况–需要移除//移除动画执行完的操作function removeEnd(obj,fn)} obj.removeEventListener(’transitionend’,fn,false); obj.removeEventListener(‘WebkitTransitionEnd’,fn,false);{使用注意(1)不是所有的CSS属性都支持transitionhttp://oli.jp/2010/css-animatable-properties/http://leaverou.github.io/animatable/(2)transition需要明确知道,开始状态和结束状态的具体数值,才能计算出中间状态transition的局限transition的优点在于简单易用,但是它有几个很大的局限。(1)transition需要事件触发,所以没法在网页加载时自动发生。(2)transition是一次性的,不能重复发生,除非一再触发。(3)transition只能定义开始状态和结束状态,不能定义中间状态,也就是说只有两个状态。(4)一条transition规则,只能定义一个属性的变化,不能涉及多个属性。CSS Animation就是为了解决这些问题而提出的。2.transformrotate() 旋转函数 取值度数 deg 度数 -origin 旋转的基点skew() 倾斜函数 取值度数skewX()skewY()scale() 缩放函数 取值 正数、负数和小数scaleX()scaleY()translate() 位移函数translateX()translateY()Transform 执行顺序问题 — 后写先执行-webkit-transform:rotate(360deg);旋转原点可以是关键字+像素位置:相对于左上角作为零点:正为下,右-webkit-transform-origin:right bottom;-webkit-transform-origin:200px 200px;一个transform可以有多个值: -webkit-transform:rotate(360deg) scale(0.2);-webkit-transform:skewX(45deg);-webkit-transform:skewY(45deg); -webkit-transform:skew(15deg,30deg);3.Animation 关键帧——keyFrames只需指明两个状态,之间的过程由计算机自动计算关键帧的时间单位数字:0%、25%、100%等字符:from(0%)、to(100%)格式@keyframes 动画名称{动画状态}@keyframes miaov_test{from { background:red; }to { background:green; }}可以只有to必要属性animation-name 动画名称(关键帧名称)animation-duration 动画持续时间属性:animation-play-state 播放状态( running 播放 和paused 暂停 )animation-timing-function 动画运动形式linear 匀速。ease 缓冲。ease-in 由慢到快。ease-out 由快到慢。ease-in-out 由慢到快再到慢。cubic-bezier(number, number, number, number): 特定的贝塞尔曲线类型,4个数值需在[0, 1]区间内animation-delay 动画延迟只是第一次animation-iteration-count 重复次数/infinite为无限次animation-direction 播放前重置/动画是否重置后再开始播放alternate 动画直接从上一次停止的位置开始执行normal 动画第二次直接跳到0%的状态开始执行reversealternate-reverseanimation-fill-modeforwards 让动画保持在结束状态none:默认值,回到动画没开始时的状态。backwards:让动画回到第一帧的状态。both: 根据animation-direction(见后)轮流应用forwards和backwards规则。animation-play-statepausedrunning动画播放过程中,会突然停止。这时,默认行为是跳回到动画的开始状态,想让动画保持突然终止时的状态,就要使用animation-play-state属性大前端团队 > 前端动画实现 > image2017-11-28 14:29:8.pnganimation也是一个简写形式div:hover { animation: 1s 1s rainbow linear 3 forwards normal;}分解成各个单独的属性div:hover { animation-name: rainbow; animation-duration: 1s; animation-timing-function: linear; animation-delay: 1s; animation-fill-mode:forwards; animation-direction: normal; animation-iteration-count: 3;}Animation与Js的结合通过class,在class里加入animation的各种属性直接给元素加-webkit-animation-xxx样式animation的问题写起来麻烦没法动态改变目标点位置animation的函数:obj.addEventListener(‘webkitAnimationEnd’, function (){}, false);实例1:无缝滚动animation的stepeg: http://dabblet.com/gist/1745856animation-timing-function: steps(30, end)1.什么时候使用:animation默认以ease方式过渡,它会在每个关键帧之间插入补间动画,所以动画效果是连贯性的,除了ease,linear、cubic-bezier之类的过渡函数都会为其插入补间。但有些效果不需要补间,只需要关键帧之间的跳跃,这时应该使用steps过渡方式大前端团队 > 前端动画实现 > image2017-11-28 14:29:45.png线性动画: http://sandbox.runjs.cn/show/…帧动画:http://sandbox.runjs.cn/show/…2.step使用:语法:steps(number[, end | start])参数说明:number参数指定了时间函数中的间隔数量(必须是正整数)第二个参数是可选的,可设值:start和end,表示在每个间隔的起点或是终点发生阶跃变化,如果忽略,默认是end。大前端团队 > 前端动画实现 > image2017-11-28 14:30:37.png横轴表示时间,纵轴表示动画完成度(也就是0%~100%)。第一个图,steps(1, start)将动画分为1段,跳跃点为start,也就是说动画在每个周期的起点发生阶跃(即图中的空心圆 → 实心圆)。由于只有一段,后续就不再发生动画了。第二个图,steps(1, end)同样是将动画分为1段,但跳跃点是end,也就是动画在每个周期的终点发生阶跃,也是图中的空心圆 → 实心圆,但注意时间,是在终点才发生动画。第三个图,steps(3, start)将动画分为三段,跳跃点为start,动画在每个周期的起点发生阶跃(即图中的空心圆 → 实心圆)。在这里,由于动画的第一次阶跃是在第一阶段的起点处(0s),所以我们看到的动画的初始状态其实已经是 1/3 的状态,因此我们看到的动画的过程为 1/3 → 2/3 → 1 。第四个图,steps(3, end)也是将动画分为三段,但跳跃点为end,动画在每个周期的终点发生阶跃(即图中的空心圆 → 实心圆)。虽然动画的状态最终会到达100%,但是动画已经结束,所以100%的状态是看不到的,因此我们最终看到的动画的过程是0 → 1/3 → 2/3。https://idiotwu.me/study/timi…steps第一个参数的错误的理解:第一个参数 number 为指定的间隔数,即把动画分为 n 步阶段性展示,估计大多数人理解就是keyframes写的变化次数@-webkit-keyframes circle { 0% {background-position-x: 0;} 100%{background-position-x: -400px;} }@-webkit-keyframes circle { 0% {} 25%{} 50%{} 75%{} 100%{} }如果有多个帧动画@-webkit-keyframes circle { 0% {background-position-x: 0;} 50% {background-position-x: -200px;} 100%{background-position-x: -400px;} } 0-25 之间变化5次, 25-50之间 变化5次 ,50-75 之间变化5次,以此类推应用:Sprite 精灵动画 2D游戏https://idiotwu.me/css3-runni…4.3D转换父容器:transform-style(preserve-3d) 建立3D空间Perspective 景深Perspective- origin 景深基点子元素:Transform 新增函数rotateX()rotateY()rotateZ()translateZ()scaleZ()实例1:3D盒子http://beiyuu.com/css3-animation使用实例:requestAnimationFrame是什么js的一个API该方法通过在系统准备好绘制动画帧时调用该帧,从而为创建动画网页提供了一种更平滑更高效的方法使用var handle = setTimeout(renderLoop, PERIOD);var handle = window.requestAnimationFrame(renderLoop);window.cancelAnimationFrame(handle);为什么出现css:统一的向下兼容策略 IE8, IE9之流CSS3动画不能应用所有属性 scrollTop值。如果我们希望返回顶部是个平滑滚动效果CSS3支持的动画效果有限 CSS3动画的贝塞尔曲线是一个标准3次方曲线缓动(Tween)知识:Linear:无缓动效果Quadratic:二次方的缓动(t^2)Cubic:三次方的缓动(t^3)Quartic:四次方的缓动(t^4)Quintic:五次方的缓动(t^5)Sinusoidal:正弦曲线的缓动(sin(t))Exponential:指数曲线的缓动(2^t)Circular:圆形曲线的缓动(sqrt(1-t^2))Elastic:指数衰减的正弦曲线缓动超过范围的三次方缓动((s+1)t^3 – st^2)指数衰减的反弹缓动js:1.延迟时间固定导致了动画过度绘制,浪费 CPU 周期以及消耗额外的电能等问题2.即使看不到网站,特别是当网站使用背景选项卡中的页面或浏览器已最小化时,动画都会频繁出现大前端团队 > 前端动画实现 > image2017-11-28 14:31:6.png相当一部分的浏览器的显示频率是16.7ms搞个10ms setTimeout,就会是下面一行的模样——每第三个图形都无法绘制显示器16.7ms刷新间隔之前发生了其他绘制请求(setTimeout),导致所有第三帧丢失,继而导致动画断续显示(堵车的感觉),这就是过度绘制带来的问题requestAnimationFrame 与setTimeout相似,都是延迟执行,不过更智能,跟着浏览器的绘制走,如果浏览设备绘制间隔是16.7ms,那我就这个间隔绘制;如果浏览设备绘制间隔是10ms, 我就10ms绘制,浏览器(如页面)每次要重绘,就会通知(requestAnimationFrame)页面最小化了,或者被Tab切换当前页面不可见。页面不会发生重绘兼容性Android设备不支持,其他设备基本上跟CSS3动画的支持一模一样https://developer.mozilla.org… ...

March 1, 2019 · 2 min · jiezi

canvas 波浪效果

基于canvas的三次贝塞尔曲线(bezierCurveTo)<canvas id=“myCanvas”></canvas><script> var WAVE_HEIGHT = 200 //波浪变化高度 var SCALE = 0.5 // 绘制速率 var CYCLE = 360 / SCALE var TIME = 0 function initCanvas() { var c = document.getElementById(“myCanvas”) var width = window.screen.width var height = window.screen.height var ctx = c.getContext(“2d”) c.width = width c.height = height // start window.requestAnimationFrame(function() { draw(ctx, width, height) }) } function draw(ctx, width, height) { ctx.clearRect(0, 0, width, height) TIME = (TIME + 1) % CYCLE var angle = SCALE * TIME // 当前正弦角度 var dAngle = 60 // 两个波峰相差的角度 ctx.beginPath() ctx.moveTo(0, height * 0.5 + distance(WAVE_HEIGHT, angle, 0)) ctx.bezierCurveTo( width * 0.4, height * 0.5 + distance(WAVE_HEIGHT, angle, dAngle), width * 0.6, height * 0.5 + distance(WAVE_HEIGHT, angle, 2 * dAngle), width, height * 0.5 + distance(WAVE_HEIGHT, angle, 3 * dAngle) ) ctx.strokeStyle = “#ff0000” ctx.stroke() ctx.beginPath() ctx.moveTo(0, height * 0.5 + distance(WAVE_HEIGHT, angle, -30)) ctx.bezierCurveTo( width * 0.3, height * 0.5 + distance(WAVE_HEIGHT, angle, dAngle - 30), width * 0.7, height * 0.5 + distance(WAVE_HEIGHT, angle, 2 * dAngle - 30), width, height * 0.5 + distance(WAVE_HEIGHT, angle, 3 * dAngle - 30) ) ctx.strokeStyle = “#0000ff” ctx.stroke() function distance(height, currAngle, diffAngle) { return height * Math.cos((((currAngle - diffAngle) % 360) * Math.PI) / 180) } // animate window.requestAnimationFrame(function() { draw(ctx, width, height) }) } initCanvas()</script> ...

March 1, 2019 · 2 min · jiezi

学习 PixiJS — 碰撞检测

说明碰撞检测,用来检查两个精灵是否接触。Pixi 没有内置的碰撞检测系统, 所以这里我们使用一个名为 Bump 的库,Bump 是一个易于使用的2D碰撞方法的轻量级库,可与 Pixi 渲染引擎一起使用。它提供了制作大多数2D动作游戏所需的所有碰撞工具。使用 Bump 库要开始使用 Bump,首先直接用 script 标签,引入 js 文件<script src=“https://www.kkkk1000.com/js/bump.js"></script>然后创建它的实例let b = new Bump(PIXI);变量 b 现在代表 Bump 实例。可以使用它来访问 Bump 的所有碰撞方法。使用 Bump 的碰撞方法hithit 方法是一种通用碰撞检测功能。它会自动检测碰撞中使用的精灵种类,并选择适当的碰撞方法。这意味着你不必记住要使用 Bump 库中的许多碰撞方法的哪一个,你只需要记住一个 hit 。但是为了避免 hit 方法最后产生的效果和你想象的不一样,最好还是要了解一下 Bump 库中其他的方法。以下是 hit 方法最简单的使用形式:b.hit(sprite1, sprite2);如果两个精灵碰撞到了,就返回 true,没有碰撞到,则返回 false。查看示例在碰撞检测时,Bump 的方法默认精灵是矩形的,使用矩形碰撞检测的算法,如果你想让方法把一个精灵当做圆形,使用圆形碰撞检测的算法,需要将精灵的 circular 属性设置为 true 。anySprite.circular = true;如果你使用 hit 方法检测两个圆形精灵是否碰撞,你还需要将两个精灵的 diameter 属性设置为 true 。查看示例如果你希望精灵对碰撞作出反应,使它们不重叠,请将第三个参数设置为 true 。b.hit(sprite1, sprite2, true);这个防止重叠的功能,对于制作墙壁,地板或任何其他类型的边界非常有用。查看示例如果你想让精灵碰撞后反弹,请将第四个参数设置为 true。b.hit(sprite1, sprite2, true, true);注意: 如果需要精灵反弹,精灵还必须有速度属性,也就是 vx 和 vy 属性。查看示例设置第五个参数为 true 使 hit 方法使用精灵的全局坐标。在检测不同父容器的精灵之间的碰撞时,这很有用。b.hit(sprite1, sprite2, true, true, true);精灵的全局坐标是相对于画布左上角的位置。精灵的局部坐标是相对于其父容器的左上角的位置。如果要检查点对象是否与精灵碰撞,将点对象作为第一个参数,如下所示:b.hit({x: 200, y:120}, sprite);点对象是一个具有 x 和 y 两个属性的对象,x 和 y 表示了画布中一个点的坐标。查看示例hit 方法还允许你检查精灵和精灵组之间的碰撞。只需将精灵组作为第二个参数即可。在此示例中,精灵组是 spriteArray。b.hit(sprite, spriteArray, true, true, true);你将看到 hit 方法自动遍历精灵组中的所有精灵,并根据参数中的第一个精灵检测它们。这意味着你不必自己编写 for 循环或 forEach 循环。查看示例你还可以使用回调函数作为第六个参数。这对于检查单个精灵和精灵组之间的碰撞特别有用。如果发生碰撞,回调函数将运行,你可以访问碰撞返回值和碰撞中涉及的精灵。下面是如何使用这个特性来检测一个名为 sprite 的精灵和一个名为 spriteArray 的精灵组之间的碰撞。b.hit( sprite, spriteArray, true, true, true, function (collision, platform) { //collision 表示 sprite 的哪一边发生碰撞 //platform 表示 sprite 正在碰撞的精灵组中的精灵 console.log(collision); console.log(platform); });这是一种执行复杂碰撞检测的简洁方式,可以为你提供大量信息和低级控制,但不必手动遍历数组中的所有精灵。查看示例hit 方法的返回值会与你正在检查的精灵的种类相匹配。例如,如果两个精灵都是矩形,并且 hit 方法的第三个参数是 true,碰撞后,返回值表示参数中第一个矩形发生碰撞的一侧,如果没有发生碰撞,返回值就是 undefined 。示例:let collision = b.hit(rectangleOne, rectangleTwo, true);message.text = “参数中第一个矩形的碰撞侧是: " + collision;查看示例hit 方法只是 Bump 的许多低级碰撞方法的高级包装器。如果你更喜欢使用较低级别的方法,接下来会列出所有的这些方法。hitTestPoint最基本的碰撞检测是检查点对象是否与精灵碰撞。hitTestPoint 方法将帮助你解决这个问题。 hitTestPoint 方法需要两个参数:名称描述point具有 x 和 y 属性的点对象,x 和 y 表示了画布中一个点的坐标sprite精灵示例:let collision = b.hitTestPoint( { x: 180, y: 128 }, //具有 x 和 y 属性的点对象 sprite //需要检测的精灵)如果点对象与精灵碰撞,hitTestPoint 方法返回 true,否则返回 false。查看示例上面示例中的精灵被当作是矩形的,但 hitTestPoint 方法同样适用于圆形精灵。如果精灵具有 radius 属性,则 hitTestPoint 方法假定精灵是圆形的并且对它应用圆形碰撞检测算法。如果精灵没有 radius 属性,则该方法假定它是矩形。你可以给任何精灵一个 radius 属性。而一个更简单的方法是给精灵一个 circular 属性并将其设置为 true 。anySprite.circular = true;这样精灵就会应用圆形碰撞检测算法,并具有一个 radius 属性,该属性的值等于精灵宽度的一半。查看示例hitTestCirclehitTestCircle 方法用来检测两个圆形精灵之间的碰撞。b.hitTestCircle(sprite1,sprite2)作为参数传入 hitTestCircle 方法的精灵需要有 radius 属性,如果精灵碰撞则返回 true,因此你可以将其与 if 语句一起使用来检测碰撞,如下所示:if(b.hitTestCircle(sprite1,sprite2)){ message.text = “碰撞到了!”; //碰撞到后,将 vx 设置为0,停止移动 sprite1.vx=0;}查看示例circleCollision当移动的圆形精灵碰到没有移动的圆形精灵时,你可以使用 circleCollision 方法创建碰撞反应。参数:名称默认值描述circle1 移动的圆形精灵circle2 没有移动的圆形精灵bouncefalse用于确定第一个精灵碰撞到第二个精灵时是否应该反弹globalfalse是否使用精灵的全局坐标。如果要检测具有不同父容器的精灵之间的碰撞 ,这很有用注意: 如果你希望参数中第一个精灵碰撞到第二个精灵时反弹,那第一个精灵必须有速度属性,也就是 vx 和 vy 属性。查看示例movingCircleCollisionmovingCircleCollision 方法可以让两个移动的圆形精灵在碰撞时弹开,它们会以一种非常逼真的方式将速度传递给对方,从而使它们弹开。参数:名称默认值描述circle1 移动的圆形精灵circle2 移动的圆形精灵globalfalse是否使用精灵的全局坐标。如果要检测具有不同父容器的精灵之间的碰撞 ,b.movingCircleCollision(circle1, circle2)如果圆形精灵具有 mass 属性,则该值将用于帮助确定圆形精灵应该相互反弹的力。查看示例如果你有一堆移动的圆形精灵,你希望这些精灵都在碰撞后进行反弹,这个时候你需要把这些精灵进行两两检查,判断它们是否碰撞,这需要把这些精灵放在一个数组中,使用两层 for 循环,并且内层 for 循环的计数器比外层的 for 循环大1,这样就可以检测所有圆形精灵的碰撞情况。for (let i = 0; i < container.children.length; i++) { //碰撞检查中使用的第一个圆形精灵 var c1 = container.children[i]; for (let j = i + 1; j < container.children.length; j++) { //碰撞检查中使用的第二个圆形精灵 let c2 = container.children[j]; //检查碰撞情况,如果精灵发生碰撞,将精灵弹开 b.movingCircleCollision(c1, c2); }}你可以看到内层 for 循环的计数器开始就是一个大于外层 for 循环的数字:let j = i + 1这可以防止对任何一对精灵进行多次碰撞检测。Bump 库还有一个方便的方法 multipleCircleCollision,使用这个方法可以替代 for 循环的方式。这个方法会对每对精灵自动调用 movingCircleCollision,使它们互相反弹。 你可以在游戏循环中使用它来检查数组中的所有精灵,但是要注意数组中的精灵是不能重复的。示例:b.multipleCircleCollision(container.children);查看示例hitTestRectangle要确定两个矩形精灵是否碰撞,请使用 hitTestRectangle 方法:b.hitTestRectangle(rectangle1, rectangle2)如果矩形精灵碰撞,hitTestRectangletrue 方法返回 true,没有碰撞则返回 false。示例:if(b.hitTestRectangle(sprite1,sprite2)){ message.text = “碰撞到了!”;}else{ message.text = “没有碰到”;}查看示例rectangleCollisionrectangleCollision 方法使矩形精灵表现得好像它们有质量。它可以防止参数中的两个矩形精灵重叠。参数:名称默认值描述rectangle1 矩形精灵rectangle2 矩形精灵bouncefalse用于确定第一个精灵是否应该从第二个精灵反弹globaltrue是否使用精灵的全局坐标返回值:如果精灵碰撞到了,rectangleCollision 方法返回一个字符串值,告诉你第一个矩形精灵的哪一侧碰到了第二个矩形精灵。其值可能是 left,right,top 或 bottom 。如果没有碰撞到返回值就是 undefined 。示例:let collision = b.rectangleCollision(sprite2, sprite1);//碰撞发生在矩形1(第一个参数)的哪一侧switch (collision) { case “left”: message.text = “参数中的第一个精灵的 左侧 发生碰撞”; break; case “right”: message.text = “参数中的第一个精灵的 右侧 发生碰撞”; break; case “top”: message.text = “参数中的第一个精灵的 上方 发生碰撞”; break; case “bottom”: message.text = “参数中的第一个精灵的 下方 发生碰撞”; break; default: message.text = “没有发生碰撞”;}此示例代码将阻止矩形重叠,并在名为 message 的文本精灵中显示碰撞侧。rectangleCollision 方法具有非常有用的副作用。参数中的第二个精灵能够将第一个精灵推走。如果你需要类似于推箱子游戏中的那种功能,这会很有用。查看示例hitTestCircleRectanglehitTestCircleRectangle 方法可以检查圆形和矩形精灵之间的碰撞。参数:名称默认值描述circle 圆形精灵rectangle 矩形精灵globalfalse是否使用精灵的全局坐标返回值:如果精灵碰撞到了,hitTestCircleRectangle 方法同样返回一个字符串值,告诉你圆形精灵在哪里碰到了矩形精灵。其值可能是 topLeft,topMiddle,topRight,leftMiddle,rightMiddle,bottomLeft,bottomMiddle 或 bottomRight 。如果没有碰撞到返回值就是 undefined 。示例:let collision = b.hitTestCircleRectangle(circle, rectangle);if (collision) { message.text = “圆形精灵的 " + collision + " 侧,发生碰撞”;} else { message.text = “没有发生碰撞”;}查看示例circleRectangleCollision使用 circleRectangleCollision 方法让一个圆形精灵从矩形精灵的侧面或角反弹。参数:名称默认值描述circle 圆形精灵rectangle 矩形精灵bouncefalse是否使使精灵反弹globalfalse是否使用精灵的全局坐标示例:b.circleRectangleCollision(circle, rectangle, true);查看示例containcontain 方法可以将精灵限制在一定矩形区域内。参数:名称默认值描述sprite 精灵container 容器,这是一个对象,具有 x、y、width 和 height 属性,表示一个矩形区域。bouncefalse确定精灵在碰到容器边界时是否应该反弹。callbackFunction 回调函数,当精灵碰撞到容器边界时会调用它,并且会将 contain 方法的返回值作为参数传入这个回调函数。返回值:如果精灵碰撞到容器边界,contain 方法将返回一个 Set 对象,告诉你精灵撞到了哪一侧,它的值可能有 left,right,top 或 bottom ,如果精灵没有碰撞到容器边界, 返回值就是 undefined 。示例:let collision = b.contain(sprite, { x: 0, y: 0, width: 512, height: 512 }, true, callbackFunction);//发生碰撞时的回调函数function callbackFunction(collision) { console.log(“collision”, collision);}//如果发生碰撞,显示哪边的边界发生碰撞if (collision) { if (collision.has(“left”)) { message.text = “边界 左侧 发生碰撞”; }; if (collision.has(“right”)) { message.text = “边界 右侧 发生碰撞”; }; if (collision.has(“top”)) { message.text = “边界 上方 发生碰撞”; }; if (collision.has(“bottom”)) { message.text = “边界 下方 发生碰撞”; };}上面的代码会将精灵限制在对象定义的512 x 512像素区域内。如果精灵碰撞到容器的边界,它将会反弹, 并且显示碰到了哪边的边界,callbackFunction(第四个参数)也将运行。查看示例contain 方法的另一个特点是,如果精灵具有 mass 属性,该值将用于以非常自然的方式抑制精灵的反弹。注意:使用 Bump 库时,最好给精灵设置上速度属性(vx,vy),因为 Bump 库中许多方法实现效果时,都需要用到这个两个属性。上一篇 学习 PixiJS — 补间动画 ...

February 25, 2019 · 3 min · jiezi

canvas学习总结

canvas描述HTML5 < canvas> 标签用于绘制图像(通过脚本,通常是 JavaScript)。不过,< canvas>元素本身并没有绘制能力(它仅仅是图形的容器)-必须使用脚本来完成实际的绘图任务。getContent()方法可返回一个对象,该对象提供了用于在画布上绘图的方法和属性。浏览器支持情况Internet Explorer 9、Firefox、Opera、Chrome以及Safari支持< canvas>及其属性和方法。(Internet Explorer 8以及更早的版本不支持< canvas>元素)1.canvas设置height、width1.通过html设置<canvas id=“canvas” width=“400” height=“400”></canvas>2.通过js设置<canvas id=“canvas”></canvas><script>var canvas=document.getElementById(‘canvas’);var cx=canvas.width=400;var cy=canvas=height=400;</script>3.通过css设置<canvas id=“canvas”></canvas><style>#canvas{ width:400px; height:400px;}</style>//使用css来设置宽高的画,画布就会按照300150的比例进行缩放,也就是将300150的页面显示在400400的容器中所以尽量使用HTML的width和height属性或者直接使用js动态设置宽高,不要使用css设置。获取Canvas对象创建好canvas标签后就要获取Canvas对象<canvas id=“canvas”></canvas><script>var canvas=document.getElementById(‘canvas’);var context=canvas.getContext(‘2d’);//可在画布上绘制文本、线条、矩形、圆形。</script>在画布上绘制圆创建画布<canvas id=“canvas” width=“400” height=‘400’></canvas>使用arc()画圆var canvas=document.getElementById(‘canvas’);var context=canvas.getContext(‘2d’);context.beginPath()//起始一条路径或重置当前路径context.arc(90,90,50,Math.PI2,false)// arc(x,y,r,start,stop)context.strokeStyle=“green”//设置或返回用于笔触的颜色、渐变或模式。context.stroke()//绘制已定义的路径。在画布上线条创建画布<canvas id=“canvas” width=“400” height=“400”></canvas>使用moveTo()定义线条开始坐标,lineTo()线条结束坐标var canvas=document.getElementById(‘canvas’);var context=canvas.getContext(‘2d’);context.beginPath();var grd=context.createLinearGradient(0,0,170,0);//createLinearGradient(x0,y0,x1,y1);创建线性渐变对象grd.grd.addColorStop(0,“green”);//规定渐变对象中的颜色和停止位置。grd.addColorStop(1,“white”);context.moveTo(10,10);context.lineTo(100,100);context.lineCap=“round”//定义设置或返回线条的结束端点样式 round圆形 butt默认 square方形context.lineWidth=10//设置线条宽度context.strokeStyle=grdcontent.stroke()绘制渐变文本html<canvas id=“canvas” height=“400” width=“400”></canvas>jsvar canvas=document.getElementById(‘canvas’);var context=getContext(‘2d’);context.beginPath();var grd=context.createLinearGradient(0,0,170,0);grd.addColorStop(0,“green”);grd.addColorStop(1,“white”);context.font=“30px Arial”//设置或返回文本内容的当前字体属性。context.fillStyle=grdcontext.fillText(“Hello World”,10,50);仅用于个人学习使用

February 19, 2019 · 1 min · jiezi

学习 PixiJS — 补间动画

说明补间动画指的是,我们可以通过为精灵的位置、比例、透明度,等属性,设置开始值和结束值,制作动画,动画中间需要的部分由软件自动计算填充。Pixi 没有内置补间引擎,但是你可以使用很多很好的开源的补间库,比如 Tween.js 和 Dynamic.js 。如果要制作非常专业的自定义补间效果,可以使用这两个库中的其中一个。但是现在我们要使用的是一个名为 Charm.js 的专门用于 Pixi 的补间库。使用 Charm 库要开始使用 Charm ,首先直接用 script 标签,引入 js 文件<script src=“https://www.kkkk1000.com/js/Charm.js"></script>然后创建它的实例let c = new Charm(PIXI);变量 c 现在代表 Charm 实例。和前面的文章中讲到的粒子效果一样,在调用 state 函数之后,必须为游戏循环中的每个帧更新补间。就是在游戏循环中调用 Charm 的 update 方法,如下所示:function gameLoop() { requestAnimationFrame(gameLoop); state(); c.update(); renderer.render(stage);}滑动补间Charm 最有用的补间效果之一是 slide 。使用 slide 方法可以使精灵从画布上的当前位置平滑移动到任何其他位置。slide 方法有七个参数,但只有前三个参数是必需的。名称默认值描述anySprite 需要移动的精灵finalXPosition 滑动结束时 x 坐标finalYPosition 滑动结束时 y 坐标durationInFrames60补间需要的帧数,也就是动画应该持续多长时间easingType"smoothstep"缓动类型yoyofalse用于确定精灵是否应在补间的起点和终点之间来回移动。delayTimeBeforeRepeat0一个以毫秒为单位的数字,用于确定精灵 yoyo 之前的延迟时间。示例:以下是如何使用 slide 方法使精灵用120帧从原始位置移动到坐标为(128,128)的位置的关键代码。c.slide(sprite, 128, 128, 120);效果图:查看示例如果你想让精灵在起点和终点之间来回移动,请将 yoyo(第六个参数)设置为 true,代码如下所示:c.slide(sprite, 128, 128, 120, “smoothstep”, true);查看示例补间对象Charm 所有的补间方法都返回一个补间对象,你可以这样创建:let slidePixie = c.slide(sprite, 80, 128, 120, “smoothstep”,true);slidePixie 就是补间对象,它包含一些有用的属性和方法,可以用于控制补间。其中一个是 onComplete 方法,它将在补间完成后立即运行。以下代码是精灵到达终点时如何使用 onComplete 方法在控制台中显示消息。let slidePixie = c.slide(sprite, 80, 128, 120, “smoothstep”,true);slidePixie.onComplete = () => console.log(“一次滑动完成”);如果将 yoyo (slide 方法的第六个参数)设置为 true,则每当精灵到达其起点和终点时,onComplete 方法都将运行。补间还有 pause 和 play 方法,可以停止和开始补间。slidePixie.pause();slidePixie.play();补间对象还具有 playing 属性,如果补间当前正在播放,则该属性值为 true。只不过有些补间方法返回的对象中直接有 playing 属性,有些补间方法返回的对象中的 playing 属性是在一个叫 tweens 的数组中, tweens 数组中包括了这个补间方法创建的所有补间对象。以 slide 方法为例,完成一个滑动需要创建 x 轴补间对象和 y 轴补间对象,这两个对象都放在了 tweens 数组中,这两个对象也都分别有 playing 属性。查看示例所有 Charm 的补间方法都返回你可以控制和访问的补间对象。设置缓动类型slide 方法的第四个参数是 easingType 。它是一个字符串,用于确定补间加速和减速的类型。这些类型共有15种可供选择,并且它们对于 Charm 的所有不同补间方法都是相同的。某些类型对应的会有一个基本类型,一个 squared 类型和一个cubed 类型。squared 类型和 cubed 类型只是将基本类型的效果放大而已。大多数 Charm 的补间效果的默认缓动类型是 smoothstep。Linear:linear,精灵从开始到停止保持匀速运动。Smoothstep:smoothstep,smoothstepSquared,smoothstepCubed。加速精灵并以非常自然的方式减慢速度Acceleration:acceleration, accelerationCubed。逐渐加速精灵并突然停止。如果要更加平滑的加速效果,请使用 sine,sineSquared 或 sineCubed。Deceleration:deceleration,decelerationCubed。突然加速精灵并逐渐减慢它。要获得更加平滑的减速效果,请使用inverseSine,inverseSineSquared或inverseSineCubed。Bounce:bounce 10 -10 ,这将使精灵到达起点和终点时略微反弹,更改乘数10和 -10,可以改变效果。查看示例使用 slide 进行场景过渡你在游戏或应用程序中肯定要做的一件事就是让场景过渡,然后将新场景滑入视图。它可能是你游戏的标题滑动以显示游戏的第一级,或者可能是一个菜单,可以滑动以显示更多的应用程序内容。你可以使用 slide 方法执行此操作。首先,创建两个容器对象:sceneOne 和 sceneTwo,并将它们添加到舞台上。sceneOne = new Container();sceneTwo = new Container();stage.addChild(sceneOne);stage.addChild(sceneTwo);接下来,为每个场景创建精灵。制作一个像画布一样大的蓝色矩形; 并在矩形中间添加上 Scene One 的文字,将两者都添加到 sceneOne 容器中。再制作一个像画布一样大的红色矩形;并在矩形中间添加上Scene Two 的文字,将这两者添加到 sceneTwo 容器中。你最终得到的两个容器对象,如下图所示。以下是关键代码://1. Scene one sprites: //画蓝色矩形let blueRectangle = new PIXI.Graphics();blueRectangle.beginFill(0x014ACA);blueRectangle.drawRect(0, 0, canvasWith, canvasHeight);blueRectangle.endFill();sceneOne.addChild(blueRectangle);//添加文字,并在容器中居中let sceneOneText = new PIXI.Text(“Scene One”);sceneOneText.style = { fill: “#fff”, fontSize: “40px” };let sceneOneTextX = (canvasWith - sceneOneText.width) / 2;let sceneOneTextY = (canvasWith - sceneOneText.height) / 2;sceneOneText.position.set(sceneOneTextX, sceneOneTextY);sceneOne.addChild(sceneOneText);//2. Scene two sprites://画红色矩形let redRectangle = new PIXI.Graphics();redRectangle.beginFill(0xEF4631);redRectangle.drawRect(0, 0, canvasWith, canvasHeight);redRectangle.endFill();sceneTwo.addChild(redRectangle);//添加文字,并在容器中居中let sceneTwoText = new PIXI.Text(“Scene Two”);sceneTwoText.style = { fill: “#fff”, fontSize: “40px” };let sceneTwoTextX = (canvasWith - sceneTwoText.width) / 2;let sceneTwoTextY = (canvasHeight - sceneTwoText.height) / 2;sceneTwoText.position.set(sceneTwoTextX, sceneTwoTextY);sceneTwo.addChild(sceneTwoText);在一个真实的项目中,你可以为每个容器填充每个场景所需的精灵数量,你也可以为你的项目添加尽可能多的场景容器。接下来,将 sceneTwo 移开,使其位于画布的右边缘之外。代码如下所示:sceneTwo.x = canvasWith;这将在画布上显示 sceneOne,而 sceneTwo 在需要时会从左侧滑出,如下所示。sceneTwo 就在屏幕外等着。最后,使用 slide 方法从 sceneOne 过渡到 sceneTwo 。只需将 sceneOne 滑动到左侧,然后从右侧滑动 sceneTwo ,取代它的位置,代码如下。c.slide(sceneTwo, 0, 0);c.slide(sceneOne, -canvasWith, 0);下图显示了这段代码的效果。查看示例时间过渡你可以自定义一个 wait 函数在设定的时间间隔后进行过渡。function wait(duration = 0) { return new Promise((resolve, reject) => { setTimeout(resolve, duration); });}要使用 wait,请为其提供一个参数,它代表你希望等待的时间(以毫秒为单位)。以下是在延迟1秒(1000毫秒)后从 sceneOne 过渡到 sceneTwo 的方法。wait(1000).then(() => { c.slide(sceneTwo, 0, 0); c.slide(sceneOne, -canvasWith, 0);});查看示例其实在 Charm 库中已经定义了 wait 这个方法,原理和上面的 wait 函数是一样的。你可以这样使用它。c.wait(1000).then(() => { c.slide(sceneTwo, 0, 0); c.slide(sceneOne, -canvasWith, 0);});沿贝塞尔曲线移动如果你还不清楚什么是贝塞尔曲线,可以先看看这篇文章。slide 方法沿直线为精灵制作动画,但你也可以使用另一种方法(followCurve)使精灵沿贝塞尔曲线移动。首先,将贝塞尔曲线定义为4个坐标点的二维数组,如下所示:let curve = [ [sprite.x, sprite.y], //起始点 [108, 32], //控制点1 [176, 32], //控制点2 [196, 160] //结束点];followCurve 方法的参数如下:名称默认值描述anySprite 需要移动的精灵curve 贝塞尔曲线数组durationInFrames60补间需要的帧数,也就是动画应该持续多长时间easingType"smoothstep"缓动类型yoyofalse用于确定精灵是否应在补间的起点和终点之间来回移动。delayTimeBeforeRepeat0一个以毫秒为单位的数字,用于确定精灵 yoyo 之前的延迟时间。接下来,使用 Charm 的 followCurve 方法使精灵跟随该曲线。(提供 curve 数组作为第二个参数)c.followCurve( sprite, //需要移动的精灵 curve, //贝塞尔曲线数组 120, //持续时间,以帧为单位 “smoothstep”, //缓动类型 true, //yoyo 1000 //yoyo之前的延迟时间);效果图:如果你需要使精灵的中点沿着曲线移动,还需要设置精灵的锚点(anchor)居中,如下所示:sprite.anchor.set(0.5, 0.5);查看示例slide 和 followCurve 方法适用于简单的来回动画效果,但你也可以结合它们以使精灵遵循更复杂的路径。沿路径移动你可以使用 Charm 的 walkPath 方法连接一系列点,并使精灵移动到每个点。该系列中的每个点都称为 waypoint 。首先,从由坐标点组成的二维数组定位路径点开始,这些 waypoint 映射出你希望精灵遵循的路径。let waypoints = [ [32, 32], //要移动到的第一个坐标点 [32, 128], //要移动到的第二个坐标点 [300, 128], //要移动到的第三个坐标点 [300, 32], //要移动到的第四个坐标点 [32, 32] //要移动到的第五个坐标点];你可以根据需要使用任意多的 waypoint。walkPath 方法的参数如下:名称默认值描述anySprite 需要移动的精灵waypoints 坐标点的二维数组durationInFrames60补间需要的帧数,也就是动画应该持续多长时间easingType"smoothstep"缓动类型loopfalse用于确定精灵在到达结尾时是否从头开始yoyofalse用于确定精灵是否应在补间的起点和终点之间来回移动。delayBetweenSections0一个以毫秒为单位的数字,用于确定精灵在移动到路径的下一部分之前应该等待的时间。接下来,使用 walkPath 方法使精灵按顺序移动到所有这些点。(前两个参数是必需的)c.walkPath( sprite, //需要移动的精灵 waypoints, //坐标点的二维数组 300, //持续时间,以帧为单位 “smoothstep”, //缓动类型 true, //循环 true, //轮流反向播放动画 1000 //移动到路径的下一部分之前应该等待的时间);效果图:查看示例而使用 walkCurve 方法,可以使精灵遵循一系列连接的贝塞尔曲线。首先,创建任何贝塞尔曲线数组,描述你希望精灵遵循的路径。let curvedWaypoints = [ //第一条曲线 [[sprite.x, sprite.y],[75, 500],[200, 500],[300, 300]], //第二条曲线 [[300, 300],[250, 100],[100, 100],[sprite.x, sprite.y]]];每条曲线的四个点与 followCurve 方法中的相同:起始位置,控制点1,控制点2和结束位置。第一条曲线中的最后一个点应与下一条曲线中的第一个点相同。你可以根据需要使用尽可能多的曲线。walkCurve 方法的参数如下:名称默认值描述anySprite 需要移动的精灵curvedWaypoints 贝塞尔曲线的坐标点的数组durationInFrames60补间需要的帧数,也就是动画应该持续多长时间easingType"smoothstep"缓动类型yoyofalse用于确定精灵是否应在补间的起点和终点之间来回移动。delayBeforeContinue0一个以毫秒为单位的数字,用于确定精灵yoyo之前的延迟时间。接下来,提供 curvedWaypoints 数组作为 walkCurve 方法中的第二个参数,来试试这个方法。c.walkCurve( sprite, //需要移动的精灵 curvedWaypoints, //贝塞尔曲线的坐标点的数组 300, //持续时间,以帧为单位 “smoothstep”, //缓动类型 true, //循环 true, //轮流反向播放动画 1000 //移动到路径的下一部分之前应该等待的时间);效果图:查看示例使用 walkPath 和 walkCurve 将为你提供了一个很好的开端,它们可以为游戏制作一些有趣的动画。更多补间效果Charm 有许多其他内置的补间效果,你会发现它们在游戏和应用程序中有很多用处。下面是其他一些效果的介绍。fadeOut 和 fadeInfadeOut 方法使精灵逐渐变得透明,fadeIn 方法使精灵从透明逐渐显现。这两个方法需要的参数是一样的。参数:名称默认值描述anySprite 需要产生效果的精灵durationInFrames60持续的帧数示例:c.fadeOut(anySprite);c.fadeIn(anySprite);查看示例pulse使用 pulse 方法可以使精灵以稳定的速率连续淡入淡出。参数:名称默认值描述anySprite 需要产生效果的精灵durationInFrames60淡入淡出应该持续的帧数,也就是持续时间minAlpha0精灵可以减少到的最小的透明度值示例:c.pulse(anySprite);查看示例如果你只希望精灵在再次淡入之前变为半透明,请将第三个参数设置为0.5,如下所示:c.pulse(anySprite, 60, 0.5);scale你可以使用 scale 方法让精灵产生缩放效果。参数:名称默认值描述anySprite 需要产生效果的精灵endScaleX0.5x 轴缩放的比例endScaleY0.5y 轴缩放的比例durationInFrames60持续时间,以帧为单位示例:c.scale( sprite, //精灵 0.1, //x轴缩放的比例 0.1, //y轴缩放的比例 100 //持续时间,以帧为单位);查看示例breathe如果你希望缩放效果来回 yoyo,请使用 breathe 方法。它是一种缩放效果,使精灵看起来好像在呼吸。参数:名称默认值描述anySprite 需要产生效果的精灵endScaleX0.5x 轴缩放的比例endScaleY0.5y 轴缩放的比例durationInFrames60持续时间,以帧为单位yoyotrue是否轮流反向播放delayBeforeRepeat0一个以毫秒为单位的数字,用于确定精灵 yoyo 之前的延迟时间。示例:c.breathe( sprite, //精灵 0.1, //x轴缩放的比例 0.1, //y轴缩放的比例 100, //持续时间,以帧为单位 true, //轮流反向播放 0, //yoyo 之间的延迟时间);查看示例strobe使用 strobe 方法通过快速改变精灵比例,使精灵看起来像闪光灯一样闪烁。参数:只需要传入一个精灵作为参数即可。示例:c.strobe(sprite);查看示例wobble使用 wobble 方法可以使精灵像果冻一样摆动。参数:只需要传入一个精灵作为参数即可。示例:c.wobble(sprite);查看示例如果你使用这些缩放补间效果(scale,breathe,strobe,或者 wobble),将精灵的锚点居中,就可以从精灵的中心进行缩放。sprite.anchor.set(0.5, 0.5);注意: 目前, Charm 这个库支持的 Pixi 版本是 3.0.11。如果使用比较高的版本会有一些问题,比如出现这样的警告。这是因为 Pixi 版本4.0.0起已弃用 ParticleContainer ,改为使用 particles.ParticleContainer 了。所以要解决这个问题需要把 Charm.js 文件中的 ParticleContainer 改为 particles.ParticleContainer 。上一篇 学习 PixiJS — 视觉效果 ...

February 18, 2019 · 3 min · jiezi

前端爬坑之旅--echarts渲染时canvas变为100px

开发要求:在实习时分配的一个页面,有三个标签,默认加载的是一个table,后两个标签都是echarts图表,但是三部分用的是相同的数据,包括分页。问题描述:刚开始设置图表渲染为默认加载,通过v-show控制所要展示的标签,但是图表缩小为100px,需要等一段时间后才会恢复。(菜鸡实习生被折磨了很久)问题分析:echarts不会自动渲染,经常改了数据进入页面需要刷新才能显示新得页面,所以可以从重绘和首次加载两方向去解决。解决方法1:重绘 使用watch监听,定义宽高,传进去重新绘制解决方法2:点击时再绘制 给标签绑定点击事件,在点击时发送请求进行判读,读取数据绘制(我采用这种方法) this.drawLine();在点击开始绘制图形,

February 18, 2019 · 1 min · jiezi

canvas绘图按照contain或者cover方式适配,并居中显示

canvas绘图时drawImage,需要绘制的图片大小不同,比例各异,所以就需要像html+css布局那样,需要contain和cover来满足不同的需求。contain保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。图片按照contain模式放到固定盒子的矩形内,则需要对图片进行一定的缩放。原则是:如果图片宽高不等,使图片的长边能完全显示出来,则原图片高的一边缩放后等于固定盒子对应的一边,等比例求出另外一边,如果图片宽高相等,则根据固定盒子的宽高来决定缩放后图片的宽高,固定盒子的宽大于高,则缩放后的图片高等于固定盒子的高度,对应求出另外一边即可,反之亦然。 /** * @param {Number} sx 固定盒子的x坐标,sy 固定盒子的y左标 * @param {Number} box_w 固定盒子的宽, box_h 固定盒子的高 * @param {Number} source_w 原图片的宽, source_h 原图片的高 * @return {Object} {drawImage的参数,缩放后图片的x坐标,y坐标,宽和高},对应drawImage(imageResource, dx, dy, dWidth, dHeight) / function containImg(sx, sy , box_w, box_h, source_w, source_h){ var dx = sx, dy = sy, dWidth = box_w, dHeight = box_h; if(source_w > source_h || (source_w == source_h && box_w < box_h)){ dHeight = source_hdWidth/source_w; dy = sy + (box_h-dHeight)/2; }else if(source_w < source_h || (source_w == source_h && box_w > box_h)){ dWidth = source_wdHeight/source_h; dx = sx + (box_w-dWidth)/2; } return{ dx, dy, dWidth, dHeight } } var c=document.getElementById(“myCanvas”); var ctx=c.getContext(“2d”); ctx.fillStyle = ‘#e1f0ff’; //固定盒子的位置和大小–图片需要放在这个盒子内 ctx.fillRect(30, 30, 150, 200); var img = new Image(); img.onload = function () { console.log(img.width,img.height); var imgRect = containImg(30,30,150,200,img.width,img.height); console.log(‘imgRect’,imgRect); ctx.drawImage(img, imgRect.dx, imgRect.dy, imgRect.dWidth, imgRect.dHeight); } img.src = “./timg2.jpg”; //注:img预加载模式下,onload应该放在为src赋值的上面,以避免已有缓存的情况下无法触发onload事件从而导致onload中的事件不执行的情况发生cover保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。原理:按照固定盒子的比例截取图片的部分 /** * @param {Number} box_w 固定盒子的宽, box_h 固定盒子的高 * @param {Number} source_w 原图片的宽, source_h 原图片的高 * @return {Object} {截取的图片信息},对应drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)参数 / function coverImg(box_w, box_h, source_w, source_h){ var sx = 0, sy = 0, sWidth = source_w, sHeight = source_h; if(source_w > source_h || (source_w == source_h && box_w < box_h)){ sWidth = box_wsHeight/box_h; sx = (source_w-sWidth)/2; }else if(source_w < source_h || (source_w == source_h && box_w > box_h)){ sHeight = box_hsWidth/box_w; sy = (source_h-sHeight)/2; } return{ sx, sy, sWidth, sHeight } } var c=document.getElementById(“myCanvas”); var ctx=c.getContext(“2d”); ctx.fillStyle = ‘#e1f0ff’; //固定盒子的位置和大小–图片需要放在这个盒子内 ctx.fillRect(30, 30, 150, 200); var img = new Image(); img.onload = function () { console.log(img.width,img.height); var imgRect = coverImg(150,200,img.width,img.height); console.log(‘imgRect’,imgRect); ctx.drawImage(img, imgRect.sx, imgRect.sy, imgRect.sWidth, imgRect.sHeight, 30, 30, 150, 200); } img.src = “./timg2.jpg”; //注:img预加载模式下,onload应该放在为src赋值的上面,以避免已有缓存的情况下无法触发onload事件从而导致onload中的事件不执行的情况发生 ...

February 15, 2019 · 2 min · jiezi

JS图片压缩预览/下载

前言好像没啥好说的~大概做法先由filereader获取图片的base64,控制图片生成,但不显示。然后将图片按比例设置好压缩后的宽高绘制在canvas画布上。之后利用canvas的自带方法再次转换成base64,再对base64进行解码存储到数组缓存区,生成blob,然后创建下载链接就完了。上代码,看注释就完了//html<input type=“file” id=“file”> //这里选择图片<canvas id=“canvas”></canvas> //canvas画布//jsdocument.getElementById(‘file’).onchange = function () { console.log(this.files[0]); //注意这个files是数组 reader.readAsDataURL(this.files[0]); var reader = new FileReader(); reader.onload = function (e) { //下面这三行就可以实现文件选择了图片以后,预览的功能,但是有些图片可能太大了影响页面观感,得统一缩小下。 //var img = new Image(); // img.src = e.target.result; // document.body.appendChild(img); render(e.target.result) //这个方法实现图片的压缩下载 } } var MAX_H = 100; function render(src){ // 创建一个 Image 对象 var image = new Image(); // 设置src属性,加载图片内容,此时还未压缩 image.src = src; // 绑定 load 事件处理器,加载完成后执行 image.onload = function(){ // 获取 canvas DOM 对象 var canvas = document.getElementById(“canvas”); // 如果高度超标 if(image.height > MAX_H) { // 宽度等比例缩放 *= image.width *= MAX_H / image.height; image.height = MAX_H; } // 获取 canvas的 2d 环境对象, 有些上古浏览器不支持canvas var ctx = canvas.getContext(“2d”); // canvas清屏 ctx.clearRect(0, 0, canvas.width, canvas.height); // 把canvas宽高设置为图片宽高 canvas.width = image.width; canvas.height = image.height; // 将图像绘制到canvas上 //drawImage(img,startX,startY,endX,endY) ctx.drawImage(image, 0, 0, image.width, image.height); //将绘制好的canvas图像转为DataURL //toDataURL(图片类型,图片质量),这个图片质量越高就越清晰(相同宽高) //canvas.toDataURL 返回的默认格式就是 image/png var data = canvas.toDataURL(‘image/jpeg’,0.5); //获取图片的dataUrl转成blob //这下面转blob的代码我也没搞懂,无百度了DataURL转blob就是这些代码了 data = data.split(’,’)[1]; data = window.atob(data); var ia = new Uint8Array(data.length); for (var i = 0; i < data.length; i++) { ia[i] = data.charCodeAt(i); }; var blob = new Blob([ia], { type: “image/jpeg” }); //生成blob文件的下载链接,把链接附在a便签上,把a便签加入dom中,点击就可以下载啦 var url3 = URL.createObjectURL(blob); var a = document.createElement(‘a’); a.href = url3; a.text = ‘测试图片’; a.download = ‘mytest.jpg’; document.body.appendChild(a); }; }; 效果预览后语压缩后上传的操作,这里就不写了,百度下blob如何生成file上传即可。 ...

February 15, 2019 · 1 min · jiezi

canvas中的拖拽、缩放、旋转 (下) —— 代码实现

写在前面本文首发于公众号:符合预期的CoyPandemo体验地址及代码在这里:请用手机或浏览器模拟手机访问上一篇文章介绍了canvas中的拖拽、缩放、旋转中涉及到的数学知识。可以点击下面的链接查看。canvas中的拖拽、缩放、旋转 (上) —— 数学知识准备。代码准备 - 如何在canvas中画出一个带旋转角度的元素在canvas中,如果一个元素带有一个旋转角度,可以直接变化canvas的坐标轴来画出此元素。举个例子,代码整体思路整个demo的实现思路如下:用户开始触摸(touchstart)时,获取用户的触摸对象,是Sprite的本体?删除按钮?缩放按钮?旋转按钮?并且根据各种情况,对变化参数进行初始化。用户移动手指(touchmove)时,根据手指的坐标,更新stage中的所有元素的位置、大小,记录变化参数。修改对应sprite的属性值。同时对canvas进行重绘。用户一旦停止触摸(touchend)时,根据变化参数,更新sprite的坐标,同时对变化参数进行重置。需要注意的是,在touchmove的过程中,并不需要更新sprite的坐标,只需要记录变化的参数即可。在touchend过程中,再进行坐标的更新。坐标的唯一用处,就是判断用户点击时,落点是否在指定区域内。代码细节首先,声明两个类:Stage和Sprite。Stage表示整个canvas区域,Sprite表示canvas中的元素。我们可以在Stage中添加多个Sprite,删除Sprite。这两个类的属性如下。class Stage { constructor(props) { this.canvas = props.canvas; this.ctx = this.canvas.getContext(‘2d’); // 用一个数组来保存canvas中的元素。每一个元素都是一个Sprite类的实例。 this.spriteList = []; // 获取canvas在视窗中的位置,以便计算用户touch时,相对与canvas内部的坐标。 const pos = this.canvas.getBoundingClientRect(); this.canvasOffsetLeft = pos.left; this.canvasOffsetTop = pos.top; this.dragSpriteTarget = null; // 拖拽的对象 this.scaleSpriteTarget = null; // 缩放的对象 this.rotateSpriteTarget = null; // 旋转的对象 this.dragStartX = undefined; this.dragStartY = undefined; this.scaleStartX = undefined; this.scaleStartY = undefined; this.rotateStartX = undefined; this.rotateStartY = undefined; }}class Sprite { constructor(props) { // 每一个sprite都有一个唯一的id this.id = Date.now() + Math.floor(Math.random() * 10); this.pos = props.pos; // 在canvas中的位置 this.size = props.size; // sprite的当前大小 this.baseSize = props.size; // sprite的初始化大小 this.minSize = props.minSize; // sprite缩放时允许的最小size this.maxSize = props.maxSize; // sprite缩放时允许的最大size // 中心点坐标 this.center = [ props.pos[0] + props.size[0] / 2, props.pos[1] + props.size[1] / 2 ]; this.delIcon = null; this.scaleIcon = null; this.rotateIcon = null; // 四个顶点的坐标,顺序为:左上,右上,左下,右下 this.coordinate = this.setCoordinate(this.pos, this.size); this.rotateAngle = 0; // 累计旋转的角度 this.rotateAngleDir = 0; // 每次旋转角度 this.scalePercent = 1; // 缩放比例 }}demo中,点击canvas下方的红色方块时,会实例化一个sprite,调用stage.append时,会将实例化的sprite直接push到Stage的spriteList属性内。window.onload = function () { const stage = new Stage({ canvas: document.querySelector(‘canvas’) }); document.querySelector(’.red-box’).addEventListener(‘click’, function () { const randomX = Math.floor(Math.random() * 200); const randomY = Math.floor(Math.random() * 200); const sprite = new Sprite({ pos: [randomX, randomY], size: [120, 60], minSize: [40, 20], maxSize: [240, 120] }); stage.append(sprite); });}下面是Stage的方法:class Stage { constructor(props) {} // 将sprite添加到stage内 append(sprite) {} // 监听事件 initEvent() {} // 处理touchstart handleTouchStart(e) {} // 处理touchmove handleTouchMove(e) {} // 处理touchend handleTouchEnd() {} // 初始化sprite的拖拽事件 initDragEvent(sprite, { touchX, touchY }) {} // 初始化sprite的缩放事件 initScaleEvent(sprite, { touchX, touchY }) {} // 初始化sprite的旋转事件 initRotateEvent(sprite, { touchX, touchY }) {} // 通过触摸的坐标重新计算sprite的坐标 reCalSpritePos(sprite, touchX, touchY) {} // 通过触摸的【横】坐标重新计算sprite的大小 reCalSpriteSize(sprite, touchX, touchY) {} // 重新计算sprite的角度 reCalSpriteRotate(sprite, touchX, touchY) {} // 返回当前touch的sprite getTouchSpriteTarget({ touchX, touchY }) {} // 判断是否touch在了sprite中的某一部分上,返回这个sprite getTouchTargetOfSprite({ touchX, touchY }, part) {} // 返回触摸点相对于canvas的坐标 normalizeTouchEvent(e) {} // 判断是否在在某个sprite中移动。当前默认所有的sprite都是长方形的。 checkIfTouchIn({ touchX, touchY }, sprite) {} // 从场景中删除 remove(sprite) {} // 画出stage中的所有sprite drawSprite() {} // 清空画布 clearStage() {}}Sprite的方法:class Sprite { constructor(props) {} // 设置四个顶点的初始化坐标 setCoordinate(pos, size) {} // 根据旋转角度更新sprite的所有部分的顶点坐标 updateCoordinateByRotate() {} // 根据旋转角度更新顶点坐标 updateItemCoordinateByRotate(target, center, angle){} // 根据缩放比例更新顶点坐标 updateItemCoordinateByScale(sprite, center, scale) {} // 根据按钮icon的顶点坐标获取icon中心点坐标 getIconCenter(iconCoordinate) {} // 根据按钮icon的中心点坐标获取icon的顶点坐标 getIconCoordinateByIconCenter(center) {} // 根据缩放比更新顶点坐标 updateCoordinateByScale() {} // 画出该sprite draw(ctx) {} // 画出该sprite对应的按钮icon drawIcon(ctx, icon) {} // 对sprite进行初始化 init() {} // 初始化删除按钮,左下角 initDelIcon() {} // 初始化缩放按钮,右上角 initScaleIcon() {} // 初始化旋转按钮,左上角 initRotateIcon() {} // 重置icon的位置与大小 resetIconPos() {} // 根据移动的距离重置sprite所有部分的位置 resetPos(dirX, dirY) {} // 根据触摸点移动的距离计算缩放比,并重置sprite的尺寸 resetSize(dir) {} // 设置sprite的旋转角度 setRotateAngle(angleDir) {}}Stage的方法主要是处理和用户交互的逻辑,得到用户操作的交互参数,然后根据交互参数调用Sprite的方法来进行变化。代码在这里:https://coypan.info/demo/canvas-drag-scale-rotate.html写在后面本文介绍了文章开头给出的demo的详细实现过程。代码还有很大的优化空间。事实上,工作上的需求并没有要求【旋转】,只需要实现【拖拽】、【缩放】即可。在只实现【拖拽】和【缩放】的情况下,会容易很多,不需要用到四个顶点的坐标以及之前的那些复杂的数学知识。而在自己实现【旋转】的过程中,也学到了很多。符合预期。 ...

February 15, 2019 · 2 min · jiezi

Canvas 文本转粒子效果

通过粒子来绘制文本让人感觉很有意思,配合粒子的运动更会让这个效果更加酷炫。本文介绍在 canvas 中通过粒子来绘制文本的方法。实现原理总的来说要做出将文本变成粒子展示的效果其实很简单,实现的原理就是使用两张 canvas,一张是用户看不到的 A canvas,用来绘制文本;另一张是用户看到的 B canvas,用来根据 A 的文本数据来生成粒子。直观表示如图:创建离屏 canvasHTML 只需要放置主 canvas 即可:<!– HTML 结构 –><html><head> …</head><body> <canvas id=“stage”></canvas></body></html>然后创建一个离屏 canvas,并绘制文本:const WIDTH = window.innerWidth;const HEIGHT = window.innerHeight;const offscreenCanvas = document.createElement(‘canvas’);const offscreenCtx = offscreenCanvas.getContext(‘2d’);offscreenCanvas.width = WIDTH;offscreenCanvas.height = HEIGHT;offscreenCtx.font = ‘100px PingFang SC’;offscreenCtx.textAlign = ‘center’;offscreenCtx.baseline = ‘middle’;offscreenCtx.fillText(‘Hello’, WIDTH / 2, HEIGHT / 2);这时页面上什么也没有发生,但实际上可以想象在离屏 canvas 上,此时应该如图所示:核心方法 getImageData使用 canvas 的 getImageData 方法,可以获取一个 ImageData 对象,这个对象用来描述 canvas 指定区域内的像素数据。也就是说,我们可以获取 “Hello” 这个文本每个像素点的位置和颜色,也就可以在指定位置生成粒子,最后形成的效果就是粒子拼凑成文本了。要获取像素信息,需要使用 ImageData 对象的 data 属性,它将所有像素点的 rgba 值铺开成了一个数组,每个像素点有 rgba 四个值,这个数组的个数也就是 像素点数量 * 4。假设我选取了一个 3 * 4 区域,那么一共 12 个像素点,每个像素点有 rgba 四个值,所以 data 这个数组就会有 12 * 4 = 48 个元素。如果打印出 data,可以看到即从左往右,从上往下排列这些像素点的 rgba。当然我们要获取的区域必须要包含文本,所以应该获取整个离屏 canvas 的区域:const imgData = offscreenCtx.getImageData(0, 0, WIDTH, HEIGHT).data;生成粒子拿到 ImageData 后,通过遍历 data 数组,可以判断在离屏 canvas 的画布中,哪些点是有色彩的(处于文本中间),哪些点是没有色彩的(不在文本上),把那些有色彩的像素位置记下来,然后在主 canvas 上生成粒子,就 ok 了。首先创建一下粒子类:class Particle { constructor (options = {}) { const { x = 0, y = 0, color = ‘#fff’, radius = 5} = options; this.radius = radius; this.x = x; this.y = y; this.color = color; } draw (ctx) { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath(); }}遍历 data,我们可以根据透明度,也就是 rgba 中的第四个元素是否不为 0 来判断该像素是否在文本中。const particles = [];const skip = 4;for (var y = 0; y < HEIGHT; y += skip) { for (var x = 0; x < WIDTH; x += skip) { var opacityIndex = (x + y * WIDTH) * 4 + 3; if (imgData[opacityIndex] > 0) { particles.push(new Particle({ x, y, radius: 1, color: ‘#2EA9DF’ })); } }}我们用 particles 数组来存放所有的粒子,这里的 skip 的作用是遍历的步长,如果我们一个像素一个像素地扫,那么最后拼凑文本的粒子将会非常密集,增大这个值,最后产生的粒子就会更稀疏。最后在创建主 canvas 并绘制即可:const canvas = document.querySelector(’#stage’);canvas.width = WIDTH;canvas.height = HEIGHT;const ctx = canvas.getContext(‘2d’);for (const particle of particles) { particle.draw(ctx);}效果如下:完整代码见 01-basic-text-to-particles添加效果了解实现原理之后,其实其他的就都是给粒子添加一些动效了。首先可以让粒子有一些随机的位移,避免看上去过于整齐。const particles = [];const skip = 4;for (var y = 0; y < HEIGHT; y += skip) { for (var x = 0; x < WIDTH; x += skip) { var opacityIndex = (x + y * WIDTH) * 4 + 3; if (imgData[opacityIndex] > 0) { // 创建粒子时加入随机位移 particles.push(new Particle({ x: x + Math.random() * 6 - 3, y: y + Math.random() * 6 - 3, radius: 1, color: ‘#2EA9DF’ })); } }}效果如下:如果想实现变大的效果,如:这种要怎么实现呢,首先需要随机产生粒子的大小,这只需要在创建粒子时对 radius 进行 random 即可。另外如果要让粒子半径动态改变,那么需要区分开粒子的渲染半径和初始半径,并使用 requestAnimationFrame 进行动画渲染:class Particle { constructor (options = {}) { const { x = 0, y = 0, color = ‘#fff’, radius = 5} = options; this.radius = radius; // … this.dynamicRadius = radius; // 添加 dynamicRadius 属性 } draw (ctx) { // … ctx.arc(this.x, this.y, this.dynamicRadius, 0, 2 * Math.PI, false); // 替换为 dynamicRadius // … } update () { // TODO }}requestAnimationFrame(function loop() { requestAnimationFrame(loop); ctx.fillStyle = ‘#fff’; ctx.fillRect(0, 0, WIDTH, HEIGHT); for (const particle of particles) { particle.update(); particle.draw(ctx); }});那么关键就在于粒子的 update 方法要如何实现了,假设我们想让粒子半径在 1 到 5 中平滑循环改变,很容易让人联想到三角函数,如:横轴应该是与时间相关,可以再维护一个变量每次调用 update 的时候进行加操作,简单做也可以直接用时间戳来进行计算。update 方法示例如下:update () { this.dynamicRadius = 3 + 2 * Math.sin(new Date() / 1000 % 1000 * this.radius);}完整代码见 02-text-to-particles-with-size-changing ...

February 13, 2019 · 3 min · jiezi

使用Fabric.js玩转H5 Canvas

前言之前使用这个框架写过一个卡片DIY的项目,中间遇到很多问题都只能通过google或github issues才能解决,国内资料较少,所以才想写这篇文章来简单的做下总结,希望可以帮到其他人哈。附上个人项目地址:vue-card-diy 欢迎star~ ✨什么是Fabric.js?Fabric.js 是一个强大的H5 canvas框架,在原生canvas之上提供了交互式对象模型,通过简洁的api就可以在画布上进行丰富的操作。该框架是个开源项目,项目地址: githubFabric.js有什么功能?使用Fabric.js,你可以在画布上创建和填充对象; 比如简单的几何形状 - 矩形,圆形,椭圆形,多边形,自定义图片或由数百或数千个简单路径组成的更复杂的形状。 另外,还可以使用鼠标缩放,移动和旋转这些对象; 修改它们的属性 - 颜色,透明度,z-index等。也可以将画布上的对象进行组合。下面我将会介绍我常用的功能以及场景,更多功能可以参考 官方文档安装npm安装npm install fabric –save通过cdn引用<script src=“http://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.6/fabric.min.js"></script>初始化首先在html页面中写一个350 x 200的canvas标签, 这里不写宽高也行,后面可以通过js来设置宽高<canvas id=“canvas” width=“350” height=“200”></canvas>初始化fabric的canvas对象,创建一个卡片(后面都用card表示画布对象)const card = new fabric.Canvas(‘canvas’) // …这里可以写canvas对象的一些配置,后面将会介绍// 如果<canvas>标签没设置宽高,可以通过js动态设置card.setWidth(350)card.setHeight(200)就是这么简单,这样就创建了一个基本的画布。开始花样操作监听画布上的事件官方提供了很多事件,以下为常用的事件:object:added 添加图层object:modified 编辑图层object:removed 移除图层selection:created 初次选中图层selection:updated 图层选择变化selection:cleared 清空图层选中// 在canvas对象初始化后,通过以下方式监听// 比如监听画布的图层编辑事件card.on(‘object:modified’, (e) => { console.log(e.target) // e.target为当前编辑的Object // …旋转,缩放,移动等编辑图层的操作都监听到 // 所以如果有撤销/恢复的场景,这里可以保存编辑状态});设置画布背景// 读取图片地址,设置画布背景fabric.Image.fromURL(‘xx/xx/bg.jpg’, (img) => { img.set({ // 通过scale来设置图片大小,这里设置和画布一样大 scaleX: card.width / img.width, scaleY: card.height / img.height, }); // 设置背景 card.setBackgroundImage(img, card.renderAll.bind(card)); card.renderAll();});如果要设置画布的背景颜色,可以在canvas初始化时设置const card = new fabric.Canvas(‘canvas’, { backgroundColor: ‘blue’ // 画布背景色为蓝色});// 或者card.backgroundColor = ‘blue’;// 或者card.setBackgroundColor(‘blue’);向画布添加图层对象fabric.js提供了很多对象,除了基本的 Rect,Circle,Line,Ellipse,Polygon,Polyline,Triangle对象外,还有如 Image,Textbox,Group等更高级的对象,这些都是继承自Fabric的Object对象。下面我就介绍如何添加图片和文字,其他对象大同小异/*** 如何向画布添加一个Image对象?/// 方式一(通过img元素添加)const imgElement = document.getElementById(‘my-image’);const imgInstance = new fabric.Image(imgElement, { left: 100, // 图片相对画布的左侧距离 top: 100, // 图片相对画布的顶部距离 angle: 30, // 图片旋转角度 opacity: 0.85, // 图片透明度 // 这里可以通过scaleX和scaleY来设置图片绘制后的大小,这里为原来大小的一半 scaleX: 0.5, scaleY: 0.5});// 添加对象后, 如下图card.add(imgInstance);// 方式二(通过图片路径添加)fabric.Image.fromURL(‘xx/xx/vue-logo.png’, (img) => { img.set({ hasControls: false, // 是否开启图层的控件 borderColor: ‘orange’, // 图层控件边框的颜色 }); // 添加对象后, 如下图 canvas.add(img);});/** 如何向画布添加一个Textbox对象?*/const textbox = new fabric.Textbox(‘这是一段文字’, { left: 50, top: 50, width: 150, fontSize: 20, // 字体大小 fontWeight: 800, // 字体粗细 // fill: ‘red’, // 字体颜色 // fontStyle: ‘italic’, // 斜体 // fontFamily: ‘Delicious’, // 设置字体 // stroke: ‘green’, // 描边颜色 // strokeWidth: 3, // 描边宽度 hasControls: false, borderColor: ‘orange’, editingBorderColor: ‘blue’ // 点击文字进入编辑状态时的边框颜色});// 添加文字后,如下图card.add(textbox);获取当前选中的图层对象// 方式一this.selectedObj = card.getActiveObject(); // 返回当前画布中被选中的图层 // 方式二card.on(‘selection:created’, (e) => { // 选中图层事件触发时,动态更新赋值 this.selectedObj = e.target})旋转图层// 顺时针90°旋转const currAngle = this.selectedObj.angle; // 当前图层的角度const angle = currAngle === 360 ? 90 :currAngle + 90;this.selectedObj.rotate(angle);// 如果是通过滑块的方式控制旋转// this.selectedObj.rotate(slideValue);// 所有图层的操作之后,都需要调用这个方法card.renderAll()翻转图层// 水平翻转,同理垂直翻转改为scaleY属性this.selectedObj.set({ scaleX: -this.selectedObj.scaleX,})card.renderAll()移除图层card.remove(this.selectedObj) // 传入需要移除的objectcard.renderAll()控制画布上的图层层级向画布添加图层,默认是依次往上叠加,但是当你选中一个图层进入active状态时,该图层会默认置于顶层,如果像禁止选中图层时指定,可以:// 在画布初始化后设置card.preserveObjectStacking = true // 禁止选中图层时自定置于顶部设置之后,我选中vue logo就是这个样子,不会置顶。如何上移和下移图层?// 上移图层this.selectedObj.bringForward();// 下移图层this.selectedObj.sendBackwards();// 也可以使用canvas对象的moveTo方法,移至图层到指定位置card.moveTo(object, index);画布状态记录框架提供了如 toJSON 和 loadFromJSON 方法,作用分别为导出当前画布的json信息,加载json画布信息来还原画布状态。// 导出当前画布信息const currState = card.toJSON(); // 导出的Json如下图// 加载画布信息card.loadFromJSON(lastState, () => { card.renderAll();});将画布导出成图片const dataURL = card.toDataURL({ format: ‘jpeg’, // jpeg或png quality: 0.8 // 图片质量,仅jpeg时可用 // 截取指定位置和大小 //left: 100, //top: 100, //width: 200, //height: 200});Fabric.js的基本介绍就到这里,这个框架很强大,还有很多功能可以去试试,欢迎大家评论交流哈!如转载本文请注明文章作者及出处! ...

February 3, 2019 · 2 min · jiezi

学习 PixiJS — 视觉效果

平铺精灵平铺精灵是一种特殊的精灵,可以在一定的范围内重复一个纹理。你可以使用它们创建无限滚动的背景效果。要创建平铺精灵,需要使用带有三个参数的 TilingSprite 类(PIXI.extras.TilingSprite)用法:let tilingSprite = new PIXI.extras.TilingSprite(texture, width, height);参数:名称默认值描述texture 平铺精灵的纹理width100平铺精灵的宽度height100平铺精灵的高度除此之外,平铺精灵具有与普通精灵所有相同的属性,并且与普通精灵的工作方式相同。他们还有 fromImage 和 fromFrame 方法,就像普通精灵一样。以下是如何使用名称是 brick.jpg 的100 x 100像素的图像创建200 x 200像素的平铺精灵。并且从画布左上角偏移30像素。以下是关键代码:let tilingSprite = new PIXI.extras.TilingSprite( PIXI.loader.resources[imgURL].texture, 200, 200);tilingSprite.x = 30;tilingSprite.y = 30;下图显示了 brick.jpg 图像以及上面代码的效果。你可以使用 tilePosition.x 和 tilePosition.y 属性来移动平铺精灵使用的纹理。以下是如何将平铺精灵使用的纹理移动30像素。tilingSprite.tilePosition.x = 30;tilingSprite.tilePosition.y = 30;这里不是在移动平铺精灵,而是移动平铺精灵使用的纹理。下图是两种情况的对比。你还可以使用 tileScale.x 和 tileScale.y 属性更改平铺精灵使用的纹理的比例。以下是如何将平铺精灵使用的纹理的大小增加到1.5倍的关键代码:tilingSprite.tileScale.x = 1.5;tilingSprite.tileScale.y = 1.5;原图 与 上面代码实现的效果的对比:tileScale 和 tilePosition 都有一个 set 方法,可以一行代码设置 x 属性和 y 属性。参数:名称默认值描述x0新的 x 属性值y0新的 y 属性值用法:tilingSprite.tilePosition.set(30, 30);tilingSprite.tileScale.set(1.5, 1.5);平铺精灵是创建重复图像模式的便捷方式。因为你可以移动纹理的位置,所以你可以使用平铺精灵创建无缝的滚动背景。这对于许多类型的游戏都非常有用。让我们来看看如何做到这一点。首先,从无缝平铺图像开始。无缝图像是图案在各方面匹配的图像。如果并排放置图像的副本,它们看起来就像是一个连续的大图像,上面示例中用到的 brick.jpg 就是这种图像。接下来,使用此图像创建一个平铺精灵。然后在游戏循环中更新精灵的 tilePosition.x 属性。关键代码:function play() { tilingSprite.tilePosition.x -= 1;}效果图:查看示例你还可以使用此功能创建一个称为视差滚动的伪3D效果。就是在同一位置层叠多个这样的平铺精灵,并使看上去更远的图像移动得比更近的图像慢。就像下面这个示例一样!两张用于做平铺精灵的图像:实现的效果图:查看示例着色精灵有一个 tint 属性,给这个属性赋值一个十六进制颜色值可以改变精灵的色调。我们来试试吧!关键代码:sprite.tint = 0xFFFF660;原图 与 上面代码实现的效果的对比:查看示例每个精灵的 tint 属性默认值是白色(0xFFFFFF),也就是没有色调。如果你想改变一个精灵的色调而不完全改变它的纹理,就使用着色。蒙版Pixi 允许你使用 Graphics (图形)对象来屏蔽任何精灵或具有嵌套子精灵的容器。蒙版是隐藏在形状区域之外的精灵的任何部分的形状。要使用蒙版,先创建精灵和 Graphics 对象。然后将精灵的 mask 属性设置为创建的 Graphics 对象。示例:首先,用皮卡丘的图像创建一个精灵。然后创建一个蓝色正方形并定位在精灵的上方(形状的颜色并不重要)。最后,精灵的 mask 属性设置为创建的正方形对象。这样会只显示正方形区域内精灵的图像。精灵在正方形之外的任何部分都是不可见的。原图 与 使用蒙版后的对比:关键代码://创建精灵let Pikachu = new PIXI.Sprite(PIXI.loader.resources[imgURL].texture);//创建一个正方形对象let rectangle = new PIXI.Graphics();rectangle.beginFill(0x66CCFF);rectangle.drawRect(0, 0, 200, 200);rectangle.endFill();rectangle.x = 100;rectangle.y = 100;//给精灵设置蒙版Pikachu.mask = rectangle;查看示例你还可以为蒙版设置动画,去做出一些有趣的效果。而且如果是用 WebGL 渲染的话,还可以用精灵作为蒙版。下面这个示例是用三张图片做成精灵,然后把一个精灵作为蒙版,并且给蒙版设置动画的示例。效果图:查看示例混合模式blendMode 属性确定精灵如何与其下层的图像混合。如下所示,可以将它们应用于精灵:sprite.blendMode = PIXI.BLEND_MODES.MULTIPLY;以下是可以使用的17种混合模式的完整列表:没有混合NORMAL(正常)对比比较(饱和度模式)SOFT_LIGHT(柔光)HARD_LIGHT(强光)OVERLAY(叠加)对比比较(差集模式)DIFFERENCE(差值)EXCLUSION(排除)减淡效果(变亮模式)LIGHTEN(变亮)COLOR_DODGE(颜色减淡)SCREEN(滤色)ADD(线性减淡,添加)加深效果(变暗模式)DARKEN(变暗)COLOR_BURN(颜色加深)MULTIPLY(正片叠底)色彩效果(颜色模式)HUE(色相)SATURATION(饱和度)COLOR(颜色)LUMINOSITY(明度)这些混合模式和图像编辑器,比如 Photoshop 中使用的混合模式是一样的,如果你想尝试每种混合模式,你可以在 Photoshop 中打开一些图像,将这些混合模式应用于这些图像上,观察效果。注意: WebGL 渲染器仅支持 NORMAL,ADD,MULTIPLY 和 SCREEN 混合模式。任何其他模式都会像 NORMAL 一样。查看示例滤镜Pixi 拥有多种滤镜,可以将一些特殊效果应用于精灵。所有滤镜都在 PIXI.filters 对象中。滤镜是 Pixi 最好的功能之一,因为它们可以让你轻松创建一些特殊效果,否则只有通过复杂的低级 WebGL 编程才能实现这些效果。这是一个如何创建 BlurFilter (模糊滤镜)的示例(其他滤镜遵循相同的格式)://创建一个模糊滤镜let blurFilter = new PIXI.filters.BlurFilter();//设置模糊滤镜的属性blurFilter.blur = 20;//将模糊滤镜添加到精灵的滤镜数组中sprite.filters = [blurFilter];Pixi 的所有显示对象(Sprite 和 Container 对象)都有一个滤镜数组。要向精灵添加滤镜,先创建滤镜,然后将其添加到精灵的滤镜数组中。你可以根据需要添加任意数量的滤镜。sprite.filters = [blurFilter, sepiaFilter, displacementFilter];使用它就像使用其他普通数组一样。要清除所有精灵的滤镜,只需清除数组即可。sprite.filters = [];除了这些属性,所有滤镜还包括额外的 padding 和 uniforms 属性。padding 增加了滤镜区域周围的空间。uniforms 是一个可用于向 WebGL 渲染器发送额外值的对象。在日常使用中,你永远不必担心设置 uniforms 属性。PixiJS 在4.0.0版本的时候,将非核心滤镜转移到新的包 — pixi-filters,现在 PixiJS 内置的滤镜有下面这几种。AlphaFilter用来修改对象透明度的滤镜。 在其他一些文档中,你可能看到的是 VoidFilter 这个滤镜,这是因为在 PixiJS 的4.6.0版本的时候,才添加 AlphaFilter,而弃用 VoidFilter。BlurFilterBlurFilter 将高斯模糊应用于对象。可以分别为x轴和y轴设置模糊强度。BlurXFilterBlurXFilter 将水平高斯模糊应用于对象。BlurYFilterBlurYFilter 将垂直高斯模糊应用于对象。ColorMatrixFilterColorMatrixFilter 类允许你对 显示对象(displayObject) 上每个像素的 RGBA 颜色和 alpha 值应用5x4矩阵变换,以生成一组具有新的 RGBA 颜色和 alpha 值的结果。它非常强大!使用它可是实现黑白、调整亮度、调整对比度、去色、灰度、调整色调,等许多效果。DisplacementFilterDisplacementFilter 类使用指定纹理(称为置换贴图)中的像素值来执行对象的位移。你可以使用这个滤镜来实现扭曲的效果。在这篇文章中已经讲过什么是 DisplacementFilter(置换滤镜)了,并且文章中也有一个不错的示例。FXAAFilter快速近似抗锯齿滤镜。NoiseFilter杂色效果滤镜。注意:Pixi 的滤镜仅适用于 WebGL 渲染,因为 Canvas 绘图 API 太慢而无法实时更新它们。这里有一个示例,包含了 Pixi 中绝大部分的滤镜。查看示例视频纹理你可以将视频用作精灵的纹理,就像使用图像一样容易。使用 Texture 类的 fromVideo 方法就可以创建视频纹理。videoTexture = PIXI.Texture.fromVideo(videoUrl);videoSprite = new PIXI.Sprite(videoTexture);stage.addChild(videoSprite);或者,也可以使用 fromVideoUrl 方法从 URL 地址创建视频纹理。视频纹理只是一个普通的 HTML5 <video> 元素,你可以通过纹理的 baseTexture.source 属性访问它,如下所示:let videoSource = videoTexture.baseTexture.source;然后,你可以使用任何 HTML5 <video> 元素的属性和方法控制视频,例如 play 和 pause 。videoSource.play();videoSource.pause();查看 HTML <video> 元素的完整规范,可以知道所有可以使用的属性和方法。查看示例适配多种分辨率如果你对物理像素、设备独立像素、设备像素比,等一些名词还不熟悉,可以先看看这篇文章 。Pixi 会自动调整像素密度,以匹配运行内容的设备的分辨率。你所要做的就是为高分辨率和低分辨率提供不同的图像,Pixi 将帮助你根据当前的设备像素比选择正确的图像。注意:当你创建高分辨率图像时,可以将“@2x”添加到图像文件名称后面,以说明图像是支持高分辨率的屏幕,例如,Retina 屏幕。同时这也会设置精灵的 baseTexture.resolution 属性(sprite.texture.baseTexture.resolution)。第一步是找出当前的设备像素比。你可以使用 window.devicePixelRatio 方法执行此操作。将此值分配给变量。let displayResolution = window.devicePixelRatio;displayResolution 是一个描述设备像素比的数字。它由运行应用程序的设备自动提供。1是标准分辨率; 2是高密度分辨率; 你将越来越多地发现一些报告3的超高密度显示器。下一步是将此值分配给渲染选项的 resolution 属性。在创建 Pixi 应用时执行此操作,如下所示://创建一个 Pixi应用 需要的一些参数let option = { width: 640, height: 360, transparent: true, resolution: displayResolution}//创建一个 Pixi应用let app = new PIXI.Application(option);然后根据设备像素比选择正确的图像加载到纹理中。如下所示:let texture;if (displayResolution === 2) { //加载高分辨率图像 texture = PIXI.Texture.fromImage(“highResImage@2x.png”);} else { //加载普通分辨率图像 texture = PIXI.Texture.fromImage(“normalResImage.png”);}let anySprite = new PIXI.Sprite(texture);如果你需要知道加载纹理的设备像素比是多少,可以使用 texture 的 baseTexture.resolution 属性(texture.baseTexture.resolution)找出。查看示例绳(Rope)另一个有趣的效果是 Rope。它允许精灵像波浪一样振荡或像蛇一样滑行,如下图所示。首先,从想要变形的事物的图像开始。滑行蛇实际上是一个简单的直线图像,如下图所示。然后决定你想要独立移动蛇的段数。蛇图像的宽度为600像素,因此大约20个片段会产生很好的效果。将图像宽度除以段数,就是每个绳段的长度。let numberOfSegments = 20;let imageWidth = 600;let ropeSegment = imageWidth / numberOfSegments;接下来,创建一个包含20个 Point 对象的数组。每个 Point 的 x 位置(第一个参数)将与下一个 Point 分开一个 ropeSegment 的距离。let points = [];for (let i = 0; i < numberOfSegments; i++) { points.push(new PIXI.Point(i * ropeLength, 0));}现在使用 PIXI.mesh.Rope 方法 new 一个 Rope 对象。这个方法需要两个参数:一个是 Rope 对象使用的纹理一个是包含 Point 对象的数组let snake = new PIXI.mesh.Rope(PIXI.Texture.fromImage(‘snake.png’), points);将蛇添加到一个容器中,这样可以更容易定位。然后将容器添加到舞台并定位它。let snakeContainer = new PIXI.Container();snakeContainer.addChild(snake);stage.addChild(snakeContainer);snakeContainer.position.set(10, 150);现在为游戏循环中的 Point 对象设置动画。通过 for 循环将数组中的每个 Point 按照椭圆形的轨迹移动,形成波浪效果。count += 0.1;for (let i = 0; i < points.length; i++) { points[i].y = Math.sin((i * 0.5) + count) * 30; points[i].x = i * ropeLength + Math.cos((i * 0.3) + count) * numberOfSegments;}查看示例 这里还有一篇文章,讲的是用 Rope 来实现游动的锦鲤的效果,看上去也很好玩。总结本文主要聊了聊平铺精灵、着色、蒙版、混合模式、滤镜、视频纹理、适配多种分辨率、绳(Rope),相关的东西。如果你觉得文字解释的不清楚,在每小节中,都有一个或者多个相应的示例,你可以点开看看,而且示例中的注释也比较清楚。 还有就是因为 PixiJS 的 API 时常有变化,所以要注意 PixiJS 的版本,文中大部分示例用的版本是4.8.2,如果你在尝试使用的时候,发现和示例的效果不一样,可以先检查一下版本。如果文中有错误的地方,还请小伙伴们指出,万分感谢。 ...

February 3, 2019 · 2 min · jiezi

canvas——橡皮筋式线条绘图应用

什么叫橡皮筋式指画图时橡皮筋一样伸缩自如。。 例子如下???? 思路思路很简单,只有橡皮筋式绘制功能要注意,以下总结mousedown,mousemove,mouseup三个阶段的思路 mousedown:记录start位置,drag(记录是否处于拖动状态)设置为true,getImageData(橡皮筋效果关键1) mousemove:获取拖动时的位置pos,putImageData(对应getImageData,橡皮筋效果关键2),根据pos与start画直线 mouseup:drag恢复为false 关键就在于putImageData()与getImageData()这两个canvas的方法,putImageData()记录了鼠标点下时的图像,getImageData()对应还原。如果没有执行这两个方法就会出现以下的效果 putImageData()相当于把“扫描”出来的线都擦掉代码 <canvas id=“canvas” width=“600” height=“400” style=“border: 1px solid black;"> </canvas> <script type=“text/javascript”> let canvas = document.getElementById(‘canvas’), ctx = canvas.getContext(‘2d’), canvasLeft = canvas.getBoundingClientRect().left, //getBoundingClientRect()获取元素位置 canvasTop = canvas.getBoundingClientRect().top; let imageData; //记录图像数据 let start = new Map([[‘x’,null],[‘y’,null]]); let drag = false;//记录是否处于拖动状态 canvas.onmousedown = function (e) { let pos = positionInCanvas(e, canvasLeft, canvasTop); start.set(‘x’, pos.x); start.set(‘y’, pos.y); drag = true; //记录imageData imageData = ctx.getImageData(0,0,canvas.width,canvas.height); } canvas.onmousemove = function (e) { if(drag === true){ let pos = positionInCanvas(e, canvasLeft, canvasTop); //相当于把扫描出来的线都擦掉,重新画 ctx.putImageData(imageData, 0, 0); ctx.beginPath(); ctx.moveTo(start.get(‘x’), start.get(‘y’)); ctx.lineTo(pos.x, pos.y); ctx.stroke(); } } canvas.onmouseup = function (e) { drag = false; } function positionInCanvas (e, canvasLeft, canvasTop) {//获取canvas中鼠标点击位置 return { x:e.clientX - canvasLeft, y:e.clientY - canvasTop } } </script> ...

February 2, 2019 · 1 min · jiezi

canvas中的拖拽、缩放、旋转 (上) —— 数学知识准备

写在前面本文首发于公众号:符合预期的CoyPan最近做了一个移动端活动页的需求,大概就是diy一个页面。用户可以对物料进行拖动、缩放、旋转,来达到diy的目的。用DOM来实现是不现实的,我采用了canvas来实现和用户的交互。开发过程中,涉及到了canvas中对物料元素的拖动、缩放、旋转等。本文将详细介绍在不使用任何第三方库的情况下,如何实现这些功能。最终的效果demo,可以参考上面的gif图。demo体验地址在这里:请用手机或浏览器模拟手机访问本文先介绍整个需求中需要注意的数学知识。需求分析整个需求的大致流程是:用户点击选择一个元素,则将该元素画在canvas中。用户在canvas中对元素进行拖动、缩放、旋转等操作。用户可以删除canvas中的某个元素。在canvas中实现拖动、缩放、旋转等交互,最核心的两个点就是:用户触摸时,判断用户是否触摸到了元素、是否触摸到了元素的缩放,旋转按钮。用户移动手指时,根据手指的路劲,控制元素的运动。我们知道,canvas中最基础的是坐标系统。本次的需求中,两个最关键的点是:如何判断用户是否触摸到了某个元素,即触摸的落点问题。如何通过坐标控制canvas中的元素的运动:移动、缩放、旋转。落点问题:如何判断是否触摸到了某元素首先提供一种简单的方法,canvas中有一个isPointInPath方法可以判断一个落点是否在某个路径中。不过如果canvas中的元素是图片,那么我们必须在画每一张图片时,为其加上一个路径包裹起来。这是一种解决落点问题的方案。这里不做深入介绍了,我采用的是下面的方案。为了使问题简单化,可以大胆假设:canvas中的所有元素(图片、各种图形等)都是长方形的。这已经能覆盖大部分情况了。长方形四个顶点的坐标就能确定一个元素的位置了。而当用户触摸canvas时,通过触摸事件,可以拿到用户触摸的坐标。如果触摸坐标在长方形顶点坐标"内部",则表示触摸到了元素。于是,我们的问题可以抽象为:已知长方形四个顶点坐标,某点的坐标,如何判断这个点是否在这个长方形内部。如果长方形没有产生旋转,那么问题很简单,只用判断点的横纵坐标均在长方形的横纵坐标范围内即可。如果长方形产生了旋转,这种方法就没用了。要解决这个问题,先复习一下高中数学 。向量我们可以将二维平面上的坐标都转化为向量来计算,可以将问题简化很多。向量的叉积两个向量的叉积运算结果是一个向量而不是一个标量。叉积的方向与这两个向量所在的平面垂直。对于平面中的两个向量,第三维方向上的值都为0,其叉积的值为:换句话说,我们可以很方便地判断:一个平面中,在旋转角不超过180度的情况下,从一个向量到另外一个向量,是顺时针转动还是逆时针转动。直接以canvas的二维坐标系统为例:可以得到这样的结论:在canvas二维平面中,设向量A与向量B的叉积对应的二项式的值为m。如果m>0,则向量A顺时针转动一个角度(小于180度),就能够到达向量B的方向。如果m<0,则需要逆时针转动。判断落点在长方形内落点在长方形内的情景如下:于是,判断【是否触摸到了canvas中某元素】的函数就有了:/** * 判断落点是否在长方形内 * * @param {Array} point 落点坐标。 数组:[x, y] * @param {Array} rect 长方形坐标, 按顺序分别是:左上、右上、左下、右下。 * 数组:[[x1, y1], [x2, y2], [x3, y3], [x4, y4]] * * @return {boolean} /function isPointInRect(point, rect) { const [touchX, touchY] = point; // 长方形四个点的坐标 const [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] = rect; // 四个向量 const v1 = [x1 - touchX, y1 - touchY]; const v2 = [x2 - touchX, y2 - touchY]; const v3 = [x3 - touchX, y3 - touchY]; const v4 = [x4 - touchX, y4 - touchY]; if( (v1[0] * v2[1] - v2[0] * v1[1]) > 0 && (v2[0] * v4[1] - v4[0] * v2[1]) > 0 && (v4[0] * v3[1] - v3[0] * v4[1]) > 0 && (v3[0] * v1[1] - v1[0] * v3[1]) > 0 ){ return true; } return false;}旋转角度用户可以对canvas中的元素进行旋转,那么如何通过用户前后两次的触摸落点坐标求旋转角度呢?从上面的等式可以看出,点积和叉积都能求夹角。选用哪一个呢?在旋转的场景中,旋转的方向(逆时针or顺时针)是很重要的,而点积最终得到的只是一个标量,是没有方向。叉积是一个向量,是有方向的。我们选择叉积来计算旋转角度。1、一般以canvas中的元素的中心为旋转原点,用户在canvas中触摸移动时,通过事件监听函数得到的前后两次触摸点的位移是很小的,与旋转中心形成的向量夹角必然是小于90度的。2、向量的叉积正负值可以确定旋转方向。3、反正弦函数是在负90度到90度之间单调递增的。通过以上三点,可以得到:于是,在canvas中,可以用以下函数来计算连续两次触摸落点与旋转中心形成的旋转角度:/* * 计算旋转角度 * * @param {Array} centerPoint 旋转中心坐标 * @param {Array} startPoint 旋转起点 * @param {Array} endPoint 旋转终点 * * @return {number} 旋转角度 /function getRotateAngle(centerPoint, startPoint, endPoint) { const [centerX, centerY] = centerPoint; const [rotateStartX, rotateStartY] = startPoint; const [touchX, touchY] = endPoint; // 两个向量 const v1 = [rotateStartX - centerX, rotateStartY - centerY]; const v2 = [touchX - centerX, touchY - centerY]; // 公式的分子 const numerator = v1[0] * v2[1] - v1[1] * v2[0]; // 公式的分母 const denominator = Math.sqrt(Math.pow(v1[0], 2) + Math.pow(v1[1], 2)) * Math.sqrt(Math.pow(v2[0], 2) + Math.pow(v2[1], 2)); const sin = numerator / denominator; return Math.asin(sin);}已知旋转角度和初始坐标,求旋转后坐标已知旋转起点、旋转中心以及旋转角度,求旋转终点坐标的函数如下:/* * * 根据旋转起点、旋转中心和旋转角度计算旋转终点的坐标 * * @param {Array} startPoint 起点坐标 * @param {Array} centerPoint 旋转点坐标 * @param {number} angle 旋转角度 * * @return {Array} 旋转终点的坐标 */function getEndPointByRotate(startPoint, centerPoint, angle) { const [centerX, centerY] = centerPoint; const [x1, y1] = [startPoint[0] - centerX, startPoint[1] - centerY]; const x2 = x1 * Math.cos(angle) - y1 * Math.sin(angle); const y2 = x1 * Math.sin(angle) + y1 * Math.cos(angle); return [x2 + centerX, y2 + centerY];}拖拽、缩放拖拽和缩放在本次需求中,对于数学上的要求并不高。拖拽需要计算好触摸点横纵坐标的差值,加到canvas中的元素上即可。写在后面本文主要介绍了在canvas中实现拖拽、缩放、旋转等交互时,所需要的一些数学知识。如有不对的地方,欢迎指正。同时,如果有其他解决需求的思路,欢迎交流。下一篇文章将介绍【使用本文介绍的数学知识,来实现文章开头的demo】的过程。符合预期。 ...

February 1, 2019 · 2 min · jiezi

canvas学习笔记-贝塞尔曲线

3.4 贝塞尔曲线canvas提供了两个绘制贝塞尔曲线api:ctx.quadraticCurveTo(cpx, cpy, x, y);二次贝塞尔曲线,(cpx, cpy)控制点 (x, y)终点ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);三次贝塞尔曲线,(cp1x, cp1y)控制点一, (cp2x, cp2y)控制点二, (x, y)终点题外话:贝塞尔曲线的数学基础是早在 1912 年就广为人知的伯恩斯坦多项式。最早用来辅助汽车车体的工业设计。CSS3的transition-timing-function属性,取值就可以设置为一个三次贝塞尔曲线方程transition-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1)。canvas绘图示例:// 二次ctx.moveTo(200, 100);ctx.quadraticCurveTo(230, 250, 350, 200);// 三次ctx.moveTo(450, 250);ctx.bezierCurveTo(530, 150, 650, 300, 700, 200);蓝色是控制点问题一:那canvas是如何通过控制点来绘制出曲线的,或者如果不用这个,自己绘制曲线该如何操作呢:这个是n阶贝塞尔曲线的方程:我们重点看二(三)阶方程:B(t)是曲线上的点,t在01之间取值, P0起始点,P2终点,P1控制点t从01之间取值不断增大,B(t)不断取出曲线上的点,从P0移至P1const bx = (1-t)(1-t)start.x + 2t(1-t)control.x + ttend.x;const by = (1-t)(1-t)start.y + 2t*(1-t)control.y + tt*end.y;问题二:我咋知道控制点该怎么选,特别是起终点动态数据时(也就是说,我们使用时,往往只知道起点P0终点P1):这个根据曲线斜率,可视化需求可能选取的方式不一致,不过大致原理相似可以在起点和终点的垂直平分线上选一点作为控制点, 然后用一个参数来控制曲线的弯曲程度// curveness 弯曲程度(0-1)const cp = { x: ( start.x + end.x ) / 2 - ( start.y - end.y ) * curveness, y: ( start.y + end.y ) / 2 - ( end.x - start.x ) * curveness};题外话:关于cp点的求解:线段中点:const mid = [ ( start.x + end.x) / 2, ( start.y + end.y ) / 2 ];根据起点和终点也可以得到一个向量v: const v = [ end.x - start.x, end.y- start.y ]; 将这个向量顺时针旋转90度,得到一个垂直于它的向量v2:const v2 = [ v.y, -v.x ];那么中间控制点的坐标为(向量v2乘curveness加上中间点坐标)const cp = { x: mid.x + v2.x curveness, y: mid.y + v2.y curveness} = { x:( start.x + end.x ) / 2 - ( start.y - end.y ) * curveness, y:( start.y + end.y ) / 2 - ( end.x - start.x ) * curveness} ...

January 28, 2019 · 1 min · jiezi

canvas学习笔记-绘制矩形及路径(一)

矩形canvas只支持一种原生的图形绘制:矩形。所有其他的图形的绘制都至少需要生成一条路径。绘制矩形三种方法:// 绘制一个填充的矩形fillRect(x, y, width, height);// 绘制一个矩形的边框strokeRect(x, y, width, height);// 清除指定矩形区域,让清除部分完全透明。clearRect(x, y, width, height);矩形示例3. 路径图形的基本元素是路径。路径是点的集合。使用路径绘制图形一般步骤如下:1.beginPath()新建一条路径(有时需要创建路径起始点)2.lineTo,arc,rect等绘制路径3.closePath闭合路径(根据实际需求)4.stroke fill绘制或者填充(路径没有此步骤,图形不会显示)路径绘制常见方法// 直线路径lineTo(x, y)// 矩形路径rect(x, y, width, height)// 圆弧路径arc(x, y, radius, startAngle, endAngle, anticlockwise)// 椭圆路径(chrome37+)ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)// 二次贝塞尔曲线quadraticCurveTo(cp1x, cp1y, x, y)// 三次贝塞尔曲线bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)// Path2D(chrome36+, addPath chrome68+)new Path2D(path);

January 28, 2019 · 1 min · jiezi

canvas学习笔记-绘制简单路径

3.1 线段(直线路径)绘制线段一般步骤:moveTo(x,y) 移动画笔到指定的坐标点(x,y)lineTo(x,y) 使用直线连接当前端点和指定的坐标点(x,y)stroke() 根据当前的画线样式,绘制当前或已经存在的路径3.2 矩形路径绘制矩形路径一般步骤:rect(x, y, width, height) 矩形路径,坐标点(x,y),width height宽高stroke()或fill 根据当前的样式,绘制或填充路径也可使用前文提到的strokeRect或fillRect, 或者是通过lineTo拼接成矩形3.3 圆弧路径先看下绘制圆弧的api:ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);x, y 圆弧中心, radius 圆弧半径, startAngle 起始点, endAngle 圆弧终点, anticlockwise 默认为顺时针, true逆时针CSS中做旋转用到都是角度(deg),但是arc函数中表示角的单位是弧度,不是角度。角度与弧度的js表达式: 弧度 = (Math.PI/180) * 角度(deg)。这里弧度是以x轴正方向为基准、默认顺时针旋转的角度来计算图示:(图片来自大漠)ctx.beginPath();ctx.arc(200, 100, 100, 0, Math.PI / 2, false);ctx.closePath();ctx.stroke();ctx.fill();arc示例

January 28, 2019 · 1 min · jiezi

canvas学习笔记-2d画布基础

一. Canvas是啥< canvas > 是一个可以使用脚本(通常是js)来绘图的HTML元素< canvas > 最早由Apple引入WebKit,用于Mac OS X 的 Dashboard和 Safari如今,所有主流的浏览器都支持它(IE9+,更低版本需引入Explorer Canvas来支持)1. 开始画图(渲染上下文)< canvas > 元素创造了一个固定大小的画布,其上的渲染上下文CanvasRenderContext2D,可以用来绘制和处理要展示的内容。若要在canvas上绘图,首先得获取CanvasRenderContext2D2d渲染上下文。(此处指2d的,不谈webgl)const canvas = document.getElementById(‘mycanvas’);const ctx = canvas.getContext(‘2d’);ctx.fillStyle = ‘pink’;ctx.fillRect(10, 10, 300, 300);示例2. CanvasRenderContext2D的属性:通过设置上下文的属性,可以指定绘图的样式。所有属性如下:属性简介canvascanvas元素fillStyle用来填充路径的当前的颜色、模式或渐变font字体样式globalAlpha指定在画布上绘制的内容的不透明度globalCompositeOperation指定颜色如何与画布上已有的颜色组合(合成)lineCap指定线条的末端如何绘制lineDashOffset设置虚线偏移量lineJoin指定两条线条如何连接lineWidth指定画笔(绘制线条)操作的线条宽度miterLimit当 lineJoin 属性为 “miter” 的时候,这个属性指定了斜连接长度和线条宽度的最大比率shadowBlur模糊效果程度shadowColor阴影颜色shadowOffsetX阴影水平偏移距离shadowOffsetY阴影垂直偏移距离strokeStyle用于画笔(绘制)路径的颜色、模式和渐变textAlign文本的对齐方式textBaseline文字垂直方向的对齐方式3. Canvas宽高Canvas的宽高需要用属性值width,height来指定若未指定,则Canvas 的默认大小为300×150通过样式指定的宽高,只是canvas元素的显示大小,并不是绘图环境的大小canvas {width: 1000px;height: 600px;}<canvas id=“mycanvas” width=“1000” height=“600”></canvas><canvas id=“mycanvas1” width=“500” height=“300”></canvas><canvas id=“mycanvas2”></canvas>…ctx.fillStyle = “red”;ctx.fillRect(10, 10, 100, 100);宽高示例为什么样式设置了同样大小,显示却截然不同的情况呢?canvas本身有两套大小:一个是元素本身大小,一个是绘图表面(drawing surface)的大小如果通过width,height属性来设置,是同时修改了元素本身和绘图表面大小,如果canvas元素的大小不符合绘图表面大小时,则会对绘图表面进行缩放,使之符合元素本身大小,无特殊需求,建议直接使用canvas的width和height就好

January 28, 2019 · 1 min · jiezi

学习 PixiJS — 粒子效果

你如何创造火,烟,魔法和爆炸等效果?你制作了许多小精灵,几十,几百,甚至上千个精灵。然后对这些精灵应用一些物理效果,使它们的行为类似于你尝试模拟的元素。你还必须给他们一些关于它们应该如何出现和消失以及应该形成什么样的模式的规则。这些微小的精灵被称为粒子。你可以使用它们为游戏制作各种特效。使用 Dust 库PIXI 没有内置的制作粒子效果的功能,但你可以使用一个名为 Dust 的轻量级的库来制作它们。注意:Dust 是一种快速简便的方法,可以制作游戏所需的大部分粒子效果,但如果你需要功能更全面,更复杂的库,请查看 Proton 使用 Dust 库和使用 SpriteUtilities 库是一样的。首先直接用 script 标签,引入 js 文件<script src=“https://www.kkkk1000.com/js/dust.js"></script>然后创建它的实例d = new Dust(PIXI);变量 d 现在就代表 Dust 实例。接下来,在游戏循环中调用 Dust 的 update 方法,这个方法用于更新粒子。我们在上篇文章中制作的示例中有 gameLoop 和 play 两个函数 ,你可以在这两个函数中执行此操作。建议在 gameLoop 中执行此操作,就在调用 state 函数之后但在渲染阶段之前,如下所示:function gameLoop(){ requestAnimationFrame(gameLoop); state(); d.update(); renderer.render(stage);}制作粒子制作粒子需要用到 Dust 库的 create 方法 参数:名称类型默认值描述xnumber0粒子出现的 x 坐标ynumber0粒子出现的 y 坐标spriteFunctionfunction 一个函数,它返回要用于每个粒子的精灵,如果提供具有多个帧的精灵,Dust 将随机显示不同帧containerobject一个 PIXI 容器要添加粒子的容器numberOfParticlesnumber20要创建的粒子数gravitynumber0重力randomSpacingbooleantrue随机间隔minAnglenumber0最小角度maxAnglenumber6.28最大角度minSizenumber4最小尺寸maxSizenumber16最大尺寸minSpeednumber0.3最小速度maxSpeednumber3最大速度minScaleSpeednumber0.01最小比例速度maxScaleSpeednumber0.05最大比例速度minAlphaSpeednumber0.02最小alpha速度maxAlphaSpeednumber0.02最大alpha速度minRotationSpeednumber0.01最小旋转速度maxRotationSpeednumber0.03最大旋转速度返回值:返回一个数组,其中包含对用作粒子的所有精灵的引用,如果需要进行碰撞检测等原因必须访问它们,这可能很有用。现在我们来试试这个方法。示例代码:<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script> <script src=“https://www.kkkk1000.com/js/dust.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 let option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 let app = new PIXI.Application(option); //获取舞台 let stage = app.stage; //获取渲染器 let renderer = app.renderer; let playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); let su = new SpriteUtilities(PIXI); let d = new Dust(PIXI); //需要加载的图片的地址 let imgURL = “https://www.kkkk1000.com/images/learnPixiJS-ParticleEffects/star.png"; //加载图像,加载完成后执行setup函数 PIXI.loader.add(imgURL).load(setup); function setup() { stars = d.create( 128, //x 起始坐标 128, //y 起始坐标 () => su.sprite(imgURL), //返回要用于每个粒子的精灵 stage, //粒子的容器 50, //粒子数 0,//重力 false,//随机间隔 0, 6.28,//最小/最大角度 30, 90,//最小/最大尺寸 1, 3//最小/最大速度 ); //开始游戏循环 gameLoop(); } function gameLoop() { requestAnimationFrame(gameLoop); d.update(); renderer.render(stage); } </script></body></html>查看效果使用 ParticleContainer在前面的示例代码中,我们创建的粒子都被添加到根容器(第四个参数)。但是,你可以将粒子添加到任何你喜欢的容器或任何其他精灵。PIXI 有一个叫 ParticleContainer 的方法,任何在 ParticleContainer 里的精灵都会比在一个普通的 Container 的渲染速度快2到5倍。到这里可以了解 ParticleContainer 如果要对粒子使用 ParticleContainer,只需在 create 方法的第四个参数中添加要使用的 ParticleContainer 对象的名称。以下是修改前面的示例代码以将粒子添加到名为 starContainer 的 ParticleContainer 的方法。//创建ParticleContainer并将其添加到stagelet starContainer = new PIXI.particle.ParticleContainer( 1500, { alpha: true, scale: true, rotation: true, uvs: true });stage.addChild(starContainer);function setup() { //创建星形粒子并将它们添加到starContainer stars = d.create( 128, //x 起始坐标 128, //y 起始坐标 () => su.sprite(imgURL), starContainer, //粒子的容器 50, //粒子数 0,//重力 false,//随机间隔 0, 6.28,//最小/最大角度 30, 90,//最小/最大尺寸 1, 3//最小/最大速度 ); //开始游戏循环 gameLoop();}查看效果 ParticleContainers 针对推送数千个精灵进行了优化,因此,除非你为很多粒子设置动画,否则你可能不会注意到对于使用普通 Container 对象的性能提升。使用粒子发射器create 方法会产生一次粒子爆发,但通常你必须产生连续的粒子流。你可以在粒子发射器的帮助下完成此操作。粒子发射器以固定的间隔产生粒子以产生流效果,你可以使用 Dust 的 emitter 方法创建一个粒子发射器。发射器具有 play 和 stop 方法,可让打开和关闭粒子流,并可以定义粒子的创建间隔。下面的代码是使用 Dust 的 emitter 方法的一般格式。它需要两个参数。第一个参数是创建粒子间隔(以毫秒为单位)。第二个参数与我们在前面的示例中使用的 create 方法相同。let particleStream = d.emitter( 100, () => d.create(););任何100毫秒或更短的间隔值将使颗粒看起来以连续流的形式流动。这里有一些产生星形喷泉效果的代码。let particleStream = d.emitter( 100, () => d.create( 128, 128, () => su.sprite(imgURL), stage, 30, 0.1, false, 3.14, 6.28, 30, 60, 1, 5 ) );第六个参数,0.1,是重力。将重力设置为更高的数字,粒子将更快的下落。角度介于3.14和6.28之间。这使得粒子出现在其原点之上的半月形大小的角度内。下图说明了如何定义该角度。星星在中心原点处创建,然后在圆圈的上半部分向上飞出。然而,星星在重力的作用下,最终将落在画布的底部,这就是产生星形喷泉效果的原因。你可以使用 emitter 的 play 和 stop 方法在代码中随时打开或关闭粒子流,如下所示:particleStream.play();particleStream.stop();效果图:完整代码:<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script> <script src=“https://www.kkkk1000.com/js/dust.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 let option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 let app = new PIXI.Application(option); //获取舞台 let stage = app.stage; //获取渲染器 let renderer = app.renderer; let playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); let su = new SpriteUtilities(PIXI); let d = new Dust(PIXI); let particleStream; //需要加载的图片的地址 let imgURL = “https://www.kkkk1000.com/images/learnPixiJS-ParticleEffects/star.png"; //加载图像,加载完成后执行setup函数 PIXI.loader.add(imgURL).load(setup); function setup() { let particleStream = d.emitter( 100, () => d.create( 128, 128, () => su.sprite(imgURL), stage, 30, 0.1, false, 3.14, 6.28, 30, 60, 1, 5 ) ); particleStream.play(); //开始游戏循环 gameLoop(); } function gameLoop() { requestAnimationFrame(gameLoop); d.update(); renderer.render(stage); } </script></body></html>查看效果上一篇 学习 PixiJS — 精灵状态 ...

January 22, 2019 · 2 min · jiezi

庆祝新年?画一颗圣诞树?还是...

关于节日圣诞节,元旦,看大家(情侣)在朋友圈里发各种庆祝的或者祝福的话语,甚是感动,然后悄悄拉黑了。作为单身狗,我们也有自己庆祝节日的方式,今天我们就来实现一些祝福的效果。需要说明的是,所有的效果都是利用canvas来实现的。祝福话语偷了朋友的图,很基本的庆祝方式,展示不同的文字,一段时间切换一次,普普通通,但是对于低像素来说,是最好的方法了,也是庆祝节日用的最多的了,我们这里做个效果多一点的版本效果展示:基本原理是这样的:在canvas中把字画出来,渐变色效果,通过canvas的相关API获取imageData,就是像素点信息,同rgba。遍历imageData,生成相关 dom。设置定时,因为渲染不同的文字效果,当然,有过渡效果。过程对应的代码:在canvas里写字,且渐变效果:// 像素点的单位长度const rectWidth = parseFloat(document.documentElement.style.getPropertyValue(’–rect-width’));const canvas = document.createElement(‘canvas’);canvas.width = 100;canvas.height = 20;const ctx = canvas.getContext(‘2d’);ctx.font = ‘100 18px monospace’;ctx.textBaseline = ’top’; // 设置文字基线ctx.textAlign = ‘center’;// 将区域内所有像素点设置成透明ctx.clearRect(0, 0, canvas.width, canvas.height);// 渐变效果const gradient = ctx.createLinearGradient(10, 0, canvas.width - 10, 0);gradient.addColorStop(0, ‘red’);gradient.addColorStop(1 / 6, ‘orange’);gradient.addColorStop(2 / 6, ‘yellow’);gradient.addColorStop(3 / 6, ‘green’);gradient.addColorStop(4 / 6, ‘blue’);gradient.addColorStop(5 / 6, ‘indigo’);gradient.addColorStop(1, ‘violet’);ctx.fillStyle = gradient;// y设置2,是因为火狐浏览器下效果有异常…ctx.fillText(‘这是测试’, canvas.width / 2, 2);// 插入document.body.appendChild(canvas);像素点过多会卡顿,所以这里尽量用少的点去完成效果获取imageData,生成相关 domconst imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);// 打印一下console.log(imageData);imageData包含三个属性,data,width和height,data是一个一维数组,[[0-255], [0-255], [0-255], [0-255]],长度是4的倍数,4个算一小组,相当于rgba,只不过透明度范围也是0~255,width和height相当于长宽,像素点数量 = (高 宽) 4{ let i = 2000; const fragment = document.createDocumentFragment(); while (i– > 0) { fragment.appendChild(document.createElement(’li’)); } ul.appendChild(fragment);}let iLi = 0;for (let column = 0; column < imageData.width; column++) { for (let row = 0; row < imageData.height; row++) { // 第几个像素点起始位置,肯定是4的倍数 const idx = ((row * imageData.width) + column) * 4; if (imageData.data[idx + 3] > 0) { const li = ul.children[iLi++]; li.style.opacity = ‘1’; // 观察css你会发现,所有显示的点初始位置都是在中心 li.style.transform = translate( ${column * rectWidth}px, ${row * rectWidth}px) scale(1.5); // 这里 scale 完全是为了好看 li.style.background = rgba(${imageData.data[idx]},${imageData.data[idx + 1]},${imageData.data[idx + 2]},${imageData.data[idx + 3] / 255}); } }}while (iLi < 2000) { const li = ul.children[iLi++]; li.style.opacity = ‘0’;}定时器比较简单,就不写了,具体可以看源码。注意的点,Chrome下有点卡顿,Safari和Firefox下没有卡顿,原因未知。预览效果-本地Chrome下打开很卡,火狐、safari正常圣诞树早先的时候是圣诞节的时候,看到各种用字符组成圣诞树的形式,于是自己就去试了下,还是比较简单的。这段用的是项目里的js代码,不过一看就是不可执行的,因为我是按照空格分割的。需要注意的点是:因为是处理文件,所以我们需要借助 node怎样处理图片,生成相应的代码如何让切割后的代码仍然可以执行对于上面的几点,做以下分析:关于第一点和第二点,和上面的例子一样,我们还是需要 canvas,node 环境并没有 canvas 这个 element,需要借助第三方的库node-canvas(npm)例子:绘制好图片,我们就能像上面一样拿到需要的 ImageData,然后就是写文件,基本上是非常简单了,写的时候考虑到 canvas 的API比较多,用了 typescript,不影响阅读,都9102年了,你可以不用,你也应该全局装以下typescript(毕竟如今typescript已经成了社交语言,“哎呦,你也在用typescript的啊,我也在用呢~”)先写个简单版本,用text格式,展示基本图形const fs = require(“fs”);const path = require(‘path’);const { createCanvas, loadImage } = require(‘canvas’);const canvas = createCanvas(80, 80)const ctx: CanvasRenderingContext2D = canvas.getContext(‘2d’)async function transform(input: string, output: string) { const image: ImageBitmap = await loadImage(input); ctx.drawImage(image, 0, 0, 80, 80); const imageData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const { width, height, data } = imageData; let outStr = ‘’; for (let col = 0; col < height; col++) { for (let row = 0; row < width; row++) { const index = ((col * height) + row) * 4; const r = data[index]; const g = data[index + 1]; const b = data[index + 2]; const a = data[index + 3]; // “黑色”区间, 找的图片不是完全黑色 if (r < 100 && g < 100 && b < 100 && a === 255) { outStr += ‘+’; } else { outStr += ’ ‘; } } outStr += ‘\n’; } console.log(outStr); fs.writeFileSync(output, outStr);}transform(path.join(__dirname, ‘../img/tree.jpg’), path.join(__dirname, ‘../outputs/demo2.txt’));效果:关于把js代码切割成可执行的样子,这块我想了很久,刚开始只是是想把js文件按空格切割成数组,给定一个初始的变量start,记录到什么位置,因为一些变量名是不能分割,但js一些语法特性不好处理,比如说function test() { return function aa() {}}和function test() { return function aa() {}}完全是两个函数,后面在网上看了下,发现了芋头大大很久以前写过一篇类似的,地址,有兴趣的小伙伴可以看看,这块不做过多说明,实现还是有点麻烦的会动的字符上面说了字符和图片,自然而然的,下面说的应该就是视频了。视频的话,也是非常简单的,因为视频是由连续的图片组成的,也就是不断变化的图片,就是所谓的“帧”。也就是,如果我们能拿到视频所有定格的图片,就能作出相应的动画效果。需要把视频“拆成”图片,需要借助第三方的工具,ffmpeg,功能比较强大,具体不做说明,需要安装到全局,利用brew,运行brew install ffmpeg就好了(大概,我好像是这样装的233),windows用户下载要配置环境变量之类的,自己查一下吧。// 主要代码const mvPath = path.join(__dirname, ‘../mv/bad-apple.flv’);const imgPath = path.join(__dirname, ‘../img’);const setTime = (t: number) => new Promise((resolve) => { setTimeout(() => resolve(), t);});try { void async function main() { let img = fs.readdirSync(imgPath); let len = img.length; if (len <= 1) { await execSync(cd ${imgPath} &amp;&amp; ffmpeg -i ${mvPath} -f image2 -vf fps=fps=30 bad-%d.png); img = fs.readdirSync(imgPath); len = img.length; } let start = 1; let count = len; (async function inter(i: number) { if (i < count) { await transform(path.join(__dirname, ../img/bad-${i}.png)); await setTime(33.33); await inter(++i); } })(start); }()} catch (err) { console.log(err);}工具的配置非常多,文档看起来也是很麻烦,有个 npm 包,node-fluent-ffmpeg,用着也还可以,我刚开始用了,但是感觉功能不能满足,而且使用这个包的前提是你全局安装了ffmpeg…总结GitHub源码这个我拖了比较久,有的东西有点记不清楚,可能有些东西表达的不好,说的不是很细,一些api的说明我都省略了,这些MDN上都有,就没做过多说明,当然,你可以做些更有趣的事情,文档,本来自己还想做些有趣的东西,但后面没啥时间,就没继续做下去了,希望有兴趣的朋友可以去尝试一波,还是很有意思的。就酱,感谢阅读~ ...

January 21, 2019 · 3 min · jiezi

学习 PixiJS — 精灵状态

精灵状态如果你有复杂的游戏角色或交互式对象,你可能希望该角色根据游戏环境中发生的情况,以不同的方式运行。每个单独的行为称为状态。如果你在精灵上定义状态,那么只要游戏中出现与该状态相对应的事件,就可以触发这些状态。比如,通过键盘的方向键控制一个游戏角色时,按下左箭头,角色就向左移动,其实可以理解为,按下左键头时,触发了角色的向左移动的状态。如果要开始使用精灵状态,首先需要一个状态播放器。状态播放器用于控制精灵状态。Pixi 精灵没有自己的状态播放器,但你可以使用 SpriteUtilities 库中的 sprite 的方法,该方法将创建一个内置状态播放器的精灵。 SpriteUtilities 库的使用上一篇提到过了,可以看 学习 PixiJS — 动画精灵 这篇文章。sprite定义:使用 sprite 函数制作任何类型的 Pixi 精灵。用法:let anySprite = su.sprite(frameTextures, xPosition, yPosition);参数:第一个参数 frameTextures 可以是以下任何一个:一个 PNG 图像字符串一个Pixi 纹理对象纹理图集帧 id 数组一个 PNG 图像字符串的数组一个 Pixi 纹理对象数组如果你为 sprite 方法提供一个数组,它将返回一个动画精灵,这个动画精灵会内置了一个状态播放器。状态播放器只是四个新属性和方法的集合,用于控制精灵动画状态。fps:用于设置精确的动画速度的属性,以每秒帧数为单位。它的默认值是12,fps 与游戏循环 fps 无关,这意味着你可以让精灵动画以独立于游戏或应用程序速度的速度播放。playAnimation:一种播放精灵动画的方法。如果要播放帧的子集,就传入开始帧编号和结束帧编号两个参数。默认情况下,动画将循环播放,除非你将精灵的 loop 属性值设置为 false 。stopAnimation:一种在当前帧停止精灵动画的方法。show:接受参数是一个数字,用来显示特定帧编号的方法。第二个参数 xPosition 和 第三个参数 yPosition 表示创建的精灵的 x 和 y 坐标。什么是精灵状态?下图是一个游戏角色的 PNG 图像,其中包含使角色看起来像是在四个不同方向行走所需的所有帧。这个雪碧图中实际上有八个精灵状态:四个静态状态和四个动画状态。让我们看看这些状态是什么以及如何定义它们。静态状态精灵的静态状态定义精灵在不移动时的四个位置。这些状态是:down, left, right,和up。下图显示了雪碧图上的状态以及标识这些状态的帧号。可以看到第0帧是向下状态,第4帧是左侧状态,第8帧是右侧状态,第12帧是向上状态。怎么定义这些状态呢?首先,创建精灵,以下代码展示了如何使用 sprite 方法创建精灵。let frames = su.filmstrip(“images/adventuress.png”, 32, 32);let adventuress = su.sprite(frames);接下来,在精灵上创建一个名为 states 的对象字面量属性。并在 states 对象中创建down,left,right,和up 的键。将每个键的值设置为与状态对应的帧编号。adventuress.states = { down: 0, left: 4, right: 8, up: 12};接下来就是使用精灵的 show 方法来显示正确的状态。例如,以下代码展示如何显示精灵的 left 状态:adventuress.show(adventuress.states.left);下图显示了改变这些状态对精灵外观的影响。你在可以在任何你需要的地方使用它,让精灵对游戏世界的变化作出反应。比较常见的一个场景是在键盘按键的时候,这样你就可以通过箭头键的方向改变精灵面向的方向。例如,按下左箭头键时,你可以通过以下方式将精灵转向左侧。//左箭头按下left.press = () => { //显示left状态 adventuress.show(adventuress.states.left);};只需对其余的箭头键使用相同的格式,就可以使精灵面向所有的四个方向。动画状态精灵的动画状态定义了精灵移动时的四个动作序列。这些状态是:walkDown,walkLeft,walkRight,和walkUp 。下图显示了这些状态在雪碧图上的位置。这些状态中的每一个由四个帧组成,当在循环中播放时,将创建连续的步行动画。要定义每个动画状态,就在 states 对象中创建描述该状态的键。键的值应该是一个包含两个元素的数组:起始帧编号和结束帧编号。例如,以下是如何定义 walkLeft 状态://3是动画序列 开始的帧编号,5是结束的帧编号walkLeft: [3, 5]以下是如何将这四种新动画状态添加到 adventuress 精灵中:adventuress.states = { down: 0, left: 4, right: 8, up: 12, walkDown: [0, 3], walkLeft: [4, 7], walkRight: [8, 11], walkUp: [12, 15]};现在它的状态都被定义了,让我们做一个会走的精灵。 把制作动画精灵和定义状态还有键盘响应所学到的知识相结合,就可以制作一个步行游戏角色。查看效果如果希望精灵在屏幕上移动得更快或更慢,就在箭头键方法中更改 vx 和 vy 的值。如果希望精灵的步行动画效果更快或更慢,就更改精灵的 fps 属性。制作动画帧的工具使用 Adobe Illustrator 或 Photoshop 手动绘制每个帧。Flash Professional 只需将动画导出为雪碧图,就可以在 JavaScript 游戏中使用它。你还可以使用 Shoebox 等工具将 Flash 的 SWF 文件格式转换为纹理图集。Piskel 是一个免费的在线工具,用于制作像素风格的动画游戏角色。Dragon Bones,Spine,和 Creature。这三个工具都非常相似。它们可以创建复杂的游戏角色,为它们设置动画,并将它们导出为雪碧图和 JSON 文件。Shoebox 是一款基于Adobe Air 的免费应用程序,它的功能挺多,比如可以用来制作雪碧图,也可以拆分雪碧图,还可以检测透明图像中的精灵并将其剪切出来 等。上一篇 学习 PixiJS — 动画精灵 ...

January 19, 2019 · 1 min · jiezi

vue + canvas 实现炫酷时钟

如何基于canvas实现下图的样式demo演示地址: https://mirror198829.github.i...canvas绘制步骤思路绘制表盘的(时针)刻度绘制表盘的(分针)刻度绘制表盘数字刻度绘制时针、分针、秒针绘制中心点绘制秒针的尾部(优化部分)实现所需要的知识点画圆 ctx.arc(x,y,r,开始弧度,结束弧度)画线 ctx.moveTo(x,y) ctx.lineTo(x1,y1)直角坐标系的计算方法 x = x0 + Math.cos(角度)*长度 y = y0 + Math.sin(角度)*长度定时器如何实现(时针)刻度盘的绘制思路:已知直角坐标系的中心点坐标(x0,y0)即canvas画布中心点位置。设定L = 长度计算出时针的角度,通过直角坐标系的计算方法便得出时针刻度的位置。 (12个刻度进行循环便可全部绘制)计算时针角度的方法: -90 + i * (360 / 12)代码如下:drawHoursScale(ctx, x0, y0, scaleNum, scaleW, maxL, minL) { for (let i = 0; i < scaleNum; i++) { let angel = -90 + i * (360 / scaleNum) //角度 let [x1, y1] = [x0 + Math.cos(angel * Math.PI / 180) * maxL, y0 + Math.sin(angel * Math.PI / 180) * maxL] let [x2, y2] = [x0 + Math.cos(angel * Math.PI / 180) * minL, y0 + Math.sin(angel * Math.PI / 180) * minL] ctx.save() ctx.beginPath() ctx.lineWidth = scaleW ctx.lineCap = “round” ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.stroke() ctx.closePath() ctx.restore() } }如何绘制时针/分针/秒针思路:已知指针的起点坐标(x0,y0)计算出指针的终点坐标(x1,y1),便可通过lineTo的方式进行绘制如何计算终点坐标 与绘制刻度的思想式类似的 代码如下:drawTimeNeedle(ctx, x0, y0, lineW, L, angel, color = ‘#000’) { let [x, y] = [x0 + Math.cos(angel * Math.PI / 180) * L, y0 + Math.sin(angel * Math.PI / 180) * L] ctx.save() ctx.beginPath() ctx.strokeStyle = color ctx.lineWidth = lineW ctx.lineCap = “round” ctx.moveTo(x0, y0) ctx.lineTo(x, y) ctx.stroke() ctx.closePath() ctx.restore() },说明事项时钟样式 参考 http://www.jq22.com/jquery-in… (可惜下载代码要币,一不做二不休自己写)源码地址: https://github.com/Mirror1988… (喜欢的,请给作者github小星星)demo演示地址: https://mirror198829.github.i… ...

January 17, 2019 · 1 min · jiezi

学习 PixiJS — 动画精灵

说明看完官方教程中提到的这本书 — Learn Pixi.js ,准备写写读后感了,官方教程中所说的内容,基本上就是书的前4章,所以我就从第5章开始写写吧。动画精灵指的是按顺序使用一系列略有不同的图像,创建的精灵,之后一帧一帧的播放这些图像,就可以产生运动的幻觉。也就是说用这种图片做出这样的效果 要制作动画精灵我们需要用到 PixiJS 的 AnimatedSprite 方法。PIXI.extras.AnimatedSprite定义:使用纹理数组创建动画精灵的方法。用法:new PIXI.extras.AnimatedSprite(textures,autoUpdate)参数 :名称类型默认值描述texturesarray 用一系列略有不同的图像做的纹理数组。autoUpdatebooleantrue用来判断是否使用 PIXI.ticker.shared 自动更新动画时间。返回值: 返回一个对象,对象会有一些属性和方法,用于控制动画精灵。返回值对象的属性:名称类型描述animationSpeednumber动画精灵的播放速度。越高越快,越低越慢,默认值是1currentFramenumber(只读)正在显示的当前帧编号onCompletefunction当loop属性为false时,一个动画精灵完成播放时调用playingBoolean确定当前动画精灵是否正在播放onFrameChangefunction当一个动画精灵更改要呈现的纹理时调用loopboolean动画精灵是否在播放后重复播放onLoopfunction当loop属性为true时调用的函数texturesarray用于这个动画精灵的纹理数组totalFramesnumber (只读)动画中的帧总数返回值对象的方法:名称参数描述play 播放动画精灵gotoAndPlayframeNumber,number类型,开始帧的索引转到特定的帧并开始播放动画精灵stop 停止播放动画精灵gotoAndStopframeNumber,number类型,停止帧的索引转到特定的帧并停止播放动画精灵使用返回值中的这些属性和方法,我们就可以控制动画精灵了,比如播放动画精灵,设置动画的速度,设置是否循环播放等,除此之外,还要知道就是 PIXI.extras.AnimatedSprite 方法继承自 PIXI.Sprite 方法,所以动画精灵也可以用普通精灵的属性和方法,比如x,y,width,height,scale,rotation 。好的,我们开始试试这个方法。<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”> <title>动画精灵</title></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script> // 创建一个 Pixi应用 需要的一些参数 let option = { width: 400, height: 300, transparent: true, } // 创建一个 Pixi应用 let app = new PIXI.Application(option); // 获取渲染器 let renderer = app.renderer; let playground = document.getElementById(‘px-render’); // 把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); //设置别名 let TextureCache = PIXI.utils.TextureCache; let Texture = PIXI.Texture; let Rectangle = PIXI.Rectangle; let AnimatedSprite = PIXI.extras.AnimatedSprite; //需要加载的雪碧图的地址(该图片服务器端已做跨域处理) let imgURL = “https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.png"; //加载图像,加载完成后执行setup函数 PIXI.loader.add(imgURL).load(setup); function setup() { //获取纹理 let base = TextureCache[imgURL]; //第一个纹理 let texture0 = new Texture(base); texture0.frame = new Rectangle(0, 0, 80, 143); //第二个纹理 let texture1 = new Texture(base); texture1.frame = new Rectangle(80, 0, 80, 143); //第三个纹理 let texture2 = new Texture(base); texture2.frame = new Rectangle(160, 0, 80, 143); //第四个纹理 let texture3 = new Texture(base); texture3.frame = new Rectangle(240, 0, 80, 143); //创建纹理数组 let textures = [texture0, texture1, texture2,texture3]; //创建动画精灵 let pixie = new PIXI.extras.AnimatedSprite(textures); //设置动画精灵的速度 pixie.animationSpeed=0.1; //把动画精灵添加到舞台 app.stage.addChild(pixie); //播放动画精灵 pixie.play(); } </script></body></html>查看效果 上面这个例子中,创建纹理数组时似乎点麻烦,要解决这个问题,我们可以用名叫 SpriteUtilities 的库,该库包含许多有用的函数,用于创建Pixi精灵并使它们更易于使用。安装:直接用 script 标签,引入js 文件就可以<script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script>安装好之后,我们需要创建一个新实例,代码如下let su = new SpriteUtilities(PIXI);之后就可以用 su 对象访问所有方法了。我们这里需要用到的就是 su 对象的 filmstrip 方法。定义:filmstrip 方法可以自动将雪碧图转换为可用于制作精灵的纹理数组用法:su.filmstrip(“anyTilesetImage.png”, frameWidth, frameHeight, optionalPadding);参数:名称类型描述anyTilesetImagestring雪碧图的路径frameWidthnumber每帧的宽度(以像素为单位)frameHeightnumber每帧的高度(以像素为单位)optionalPaddingnumber每帧周围的填充量(可选值,以像素为单位)返回值: 返回一个数组,可用于制作动画精灵的纹理数组。现在我们使用 SpriteUtilities 来改写下刚才的示例代码。<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”> <title>动画精灵</title></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 var option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 var app = new PIXI.Application(option); //获取渲染器 var renderer = app.renderer; var playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); let su = new SpriteUtilities(PIXI); //需要加载的雪碧图的地址(该图片服务器端已做跨域处理) let imgURL = “https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.png"; PIXI.loader.add(imgURL).load(setup); function setup() { //创建纹理数组 let frames = su.filmstrip(imgURL, 80, 143); //创建动画精灵 let pixie = new PIXI.extras.AnimatedSprite(frames); //设置动画精灵的速度 pixie.animationSpeed=0.1; //把动画精灵添加到舞台 app.stage.addChild(pixie); //播放动画精灵 pixie.play(); } </script></body></html>查看效果 filmstrip 方法自动将整个雪碧图转换为可用于制作动画精灵的纹理数组。但是,如果我们只想使用雪碧图中的一部分帧呢?这时候需要用到 frames 方法了。定义:frames 方法使用雪碧图中的一组子帧,来创建纹理数组。用法:su.frames(source, coordinates, frameWidth, frameHeight)参数:名称类型描述sourcestring雪碧图的路径coordinatesarray包含每帧的 x 和 y 坐标的二维数组frameWidthnumber每帧的宽度(以像素为单位)frameHeightnumber每帧和高度(以像素为单位)返回值: 返回一个数组,可用于制作动画精灵的纹理数组。示例代码:<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”> <title>动画精灵</title></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 var option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 var app = new PIXI.Application(option); //获取渲染器 var renderer = app.renderer; var playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); let su = new SpriteUtilities(PIXI); //需要加载的雪碧图的地址(该图片服务器端已做跨域处理) let imgURL = “https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.png"; PIXI.loader.add(imgURL).load(setup); function setup() { //创建纹理数组 let frames = su.frames(imgURL, [[0,0],[80,0],[160,0],[240,0]],80, 143); //创建动画精灵 let pixie = new PIXI.extras.AnimatedSprite(frames); //设置动画精灵的速度 pixie.animationSpeed=0.1; //把动画精灵添加到舞台 app.stage.addChild(pixie); //播放动画精灵 pixie.play(); } </script></body></html>查看效果除了上面提到的方式,还可以用纹理贴图集来创建动画精灵。使用纹理贴图集来创建动画精灵,就是先通过json文件,加载所有纹理,然后把需要的纹理再放进一个数组中,最后把这个数组当参数,传入PIXI.extras.AnimatedSprite 方法中,来创建动画精灵。 代码:<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”> <title>动画精灵</title></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 var option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 var app = new PIXI.Application(option); //获取渲染器 var renderer = app.renderer; var playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); //需要加载的纹理贴图集的地址 let textureURL = “https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.json"; //加载纹理贴图集,加载完成后执行setup函数 PIXI.loader.add(textureURL).load(setup); function setup() { let id = PIXI.loader.resources[textureURL].textures; //创建纹理数组 let frames = [ id[“dnf0.png”], id[“dnf1.png”], id[“dnf2.png”], id[“dnf3.png”] ]; //创建动画精灵 let pixie = new PIXI.extras.AnimatedSprite(frames); //设置动画精灵的速度 pixie.animationSpeed=0.1; //把动画精灵添加到舞台 app.stage.addChild(pixie); //播放动画精灵 pixie.play(); } </script></body></html>查看效果上面的代码创建纹理数组时,是把纹理一个一个的放进数组中,如果数量比较少还好,多一点呢?假如有100个呢?一个一个的放就太麻烦了,这时候我们可以用 SpriteUtilities 库中提供的 frameSeries 方法。定义:frameSeries 方法可以通过已加载的纹理贴图集,使用一系列编号的帧ID来创建动画精灵。用法:su.frameSeries(startNumber, endNumber, baseName, extension)参数:名称类型描述startNumbernumber起始帧序列号(默认值是0)endNumbernumber结束帧序列号(默认值是1)baseNamestring可选的基本文件名extensionstring可选的文件扩展名返回值: 返回一个数组,可用于制作动画精灵的纹理数组。注意: 使用 frameSeries 方法时,要确保在 json 文件中,定义的每帧的名称都是按顺序来的,比如 frame0.png frame1.png frame2.png 这种。因为 frameSeries 方法的源码是这样写的 frameSeries(startNumber = 0, endNumber = 1, baseName = “”, extension = “”) { //创建一个数组来存储帧名 let frames = []; for (let i = startNumber; i < endNumber + 1; i++) { let frame = this.TextureCache[${baseName + i + extension}]; frames.push(frame); } return frames; }源码中其实是用 for 循环把帧名拼接起来的。所以要保证帧名是按顺序来的,不然就获取不到了。下来我们就试试 frameSeries 方法吧。<!doctype html><html lang=“zn”><head> <meta charset=“UTF-8”> <title>动画精灵</title></head><body> <div id=“px-render”></div> <script src=“https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script> <script src=“https://www.kkkk1000.com/js/spriteUtilities.js"></script> <script> //创建一个 Pixi应用 需要的一些参数 var option = { width: 400, height: 300, transparent: true, } //创建一个 Pixi应用 var app = new PIXI.Application(option); //获取渲染器 var renderer = app.renderer; var playground = document.getElementById(‘px-render’); //把 Pixi 创建的 canvas 添加到页面上 playground.appendChild(renderer.view); let su = new SpriteUtilities(PIXI); //需要加载的纹理贴图集的地址 let textureURL = “https://www.kkkk1000.com/images/learnPixiJS-AnimatedSprite/dnf.json"; PIXI.loader.add(textureURL).load(setup); function setup() { //创建纹理数组 let frames = su.frameSeries(0,7,“dnf”,".png”); //创建动画精灵 let pixie = new PIXI.extras.AnimatedSprite(frames); //设置动画精灵的速度 pixie.animationSpeed=0.1; //把动画精灵添加到舞台 app.stage.addChild(pixie); //播放动画精灵 pixie.play(); } </script></body></html>查看效果注意版本问题: 1、PIXI.extras.AnimatedSprite 这个方法原来叫PIXI.extras.MovieClip ,是在 4.2.1 版本的时候修改的,本文示例代码中用 PixiJS 的版本是 4.8.2,所以没有问题,如果你在使用过程中发现调用PIXI.extras.AnimatedSprite 这个方法有问题,可以先检查下版本是否正确。2、 SpriteUtilities 目前支持的 PixiJS 的版本是 3.0.11,而 SpriteUtilities 中用的就是PIXI.extras.MovieClip 方法,所以你如果用了比较高的 PixiJS 的版本,需要在SpriteUtilities 中修改下方法的别名。在 spriteUtilities.js 文件中需要把 renderingEngine.extras.MovieClip 改成renderingEngine.extras.AnimatedSprite,把 renderingEngine.ParticleContainer 改成 PIXI.particles.ParticleContainer 。这个 spriteUtilities.js 就是修改后的。当然你也可以使用低版本的 PixiJS,这样就不用改 spriteUtilities.js 的代码了。总结动画精灵就是逐帧动画,通过一帧一帧的播放图像来产生运动的幻觉。本文就是聊了聊创建动画精灵的一些方式和如何使用动画精灵。如果文中有错误的地方,还请小伙伴们指出,万分感谢。 ...

January 14, 2019 · 4 min · jiezi

HTML5系列之canvas用法

html:<canvas id=“canvas” width=“500px” height=“500px”></canvas>Js:var can = document.getElementById(“canvas”);if(can.getContext){ var ctx=can.getContext(“2d”); //实心矩形(起点坐标,宽和长) ctx.fillStyle=“red”; ctx.fillRect(300,300,50,50); ctx.fillRect(25,25,100,100); ctx.clearRect(45,45,60,60); ctx.strokeRect(50,50,50,50); //设置透明度 //ctx.globalAlpha=0.8; //创建路径,描边圆形(圆心,半径,角度,方向) ctx.beginPath(); //新建一个起点 ctx.arc(200,200,50,0,Math.PI2,true); ctx.stroke(); //创建路径(三角形) ctx.fillStyle=“rgba(0,0,200,0.5)”; ctx.beginPath(); ctx.moveTo(100,100); ctx.lineTo(120,100); ctx.lineTo(100,120); ctx.fill(); //二次贝塞尔(控制点,结束点) ctx.beginPath(); ctx.moveTo(75,25); ctx.quadraticCurveTo(25,25,25,62.5); ctx.quadraticCurveTo(25,100,50,100); ctx.stroke(); //三次贝塞尔(控制点1,,控制点2,结束点) ctx.beginPath(); ctx.moveTo(75,40); ctx.bezierCurveTo(75,37,70,25,50,25); ctx.bezierCurveTo(20,25,20,62.5,20,62.5); ctx.stroke(); //Path2D 保存路径 var bb=new Path2D(); bb.rect(70,70,30,30); ctx.fill(bb); var cc=new Path2D(bb); ctx.fill(cc); //SVG paths var dd=new Path2D(“M10 10 H80 V80 H-80 Z”); ctx.strokeStyle=“pink”; ctx.stroke(dd);/ //linewidth设置线宽 for(var i=0; i<10; i++){ ctx.lineWidth=i+1; ctx.beginPath(); ctx.moveTo(5 + i * 10, 5); ctx.lineTo(5+i10,150); ctx.stroke(); } //linecap设置线条末端样式 var linecap=[“butt”,“round”,“square”]; ctx.strokeStyle=“green”; for(var i=0;i<linecap.length;i++){ ctx.lineWidth=10; ctx.lineCap=linecap[i]; ctx.beginPath(); ctx.moveTo(150+i20,10); ctx.lineTo(150+i20,150); ctx.stroke(); } //lineJoin设置线条接合处样式 var linejoin=[“round”,“bevel”,“miter”]; ctx.strokeStyle=“gold”; for(var i=0;i<linecap.length;i++){ ctx.lineWidth=10; ctx.lineJoin=linejoin[i]; ctx.beginPath(); ctx.moveTo(300,10+i20); ctx.lineTo(350,50+i20); ctx.lineTo(400,10+i20); ctx.lineTo(450,50+i20); ctx.lineTo(500,10+i20); ctx.stroke(); } //miterLimit设置相交最大长度 var miterlimit=[“round”,“bevel”,“miter”]; ctx.strokeStyle=“green”; ctx.lineWidth=10; ctx.miterLimit=1; ctx.beginPath(); ctx.moveTo(300,100); ctx.lineTo(350,150); ctx.lineTo(400,100); ctx.lineTo(450,150); ctx.lineTo(500,100); ctx.stroke(); //setLineDash lineDashOffset设置虚线 var offset=0; function draw(){ offset=offset+3; ctx.clearRect(0,0,canvas.width, canvas.height); ctx.setLineDash([4,6]); //接收数组 ctx.lineDashOffset= -offset; ctx.strokeRect(10,20,200,200); if(offset>20){ offset=0; } } setInterval(draw,200); //linearGradient线性渐变 var fff=ctx.createLinearGradient(100,100,50,150); //渐变的起点和终点 fff.addColorStop(“0”,“black”); fff.addColorStop(“1”,“red”); ctx.fillStyle=fff; ctx.fillRect(10,100,50,150);/ //radialgradient/radgrad径向渐变 /var ggg=ctx.createRadialGradient(100,150,30,100,150,50); //圆心,半径/圆心,半径 ggg.addColorStop(“0”,“black”); ggg.addColorStop(“0.9”,“red”); ggg.addColorStop(1,‘rgba(1,159,98,0)’); ctx.fillStyle=ggg; ctx.fillRect(50,100,100,100); //创建新图案,用图案填充 var img= new Image(); img.src=“images/4.png”; //不可放在后面 img.onload=function(){ var ppr=ctx.createPattern(img,“no-repeat”); ctx.fillStyle=ppr; ctx.fillRect(300,200,50,50); } //创建文本阴影效果 ctx.shadowOffsetX= 2; ctx.shadowOffsetY= 2; ctx.shadowBlur= 5; ctx.shadowColor= “red”; ctx.font=“20px ‘楷体’”; ctx.fillText(“哈哈”,100,50); //填充字体(坐标) //填充规则 ctx.beginPath(); ctx.moveTo(250,200); ctx.arc(200,200,50,0,Math.PI2,true); ctx.arc(200,200,30,0,Math.PI2,true); ctx.fill(“evenodd”); //默认值’nonzero’ //绘制文字(描边文字) ctx.shadowOffsetX= 2; ctx.shadowOffsetY= 2; ctx.shadowBlur= 5; ctx.shadowColor= “red”; ctx.font=“50px ‘楷体’”; ctx.strokeText(“嘻嘻”,100,50); //基线校准 ctx.font=“50px serif”; ctx.textAlign=“left”; ctx.textBaseline=“top”; ctx.strokeText(“hello”,100,50); //应用图像 var img= new Image(); img.src=“images/4.png”; img.onload=function(){ ctx.drawImage(img,50,50);//坐标 ctx.beginPath(); ctx.moveTo(100,100); ctx.lineTo(120,100); ctx.lineTo(100,120); ctx.fill(); } //缩放图像 var img= new Image(); img.src=“images/4.png”; img.onload=function(){ for(var i=0;i<4;i++){ for(var j=0;j<4;j++){ ctx.drawImage(img,j60,i60,60,60); //坐标,图片的大小 } } } //切片 var img1= new Image(); img1.onload=function(){ //所切图片的切片位置和大小,目标显示位置和大小 ctx.drawImage(img1,250,250,200,200,0,0,100,100); } img1.src=“images/1.png”; //可以放在后面,可识别 var img2= new Image(); img2.src=“images/2.png”; img2.onload=function(){ //所切图片的切片位置和大小,目标显示位置和大小 ctx.drawImage(img2,250,250,500,500,10,10,80,80); } //保存(属性)与还原 save,restore与栈相似 ctx.fillRect(0,0,150,150); // Draw a rectangle with default settings ctx.save(); // Save the default state ctx.fillStyle = ‘#09F’ // Make changes to the settings ctx.fillRect(15,15,120,120); // Draw a rectangle with new settings ctx.save(); // Save the current state ctx.fillStyle = ‘#FFF’ // Make changes to the settings ctx.globalAlpha = 0.5; ctx.fillRect(30,30,90,90); // Draw a rectangle with new settings ctx.restore(); // Restore previous state ctx.fillRect(45,45,60,60); // Draw a rectangle with restored settings ctx.restore(); // Restore original state ctx.fillRect(60,60,30,30);/ // Draw a rectangle with restored settings //旋转canvas坐标 rotate/translate移动canvas坐标原点 ctx.translate(100,100); for (var i=1;i<6;i++){ // Loop through rings (from inside to out) ctx.fillStyle = ‘rgb(’+(51i)+’,’+(255-51i)+’,255)’; for (var j=0;j<i6;j++){ // draw individual dots ctx.rotate(Math.PI2/(i6)); ctx.beginPath(); ctx.arc(0,i12.5,5,0,Math.PI2,true); ctx.fill(); } }} else { console.log(“not support”);} ...

January 13, 2019 · 2 min · jiezi

鼠标跟随炫彩效果

以前在网上看到了别人这个效果,感觉很酷也很难,但当真的去了解怎么做的时候会发现其实没那么难。用到的就是canvas。先来看一下效果可能不是很好看啊。1.先创建一个canvas(大小、样式自己随意定义)<canvas id=“canvas” width=“300” height=“300”></canvas>2.获取到当前的canvas,并准备画图。 let canvas = document.getElementById(‘canvas’), context = canvas.getContext(‘2d’);3.画圆形context.arc(x, y, size, startAngle, endAngle); //这里就不写顺时针逆时针了下面我们就来看看怎么做吧。我以对象的方法去创建圆形。圆形构造函数function Circle(x, y, size, speed) { this.x = x; //x坐标 this.y = y; //y坐标 this.size = size; //大小 this.color = getRandomCokor(); //随机的颜色 this.X = getRandom(speed); //x轴随机的移动速度 this.Y = getRandom(speed); //y轴随机的移动速度 circleArr.push(this); //放到一个数组保存起来}创建图形Circle.prototype.createCircle = function () { context.beginPath(); context.arc(this.x, this.y, this.size, 0, 2Math.PI); context.fillStyle = this.color; //填充的颜色 context.fill(); context.stroke(); this && this.move(); //移动函数}移动Circle.prototype.move = function () { this.x += this.X; //x轴位移量 this.y += this.Y; //Y轴位移量 this.r -= 1; //半径缩小的尺寸(这里我就直接固定大小了) if(this.r <= 0){ this.delCircle(); //如果半径小于0,删除元素 } }删除Circle.prototype.delCircle = function () { for (let i = circleArr.length - 1; i >= 0; i–) { if(circleArr[i] === this){ circleArr.splice(i, 1); //删除那个元素 } } }当鼠标移动的时候创建圆形canvas.onmousemove = function mousemove(e) { let circle = new Circle(e.clientX, e.clientY, rValue, speedValue); context.clearRect(0, 0, canvas.width, canvas.height); //每次清理干净画布 circleArr.forEach( function(element, index) { element.createCircle(); //创建每一个元素 });}获得随机颜色函数function getRandomCokor() { let colorR = getRandom(255), colorG = getRandom(255), colorB = getRandom(255), rgb = rgb(${colorR}, ${colorG}, ${colorB}); return rgb;}function getRandom(num) { return Math.floor( Math.random(0, 1) * (num) + 1);}当鼠标离开或点击的时候清空画布和当前数组canvas.onmouseleave = canvas.onmousedown = function mouseDown() { circleArr.length = 0; context.clearRect(0, 0, canvas.width, canvas.height);}下面我们再来拓展一下功能先看下效果:就是能自定义球的大小和位移大小。HTML结构<div class=“app”> <canvas id=“canvas” width=“300” height=“300”></canvas> <h3>当前半径:</h3> <input type=“text” id=“rText”> <input type=“range” min=“1” max=“20” id=“rRange”> <h3>当前速度:</h3> <input type=“text” id=“speedText”> <input type=“range” min=“1” max=“20” id=“speedRange”></div>获取各个大小并赋值let rRange = document.getElementById(‘rRange’), //大小 rText = document.getElementById(‘rText’), //大小显示框 speedRange = document.getElementById(‘speedRange’), //速度 speedText = document.getElementById(‘speedText’), //速度大小显示框 rValue = +rRange.value, //大小 speedValue = +speedRange.value; //速度 rText.value = rValue; //赋值显示 speedText.value = speedValue; //赋值显示当改变的时候重新赋值rRange.onchange = function valueChange(e) { //大小 rValue = + this.value; rText.value = rValue;}speedRange.onchange = function valueChange(e) { //速度 speedValue = + this.value; speedText.value = speedValue;}+整体代码let canvas = document.getElementById(‘canvas’), //获取canvas rRange = document.getElementById(‘rRange’), //大小 rText = document.getElementById(‘rText’), speedRange = document.getElementById(‘speedRange’), //速度 speedText = document.getElementById(‘speedText’), context = canvas.getContext(‘2d’), circleArr = [], rValue = +rRange.value, speedValue = +speedRange.value;rText.value = rValue; //赋值显示speedText.value = speedValue;function Circle(x, y, size, speed) { //构造函数 this.x = x; this.y = y; this.size = size; this.color = getRandomCokor(); this.X = getRandom(speed); this.Y = getRandom(speed); circleArr.push(this);}Circle.prototype.createCircle = function () { //创建图形 context.beginPath(); context.arc(this.x, this.y, this.size, 0, 2Math.PI); context.fillStyle = this.color; context.fill(); context.stroke(); this && this.move();} Circle.prototype.move = function () { //移动 this.x += this.X; this.y += this.Y; this.r -= 1; if(this.r <= 0){ this.delCircle(); } }Circle.prototype.delCircle = function () { //删除 for (let i = circleArr.length - 1; i >= 0; i–) { if(circleArr[i] === this){ circleArr.splice(i, 1); } } }rRange.onchange = function valueChange(e) { //大小改变的时候重新赋值 rValue = + this.value; rText.value = rValue;}speedRange.onchange = function valueChange(e) { //速度改变的时候重新赋值 speedValue = + this.value; speedText.value = speedValue;}canvas.onmousemove = function mousemove(e) { //鼠标在canvas上移动 let circle = new Circle(e.clientX, e.clientY, rValue, speedValue); context.clearRect(0, 0, canvas.width, canvas.height); circleArr.forEach( function(element, index) { element.createCircle(); });}canvas.onmouseleave = canvas.onmousedown = function mouseDown() { circleArr.length = 0; context.clearRect(0, 0, canvas.width, canvas.height);}function getRandomCokor() { //产生随机颜色 let colorR = getRandom(255), colorG = getRandom(255), colorB = getRandom(255), rgb = rgb(${colorR}, ${colorG}, ${colorB}); return rgb;}function getRandom(num) { return Math.floor( Math.random(0, 1) * (num) + 1);}我只在canvas大小区域内绘制图形,你可以修改在整个document上绘制图形。 ...

January 13, 2019 · 3 min · jiezi

使用canvas绘制圆弧动画

效果预览canvas 绘制基本流程初始画布对于canvas的绘制,首先需要在html内指定一块画布,即<canvas></canvas>, 可以看做是在PS中新建一个空白文档,之后所有的操作都将呈现在这个文档之上,与PS的区别是,canvas本身没有图层的特性,当需要展示不同维度的视图时,需要交由html的位置关系来解决。canvas标签上,值得一提的就是width和height两个属性,这两个属性代表着画布的宽高,与canvas样式上的宽高有很大区别。在浏览器当中,看到的图形绘制大小,本身是由canvas.style.width/canvas.style.height决定的,他们决定了canvas这个dom元素的大小关系,而canvas.width和canvas.height决定的是canvas内部图形的大小关系。当这两个宽高比不同时,就会产生视觉上的形变。即,把canvas.style.height放大为2倍时,显示效果会被拉伸:当不设置样式宽高时,浏览器中canvas大小由画布大小决定(在实际开发中,碰到一个例外,是在使用mapbox时,绘制map的标签如果只设置canvas画布大小时,在ios移动端的浏览器上显示异常,PC正常)。获取上下文所谓上下文,代表的就是一个环境,在这个环境当中你可以获取到相关的方法,变量。程序中有上下文,html的媒体中也有上下文,比如音频上下文(AudioContext),只有拿到了上下文,才能进行相关的方法操作,canvas也是如此,canvas上的方法都是借由canvas上下文得到。<canvas id=“leftCanvas”></canvas>const canvasL = document.getElementById(“leftCanvas”);const cxtL = canvasL.getContext(“2d”);配置线条本次圆弧动画需要用到的上下文属性有:lineCap 线段端点形状,本次设置为roundlineWidth 线宽strokeStyle 线条填充颜色clearRect 清除画布里面的内容beginPath 在画布上开始一段新的路径arc 圆弧绘制参数配置stroke 绘制角度计算角度计算之前,先介绍一下绘制圆弧的基础api arc。ctx.arc(x, y, radius, startAngle, endAngle [, anticlockwise]);这个函数可以接收6个参数,前五个为必填,分别为圆心x坐标,圆心y坐标,半径,起始角度,结束角度,方向(默认为false,顺时针)。回到圆弧动画,当前动画有两段,以顺时针方向这段为例。x, y:在canvas当中,坐标系默认以左上角为原点,如果想让圆弧动画以画布中心点旋转,可以将圆心点设置为画布中心点,即画布长宽的1/2,假设设置的画布长宽均为100,那么圆心点的坐标即为(50, 50),这个圆就绘制在了画布中间。radius:为了不与画布产生切角,半径设置比画布一般略小,。startAngle:起始角度为正北方向,而圆以x轴水平方向为0度,因此将起始点逆时针旋转90°,即:-1 / 2 * Math.PI。endAngle:因为圆弧长度为30°,终点角度在起始角度的基础上增加 1 / 6 * Math.PI。顺时针方向圆弧初始配置为: cxtL.arc(WidthL / 2, HeightL / 2, WidthL / 2 - 5, -1 / 2 * Math.PI, 1 / 6 * Math.PI, false);开启动画window.requestAnimationFrame()借助requestAnimationFrame,来对canvas圆弧进行不断的重绘,每次重绘canvas之前清空画布,每轮动画方向角偏移2°,即2 / 180 * Math.PI,动画结束的标记为圆弧终点的角度,移动至3 / 2 * Math.PI,当满足条件时,调用window.cancelAnimationFrame(animationId)取消动画。屏幕适配通过进入html后,动态获取视口,来设置canvas宽高,比如希望画布大小为窗口的宽度的15%,可以通过const clientWidth = document.documentElement.clientWidth;const canvasWidth = Math.floor(clientWidth * 0.15);const canvasL = document.getElementById(“leftCanvas”);canvasL.setAttribute(“width”, canvasWidth + “px”);这样就可以使画布适应不同屏幕大小。以下为未整理代码,较乱,仅供参考。https://codepen.io/jbleach/pe… ...

January 12, 2019 · 1 min · jiezi

Vue2学习之旅五:添加数据可视化支持

作者:心叶时间:2018-04-25 16:33本篇最终项目文件Github地址:github.com/yelloxing/vue.quick/tree/V5安装npm install clay-core –save首先,通过npm安装数据可视化库clay.js,具体的api你可以查阅文档:https://yelloxing.github.io/c…这是一个非嵌入的库。初始化新建文件src/clay.js/index.jsimport render from ‘clay-core’;import Sizzle from ‘sizzle’;let clay = render(window);clay.config("$sizzleProvider", () => (selector, context) => Sizzle(selector, context));export default clay;clay.js启动前可以有多项配置,用以针对具体开发环境进行调整或加强,上面我们加强了选择器功能,因此,你可能还需要安装sizzle:npm install –save sizzle使用现在,就可以对照api使用这个库了,举例子:比如修改entry.js里面的方法:1.首先在开头导入:import clay from ‘./clay.js’;2.在需要的地方使用:el: clay(’#root’)[0],使用组件首先,我们去组件库中复制一个组件过来,组件仓库地址:https://github.com/yelloxing/clay-component复制到clay.js文件夹中,因为组件基于浏览器开发,而不是模块开发,因此,你需要在组件开发加入:import clay from “./index.js”;然后,clay.vue是使用方法:clay(“svg”) .attr(“width”, 800) .attr(“height”, 700) // 使用组件 .use(“circleTree”, { // 数据 data: program.data, // 结点名称 name: orgItem => orgItem.name, // 树的圆心和半径 cx: 350, cy: 350, r: 300, // 树结构配置 root: initTree => initTree, child: parentTree => parentTree.children, id: treedata => treedata.name + (treedata.value ? “_” + treedata.value : “”) }); ...

January 1, 2019 · 1 min · jiezi

canvas中普通动效与粒子动效的实现

canvas用于在网页上绘制图像、动画,可以将其理解为画布,在这个画布上构建想要的效果。canvas可以绘制动态效果,除了常用的规则动画之外,还可以采用粒子的概念来实现较复杂的动效,本文分别采用普通动效与粒子特效实现了一个简单的时钟。普通时钟普通动效即利用canvas的api,实现有规则的图案、动画。效果该效果实现比较简单,主要分析一下刻度与指针角度偏移的实现。绘制刻度此例为小时刻度的绘制:表盘上共有12个小时,Math.PI为180°,每小时占据30°。.save()表示保存canvas当前环境的状态,在此基础上进行绘制。绘制完成之后,返回之前保存过的路径状态和属性。分钟刻度同理,改变角度与样式即可。 // 小时时间刻度 offscreenCanvasCtx.save(); for (var i = 0; i < 12; i++) { offscreenCanvasCtx.beginPath(); // 刻度颜色 offscreenCanvasCtx.strokeStyle = ‘#fff’; // 刻度宽度 offscreenCanvasCtx.lineWidth = 3; // 每小时占据30° offscreenCanvasCtx.rotate(Math.PI / 6); // 开始绘制的位置 offscreenCanvasCtx.lineTo(140, 0) // 结束绘制的位置; offscreenCanvasCtx.lineTo(120, 0); // 绘制路径 offscreenCanvasCtx.stroke(); } offscreenCanvasCtx.restore();指针指向以秒针为例:获取当前时间的秒数,并计算对应的偏移角度 var now = new Date(), sec = now.getSeconds(), min = now.getMinutes(), hr = now.getHours(); hr = hr > 12 ? hr - 12 : hr; //秒针 offscreenCanvasCtx.save(); offscreenCanvasCtx.rotate(sec * (Math.PI / 30)); …… offscreenCanvasCtx.stroke();粒子动效canvas可以用来绘制复杂,不规则的动画。粒子特效可以用来实现复杂、随机的动态效果。粒子,指图像数据imageData中的每一个像素点,获取到每个像素点之后,添加属性或事件对区域内的粒子进行交互,达到动态效果。效果粒子获取以下图的图片转化为例,该效果是先在canvas上渲染图片,然后获取文字所在区域的每个像素点。 let image = new Image(); image.src=’../image/logo.png’; let pixels=[]; //存储像素数据 let imageData; image.width = 300; image.height = 300 // 渲染图片,并获取该区域内像素信息 image.onload=function(){ ctx.drawImage(image,(canvas.width-image.width)/2,(canvas.height-image.height)/2,image.width,image.height); imageData=ctx.getImageData((canvas.width-image.width)/2,(canvas.height-image.height)/2,image.width,image.height); //获取图表像素信息 //绘制图像 };像素信息图片的大小为300*300,共有90000个像素,每个像素占4位,存放rgba数据。粒子绘制 function getPixels(){ var pos=0; var data=imageData.data; //RGBA的一维数组数据 //源图像的高度和宽度为300px for(var i=1;i<=image.width;i++){ for(var j=1;j<=image.height;j++){ pos=[(i-1)*image.width+(j-1)]*4; //取得像素位置 if(data[pos]>=0){ var pixel={ x:(canvas.width-image.width)/2+j+Math.random()*20, //重新设置每个像素的位置信息 y:(canvas.height-image.height)/2+i+Math.random()*20, //重新设置每个像素的位置信息 fillStyle:‘rgba(’+data[pos]+’,’+(data[pos+1])+’,’+(data[pos+2])+’,’+(data[pos+3])+’)’ } pixels.push(pixel); } } } } function drawPixels() { var canvas = document.getElementById(“myCanvas”); var ctx = canvas.getContext(“2d”); ctx.clearRect(0,0,canvas.width,canvas.height); var len = pixels.length, curr_pixel = null; for (var i = 0; i < len; i++) { curr_pixel = pixels[i]; ctx.fillStyle = curr_pixel.fillStyle; ctx.fillRect(curr_pixel.x, curr_pixel.y, 1, 1); } }粒子时钟渲染文字时钟 function time() { ctx.clearRect(0,0,canvas.width,canvas.height) ctx.font = “150px 黑体”; ctx.textBaseline=‘top’; ctx.fillStyle = “rgba(245,245,245,0.2)”; ctx.fillText(new Date().format(‘hh:mm:ss’),(canvas.width-textWidth)/2,(canvas.height-textHeight)/2,textWidth,textHeight); }效果获取粒子文字转换粒子概念同上,获取选定区域的像素,根据筛选条件进行选择并存入数组。经过遍历后重新绘制。 function getPixels(){ let imgData = ctx.getImageData((canvas.width-textWidth)/2,(canvas.height-textHeight)/2,textWidth,textHeight); let data = imgData.data pixelsArr = [] for(let i=1;i<=textHeight;i++){ for(let j=1;j<=textWidth;j++){ pos=[(i-1)*textWidth+(j-1)]*4; //取得像素位置 if(data[pos]>=0){ var pixel={ x:j+Math.random()*20, //重新设置每个像素的位置信息 y:i+Math.random()*20, //重新设置每个像素的位置信息 fillStyle:‘rgba(’+data[pos]+’,’+(data[pos+1])+’,’+(data[pos+2])+’,’+(data[pos+3])+’)’ }; pixelsArr.push(pixel); } } } }imgData保存了所选区域内的像素信息,每个像素点占据4位,保存了RGBA四位信息。筛选每个像素的第四位,这段代码中将所有透明度不为0的像素都保存到了数组pixelsArr中。x、y记载了该粒子的位置信息,为了产生效果图中的运动效果,给每个粒子添加了0-20个像素的偏移位置,每次重绘时,偏移位置随机生成,产生运动效果。粒子重绘获取粒子之后,需要清除画布中原有的文字,将获取到的粒子重新绘制到画布上去。 function drawPixels() { // 清除画布内容,进行重绘 ctx.clearRect(0,0,canvas.width,canvas.height); for (let i in pixelsArr) { ctx.fillStyle = pixelsArr[i].fillStyle; let r = Math.random()*4 ctx.fillRect(pixelsArr[i].x, pixelsArr[i].y, r, r); } }粒子重绘时的样式为筛选像素时原本的颜色与透明度,并且每个在画布上绘制每个粒子时,定义大小参数r,r取值为0-4中随机的数字。最终生成的粒子大小随机。实时刷新获取粒子并成功重绘之后,需要页面实时刷新时间。这里采用window.requestAnimationFrame(callback)方法。 function time() { …… getpixels(); //获取粒子 drawPixels(); // 重绘粒子 requestAnimationFrame(time); }window.requestAnimationFrame(callback) 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。该方法不需要设置时间间隔,调用频率采用系统时间间隔(1s)。文档解释戳这里效果总结本文主要通过两种不同的方式实现了时钟的动态效果,其中粒子时钟具有更多的可操作性。在以后的canvas系列中会针对粒子系统实现更多的动态效果。广而告之本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。欢迎讨论,点个赞再走吧 。◕‿◕。 ~ ...

December 29, 2018 · 2 min · jiezi

使用 canvas 绘图

HTML5 添加的最受欢迎的功能就是<canvas>元素,这个元素负责在页面中设定一个区域,然后就可以通过 javascript 动态的在这个区域中绘制图形基本用法使用 canvas 元素,必须指定 width,height 属性开始标签和结束标签之间的内容,是后备信息。如果浏览器不支持 canvas,则显示该信息<canvas id=“drawing” width=“200” height=“200”> drawing something </canvas>要在画布(canvas)上绘图,需获取 2D 上下文。 var drawing = document.getElementById(“drawing”); if(drawing.getContext){ //必须先判断,因为有些浏览器并没有getContext方法 var context = drawing.getContext(“2d”); //更多操作 }2D 上下文原点坐标坐标原点位于 canvas 的左上角,原点为(0,0),越往右 x 值越大,越往下 y 值越大填充和描边2D 上下文绘图操作基本是两种:填充:用指定样式填充图形,操作结果取决于 fillStyle描边:只在图形边缘划线 ,操作结果取决于 strokeStyle将 context.fillStyle 或 context.strokeStyle 设为某个值后,之后所有的描边和填充操作都会使用对应的值,直到重新设置这两个值如何使用将与下面绘制矩形一同介绍绘制矩形矩形是唯一一种可以在 2D 上下文中绘制的形状,包含三个方法:fillRect() 、 strokeRect() 、 clearRect()接受参数都一样是 4 个参数,分别是:x 坐标,y 坐标,宽度,高度下面是使用的例子:var drawing = document.getElementById(“drawing”);if (drawing.getContext) { var context = drawing.getContext(“2d”); context.strokeStyle = “#ff0000”; context.strokeRect(10, 10, 50, 50); //绘制空心矩形(描边) context.fillStyle = “#0000ff”; context.fillRect(70, 10, 50, 50);//绘制实心矩形(填充) context.clearRect(80, 20, 20, 20);//清空某个区域}执行结果:第二个矩形中间的空白部分就是被清空了的绘制路径2D 上下文支持很多在画布上绘制路径的方法,通过路径可以创造出复杂的图形绘制路径的操作流程为:必须调用 beginPath(), 表示开始绘制新路径调用绘制路径的方法。如果想绘制一条连接到路径起点的线条,调用 closePath()对路径进行填充 fill() 或 描边 stroke() 、或者创建一个剪切区域 clip()接下来让我们来绘制一个钟表首先了解下待会会使用到的绘制路径的方法:arc(x , y , radius , startAngle , endAngle , counterclockwise): 以圆心绘制一条弧线,半径为 radius,起始角度和结束角度lineTo(x,y)从上一点(也就是游标当前点)开始绘制一条直线,到 ( x,y ) 为止moveTo(x,y)将游标移动到 ( x , y ), 不画线除了这些之外,绘制路径还有其他一些方法:arcTo , bezierCurveTo , quadraticCurveTo ,rect,这些就请自行参与官方文档了 var drawing = document.getElementById(“drawing”); if (drawing.getContext) { var context = drawing.getContext(“2d”); //1.开始路径 context.beginPath(); //2.开始绘制路径 //绘制外圆 context.arc(100, 100, 99, 0, 2 * Math.PI, false); //绘制内圆 context.moveTo(194, 100);//先把游标移动到要内圆起点 context.arc(100, 100, 94, 0, 2 * Math.PI, false); //绘制分针 context.moveTo(100, 100); context.lineTo(100, 15); //绘制时针 context.moveTo(100, 100); context.lineTo(35, 100); //3.绘制路径完,进行描边 context.stroke(); }执行结果:注意:arc()中的参数度数是弧度而不是角度,1 弧度 = 180° / ,1° = /180 ,所以,旋转 360°,需要旋转 2 弧度绘制文本2D 绘图上下文也提供了绘制文本的方法: fillText() 和 strokeText()都有 4 个参数:要绘制的文本字符串,x 坐标 ,y 坐标,最大像素宽度(可选)在使用绘制文本方法之前,应该先设置下列 3 个属性:font,textAlign,textBaseLine让我们为上一个例子的钟表添加一个 12 的数字context.font = “bold 14px Arial”;context.textAlign = “center”;context.textBaseline = “middle”;context.fillText(“12”, 100, 20);运行结果:变换通过上下文的变换,可以把处理后的图像绘制到画布上。可通过如下方法来修改变换矩阵:rotate(angle):围绕原点旋转图像 angle 弧度。scale(scaleX,scaleY):缩放图像translate(x,y)将坐标原点移到 x , ytransform(m1_1,m1_2,m2_1,m2_2,dx,dy):直接修改变换矩阵变换可能很简单。对于绘制表针来说,如果把原点变换到表盘中心,在绘制表针,就容易得多var drawing = document.getElementById(“drawing”); if (drawing.getContext) { var context = drawing.getContext(“2d”); //1.开始路径 context.beginPath(); //2.开始绘制路径 //绘制外圆 context.arc(100, 100, 99, 0, 2 * Math.PI, false); //绘制内圆 context.moveTo(194, 100);//先把游标移动到要内圆起点 context.arc(100, 100, 94, 0, 2 * Math.PI, false); context.translate(100, 100);//主要是这里将坐标原点移到(0,0),所以接下来的计算都相对于圆心来计算 //绘制分针 context.moveTo(0, 0); context.lineTo(0, -85); //绘制时针 context.moveTo(0, 0); context.lineTo(-65, 0); //3.绘制路径完,进行描边 context.stroke(); }结果与上面一模一样,但是计算就要简单得多。对于上下文的属性与变换,可以使用save()来保存,当时的设置会被保存进一个栈结构。当想使用之前保存的设置,则调用restore()方法绘制图像2D 上下文内置了对图像的支持。drawImage()context.drawImage(img, 0, 0, 200, 200, 0, 0, 300, 300);//参数分别是 : 要绘制的图像、原图像的x、y、宽度、高度、目标图像的x、y、宽度、高度注意:图像需要预加载,或者在 img.onload 中执行 drawImage 才有效toDataURL()var drawing = document.getElementById(“drawing”);if (drawing.getContext) { var context = drawing.getContext(“2d”);}var src = drawing.toDataURL();//获得canvas中的图像数据var canvasImg = new Image();canvasImg.src = src;document.body.appendChild(canvasImg);使用图像数据2D 上下文有个长处,可以通过 getImageData()取得原始图像数据.这个方法非常有用。var imageData = context.getImageData(10,5,50,50);//(10,5)左上角,大小为50*50的区域的图像数据让我做一个灰阶过滤器:var drawing = document.getElementById(“drawing”);if (drawing.getContext) { var context = drawing.getContext(“2d”); var img = document.images[0]; context.drawImage(img, 0, 0); var imageData = context.getImageData(0, 0, img.width, img.height); var data = imageData.data; var red, green, blue, alpha, average; //对图像数据的red green blue 进行处理 for (var i = 0, len = data.length; i < len; i += 4) { red = data[i]; green = data[i + 1]; blue = data[i + 2]; alpha = data[i + 3]; average = Math.floor((red + green + blue) / 3); data[i] = red + 100; data[i + 1] = green + 100; data[i + 2] = blue + 100; } imageData.data = data; context.putImageData(imageData, 0, img.height);}结果:分别是处理前和处理后。图像数据除了做灰阶过滤器,还有很多其他功能,例如 锐化、亮度之类的,有兴趣的可以到这里了解下:http://www.html5rocks.com/en/… ,请自备梯子其他:2d 上下文还有其他的以下功能: 阴影、渐变、 模式、 合成,这些就不细讲了!: )GitHub:https://github.com/chen4342024个人博客:https://chen4342024.github.io…常用代码片段整理:https://chen4342024.github.io… ...

December 25, 2018 · 2 min · jiezi

利用 canvas 压缩图片

利用 canvas 压缩图片前言在一个移动端的项目中,图片上传是一个比较常用的功能。但是,目前手机的随便拍的照片一张都要好几 M , 直接上传的话特别耗费流量,而且所需时间也比较长。所以需要前端在上传之前先对图片进行压缩。原理要使用 js 实现图片压缩效果, 原理其实很简单,主要是:利用 canvas 的 drawImage 将目标图片画到画布上利用画布调整绘制尺寸,以及导出的 quality ,确定压缩的程度利用 canvas的 toDataURL 或者 toBlob 可以将画布上的内容导出成 base64 格式的数据。注意点IOS 下会出现图片翻转的问题这个需要 import EXIF from ’exif-js’;来获取到手机的方向,然后对 canvas 的宽高进行处理压缩到特定大小let imgDataLength = dataUrl.length; 获取到数据后,判断压缩后的图片大小是否满足需求,否则就降低尺寸以及质量,再次压缩quality 对 png 等无效,所以导出格式统一为 jpeg ,透明背景填充为白色// 填充白色背景ctx.fillStyle = fillBgColor;ctx.fillRect(0, 0, size.w, size.h);具体源码/** * 文件读取并通过canvas压缩转成base64 * @param files * @param callback ///EXIF js 可以读取图片的元信息 https://github.com/exif-js/exif-jsimport EXIF from ’exif-js’;// 压缩图片时 质量减少的值const COMPRESS_QUALITY_STEP = 0.03;const COMPRESS_QUALITY_STEP_BIG = 0.06;// 压缩图片时,图片尺寸缩放的比例,eg:0.9, 等比例缩放为0.9const COMPRESS_SIZE_RATE = 0.9;let defaultOptions = { removeBase64Header: true, // 压缩后允许的最大值,默认:300kb maxSize: 200 * 1024, fillBgColor: ‘#ffffff’};/* * 将待上传文件列表压缩并转换base64 * !!!! 注意 : 图片会默认被转为 jpeg , 透明底会加白色背景 * files : 文件列表 ,必须是数组 * callback : 回调,每个文件压缩成功后都会回调, * options :配置 * options.removeBase64Header : 是否需要删除 ‘data:image/jpeg;base64,‘这段前缀,默认true * @return { base64Data: ‘’,fileType: ’’ }, //fileType强制改为jpeg /export function imageListConvert(files, callback, options = {}) { if (!files.length) { console.warn(‘files is null’); return; } options = { …defaultOptions, …options }; // 获取图片方向--iOS拍照下有值 EXIF.getData(files[0], function() { let orientation = EXIF.getTag(this, ‘Orientation’); for (let i = 0, len = files.length; i < len; i++) { let file = files[i]; let fileType = getFileType(file.name); //强制改为jpeg fileType = ‘jpeg’; let reader = new FileReader(); reader.onload = (function() { return function(e) { let image = new Image(); image.onload = function() { let data = convertImage( image, orientation, fileType, options.maxSize, options.fillBgColor ); if (options.removeBase64Header) { data = removeBase64Header(data); } callback({ base64Data: data, fileType: fileType }); }; image.src = e.target.result; }; })(file); reader.readAsDataURL(file); } });}/* * 将 image 对象 画入画布并导出base64数据 /export function convertImage( image, orientation, fileType = ‘jpeg’, maxSize = 200 * 1024, fillBgColor = ‘#ffffff’) { let maxWidth = 1280, maxHeight = 1280, cvs = document.createElement(‘canvas’), w = image.width, h = image.height, quality = 0.9; /* * 这里用于计算画布的宽高 / if (w > 0 && h > 0) { if (w / h >= maxWidth / maxHeight) { if (w > maxWidth) { h = (h * maxWidth) / w; w = maxWidth; } } else { if (h > maxHeight) { w = (w * maxHeight) / h; h = maxHeight; } } } let ctx = cvs.getContext(‘2d’); let size = prepareCanvas(cvs, ctx, w, h, orientation); // 填充白色背景 ctx.fillStyle = fillBgColor; ctx.fillRect(0, 0, size.w, size.h); //将图片绘制到Canvas上,从原点0,0绘制到w,h ctx.drawImage(image, 0, 0, size.w, size.h); let dataUrl = cvs.toDataURL(image/${fileType}, quality); //当图片大小 > maxSize 时,循环压缩,并且循环不超过5次 let count = 0; while (dataUrl.length > maxSize && count < 10) { let imgDataLength = dataUrl.length; let isDoubleSize = imgDataLength / maxSize > 2; // 质量一次下降 quality -= isDoubleSize ? COMPRESS_QUALITY_STEP_BIG : COMPRESS_QUALITY_STEP; quality = parseFloat(quality.toFixed(2)); // 图片还太大的情况下,继续压缩 。 按比例缩放尺寸 let scaleStrength = COMPRESS_SIZE_RATE; w = w * scaleStrength; h = h * scaleStrength; size = prepareCanvas(cvs, ctx, w, h, orientation); //将图片绘制到Canvas上,从原点0,0绘制到w,h ctx.drawImage(image, 0, 0, size.w, size.h); console.log(imgDataLength:${imgDataLength} , maxSize --&gt; ${maxSize}); console.log(size.w:${size.w}, size.h:${size.h}, quality:${quality}); dataUrl = cvs.toDataURL(image/jpeg, quality); count++; } console.log(imgDataLength:${dataUrl.length} , maxSize --&gt; ${maxSize}); console.log(size.w:${size.w}, size.h:${size.h}, quality:${quality}); cvs = ctx = null; return dataUrl;}/* * 准备画布 * cvs 画布 * ctx 上下文 * w : 想要画的宽度 * h : 想要画的高度 * orientation : 屏幕方向 /function prepareCanvas(cvs, ctx, w, h, orientation) { cvs.width = w; cvs.height = h; //判断图片方向,重置canvas大小,确定旋转角度,iphone默认的是home键在右方的横屏拍摄方式 let degree = 0; switch (orientation) { case 3: //iphone横屏拍摄,此时home键在左侧 degree = 180; w = -w; h = -h; break; case 6: //iphone竖屏拍摄,此时home键在下方(正常拿手机的方向) cvs.width = h; cvs.height = w; degree = 90; // w = w; h = -h; break; case 8: //iphone竖屏拍摄,此时home键在上方 cvs.width = h; cvs.height = w; degree = 270; w = -w; // h = h; break; } // console.log(orientation --&gt; ${orientation} , degree --&gt; ${degree}); // console.log(w --&gt; ${w} , h --&gt; ${h}); //使用canvas旋转校正 ctx.rotate((degree * Math.PI) / 180); return { w, h };}/* * 截取 ‘data:image/jpeg;base64,’, * 截取到第一个逗号 */export function removeBase64Header(content) { if (content.substr(0, 10) === ‘data:image’) { let splitIndex = content.indexOf(’,’); return content.substring(splitIndex + 1); } return content;}export function getFileType(fileName = ‘’) { return fileName.substring(fileName.lastIndexOf(’.’) + 1);}export function checkAccept( file, accept = ‘image/jpeg,image/jpg,image/png,image/gif’) { return accept.toLowerCase().indexOf(file.type.toLowerCase()) !== -1;}相关链接个人博客代码片段 ...

December 20, 2018 · 3 min · jiezi

canvas 绘制双线技巧

楔子最近一个项目,需要绘制双线的效果,双线效果表示的是轨道(类似铁轨之类的),如下图所示:负责这块功能开发的小伙,姑且称之为L吧,最开始是通过数学计算的方式来实现这种双线,也就是在原来的路径的基础上,计算出两条路径。但是这个过程的计算算挺复杂,而是最终实现的效果很耗性能,性能损耗估计主要在于路径的计算上。优化技巧后来他找到我来看这个问题,我在分析了项目背景的情况下,给予了一个简单的绘制技巧,就是先用较粗的线条绘制路径,然后再用较细的线条绘制路径,较细线条的颜色正好是背景颜色。之所以能够使用这个技巧,是因为该项目的绘制背景是纯色的,而不是渐变色或者图片。示例代码如下: ctx.beginPath(); ctx.fillStyle = ‘blue’; ctx.rect(10,10,1000,1000); ctx.fill(); ctx.save(); ctx.strokeStyle = ‘red’; ctx.lineWidth = 10; ctx.lineCap = “round”; ctx.beginPath(); ctx.moveTo(200,100); //起始点 ctx.lineTo(400,100); ctx.quadraticCurveTo(500,100,500,200); ctx.lineTo(500,400); ctx.quadraticCurveTo(500,500,400,500); ctx.lineTo(200,500); ctx.quadraticCurveTo(100,500,100,400); ctx.lineTo(100,200); ctx.quadraticCurveTo(100,100,200,100); ctx.stroke(); ctx.strokeStyle= ‘blue’ ctx.lineWidth = 4; ctx.stroke(); ctx.restore(); 代码的思路是,首先使用纯色blue绘制了一个背景,然后使用线条颜色red绘制一条线,然后使用较小的线宽,并把线条颜色改成背景颜色blue,绘制另外一个条线段。最终的绘制效果如下:到此,项目的这个技术难点问题,算是被解决了。这种解决方法,不仅算法简单,不用构思数学方法来构造双线,而且轻量,不会有性能负担。背景不是纯色情况前面说到:之所以能够使用这个技巧,是因为该项目的绘制背景是纯色的,而不是渐变色或者图片。那如果背景是图片或者渐变颜色情况下,用这种技巧,肯定就是失效的了。之所以会思考这个问题,是得益于公司的技术分享会。我会要求员工定期组织分享会,分享一些经验。在此打个小广告,可以看出我们公司的技术氛围是很好的,所以有兴趣的小伙伴可以抓紧时间投简历。怎么投简历呢,关注微信号ITman彪叔。过程中,当时小伙伴L也分享了前面提到这种思路。在分享的过程中,我提出了进一步的问题,如果背景不是纯色,而是渐变色或者图片怎么办?并且灵感乍现,想到了一个解决方法,就是使用ctx.globalCompositeOperation。有关globalCompositeOperation的说明,可以参考如下链接的说明:https://developer.mozilla.org…http://www.w3school.com.cn/ta...globalCompositeOperation的定义和用法globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有)的图像上。其中:源图像 = 您打算放置到画布上的绘图。目标图像 = 您已经放置在画布上的绘图下图显示了globalCompositeOperation的不同的值的解释:要实现双线的绘制,就要求用同样的路径,不同的线宽绘制两条线路(我们称之为目标线路和源线路)。并要达到一条线路抠出另外一条线路的效果。结合上图,我们可以看出destination-out,source-out,xor可以达到效果。下面以destination-out举例说明。destination-out绘制原理说明比如首先通过 css 设置背景图,并去掉绘制背景颜色,代码如下: <body onload=“init()” style=“background: url(../test/images/diffuse.png);">然后绘制代码如下: ctx.save(); ctx.strokeStyle = ‘red’; ctx.lineWidth = 10; ctx.lineCap = “round”; ctx.beginPath(); ctx.moveTo(200,100); //起始点 ctx.lineTo(400,100); ctx.quadraticCurveTo(500,100,500,200); ctx.lineTo(500,400); ctx.quadraticCurveTo(500,500,400,500); ctx.lineTo(200,500); ctx.quadraticCurveTo(100,500,100,400); ctx.lineTo(100,200); ctx.quadraticCurveTo(100,100,200,100); ctx.stroke(); ctx.globalCompositeOperation = ‘destination-out’; ctx.lineWidth = 4; ctx.stroke(); ctx.restore();首先设置路径,然后设置线宽为10,调用stroke方法绘制一条线宽为10的路线A。之后设置globalCompositeOperation为 ‘destination-out’,调整线宽为4,调用stroke方法绘制一条线宽为4的路线B。 看下destination-out的解释:在源图像外显示目标图像。只有源图像外的目标图像部分会被显示,源图像是透明的。绘制了线路A的canvas图像是目标图像,线路B是源图像。根据上面解释,只有源图像之外的目标图像能够被显示。最终绘制的效果如下:xor 和 source-out把上面的代码的globalCompositeOperation修改成xor,发现效果也是可以的,xor的解释如下:使用异或操作对源图像与目标图像进行组合。 英文解释如下:Shapes are made transparent where both overlap and drawn normal everywhere else.意思源和目标的像素重叠(overlap)的部分会被变成透明像素,其他部分正常绘制。 所以上面示例中,线条A和线条B重叠的部分会被变成透明。绘制的效果也是线条A的被挖空。对于source-out,其效果正好和destination-out的效果相反:在目标图像之外显示源图像。只会显示目标图像之外源图像部分,目标图像是透明的。应此只需要取反操作即可,先用宽度4绘制线条A,然后用宽度10绘制线条B,其结果也是一样的。背景不是纯色情况2前面的背景是通过css的方式设置上去的,如果是通过canvas的drawImage直接绘制上去,效果就不一样了。还是以destination-out为例说明,首先绘制了image,然后绘制线路A,此时的目标图像不在是线路A组成的图形,而是image和线路A组合成的图形,此时用destination-out的方式绘制线路B,不仅会挖空线路A,背景也会被挖空,如下图所示:应此要想达到真正的双线效果,要么背景只能是用css设置,要么用两个canvas叠加,一个绘制背景图片,一个绘制路径。当然还有一种方式,就是绘制双线总是在一个临时的canvas上面进行,然后把这个临时的canvas绘制结果再次绘制到工作canvas上面,相关实践留给读者自己进行。后记在网络上面搜索canvas double line,搜索到stackoverflow上的一条结果如下:https://stackoverflow.com/que…其中的答案也是采用了globalCompositeOperation设置为destination-out的方式。欢迎关注公众号“ITman彪叔”。彪叔,拥有10多年开发经验,现任公司系统架构师、技术总监、技术培训师、职业规划师。熟悉Java、JavaScript、Python语言,熟悉数据库。熟悉java、nodejs应用系统架构,大数据高并发、高可用、分布式架构。在计算机图形学、WebGL、前端可视化方面有深入研究。对程序员思维能力训练和培训、程序员职业规划有浓厚兴趣。 ...

December 20, 2018 · 1 min · jiezi

炫酷粒子表白,双十一脱单靠它了!

双十一光棍节又要来临了,每年这个时候都是本人最苦闷的时刻。日渐消瘦的钱包,愈发干涸的双手,虽然变强了,头却变凉了。今年一定要搞点事情!<img src=“https://img.alicdn.com/tfs/TB...; alt=“fxxking things” width=“160”> 最近听女神说想谈恋爱了,✧(≖ ◡ ≖) 嘿嘿,一定不能放过这个机会,给她来个不一样的表白。<img src=“https://img.alicdn.com/tfs/TB...; alt=“我老婆” width=“360”>作为整天搞可视化的前端攻城狮,最先想到的就是常玩的各种粒子。那么咱们就一起来把这个粒子系统玩出花来吧。演示地址用粒子组成文字首先,咱们想下要如何将一系列的粒子组成一句表白呢?实现原理其实很简单,Canvas 中有个 getImageData 的方法,可以得到一个矩形范围所有像素点数据。那么我们就试试来获取一个文字的形状吧。第一步,用 measureText 的方法来计算出文字适当的尺寸和位置。// 创建一个跟画布等比例的 canvasconst width = 100;const height = ~~(width * this.height / this.width); // this.width , this.height 说整个画布的尺寸const offscreenCanvas = document.createElement(‘canvas’);const offscreenCanvasCtx = offscreenCanvas.getContext(‘2d’);offscreenCanvas.setAttribute(‘width’, width);offscreenCanvas.setAttribute(‘height’, height);// 在这离屏 canvas 中将我们想要的文字 textAll 绘制出来后,再计算它合适的尺寸offscreenCanvasCtx.fillStyle = ‘#000’;offscreenCanvasCtx.font = ‘bold 10px Arial’;const measure = offscreenCanvasCtx.measureText(textAll); // 测量文字,用来获取宽度const size = 0.8;// 宽高分别达到屏幕0.8时的sizeconst fSize = Math.min(height * size * 10 / lineHeight, width * size * 10 / measure.width); // 10像素字体行高 lineHeight=7 magicoffscreenCanvasCtx.font = bold ${fSize}px Arial;// 根据计算后的字体大小,在将文字摆放到适合的位置,文字的坐标起始位置在左下方const measureResize = offscreenCanvasCtx.measureText(textAll);// 文字起始位置在左下方let left = (width - measureResize.width) / 2;const bottom = (height + fSize / 10 * lineHeight) / 2;offscreenCanvasCtx.fillText(textAll, left, bottom);咱们可以 appendChild 到 body 里看眼好的。同学们注意,我要开始变形了 [推眼镜] 。getImageData 获取的像素数据是一个 Uint8ClampedArray (值是 0 - 255 的数组),4 个数一组分别对应一个像素点的 R G B A 值。我们只需要判断 i * 4 + 3 不为 0 就可以得到需要的字体形状数据了。// texts 所有的单词分别获取 data ,上文的 textAll 是 texts 加一起Object.values(texts).forEach(item => { offscreenCanvasCtx.clearRect(0, 0, width, height); offscreenCanvasCtx.fillText(item.text, left, bottom); left += offscreenCanvasCtx.measureText(item.text).width; const data = offscreenCanvasCtx.getImageData(0, 0, width, height); const points = []; // 判断第 i * 4 + 3 位是否为0,获得相对的 x,y 坐标(使用时需乘画布的实际长宽, y 坐标也需要取反向) for (let i = 0, max = data.width * data.height; i < max; i++) { if (data.data[i * 4 + 3]) { points.push({ x: (i % data.width) / data.width, y: (i / data.width) / data.height }); } } // 保存到一个对象,用于后面的绘制 geometry.push({ color: item.hsla, points });})制定场景,绘制图形文字图形的获取方式以及搞定了,那么咱们就可以把内容整体输出了。咱们定义一个简单的脚本格式。// hsla 格式方便以后做色彩变化的扩展const color1 = {h:197,s:‘100%’,l:‘50%’,a:‘80%’};const color2 = {h:197,s:‘100%’,l:‘50%’,a:‘80%’};// lifeTime 祯数const Actions = [ {lifeTime:60,text:[{text:3,hsla:color1}]}, {lifeTime:60,text:[{text:2,hsla:color1}]}, {lifeTime:60,text:[{text:1,hsla:color1}]}, {lifeTime:120,text:[ {text:‘I’,hsla:color1}, {text:’❤️’,hsla:color2}, {text:‘Y’,hsla:color1}, {text:‘O’,hsla:color1}, {text:‘U’,hsla:color1} ]},];根据预设的脚本解析出每个场景的图形,加一个 tick 判断是否到了 lifeTime 切换到下一个图形重新绘制图形。function draw() { this.tick++; if (this.tick >= this.actions[this.actionIndex].lifeTime) { this.nextAction(); } this.clear(); this.renderParticles(); // 绘制点 this.raf = requestAnimationFrame(this.draw);}function nextAction() { ….//切换场景 balabala.. this.setParticle(); // 随机将点设置到之前得到的 action.geometry.points 上}这样咱们基本的功能已经完成了。能不能再给力一点说好的粒子系统,现在只是 context.arc 简单的画了一点。那咱们就来加个粒子系统吧。class PARTICLE { // x,y,z 为当前的坐标,vx,vy,vz 则是3个方向的速度 constructor(center) { this.center = center; this.x = 0; this.y = 0; this.z = 0; this.vx = 0; this.vy = 0; this.vz = 0; } // 设置这些粒子需要运动到的终点(下一个位置) setAxis(axis) { this.nextX = axis.x; this.nextY = axis.y; this.nextZ = axis.z; this.color = axis.color; } step() { // 弹力模型 距离目标越远速度越快 this.vx += (this.nextX - this.x) * SPRING; this.vy += (this.nextY - this.y) * SPRING; this.vz += (this.nextZ - this.z) * SPRING; // 摩擦系数 让粒子可以趋向稳定 this.vx *= FRICTION; this.vy *= FRICTION; this.vz *= FRICTION; this.x += this.vx; this.y += this.vy; this.z += this.vz; } getAxis2D() { this.step(); // 3D 坐标下的 2D 偏移,暂且只考虑位置,不考虑大小变化 const scale = FOCUS_POSITION / (FOCUS_POSITION + this.z); return { x: this.center.x + (this.x * scale), y: this.center.y - (this.y * scale), }; }}大功告成!既然是 3D 的粒子,其实这上面还有不是文章可做,同学们可以发挥想象力来点更酷炫的。还有什么好玩的上面是将粒子摆成文字。那咱们当然也可以直接写公式摆出个造型。// Actions 中用 func 代替 texts{ lifeTime: 100, func: (radius) => { const i = Math.random() * 1200; let x = (i - 1200 / 2) / 300; let y = Math.sqrt(Math.abs(x)) - Math.sqrt(Math.cos(x)) * Math.cos(30 * x); return { x: x * radius / 2, y: y * radius / 2, z: (Math.random() * 30), color: color3 }; }}再把刚才文字转换形状的方法用一下{ lifeTime: Infinity, func: (width, height) => { if(!points.length){ const img = document.getElementById(“tulip”); const offscreenCanvas = document.createElement(‘canvas’); const offscreenCanvasCtx = offscreenCanvas.getContext(‘2d’); const imgWidth = 200; const imgHeight = 200; offscreenCanvas.setAttribute(‘width’, imgWidth); offscreenCanvas.setAttribute(‘height’, imgHeight); offscreenCanvasCtx.drawImage(img, 0, 0, imgWidth, imgHeight); let imgData = offscreenCanvasCtx.getImageData(0, 0, imgWidth, imgHeight); for (let i = 0, max = imgData.width * imgData.height; i < max; i++) { if (imgData.data[i * 4 + 3]) { points.push({ x: (i % imgData.width) / imgData.width, y: (i / imgData.width) / imgData.height }); } } } const p = points[(Math.random() * points.length)] const radius = Math.min(width * 0.8, height * 0.8); return { x: p.x * radius - radius / 2, y: (1 - p.y) * radius - radius / 2, z: ~~(Math.random() * 30), color: color3 }; }}完美 ????。然后咱也可以用 drawImage 绘制图片来代替 arc 画点。<img src=“https://img.alicdn.com/tfs/TB...; width=“160”>等等!!前面的效果总觉得哪里不对劲,好像有些卡 。优化小提示分层。如果还需要增加一些其他的内容到 Canvas 中的话,可以考虑拆出多个 Canvas 来做。减少属性设置。包括 lineWidth、fillStyle 等等。Canvas 上下文是很复杂的一个对象,当你调它的一些属性设置时消耗的性能还是不少的。arc 之类的画图方法也要减少。离屏绘制。不用 arc 画点,要怎么办。上面的延迟其实就是每次画点时都调用了一遍 fillStyle、arc。但是当我们绘制图片 drawImage 时,性能明显会好上很多。drawImage 除了直接绘图片外,还能绘制另一个 Canvas,所以我们提前将这些点画到一个不在屏幕上的 Canvas 里就可以了。减少 js 计算,避免堵塞进程,可以使用 web worker。当然咱们目前的计算完全用不上这个。我这使用了 3000 个粒子,对比下使用离屏绘制前后的帧率。总结现在唯一限制你的就是想象力了。大家一起去征服老板,征服女神!这个双十一脱贫脱单不脱发!好了不说了,女神喊我修电脑去了。参考deformable particlesheart.pngCanvas 参考手册y=sqrt(abs(x))-sqrt(cos(x))*cos(40x)文章可随意转载,但请保留此 原文链接。非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。 ...

November 5, 2018 · 3 min · jiezi

小程序项目之填坑小记

作者:首席填坑官∙苏南公众号:honeyBadger8,本文原创,著作权归作者所有,转载请注明原链接及出处。简诉 是的,真的,你没有看错,我就是上次那个加薪的,但是现在问题来了,最近又搞了个小程序的需求,又填了不少坑,其中的辛酸就不说了,说多了都是泪????????,此处省略三千字 ………^……,说重点吧,反正最后就是差点这让老板叫走人了,你说优秀不优秀~。 前段时间网上一直说的“<u>你可以骂那些中年人,尤其是有车有房的……</u>”,虽然我没有房、也没有车,但也坚决不做那个可以随便骂的中年人(人到中年不如狗??),不存在的啦,这个仇宝宝已经记下了????,先分享一下最近遇到的几个坑吧。 —— 我是首席填坑官——苏南,早上好,新的一周加油。填坑一,canvas遮挡问题:随着小程序的API调整,很多东西都要用户手动授权,不能直接调用后,toast、弹窗这种提示的场景越来越多了,下图就是公司活动的canvas合成,现在微信API不让直接调用授权了,某些场景要多一个弹窗来提示用户开启设置,但当遇上canvas API这个大佬后,一切都变了,谁都只能站在它后面,场景一 :如之前拒绝授权了,后续引导用户打开设置页,即 wx.openSetting,下图就是:坑一 小结 :当遇上这种情况,我的第一思路是 设置样式:visibility: hidden;opacity:0;,但是结果是让人失望的,canvas 大佬就是大佬,这两属性在手机上失效了,该显示还是显示,你阻挡不了它的光辉,真的,不信可以去测试!解决思路:canvas 图片合成,获取到图片的地址后,隐藏canvas,改用image标签显示,这种场景有局限性,如果你的需求是echart交互的,显示挂了;cover-view 貌似也是有局限,<cover-view /> 内只能嵌套 <cover-view /> 和 <cover-image />,view 标签的子节点树在真机上都会被忽略,这是我测试后的浏览器给出的警告,如果自定modal,要加button按钮让用户点击后授权某功能,肯定也就挂了 ;当弹窗出现的时候,隐藏canvas,这种比较暴力,但覆盖面广,任何场景都能照顾到,却也影响体验;把canvas定位移动到屏幕之外绘制内容;有同学可能说直接使用原生的 wx.showModal,但官方目前,button还不支持设置open-type属性;等微信小程序官方修复????,好吧,看到这里你肯定笑了~,这不是一个方法,估计还没等到老板真叫你走人了,欢迎大佬们补充!!!填坑二,Maximum call stack size exceeded发现这个bug,要从最近换了个手机说起,用了3年的5S终于歇菜了(再也买不起iphone了~),换了个android vivo x23, 以为从此走上人生巅峰了,现实却给了我一个响亮的耳光,又是一个记忆深刻的梗~,被组里的同事笑话好久!!话说,堆栈溢出,是怎么造成的呢?—— 循环引用;同时我又有些疑惑了,为什么其他手机都正常,就vivo 报了这个错,同样的代码,希望有大神看到能给于解惑!先来看个示例,简单演示一下 :let sum = 20; (function test(){ sum–; console.log(sum); test(); /* if( sum > 0 ){ test(); }*/ })()# 而项目中的报错是这样的 #: //fetch.js import wepy from ‘wepy’ import login from ‘./login’; ……省略N行 //login.js import {fetch} from “./fetch.js”; import Storage from “./storage.js”; ……省略N行 //更改后 login.js ,避免了循环引用 loginFn = ()=>{ require("./fetch.js").fetch({ url:"/wabApi/login/login", }); }坑二 小结 :循环引用,可以理解为 a.js内调用了b.js,b.js里又引用了a.js,所以在项目开发中要注意一下,看了下网上的讨论,这个问题需要等官方解决,貌似h5里是可以这样写的。填坑三,canvasGetImageData、canvasToTempFilePath这两个方法,之间的调用,要做一定的延时,不明白是为什么,如果不做延时,也不会报错,也不提示,方法执行完,canvas上还是空白的;但是让人尴尬的是,此在写总结的我,又验证了一下,不加setTimeout,调试器上可以,真机挂了!目测跟绘制的目标对象大小有关系!其他微信API的调整,如:「 wx.getUserInfo」「 wx.getSetting」「 wx.openSetting」「 wx.getPhoneNumber」等这些现在需要添加按钮,用户手动来点击,带来的不便大家都知道了,就不再多说;字体文件 ,加载报错,但也能正常显示,而且只有第一次报错哦;其他还有待发现的坑…… @font-face { font-family: ’test’; src: url(“https://cdn-xx.xxx.com/common/font/font.ttf") format(’truetype’); font-weight: normal; font-style: normal; }扯淡段子 小明公司之前上线的小程序项目,好久没有迭代了,产品说有个需求要改一下,很快,就一点点东西,比如一个按钮UI调整一下,改了赶紧发上去,嗯,最好今天就发了审核吧; 这话,是你会怎么接呢??小明说要一天,产品就惊呆了????,这家伙没有发烧吧??小明后来经过半天的努力,终于让产品知道了小程序API更新后,再发布的相关流程都要改的; 有谁能理解小明的痛苦?有谁能理解小程序的API更新机制?更新过的API没有向下兼容的余地,已经发布过的就放过你了,但是你再改动,所有它改过的流程,你都要改一遍。结尾 开心一笑,给自己找点乐,为今天的分享画上圆满的句号,以上就是我最近的一次小小填坑记整理,希望能给其他同学带来些许帮助,文中如有理解不足之处,请指正????。作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流群:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 5, 2018 · 1 min · jiezi

佛系码农~手把手教你如何绘制一辆会跑车

作者:首席填坑官∙苏南来源:@IT·平头哥联盟 交流群:912594095。本文原创,著作权归作者所有,转载请注明原链接及出处前言 灵感来源于前些天捡到钱了,就想着是时候给自己买辆车了,工作这么多年了应该对自己好一点,在网上搜索了一下看到这个车型。其实几年前是买过一辆的,但是不到一个月就被人偷了,伤心了好久。这次一定锁好,上三把锁保证小偷再也偷不走了,于是我拿着钱去买了些益力多,跟同事分享了,心情还是比较愉悦的。—— @IT·平头哥联盟,我是首席填坑官∙苏南(South·Su) ^_^~ 但想来作为一名程序(嗯,还是个菜鸟,专业首席填坑官哦????),车基本是用不上的啦,为啥?因为有改不完的bug,记得刚毕业那时候最大的梦想是:“撩个妹子 携手仗剑天涯,惩奸除恶、劫富济贫,快意人生~”,无奈一入IT深似海,从此BUG改不完啊。所以还是多学习吧,这不就学着画了个车满足一下自己的心里安慰,在这里把大家一起分享一下,唉,有点扯偏了~,大家先来看一下最终的效果图吧! 过程解析: 效果已经看了到,有没有感觉很牛B??其实也就一般般啦~,接下来就让我带大家一起分解一下它的实现过程吧 canvas中文名中:画布,它就跟我们在纸上画画一样,画某样东西之前,我们要先学会构思、拆解你要画的东西,就跟汽车、手机等东西一样,一个成品都是由很多零件组成的,当你拆解开来,一点点完成再组装的,就会变的容易的多。绘制地平线 :首先我们基于画布的高度取一定的比例,在底部画一条线;从观察动画,它还有几个点,这个是用于视差滚动的时候,来欺骗我们的眼睛的,直接一条线肯定再怎么动也没有用,点的移动可以形成一个动画的效果;再加一点修饰,几个点移动有点太单调了,大家可以想像一下,当你骑车的时候,车的速度与周围的事物、建筑、人产生一个交差,那种感觉是很刺激的,那么我们也来加一点东西,让动画看起来更丰富一些,我选择了 三条线,线本身有个渐变过渡的效果,比纯色要灵动些动画看起来更逼真,而且初始它是不在画布范围内的,这个点要注意一下;下面的两张图,第二张是生成gif工具里截出来的,它就是动画的分解,其实所谓的动画,也是由一张张静态图组成,然后快速过渡,让视觉形成了视差,最后欺骗了大脑,我看见动画了……知识点:lineTo、strokeStyle、stroke、restore等,这里不一一讲解了,如有不了解可自行查看 w3school API, horizon(){ /** * 轮子的底部,也称地平线: 1.清除画布 2.画一条直线,且高度6px 本文@IT·平头哥联盟-首席填坑官∙苏南分享,非商业转载请注明原链接及出处 / this.wheelPos = []; this.ctx.save(); this.ctx.clearRect(0, 0, this.canvasW, this.canvasH); let horizonX = 0,horizonY = this.canvasH-100; this.ctx.beginPath(); this.ctx.strokeStyle = this.color; this.ctx.lineWidth=6; this.ctx.moveTo(horizonX,horizonY); this.ctx.lineTo(this.canvasW,horizonY); this.ctx.closePath(); this.ctx.stroke(); Array.from({length:5}).map((k,v)=>{ let dotProportion = (this.canvasW0.49)v-this.oneCent; this.wheelPos.push({x:dotProportion,y:horizonY-this.wheelRadius}); let startX = dotProportion-(this.animateNum2); //用于动画滚动移动 this.ctx.beginPath(); this.ctx.strokeStyle = “#f9f8ef”; this.ctx.lineWidth=6; this.ctx.moveTo(startX,horizonY); this.ctx.lineTo(startX+5,horizonY); this.ctx.closePath(); this.ctx.stroke(); }); this.ctx.restore(); this.shuttle(); // this.wheel(); } shuttle(){ /** * 画几根横线,有点视差,感觉骑车在飞速穿梭的感觉: 本文@IT·平头哥联盟-首席填坑官∙苏南分享,非商业转载请注明原链接及出处 / let shuttleX = this.canvasW+100, shuttleY = this.canvasH/6; let shuttleW = shuttleX+100; [0,40,0].map((k,v)=>{ let random = Math.random()+2; let x = shuttleX+k-(this.animateNum(2.2random)); let y = shuttleY+v24; let w = shuttleW+k-(this.animateNum*(2.2random)); let grd=this.ctx.createLinearGradient(x,y,w,y); grd.addColorStop(0,"#30212c"); grd.addColorStop(1,"#fff"); this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle = grd; this.ctx.lineWidth=3; this.ctx.moveTo(x,y); this.ctx.lineTo(w,y); this.ctx.stroke(); this.ctx.closePath(); }); }绘制车轮 :接下来我们来画车的两个轮子,轮子的位置在哪里呢?我也是观察了有一会才发现的,其实刚才的地平线,两点的位置,就是车轮的中心点;所以在刚才绘制点的时候,就记录了5个点的坐标,这样就省去了一次计算,中间有两次是我们需要的知识点:arc、fill console.log(this.wheelPos); this.wheelPos = this.wheelPos.slice(1,3); //这里取1-3 console.log(this.wheelPos); this.wheelPos.map((wheelItem,v)=>{ let wheelItemX = wheelItem.x, wheelItemY= wheelItem.y-this.wheelBorder/1.5; //外胎 this.ctx.beginPath(); this.ctx.lineWidth=this.wheelBorder; this.ctx.fillStyle = “#f5f5f0”; this.ctx.strokeStyle = this.color; this.ctx.arc(wheelItemX,wheelItemY,this.wheelRadius,0,Math.PI2,false); this.ctx.closePath(); this.ctx.stroke(); this.ctx.fill(); //最后两轮胎中心点圆轴承 this.axisDot(wheelItemX,wheelItemY); this.ctx.restore(); }); this.ctx.restore();同理,上面画好了两个圆,但车轮肯定有轴承,前后轮我做了些汪样的处理,后轮是实心的加了个填充;前轮是画了一点断点的圆,用于动画的转动,在外轮的半径上进行缩小一定比较,画内圈,这里我取了外圈的.94,作为内圆的半径,还加了两个半圆的描边修饰,让动画跑起来的时候,车轮有动起来的感觉,半圆 Math.PI 就是一个180,(Math.PI * degrees) / 180; degrees 就是我们想要绘制的起始/结束角度;从下图可以看出,圆的填充用了 放射性渐变,createRadialGradient-创建放射状/环形的渐变(用在画布内容上) context.createRadialGradient(x0,y0,r0,x1,y1,r1); + createRadialGradient API 说明: x0 = 渐变的开始圆的 x 坐标 y0 = 渐变的开始圆的 y 坐标 r0 = 开始圆的半径 x1 = 渐变的结束圆的 x 坐标 y1 = 渐变的结束圆的 y 坐标 r1 = 结束圆的半径 详细使用请看下面代码的实例 let scaleMultiple = this.wheelRadius*.94; let speed1 = this.animateNum2; //外圈半圆速度 let speed2 = this.animateNum3; //内小圈半圆速度 //后轮 if(v === 0){ //内圆 this.ctx.beginPath(); let circleGrd=this.ctx.createRadialGradient(wheelItemX,wheelItemY,18,wheelItemX,wheelItemY,scaleMultiple); circleGrd.addColorStop(0,"#584a51"); circleGrd.addColorStop(1,"#11090d"); this.ctx.fillStyle = circleGrd; this.ctx.arc(wheelItemX,wheelItemY,scaleMultiple,0,Math.PI2,false); this.ctx.fill(); this.ctx.closePath(); //两个半圆线 [ {lineW:2,radius:scaleMultiple.6,sAngle:getRads(-135+speed1) , eAngle:getRads(110+speed1)}, {lineW:1.2,radius:scaleMultiple*.45,sAngle:getRads(45+speed2) , eAngle:getRads(-50+speed2)} ].map((k,v)=>{ this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle ="#fff"; this.ctx.lineWidth=k.lineW; this.ctx.arc(wheelItemX,wheelItemY,k.radius,k.sAngle,k.eAngle,true); this.ctx.stroke(); this.ctx.closePath(); }); this.ctx.restore(); } 拉下来我们就拿前轮开刀 :前轮也是画了几个半圆,大概就是以某个角度为起点,然后分别画几个半圆,整体是一个半径,中间有断开,如: eAngle = [0,135,270], sAngle = [-45,0,180];就能画出如下图的圆: 具体实现请看下面代码 : //两个圆,再缩小一圈,画线圆 Array.from({length:3}).map((k,v)=>{ let prevIndex = v-1 <= 0 ? 0 : v-1; let eAngle = v135, sAngle = -45+(prevIndex45)+v90; let radius = scaleMultiple.75; let color = “#120008”; this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle = color; this.ctx.lineWidth=3.5; this.ctx.arc(wheelItemX,wheelItemY,radius,getRads(sAngle+speed1),getRads(eAngle+speed1),false); this.ctx.stroke(); this.ctx.closePath(); if(v<2){ //再缩小一圈 let eAngleSmaller = 15+ v210, sAngleSmaller = -30+v90; let radiusSmaller = scaleMultiple*.45; this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle = color; this.ctx.lineWidth=3; this.ctx.arc(wheelItemX,wheelItemY,radiusSmaller,getRads(sAngleSmaller+speed2),getRads(eAngleSmaller+speed2),false); this.ctx.stroke(); this.ctx.closePath(); } this.ctx.restore(); });绘制车身车架 :车架,应该也是本次分享中较大的难点之一,刚开始我也是这么认为的,但认真冷静、冷静、静静之后分析也还好,最开始是用了最笨的办法,lineTO、moveTo、一根一根线的画,画到一半时发现画两个三角或者一个菱形即可,然后再把几根主轴重新画一下,于是两种方法都尝试了一下,先说三角的吧,配合下面画的一个图讲解一下,找到圆盘的中心点,介于后轮半径之上;分析车架的结构,我们可以看为是一个菱形,也可以看着是两个三角形,这里以三角为例,菱形可以看 carBracket2方法;首先算出三角形的起点、再算出三角形的角度、高度,请看下面示图;最后在后轮的中心点盖上一个圆点 用于遮挡三角的部分菱形 就要简单些的,但看起来逼格没有这么高端,就是用lineTo点对点的划线,以上就是车架的绘制过程,其实感觉菱形是是要简单、代码量也少些的,有兴趣的同学可以自己尝试一下,大家可以看下面的主要代码,新手上路,如果有更好的方式,欢迎老司机指点:结论 :使用moveTo把画布坐标从O移动到A点 x/y,lineTo从A开始画到B结束,再从B到C点,闭合,即一个三角完成//方法二:三角形 …………此处省略N行代码 [ { moveX:triangleX1, moveY:triangleY1, lineX1:coordinateX, lineY1:triangleH1, lineX2:discX, lineY2:discY, }, { moveX:triangleX2+15, moveY:triangleY2, lineX1:triangleX1, lineY1:triangleY1, lineX2:discX, lineY2:triangleH2, }, ].map((k,v)=>{ this.ctx.beginPath(); this.ctx.moveTo(k.moveX,k.moveY); //把坐标移动到A点,从A开始 this.ctx.strokeStyle = this.gearColor; this.ctx.lineWidth=coordinateW; this.ctx.lineTo(k.lineX1,k.lineY1);//从A开始,画到B点结束 this.ctx.lineTo(k.lineX2,k.lineY2); //再从B到C点,闭合 this.ctx.closePath(); this.ctx.stroke(); this.ctx.restore(); }); ……//方法一:菱形 …………此处省略N行代码 this.ctx.beginPath(); this.ctx.strokeStyle = this.gearColor; this.ctx.lineWidth=coordinateW; this.ctx.moveTo(polygon1X,polygon1Y); this.ctx.lineTo(coordinateX,height); this.ctx.lineTo(discX,discY); this.ctx.lineTo(polygon2X,polygon1Y+5); this.ctx.lineTo(polygon2X-5,polygon1Y); this.ctx.lineTo(polygon1X,polygon1Y); this.ctx.closePath(); this.ctx.stroke(); ……绘制车的豪华宝坐、扶手 :坐位一开始是比较懵逼的,不知道如何下手,圆也不圆、方也不方,后面又去复习一下canvas的API,发现了quadraticCurveTo能满足这个需求,—— 二次贝塞尔曲线画完之后,思考了很久,也没有发现什么技巧,或者规律,可能数学学的不好,没办法只能这样慢慢描了扶手也是一样的,开始尝试quadraticCurveTo,半天也没画成功,后面尝试去找了它邻居bezierCurveTo,—— 三次贝塞尔曲线提示:三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点知识点:quadraticCurveTo、bezierCurveTo、createLinearGradient //坐位 this.ctx.restore(); let seatX = (discX-85),seatY=discY-140; let curve1Cpx = [seatX-5,seatY+30,seatX+75,seatY+8]; let curve2Cpx =[seatX+85,seatY-5,seatX,seatY]; this.ctx.beginPath(); // this.ctx.fillStyle = this.gearColor; let grd=this.ctx.createLinearGradient(seatX,seatY,seatX+10,seatY+60); //渐变的角度 grd.addColorStop(0,"#712450"); grd.addColorStop(1,"#11090d"); this.ctx.fillStyle = grd; this.ctx.moveTo(seatX,seatY); this.ctx.quadraticCurveTo(…curve1Cpx); this.ctx.quadraticCurveTo(…curve2Cpx); this.ctx.fill(); //车前轴上的手柄 let steeringX = lever1X-20,steeringY = lever1Y-45; let steeringStep1 = [steeringX+40,steeringY-10,steeringX+40,steeringY-10,steeringX+35,steeringY+15] let steeringStep2 = [steeringX+30,steeringY+25,steeringX+25,steeringY+23,steeringX+18,steeringY+23] this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle = “#712450”; this.ctx.lineWidth=coordinateW; this.ctx.moveTo(steeringX,steeringY); //40 60; this.ctx.bezierCurveTo(…steeringStep1); this.ctx.bezierCurveTo(…steeringStep2); this.ctx.stroke(); this.ctx.closePath();绘制车的发动机、脚踏板 :到了这里,也快接近本文的尾声了,接下来要讲的是是车辆中最重要的部分,车中间齿轮盘,一辆车没有它,你做的再好也是白搭了;前面多次讲到齿轮的中心点,包括两个三角都是以它的中心计算的三角角度,知道了位置那就容易了,一样的先画几个圆,每个按一定的比例缩小;然后外围再画一圈锯齿,这样齿轮大概就画好了,齿轮的技巧在于以圆盘为中心点,画一圈线,它跟时钟的刻度原理是一样的;脚踏板,这个好理解,就是用lineTo画两跟线,其中一根进行一个90度的旋转就ok了,但重点是它在动画过程中的一个过程呢,我的分析过程是这样:竖着的这根轴是,以圆盘齿轮的中点为基点 N* (Math.PI / 180)转动;横着的这根轴,也就是脚踏板,它是以竖着的轴底部为Y轴中心点,以自身宽度的二分之一为X轴为中心点,同样以 N* (Math.PI / 180)的 rotate角度旋转。说了这么多,我们来看几张动态图吧,顺便贴上代码: discGear(coordinateX,coordinateY,coordinateW){ //车中间齿轮盘 disc let discX = coordinateX,discY = coordinateY; let discRadius = this.wheelRadius*.36;//车轮的3.6; let discDotX = discX+discRadius+8,discDotY = discRadius/.98; this.ctx.restore(); this.ctx.save(); this.ctx.translate(discX,discY); // this.ctx.rotate(-(Math.PI/2)); Array.from({length:30}).map((v,index)=>{ let radian = (Math.PI / 15) ; this.ctx.beginPath(); this.ctx.lineCap=“round”; this.ctx.strokeStyle = this.color; this.ctx.rotate(radian); this.ctx.lineWidth=3; this.ctx.moveTo(0,discDotY); this.ctx.lineTo(1.5,discDotY); // ctx.arc(discDotX,discDotY,6,0,Math.PI2,false); this.ctx.closePath(); this.ctx.stroke(); }); this.pedal(discX,discY,discRadius); this.pedal(discX,discY,discRadius,1); this.ctx.restore(); } pedal(coordinateX,coordinateY,discRadius,turnAngle=0){ //脚踏板,分两次初始化,一次在中心齿轮绘制之前,一次在之后, let pedalX = coordinateX, pedalY = coordinateY - discRadius.7; let pedalW = 6, pedalH = discRadius1.9; let radian = (this.animateNum)(Math.PI / 180) ; let radianHor = (this.animateNum)(Math.PI / 180) ; let turnAngleNum = 1; let moveY = 28; if(turnAngle !== 0){ this.ctx.rotate(-180(Math.PI/180)); turnAngleNum = (Math.PI/180); }; this.ctx.beginPath(); this.ctx.rotate(radianturnAngleNum); this.ctx.lineCap=“round”; this.ctx.strokeStyle = this.gearColor; this.ctx.lineWidth=pedalW; this.ctx.moveTo(-1,moveY); this.ctx.lineTo(0,pedalH); this.ctx.closePath(); this.ctx.stroke(); this.ctx.save(); let pedalHorW = pedalH/1.5,pedalHorH=pedalW; this.ctx.translate(0,pedalH); this.ctx.beginPath(); this.ctx.rotate(-radianHor); this.ctx.lineCap=“round”; this.ctx.fillStyle = “#fff”; this.ctx.strokeStyle = this.gearColor; this.ctx.lineWidth =2; this.ctx.roundRect(-pedalHorW/2,-2,pedalHorW,pedalHorH,5); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); this.ctx.restore(); }绘制车的链条 :链条用的是 bezierCurveTo ,cp1x,cp1y,cp2x,cp2y,x,y等参数画出来的,具体看下面代码吧,其实就是两个半椭圆的拼接…… //链条 let chainW = ( coordinateX+discRadius - this.wheelPos[0].x) / 2; let chainX = this.wheelPos[0].x +chainW-5 ; let chainY = coordinateY; this.ctx.save(); this.ctx.translate(chainX,chainY+4.8); this.ctx.rotate(-2(Math.PI/180)); let r = chainW+chainW*.06,h = discRadius/2; this.ctx.beginPath(); this.ctx.moveTo(-r, -1); this.ctx.lineWidth=3; this.ctx.strokeStyle = “#1e0c1a”; this.ctx.bezierCurveTo(-r,h1.5,r,h4,r,0); this.ctx.bezierCurveTo(r,-h4,-r,-h1.5,-r,0); this.ctx.closePath(); this.ctx.stroke(); this.ctx.restore();尾声 以上就是今天@IT·平头哥联盟-首席填坑官∙苏南给你带来的分享,整个车的绘制过程,感觉车架部分应该还有更好的做法,如果您有更好的建议及想法,欢迎斧正,最后送上完整的示例图! 文章源码获取-> blog-resource ???? 想直接在线预览 ????作者:苏南 - 首席填坑官来源:@IT·平头哥联盟链接:https://honeybadger8.github.i…交流群:912594095本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

October 22, 2018 · 3 min · jiezi

实践是检验程序员的唯一标准02:用户不想跟你说话并向你扔出一张图片 - 图片上传组件开发【开发篇】

温馨提示:这里除了一些幼稚的小组件啥也没有写在前面距离写完上一篇实践是检验程序员的唯一标准01:用户不想跟你说话并向你扔出一张图片 - 图片上传组件开发【思路篇】过去了大半年,才开始写开发篇真的是令人悲哀,不过有句话说的好,开始做一件事最好的时间是大半年前,其次是现在上一篇偏设计和尝试技术能否实现,这一篇会在工程层面实现,并且保证他能被(轻易)引用!上一篇文章的评论里好多同学(差不多3个人)希望我传到git上。好吧,本文最终的劳动成果会放上去的,不过那是下一篇文章干的事了,不过这里我已经把全部源码贴上来了- -功能完善在之前那篇文章中,又习惯性的做了很多无用的设计,你就是一个上传图片的组件,低调点谢谢,所以最终我搞成了这样子state-1:初始状态state-2:完成载入状态state-3:图片截取总体来说,把能剩的按钮都省了,本体就是个框,适合放在任何地方,此外为了防止破坏页面的整体性,组件不再自带截图预览功能,而是通过事件的方式将所截取的图像的DataURL实时穿给父组件,方便父组件自由使用(图中的展示区就是在父组件中写的)组件设计在一开始设计组件的时候简直就是父母给孩子报课外班的心情,希望能尽可能的满足各种需求,但转头想想先把最基本的功能(做个好人)做好别的都是可以慢慢加上的(懒)要保证基本功能能(好)用,大概以下这几点:1.要让其大小可控,方便应用于不同场景,所以组件的宽高有必要成为参数2.对于被裁出的部分,在原图中看和拎出来单独看视觉上差别还挺大的,所以一个可以实时单独展现所截取内容的功能就挺重要的3.在大多数情况下,裁剪区域的选定可能是有固定比例的,所以要将是否限制比例以及按照什么比例作为参数,根据适用场景决定所以组件的参数和事件大概也就这么几个了参数名:inputWidth说明:组件宽度类型:Number默认值:200px参数名:inputHeight说明:组件高度类型:Number默认值:200px参数名:cuttingRatio说明:裁剪比例,限定比例为宽/高,为空时没有比例限制类型:Number默认值:0事件名:getImageData说明:框选完成后鼠标抬起时触发,返回选定区域的图像数据参数:blobData参数格式:Blob对象事件名:getImageDataURL说明:鼠标拖动的每一帧触发,返回选定区域的图像数据,可用于预览区域展示参数:dataURL参数格式:dataURL代码实现HTML框架搭建由于功能很单一,HTML的布局也就很简单大概结构如下<根标签> <提示信息 />//绝对定位,位于组件下方,初始状态不可见,载入图片后出现 <重新选择按钮 />//绝对定位,位于组件右上角,初始状态不可见,载入图片后出现 <初始及载入层 />//绝对定位,位于画布上方,大小与画布完全相同 <画布 />//canvas <隐藏的input标签 />//不可见</根标签>HTML代码如下<template> <div class=“inputArea” :style="{height:inputHeight+‘px’,width:inputWidth+‘px’}"> <!–提示区域–> <div class=“notice” :class="{showNotice:noticeFlag}"> {{notice}} <div class=“close-notice” @click=“closeNotice”>X</div> </div> <!–重新选择按钮–> <div class=“reloadBtn” @click=“openWindow”> 重新选择 </div> <!–初始及载入层–> <div class=“blankMask” @click=“openWindow” v-if=“loadFlag!=2”> <img v-if=“loadFlag==0” src="../assets/img.png" /> <img v-if=“loadFlag==1” src="../assets/loading.png" /> <div class=“text”>{{loadFlag == 0?‘点击浏览图片’:‘加载中’}}</div> </div> <!–画布–> <div class=“canvasArea”> <canvas id=“inputAreaCanvas” @mousedown=“setStartPoint” @mousemove=“drawArea” @mouseup=“reset”> </canvas> </div> <!–隐藏的input标签–> <input id=“input” type=“file” @change=“loadImg” /> </div></template>对应的css如下<style> .inputArea { position: relative; background: #000; } .inputArea .notice { height: 30px; line-height: 30px; text-align: center; background: #FFF; color: #2C3E50; font-size: 12px; text-align: center; position: absolute; width: 90%; margin-left: 5%; left: 0px; transition: all .5s; bottom: -30px; opacity: 0; box-shadow: 0px 0px 5px rgba(0,0,0,0.3); border-radius: 2px; -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; } .inputArea .notice.showNotice { bottom: 0px; opacity: 1; } .inputArea .notice .close-notice { position: absolute; right: 10px; top: 0px; height: 30px; line-height: 30px; cursor: pointer; } .inputArea .reloadBtn { height: 20px; padding: 2px 5px 2px 5px; text-align: center; line-height: 20px; font-size: 12px; background: #FFFFFF; box-shadow: 0px 0px 5px rgba(0,0,0,0.3); color: #2C3E50; position: absolute; top: 5px; right: 5px; cursor: pointer; transition: all 0.5s; } .inputArea .reloadBtn:hover { box-shadow: 0px 0px 8px rgba(0,0,0,0.5); } .inputArea .blankMask { position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; display: flex; color: gainsboro; border-radius: 2px; background: #FFF; cursor: pointer; flex-direction: column; -ms-flex-direction: column; justify-content: center; -webkit-justify-content: center; align-items: center; -webkit-align-items: center; transition: all 0.5s; z-index: 2; } .inputArea .blankMask:hover { background: #F6F6F6; } .inputArea .blankMask .text { margin-top: 10px; font-size: 16px; font-weight: bold; } .inputArea .blankMask img { height: 40px; width: 40px; } .inputArea .canvasArea { display: flex; align-items: center; -webkit-align-items: center; justify-content: center; -webkit-justify-content: center; height: 100%; width: 100%; } #input { display: none; }</style>参数及变量定义以及对象初始化props:{ inputWidth:{ type:Number, default:200 }, inputHeight:{ type:Number, default:200 }, cuttingRatio:{ type:Number, default:0 }},data() { return { mouseDownFlag: false,//记录鼠标点击状态用标记 loadFlag: 0,//记录图像家在状态用标记 resultImgData: {},//被截取数据 input: {},//输入框对象 imgObj: new Image(),//图片对象 inputAreaCanvas: {},//主体canvas对象 inputArea2D: {},//主体CanvasRenderingContext2D对象 notice: “拖拽鼠标框选所需要的区域”,//提示区域文本 noticeFlag: false,//提示区域展示状态标记 dataURL:"",//被截取dataURL tempCanvas:{},//存放截取结果用canvas对象 tempCanvas2D:{},//存放截取结果用CanvasRenderingContext2D对象 resetX:0,//组件起点横坐标 resetY:0,//组件起点纵坐标 startX:0,//截取开始点横坐标 startY:0,//截取开始点纵坐标 resultX:0,//截取结束点横坐标 resultY:0,//截取结束点纵坐标 }},mounted: function() { //对象初始化 this.input = document.getElementById(‘input’) this.inputAreaCanvas = document.getElementById(“inputAreaCanvas”); this.inputArea2D = this.inputAreaCanvas.getContext(“2d”); this.tempCanvas = document.createElement(‘canvas’); this.tempCanvas2D = this.tempCanvas.getContext(‘2d’);},图片的读取此部分开始放在methods对象下图片读取的功能主要设计两个方法:openWindow方法主要用于触发隐藏的<input>标签的文件读取功能//打开文件选择窗口openWindow() { this.input.click();},loadImg方法完成了以下几个步骤新建一个FileReader对象用来读取选中的图片文件将原有的被选中的dataURL变量清空将读取的图片文件转为dataURL格式将dataURL赋给一个创建的image对象计算image对象的长宽比决定图片渲染方式获取canvas起点坐标将image对象中的图像数据赋给canvas//载入图片方法,当图片被选中后,input的value发生改变时触发loadImg() { let vm = this; let reader = new FileReader(); //每次载入后传给父组件的dataURL清空 this.dataURL = ‘’; //文件为空时返回 if(this.input.files[0] == null) { return } //开始载入图片,并将数据通过dataURL的方式读取,展现载入层信息 this.loadFlag = 1; reader.readAsDataURL(this.input.files[0]); //读取完成后将图像的dataURL数据赋给image对象的src的属性,使其加载图像 reader.onload = function(e) { vm.imgObj.src = e.target.result; } //图像加载完成,利用drawImage将image对象渲染至canvas this.imgObj.onload = function() { vm.loadFlag = 2; vm.noticeFlag = true; //计算载入图像的长宽比,决定图片显示方式 let ratioHW = (vm.imgObj.height/vm.imgObj.width) //每张图片根据比例不同,总有一个方向占满显示区域 if(ratioHW > 1) { vm.inputAreaCanvas.height = vm.inputHeight; vm.inputAreaCanvas.width = vm.inputHeight / ratioHW; } else { vm.inputAreaCanvas.width = vm.inputWidth; vm.inputAreaCanvas.height = vm.inputWidth * ratioHW; } //获取组件起点坐标 vm.resetX = vm.inputAreaCanvas.getBoundingClientRect().left; vm.resetY = vm.inputAreaCanvas.getBoundingClientRect().top; //将获取的图像数据选在至canvas vm.inputArea2D.clearRect(0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height); vm.inputArea2D.drawImage(vm.imgObj, 0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height); vm.inputArea2D.fillStyle = ‘rgba(0,0,0,0.5)’; //设定为半透明的黑色 vm.inputArea2D.fillRect(0, 0, vm.inputWidth, vm.inputHeight); //矩形A }},图像的截取图像截取功能包含四个方法:setStartPoint方法用于获取截取范围的起点以及更改点击状态//获取截取范围起始坐标,当鼠标在canvas标签上点击时触发setStartPoint(e) { this.mouseDownFlag = true; //改变标记状态,置为点击状态 this.startX = e.offsetX //获得起始点横坐标 this.startY = e.offsetY //获得起始点纵坐标},drawArea方法通过以下步骤实现了选定区域的展现和截取功能:取得实时鼠标坐标作为截取区域的终点在被选择区域外绘制半透明蒙版获取将所选区域图像对应imageData数据利用新建的canvas对象将imageData转为dataURL//选择截取范围,当鼠标被拖动时触发drawArea(e) { //当鼠标被拖动时触发(处于按下状态且移动) if(this.mouseDownFlag) { //在canvas标签上范围的终点横坐标 this.resultX = parseInt(e.clientX - this.resetX); //在canvas标签上范围的终点纵坐标,根据比例参数决定 if(this.cuttingRatio != 0) { //根据一定比例截取 this.resultY = this.startY + parseInt((1 / this.cuttingRatio) * (this.resultX - this.startX)) } else { //自由截取 this.resultY = parseInt(e.clientY - this.resetY); } //所选区域外阴影部分 this.inputArea2D.clearRect(0, 0, this.inputWidth, this.inputHeight); //清空整个画面 this.inputArea2D.drawImage(this.imgObj, 0, 0, this.inputAreaCanvas.width, this.inputAreaCanvas.height); //重新绘制图片 this.inputArea2D.fillStyle = ‘rgba(0,0,0,0.5)’; //设定为半透明的白色 this.inputArea2D.fillRect(0, 0, this.resultX, this.startY); //矩形A this.inputArea2D.fillRect(this.resultX, 0, this.inputWidth, this.resultY); //矩形B this.inputArea2D.fillRect(this.startX, this.resultY, this.inputWidth - this.startX, this.inputHeight - this.resultY); //矩形C this.inputArea2D.fillRect(0, this.startY, this.startX, this.inputHeight - this.startY); //矩形D //当选择区域大于0时,将所选范围内的图像数据实时返回 if(this.resultX - this.startX > 0 && this.resultY - this.startY > 0) { this.resultImgData = this.inputArea2D.getImageData(this.startX, this.startY, this.resultX - this.startX, this.resultY - this.startY); //canvas to DataURL this.tempCanvas.width = this.resultImgData.width; this.tempCanvas.height = this.resultImgData.height; this.tempCanvas2D.putImageData(this.resultImgData, 0, 0) this.dataURL = this.tempCanvas.toDataURL(‘image/jpeg’, 1.0); } }},reset方法用于重制鼠标点击状态,并获取blob格式的所截图像数据,触发getImageData事件将数据专递给父组件//结束选择截取范围,返回所选范围的数据,重制鼠标状态,当鼠标点击结束时触发reset() { this.mouseDownFlag = false; //将标志置为已抬起状态 let blob = this.dataURLtoBlob(this.dataURL) this.$emit(‘getImageData’, blob);},dataURLtoBlob方法的作用是将dataURL对象转化为Blob对象,来自Blob/DataURL/canvas/image的相互转换-Lorem由于在IE中并不支持Canvas.toBlob,所以需要这里走个弯路,自己写一下这个方法//DataURL to Blob,兼容IEdataURLtoBlob(dataurl) { let arr = dataurl.split(’,’) let mime = arr[0].match(/:(.*?);/)[1] let bstr = atob(arr[1]) let n = bstr.length let u8arr = new Uint8Array(n) while(n–) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime });}其他方法//关闭提示信息closeNotice() { this.noticeFlag = false},通过监听dataURL的变化,将结果实时返回给父组件以达到预览的目的watch:{ dataURL:function(newVal,oldVal){ this.$emit(‘getImageDataUrl’, this.dataURL)//将所截图的dataURL返回给父组件,共预览使用 }},应用方式用起来嘛,就很简单了html<template> <div id=“app”> <MainBlock @getImageData=“getImageData” @getImageDataUrl=“getImageDataUrl” :inputHeight=‘300’ :inputWidth=‘300’ ></MainBlock> <img :src=“src”/> </div></template>javascript<script> import MainBlock from ‘./components/mainBlock’ export default { name: ‘App’, components: { MainBlock, }, data() { return { imageData: ‘’, src: "" } }, methods: { getImageData(imageData) { this.imageData = imageData console.log(this.imageData) }, getImageDataUrl(dataUrl) { this.src = dataUrl } }, }</script>写在后面第一次写相对独立的组件从有想法到完全实现成一个能用的组件,中间还是有很多路的,而且功能还简单的令人发质,怎么说呢感觉自己可以进步的空间还很大啊不过令人欣慰的是这个组件已经用在单位的一个项目中了,可喜可贺虽然拖了很久,不过还是有成就感的,希望能继续下去,谁知道能走到哪呢欢迎大家挑错提意见,虽然不情愿,但是接受能看到这的,功能应该都实现了把?! ...

October 21, 2018 · 4 min · jiezi

canvas进阶——如何画出平滑的曲线?

背景概要相信大家平时在学习canvas 或 项目开发中使用canvas的时候应该都遇到过这样的需求:实现一个可以书写的画板小工具。嗯,相信这对canvas使用较熟的童鞋来说仅仅只是几十行代码就可以搞掂的事情,以下demo就是一个再也简单不过的例子了:<!DOCTYPE html><html><head> <title>Sketchpad demo</title> <style type=“text/css”> canvas { border: 1px blue solid; } </style></head><body> <canvas id=“canvas” width=“800” height=“500”></canvas> <script type=“text/javascript”> let isDown = false; let beginPoint = null; const canvas = document.querySelector(’#canvas’); const ctx = canvas.getContext(‘2d’); // 设置线条颜色 ctx.strokeStyle = ‘red’; ctx.lineWidth = 1; ctx.lineJoin = ‘round’; ctx.lineCap = ‘round’; canvas.addEventListener(‘mousedown’, down, false); canvas.addEventListener(‘mousemove’, move, false); canvas.addEventListener(‘mouseup’, up, false); canvas.addEventListener(‘mouseout’, up, false); function down(evt) { isDown = true; beginPoint = getPos(evt); } function move(evt) { if (!isDown) return; const endPoint = getPos(evt); drawLine(beginPoint, endPoint); beginPoint = endPoint; } function up(evt) { if (!isDown) return; const endPoint = getPos(evt); drawLine(beginPoint, endPoint); beginPoint = null; isDown = false; } function getPos(evt) { return { x: evt.clientX, y: evt.clientY } } function drawLine(beginPoint, endPoint) { ctx.beginPath(); ctx.moveTo(beginPoint.x, beginPoint.y); ctx.lineTo(endPoint.x, endPoint.y); ctx.stroke(); ctx.closePath(); } </script></body></html>它的实现逻辑也很简单:我们在canvas画布上主要监听了三个事件:mousedown、mouseup和mousemove,同时我们也创建了一个isDown变量;当用户按下鼠标(mousedown,即起笔)时将isDown置为true,而放下鼠标(mouseup)的时候将它置为false,这样做的好处就是可以判断用户当前是否处于绘画状态;通过mousemove事件不断采集鼠标经过的坐标点,当且仅当isDown为true(即处于书写状态)时将当前的点通过canvas的lineTo方法与前面的点进行连接、绘制;通过以上几个步骤我们就可以实现基本的画板功能了,然而事情并没那么简单,仔细的童鞋也许会发现一个很严重的问题——通过这种方式画出来的线条存在锯齿,不够平滑,而且你画得越快,折线感越强。表现如下图所示:为什么会这样呢?问题分析出现该现象的原因主要是:我们是以canvas的lineTo方法连接点的,连接相邻两点的是条直线,非曲线,因此通过这种方式绘制出来的是条折线;受限于浏览器对mousemove事件的采集频率,大家都知道在mousemove时,浏览器是每隔一小段时间去采集当前鼠标的坐标的,因此鼠标移动的越快,采集的两个临近点的距离就越远,故“折线感越明显“;如何才能画出平滑的曲线?要画出平滑的曲线,其实也是有方法的,lineTo靠不住那我们可以采用canvas的另一个绘图API——quadraticCurveTo ,它用于绘制二次贝塞尔曲线。二次贝塞尔曲线quadraticCurveTo(cp1x, cp1y, x, y) 调用quadraticCurveTo方法需要四个参数,cp1x、cp1y描述的是控制点,而x、y则是曲线的终点:更多详细的信息可移步MDN既然要使用贝塞尔曲线,很显然我们的数据是不够用的,要完整描述一个二次贝塞尔曲线,我们需要:起始点、控制点和终点,这些数据怎么来呢?有一个很巧妙的算法可以帮助我们获取这些信息获取二次贝塞尔关键点的算法这个算法并不难理解,这里我直接举例子吧:假设我们在一次绘画中共采集到6个鼠标坐标,分别是A, B, C, D, E, F;取前面的A, B, C三点,计算出B和C的中点B1,以A为起点,B为控制点,B1为终点,利用quadraticCurveTo绘制一条二次贝塞尔曲线线段;接下来,计算得出C与D点的中点C1,以B1为起点、C为控制点、C1为终点继续绘制曲线;依次类推不断绘制下去,当到最后一个点F时,则以D和E的中点D1为起点,以E为控制点,F为终点结束贝塞尔曲线。OK,算法就是这样,那我们基于该算法再对现有代码进行一次升级改造:let isDown = false;let points = [];let beginPoint = null;const canvas = document.querySelector(’#canvas’);const ctx = canvas.getContext(‘2d’);// 设置线条颜色ctx.strokeStyle = ‘red’;ctx.lineWidth = 1;ctx.lineJoin = ‘round’;ctx.lineCap = ‘round’;canvas.addEventListener(‘mousedown’, down, false);canvas.addEventListener(‘mousemove’, move, false);canvas.addEventListener(‘mouseup’, up, false);canvas.addEventListener(‘mouseout’, up, false);function down(evt) { isDown = true; const { x, y } = getPos(evt); points.push({x, y}); beginPoint = {x, y};}function move(evt) { if (!isDown) return; const { x, y } = getPos(evt); points.push({x, y}); if (points.length > 3) { const lastTwoPoints = points.slice(-2); const controlPoint = lastTwoPoints[0]; const endPoint = { x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2, y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2, } drawLine(beginPoint, controlPoint, endPoint); beginPoint = endPoint; }}function up(evt) { if (!isDown) return; const { x, y } = getPos(evt); points.push({x, y}); if (points.length > 3) { const lastTwoPoints = points.slice(-2); const controlPoint = lastTwoPoints[0]; const endPoint = lastTwoPoints[1]; drawLine(beginPoint, controlPoint, endPoint); } beginPoint = null; isDown = false; points = [];}function getPos(evt) { return { x: evt.clientX, y: evt.clientY }}function drawLine(beginPoint, controlPoint, endPoint) { ctx.beginPath(); ctx.moveTo(beginPoint.x, beginPoint.y); ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); ctx.stroke(); ctx.closePath();}在原有的基础上,我们创建了一个变量points用于保存之前mousemove事件中鼠标经过的点,根据该算法可知要绘制二次贝塞尔曲线起码需要3个点以上,因此我们只有在points中的点数大于3时才开始绘制。接下来的处理就跟该算法一毛一样了,这里不再赘述。代码更新后我们的曲线也变得平滑了许多,如下图所示:本文到这里就结束了,希望大家在canvas画板中“画”得愉快我们下次再见:)感兴趣的童鞋可戳这里关注我的博客,任何新鲜好玩的博文将会第一时间分享到这儿哦 ...

October 14, 2018 · 2 min · jiezi

三大图表库:ECharts 、 BizCharts 和 G2,该如何选择?

最近阿里正式开源的BizCharts图表库基于React技术栈,各个图表项皆采用了组件的形式,贴近React的使用特点。同时BizCharts基于G2进行封装,Bizcharts也继承了G2相关特性。公司目前统一使用的是ECharts图表库,下文将对3种图表库进行分析比对。BizCharts文档地址:BizCharts一、安装通过 npm/yarn 引入npm install bizcharts –saveyarn add bizcharts –save二、引用成功安装完成之后,即可使用 import 或 require 进行引用。例子:import { Chart, Geom, Axis, Tooltip, Legend } from ‘bizcharts’;import chartConfig from ‘./assets/js/chartConfig’;<div className=“App”> <Chart width={600} height={400} data={chartConfig.chartData} scale={chartConfig.cols}> <Axis name=“genre” title={chartConfig.title}/> <Axis name=“sold” title={chartConfig.title}/> <Legend position=“top” dy={-20} /> <Tooltip /> <Geom type=“interval” position=“genresold” color=“genre” /> </Chart></div>该示例中,图表的数据配置单独存入了其他js文件中,避免页面太过冗杂module.exports = { chartData : [ { genre: ‘Sports’, sold: 275, income: 2300 }, { genre: ‘Strategy’, sold: 115, income: 667 }, { genre: ‘Action’, sold: 120, income: 982 }, { genre: ‘Shooter’, sold: 350, income: 5271 }, { genre: ‘Other’, sold: 150, income: 3710 } ], // 定义度量 cols : { sold: { alias: ‘销售量’ }, // 数据字段别名映射 genre: { alias: ‘游戏种类’ } }, title : { autoRotate: true, // 是否需要自动旋转,默认为 true textStyle: { fontSize: ‘12’, textAlign: ‘center’, fill: ‘#999’, fontWeight: ‘bold’, rotate: 30 }, // 坐标轴文本属性配置 position:‘center’, // 标题的位置,新增 }}效果预览:三、DataSetBizCharts中可以通过dataset(数据处理模块)来对图标数据进行处理,该方法继承自G2,在下文中将对此进行详细分析。快速跳转G2BizCharts基于G2进行开发,在研究BizCharts的过程中也一起对G2进行了实践。一、安装和BizCharts一样,可以通过 npm/yarn 引入npm install @antv/g2 –saveyarn add @antv/g2 –save与BizCharts不同,G2初始化数据并非以组件的形式引入,而是需要获取需要在某个DOM下初始化图表。获取该DOM的唯一属性id之后,通过chart()进行初始化。二、引用示例:import React from ‘react’;import G2 from ‘@antv/g2’; class g2 extends React.Component {constructor(props) { super(props); this.state = { data :[ { genre: ‘Sports’, sold: 275 }, { genre: ‘Strategy’, sold: 115 }, { genre: ‘Action’, sold: 120 }, { genre: ‘Shooter’, sold: 350 }, { genre: ‘Other’, sold: 150 } ] }; } componentDidMount() { const chart = new G2.Chart({ container: ‘c1’, // 指定图表容器 ID width: 600, // 指定图表宽度 height: 300 // 指定图表高度 }); chart.source(this.state.data); chart.interval().position(‘genresold’).color(‘genre’); chart.render(); } render() { return ( <div id=“c1” className=“charts”> </div> ); }}export default g2;效果图:三、DataSetDataSet 主要有两方面的功能,解析数据(Connector)&加工数据(Transform)。官方文档描述得比较详细,可以参考官网的分类:源数据的解析,将csv, dsv,geojson 转成标准的JSON,查看Connector加工数据,包括 filter,map,fold(补数据) 等操作,查看Transform统计函数,汇总统计、百分比、封箱 等统计函数,查看 Transform特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云 的数据处理,查看 Transform// step1 创建 dataset 指定状态量const ds = new DataSet({ state: { year: ‘2010’ }});// step2 创建 DataViewconst dv = ds.createView().source(data);dv.transform({ type: ‘filter’, callback(row) { return row.year === ds.state.year; }});// step3 引用 DataViewchart.source(dv);// step4 更新状态量ds.setState(‘year’, ‘2012’);以下采用官网文档给出的示例进行分析 示例一该表格里面的数据是美国各个州不同年龄段的人口数量,表格数据存放在类型为CVS的文件中数据链接(该链接中为json类型的数据)State小于5岁5至13岁14至17岁18至24岁25至44岁45至64岁65岁及以上WY3825360890293145398013733814727965614DC3635250439252257556919355714004370648VT3263562538337576167915541918859386649……………………初始化数据处理模块import DataSet from ‘@antv/data-set’;const ds = new DataSet({//state表示创建dataSet的状态量,可以不进行设置 state: { currentState: ‘WY’ }});const dvForAll = ds// 在 DataSet 实例下创建名为 populationByAge 的数据视图 .createView(‘populationByAge’) // source初始化图表数据,data可为http请求返回的数据结果 .source(data, { type: ‘csv’, // 使用 CSV 类型的 Connector 装载 data,如果是json类型的数据,可以不进行设置,默认为json类型});/**trnasform对数据进行加工处理,可通过type设置加工类型,具体参考上文api文档加工过后数据格式为[{state:‘WY’,key:‘小于5岁’,value:38253},{state:‘WY’,key:‘5至13岁’,value:60890},]/ dvForAll.transform({ type: ‘fold’, fields: [ ‘小于5岁’,‘5至13岁’,‘14至17岁’,‘18至24岁’,‘25至44岁’,‘45至64岁’,‘65岁及以上’ ], key: ‘age’, value: ‘population’});//其余transform操作const dvForOneState = ds .createView(‘populationOfOneState’) .source(dvForAll); // 从全量数据继承,写法也可以是.source(‘populationByAge’) dvForOneState .transform({ // 过滤数据,筛选出state符合的地区数据 type: ‘filter’, callback(row) { return row.state === ds.state.currentState; }}) .transform({ type: ‘percent’, field: ‘population’, dimension: ‘age’, as: ‘percent’ });使用G2绘图G2-chart Api文档import G2 from ‘@antv/g2’;// 初始化图表,id指定了图表要插入的dom,其他属性设置了图表所占的宽高const c1 = new G2.Chart({ id: ‘c1’, forceFit: true, height: 400,});// chart初始化加工过的数据dvForAllc1.source(dvForAll);// 配置图表图例c1.legend({ position: ’top’,});// 设置坐标轴配置,该方法返回 chart 对象,以下代码表示将坐标轴属性为人口的数据,转换为M为单位的数据c1.axis(‘population’, { label: { formatter: val => { return val / 1000000 + ‘M’; } }});c1.intervalStack() .position(‘statepopulation’) .color(‘age’) .select(true, { mode: ‘single’, style: { stroke: ‘red’, strokeWidth: 5 } }); //当tooltip发生变化的时候,触发事件,修改ds的state状态量,一旦状态量改变,就会触发图表的更新,所以c2饼图会触发改变c1.on(’tooltip:change’, function(evt) { const items = evt.items || []; if (items[0]) { //修改的currentState为鼠标所触及的tooltip的地区 ds.setState(‘currentState’, items[0].title); }});// 绘制饼图const c2 = new G2.Chart({ id: ‘c2’, forceFit: true, height: 300, padding: 0,});c2.source(dvForOneState);c2.coord(’theta’, { radius: 0.8 // 设置饼图的大小});c2.legend(false);c2.intervalStack() .position(‘percent’) .color(‘age’) .label(‘age*percent’,function(age, percent) { percent = (percent * 100).toFixed(2) + ‘%’; return age + ’ ’ + percent; });c1.render();c2.render();EChartsECharts是一个成熟的图表库, 使用方便、图表种类多、容易上手。文档资源也比较丰富,在此不做赘述。ECharts文档ECharts & BizCharts & G2 对比对比BizCharts和G2两种图表库,BizCharts主要是进行了一层封装,使得图表可以以组件的形式进行调用,按需加载,使用起来更加方便。简单对比一下三个图表库的区别:初始化图表:ECharts:// 基于准备好的dom,初始化ECharts实例var myChart = echarts.init(document.getElementById(‘main’));BizCharts:// 以组件的形式,组合调用import { Chart, Geom, Axis, … } from ‘bizcharts’;<Chart width={600} height={400} data={data}> …</Chart>G2:// 基于准备好的dom,配置之后进行初始化const chart = new G2.Chart({ container: ‘c1’, // 指定图表容器 ID width: 600, // 指定图表宽度 height: 300 // 指定图表高度});chart.source(data);chart.render(); <div id=“c1” className=“charts”></div>配置:ECharts:// 集中在options中进行配置myChart.setOption({ title: { … }, tooltip: {}, xAxis: { data: […] }, yAxis: {}, series: [{ … }]});BizCharts:// 根据组件需要,配置参数之后进行赋值const cols = {…};const data = {…};<Chart width={600} height={400} data={data} scaenter code herele={cols}> …</Chart>G2:chart.tooltip({ triggerOn: ‘…’ showTitle: {boolean}, // 是否展示 title,默认为 true crosshairs: { … style: { … } }});事件:ECharts:事件 api文档myChart.on(‘click’, function (params) { console.log(params);});BizCharts:事件 api文档<chart onEvent={e => { //do something }}/>G2: 事件 api文档chart.on(‘mousedown’, ev => {});总结对比以上3种图表,ECharts和BizCharts相对容易使用,尤其ECharts的配置非常清晰,BizCharts与其也有一定相似之处。BizCharts优势在于组件化的形式使得dom结构相对清晰,按需引用。G2比较适合需要大量图表交互时引用,其丰富的api处理交互逻辑相对更有优势。广而告之本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。欢迎讨论,点个赞再走吧 。◕‿◕。 ~ ...

September 20, 2018 · 3 min · jiezi

用canvas画心电图

效果图:思路:1.模拟点(如果你有真实的数据,那就是把数据幻化成canvas对应的坐标点) 模拟点时注意的点就是高起部分需要对称以及为了好看要随机出现上上下下2.画线 画线需要注意有一个匀速移动的过程。 比如 A点到B点,不是简单的A画到B,而是A点到A1,A2….最后到B(这一块按照比例移动比较难)3.画线的一些效果,比如加上阴影(这里就可以自由发挥了)具体代码<!DOCTYPE html> <html lang=“en”> <head> <meta charset=“UTF-8”> <title>心电图</title> <meta name=“viewport” content=“width=device-width, initial-scale=1, user-scalable=no”> <style> html,body{ width: 100%; height: 100%; margin: 0; } canvas{ background: #000; width: 100%; height: 100%; } </style> </head> <body> <div id=“canvas”> <canvas id=“can”></canvas> </div> <script> var can = document.getElementById(‘can’), pan, index = 0, flag = true, wid = document.body.clientWidth, hei = document.body.clientHeight, x = 0, y = hei/2, drawX = 0, drawY = hei/2, drawXY = [], cDrawX = 0, i = 0, reX = 0, reY = 0; start(); function start(){ can.height = hei; can.width = wid; pan = can.getContext(“2d”); pan.strokeStyle = “white”; pan.lineJoin = “round”; pan.lineWidth = 6; pan.shadowColor = “#228DFF”; pan.shadowOffsetX = 0; pan.shadowOffsetY = 0; pan.shadowBlur = 20; pan.beginPath(); pan.moveTo(x,y); drawXYS(); index = setInterval(move,1); }; function drawXYS(){ if(drawX > wid){ }else{ if(drawY == hei/2){ if(flag){ flag = false; }else{ var _y = Math.ceil(Math.random()*10); _y = _y/2; if(Number.isInteger(_y)){ drawY += Math.random()*180+30; }else{ drawY -= Math.random()*180+30; } flag = true; } cDrawX = Math.random()*40+15; }else{ drawY = hei/2; } drawX += cDrawX; drawXY.push({ x : drawX, y : drawY }); drawXYS(); } } function move(){ var x = drawXY[i].x, y = drawXY[i].y; if(reX >= x - 1){ reX = x; reY = y; i++; cc(); return; } if(y > hei/2){ if(reY >= y){ reX = x; reY = y; i++; cc(); return; } }else if(y < hei/2){ if(reY <= y){ reX = x; reY = y; i++; cc(); return; } }else{ reX = x; reY = y; i++; cc(); return; } reX += 1; if(y == hei/2){ reY = hei/2; }else{ var c = Math.abs((drawXY[i].x-drawXY[i-1].x)/(drawXY[i].y-drawXY[i-1].y)); var _yt = (reX-drawXY[i-1].x)/c; if(drawXY[i].y < drawXY[i-1].y){ reY = drawXY[i-1].y - _yt; }else{ reY = drawXY[i-1].y + _yt; } } cc(); } function cc(){ if(i == drawXY.length){ pan.closePath(); clearInterval(index); index = 0; x = 0; y = hei/2; flag = true; i = 0; }else{ pan.lineTo(reX, reY); pan.stroke(); } } </script></body></html>备注代码没有注释,如果有看不懂的地方,可以联系我sf上联系不到的话可以联系我的公众号:乐趣区 ...

September 7, 2018 · 2 min · jiezi

学习 canvas 的 globalCompositeOperation 做出的神奇效果

说明最早知道 canvas 的 globalCompositeOperation 属性,是在需要实现一个刮刮卡效果的时候,当时也就是网上找到刮刮卡的效果赶紧完成任务就完了,这次又学习一次,希望能加深理解吧。先来看下 canvas 的 globalCompositeOperation属性,具体是干什么的。定义globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有)的图像上。 源图像 = 您打算放置到画布上的绘图。 目标图像 = 您已经放置在画布上的绘图。这个属性用来设置要在绘制新形状时应用的合成操作的类型,比如在一个蓝色的矩形上画一个红色的圆形,是红色在上显示,还是蓝色在上显示,重叠的部分显示还是不显示,不重叠的部分又怎么显示,等一些情况,在面对这些情况的时候,就是 globalCompositeOperation 属性起作用的时候了。 在取默认值的情况下,都是显示的,新画的图形会覆盖原来的图形。用法默认值: source-over 语法: context.globalCompositeOperation=“source-in”;表格中的蓝色矩形为目标图像,红色圆形为源图像。属性值描述效果source-over默认。在目标图像上显示源图像。source-atop在目标图像顶部显示源图像。源图像位于目标图像之外的部分是不可见的。source-in在目标图像中显示源图像。只有目标图像内的源图像部分会显示,目标图像是透明的。source-out在目标图像之外显示源图像。只会显示目标图像之外源图像部分,目标图像是透明的。destination-over在源图像上方显示目标图像。destination-atop在源图像顶部显示目标图像。源图像之外的目标图像部分不会被显示。destination-in在源图像中显示目标图像。只有源图像内的目标图像部分会被显示,源图像是透明的。destination-out在源图像外显示目标图像。只有源图像外的目标图像部分会被显示,源图像是透明的。lighter显示源图像 + 目标图像。copy显示源图像。忽略目标图像。xor使用异或操作对源图像与目标图像进行组合。好的,下来实现一个水滴扩散的效果 效果图 实现思路 在一个 canvas 上先画出黑白色的图片,然后设置背景是一张彩色的图片,鼠标点击时,设置 canvas 的 globalCompositeOperation 属性值为 destination-out,根据鼠标在 canvas 中的 坐标,用一个不规则的图形逐渐增大,来擦除掉黑白色的图片,就可以慢慢显示彩色的背景了。也就是说我们需要三张图片 黑白的图片彩色的图片 不规则形状的图片代码<!doctype html><html><head> <meta charset=“UTF-8”> <style> canvas { /* 设置鼠标的光标是一张图片, 16和22 分别表示热点的X坐标和Y坐标 / / https://developer.mozilla.org/zh-CN/docs/Web/CSS/cursor/url */ cursor: url(‘https://www.kkkk1000.com/images/mouse.png') 16 22, auto; } </style></head><body> <canvas id=“canvas” width=“400px” height=“250px”></canvas> <script type=“text/javascript”> var canvas = document.getElementById(“canvas”); var context = canvas.getContext(“2d”); // 保存图片路径的数组 var urlArr = [“https://www.kkkk1000.com/images/bg2.png”, “https://www.kkkk1000.com/images/clear.png”]; // imgArr 保存加载后的图片的数组,imgArr中保存的是真实的图片 // loadImg 函数用来加载 urlArr 中所有的图片 // 并返回一个保存所有图片的数组 var imgArr = loadImg(urlArr); // flag 用来限制 点击事件,一张图片只会产生一次效果 var flag = false; function loadImg(urlArr) { var index = 0; var res = []; // 每次给 load 函数传入一个图片路径,来加载图片 load(urlArr[index]); function load(url) { // 如果 index 等于 urlArr.length, // 表示加载完 全部图片了,就结束 load函数 if (index == urlArr.length) { // 加载完全部图片,调用 init 函数 init(); return; } var img = new Image(); img.src = url; // 不管当前图片是否加载成功,都要加载下一张图片 img.onload = next; img.onerror = function () { console.log(res[index] + “加载失败”); next(); } // next 用来加载下一张图片 function next() { // 把加载后的图片,保存到 res 中 res[index] = img; load(urlArr[++index]) } } // 最后返回保存所有真实图片的数组 return res; } function init() { // 先在canvas上画黑白的图片,然后再设置背景是彩色的图片 // 避免先显示出彩色图片,再显示出黑白的图片 context.globalCompositeOperation = “source-over”; context.drawImage(imgArr[0], 0, 0, 400, 250); canvas.style.background = ‘url(https://www.kkkk1000.com/images/bg.jpg)'; canvas.style.backgroundSize = “100% 100%”; // flag 是 true 时,鼠标点击才有水滴扩散的效果 flag = true; // canvas 绑定点击事件,点击时产生水滴扩散效果 canvas.onclick = diffusion; } // width 表示 不规则形状的图片的尺寸 var width = 0; // speed 表示扩散效果的速度 var speed = 8; // diffusion 函数根据鼠标坐标,产生效果 function diffusion (e) { if (flag) { flag = false; context.globalCompositeOperation = “destination-out”; window.requestAnimationFrame(draw); // 根据鼠标坐标,画扩散效果 function draw() { // 这里不一定需要是 1800 ,但必须是一个足够大的数,可以扩散出整张背景图 if (width > 1800) { flag = true; return; } width += speed; // 获取鼠标相对于 canvas 的坐标 var x = e.layerX; var y = e.layerY; // 画不规则形状的图片,逐渐增大图片尺寸 context.drawImage(imgArr[1], x - (width / 2), y - (width / 2), width, width); window.requestAnimationFrame(draw); } } } </script></body></html>我们继续来实现一个刮刮卡的效果效果图刮刮卡效果实现的思路:一个 canvas 上先画一层灰色,然后设置canvas的背景图,设置 canvas 的 globalCompositeOperation属性值为 destination-out,点击并移动时,根据移动点的坐标,擦除掉灰色,当擦掉一部分时,再自动擦除掉全部灰色,显示出背景来。刮刮卡的效果和水滴扩散的效果,在开始的时候几乎是一样的,不过水滴扩散效果,用的是一张不规则形状的图片来清除黑白图片,而刮刮卡效果,是通过画线的方式,线比较粗而已,来清除上面的灰色。主要的不同是,刮刮卡效果最后需要自动擦除掉全部灰色,这里有两种方式。第一种 使用 canvas 的 getImageData 方法,来获取 canvas 上的像素信息,这个方法返回的对象的 data 属性是一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示,详细的可以看看 canvas 的像素操作。 用这个方法来判断有多少已经擦除掉了,也就是通过一个变量来记录有多少像素的RGBA的值是0,当变量的值超过某一个值时,就清除全部灰色。代码在这里。第二种 就直接看移动了多少,鼠标移动时,会有一个变量进行自增运算,当这个变量,超过一定值时,就擦除全部灰色。代码在这里。注意: 第一种方式使用 getImageData 存在跨域问题,不过因为这个效果中,没有在canvas上画图片,而是设置canvas的 background 为一张图片,所以这个还没有影响,但是如果canvas上画了其他图片,就可能需要处理跨域的问题了。 使用 getImageData 能获取到 canvas 上的像素信息,可以更加灵活的控制擦除全部灰色的时机。第二种方式,使用图片地址,不存在跨域的问题,但是不能很好的控制最后擦除全部灰色的时机。总结文章中的效果主要是使用 globalCompositeOperation属性取值为 destination-out ,而取值为其他值的时候,同样也是可以制作出各种效果的,大家也可以发挥自己的想象力,去试试其它值,也许有新发现呢。 ...

September 1, 2018 · 2 min · jiezi