对于大多数做动效的人来说,canvas实际应用一般都是2D平面视觉动效,而3D,一般会出动webgl(或者threejs or pixi等,pixi本人也没用过未曾学过),而webgl写起来有点忧伤…繁琐,还要自己写顶点着色器与片元着色器,本人稍微学过一些webgl,从入门到放弃(不过一定会重拾)。
有些时候,我们仅仅是想实现一些3D视觉,但又不想为了一个视觉而加入一个巨大的库(比如threejs),那么这篇文章对你来说可能会有所收获,学会这些,你将对threejs里一些绘制方式的实现有所了解(为什么plane几何体也需要片段参数?为什么全景里纹理贴图总能看出变形无法避免?)
本文将教你从零实现canvas 2d api 实现 3D 图片旋转视觉,相信我,我会讲解的非常详细(特别是对于能用在应用上的知识),毕竟,这是我曾经的分享,图跟demo都是改过一次又一次的

平面透视视觉:
近者大而远着小乎,对于一个图形而言,在空间内,大小不变的情况下,随着Z轴的正向运动(指向屏幕),那么我们会看到物体变得越来越小,反之则越来越大;而对于我们代码而言,需要关心的,是它现在应该绘制成多大。那么对此我们需要推导出一个“缩放比例”

以上图为例,同一个圆,它位于屏幕的大小,我们将它的单位定为1单位,那么在它延Z轴方向运动的时候(左图圆形虚线处),它的投影,在我们视觉当中应该为右图大小,那么此时它的缩放比例((我真的好想好想吐槽简书这所谓的markdown,该有的都没有啊))
scale = fl / (fl + z)
这条公式怎么得到的?

上图为相似三角形,假设BC为原来圆形的大小(参照上一幅图右侧),DE则为投影大小,那么根据相似三角形等比关系,DE:BC = FL : (FL + Z),而我们将原来圆形的大小定为1单位,即BC = 1,那么DE = FL / (FL + Z)
在得到缩放比例后,那么对应的,图形在3D世界中的大小及坐标轴对应参数也可以轻易的得到,将圆图形的大小,x,y坐标均乘以缩放比例,就能得到在Z轴上运动时,物体此时的大小及位置参数(pos_为原图形参数)

x = pos_x * scale;
y = pos_y * scale;
size = pos_size * scale;
此时应该有同学发现一个问题,那就是canvas的起始坐标是在(0,0)位置上,那往Z轴正方向运动不就挪到屏幕外边去了?因此,为了方便理解,我们将原点挪到canvas中心上,也就是需要加上canvas长宽各一半

那么描述图形位置代码则变为(centerX = canvas.cilentWidth / 2,centerY同理,其实如果图形处于中心点,那么此图形实际只会有一个缩放效果,而不会有位置变化关系,有疑惑的同学可以自己演算一下就知道我说的什么了,上图如果白色圆形想变化出这四个位置,其实并不能在原点上,这里只是给同学们做一个图从视觉上联想一下结果):
x = pos_x * scale + centerX;
y = pos_y * scale + centerY;
上面这么一丢丢知识能干嘛?那就来个例子让大家可以用在应用里吧,一个很常见的粒子透视视觉

