在Konva的 SVG路径中加入粒子效果

前端开发 2026-07-12

我已经创建了一个火焰粒子效果,但无法把它包含在Konva路径内部(因为路径是非规则形状)。我所能做到的最佳效果是在路径周围画一个矩形,但这会导致粒子溢出矩形之外。这些是SVG路径,叠加在一张图片上。

当前的代码是我目前能用的解决方案,直到我能让它工作为止,它只是把圆放在路径(或区域)的中心,并从那里生成粒子。我尝试了关于在fireGroup内使用clipFunc,或globalCompositeOperation('source-in') 的各种建议。最好的情况是没有任何变化,最坏的情况是它会在路径上方或整屏上方创建一个黑色方块。

我希望有人知道一些我可以参考的资源,或给出任何建议来让它工作。我非常感激!!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Path Test</title>
    <script src="MapData.js"></script>
    <script src="konva.js"></script>
    <script src="baseImage.js"></script>
    <link rel="stylesheet" href="controlpanel.css">

    <script src = ""></script>
    <style>
        body {margin: 0; background:#ffffff; display: flex; justify-content: center; font-family:'Courier New', monospace}
        #container {width: 1462; height: 894;background-color:#333;}
    </style>
</head>

<body>
    <div id="container"></div>
    <div id="control-panel">
        <h3 id="room-name">Click on a room.</h3>
        <button onclick="changeStatus('Fire')">Fire</button>
        <button onclick="changeStatus('Clear')">Clear</button>
    </div>
    <script>


        const stage = new Konva.Stage({
            container: 'container',width: 1462,height: 894
        });

        const layer = new Konva.Layer();
        stage.add(layer);

        const imageObj = new Image();
        imageObj.src = baseImage;
        imageObj.onload = function(){
            const kImg = new Konva.Image({
                x: 0, y: 0,
                image: imageObj,
                width: 1462, height: 894
            });
            layer.add(kImg);
            kImg.moveToBottom();
        }

        function loadSectors(){
            const parser = new DOMParser();
            const svgDoc = parser.parseFromString(siteSVG, 'image/svg+xml');    
            const paths = svgDoc.querySelectorAll('path');

            Array.from(paths).forEach(p=> {
                const room = new Konva.Path({
                    data: p.getAttribute('d'),
                    fill: 'rgba(102,252,241,0.2)',
                    stroke: 'green',
                    strokewidth: 1,
                    name: p.getAttribute('title'),
                    scaleX: 3.8, scaleY: 3.8,
                    strokeScaleEnabled: false,
                    listening: true
                });

                room.on('mouseenter', ()=> {
                    room.fill('rgba(102,55,241,0.2)');
                    layer.draw();
                });

                room.on('mouseleave', ()=> {
                    room.fill('rgba(102,252,241,0.2)');
                    layer.draw();
                });
                room.on('click', ()=> {
                document.getElementById('room-name').innerText=room.name();
                window.selectedRoom=room;
            })
                layer.add(room);
            });



        layer.draw();
    }

    loadSectors();

    let selectedRoom=null;

    function changeStatus(newStatus){
        if (!window.selectedRoom) {
            alert("Select a room first");
            return;
        }
        if (newStatus==='Fire'){
            startFire(window.selectedRoom);
            window.selectedRoom.stroke('red');
        } else if (newStatus==='Clear')
        {
                window.selectedRoom.stroke('green');
            if (window.selectedRoom.fireAnimation){
                window.selectedRoom.fireAnimation.stop();
                window.selectedRoom.fireGroup.destroy();
                window.selectedRoom.fireAnimation=null;
                window.selectedRoom.fireGroup = null;
            }
        }
        layer.draw()

    }

    function startFire(room){
        const box = room.getClientRect();
        const layer = room.getLayer();
        const centerX = box.x + (box.width / 2);
        const centerY = box.y + (box.height / 2);

        const fireGroup = new Konva.Group({
            name: 'fire-container',
        });
        layer.add(fireGroup);

        const anim = new Konva.Animation((frame) => {
            if (Math.random() > 0.1) {
                const particle= new Konva.Circle({
                    x: centerX + (Math.random() * 20 - 10),
                    y: centerY + (Math.random() * 20 - 10),
                    radius: Math.random() * 3 + 1,
                    fill: Math.random() > 0.5 ? 'orange' : 'red',
                    opacity: 0.8,
                });
                fireGroup.add(particle);
            }

            fireGroup.getChildren().forEach(p=> {
                p.y(p.y() - 1.5);
                p.opacity(p.opacity() - 0.015);
                if (p.opacity() <= 0){
                    p.destroy();
                }
            });
        }, layer);
        anim.start();

        room.fireAnimation = anim;
        room.fireGroup = fireGroup;
    }


    </script>
</body>

</html>

解决方案

我相信我已经实现了你想要的效果,当你创建你的 fireGroup 时,将 clipFunc 设置为以下函数:

function clipFuncFromPath(shape) {
  // All path commands are converted to A, Q, C, L, M or z
  // See https://github.com/konvajs/konva/blob/9ebdeba344d9ce294dab9d47ba9fd39691e38738/src/shapes/Path.ts#L442
  const cmdToFuncMap = {
    A: 'arcTo',
    Q: 'quadraticCurveTo',
    C: 'bezierCurveTo',
    L: 'lineTo',
    M: 'moveTo',
    z: 'closePath',
  };
  return (ctx) => {
    ctx.save();
    ctx.transform(...shape.getTransform().m);
    ctx.beginPath();
    let closed = false;
    for (const { command, points, start } of shape.dataArray) {
      if (closed && command !== 'z') {
        ctx.moveTo(start.x, start.y);
        closed = false;
      }

      ctx[cmdToFuncMap[command]](...points);

      if (command === 'z') {
        closed = true;
      }
    }
    ctx.closePath();
    ctx.clip();
    ctx.restore();
  };
}

请参阅 这个fiddle 以获得一个工作示例。

注:我对Konva或画布不是很熟悉,所以可能有更好的实现方式。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章