3D引擎那麼火,你讓2D怎麼辦? 閒來無事,用Canvas實現3D效果的業務關係,是否也是一種樂趣?javascript
WebGL能繪製3D效果,Canvas的2D繪圖就不能了嗎? 其實否則,也能繪製,只是消耗的都是內存,繪製效率天然收到影響;但若場景不大,3D效果不太真,也不妨試試;css
Canvas繪製3D Cubehtml
<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>
效果java
封裝一個Cube模塊node
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}; }, });
就是把小學初中所學的幾何知識用上就能夠了;canvas
再封裝一個傾斜平面app
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旋轉效果ui