「驚訝」、「吶喊」或「強調」對話框

 

程式更新為:

function drawSpikyBubble(context, x, y, width, height, baseSpikeLength, spikeLengthVariation, numSpikes, innerFillColor, spikeLineColor, spikeLineWidth = 20.0) {
        const ellipseCenterX = x + width / 2;  // 橢圓的中心 X
        const ellipseCenterY = y + height / 2; // 橢圓的中心 Y
        const radiusX = width / 2;
        const radiusY = height / 2;

        context.save();
        context.clearRect(0, 0, canvas.width, canvas.height);

        // 1. 繪製中心的白色橢圓
        context.beginPath();
        context.ellipse(ellipseCenterX, ellipseCenterY, radiusX, radiusY, 0, 0, 2 * Math.PI);
        context.fillStyle = innerFillColor;
        context.fill();

        // 2. 繪製放射狀的尖刺
        context.beginPath();
        context.strokeStyle = spikeLineColor;
        context.lineWidth = 2;//spikeLineWidth;

        for (let i = 0; i < numSpikes; i++) {
            const angle = (i / numSpikes) * 2 * Math.PI; // 尖刺的方向角度
            // 計算當前尖刺的實際總長度
            // 讓 baseSpikeLength 作為一個平均值,允許在其上下浮動
            // 例如,讓長度在 baseSpikeLength 的 0.5 倍到 1.0 倍之間隨機
            const minLengthFactor = 0.5;
            const maxLengthFactor = 1.0;

            // 生成一個在 [0, 1) 之間的隨機數
            const randomScale = Math.random();

            // 將隨機數映射到 [minLengthFactor, maxLengthFactor] 範圍
            let currentSpikeTotalLength = (minLengthFactor + randomScale * (maxLengthFactor - minLengthFactor)) * baseSpikeLength;

            // (可選) 為了防止出現過於微小的尖刺,可以設定一個絕對的最小像素值
            currentSpikeTotalLength = Math.max(5, currentSpikeTotalLength); // 例如,尖刺至少有3像素長

            // 尖刺在橢圓邊緣上的錨點 (這個點是尖刺的"中心")
            const anchorX = ellipseCenterX + radiusX * Math.cos(angle);
            const anchorY = ellipseCenterY + radiusY * Math.sin(angle);

            // 計算尖刺的起點和終點,使其圍繞錨點對稱分佈
            // 向量 (cos(angle), sin(angle)) 是從橢圓中心指向錨點的方向
            const dx = Math.cos(angle); // 方向向量的 x 分量
            const dy = Math.sin(angle); // 方向向量的 y 分量

            // 起點:從錨點向內移動總長度的一半
            const startX = anchorX - dx * (currentSpikeTotalLength / 2);
            const startY = anchorY - dy * (currentSpikeTotalLength / 2);

            // 終點:從錨點向外移動總長度的一半
            const endX = anchorX + dx * (currentSpikeTotalLength / 2);
            const endY = anchorY + dy * (currentSpikeTotalLength / 2);

            context.moveTo(startX, startY);
            context.lineTo(endX, endY);
        }
        context.stroke();
        context.restore();
    }    


這種漫畫中常見的「驚訝」、「吶喊」或「強調」效果的對話框,在 Canvas 中繪製通常需要組合幾何圖形和大量的線條。

以下是一種實現方法:

  1. 中心橢圓/圓形區域:作為文字的背景,通常是白色。

  2. 放射狀的尖刺線條:從中心橢圓的邊緣向外輻射,形成「爆炸」或「毛刺」效果。

實現步驟與程式碼範例:

假設使用者輸入的是:

  • targetX, targetY: 對話框內容區域 (白色橢圓) 的左上角 bounding box 座標。

  • targetWidth, targetHeight: 對話框內容區域 (白色橢圓) 的寬度和高度。

  • 你可能還需要一些參數來控制尖刺的外觀,例如:

    • spikeLength: 尖刺的長度。

    • numSpikes: 尖刺的數量 (越多越密集)。

    • spikeColor: 尖刺的顏色 (通常是黑色)。

    • fillColor: 中心區域的填充色 (通常是白色)。

      <!DOCTYPE html>