<div class="canvas-wrap">
<canvas id="cas"></canvas>
</div>
<script>
function Stage(elm){
this.cas = document.getElementById(elm);
this.ctx = this.cas.getContext("2d");
this.counts = 500; //最大粒子数
this.particlesArr = [];
this.init();
}
Stage.prototype = {
resize: function(booleam){
this.width = this.cas.width = booleam ? this.cas.parentNode.clientWidth * 2 : window.innerWidth * 2;
this.height = this.cas.height = booleam ? this.cas.parentNode.clientHeight * 2 : window.innerHeight * 2;
},
clear: function(){
this.ctx.clearRect(0,0,this.width,this.height);
},
createParticles: function(){
var halfWidth = this.width / 2,
halfHeight = this.height / 2;
for(var i = 0; i < this.counts; i++){
var circle = new Circle({
ctx: this.ctx,
fl: 100,
posx: Math.random() * this.width - halfWidth,
posy: Math.random() * this.height - halfHeight,
posz: Math.random() * 250,
size: 10,
speed: Math.random() * 2,
origin: {
x: this.width/2,
y: this.height/2
}
});
this.particlesArr.push(circle);
}
},
render: function(){
this.clear();
this.particlesArr.forEach(function(elm){
elm.draw();
})
},
animate: function(){
var _this = this;
this.render();
window.requestAnimationFrame(function(){
_this.animate();
});
},
init: function(){
this.resize(true);
this.createParticles();
this.animate();
}
}
function Circle(){
this.ctx = arguments[0]['ctx'];
this.fl = arguments[0]['fl'];
this.posx = arguments[0]['posx'];
this.posy = arguments[0]['posy'];
this.posz = arguments[0]['posz'];
this.origenZ = arguments[0]['posz'];
this.size = arguments[0]['size'];
this.r = arguments[0]['size']/2
this.x = this.posx;
this.y = this.posy;
this.origin = arguments[0]['origin'];
this.speed = arguments[0]['speed'];
this.color = arguments[0]['color'] ? arguments[0]['color'] : "#fff";
this.died = false;
}
Circle.prototype = {
//3D坐标投影
projection: function(){
if (this.posz > -this.fl) {
var scale = this.fl / (this.fl + this.posz);
this.x = this.origin.x + this.posx * scale;
this.y = this.origin.y + this.posy * scale;
this.size = this.r * scale;
this.posz -= this.speed;
} else {
this.posz = this.origenZ;
}
},
draw: function(){
this.projection();
this.ctx.save();
this.ctx.fillStyle = this.color;
this.ctx.translate(this.x, this.y);
this.ctx.beginPath();
this.ctx.arc(-this.r,-this.r,this.size,0,Math.PI*2,false);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
}
new Stage("cas");
</script>
旋转后坐标计算:

- 有过canvas2D开发经验的童鞋应该有学过如果计算旋转后坐标,旋转α度,则公式为:
x = r * cosα, y = r * sinα;

-
而在α角度基础上再旋转β度,则公式变成
x' = r * cos(α+β) , y' = r * sin(α+β) -
那么根据三角函数两角和差公式,则2转变为(这里的减加符号打不出来,只能截图,ppt里我是截图旋转图片,因为实在找不到这个符号):
三角函数两角和差公式
-
将3代入2,可得:
x' = r * (cosαcosβ - sinαsinβ ), y' = r * (sinαcosβ + cosαsinβ) -
将1代入4,可得(结论,重点,高中知识忘记的现在也已经一步步重新推出来了,这是Z轴旋转计算公式):
x' = xcosβ - ysinβ, y' = ycosβ + xsinβ

上面就这么两个知识点能干哈?又得给出粒子demo来让大家学以致用了

<div class="canvas-wrap">
<canvas id="cas"></canvas>
</div>
<script>
function Stage(elm){
this.cas = document.getElementById(elm);
this.ctx = this.cas.getContext("2d");
this.origin = {};
this.vertex = [];
this.counts = 50;
this.init();
}
Stage.prototype = {
resize: function(booleam){
this.width = this.cas.width = booleam ? this.cas.parentNode.clientWidth * 2 : window.innerWidth * 2;
this.height = this.cas.height = booleam ? this.cas.parentNode.clientHeight * 2 : window.innerHeight * 2;
},
clear: function(){
this.ctx.clearRect(0,0,this.width,this.height);
},
createPosition: function(){
var circle_arr = [],
radius_x = this.width/2,
radius_y = this.height/2;
for(var i = 0; i < this.counts; i++){
circle_arr.push({
posx: Math.random()*radius_x-radius_x/2,
posy: Math.random()*radius_y-radius_y/2,
posz: Math.random()*radius_x-radius_x/2
});
}
this.creatVertex(circle_arr);
},
creatVertex: function(vertex){
//设置原点坐标
var origin = {
x: this.width/2,
y: this.height/2
};
var rotateSpeed = -Math.PI/2/20;
vertex.forEach(function(e, i){
var vex = new particle(e, 1000, origin, rotateSpeed);
this.vertex.push(vex);
}.bind(this));
},
sort: function(){
this.vertex.sort(function (a, b) { return b.posz-a.posz });
},
render: function(){
this.clear();
this.sort();
this.vertex.forEach(function(e, i){
e.draw(this.ctx);
}.bind(this));
},
animate: function(){
var _this = this;
this.render();
window.requestAnimationFrame(function(){
_this.animate();
});
},
init: function(){
this.resize(true);
this.createPosition();
this.animate();
}
}
function particle(vex, fl, origin, angle, size, color){
var r = Math.floor(Math.random()*255),
g = Math.floor(Math.random()*255),
b = Math.floor(Math.random()*255);
this.x = 0;
this.y = 0;
this.fl = fl; //视距
this.origin = origin;
this.angle = angle;
this.posx = vex.posx;
this.posy = vex.posy;
this.posz = vex.posz;
this.size = size ? size : 20;
this.r = size ? size : 20;
this.color = color ? color : 'rgba('+r+','+g+','+b+',0.6)';
}
particle.prototype = {
//Y轴旋转
ratateY: function(){
var cosy = Math.cos(this.angle),
siny = Math.sin(this.angle),
x1 = this.posx * cosy + this.posz * siny,
z1 = this.posz * cosy - this.posx * siny;
this.posx = x1;
this.posz = z1;
},
//3D坐标投影
projection: function(){
if (this.posz > -this.fl) {
var scale = this.fl / (this.fl + this.posz);
this.x = this.origin.x + this.posx * scale;
this.y = this.origin.y + this.posy * scale;
this.size = this.r * scale;
}
},
draw: function(ctx){
this.ratateY();
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI*2, false);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
this.projection();
}
}
new Stage("cas");
</script>

