當前位置: 妍妍網 > 碼農

離譜!產品要求我用 JavaScript 畫一顆【隨機樹】!

2024-06-11碼農

前言

大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心~

用 JavaScript 畫一棵樹?

產品說要讓前端用 JavaScript 畫一棵樹出來,但是這難道不能直接讓 UI 給一張圖片嗎?


後來一問才知道,產品要的是一顆 隨機樹 ,也就是樹的 茂盛程度、長度、枝幹粗細 都是隨機的,那這確實沒辦法叫 UI 給圖,畢竟 UI 不可能給我 10000 張樹的圖片吧?

所以第一時間想到的就是 Canvas ,用它來畫這棵 隨機樹(文末有完整程式碼)

Canvas 畫一顆隨機樹

接下來使用 Canvas 去畫這棵隨機樹

基礎頁面

我們需要在頁面上寫一個 canvas 標簽,並設定好寬高,同時需要獲取它的 Dom 節點、繪制上下文,以便後續的繪制


座標調整

預設的 Canvas 座標系是這樣的


但是我們現在需要從中間去向上去畫一棵樹,所以座標得調整成這樣:

  • X 軸從最上面移動到 最下面

  • Y 軸的方向由往下調整成 往上 ,並且從最左邊移動到 畫布中間


  • 這些操作可以使用 Canvas 的方法

  • ctx.translate: 座標系移動

  • ctx.scale: 座標系縮放


  • 繪制一棵樹的要素

    繪制一棵樹的要素是什麽呢?其實就是 樹枝 果實 ,但是其實 樹枝 才是第一要素,那麽 樹枝 又有哪些要素呢?無非就這幾個點

  • 起始點

  • 樹枝長度、樹枝粗細

  • 生長角度

  • 終點

  • 開始繪制

    所以我們可以寫一個 drawBranch 來進行繪制,並且初始呼叫肯定是繪制 樹幹 ,樹幹的參數如下:

  • 起始點:(0, 0)

  • 樹枝長度、樹枝粗細:這些可以自己自訂

  • 生長角度:90度

  • 終點:需要算


  • 這個終點應該怎麽算呢?其實很簡單,根據 樹枝長度、生長角度 就可以算出來了,這是初高中的知識


    於是我們可以使用 Canvas 的繪制方法,去繪制 線段 ,其實樹枝就是一個一個的 線段


    到現在我繪制出了一個 樹幹 出來

    但是我們是想讓這棵樹開枝散葉,所以需要繼續遞迴繼續去繪制更多的樹枝出來~

    遞迴繪制

    其實往哪開枝散葉呢?無非就是往左或者往右


    所以需要遞迴畫左邊和右邊的樹枝,並且 子樹枝肯定要比父樹枝更短、比父樹枝更細 ,比如我們可以定義一個比例

  • 子樹枝是父樹枝長度的 0.8

  • 子樹枝是父樹枝粗細的 0.75

  • 而子樹枝的生長角度,其實可以隨機,我們可以在 0° - 30° 之間隨機選一個角度,於是增加了遞迴呼叫的程式碼


    但是這個時候會發現,報錯了,爆棧了,因為我們只遞迴開始,但卻沒有在某個時刻遞迴停止

    我們可以自己定義一個停止規則(規則可以自己定義,這會決定你這棵樹的茂盛程度):

  • 粗細小於 2 時馬上停止

  • (粗細小於 10 時 + 隨機數)決定是否停止


  • 現在可以看到我們已經大致繪制出一棵樹了


    不過還少了樹的果實

    繪制果實

    繪制果實很簡單,只需要在繪制樹枝結束的時候,去把果實繪制出來就行,其實果實就是一個個的 白色實心圓


    至此這棵樹完整繪制完畢


    繪制部份的程式碼如下


    結語

    我是林三心

  • 一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;

  • 一個偏前端的全幹工程師;

  • 一個不正經的金塊作者;

  • 逗比的B站up主;

  • 不帥的小紅書博主;

  • 喜歡打鐵的籃球菜鳥;

  • 喜歡歷史的乏味少年;

  • 喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 -->

  • 完整程式碼

    <template>
    <div style="background-color: cadetblue">
    <canvasref="canvasRef"width="1000"height="750"></canvas>
    </div>
    </template>
    <scriptsetuplang="ts">
    import { ref, onMounted } from'vue';
    // 獲取 canvas 的 dom 節點
    const canvasRef = ref<HTMLCanvasElement | null>(null);
    onMounted(() => {
    const canvasEle = canvasRef.value;
    if (!canvasEle) return;
    // 獲取 canvas 上下文
    const ctx = canvasEle.getContext('2d')!;
    // 座標系移動
    ctx.translate(canvasEle.width / 2, canvasEle.height);
    // y軸反向
    ctx.scale(1-1);
    // coordinate 起始點
    // len 樹枝長度
    // thick 樹枝粗細
    // angle 生長角度
    const drawBranch = (coordinate: [number, number], len: number, thick: number, angle: number) => {
    // 繪制結束條件
    if (thick < 10 && Math.random() < 0.1return;
    if (thick < 2) {
    // 繪制果實
    ctx.beginPath();
    ctx.arc(...coordinate, 502 * Math.PI);
    ctx.fill style = '#fff';
    ctx.fill();
    return;
    }
    ctx.beginPath(); // 開啟線段繪制
    ctx.moveTo(...coordinate); // 初始起始點
    // 計算結束點
    const endCoordinate: [number, number] = [
    coordinate[0] + len * Math.cos((angle * Math.PI) / 180),
    coordinate[1] + len * Math.sin((angle * Math.PI) / 180),
    ];
    ctx.lineTo(...endCoordinate); // 線段終點
    ctx.stroke style = '#333'// 線段顏色
    ctx.lineWidth = thick; // 線段粗細
    ctx.lineCap = 'round';
    ctx.stroke(); // 開始畫
    // 左分支
    drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle + Math.random() * 30);
    // 右分支
    drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle - Math.random() * 30);
    };
    // 先畫樹幹
    drawBranch([00], 1002090);
    // // 座標、長度、粗細、角度
    // const drawBranch = (coordinate: [number, number], len: number, thick: number, angle: number) => {
    // if (thick < 10 && Math.random() < 0.1) return;
    // if (thick < 2) {
    // ctx.beginPath();
    // ctx.arc(...coordinate, 5, 0, 2 * Math.PI);
    // ctx.fill style = '#fff';
    // ctx.fill();
    // return;
    // }
    // ctx.beginPath(); // 開啟線段繪制
    // ctx.moveTo(...coordinate); // 初始起始點
    // const endCoordinate: [number, number] = [
    // coordinate[0] + len * Math.cos((angle * Math.PI) / 180),
    // coordinate[1] + len * Math.sin((angle * Math.PI) / 180),
    // ];
    // ctx.lineTo(...endCoordinate); // 線段終點
    // ctx.stroke style = '#333'; // 線段顏色
    // ctx.lineWidth = thick; // 線段粗細
    // ctx.lineCap = 'round';
    // ctx.stroke(); // 開始畫
    // // 左分支
    // drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle + Math.random() * 30);
    // // 右分支
    // drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle - Math.random() * 30);
    // };
    });












    </script>