唯杰地图 logo唯杰地图
VJMAP
EN

粒子效果使用指南

vjmap3d/references/particle/useguide.md 24.8 KB613 行
原文

直接创建粒子效果

通过App中的addParticle增加粒子效果

示例

const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
app.addParticle({
    system: {
        // 粒子发射周期  发射5秒后进入下一个粒子发射周期。
        duration: 5,
        // 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
        looping: true,
        // 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
        prewarm: false,
        //instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
        // 粒子从发生到消失的时间长短
        startLife: new vjmap3d.IntervalValue(4, 5),
        // 粒子初始发生时候的速度
        startSpeed: new vjmap3d.ConstantValue(1),
        // 粒子初始大小
        startSize: new vjmap3d.IntervalValue(1, 2),
        //startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
        // startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
        // 粒子初始颜色
        startColor: new vjmap3d.ColorRange(
            new THREE.Vector4(0, 1, 1, 1),
            new THREE.Vector4(1, 0, 0, 1)
        ),
        // 世界空间坐标系
        worldSpace: false,
        //maxParticle: 100,
        // 发射频率按时间 1秒发射的数量。
        emissionOverTime: new vjmap3d.ConstantValue(0),
        // 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
        emissionBursts: [
            {
                // 粒子爆发的时间,不能超过粒子系统的Duration
                time: 0,
                // 发射多少个
                count: new vjmap3d.ConstantValue(10),
                // 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
                cycle: 1,
                // 两次循环的间隔时间(⚠️ 同上,未实现)
                interval: 0.01,
                // 每次爆发的可能性
                probability: 1
            }
        ],
        // 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
        // CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
        shape: new vjmap3d.PointEmitter(),
        material: new THREE.MeshBasicMaterial({
            blending: THREE.NormalBlending,
            transparent: true,
            map: texture
            //side: DoubleSide,
        }),
        // 纹理总的图像数里面的位置索引
        startTileIndex: new vjmap3d.ConstantValue(0),
        // 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
        uTileCount: 1,
        // 纹理垂直方向有几张图片
        vTileCount: 1,
        renderOrder: 2,
        //
        // BillBoard = 0, 粒子始终面向摄像机。
        // StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
        // Mesh = 2,
        // Trail = 3,
        // HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
        // VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
        renderMode: vjmap3d.RenderMode.VerticalBillBoard,
        rendererEmitterSettings: {
            // ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
            // 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
            speedFactor: 0,
            // 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
            lengthFactor: 2
        }
    },
    simulations: new vjmap3d.SpeedOverLife(
        new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
    ),
    position: [-5, 0, 0]
});

通过App中的removeParticle删除粒子效果

通过particle.toJSON()导出粒子场景

⚠️ 直接对发射器调用 particle.toJSON() 时,粒子系统的材质/纹理写进了内部临时 meta 后被丢弃,导出的 JSON 不含材质,无法再用 loadParticle/loadParticleFromJson 加载。应包一层 Group 再序列化(直接替换 children,不要用 add()——add 会把发射器从场景中摘下导致粒子停止渲染):

