美文网首页W3C
Canvas呈现3D业务逻辑关系

Canvas呈现3D业务逻辑关系

作者: Cesium4Unreal | 来源:发表于2017-03-24 22:47 被阅读53次

    导言

    3D引擎那么火,你让2D怎么办? 闲来无事,用Canvas实现3D效果的业务关系,是否也是一种乐趣?

    先睹为快

    Paste_Image.png

    实验天地

    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>
    

    效果

    Paste_Image.png Paste_Image.png

    封装一个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};
        },
      });
    

    就是把小学初中所学的几何知识用上就可以了;

    Paste_Image.png 换个色

    再封装一个倾斜平面

    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旋转效果

    相关文章

      网友评论

        本文标题:Canvas呈现3D业务逻辑关系

        本文链接:https://www.haomeiwen.com/subject/wurqottx.html