<html>
<head>
<title>Canvas Spiky Speech Bubble</title>
<style>
  body { margin: 20px; display: flex; flex-direction: column; align-items: center; }
  canvas { border: 1px solid black; margin-bottom: 10px; }
  .controls label { display: inline-block; min-width: 100px; }
  .controls input { margin-bottom: 5px; }
</style>
</head>
<body>

<canvas id="myCanvas" width="500" height="400"></canvas>

<div class="controls">
  <div><label for="posX">X:</label><input type="number" id="posX" value="100"></div>
  <div><label for="posY">Y:</label><input type="number" id="posY" value="100"></div>
  <div><label for="pWidth">Width:</label><input type="number" id="pWidth" value="200"></div>
  <div><label for="pHeight">Height:</label><input type="number" id="pHeight" value="120"></div>
  <div><label for="spikeLen">Spike Length:</label><input type="number" id="spikeLen" value="40"></div>
  <div><label for="numSpikes">Num Spikes:</label><input type="number" id="numSpikes" value="300"></div>
  <div><label for="spikeColor">Spike Color:</label><input type="color" id="spikeColor" value="#000000"></div>
  <div><label for="fillColor">Fill Color:</label><input type="color" id="fillColor" value="#FFFFFF"></div>
  <button id="drawButton">Draw</button>
</div>

<script>
    const canvas = document.getElementById("myCanvas");
    const ctx = canvas.getContext("2d");

    const posXInput = document.getElementById('posX');
    const posYInput = document.getElementById('posY');
    const pWidthInput = document.getElementById('pWidth');
    const pHeightInput = document.getElementById('pHeight');
    const spikeLenInput = document.getElementById('spikeLen');
    const numSpikesInput = document.getElementById('numSpikes');
    const spikeColorInput = document.getElementById('spikeColor');
    const fillColorInput = document.getElementById('fillColor');
    const drawButton = document.getElementById('drawButton');

    /**
     * 繪製帶尖刺的對話框
     * @param {CanvasRenderingContext2D} context
     * @param {number} x - 內部橢圓 bounding box 的左上角 x
     * @param {number} y - 內部橢圓 bounding box 的左上角 y
     * @param {number} width - 內部橢圓的寬度
     * @param {number} height - 內部橢圓的高度
     * @param {number} spikeLength - 尖刺的長度
     * @param {number} numSpikes - 尖刺的數量
     * @param {string} innerFillColor - 內部橢圓的填充色
     * @param {string} spikeLineColor - 尖刺線條的顏色
     * @param {number} spikeLineWidth - 尖刺線條的寬度 (設小一點,例如 0.5 或 1)
     */
    function drawSpikyBubble(context, x, y, width, height, spikeLength, numSpikes, innerFillColor, spikeLineColor, spikeLineWidth = 1) {
        const centerX = x + width / 2;
        const centerY = y + height / 2;
        const radiusX = width / 2;
        const radiusY = height / 2;

        // 儲存當前狀態
        context.save();
        context.clearRect(0, 0, canvas.width, canvas.height); // 清除畫布

        // 1. 繪製中心的白色橢圓 (作為文字背景)
        context.beginPath();
        context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
        context.fillStyle = innerFillColor;
        context.fill();
        // (可選) 如果內部橢圓也需要邊框
        // context.strokeStyle = "gray";
        // context.lineWidth = 1;
        // context.stroke();

        // 2. 繪製放射狀的尖刺
        context.beginPath(); // 開始一個新的路徑來畫所有的尖刺線條
        context.strokeStyle = spikeLineColor;
        context.lineWidth = spikeLineWidth;

        for (let i = 0; i < numSpikes; i++) {
            const angle = (i / numSpikes) * 2 * Math.PI; // 0 到 2π 的角度

            // 尖刺在橢圓邊緣上的起點
            const startX = centerX + radiusX * Math.cos(angle);
            const startY = centerY + radiusY * Math.sin(angle);

            // 尖刺的終點 (從中心沿角度向外延伸)
            // 這裡我們假設尖刺是從橢圓邊緣向外固定長度
            // 更精確的做法是讓尖刺的方向垂直於橢圓在該點的切線,但對於這種效果,直接從中心放射通常足夠
            const endX = centerX + (radiusX + spikeLength) * Math.cos(angle);
            const endY = centerY + (radiusY + spikeLength) * Math.sin(angle);

            context.moveTo(startX, startY);
            context.lineTo(endX, endY);
        }
        context.stroke(); // 一次性描邊所有尖刺線條

        context.restore(); // 恢復之前儲存的狀態
    }

    function redraw() {
        drawSpikyBubble(
            ctx,
            parseFloat(posXInput.value),
            parseFloat(posYInput.value),
            parseFloat(pWidthInput.value),
            parseFloat(pHeightInput.value),
            parseFloat(spikeLenInput.value),
            parseInt(numSpikesInput.value, 10),
            fillColorInput.value,
            spikeColorInput.value,
            0.7 // 可以調整尖刺線條的寬度
        );
    }

    // 初始繪製
    redraw();

    // 為所有輸入框添加事件監聽器以重新繪製
    [posXInput, posYInput, pWidthInput, pHeightInput, spikeLenInput, numSpikesInput, spikeColorInput, fillColorInput].forEach(input => {
        input.addEventListener('input', redraw);
    });
    drawButton.addEventListener('click', redraw);

