Skip to main content

Custom Effects

Introduction#

As we've seen before, templates are powerful tools, but sometimes you might want more control over the effects you create than what the library offers pre-made. This chapter will break down how a template is built, and how you can use that knowledge to create your own effects using emitters, variations and modules.

Example#

To understand how custom effects work, let's break down the confetti template we saw before. Note that some parts are omitted for the sake of brevity.

function confetti(source, options): Emitter {
const populated = party.util.overrideDefaults(
{
modules: [
new ModuleBuilder()
.drive("size")
.by((t) => Math.min(1, t * 3))
.build(),
],
},
options
);
const emitter = party.scene.current.createEmitter({
emitterOptions: {
modules: populated.modules,
},
emissionOptions: {
rate: 0,
bursts: [{ time: 0, count: populated.count }],
source,
angle: party.variation.skew(
-90,
party.variation.evaluateVariation(populated.spread)
),
initialSpeed: populated.speed,
initialLifetime: party.variation.range(6, 8),
},
rendererOptions: {
shapeFactory: populated.shapes,
},
});
return emitter;
}

Breakdown#

Configuration#

Let's break down the effect bit by bit. First, we provide fill in values that the user potentially omitted, to ensure that all the configuration we need has a proper value. For now, let's just focus on the modules.

const populated = party.util.overrideDefaults(
{
modules: [
new ModuleBuilder()
.drive("size")
.by((t) => Math.min(1, t * 3))
.build(),
],
},
options
);

Modules allow particle to change their appearance over their lifetime. How they work is best explained on their own documentation page, but here we are simply creating a new module function that drives the size of the particles over their lifetime.

Creating the emitter#

After we've prepared all the options we need, it's time to finally create the Emitter object. This is the instance that will ultimately be responsible for spawning our confetti-particles.

We use the fact that we can pass every configuration option that we need directly into the method that creates a new emitter in the scene.

const emitter = party.scene.current.createEmitter({
emitterOptions: {
modules: populated.modules,
},
emissionOptions: {
rate: 0,
bursts: [{ time: 0, count: populated.count }],
source,
angle: party.variation.skew(
-90,
party.variation.evaluateVariation(populated.spread)
),
initialSpeed: populated.speed,
initialLifetime: party.variation.range(6, 8),
},
rendererOptions: {
shapeFactory: populated.shapes,
},
});

Note that we're passing variations to some of the options. This is to - as the name might suggest - allow variety in the way that particles are emitted. You can learn more about variations on their documentation page.

The emitter options control what properties the emitted particle are given after they are spawned, things like color, velocity or rotation.

The emission options control the speed at which particles are emitted. The rate option specifies how many particles should be emitted per second, while the bursts option can be used to specify points in time at which groups of particles should be spawned. For the confetti effect, we only want particles to be emitted all at once, so this is fitting.

The shape options specify the area that particles are emitted from, and the direction that they are emitted in. The source is defined by a rectangle.

The renderer options define the way that particles appear on the screen, and how certain properties of them are applied to their looks, for example the lighting and transform.

All done!#

That's all there is to it! If you want to learn more about how to create custom effects, you can check out the other guides.

"Hearts" Demo#

With the knowledge we just gained from learning how custom effects work, we can create more effects, such as the hearts effect seen in the demo.

const heartPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
heartPath.setAttribute(
"d",
"M316.722,29.761c66.852,0,121.053,54.202,121.053,121.041c0,110.478-218.893,257.212-218.893,257.212S0,266.569,0,150.801 C0,67.584,54.202,29.761,121.041,29.761c40.262,0,75.827,19.745,97.841,49.976C240.899,49.506,276.47,29.761,316.722,29.761z"
);
const heartShape = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
heartShape.setAttribute("viewBox", "0 0 512 512");
heartShape.setAttribute("height", "20");
heartShape.setAttribute("width", "20");
heartShape.appendChild(heartPath);
party.scene.current.createEmitter({
emitterOptions: {
loops: 1,
useGravity: false,
modules: [
new party.ModuleBuilder()
.drive("size")
.by((t) => 0.5 + 0.3 * (Math.cos(t * 10) + 1))
.build(),
new party.ModuleBuilder()
.drive("rotation")
.by((t) => new party.Vector(0, 0, 100).scale(t))
.relative()
.build(),
],
},
emissionOptions: {
rate: 0,
bursts: [{ time: 0, count: party.variation.skew(20, 10) }],
sourceSampler: party.sources.dynamicSource(runButton),
angle: party.variation.range(0, 360),
initialSpeed: 400,
initialColor: party.variation.gradientSample(
party.Gradient.simple(
party.Color.fromHex("#ffa68d"),
party.Color.fromHex("#fd3a84")
)
),
},
rendererOptions: {
shapeFactory: heartShape,
applyLighting: undefined,
},
});
You can use the following objects in your code: party, mouseEvent, codeblock, runButton.