「視覺最大寬度」與其「隨機總長度」成正比。

 讓尖刺的「視覺最大寬度」與其「隨機總長度」成正比。

也就是說,短的尖刺不僅長度短,它在視覺上也應該更細;長的尖刺可以更粗。

修改建議:

假設你有一個「基礎尖刺視覺最大寬度」(baseSpikeVisualMaxWidth),這是當尖刺長度為 baseSpikeLength 時你期望的寬度。

然後,對於每一根具有 currentSpikeTotalLength 的尖刺,計算它自己的 currentSpikeVisualMaxWidth

 // 在你的 drawSpikyBubble 函數中,循環繪製每個尖刺之前
const baseSpikeLength = parseFloat(spikeLenInput.value); // 這是你輸入的基礎長度
const baseSpikeVisualMaxWidth = 5; // << 新增:設定一個基礎視覺寬度,例如5px

// ... 進入 for 循環 ...
for (let i = 0; i < numSpikes; i++) {
    // ... (計算 angle, currentSpikeTotalLength, anchorX/Y, dx/dy, startX/Y, endX/Y 的邏輯不變) ...
    let currentSpikeTotalLength = (Math.random() + 0.2) * baseSpikeLength;
    currentSpikeTotalLength = Math.min(baseSpikeLength, currentSpikeTotalLength);
    currentSpikeTotalLength = Math.max(baseSpikeLength * 0.1, currentSpikeTotalLength); // 確保一個最小長度,例如基礎的10%

    // --- 新增:計算當前尖刺的視覺最大寬度 ---
    let currentSpikeVisualMaxWidth = (currentSpikeTotalLength / baseSpikeLength) * baseSpikeVisualMaxWidth;
    // (可選) 給 currentSpikeVisualMaxWidth 設定一個最小值和最大值
    currentSpikeVisualMaxWidth = Math.max(1, currentSpikeVisualMaxWidth); // 例如,至少1px寬
    currentSpikeVisualMaxWidth = Math.min(baseSpikeVisualMaxWidth, currentSpikeVisualMaxWidth); // 最多不超過基礎視覺寬度

    // ... (anchorX, anchorY, dx, dy, startX, startY, endX, endY 的計算) ...
    // 假設你將 drawTaperedLine 的邏輯直接整合或調用
    // 繪製這一根錐形尖刺,使用 currentSpikeVisualMaxWidth
    // 這是 drawTaperedLine 的核心邏輯,應用到當前尖刺的 centerline (startX,startY) 到 (endX,endY)
    const spikeDX = endX - startX;
    const spikeDY = endY - startY;
    const spikeLen = currentSpikeTotalLength; // Math.sqrt(spikeDX * spikeDX + spikeDY * spikeDY); // 其實就是 currentSpikeTotalLength

    if (spikeLen > 0) { // 避免除以零
        const spikeUX = spikeDX / spikeLen;
        const spikeUY = spikeDY / spikeLen;
        const spikeNX = -spikeUY; // 法向量
        const spikeNY = spikeUX;

        const spikeMidX = (startX + endX) / 2;
        const spikeMidY = (startY + endY) / 2;

        // 構成錐形/菱形的頂點
        const p1x = startX;
        const p1y = startY;
        const p2x = spikeMidX + spikeNX * (currentSpikeVisualMaxWidth / 2);
        const p2y = spikeMidY + spikeNY * (currentSpikeVisualMaxWidth / 2);
        const p3x = endX;
        const p3y = endY;
        const p4x = spikeMidX - spikeNX * (currentSpikeVisualMaxWidth / 2);
        const p4y = spikeMidY - spikeNY * (currentSpikeVisualMaxWidth / 2);

        // 將這些點加入到一個大的路徑中,或者為每個尖刺單獨 beginPath/fill
        // 為了效能,如果尖刺很多,可以考慮將所有尖刺的四邊形都 add 到一個 path,最後 fill
        // 但如果顏色固定,為每個尖刺單獨 fill 也可以
        context.beginPath(); // 為每個尖刺創建一個獨立的路徑
        context.moveTo(p1x, p1y);
        context.lineTo(p2x, p2y);
        context.lineTo(p3x, p3y);
        context.lineTo(p4x, p4y);
        context.closePath();
        context.fillStyle = spikeLineColor; // 使用你設定的尖刺顏色
        context.fill();
    }
}
// ... 結束 for 循環 ...
// 如果之前是將所有尖刺路徑收集起來,則在這裡 fill
// context.fillStyle = spikeLineColor;
// context.fill();
    

