• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2---
3title: "Particles"
4linkTitle: "Particles"
5
6weight: 40
7
8---
9
10
11Skia’s particle module provides a way to quickly generate large numbers of
12drawing primitives with dynamic, animated behavior. Particles can be used to
13create effects like fireworks, spark trails, ambient “weather”, and much more.
14Nearly all properties and behavior are controlled by scripts written in Skia’s
15custom language, SkSL.
16
17## Samples
18
19<style>
20  #demo canvas {
21    border: 1px dashed #AAA;
22    margin: 2px;
23  }
24
25  figure {
26    display: inline-block;
27    margin: 0;
28  }
29
30  figcaption > a {
31    margin: 2px 10px;
32  }
33</style>
34
35<div id=demo>
36  <figure>
37    <canvas id=trail width=400 height=400></canvas>
38    <figcaption>
39      Trail (Click and Drag!)
40    </figcaption>
41  </figure>
42  <figure>
43    <canvas id=cube width=400 height=400></canvas>
44    <figcaption>
45      <a href="https://particles.skia.org/?nameOrHash=@cube"
46         target=_blank rel=noopener>Cuboid</a>
47    </figcaption>
48  </figure>
49  <figure>
50    <canvas id=confetti width=400 height=400></canvas>
51    <figcaption>
52      <a href="https://particles.skia.org/?nameOrHash=@confetti"
53         target=_blank rel=noopener>Confetti</a>
54    </figcaption>
55  </figure>
56  <figure>
57    <canvas id=curves width=400 height=400></canvas>
58    <figcaption>
59      <a href="https://particles.skia.org/?nameOrHash=@spiral"
60         target=_blank rel=noopener>Curves</a>
61    </figcaption>
62  </figure>
63  <figure>
64    <canvas id=fireworks width=400 height=400></canvas>
65    <figcaption>
66      <a href="https://particles.skia.org/?nameOrHash=@fireworks"
67         target=_blank rel=noopener>Fireworks</a>
68    </figcaption>
69  </figure>
70  <figure>
71    <canvas id=text width=400 height=400></canvas>
72    <figcaption>
73      <a href="https://particles.skia.org/?nameOrHash=@text"
74         target=_blank rel=noopener>Text</a>
75    </figcaption>
76  </figure>
77
78</div>
79
80<script type="text/javascript" charset="utf-8">
81(function() {
82  // Tries to load the WASM version if supported, shows error otherwise
83  let s = document.createElement('script');
84  var locate_file = '';
85  if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
86    console.log('WebAssembly is supported!');
87    locate_file = 'https://particles.skia.org/dist/';
88  } else {
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>";
91    return;
92  }
93  s.src = locate_file + 'canvaskit.js';
94  s.onload = () => {
95  var CanvasKit = null;
96  CanvasKitInit({
97    locateFile: (file) => locate_file + file,
98  }).then((CK) => {
99    CanvasKit = CK;
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);
106  });
107
108  function ParticleExample(CanvasKit, id, jsonData, cx, cy) {
109    if (!CanvasKit || !jsonData) {
110      return;
111    }
112    const surface = CanvasKit.MakeCanvasSurface(id);
113    if (!surface) {
114      console.error('Could not make surface');
115      return;
116    }
117    const canvas = surface.getCanvas();
118    canvas.translate(cx, cy);
119
120    const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
121    particles.start(Date.now() / 1000.0, true);
122
123    function drawFrame(canvas) {
124      particles.update(Date.now() / 1000.0);
125
126      canvas.clear(CanvasKit.WHITE);
127      particles.draw(canvas);
128      surface.requestAnimationFrame(drawFrame);
129    }
130    surface.requestAnimationFrame(drawFrame);
131  }
132
133const confetti ={
134   "MaxCount": 200,
135   "Drawable": {
136      "Type": "SkCircleDrawable",
137      "Radius": 8
138   },
139   "Code": [
140     "void effectSpawn(inout Effect effect) {",
141     "  effect.lifetime = 2;",
142     "}",
143     "",
144     "void effectUpdate(inout Effect effect) {",
145     "  if (effect.age < 0.25 || effect.age > 0.75) { effect.rate = 0; }",
146     "  else { effect.rate = 200; }",
147     "}",
148     "",
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);",
155      "",
156      "  p.lifetime = (1 - effect.age) * effect.lifetime;",
157      "  p.scale = mix(0.6, 1, rand(p.seed));",
158      "}",
159      "",
160      "void update(inout Particle p) {",
161      "  p.color.a = 1 - p.age;",
162      "",
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;",
166      "}",
167      ""
168   ],
169   "Bindings": []
170};
171
172const cube = {
173  "MaxCount": 2000,
174  "Drawable": {
175    "Type": "SkCircleDrawable",
176    "Radius": 4
177  },
178  "Code": [
179    "void effectSpawn(inout Effect effect) {",
180    "  effect.lifetime = 2;",
181    "  effect.rate = 200;",
182    "}",
183    "",
184    "void spawn(inout Particle p) {",
185    "  p.lifetime = 10;",
186    "}",
187    "",
188    "float4x4 rx(float rad) {",
189    "  float c = cos(rad);",
190    "  float s = sin(rad);",
191    "  return float4x4(1, 0,  0, 0,",
192    "                  0, c, -s, 0,",
193    "                  0, s,  c, 0,",
194    "                  0, 0,  0, 1);",
195    "}",
196    "",
197    "float4x4 ry(float rad) {",
198    "  float c = cos(rad);",
199    "  float s = sin(rad);",
200    "  return float4x4(c, 0, -s, 0,",
201    "                  0, 1,  0, 0,",
202    "                  s, 0,  c, 0,",
203    "                  0, 0,  0, 1);",
204    "}",
205    "",
206    "float4x4 rz(float rad) {",
207    "  float c = cos(rad);",
208    "  float s = sin(rad);",
209    "  return float4x4( c, s, 0, 0,",
210    "                  -s, c, 0, 0,",
211    "                   0, 0, 1, 0,",
212    "                   0, 0, 0, 1);",
213    "}",
214    "",
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) {",
219    "      pos.x = 1;",
220    "      p.color.rgb = float3(1, 0.2, 0.2);",
221    "    } else {",
222    "      pos.x = 0;",
223    "      p.color.rgb = float3(0.2, 1, 1);",
224    "    }",
225    "  } else if (rand(p.seed) < 0.5) {",
226    "    if (pos.y > 0.5) {",
227    "      pos.y = 1;",
228    "      p.color.rgb = float3(0.2, 0.2, 1);",
229    "    } else {",
230    "      pos.y = 0;",
231    "      p.color.rgb = float3(1, 1, 0.2);",
232    "    }",
233    "  } else {",
234    "    if (pos.z > 0.5) {",
235    "      pos.z = 1;",
236    "      p.color.rgb = float3(0.2, 1, 0.2);",
237    "    } else {",
238    "      pos.z = 0;",
239    "      p.color.rgb = float3(1, 0.2, 1);",
240    "    }",
241    "  }",
242    "",
243    "  float s = effect.age * 2 - 1;",
244    "  s = s < 0 ? -s : s;",
245    "",
246    "  pos = pos * 2 - 1;",
247    "  pos = mix(pos, normalize(pos), s);",
248    "  pos = pos * 100;",
249    "",
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;",
255    "",
256    "  p.pos.x = pos.x;",
257    "  p.pos.y = pos.y;",
258    "  p.scale = ((pos.z + 50) / 100 + 0.5) / 2;",
259    "}",
260    ""
261  ],
262  "Bindings": []
263};
264
265const curves = {
266   "MaxCount": 1000,
267   "Drawable": {
268      "Type": "SkCircleDrawable",
269      "Radius": 2
270   },
271   "Code": [
272     "void effectSpawn(inout Effect effect) {",
273     "  effect.rate = 200;",
274     "  effect.color = float4(1, 0, 0, 1);",
275     "}",
276     "",
277      "void spawn(inout Particle p) {",
278      "  p.lifetime = 3 + rand(p.seed);",
279      "  p.vel.y = -50;",
280      "}",
281      "",
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; }",
286      "",
287      "  p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;",
288      "}",
289      ""
290   ],
291   "Bindings": []
292};
293
294const fireworks = {
295   "MaxCount": 300,
296   "Drawable": {
297      "Type": "SkCircleDrawable",
298      "Radius": 3
299   },
300   "Code": [
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",
313     "}",
314     "",
315     "void effectUpdate(inout Effect effect) {",
316     "  if (effect.age > 0.5 && effect.rate > 0) {",
317     "    // Phase two: Explode",
318     "    effect.rate = 0;",
319     "    effect.burst = 50;",
320     "    effect.scale = 1;",
321     "  } else {",
322     "    effect.vel.y += dt * 90;",
323     "  }",
324     "}",
325     "",
326      "void spawn(inout Particle p) {",
327      "  bool explode = p.scale == 1;",
328      "",
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;",
334      "}",
335      "",
336      "void update(inout Particle p) {",
337      "  p.color.a = 1 - p.age;",
338      "  if (p.scale == 1) {",
339      "    p.vel.y += dt * 50;",
340      "  }",
341      "}",
342      ""
343   ],
344   "Bindings": []
345};
346
347const text = {
348   "MaxCount": 2000,
349   "Drawable": {
350      "Type": "SkCircleDrawable",
351      "Radius": 1
352   },
353   "Code": [
354     "void effectSpawn(inout Effect effect) {",
355     "  effect.rate = 1000;",
356     "}",
357     "",
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;",
365      "}",
366      "",
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);",
371      "}",
372      ""
373   ],
374   "Bindings": [
375      {
376         "Type": "SkTextBinding",
377         "Name": "text",
378         "Text": "SKIA",
379         "FontSize": 96
380      }
381   ]
382};
383
384  function preventScrolling(canvas) {
385    canvas.addEventListener('touchmove', (e) => {
386      // Prevents touch events in the canvas from scrolling the canvas.
387      e.preventDefault();
388      e.stopPropagation();
389    });
390  }
391
392  function TrailExample(CanvasKit, id, jsonData) {
393    if (!CanvasKit || !jsonData) {
394      return;
395    }
396    const surface = CanvasKit.MakeCanvasSurface(id);
397    if (!surface) {
398      console.error('Could not make surface');
399      return;
400    }
401    const canvas = surface.getCanvas();
402
403    const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
404    particles.start(Date.now() / 1000.0, true);
405
406    function drawFrame(canvas) {
407      particles.update(Date.now() / 1000.0);
408
409      canvas.clear(CanvasKit.WHITE);
410      particles.draw(canvas);
411      surface.requestAnimationFrame(drawFrame);
412    }
413    surface.requestAnimationFrame(drawFrame);
414
415    let interact = (e) => {
416      particles.setPosition([e.offsetX, e.offsetY]);
417      particles.setRate(e.pressure * 1000);
418    };
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'));
423  }
424
425const trail = {
426   "MaxCount": 2000,
427   "Drawable": {
428      "Type": "SkCircleDrawable",
429      "Radius": 4
430   },
431   "Code": [
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));",
437      "}",
438      "",
439      "void update(inout Particle p) {",
440      "  p.color.r = p.age;",
441      "  p.color.g = 1 - p.age;",
442      "}",
443      ""
444   ],
445   "Bindings": []
446};
447
448  }
449  document.head.appendChild(s);
450})();
451</script>
452
453