<div class="canvas-wrap">
<canvas id="cas"></canvas>
</div>
<script>
function Stage(elm){
this.cas = document.getElementById(elm);
this.ctx = this.cas.getContext("2d");
this.origin = {};
this.vertex = [];
this.counts = 0; //因为有3个demo,实现方式不一致,所以放在下面赋值
this.radius = 300;
this.init();
}
Stage.prototype = {
resize: function(booleam){
this.width = this.cas.width = booleam ? this.cas.parentNode.clientWidth * 2 : window.innerWidth * 2;
this.height = this.cas.height = booleam ? this.cas.parentNode.clientHeight * 2 : window.innerHeight * 2;
},
clear: function(){
this.ctx.clearRect(0,0,this.width,this.height);
},
getArea: function(){
var circle_arr = [],
radius_x = this.width/2,
radius_y = this.height/2;
/* 关于球面知识点科普,应该很多同学也记不得了,我也记不得,还是百度出来公式再套的:
因为我们把球心原点定在(0,0,0),所以x0, y0, z0都为0
使用极坐标来表示半径为r的球面:
φ - 水平(纬度) - 0≤φ≤π; //必须在0到Math.PI之间,Math.acos(k)反余弦等于斜边比临边
θ - 竖直(经度) - 0≤θ≤2π; //必须在0到Math.PI*2之间
x = x0 + r*sinθcosφ
y = y0 + r*sinθsinφ
z = z0 + r*cosθ
*/
/* --
球体绘制方法 - 1
这个是我不小心试出来的,类似于多条螺旋线形成一个圆,以点来描述球体的话这个是最高效的
-- */
this.counts = 1000;
for(var i = 0; i < this.counts; i++){
var φ = Math.PI * (i / this.counts);
var θ = i / Math.PI * 2 * this.counts;
var x = this.radius * Math.sin(φ) * Math.cos(θ);
var y = this.radius * Math.sin(φ) * Math.sin(θ);
var z = this.radius * Math.cos(φ);
circle_arr.push({posx: x, posy: y, posz: z});
}
/* --
球体绘制方法 - 2
常规操作,规规矩矩的循环
-- */
// this.counts = 100;
// for(var i = 0; i < this.counts; i++){
// var φ = Math.PI * (i / this.counts);
// for(var j = 0; j < 50; j++){
// var θ = Math.PI * 2 * (j / 50);
// var x = this.radius * Math.sin(φ) * Math.cos(θ);
// var y = this.radius * Math.sin(φ) * Math.sin(θ);
// var z = this.radius * Math.cos(φ);
// circle_arr.push({posx: x, posy: y, posz: z});
// }
// }
/* --
球体绘制方法 - 3
忘记之前从哪里看到的,因为有记录过,但现在找不到来源了,实现上我问了一个数学专业出身的童鞋他也不明白…
看绘制规律跟方法2类似,但毕竟看不懂的东西逼格会高点嘛,下面的k,φ,θ就是看不懂的逼格
-- */
// this.counts = 50;
// for(var i = 0; i < this.counts; i++){
// var k = -1+(2*(i+1)-1)/this.counts;
// var φ = Math.acos(k);
// var θ = φ*Math.sqrt(this.counts*Math.PI);
// for(var j = 0; j < 50; j++){
// var θ = Math.PI * 2 * (j / 50);
// var x = this.radius * Math.sin(φ) * Math.cos(θ);
// var y = this.radius * Math.sin(φ) * Math.sin(θ);
// var z = this.radius * Math.cos(φ);
// circle_arr.push({posx: x, posy: y, posz: z});
// }
// }
this.creatVertex(circle_arr);
},
creatVertex: function(vertex){
//设置原定坐标
var origin = {
x: this.width/2,
y: this.height/2
};
var rotateSpeed = Math.PI/2/40;
vertex.forEach(function(e, i){
var vex = new imgVertex(e, 1000, origin, rotateSpeed, 4);
this.vertex.push(vex);
}.bind(this));
this.ctx.strokeStyle = "#24cb89";
},
renderPointe: function(){
this.clear();
this.vertex.forEach(function(e, i){
e.draw(this.ctx);
}.bind(this));
},
renderLine: function(){
this.clear();
this.ctx.beginPath();
this.vertex.forEach(function(e, i){
e.vertexUpDate();
this.ctx.lineTo(e.x, e.y);
}.bind(this));
this.ctx.stroke();
},
animate: function(){
var _this = this;
this.renderPointe();
//this.renderLine(); //这个是给感兴趣的童鞋通过连线观察每个点的绘制顺序
window.requestAnimationFrame(function(){
_this.animate();
});
},
init: function(){
this.resize(true);
this.getArea();
this.animate();
}
}
function imgVertex(vex, fl, origin, angle, size, color){
var r = Math.floor(Math.random()*255),
g = Math.floor(Math.random()*255),
b = Math.floor(Math.random()*255);
this.x = 0;
this.y = 0;
this.fl = fl; //视距
this.origin = origin;
this.angle = angle;
this.posx = vex.posx;
this.posy = vex.posy;
this.posz = vex.posz;
this.size = size ? size : 20;
this.r = size ? size : 20;
this.color = color ? color : 'rgba('+r+','+g+','+b+',0.6)';
}
imgVertex.prototype = {
//Y轴旋转
ratateY: function(){
var cosy = Math.cos(this.angle),
siny = Math.sin(this.angle),
x1 = this.posx * cosy + this.posz * siny,
z1 = this.posz * cosy - this.posx * siny;
this.posx = x1;
this.posz = z1;
},
//3D坐标投影
projection: function(){
if (this.posz > -this.fl) {
var scale = this.fl / (this.fl + this.posz);
this.x = this.origin.x + this.posx * scale;
this.y = this.origin.y + this.posy * scale;
this.size = this.r * scale;
}
},
draw: function(ctx){
this.ratateY();
ctx.beginPath();
// ctx.arc(this.x, this.y, this.size, 0, Math.PI*2, false); //canvas中绘制圆会比绘制方块消耗更多性能
ctx.fillRect(this.x-this.size/2, this.y-this.size/2, this.size, this.size);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
this.projection();
},
vertexUpDate: function(ctx){
this.ratateY();
this.projection();
}
}
new Stage("cas");
</script>
回到我们最原始的需求上,因为需要完成平面图片的3D旋转视觉,所以需要获取图片的长宽,确定四个顶点位置,将顶点进行连接(lineTo)填充(fill),绘制成平面进行旋转,如果上面的知识你都学会了那这里比上面的demo复杂度还要低很多很多,直接上代码:

