11 Skia’s particle module provides a way to quickly generate large numbers of
12 drawing primitives with dynamic, animated behavior. Particles can be used to
13 create effects like fireworks, spark trails, ambient “weather”, and much more.
14 Nearly all properties and behavior are controlled by scripts written in Skia’s
15 custom language, SkSL.
21 border: 1px dashed #AAA;
26 display: inline-block;
37 <canvas id=trail width=400 height=400></canvas>
39 Trail (Click and Drag!)
43 <canvas id=cube width=400 height=400></canvas>
45 <a href="https://particles.skia.org/?nameOrHash=@cube"
46 target=_blank rel=noopener>Cuboid</a>
50 <canvas id=confetti width=400 height=400></canvas>
52 <a href="https://particles.skia.org/?nameOrHash=@confetti"
53 target=_blank rel=noopener>Confetti</a>
57 <canvas id=curves width=400 height=400></canvas>
59 <a href="https://particles.skia.org/?nameOrHash=@swirl"
60 target=_blank rel=noopener>Curves</a>
64 <canvas id=fireworks width=400 height=400></canvas>
66 <a href="https://particles.skia.org/?nameOrHash=@fireworks"
67 target=_blank rel=noopener>Fireworks</a>
71 <canvas id=text width=400 height=400></canvas>
73 <a href="https://particles.skia.org/?nameOrHash=@text"
74 target=_blank rel=noopener>Text</a>
80 <script type="text/javascript" charset="utf-8">
82 // Tries to load the WASM version if supported, shows error otherwise
83 let s = document.createElement('script');
85 if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
86 console.log('WebAssembly is supported!');
87 locate_file = 'https://particles.skia.org/dist/';
89 console.log('WebAssembly is not supported (yet) on this browser.');
90 document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>";
93 s.src = locate_file + 'canvaskit.js';
97 locateFile: (file) => locate_file + file,
100 TrailExample(CanvasKit, 'trail', trail);
101 ParticleExample(CanvasKit, 'confetti', confetti, 200, 200);
102 ParticleExample(CanvasKit, 'curves', curves, 200, 300);
103 ParticleExample(CanvasKit, 'cube', cube, 200, 200);
104 ParticleExample(CanvasKit, 'fireworks', fireworks, 200, 300);
105 ParticleExample(CanvasKit, 'text', text, 75, 250);
108 function ParticleExample(CanvasKit, id, jsonData, cx, cy) {
109 if (!CanvasKit || !jsonData) {
112 const surface = CanvasKit.MakeCanvasSurface(id);
114 console.error('Could not make surface');
117 const canvas = surface.getCanvas();
118 canvas.translate(cx, cy);
120 const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
121 particles.start(Date.now() / 1000.0, true);
123 function drawFrame(canvas) {
124 particles.update(Date.now() / 1000.0);
126 canvas.clear(CanvasKit.WHITE);
127 particles.draw(canvas);
128 surface.requestAnimationFrame(drawFrame);
130 surface.requestAnimationFrame(drawFrame);
136 "Type": "SkCircleDrawable",
140 "void effectSpawn(inout Effect effect) {",
141 " effect.lifetime = 2;",
144 "void effectUpdate(inout Effect effect) {",
145 " if (effect.age < 0.25 || effect.age > 0.75) { effect.rate = 0; }",
146 " else { effect.rate = 200; }",
149 "void spawn(inout Particle p) {",
150 " int idx = int(rand(p.seed) * 4);",
151 " p.color.rgb = (idx == 0) ? float3(0.87, 0.24, 0.11)",
152 " : (idx == 1) ? float3(1.00, 0.90, 0.20)",
153 " : (idx == 2) ? float3(0.44, 0.73, 0.24)",
154 " : float3(0.38, 0.54, 0.95);",
156 " p.lifetime = (1 - effect.age) * effect.lifetime;",
157 " p.scale = mix(0.6, 1, rand(p.seed));",
160 "void update(inout Particle p) {",
161 " p.color.a = 1 - p.age;",
163 " float a = radians(rand(p.seed) * 360);",
164 " float invAge = 1 - p.age;",
165 " p.vel = float2(cos(a), sin(a)) * mix(250, 550, rand(p.seed)) * invAge * invAge;",
175 "Type": "SkCircleDrawable",
179 "void effectSpawn(inout Effect effect) {",
180 " effect.lifetime = 2;",
181 " effect.rate = 200;",
184 "void spawn(inout Particle p) {",
188 "float4x4 rx(float rad) {",
189 " float c = cos(rad);",
190 " float s = sin(rad);",
191 " return float4x4(1, 0, 0, 0,",
197 "float4x4 ry(float rad) {",
198 " float c = cos(rad);",
199 " float s = sin(rad);",
200 " return float4x4(c, 0, -s, 0,",
206 "float4x4 rz(float rad) {",
207 " float c = cos(rad);",
208 " float s = sin(rad);",
209 " return float4x4( c, s, 0, 0,",
215 "void update(inout Particle p) {",
216 " float3 pos = float3(rand(p.seed), rand(p.seed), rand(p.seed));",
217 " if (rand(p.seed) < 0.33) {",
218 " if (pos.x > 0.5) {",
220 " p.color.rgb = float3(1, 0.2, 0.2);",
223 " p.color.rgb = float3(0.2, 1, 1);",
225 " } else if (rand(p.seed) < 0.5) {",
226 " if (pos.y > 0.5) {",
228 " p.color.rgb = float3(0.2, 0.2, 1);",
231 " p.color.rgb = float3(1, 1, 0.2);",
234 " if (pos.z > 0.5) {",
236 " p.color.rgb = float3(0.2, 1, 0.2);",
239 " p.color.rgb = float3(1, 0.2, 1);",
243 " float s = effect.age * 2 - 1;",
244 " s = s < 0 ? -s : s;",
246 " pos = pos * 2 - 1;",
247 " pos = mix(pos, normalize(pos), s);",
250 " float age = float(effect.loop) + effect.age;",
251 " float4x4 mat = rx(age * radians(60))",
252 " * ry(age * radians(70))",
253 " * rz(age * radians(80));",
254 " pos = (mat * float4(pos, 1)).xyz;",
258 " p.scale = ((pos.z + 50) / 100 + 0.5) / 2;",
268 "Type": "SkCircleDrawable",
272 "void effectSpawn(inout Effect effect) {",
273 " effect.rate = 200;",
274 " effect.color = float4(1, 0, 0, 1);",
277 "void spawn(inout Particle p) {",
278 " p.lifetime = 3 + rand(p.seed);",
282 "void update(inout Particle p) {",
283 " float w = mix(15, 3, p.age);",
284 " p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand(p.seed));",
285 " if (rand(p.seed) < 0.5) { p.pos.x = -p.pos.x; }",
287 " p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;",
297 "Type": "SkCircleDrawable",
301 "void effectSpawn(inout Effect effect) {",
302 " // Phase one: Launch",
303 " effect.lifetime = 4;",
304 " effect.rate = 120;",
305 " float a = radians(mix(-20, 20, rand(effect.seed)) - 90);",
306 " float s = mix(200, 220, rand(effect.seed));",
307 " effect.vel.x = cos(a) * s;",
308 " effect.vel.y = sin(a) * s;",
309 " effect.color.rgb = float3(rand(effect.seed), rand(effect.seed), rand(effect.seed));",
310 " effect.pos.x = 0;",
311 " effect.pos.y = 0;",
312 " effect.scale = 0.25; // Also used as particle behavior flag",
315 "void effectUpdate(inout Effect effect) {",
316 " if (effect.age > 0.5 && effect.rate > 0) {",
317 " // Phase two: Explode",
319 " effect.burst = 50;",
320 " effect.scale = 1;",
322 " effect.vel.y += dt * 90;",
326 "void spawn(inout Particle p) {",
327 " bool explode = p.scale == 1;",
329 " p.lifetime = explode ? (2 + rand(p.seed) * 0.5) : 0.5;",
330 " float a = radians(rand(p.seed) * 360);",
331 " float s = explode ? mix(90, 100, rand(p.seed)) : mix(5, 10, rand(p.seed));",
332 " p.vel.x = cos(a) * s;",
333 " p.vel.y = sin(a) * s;",
336 "void update(inout Particle p) {",
337 " p.color.a = 1 - p.age;",
338 " if (p.scale == 1) {",
339 " p.vel.y += dt * 50;",
350 "Type": "SkCircleDrawable",
354 "void effectSpawn(inout Effect effect) {",
355 " effect.rate = 1000;",
358 "void spawn(inout Particle p) {",
359 " p.lifetime = mix(1, 3, rand(p.seed));",
360 " float a = radians(mix(250, 290, rand(p.seed)));",
361 " float s = mix(10, 30, rand(p.seed));",
362 " p.vel.x = cos(a) * s;",
363 " p.vel.y = sin(a) * s;",
364 " p.pos = text(rand(p.seed)).xy;",
367 "void update(inout Particle p) {",
368 " float4 startColor = float4(1, 0.196, 0.078, 1);",
369 " float4 endColor = float4(1, 0.784, 0.078, 1);",
370 " p.color = mix(startColor, endColor, p.age);",
376 "Type": "SkTextBinding",
384 function preventScrolling(canvas) {
385 canvas.addEventListener('touchmove', (e) => {
386 // Prevents touch events in the canvas from scrolling the canvas.
392 function TrailExample(CanvasKit, id, jsonData) {
393 if (!CanvasKit || !jsonData) {
396 const surface = CanvasKit.MakeCanvasSurface(id);
398 console.error('Could not make surface');
401 const canvas = surface.getCanvas();
403 const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
404 particles.start(Date.now() / 1000.0, true);
406 function drawFrame(canvas) {
407 particles.update(Date.now() / 1000.0);
409 canvas.clear(CanvasKit.WHITE);
410 particles.draw(canvas);
411 surface.requestAnimationFrame(drawFrame);
413 surface.requestAnimationFrame(drawFrame);
415 let interact = (e) => {
416 particles.setPosition([e.offsetX, e.offsetY]);
417 particles.setRate(e.pressure * 1000);
419 document.getElementById('trail').addEventListener('pointermove', interact);
420 document.getElementById('trail').addEventListener('pointerdown', interact);
421 document.getElementById('trail').addEventListener('pointerup', interact);
422 preventScrolling(document.getElementById('trail'));
428 "Type": "SkCircleDrawable",
432 "void spawn(inout Particle p) {",
433 " p.lifetime = 2 + rand(p.seed);",
434 " float a = radians(rand(p.seed) * 360);",
435 " p.vel = float2(cos(a), sin(a)) * mix(5, 15, rand(p.seed));",
436 " p.scale = mix(0.25, 0.75, rand(p.seed));",
439 "void update(inout Particle p) {",
440 " p.color.r = p.age;",
441 " p.color.g = 1 - p.age;",
449 document.head.appendChild(s);