const wrap = new THREE.Group();
wrap.children = [particle];
const json = wrap.toJSON(); // materials/textures/images 都在 json 里
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
let particle = app.addParticle({
    system: {
        // 粒子发射周期  发射5秒后进入下一个粒子发射周期。
        duration: 5,
        // 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
        looping: true,
        // 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
        prewarm: false,
        //instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
        // 粒子从发生到消失的时间长短
        startLife: new vjmap3d.IntervalValue(4, 5),
        // 粒子初始发生时候的速度
        startSpeed: new vjmap3d.ConstantValue(1),
        // 粒子初始大小
        startSize: new vjmap3d.IntervalValue(1, 2),
        //startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
        // startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
        // 粒子初始颜色
        startColor: new vjmap3d.ColorRange(
            new THREE.Vector4(0, 1, 1, 1),
            new THREE.Vector4(1, 0, 0, 1)
        ),
        // 世界空间坐标系
        worldSpace: false,
        //maxParticle: 100,
        // 发射频率按时间 1秒发射的数量。
        emissionOverTime: new vjmap3d.ConstantValue(0),
        // 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
        emissionBursts: [
            {
                // 粒子爆发的时间,不能超过粒子系统的Duration
                time: 0,
                // 发射多少个
                count: new vjmap3d.ConstantValue(10),
                // 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
                cycle: 1,
                // 两次循环的间隔时间(⚠️ 同上,未实现)
                interval: 0.01,
                // 每次爆发的可能性
                probability: 1
            }
        ],
        // 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
        // CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
        shape: new vjmap3d.PointEmitter(),
        material: new THREE.MeshBasicMaterial({
            blending: THREE.NormalBlending,
            transparent: true,
            map: texture
            //side: DoubleSide,
        }),
        // 纹理总的图像数里面的位置索引
        startTileIndex: new vjmap3d.ConstantValue(0),
        // 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
        uTileCount: 1,
        // 纹理垂直方向有几张图片
        vTileCount: 1,
        renderOrder: 2,
        //
        // BillBoard = 0, 粒子始终面向摄像机。
        // StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
        // Mesh = 2,
        // Trail = 3,
        // HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
        // VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
        renderMode: vjmap3d.RenderMode.VerticalBillBoard,
        rendererEmitterSettings: {
            // ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
            // 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
            speedFactor: 0,
            // 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
            lengthFactor: 2
        }
    },
    simulations: new vjmap3d.SpeedOverLife(
        new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
    ),
    position: [-5, 0, 0]
});
const ui = await app.getConfigPane({ title: "粒子", style: { width: "250px"}});
ui.appendChild({
    type: "button",
    label: '导出JSON',
    value: async ()=>{
        // 包一层 Group 序列化,否则导出的 JSON 不含材质(见上文说明)
        const wrap = new THREE.Group();
        wrap.children = [particle];
        vjmap3d.downloadBlob(JSON.stringify(wrap.toJSON(),0,4), "particle.json")
    }
})
ui.appendChild({
    type: "button",
    label: '删除粒子',
    value: async ()=>{
         app.removeParticle(particle);
    }
})

从文件中加载粒子效果

通过App中的loadParticle创建粒子效果

let particle = await app.loadParticle(env.assetsPath + "json/magic_zone.json");

如果需要对加载的数据进行修改,可通过属性进行设置

let particle2 = await app.loadParticle(env.assetsPath + "json/magic_zone.json", {
    // 可对加载的数据进行修改
    onData: data => {
        data.object.children[0].children[0].ps.startColor.color = {r: 0,g: 1,b: 1,a: 1};
        data.object.children[1].children[0].ps.startColor.color = { r: 0,g: 1,b: 1,a: 0.1764706};
        data.object.children[2].ps.startColor.color = { r: 0, g: 1, b: 1, a: 1 };
    },
    position: [-5, 0, 3],
    scale: [1.5, 1.5, 1.5]
});
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let particle = await app.loadParticle(env.assetsPath + "json/magic_zone.json");
let particle2 = await app.loadParticle(env.assetsPath + "json/magic_zone.json", {
    // 可对加载的数据进行修改
    onData: data => {
        data.object.children[0].children[0].ps.startColor.color = {r: 0,g: 1,b: 1,a: 1};
        data.object.children[1].children[0].ps.startColor.color = { r: 0,g: 1,b: 1,a: 0.1764706};
        data.object.children[2].ps.startColor.color = { r: 0, g: 1, b: 1, a: 1 };
    },
    position: [-5, 0, 3],
    scale: [1.5, 1.5, 1.5]
});

从导出的JSON字符串中加载粒子

通过App中的loadParticleFromJson加载JSON字符串加载粒子效果

批量创建粒子效果

通过App中的loadParticleFromRes加载粒子效果资源,再克隆批量创建