<div class="canvas-wrap">
<canvas id="cas"></canvas>
</div>
<script>
function Stage(elm){
this.cas = document.getElementById(elm);
this.ctx = this.cas.getContext("2d");
this.origin = {};
this.vertex = [];
this.loadImg();
}
Stage.prototype = {
loadImg: function(){
var img = new Image();
img.src = "img/timg.jpg"; //图片路径,自行修改
img.onload = function(){
this.img_w = img.width;
this.img_h = img.height;
this.left = (this.cas.width - this.img_w)/2;
this.top = (this.cas.height - this.img_h)/2;
this.init();
}.bind(this);
},
resize: function(booleam){
this.width = this.cas.width = booleam ? this.cas.parentNode.clientWidth * 2 : window.innerWidth * 2;
this.height = this.cas.height = booleam ? this.cas.parentNode.clientHeight * 2 : window.innerHeight * 2;
},
clear: function(){
this.ctx.clearRect(0,0,this.width,this.height);
},
getArea: function(){
var vertex = [
{posx: -this.img_w/2, posy: -this.img_h/2, posz: 0},
{posx: this.img_w/2, posy: -this.img_h/2, posz: 0},
{posx: this.img_w/2, posy: this.img_h/2, posz: 0},
{posx: -this.img_w/2, posy: this.img_h/2, posz: 0}
]
this.creatVertex(vertex);
},
creatVertex: function(vertex){
//设置原定坐标
var origin = {
x: this.width/2,
y: this.height/2
};
var rotateSpeed = Math.PI/2/80;
vertex.forEach(function(e, i){
var vex = new imgVertex(e, 1000, origin, rotateSpeed);
this.vertex.push(vex);
}.bind(this));
this.ctx.fillStyle = "#24cb89";
this.ctx.strokeStyle = "#24cb89";
},
render: function(){
this.clear();
this.ctx.beginPath();
this.vertex.forEach(function(e, i){
e.vertexUpDate();
this.ctx.lineTo(e.x, e.y);
}.bind(this));
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
},
animate: function(){
var _this = this;
this.render();
window.requestAnimationFrame(function(){
_this.animate();
});
},
init: function(){
this.resize(true);
//通过图片大小确定四个顶点坐标
this.getArea();
this.animate();
}
}
function imgVertex(vex, fl, origin, angle){
this.x = 0;
this.y = 0;
this.fl = fl; //视距
this.origin = origin;
this.angle = angle;
this.posx = vex.posx;
this.posy = vex.posy;
this.posz = vex.posz;
}
imgVertex.prototype = {
//Y轴旋转
ratateY: function(){
var cosy = Math.cos(this.angle),
siny = Math.sin(this.angle),
x1 = this.posx * cosy + this.posz * siny,
z1 = this.posz * cosy - this.posx * siny;
this.posx = x1;
this.posz = z1;
},
//3D坐标投影
projection: function(){
if (this.posz > -this.fl) {
var scale = this.fl / (this.fl + this.posz);
this.x = this.origin.x + this.posx * scale;
this.y = this.origin.y + this.posy * scale;
}
},
vertexUpDate: function(ctx){
this.ratateY();
this.projection();
}
}
new Stage("cas");
</script>
这里的关键代码是以下两部分:

上面要注意一点,我们将原点定在canvas中间,所以4个顶点在坐标上的表示(他们对应象限的正负符号)

以上代码是对上面两个知识点的应用,关于旋转与投影。
==================================================================
到这里,我们已经完成图片外形上的3D旋转视觉了,接下来只要把图片放进去,就大功告成,距离目标仅有一步之遥啦!
然而,现实总是这么残酷,下面的分享属于放弃系列,部分包含webgl的基础知识点,非战斗人员请尽快撤离
平面贴图实现思路:

现在我们来梳理一下思路:
- 加载图片,获取图片长宽,确定原始图片的四个顶点在当前canvas上的位置
- 将四个顶点绕Y轴旋转,并不停的重新计算四个顶点当前坐标,将其连接填充
- 根据观察,图形在旋转的时候多为梯形,那么我们需要将图形进行变形
- canvas api中,如果需要使用图片,需要用到drawImage()方法
CanvasRenderingContext2D.drawImage() 是 Canvas 2D API 中的方法,它提供了多种方式来在Canvas上绘制图像。
语法:
context.drawImage(image, x, y);
context.drawImage(image, x, y, width, height);
context.drawImage(img, sx, sy, swidth, sheight ,x, y, width, height);
参数 描述
img -- 规定要使用的图像、画布或视频。
sx -- 可选。开始剪切的 x 坐标位置。
sy -- 可选。开始剪切的 y 坐标位置。
swidth -- 可选。被剪切图像的宽度。
sheight -- 可选。被剪切图像的高度。
x -- 在画布上放置图像的 x 坐标位置。
y -- 在画布上放置图像的 y 坐标位置。
width -- 可选。要使用的图像的宽度。(伸展或缩小图像)
height -- 可选。要使用的图像的高度。(伸展或缩小图像)
而我们需要旋转图片,也就是需要用到该方法来进行图片绘制,那么此时将出现以下问题
drawImage()方法只能传入x, y及大小,而无法以4个顶点的方式传入绘制图片,而我们获取到的是四个投影后的顶点坐标。
那么我们来解决这一系列问题
- 图形旋转的时候,基本为梯形,那么此时我们需要将图形进行变形,也就是使用skew()来对图形进行变形,但我们得到的是顶点信息,而不是这个图形变形在xy轴上变化的角度有多少,所以我们需要使用的是canvas更加底层的方法 - 矩阵转换transform()方法。
但这里又引发了另外一个问题:矩阵转换只能做刚体变换(缩放、位移、旋转)及仿射变换(倾斜),这两种变换方式的特性是平行四边形变换后依然为平行四边形,无法实现投影变换(梯形) -
有稍微看过webgl知识的同学应该知道,任何图形都能通过点、线、面(三角片元)结合而成,例如我们上面的图形旋转,使用的就是点、线连接而成的结果,那么我们只需要应用webgl渲染原理思路,将图形切分为两个三角片元,再使用drawImage()方法,再使用一个clip()方法来进行裁切,就能完成投影变换
矩阵转换图形1
矩阵转换图形2
矩阵转换图形拼合
根据上图,每次都将两个矩阵转换后的图形重合,绿色不重合部分留上部分(或下部分),就能得到一个投影转换结果出来(ppt里不知道怎么对图形进行裁切,自行脑补吧,这里脑补量不大)
矩阵变换基础知识:
常用矩阵变换主要有以下四种,这里不对其原理进行解释,有兴趣的童鞋自行百度。