</script>

</body>
</html>
    

程式碼解釋:

  1. HTML 控制項:提供輸入框讓使用者設定對話框的各種參數。

  2. drawSpikyBubble 函數

    • 接收 context (Canvas 的 2D 繪圖環境) 和各種參數。

    • 計算橢圓的中心點 (centerX, centerY) 和半徑 (radiusX, radiusY)。

    • 繪製中心橢圓

      • 使用 ctx.ellipse() 繪製一個橢圓。

      • innerFillColor (通常是白色) 填充它。

    • 繪製尖刺

      • 開始一個新的路徑 (ctx.beginPath())。這樣可以將所有尖刺線條收集起來,最後用一次 ctx.stroke() 繪製,效能較好。

      • 迴圈 numSpikes 次,每次計算一個角度 angle

      • startX, startY:根據當前角度計算尖刺在橢圓邊緣上的起點。

      • endX, endY:根據當前角度和 spikeLength 計算尖刺的終點。這裡的計算方式是讓尖刺從橢圓中心點沿著角度向外延伸,經過橢圓邊緣後再延伸 spikeLength 的距離。

      • ctx.moveTo(startX, startY)ctx.lineTo(endX, endY):定義一條尖刺線段。

      • 迴圈結束後,ctx.stroke() 將所有定義的線段描邊繪製出來。

    • spikeLineWidth:控制每根尖刺的線條寬度。如果尖刺數量很多,設小一點 (例如 0.5 到 1) 效果會比較好,線條會顯得更密集,形成類似圖片中的效果。

如何調整以達到圖片效果:

  • numSpikes (尖刺數量):圖片中的尖刺非常密集,所以你需要一個較大的值,例如 200、300 甚至更多。

  • spikeLength (尖刺長度):根據你想要的「爆炸」範圍來調整。

  • spikeLineWidth (尖刺線寬):設為一個較小的值,比如 0.50.71。如果線寬太大,尖刺之間可能不會有圖片中那種細密的感覺。

  • widthheight:決定了中心白色區域的大小和形狀。圖片中的看起來更接近圓形,所以 widthheight 可以設得比較接近。

你可以玩一下提供的 HTML 範例中的控制項,看看不同參數如何影響最終的圖形。這個方法可以很好地模擬出你圖片中的效果。

留言

這個網誌中的熱門文章

Offscreen Canvas

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

計算 Canvas 文字最大字體