let particleRes = await vjmap3d.ResManager.loadRes(
    env.assetsPath + "json/magic_zone.json",
    true,
    "particle"
);
for(let i = 0; i < 20; i++) {
    for(let j = 0; j < 20; j++) {
        let color = vjmap3d.randColor();
        app.loadParticleFromRes(particleRes.clone(), {
            onAddSystem: (obj, system) => {
                // 增加前对每一个粒子效果修改属性
                system.startColor.color.x = color.r;
                system.startColor.color.y = color.g;
                system.startColor.color.z = color.b;
            },
            position: [i * 5 - 50, 0, j * 5- 50],
            scale: Math.random() * 2
        });
    }
}
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let particleRes = await vjmap3d.ResManager.loadRes(
    env.assetsPath + "json/magic_zone.json",
    true,
    "particle"
);
for(let i = 0; i < 20; i++) {
    for(let j = 0; j < 20; j++) {
        let color = vjmap3d.randColor();
        app.loadParticleFromRes(particleRes.clone(), {
            onAddSystem: (obj, system) => {
                // 增加前对每一个粒子效果修改属性
                system.startColor.color.x = color.r;
                system.startColor.color.y = color.g;
                system.startColor.color.z = color.b;
            },
            position: [i * 5 - 50, 0, j * 5- 50],
            scale: Math.random() * 2
        });
    }
}

粒子效果与实体对象相关联

let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
    toEntity: true,
    scale: [3, 3, 3]
});
entity.position.set(-4, 0, 3);
entity.addTo(app);
let particle = await app.loadParticle(env.assetsPath + "json/blue_fire.json", {
    parent: entity.node, // 与实体相关联
    rotation: [0, -Math.PI / 2, 0],
    scale: [0.5, 0.5, 0.5]
});
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
    toEntity: true,
    scale: [3, 3, 3]
});
entity.position.set(-4, 0, 3);
entity.addTo(app);
let particle = await app.loadParticle(env.assetsPath + "json/blue_fire.json", {
    parent: entity.node, // 与实体相关联
    rotation: [0, -Math.PI / 2, 0],
    scale: [0.5, 0.5, 0.5]
});
// 设置一个路径动画
let pathAnmiMod = entity.add(vjmap3d.PathAnimateModule)
pathAnmiMod.start({
    paths: [[-4, 0, 3], [4, 0, 3], [4, 0, -7], [-4, 0, -7], [-4, 0, 3]],
    cameraFollow: false, 
    offsetAngle: 90,
    animation: {
        duration: 15000,
        repeatForever: true
    }
})

文字粒子动画示例

let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let texture = await vjmap3d.ResManager.loadTextureSync(
    env.assetsPath + "textures/texture1.png"
);
let logo_texture1 = await vjmap3d.ResManager.loadTextureSync(
    env.assetsPath + "textures/logo_texture1.png"
);
let logo_texture2 = await vjmap3d.ResManager.loadTextureSync(
    env.assetsPath + "textures/logo_texture2.png"
);
let seq = new vjmap3d.TextureSequencer(0.15, 0.15, new THREE.Vector3(-15, 1, 1));
seq.fromImage(logo_texture1.image, 0.6);
let seq2 = new vjmap3d.TextureSequencer(0.12, 0.12, new THREE.Vector3(-6, 0, 5));
seq2.fromImage(logo_texture2.image, 0.6);
let applySeq = new vjmap3d.ApplySequences(0.001);
applySeq.appendSequencer(new vjmap3d.IntervalValue(1.0, 2.0), seq);
applySeq.appendSequencer(new vjmap3d.IntervalValue(5.0, 6.0), seq2);
//
app.addParticle({
    system: {
        duration: 8,
        looping: true,
        startLife: new vjmap3d.ConstantValue(7.8),
        startSpeed: new vjmap3d.ConstantValue(0),
        startSize: new vjmap3d.IntervalValue(0.01, 0.1),
        startColor: new vjmap3d.RandomColor(
            new THREE.Vector4(0, 1, 1, 1),
            new THREE.Vector4(1, 0, 1, 1)
        ),
        worldSpace: false,
        emissionOverTime: new vjmap3d.ConstantValue(0),
        emissionBursts: [
            {
                time: 0,
                count: new vjmap3d.ConstantValue(1000),
                cycle: 1,
                interval: 1,
                probability: 1
            }
        ],
        shape: new vjmap3d.GridEmitter({ width: 15, height: 15, column: 50, row: 50 }),
        material: new THREE.MeshBasicMaterial({
            map: texture,
            blending: THREE.NormalBlending,
            transparent: true,
            side: THREE.FrontSide
        }),
        renderMode: vjmap3d.RenderMode.BillBoard,
        startTileIndex: new vjmap3d.ConstantValue(2),
        uTileCount: 10,
        vTileCount: 10,
        renderOrder: 0
    },
    simulations: [applySeq]
});