由于2d平面只有xy,所以跟webGL不同,这里是没有z轴的,所以呢,矩阵第三列并非z组成的矢量,而是对应webGL矩阵中的第四个分量,也叫常量项。
canvas 2d api中需要操作矩阵,需要使用transform方法
画布上的每个对象都拥有一个当前的变换矩阵。transform() 方法替换当前的变换矩阵。它会在前一个变换矩阵上构建。如果想要每一次操作都还原为初始矩阵(即transform(1,0,0,1,0,0)),需要使用setTransform()方法
语法:
context.transform(a,b,c,d,e,f);
参数 描述
a -- 水平缩放绘图
b -- 水平倾斜绘图
c -- 垂直倾斜绘图
d -- 垂直缩放绘图
e -- 水平移动绘图
f -- 垂直移动绘图

根据上图我们也知道了一件事,那就是transform()方法在数组中存储的矩阵元素是按列主序

这里对到有webGL基础的同学需要关注下,因为webGL中传递的矩阵元素是按行主序的,只是我们可能会对齐为下面这种形式(比如下面这个旋转矩阵,对比一下上面的旋转矩阵图示,但它是个数组,所以传入顺序是按行主序)

矩阵计算:
原坐标进行变化后,对应的矩阵计算请看下图,已经用颜色表明了应该哪个元素乘以哪个值


