You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Konva不规则SVG路径内限制火焰粒子效果?

解决Konva粒子在不规则Path内部约束的问题

我完全懂你现在的困扰——明明想让火焰粒子乖乖待在不规则的房间Path里,结果只能用矩形边界凑合,粒子动不动就溢出,试了clipFunc和globalCompositeOperation要么没效果要么出黑块。下面给你两个靠谱的解决方案,附修改后的代码示例:

方案一:用clipFunc实现精确裁剪(首推)

之前用clipFunc没成功,大概率是没同步Room Path的变换属性(比如你设置的scaleX:3.8和scaleY:3.8)。裁剪区域必须和Room的可视形状完全匹配,包括位置、缩放、旋转这些参数,才能把粒子牢牢框住。

修改你的startFire函数如下:

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);

    // 提取Room Path的所有变换参数
    const pathProps = {
        x: room.x(),
        y: room.y(),
        scaleX: room.scaleX(),
        scaleY: room.scaleY(),
        rotation: room.rotation(),
        offsetX: room.offsetX(),
        offsetY: room.offsetY(),
        data: room.attrs.data
    };

    const fireGroup = new Konva.Group({
        name: 'fire-container',
        // 核心:用clipFunc绘制和Room完全一致的路径作为裁剪区域
        clipFunc: (ctx) => {
            ctx.save();
            // 同步Room的所有变换
            ctx.translate(pathProps.x + pathProps.offsetX, pathProps.y + pathProps.offsetY);
            ctx.scale(pathProps.scaleX, pathProps.scaleY);
            ctx.rotate(pathProps.rotation * Math.PI / 180);
            ctx.translate(-pathProps.offsetX, -pathProps.offsetY);
            // 绘制Room的SVG路径
            const clipPath = new Path2D(pathProps.data);
            ctx.fill(clipPath);
            ctx.restore();
        }
    });

    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;
}

原理说明:

  • clipFunc会传入Canvas上下文,我们通过手动复现Room Path的所有变换(平移、缩放、旋转),再绘制相同的SVG路径,让裁剪区域和Room的可视范围完全重合。
  • 用ctx.save()和ctx.restore()隔离上下文状态,避免影响其他元素的绘制。

方案二:用globalCompositeOperation实现遮罩

如果觉得clipFunc太绕,也可以用图层合成的方式,通过遮罩层限制粒子显示范围:

  1. 复制一个和Room完全一样的遮罩层,放在粒子组下方
  2. 给粒子组设置globalCompositeOperation: 'source-in',这样粒子只会显示在遮罩层的区域内

修改后的startFire函数示例:

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);

    // 创建和Room完全一致的遮罩组
    const maskGroup = new Konva.Group({
        x: room.x(),
        y: room.y(),
        scaleX: room.scaleX(),
        scaleY: room.scaleY(),
        rotation: room.rotation(),
        offsetX: room.offsetX(),
        offsetY: room.offsetY(),
    });
    const maskPath = new Konva.Path({
        data: room.attrs.data,
        fill: 'white', // 只要是不透明填充即可,颜色不影响
    });
    maskGroup.add(maskPath);
    layer.add(maskGroup);

    const fireGroup = new Konva.Group({
        name: 'fire-container',
        globalCompositeOperation: 'source-in', // 只显示与下方图层重叠的部分
    });
    layer.add(fireGroup);

    // 确保遮罩在粒子组下方
    maskGroup.moveBelow(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;
    room.maskGroup = maskGroup; // 记录遮罩组,方便清理
}

// 同时修改Clear分支的代码,记得销毁遮罩组:
else if (newStatus==='Clear') {
    window.selectedRoom.stroke('green');
    if (window.selectedRoom.fireAnimation){
        window.selectedRoom.fireAnimation.stop();
        window.selectedRoom.fireGroup.destroy();
        if(window.selectedRoom.maskGroup){
            window.selectedRoom.maskGroup.destroy();
        }
        window.selectedRoom.fireAnimation=null;
        window.selectedRoom.fireGroup = null;
        window.selectedRoom.maskGroup = null;
    }
}

注意事项:

  • source-in的逻辑是:粒子组内容仅显示与下方已绘制区域重叠的部分,所以遮罩层必须在粒子组之前绘制,且填充色要透明。
  • 清理状态时别忘销毁遮罩组,避免内存泄漏。

这两个方案都能解决粒子溢出的问题,方案一性能更优且逻辑直接,推荐优先尝试。

内容的提问来源于stack exchange,提问作者user23537785

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 10:39:07