粒子效果配置示例

let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
let options = {
    duration: 5,
    looping: true,
    prewarm: false,
    startSpeed: 1,
    startLifeMin: 4,
    startLifeMax: 5,
    startSizeMin: 1,
    startSizeMax: 2,
    startColorMin: new THREE.Color(0, 1, 1),
    startColorMax: new THREE.Color(1, 0, 0),
    emissionOverTime: 0,
    emissionBursts: {
        // 粒子爆发的时间,不能超过粒子系统的Duration
        time: 0,
        // 发射多少个
        count: 10,
        // 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
        cycle: 1,
        // 两次循环的间隔时间(⚠️ 同上,未实现)
        interval: 0.01,
        // 每次爆发的可能性
        probability: {
            uiConfig: {
                type: "slider",
                label: "可能性",
                value: 1,
                bounds: [0, 1],
                stepSize: 0.01 // 键名是 stepSize(写 step 会被忽略)
            }
        }
    }
}
let particle;
const updateParticle = (opts) => {
    if (particle) app.removeParticle(particle);
    particle = app.addParticle({
        system: {
            // 粒子发射周期  发射5秒后进入下一个粒子发射周期。
            duration: opts.duration,
            // 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
            looping: opts.looping,
            // 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
            prewarm: opts.prewarm,
            //instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
            // 粒子从发生到消失的时间长短
            startLife: new vjmap3d.IntervalValue(opts.startLifeMin, opts.startLifeMax),
            // 粒子初始发生时候的速度
            startSpeed: new vjmap3d.ConstantValue(opts.startSpeed),
            // 粒子初始大小
            startSize: new vjmap3d.IntervalValue(opts.startSizeMin, opts.startSizeMax),
            //startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
            // startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
            // 粒子初始颜色
            startColor: new vjmap3d.ColorRange(
                new THREE.Vector4(opts.startColorMin.r, opts.startColorMin.g, opts.startColorMin.b, 1),
                new THREE.Vector4(opts.startColorMax.r, opts.startColorMax.g, opts.startColorMax.b, 1)
            ),
            // 世界空间坐标系
            worldSpace: false,
            //maxParticle: 100,
            // 发射频率按时间 1秒发射的数量。
            emissionOverTime: new vjmap3d.ConstantValue(opts.emissionOverTime),
            // 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
            emissionBursts: [
                {
                    // 粒子爆发的时间,不能超过粒子系统的Duration
                    time: opts.emissionBursts.time,
                    // 发射多少个
                    count: new vjmap3d.ConstantValue(opts.emissionBursts.count),
                    // 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
                    cycle: opts.emissionBursts.cycle,
                    // 两次循环的间隔时间(⚠️ 同上,未实现)
                    interval: opts.emissionBursts.interval,
                    // 每次爆发的可能性
                    probability: opts.emissionBursts.probability.uiConfig.value
                }
            ],
            // 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
            // CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
            shape: new vjmap3d.PointEmitter(),
            material: new THREE.MeshBasicMaterial({
                blending: THREE.NormalBlending,
                transparent: true,
                map: texture
                //side: DoubleSide,
            }),
            // 纹理总的图像数里面的位置索引
            startTileIndex: new vjmap3d.ConstantValue(0),
            // 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
            uTileCount: 1,
            // 纹理垂直方向有几张图片
            vTileCount: 1,
            renderOrder: 2,
            //
            // BillBoard = 0, 粒子始终面向摄像机。
            // StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
            // Mesh = 2,
            // Trail = 3,
            // HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
            // VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
            renderMode: vjmap3d.RenderMode.VerticalBillBoard,
            rendererEmitterSettings: {
                // ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
                // 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
                speedFactor: 0,
                // 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
                lengthFactor: 2
            }
        },
        simulations: new vjmap3d.SpeedOverLife(
            new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
        ),
        position: [-5, 0, 0]
    });
}
updateParticle(options);
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
let cfg = {
    type: 'folder',
    expanded: true,
    app: app,
    label: "参数",
    children: vjmap3d.generateUiConfig(options),
    onChange: v => {
        console.log(options)
        updateParticle(options)
    }
}
ui.appendChild(cfg)