那么一个图形,如果先旋转再缩放,矩阵计算又该怎么算?请看下图,需要拆分成两步,先计算旋转矩阵乘以缩放矩阵,得到的矩阵再乘以原xy值

稍微说一下吧Ra、Rc、Re怎么来的吧,下面的Rb到Rf请自行演算,看结果是否与我图片上的一致
Ra = cosβ* Sx + (-sinβ) * 0 + 0 * 0 = cosβ * Sx
Rc = cosβ* 0 + (-sinβ) * Sy + 0 * 0 = -sinβ * Sy
Re = cosβ* 0 + (-sinβ) * 0 + 0 * 0 = 0
例子又来啦…看一下根据上面的知识,我们是否已经可以实现使用transform来实现api中的方法了呢?

<div class="canvas-wrap">
<canvas id="cas"></canvas>
<div class="btn-wrap">
<div class="btn js-btn" data="reset">reset</div>
<div class="btn js-btn" data="demo-1">rotate</div>
<div class="btn js-btn" data="demo-2">matrix旋转</div>
<div class="btn js-btn" data="demo-3">rotate+scale</div>
<div class="btn js-btn" data="demo-4">matrixMultiplication</div>
<div class="btn js-btn" data="demo-5">matrixMultiplication3</div>
</div>
</div>
<script>
/* 这个是自写的矩阵变化基础库,对应上面四个基础变换矩阵 */
!function(root , fatory){
if('define' in root && define.cmd){
define(function(require, exports, module){
module.exports = fatory()
})
}else if(typeof module === "object" && module.exports){
module.exports = fatory();
}else {
window.matrix = fatory();
}
}(this , function(){
function Matrix(){
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
}
Matrix.prototype = {
reset: function(){
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
return this;
},
rotate: function(angle){
var sin = Math.sin(Math.PI / 180 * angle),
cos = Math.cos(Math.PI / 180 * angle),
a = this.a,
b = this.b,
c = this.c,
d = this.d,
e = this.e,
f = this.f;
this.a = a * cos + c * sin;
this.b = b * cos + d * sin;
this.c = a * (-sin) + c * cos;
this.d = b * (-sin) + d * cos;
return this;
},
scale: function(sx, sy){
this.a *= sx;
this.b *= sx;
this.c *= sy;
this.d *= sy;
return this;
},
translate: function(dx, dy){
var a = this.a,
b = this.b,
c = this.c,
d = this.d;
this.e = a * dx + c * dy;
this.f = b * dx + d * dy;
return this;
},
skew: function(ax, ay){
var tanX = Math.tan(Math.PI / 180 * ax),
tanY = Math.tan(Math.PI / 180 * ay),
a = this.a,
b = this.b,
c = this.c,
d = this.d;
this.a = a + c * tanY;
this.b = b + d * tanY;
this.c = a * tanX + c;
this.d = b * tanX + d;
return this;
}
}
var martix = new Matrix();
return martix;
});
</script>
<script>
function Stage(elm){
this.cas = document.getElementById(elm);
this.ctx = this.cas.getContext("2d");
this.init();
}
Stage.prototype = {
resize: function(booleam){
this.width = this.cas.width = booleam ? this.cas.parentNode.clientWidth * 2 : window.innerWidth * 2;
this.height = this.cas.height = booleam ? this.cas.parentNode.clientHeight * 2 : window.innerHeight * 2;
},
clear: function(){
this.ctx.clearRect(0,0,this.width,this.height);
},
createRect: function(){
this.rect = new rect({
ctx: this.ctx,
width: 500,
height: 300,
x: this.width/2,
y: this.height/2,
angleSteps: 2
})
},
eventBtn: function(){
var me = this;
var btns = document.querySelectorAll(".js-btn");
btns.forEach(function(elm){
elm.addEventListener("click",function(){
me.rect.runId = this.getAttribute("data");
},false);
});
},
render: function(){
this.clear();
this.rect.draw();
},
animate: function(){
var _this = this;
this.ctx.save();
this.render();
this.ctx.restore();
window.requestAnimationFrame(function(){
_this.animate();
});
},
init: function(){
this.resize(true);
this.createRect();
this.eventBtn();
this.animate();
}
}
function rect(){
this.ctx = arguments[0]['ctx'];
this.width = arguments[0]['width'];
this.height = arguments[0]['height'];
this.pivotX = arguments[0]['width']/2;
this.pivotY = arguments[0]['height']/2;
this.x = arguments[0]['x'];
this.y = arguments[0]['y'];
this.angleSteps = arguments[0]['angleSteps'];
this.moveSpeed = 0;
this.runId = "";
this.color = arguments[0]['color'] ? arguments[0]['color'] : "#04bcd2";
}
rect.prototype = {
DegToRad: function(deg){
return Math.PI / 180 * this.moveSpeed;
},
translate: function(tx,ty){
this.ctx.transform(1,0,0,1,tx,ty);
},
draw: function(){
this.moveSpeed += this.angleSteps;
this.ctx.translate(this.x, this.y);
this.ctx.save();
this.changeFun();
this.ctx.fillStyle = this.color;
this.ctx.fillRect(-this.pivotX,-this.pivotY,this.width,this.height);
//绘制原点
this.ctx.restore();
this.ctx.beginPath();
this.ctx.arc(0,0,10,0,Math.PI*2,false);
this.ctx.closePath();
this.ctx.fillStyle = "#f81264";
this.ctx.fill();
},
changeFun: function(){
switch(this.runId){
case "demo-1":
this.fun1();
break;
case "demo-2":
this.fun2();
break;
case "demo-3":
this.fun3();
break;
case "demo-4":
this.fun4();
break;
case "demo-5":
this.fun5();
break;
case "reset":
this.reset();
break;
}
},
fun1: function(){
this.ctx.rotate(this.DegToRad());
},
fun2: function(){
this.ctx.transform(
Math.cos(this.DegToRad()), Math.sin(this.DegToRad()),
-Math.sin(this.DegToRad()), Math.cos(this.DegToRad()),
0, 0
);
},
fun3: function(){
this.ctx.rotate(this.DegToRad());
this.ctx.scale(2,1);
},
fun4: function(){
this.ctx.transform(
2*Math.cos(this.DegToRad()), 2*Math.sin(this.DegToRad()),
1*-Math.sin(this.DegToRad()), 1*Math.cos(this.DegToRad()),
0, 0
)
},
fun5: function(){
/* 对比 */
// this.ctx.rotate(this.moveSpeed);
// this.ctx.translate(10,200);
// this.ctx.scale(2,1);
var mat = matrix.reset().rotate(this.moveSpeed).translate(10,200).scale(2,1);
// var mat = matrix.reset().skew(0,45);
this.ctx.transform(
mat.a,
mat.b,
mat.c,
mat.d,
mat.e,
mat.f
);
},
reset: function(){
this.ctx.transform(1,0,0,1,0,0);
}
}
new Stage("cas");
</script>
内容太多,还很枯燥对吧,不仅枯燥,关键还看不太懂对吧…其实我分享的时候大家一样听不懂,这也就是为什么说学会前面两点就行了,那里已经足够你们做一些视觉效果了,至于图片旋转的,我还是支持各位用threejs或者直接css3吧,后面的内容其实只是自己折腾的记录,先暂时写到这…
继续未完待续…
网友评论