模擬紡綞狀筆刷. 2端細中間粗

 好問題!HTML Canvas 的 ctx.lineWidth 屬性設定的線條寬度是均勻的,即線條從頭到尾都是一樣粗。Canvas 本身沒有內建的「兩端比較細,中間比較粗」的筆刷類型或直接的屬性可以實現這種錐形/紡錘形線條。

但是,你可以透過一些技巧來模擬這種效果:

主要方法:將線條視為一個填充的形狀 (Path)

你不再是畫一條「線」(stroke),而是畫一個細長的、兩端尖銳的封閉「面」(fill)。

  1. 使用多個 lineTo 繪製一個細長的多邊形 (最簡單的錐形):

    • 想像你的線條是從 (x1, y1)(x2, y2)

    • 它的中間最寬處是 maxWidth

    • 它的兩端寬度是 minWidth (通常是 0 或接近 0)。

    • 你需要計算出構成這個錐形(或紡錘形)的至少 4 個頂點,然後用 fill() 填充。

    範例:畫一個兩端尖銳,中間最寬的直線

          function drawTaperedLine(ctx, x1, y1, x2, y2, maxWidth, color = 'black') {
        const dx = x2 - x1;
        const dy = y2 - y1;
        const len = Math.sqrt(dx * dx + dy * dy);
        if (len === 0) return;
    
        // 計算線條方向的單位向量
        const ux = dx / len;
        const uy = dy / len;
    
        // 計算垂直於線條方向的單位向量 (法向量)
        const nx = -uy;
        const ny = ux;
    
        // 中點
        const midX = (x1 + x2) / 2;
        const midY = (y1 + y2) / 2;
    
        // 計算錐形/紡錘形的4個頂點
        // 頂點1: 起點
        const p1x = x1;
        const p1y = y1;
    
        // 頂點2: 中點沿法向量偏移 maxWidth/2
        const p2x = midX + nx * (maxWidth / 2);
        const p2y = midY + ny * (maxWidth / 2);
    
        // 頂點3: 終點
        const p3x = x2;
        const p3y = y2;
    
        // 頂點4: 中點沿反法向量偏移 maxWidth/2
        const p4x = midX - nx * (maxWidth / 2);
        const p4y = midY - ny * (maxWidth / 2);
    
        ctx.beginPath();
        ctx.moveTo(p1x, p1y);
        ctx.lineTo(p2x, p2y);
        ctx.lineTo(p3x, p3y);
        ctx.lineTo(p4x, p4y);
        ctx.closePath(); // 閉合路徑形成一個四邊形
    
        ctx.fillStyle = color;
        ctx.fill();
    }
    
    // 使用範例:
    // const canvas = document.getElementById('myCanvas');
    // const ctx = canvas.getContext('2d');
    // drawTaperedLine(ctx, 50, 50, 250, 100, 10, 'blue'); // 從(50,50)到(250,100),中間最寬10px
    // drawTaperedLine(ctx, 50, 150, 250, 200, 5, 'red');
        

    這個方法會畫出一個菱形的錐形線條。

  2. 使用 quadraticCurveTobezierCurveTo 繪製更平滑的紡錘形:
    這種方法可以畫出更像毛筆筆觸的效果,線條邊緣是平滑的曲線。
    你需要定義兩條相對的貝茲曲線或二次貝茲曲線,它們在起點和終點相交(或非常接近)。

    概念(較複雜,需要仔細計算控制點):

          function drawSmoothTaperedLine(ctx, x1, y1, x2, y2, maxWidth, color = 'black') {
        const dx = x2 - x1;
        const dy = y2 - y1;
        const len = Math.sqrt(dx * dx + dy * dy);
        if (len === 0) return;
    
        const ux = dx / len;
        const uy = dy / len;
        const nx = -uy;
        const ny = ux;
    
        const midX = (x1 + x2) / 2;
        const midY = (y1 + y2) / 2;
    
        // 計算控制點的偏移量,使其在中間形成 bulge
        const controlOffsetFactor = 0.5; // 可調整此因子來改變曲線的"胖瘦"
    
        ctx.beginPath();
        ctx.moveTo(x1, y1); // 起點
    
        // 第一條曲線 (一側)
        const cp1x_1 = midX + nx * (maxWidth / 2) - ux * len * controlOffsetFactor;
        const cp1y_1 = midY + ny * (maxWidth / 2) - uy * len * controlOffsetFactor;
        // 如果使用 quadraticCurveTo,只需要一個控制點,可以簡化為中點附近向外偏移
        // 例如: const qcp1x = midX + nx * (maxWidth * 0.8); // 稍微誇張一點的控制點
        //       const qcp1y = midY + ny * (maxWidth * 0.8);
        // ctx.quadraticCurveTo(qcp1x, qcp1y, x2, y2);
    
        // 為了更像紡錘形,我們可能需要兩個控制點或更複雜的計算
        // 簡化版:用兩段二次貝茲曲線模擬一邊
        const bulgePoint1X = midX + nx * (maxWidth / 2);
        const bulgePoint1Y = midY + ny * (maxWidth / 2);
        ctx.quadraticCurveTo(x1 + dx * 0.25 + nx * (maxWidth * 0.3), y1 + dy * 0.25 + ny * (maxWidth * 0.3), bulgePoint1X, bulgePoint1Y);
        ctx.quadraticCurveTo(x2 - dx * 0.25 + nx * (maxWidth * 0.3), y2 - dy * 0.25 + ny * (maxWidth * 0.3), x2, y2);
    
    
        // 第二條曲線 (另一側,回到起點)
        const bulgePoint2X = midX - nx * (maxWidth / 2);
        const bulgePoint2Y = midY - ny * (maxWidth / 2);
        ctx.quadraticCurveTo(x2 - dx * 0.25 - nx * (maxWidth * 0.3), y2 - dy * 0.25 - ny * (maxWidth * 0.3), bulgePoint2X, bulgePoint2Y);
        ctx.quadraticCurveTo(x1 + dx * 0.25 - nx * (maxWidth * 0.3), y1 + dy * 0.25 - ny * (maxWidth * 0.3), x1, y1);
    
    
        ctx.closePath();
        ctx.fillStyle = color;
        ctx.fill();
    }
    // 使用範例:
    // drawSmoothTaperedLine(ctx, 50, 250, 250, 300, 20, 'green');
        

    注意: drawSmoothTaperedLine 的控制點計算是比較棘手的,上面的範例是一個簡化的嘗試,可能需要仔細調整控制點位置才能達到理想的平滑紡錘形。更精確的毛筆效果通常需要更複雜的算法。

  3. 逐點繪製不同寬度的短線段或圓點 (模擬筆刷壓力變化):

    • 將你的長線條分解成許多非常短的小線段。

    • 為每個小線段計算一個寬度 (例如,使用 sin 函數或一個預設的壓力曲線,使得中間的線段寬,兩端的線段窄)。

    • 使用 ctx.lineWidth 設定當前小線段的寬度。

    • 使用 ctx.lineCap = 'round' 可以讓這些短線段連接起來更平滑。

    • 畫出這個短線段。

    • 這種方法計算量較大,但能提供最大的靈活性。

          function drawPressureLine(ctx, x1, y1, x2, y2, maxLineWidth, segments = 50, color = 'black') {
        ctx.beginPath();
        ctx.lineCap = 'round'; // 讓線段末端變圓,連接更平滑
        ctx.lineJoin = 'round';
        ctx.strokeStyle = color;
    
        for (let i = 0; i <= segments; i++) {
            const t = i / segments; // 參數 t 從 0 到 1
    
            // 計算當前點的位置
            const currentX = x1 + (x2 - x1) * t;
            const currentY = y1 + (y2 - y1) * t;
    
            // 計算當前點的線條寬度 (例如,使用 sin 函數形成中間粗兩端細)
            // Math.sin(t * Math.PI) 在 t=0 和 t=1 時為 0, 在 t=0.5 時為 1
            const currentWidth = Math.sin(t * Math.PI) * maxLineWidth;
            ctx.lineWidth = Math.max(0.1, currentWidth); // 確保線寬不為0或負
    
            if (i === 0) {
                ctx.moveTo(currentX, currentY);
            } else {
                // 如果 lineWidth 變化很快,直接 lineTo 可能不夠平滑
                // 一個改進是用很多小圓點填充
                // ctx.beginPath();
                // ctx.arc(currentX, currentY, currentWidth / 2, 0, Math.PI * 2);
                // ctx.fillStyle = color;
                // ctx.fill();
                // 但這裡我們先嘗試用 lineTo
                ctx.lineTo(currentX, currentY);
            }
        }
        ctx.stroke(); // 一次性描邊整個路徑
    }
    // 使用範例
    // drawPressureLine(ctx, 300, 50, 450, 150, 15, 100, 'purple');
        

    注意: drawPressureLine 如果線寬變化劇烈,單純用 lineTostroke 可能在連接處不夠平滑,用小圓點填充會更好,但效能開銷更大。

對於你之前那個「爆炸框」的尖刺:

如果你想讓那些尖刺也帶有這種錐形效果,你需要對每一根尖刺都使用上述方法之一來繪製,而不是簡單的 ctx.lineTo()ctx.stroke()。這會顯著增加計算量。

總結:

  • 最簡單直接的錐形: 使用方法 1 (繪製四邊形並填充)。

  • 更平滑的筆觸感: 方法 2 (使用貝茲曲線填充),但控制點計算複雜。

  • 模擬筆刷壓力: 方法 3 (逐段改變 lineWidth),靈活但可能需要更多調整以達到平滑效果。

選擇哪種方法取決於你需要的視覺效果精細度和可接受的複雜度/效能開銷。對於漫畫中的尖刺,如果數量很多,方法 1 可能是效能和效果的一個較好平衡點。

留言

這個網誌中的熱門文章

Offscreen Canvas

外部網頁新增 Google 日曆行程(URL模板)

計算 Canvas 文字最大字體