【原创】《矩阵的史诗级玩法》连载十三:基向量坐标变换矩阵的代码实现
本篇我们把上篇最后提到的矩阵实现出来并替换之前45度地图演示文件的矩阵上。
var baseX = new Point(0.87, 0.5); //ex基向量
var baseY = new Point(-0.32, 0.94); //ey基向量
var matrix = new Matrix();
matrix.a = baseX.x;
matrix.b = baseX.y;
matrix.c = baseY.x;
matrix.d = baseY.y;
MatrixUtil.translate(matrix, 400, 0);
var matrixInvert = matrix.clone();
matrixInvert.invert();
可以发现,替换的只是初始矩阵,而它的逆变换无需改动任何代码,使用通用的求逆方法invert即可。
代码改过好几次了,现在我给出当前完整版本的html文件代码:
<!DOCTYPE html>
<html>
<head>
<title>斜铺砖块与矩阵</title>
<script src="Matrix.js"></script>
<script src="MatrixUtil.js"></script>
<script src="Point.js"></script>
</head>
<body>
<canvas width="800" height="800" id="canvas"></canvas>
</body>
<script>
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.strokeStyle = "#0000cc";
context.fillStyle = "#ccccff";
context.lineWidth = 0.5;
var gridNumX = 10;
var gridNumY = 10;
var unitSize = 40;
var baseX = new Point(0.87, 0.5); //ex基向量
var baseY = new Point(-0.32, 0.94); //ey基向量
var matrix = new Matrix();
matrix.a = baseX.x;
matrix.b = baseX.y;
matrix.c = baseY.x;
matrix.d = baseY.y;
MatrixUtil.translate(matrix, 400, 0);
var matrixInvert = matrix.clone();
matrixInvert.invert();
function draw(xIndex, yIndex)
{
for(var j = 0; j < gridNumY; j ++)
{
for(var i = 0; i < gridNumX; i ++)
{
var x = i * unitSize;
var y = j * unitSize;
//左上
var leftTop = new Point(x, y);
//右上
var rightTop = new Point(x + unitSize, y);
//右下
var rightBottom = new Point(x + unitSize, y + unitSize);
//左下
var leftBottom = new Point(x, y + unitSize);
//中间
var center = new Point(x + unitSize * 0.5, y + unitSize * 0.5);
//让编号对应上的砖块变成黄色
context.fillStyle = (i == xIndex && j == yIndex) ? "#ffeecc" : "#ccccff";
context.beginPath();
var transformedLeftTop = matrix.transformPoint(leftTop);
context.moveTo(transformedLeftTop.x, transformedLeftTop.y);
var transformedRightTop = matrix.transformPoint(rightTop);
context.lineTo(transformedRightTop.x, transformedRightTop.y);
var transformedRightBottom = matrix.transformPoint(rightBottom);
context.lineTo(transformedRightBottom.x, transformedRightBottom.y);
var transformedLeftBottom = matrix.transformPoint(leftBottom);
context.lineTo(transformedLeftBottom.x, transformedLeftBottom.y);
context.closePath();
context.stroke();
context.fill();
context.fillStyle = "#000";
var transformedCenter = matrix.transformPoint(center);
context.fillText(i + "," + j, transformedCenter.x - 5, transformedCenter.y + 5);//根据字号做了个粗糙的修正
}
}
}
//一开始要先绘制一次,并且不让任何编号的砖块都高亮
draw(-1, -1);
canvas.addEventListener("mousemove", function(event)
{
//鼠标位置
var mouseX = event.clientX-canvas.getBoundingClientRect().left;
var mouseY = event.clientY-canvas.getBoundingClientRect().top;
var transformedMouse = matrixInvert.transformPoint(new Point(mouseX, mouseY));
var xIndex = Math.floor(transformedMouse.x / unitSize);
var yIndex = Math.floor(transformedMouse.y / unitSize);
draw(xIndex, yIndex);
});
</script>
</html>
此处我用Point,也就是点的类来存储基向量。数学上,向量和点要严格区分,而且向量有自己的一些计算方法,但是形式上,它们却又非常相似,都是用(x,y)数对来表示(三维就是x,y,z,其它类推)。但为了后续不至于混淆,我们还是单独创建个向量类吧。
Vector2D.js代码
function Vector2D(x, y) {
this.x = isNaN(x) ? 0 : x;
this.y = isNaN(y) ? 0 : y;
function clone()
{
return new Vector2D(this.x, this.y);
}
this.clone = clone;
/**
* 向量长度
*
**/
Object.defineProperty(this, "length", {get: getLength, set: setLength});
function getLength()
{
return Math.sqrt(x * x + y * y);
}
function setLength(value)
{
var length = getLength();
var length = this.length;
console.log(length)
if(length > 0)
{
this.x *= value / length;
this.y *= value / length;
}
}
/**
* 向量单位化
*
**/
function normalize()
{
setLength(1);
}
this.normalize = normalize;
/**
* 向量加法
*
**/
function add(vec)
{
return new Vector2D(this.x + vec.x, this.y + vec.y);
}
this.add = add;
/**
* 向量减法
*
**/
function subtract(vec)
{
return new Vector2D(this.x - vec.x, this.y - vec.y);
}
this.subtract = subtract;
/**
* 向量数乘
*
**/
function multiplyNumber(num)
{
return new Vector2D(this.x * num, this.y * num);
}
this.multiplyNumber = multiplyNumber;
/**
* 向量点积
*
**/
function dot(vec)
{
return this.x * vec.x + this.y * vec.y;
}
this.dot = dot;
/**
* 向量叉积
*
**/
function cross(vec)
{
return this.x * vec.y - this.y * vec.x;
}
this.cross = cross;
}
此处我把向量的基础运算实现了下,以后有可能用得到。
向量代数不是本系列教程的重点,就不展开讨论了,大家可以自行查阅资料学习。
把html代码中表示基向量的两个Point换成Vector2D,虽然效果上没有任何变化,但它的意义却非常深远。
这篇跟上篇一样,内容不多,但已经被一些不太有营养的东西给撑长了。还有一个效果图,我贴上来之后这篇就该结束了,哈哈。
大家可能有个疑问,怎么这些数字看起来这么刁钻,作为演示拿1,2这样的整数不更好?其实这里的向量有个特点,就是都为单位向量,即长度都等于1,大家可以分别算下两个向量的x与y的平方和。
那如果不用长度等于1的向量,效果会怎样呢?我们试试把baseX改成(2,1)看看。
超出画布范围了,把translate中的400改小点吧,比如200。
两边都越界了,不过我们不纠结这个细节。我们发现,砖块在横向上被拉长了,而使用单位向量的时候,砖块的边长并没有发生变化。
这里我直接给出一个结论:被拉长的倍数恰好等于向量的长度。推导就不推了,有兴趣的朋友可以找我单独讨论,不然又得扯远。
铺贴角度反映的仅仅是一个旋转或者斜切,为了变换的纯粹,剔除其它的干扰因素,业界都统一使用单位向量作为旋转斜切的基向量。
我们发现,用基向量有很多的优点,一是通用性强,无需针对角度做特殊处理,二是运行效率较多步骤高,矩阵的每个元素都不需要计算,而且只需执行一次变换,三是数据存储方便,想想用我之前45度地图的方法,那就得整个矩阵存下来了,而且要从中反推步骤也困难,可读性差。
然而,基向量法之所以能实现得如此顺利,完全归功于我们封装好的求逆方法。有了求逆,我们在实现变换矩阵的时候才可以如此灵活,不易受到太多业务和细节的约束。从而使矩阵的应用领域更为广泛。
既然矩阵辣磨好用,那么,我们的眼光自然要放远一些,不能局限于这么一个简单的铺贴上面了。下一篇,就让我们解放双手,拥抱蓝天,向着史诗级的殿堂一起进发吧!