修改後的 drawSpikyBubble 函數的整體結構可能像這樣:

      function drawSpikyBubble(context, x, y, width, height, baseSpikeLength, spikeLengthVariation, numSpikes, innerFillColor, spikeLineColor /*, spikeLineWidth (這個可能不需要了) */) {
        const ellipseCenterX = x + width / 2;
        const ellipseCenterY = y + height / 2;
        const radiusX = width / 2;
        const radiusY = height / 2;

        const baseSpikeVisualMaxWidth = 6; // << 新增:設定一個基礎視覺寬度 (例如6px)
                                          // 你可以把它也做成一個輸入參數

        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(); // 如果想一次性 fill 所有尖刺 (但顏色單一)

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

            // 計算當前尖刺的實際總長度 (這是你之前的邏輯,可以保留或調整)
            let currentSpikeTotalLength = (Math.random() * (1 - 0.2) + 0.2) * baseSpikeLength; // 長度在基礎的20%到100%之間
            // currentSpikeTotalLength = Math.min(baseSpikeLength, currentSpikeTotalLength); // 這行其實被上面的重寫了
            currentSpikeTotalLength = Math.max(baseSpikeLength * 0.15, currentSpikeTotalLength); // 確保一個最小長度

            // 計算當前尖刺的視覺最大寬度
            let currentSpikeVisualMaxWidth = (currentSpikeTotalLength / baseSpikeLength) * baseSpikeVisualMaxWidth;
            currentSpikeVisualMaxWidth = Math.max(1.5, currentSpikeVisualMaxWidth); // 至少1.5px寬
            currentSpikeVisualMaxWidth = Math.min(baseSpikeVisualMaxWidth * 1.2, currentSpikeVisualMaxWidth); // 最多不超過基礎寬度的1.2倍 (可選)


            const anchorX = ellipseCenterX + radiusX * Math.cos(angle);
            const anchorY = ellipseCenterY + radiusY * Math.sin(angle);
            const dx = Math.cos(angle);
            const dy = Math.sin(angle);

            const startX = anchorX - dx * (currentSpikeTotalLength / 2);
            const startY = anchorY - dy * (currentSpikeTotalLength / 2);
            const endX = anchorX + dx * (currentSpikeTotalLength / 2);
            const endY = anchorY + dy * (currentSpikeTotalLength / 2);

            // --- 繪製錐形尖刺的邏輯 ---
            const spikeDX = endX - startX;
            const spikeDY = endY - startY;
            const spikeLen = currentSpikeTotalLength;

            if (spikeLen > 0.1) { // 避免長度過小導致問題
                const spikeUX = spikeDX / spikeLen;
                const spikeUY = spikeDY / spikeLen;
                const spikeNX = -spikeUY;
                const spikeNY = spikeUX;

                const spikeMidX = (startX + endX) / 2;
                const spikeMidY = (startY + endY) / 2;

                const p1x = startX;
                const p1y = startY;
                const p2x = spikeMidX + spikeNX * (currentSpikeVisualMaxWidth / 2);
                const p2y = spikeMidY + spikeNY * (currentSpikeVisualMaxWidth / 2);
                const p3x = endX;
                const p3y = endY;
                const p4x = spikeMidX - spikeNX * (currentSpikeVisualMaxWidth / 2);
                const p4y = spikeMidY - spikeNY * (currentSpikeVisualMaxWidth / 2);

                context.beginPath(); // 為每個尖刺獨立路徑和填充
                context.moveTo(p1x, p1y);
                context.lineTo(p2x, p2y);
                context.lineTo(p3x, p3y);
                context.lineTo(p4x, p4y);
                context.closePath();
                context.fillStyle = spikeLineColor;
                context.fill();
            }
        }
        // if (fillingAllAtOnce) {
        //   context.fillStyle = spikeLineColor;
        //   context.fill();
        // }
        context.restore();
    }
    

要點:

  • 引入 baseSpikeVisualMaxWidth 作為參考。

  • 根據 currentSpikeTotalLengthbaseSpikeLength 的比例,計算出 currentSpikeVisualMaxWidth

  • currentSpikeVisualMaxWidth 進行合理的 clamp (限制最小值和最大值)。

  • 使用這個動態計算出來的 currentSpikeVisualMaxWidth 來繪製每個尖刺的四邊形。

這樣修改後,短的尖刺會自然變細,長的尖刺會相對較粗,整體看起來會更協調,應該能解決那個「特別明顯」的線條問題。你需要調整 baseSpikeVisualMaxWidth 以及 currentSpikeVisualMaxWidthMath.maxMath.min 的值,以達到你最滿意的視覺效果。

留言

這個網誌中的熱門文章

Offscreen Canvas

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

計算 Canvas 文字最大字體