文章首发于我的知乎专栏,原地址:https://zhuanlan.zhihu.com/p/26606208
以前看到过一个问题:谢尔宾斯基三角形能用编程写出来么?该怎么写? - 知乎,在回答里,各方大神用各种语言各种方法实现了一遍,非常精彩。我当时也回答了这个问题,是用H5的Canvas实现的。这在前端技术上没什么难度,主要是算法比较有可玩性,所以当时就手痒了。
谢尔宾斯基三角形是分形图形的一种,大概很多人第一次见到它都是在中学教科书上。它长这样:
我用了两种方法构造它:直接绘制三角形和间接用折线逼近。
1.直接绘制三角形。具体方法就是先画一个大三角形,再递归绘制一堆倒三角形。这种方法最为直观也最好想。放码过来:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Sierpinski Triangle</title> </head> <body> <canvas id="canvas" width="600" height="600" style="display:block;margin:50px auto"> 你的浏览器不支持canvas </canvas> </body> <script type="text/javascript"> var context = document.getElementById("canvas") .getContext("2d"); //根据三顶点坐标绘制一个三角形 function triangle(p1,p2,p3){ context.moveTo(p1.x,p1.y); context.lineTo(p2.x,p2.y); context.lineTo(p3.x,p3.y); context.lineTo(p1.x,p1.y); } /*绘制谢尔宾斯基三角形的方法 p:正三角形中心点坐标,len:三角形边长*/ function SierpinskiTriangle(p,len){ var r=len/Math.sqrt(3); //计算顶点坐标 var p1={x:p.x, y:p.y-r}; var p2={x:p.x-len/2, y:p.y+r/2}; var p3={x:p2.x+len, y:p2.y}; triangle(p1,p2,p3); //绘制正三角形外框 //递归绘制所有的倒三角形 middleTriangle(p1,p2,p3); function middleTriangle(p1,p2,p3){ var tp1={x:(p2.x+p3.x)/2, y:(p2.y+p3.y)/2}; var tp2={x:(p1.x+p3.x)/2, y:(p1.y+p3.y)/2}; var tp3={x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2}; triangle(tp1,tp2,tp3); //递归前判断最短线条长度是否短于临界值 if((tp1.x-tp2.x)*(tp1.x-tp2.x)+ (tp1.y-tp2.y)*(tp1.y-tp2.y)>20){ middleTriangle(p1,tp2,tp3); middleTriangle(p2,tp1,tp3); middleTriangle(p3,tp1,tp2); } } } //绘制 SierpinskiTriangle({x:300,y:360},560); context.lineWidth = 0.5; context.strokeStyle = "#F5270B"; context.stroke(); </script> </html>
保存成html文件用浏览器打开,效果如下:
2.折线逼近法:
这个方法比较神奇,简单来说,是从一条水平线开始,每次递归都把所有线段替换成有规律的三段折线。无限递归下去,整段折线会越来越逼近谢尔宾斯基三角形。
用这个思路实现的SierpinskiTriangle函数如下,多了一个设置递归深度的depth参数:
/*绘制谢尔宾斯基三角形的方法 p:正三角形中心点坐标,len:三角形边长,depth:递归深度*/ function SierpinskiTriangle(p,len,depth){ var r=len/Math.sqrt(3); //记录当前端点,默认为左下角顶点坐标 var currentPoint={x:p.x-len/2, y:p.y+r/2}; //记录当前方向角 var currentAngle=0; //旋转方法,将下次画线的方向逆时针旋转 function turn(angle){ currentAngle+=angle; } //画线方法,根据当前端点和画线方向绘制 function draw_line(length){ var angle=currentAngle/180*Math.PI; currentPoint.x+=length*Math.cos(angle); currentPoint.y-=length*Math.sin(angle); context.lineTo(currentPoint.x,currentPoint.y); } //开始画折线,如果深度是偶数便可直接绘制折线,否则需要以斜60度为初始方向 context.moveTo(currentPoint.x,currentPoint.y); if (depth%2==0){ curve(depth,len,-60); }else{ turn(60); curve(depth,len,-60); } function curve(order,length,angle) { if (order==0){ draw_line(length); }else{ //递归画三段折线 curve(order-1,length/2,-angle); turn(angle); curve(order-1,length/2,angle); turn(angle); curve(order-1,length/2,-angle); } } }
用这个函数替换上一段HTML中的SierpinskiTriangle函数就行了,调用时需要给depth参数赋值,推荐为9。我将depth从0到9十种情况的曲线放在同一个canvas里输出了,可供直观理解: