文章首發於我的知乎專欄,原地址: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里輸出了,可供直觀理解:
參考來源:
Sierpinski triangle
Sierpiński arrowhead curve