导言
3D引擎那么火,你让2D怎么办? 闲来无事,用Canvas实现3D效果的业务关系,是否也是一种乐趣?
先睹为快
实验天地
WebGL能绘制3D效果,Canvas的2D绘图就不能了吗? 其实不然,也能绘制,只是消耗的都是内存,绘制效率自然收到影响;但若场景不大,3D效果不太真,也不妨试试;
Canvas绘制3D Cube
<html>
<head>
<meta charset="gbk" />
<title>3D cube HTML5 canvas realization</title>
<script type="text/javascript">
function color(r, g, b, a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
function point2D(x, y)
{
this.x = x;
this.y = y;
}
point2D.prototype.move = function(p2D)
{
this.x += p2D.x;
this.y += p2D.y;
}
function point3D(x, y, z)
{
this.x = x;
this.y = y;
this.z = z;
}
point3D.prototype.move = function(p3D)
{
this.x += p3D.x;
this.y += p3D.y;
this.z += p3D.z;
}
point3D.prototype.swap = function(p3D)
{
this.x = p3D.x;
this.y = p3D.y;
this.z = p3D.z;
}
point3D.prototype.rotate = function(axis, angleGr)
{
angleRad = angleGr * Math.PI / 180;
switch (axis)
{
case "x":
{
var tempPoint = new point3D(
this.x,
this.y * Math.cos(angleRad) - this.z * Math.sin(angleRad),
this.y * Math.sin(angleRad) + this.z * Math.cos(angleRad)
);
this.swap(tempPoint);
break;
}
case "y":
{
var tempPoint = new point3D(
this.x * Math.cos(angleRad) + this.z * Math.sin(angleRad),
this.y,
-this.x * Math.sin(angleRad) + this.z * Math.cos(angleRad)
);
this.swap(tempPoint);
break;
}
case "z":
{
var tempPoint = new point3D(
this.x * Math.cos(angleRad) - this.y * Math.sin(angleRad),
this.x * Math.sin(angleRad) + this.y * Math.cos(angleRad),
this.z
);
this.swap(tempPoint);
break;
}
}
}
function normal3D(p3D, length)
{
this.point = p3D;
this.length = length;
}
function poly()
{
var points = [];
for(var i = 0; i < arguments.length; i++)
points.push(arguments[i]);
this.points = points;
// Calculating normal
var v1 = new point3D(points[2].x - points[1].x, points[2].y - points[1].y, points[2].z - points[1].z);
var v2 = new point3D(points[0].x - points[1].x, points[0].y - points[1].y, points[0].z - points[1].z);
var normalP3D = new point3D(v1.y*v2.z-v2.y*v1.z, v1.z*v2.x-v2.z*v1.x, v1.x*v2.y-v2.x*v1.y);
var normalLen = Math.sqrt(normalP3D.x*normalP3D.x + normalP3D.y*normalP3D.y + normalP3D.z*normalP3D.z);
this.normal = new normal3D(normalP3D, normalLen);
}
poly.prototype.move = function(p3D)
{
for(var i = 0; i < this.points.length; i++)
{
var point = this.points[i];
point.move(p3D);
}
}
poly.prototype.rotate = function(axis, angle)
{
for(var i = 0; i < this.points.length; i++)
{
var point = this.points[i];
point.rotate(axis, angle);
}
this.normal.point.rotate(axis, angle);
}
poly.prototype.put = function(center, fillColor, edgeColor)
{
// Calulate visibility
var normalAngleRad = Math.acos(this.normal.point.z/this.normal.length);
if(normalAngleRad / Math.PI * 180 >= 90)
return;
var lightIntensity = 1 - 2 * (normalAngleRad / Math.PI);
ctx.fillStyle = 'rgba('+fillColor.r+','+fillColor.g+','+fillColor.b+','+
(fillColor.a*lightIntensity)+')';
ctx.beginPath();
for(var i = 0; i < this.points.length; i++)
{
var point = this.points[i];
if(i)
ctx.lineTo(center.x + parseInt(point.x), center.y - parseInt(point.y));
else
ctx.moveTo(center.x + parseInt(point.x), center.y - parseInt(point.y));
}
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba('+edgeColor.r+','+edgeColor.g+','+edgeColor.b+','+
(edgeColor.a*lightIntensity)+')';
ctx.beginPath();
var point = this.points[this.points.length-1];
ctx.moveTo(center.x + parseInt(point.x), center.y - parseInt(point.y));
for(var i = 0; i < this.points.length; i++)
{
var point = this.points[i];
ctx.lineTo(center.x + parseInt(point.x), center.y - parseInt(point.y));
}
ctx.stroke();
}
function Cube(size, fillColor, edgeColor)
{
var p000 = new point3D(0,0,0);
var p0S0 = new point3D(0,size,0);
var pSS0 = new point3D(size,size,0);
var pS00 = new point3D(size,0,0);
var p00S = new point3D(0,0,size);
var p0SS = new point3D(0,size,size);
var pSSS = new point3D(size,size,size);
var pS0S = new point3D(size,0,size);
var polys = [];
polys.push(new poly(p000,p0S0,pSS0,pS00));
polys.push(new poly(pS00,pSS0,pSSS,pS0S));
polys.push(new poly(pS0S,pSSS,p0SS,p00S));
polys.push(new poly(p00S,p0SS,p0S0,p000));
polys.push(new poly(p0S0,p0SS,pSSS,pSS0));
polys.push(new poly(p00S,p000,pS00,pS0S));
this.polys = polys;
var points = [];
points.push(p000);
points.push(p0S0);
points.push(pSS0);
points.push(pS00);
points.push(p00S);
points.push(p0SS);
points.push(pSSS);
points.push(pS0S);
for(var i = 0; i < polys.length; i++)
{
points.push(polys[i].normal.point);
}
this.points = points;
this.fillColor = fillColor;
this.edgeColor = edgeColor;
}
function move(o3D, p3D)
{
for(var i = 0; i < o3D.points.length - o3D.polys.length; i++)
{
var point = o3D.points[i];
point.move(p3D);
}
}
function put(o3D, center)
{
for(var i = 0; i < o3D.polys.length; i++)
{
var poly = o3D.polys[i];
poly.put(center, o3D.fillColor, o3D.edgeColor);
}
}
function rotate(o3D, axis, angle)
{
for(var i = 0; i < o3D.points.length; i++)
{
var point = o3D.points[i];
point.rotate(axis, angle);
}
}
function init(){
canvas = document.getElementById('3Dcube');
if (canvas.getContext){
ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height); // clear canvas
cube = new Cube(100, new color(50,50,200,1), new color(60,60,210,1));
move(cube, new point3D(-50,-50,-50));
rotate(cube, 'x', 45);
rotate(cube, 'y', 45);
rotate(cube, 'z', 45);
centerScreen = new point2D(canvas.width / 2, canvas.height / 2);
put(cube, centerScreen);
timer = setInterval(nextFrame, 1000 / 60);
}
}
function nextFrame()
{
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height); // clear canvas
rotate(cube, 'x', 0.4);
rotate(cube, 'y', 0.6);
rotate(cube, 'z', 0.3);
ctx.fillStyle = 'rgba(50, 50, 200, 1)';
ctx.strokeStyle = 'rgba(60, 60, 210, 1)';
put(cube, centerScreen);
}
</script>
<style type="text/css">
canvas { border: 0px solid black; }
</style>
</head>
<body onload="init();">
<h1>3D cube HTML5 canvas realization on 2D contex</h1>
<p>Features:
<ul>
<li>3D operations: rotating, moving object center</li>
<li>Direct illumination</li>
<li>Highlighting edges</li>
<li>Optimizations:
<ul>
<li>Skip outputting of invisible polygons</li>
<li>Skip processing of duplicated points</li>
</ul>
</li>
</ul>
</p>
<canvas id="3Dcube" width="400" height="225"></canvas>
</body>
</html>
效果
封装一个Cube模块
CNode = function(id) {
CNode.superClass.constructor.call(this, id);
}
twaver.Util.ext("CNode", twaver.Node, {
_split:1/3,
_cubeAngle:Math.PI/6,
getVectorUIClass: function (){
return CNodeUI;
},
setSplit:function(split){
this._split = split;
},
setCubeAngle:function(angle){
this._cubeAngle = angle;
}
});
CNodeUI = function(network, element) {
CNodeUI.superClass.constructor.call(this, network, element);
}
twaver.Util.ext('CNodeUI', twaver.vector.NodeUI, {
drawVectorBody : function(ctx) {
// CNodeUI.superClass.drawVectorBody.call(this, ctx);
var node = this._element;
var rect = this.getZoomBodyRect();
// rect.x = rect.x + rect.width /4;
// rect.y = rect.y + rect.height /4;
// rect.width /= 2;
// rect.height /= 2;
var angleSin = Math.sin(node._cubeAngle);
var angleCos = Math.cos(node._cubeAngle);
var angleTan = Math.tan(node._cubeAngle);
var split = node._split;
var dash = false;
var fill = false;
var fillColor = this.getStyle('vector.fill.color');
var close = false;
var cubeDepth = node._width * split/angleCos;
var cubeWidth = node._width * (1 - split) / angleCos;
// var cubeHeight = rect.height/3;
var cubeHeight = rect.height - cubeWidth * angleSin - cubeDepth * angleSin;
var angle = node.getClient('angle');
var center = {x:rect.x + rect.width/2,y:rect.y + rect.height/2};
var p1 = {},p2 = {}, p3 = {}, p4 = {}, p5 = {},p6 = {}, p7 = {}, p8 = {};
p1.x = rect.x + rect.width * split;
p1.y = rect.y + rect.height;
p2.x = rect.x;
p2.y = rect.y + rect.height - cubeDepth * angleSin;
p3.x = p2.x;
p3.y = p2.y - cubeHeight;
p4.x = p1.x;
p4.y = p1.y - cubeHeight ;
p6.x = rect.x + rect.width;
p6.y = rect.y + rect.height - cubeWidth * angleSin;
p5.x = p6.x;
p5.y = p6.y - cubeHeight;
p7.x = rect.x + rect.width * (1 - split);
p7.y = rect.y;
p8.x = p7.x;
p8.y = p7.y + cubeHeight;
p1 = this.rotatePoint(center,p1,angle * Math.PI / 180);
p2 = this.rotatePoint(center,p2,angle * Math.PI / 180);
p3 = this.rotatePoint(center,p3,angle * Math.PI / 180);
p4 = this.rotatePoint(center,p4,angle * Math.PI / 180);
p5 = this.rotatePoint(center,p5,angle * Math.PI / 180);
p6 = this.rotatePoint(center,p6,angle * Math.PI / 180);
p7 = this.rotatePoint(center,p7,angle * Math.PI / 180);
p8 = this.rotatePoint(center,p8,angle * Math.PI / 180);
close = false;
dash = true;
fill = false;
this.drawPoints(ctx,[p2,p8],close,dash,fill);
this.drawPoints(ctx,[p7,p8],close,dash,fill);
this.drawPoints(ctx,[p6,p8],close,dash,fill);
dash = false;
close = true;
fill = true;
this.drawPoints(ctx,[p1,p2,p3,p4],close,dash,fill,fillColor);
this.drawPoints(ctx,[p1,p4,p5,p6],close,dash,fill);
this.drawPoints(ctx,[p3,p4,p5,p7],close,dash,fill);
},
drawPoints:function(ctx,points,close,dash,fill,fillColor){
if(!points || points.length == 0){
return;
}
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.lineWidth = 0.5;
if(fill && fillColor) {
ctx.fillStyle = fillColor.colorRgb(0.6);
}
if(dash){
ctx.setLineDash([8,8]);
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
}else{
ctx.setLineDash([1,0]);
}
ctx.moveTo(points[0].x,points[0].y);
for(var i = 1;i < points.length; i++){
var p = points[i];
ctx.lineTo(p.x,p.y);
}
if(close){
ctx.lineTo(points[0].x,points[0].y);
}
ctx.closePath();
ctx.stroke();
if(fill){
ctx.fill();
}
},
rotatePoint:function(center,p,angle) {
var x = (p.x - center.x) * Math.cos(angle) - (p.y - center.y) * Math.sin(angle) + center.x;
var y = (p.x - center.x) * Math.sin(angle) + (p.y - center.y) * Math.cos(angle) + center.y;
return {x:x, y:y};
},
});
就是把小学初中所学的几何知识用上就可以了;
再封装一个倾斜平面
var CGroup = function(id){
CGroup.superClass.constructor.apply(this, arguments);
this.enlarged = false;
};
twaver.Util.ext(CGroup, twaver.Group, {
_tiltAngle:45,
getTiltAngleX : function() {
return this._tiltAngle;
},
setTiltAngleX : function(angle) {
var oldValue = this._tiltAngle;
this._tiltAngle = angle % 360;
this.firePropertyChange("tiltAngleX", oldValue, this._tiltAngle);
},
getVectorUIClass:function(){
return CGroupUI;
},
isEnlarged:function() {
return this.enlarged;
},
setEnlarged:function(value){
this.enlarged = value;
var fillColor;
if(value === false){
this.setClient("group.angle",this._tiltAngle);
this.setClient("group.shape","parallelogram");
this.setClient("group.deep",10);
this.setStyle("select.style","none");
// this.setStyle("group.gradient","linear.northeast");
this.setStyle("group.gradient","radial.center");
this.setStyle("group.deep",0);
this.setStyle("label.position","right.left");
this.setStyle("label.xoffset",-10);
this.setStyle("label.yoffset",-30);
this.setStyle("label.font","italic bold 12px/30px arial,sans-serif");
fillColor = this.changeHalfOpacity(this.getStyle("group.fill.color"));
this.setStyle("group.fill.color",fillColor);
this.setAngle(-20);
}else{
this.setAngle(0);
this.setClient("group.angle",1);
this.setClient("group.shape","parallelogram");
this.setClient("group.deep",0);
this.setStyle("select.style","none");
this.setStyle("group.gradient","linear.northeast");
this.setStyle("group.deep",0);
this.setStyle("label.position","right.right");
this.setStyle("label.xoffset",0);
this.setStyle("label.yoffset",0);
this.setStyle("label.font","italic bold 12px/30px arial,sans-serif");
fillColor = this.changeOpacity(this.getStyle("group.fill.color"));
this.setStyle("group.fill.color",fillColor);
}
},
increaseOpacity:function(rgba){
if(typeof rgba === "string" && rgba.indexOf("rgba(") !== -1 && rgba.indexOf(")") !== -1){
var rgbaSub = rgba.substring(5, rgba.length-1);
var rgbaNums = rgbaSub.split(",");
var returnColor ="rgba(";
var i;
for(i=0;i<rgbaNums.length;i++){
if(i !== rgbaNums.length-1){
returnColor = returnColor +rgbaNums[i]+",";
}else{
var opacity = parseFloat(rgbaNums[i])+0.25;
returnColor = returnColor +opacity+")";
}
}
return returnColor;
}else{
return rgba;
}
},
changeOpacity:function(rgba){
if(typeof rgba === "string" && rgba.indexOf("rgba(") !== -1 && rgba.indexOf(")") !== -1){
var rgbaSub = rgba.substring(5, rgba.length-1);
var rgbaNums = rgbaSub.split(",");
var returnColor ="rgba(";
var i;
for(i=0;i<rgbaNums.length;i++){
if(i !== rgbaNums.length-1){
returnColor = returnColor +rgbaNums[i]+",";
}else{
var opacity = 1;
returnColor = returnColor +opacity+")";
}
}
return returnColor;
}else{
return rgba;
}
},
changeHalfOpacity:function(rgba){
if(typeof rgba === "string" && rgba.indexOf("rgba(") !== -1 && rgba.indexOf(")") !== -1){
var rgbaSub = rgba.substring(5, rgba.length-1);
var rgbaNums = rgbaSub.split(",");
var returnColor ="rgba(";
var i;
for(i=0;i<rgbaNums.length;i++){
if(i !== rgbaNums.length-1){
returnColor = returnColor +rgbaNums[i]+",";
}else{
var opacity = 0.5;
returnColor = returnColor +opacity+")";
}
}
return returnColor;
}else{
return rgba;
}
},
});
var CGroupUI = function(network,element){
CGroupUI.superClass.constructor.apply(this, arguments);
};
twaver.Util.ext(CGroupUI, twaver.vector.GroupUI, {
createBodyRect: function () {
this._shapeRect = null;
var group = this._element;
var network = this._network;
var rect = null;
if (group.isExpanded()) {
group.getChildren().forEach(function (child) {
var ui = network.getElementUI(child);
ui && ui.validate();
});
var rects = this.getChildrenRects();
if (!rects.isEmpty()) {
var shape = group.getStyle('group.shape');
var func = _twaver.group[shape];
if (!func) {
throw "Can not resolve group shape '" + shape + "'";
}
this._shapeRect = func(rects);
var orgRect = this._shapeRect;
if (group._angle !== 0) {
var matrix = _twaver.math.createMatrix(group._angle * Math.PI / 180, orgRect.x + orgRect.width / 2, orgRect.y + orgRect.height / 2);
var points = [{
x : orgRect.x,
y : orgRect.y
}, {
x : orgRect.x + orgRect.width,
y : orgRect.y
}, {
x : orgRect.x + orgRect.width,
y : orgRect.y + orgRect.height
}, {
x : orgRect.x,
y : orgRect.y + orgRect.height
}];
for (var i = 0, n = points.length; i < n; i++) {
points[i] = matrix.transform(points[i]);
}
rect = _twaver.math.getRect(points);
}else{
rect = this._shapeRect;
}
}
}
if (rect) {
_twaver.math.addPadding(rect, group, 'group.padding', 1);
return rect;
} else {
return twaver.vector.GroupUI.superClass.createBodyRect.call(this);
}
},
validateBodyBounds: function () {
var $math=_twaver.math;
var node = this._element;
this.getBodyRect();
var shape = node.getClient("group.shape");
if (shape === "parallelogram" && this._shapeRect) {
var rect = this.getPathRect("group", false);
var deep = this.getStyle('group.deep');
var groupDeep = node.getClient('group.deep');
var parallelogramAngle = node.getClient("group.angle") * Math.PI / 180;
var xOffset = this._shapeRect.height*Math.tan(parallelogramAngle);
this._shapeRect.width = this._shapeRect.width + xOffset*3/2;
this._shapeRect.height = this._shapeRect.height + groupDeep;
this._shapeRect.x = this._shapeRect.x-xOffset*3/4;
var rectXOffset = rect.height*Math.tan(parallelogramAngle);
rect.width = rect.width + rectXOffset*3/2;
rect.x = rect.x - rectXOffset*3/4;
$math.grow(rect,deep+1,deep+1);
this.addBodyBounds(rect);
var bound=_twaver.cloneRect(rect);
bound.width+=10;
bound.height+=10;
this.addBodyBounds(bound);
} else {
twaver.vector.GroupUI.superClass.validateBodyBounds.call(this);
}
},
drawPath : function(ctx, prefix, padding, pattern, points, segments, close) {
var $g=_twaver.g;
var zoomManager = this._network.zoomManager;
var node = this._element;
var rect = null;
var shape = node.getClient("group.shape");
if(shape === "parallelogram"){
if (prefix == 'group') {
rect = this._shapeRect;
} else {
rect = this.getZoomBodyRect();
};
if (padding) {
$math.addPadding(rect, node, prefix + '.padding', 1);
}
var lineWidth = node.getStyle(prefix + '.outline.width');
this.setGlow(this, ctx);
this.setShadow(this, ctx);
if (node.getAngle() != 0) {
if (!( node instanceof twaver.Group)) {
rect = node.getOriginalRect();
rect = zoomManager._getElementZoomRect(this, rect);
}
ctx.save();
twaver.Util.rotateCanvas(ctx, rect, node.getAngle());
}
var fill = node.getStyle(prefix + '.fill');
var fillColor;
if (fill) {
if (this._innerColor && !$element.hasDefault(this._element)) {
fillColor = this._innerColor;
} else {
fillColor = node.getStyle(prefix + '.fill.color');
}
var gradient = node.getStyle(prefix + '.gradient');
if (gradient) {
$g.fill(ctx, fillColor, gradient, node.getStyle(prefix + '.gradient.color'), rect);
} else {
ctx.fillStyle = fillColor;
}
}
ctx.lineJoin = "round";
ctx.lineWidth = 10;
ctx.strokeStyle = "#435474".colorRgb(0.8);
//draw round rect body.
var parallelogramAngle = node.getClient("group.angle") * Math.PI / 180;
var xOffset = rect.height*Math.tan(parallelogramAngle);
var groupDeep = node.getClient('group.deep');
if(parallelogramAngle){
ctx.save();
ctx.beginPath();
ctx.moveTo(rect.x, rect.y);
ctx.lineTo(rect.x+rect.width-xOffset, rect.y);
ctx.lineTo(rect.x+rect.width, rect.y+rect.height-groupDeep);
ctx.lineTo(rect.x+xOffset,rect.y+rect.height-groupDeep);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
if(fillColor.indexOf("rgba(") !== -1){
var changedColor = this._element.increaseOpacity(fillColor);
ctx.fillStyle=changedColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(rect.x+xOffset, rect.y+rect.height-groupDeep);
ctx.lineTo(rect.x+rect.width, rect.y+rect.height-groupDeep);
ctx.lineTo(rect.x+rect.width, rect.y+rect.height);
ctx.lineTo(rect.x+xOffset, rect.y+rect.height);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
changedColor = this._element.increaseOpacity(changedColor);
ctx.fillStyle=changedColor;
ctx.save();
ctx.beginPath();
ctx.moveTo(rect.x+xOffset, rect.y+rect.height-groupDeep);
ctx.lineTo(rect.x+xOffset, rect.y+rect.height);
ctx.lineTo(rect.x, rect.y + groupDeep);
ctx.lineTo(rect.x, rect.y);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
}
}else{
twaver.vector.GroupUI.superClass.drawPath.apply(this,arguments);
}
if (node.getAngle() != 0) {
ctx.restore();
}
},
});
参考文献
[1].canvas实现简单3D旋转效果
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。