• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1Particles
2=========
3
4Skia’s particle module provides a way to quickly generate large numbers of
5drawing primitives with dynamic, animated behavior. Particles can be used to
6create effects like fireworks, spark trails, ambient “weather”, and much more.
7Nearly all properties and behavior are controlled by scripts written in Skia’s
8custom language, SkSL.
9
10
11Samples
12-------
13
14<style>
15  #demo canvas {
16    border: 1px dashed #AAA;
17    margin: 2px;
18  }
19
20  figure {
21    display: inline-block;
22    margin: 0;
23  }
24
25  figcaption > a {
26    margin: 2px 10px;
27  }
28</style>
29
30<div id=demo>
31  <figure>
32    <canvas id=trail width=400 height=400></canvas>
33    <figcaption>
34      Trail (Click and Drag!)
35    </figcaption>
36  </figure>
37  <figure>
38    <canvas id=cube width=400 height=400></canvas>
39    <figcaption>
40      <a href="https://particles.skia.org/f03ffe333483db27fb045d0f3f508db3"
41         target=_blank rel=noopener>Cuboid</a>
42    </figcaption>
43  </figure>
44  <figure>
45    <canvas id=confetti width=400 height=400></canvas>
46    <figcaption>
47      <a href="https://particles.skia.org/84a757d92c424b3d378b55481a4b2394"
48         target=_blank rel=noopener>Confetti</a>
49    </figcaption>
50  </figure>
51  <figure>
52    <canvas id=curves width=400 height=400></canvas>
53    <figcaption>
54      <a href="https://particles.skia.org/63b1970cc212740e5a44870691c49307"
55         target=_blank rel=noopener>Curves</a>
56    </figcaption>
57  </figure>
58  <figure>
59    <canvas id=fireworks width=400 height=400></canvas>
60    <figcaption>
61      <a href="https://particles.skia.org/b986ed92759cd66a36a3d589e6130931"
62         target=_blank rel=noopener>Fireworks</a>
63    </figcaption>
64  </figure>
65  <figure>
66    <canvas id=raincloud width=400 height=400></canvas>
67    <figcaption>
68      <a href="https://particles.skia.org/030d1403bbe69f7c4506d6f688e53487"
69         target=_blank rel=noopener>Raincloud</a>
70    </figcaption>
71  </figure>
72  <figure>
73    <canvas id=text width=400 height=400></canvas>
74    <figcaption>
75      <a href="https://particles.skia.org/7c13116e4b61c18b828bfc281903efe8"
76         target=_blank rel=noopener>Text</a>
77    </figcaption>
78  </figure>
79
80</div>
81
82<script type="text/javascript" charset="utf-8">
83(function() {
84  // Tries to load the WASM version if supported, shows error otherwise
85  let s = document.createElement('script');
86  var locate_file = '';
87  if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
88    console.log('WebAssembly is supported!');
89    locate_file = 'https://particles.skia.org/static/';
90  } else {
91    console.log('WebAssembly is not supported (yet) on this browser.');
92    document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>";
93    return;
94  }
95  s.src = locate_file + 'canvaskit.js';
96  s.onload = () => {
97  var CanvasKit = null;
98  CanvasKitInit({
99    locateFile: (file) => locate_file + file,
100  }).ready().then((CK) => {
101    CanvasKit = CK;
102    TrailExample(CanvasKit, 'trail', trail);
103    ParticleExample(CanvasKit, 'confetti', confetti, 200, 200);
104    ParticleExample(CanvasKit, 'curves', curves, 200, 300);
105    ParticleExample(CanvasKit, 'cube', cube, 200, 200);
106    ParticleExample(CanvasKit, 'fireworks', fireworks, 200, 300);
107    ParticleExample(CanvasKit, 'raincloud', raincloud, 200, 100);
108    ParticleExample(CanvasKit, 'text', text, 75, 250);
109  });
110
111  function ParticleExample(CanvasKit, id, jsonData, cx, cy) {
112    if (!CanvasKit || !jsonData) {
113      return;
114    }
115    const surface = CanvasKit.MakeCanvasSurface(id);
116    if (!surface) {
117      console.error('Could not make surface');
118      return;
119    }
120    const context = CanvasKit.currentContext();
121    const canvas = surface.getCanvas();
122    canvas.translate(cx, cy);
123
124    const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
125    particles.start(Date.now() / 1000.0, true);
126
127    function drawFrame(canvas) {
128      particles.update(Date.now() / 1000.0);
129
130      canvas.clear(CanvasKit.WHITE);
131      particles.draw(canvas);
132      surface.requestAnimationFrame(drawFrame);
133    }
134    surface.requestAnimationFrame(drawFrame);
135  }
136
137const confetti ={
138   "MaxCount": 200,
139   "Drawable": {
140      "Type": "SkCircleDrawable",
141      "Radius": 8
142   },
143   "EffectCode": [
144      "void effectSpawn(inout Effect effect) {",
145      "  effect.lifetime = 2;",
146      "}",
147      "",
148      "void effectUpdate(inout Effect effect) {",
149      "  if (effect.age < 0.25 || effect.age > 0.75) { effect.rate = 0; }",
150      "  else { effect.rate = 200; }",
151      "}",
152      ""
153   ],
154   "Code": [
155      "void spawn(inout Particle p) {",
156      "  float3 colors[4];",
157      "  colors[0] = float3(0.87, 0.24, 0.11);",
158      "  colors[1] = float3(1, 0.9, 0.2);",
159      "  colors[2] = float3(0.44, 0.73, 0.24);",
160      "  colors[3] = float3(0.38, 0.54, 0.95);",
161      "  int idx = int(rand * 4);",
162      "  p.color.rgb = colors[idx];",
163      "",
164      "  p.lifetime = (1 - effect.age) * effect.lifetime;",
165      "  p.scale = mix(0.6, 1, rand);",
166      "}",
167      "",
168      "void update(inout Particle p) {",
169      "  p.color.a = 1 - p.age;",
170      "",
171      "  float a = radians(rand * 360);",
172      "  float invAge = 1 - p.age;",
173      "  p.vel = float2(cos(a), sin(a)) * mix(250, 550, rand) * invAge * invAge;",
174      "}",
175      ""
176   ],
177   "Bindings": []
178};
179
180const cube = {
181  "MaxCount": 2000,
182  "Drawable": {
183    "Type": "SkCircleDrawable",
184    "Radius": 4
185  },
186  "EffectCode": [
187    "void effectSpawn(inout Effect effect) {",
188    "  effect.lifetime = 2;",
189    "  effect.rate = 200;",
190    "}",
191    ""
192  ],
193  "Code": [
194    "void spawn(inout Particle p) {",
195    "  p.lifetime = 10;",
196    "}",
197    "",
198    "float4x4 rx(float rad) {",
199    "  float c = cos(rad);",
200    "  float s = sin(rad);",
201    "  return float4x4(1, 0,  0, 0,",
202    "                  0, c, -s, 0,",
203    "                  0, s,  c, 0,",
204    "                  0, 0,  0, 1);",
205    "}",
206    "",
207    "float4x4 ry(float rad) {",
208    "  float c = cos(rad);",
209    "  float s = sin(rad);",
210    "  return float4x4(c, 0, -s, 0,",
211    "                  0, 1,  0, 0,",
212    "                  s, 0,  c, 0,",
213    "                  0, 0,  0, 1);",
214    "}",
215    "",
216    "float4x4 rz(float rad) {",
217    "  float c = cos(rad);",
218    "  float s = sin(rad);",
219    "  return float4x4( c, s, 0, 0,",
220    "                  -s, c, 0, 0,",
221    "                   0, 0, 1, 0,",
222    "                   0, 0, 0, 1);",
223    "}",
224    "",
225    "void update(inout Particle p) {",
226    "  float3 pos = float3(rand, rand, rand);",
227    "  if (rand < 0.33) {",
228    "    if (pos.x > 0.5) {",
229    "      pos.x = 1;",
230    "      p.color.rgb = float3(1, 0.2, 0.2);",
231    "    } else {",
232    "      pos.x = 0;",
233    "      p.color.rgb = float3(0.2, 1, 1);",
234    "    }",
235    "  } else if (rand < 0.5) {",
236    "    if (pos.y > 0.5) {",
237    "      pos.y = 1;",
238    "      p.color.rgb = float3(0.2, 0.2, 1);",
239    "    } else {",
240    "      pos.y = 0;",
241    "      p.color.rgb = float3(1, 1, 0.2);",
242    "    }",
243    "  } else {",
244    "    if (pos.z > 0.5) {",
245    "      pos.z = 1;",
246    "      p.color.rgb = float3(0.2, 1, 0.2);",
247    "    } else {",
248    "      pos.z = 0;",
249    "      p.color.rgb = float3(1, 0.2, 1);",
250    "    }",
251    "  }",
252    "",
253    "  float s = effect.age * 2 - 1;",
254    "  s = s < 0 ? -s : s;",
255    "",
256    "  pos = pos * 2 - 1;",
257    "  pos = mix(pos, normalize(pos), s);",
258    "  pos = pos * 100;",
259    "",
260    "  float age = effect.loop + effect.age;",
261    "  float4x4 mat = rx(age * radians(60))",
262    "               * ry(age * radians(70))",
263    "               * rz(age * radians(80));",
264    "  pos = (mat * float4(pos, 1)).xyz;",
265    "",
266    "  p.pos.x = pos.x;",
267    "  p.pos.y = pos.y;",
268    "  p.scale = ((pos.z + 50) / 100 + 0.5) / 2;",
269    "}",
270    ""
271  ],
272  "Bindings": []
273};
274
275const curves = {
276   "MaxCount": 1000,
277   "Drawable": {
278      "Type": "SkCircleDrawable",
279      "Radius": 2
280   },
281   "EffectCode": [
282      "void effectSpawn(inout Effect effect) {",
283      "  effect.rate = 200;",
284      "  effect.color = float4(1, 0, 0, 1);",
285      "}",
286      ""
287   ],
288   "Code": [
289      "void spawn(inout Particle p) {",
290      "  p.lifetime = 3 + rand;",
291      "  p.vel.y = -50;",
292      "}",
293      "",
294      "void update(inout Particle p) {",
295      "  float w = mix(15, 3, p.age);",
296      "  p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand);",
297      "  if (rand < 0.5) { p.pos.x = -p.pos.x; }",
298      "",
299      "  p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand)) / 255;",
300      "}",
301      ""
302   ],
303   "Bindings": []
304};
305
306const fireworks = {
307   "MaxCount": 1000,
308   "Drawable": {
309      "Type": "SkCircleDrawable",
310      "Radius": 1
311   },
312   "EffectCode": [
313      "void effectSpawn(inout Effect effect) {",
314      "  effect.lifetime = 2;",
315      "  effect.rate = 120;",
316      "  float a = radians(mix(-20, 20, rand) - 90);",
317      "  float s = mix(200, 220, rand);",
318      "  effect.vel.x = cos(a) * s;",
319      "  effect.vel.y = sin(a) * s;",
320      "  effect.color.rgb = float3(rand, rand, rand);",
321      "  effect.pos.x = 0;",
322      "  effect.pos.y = 0;",
323      "}",
324      "",
325      "void effectUpdate(inout Effect effect) {",
326      "  effect.vel.y += dt * 90;",
327      "}",
328      "",
329      "void effectDeath(inout Effect effect) {",
330      "  explode(false);",
331      "}",
332      ""
333   ],
334   "Code": [
335      "void spawn(inout Particle p) {",
336      "  p.lifetime = 0.5;",
337      "  float a = radians(rand * 360);",
338      "  float s = mix(5, 10, rand);",
339      "  p.vel.x = cos(a) * s;",
340      "  p.vel.y = sin(a) * s;",
341      "}",
342      "",
343      "void update(inout Particle p) {",
344      "  p.color.a = 1 - p.age;",
345      "}",
346      ""
347   ],
348   "Bindings": [
349      {
350         "Type": "SkEffectBinding",
351         "Name": "explode",
352         "MaxCount": 50,
353         "Drawable": {
354            "Type": "SkCircleDrawable",
355            "Radius": 3
356         },
357         "EffectCode": [
358            "void effectSpawn(inout Effect effect) {",
359            "  effect.burst = 50;",
360            "  effect.lifetime = 2.5;",
361            "}",
362            ""
363         ],
364         "Code": [
365            "void spawn(inout Particle p) {",
366            "  p.lifetime = 2 + rand * 0.5;",
367            "  float a = radians(rand * 360);",
368            "  float s = mix(90, 100, rand);",
369            "  p.vel.x = cos(a) * s;",
370            "  p.vel.y = sin(a) * s;",
371            "}",
372            "",
373            "void update(inout Particle p) {",
374            "  p.color.a = 1 - p.age;",
375            "  p.vel.y += dt * 50;",
376            "}",
377            ""
378         ],
379         "Bindings": []
380      }
381   ]
382};
383
384const raincloud = {
385   "MaxCount": 128,
386   "Drawable": {
387      "Type": "SkCircleDrawable",
388      "Radius": 2
389   },
390   "EffectCode": [
391      "void effectSpawn(inout Effect effect) {",
392      "  if (effect.loop == 0) {",
393      "    cloud(true);",
394      "  }",
395      "  effect.color = float4(0.1, 0.1, 1.0, 1.0);",
396      "  effect.rate = 10;",
397      "}",
398      ""
399   ],
400   "Code": [
401      "void spawn(inout Particle p) {",
402      "  p.lifetime = 4;",
403      "  p.pos.x = mix(-50, 50, rand);",
404      "  p.vel.y = 50;",
405      "}",
406      "",
407      "bool once(bool cond, inout uint flags, uint flag) {",
408      "  bool result = false;",
409      "  if (cond && (flags & flag) == 0) {",
410      "    flags |= flag;",
411      "    result = true;",
412      "  }",
413      "  return result;",
414      "}",
415      "",
416      "void update(inout Particle p) {",
417      "  p.vel.y += 20 * dt;",
418      "  if (once(p.pos.y > 150, p.flags, 0x1)) {",
419      "    p.scale = 0;",
420      "    splash(false);",
421      "  }",
422      "}",
423      ""
424   ],
425   "Bindings": [
426      {
427         "Type": "SkEffectBinding",
428         "Name": "cloud",
429         "MaxCount": 60,
430         "Drawable": {
431            "Type": "SkCircleDrawable",
432            "Radius": 16
433         },
434         "EffectCode": [
435            "void effectSpawn(inout Effect effect) {",
436            "  effect.color = float4(0.8, 0.8, 0.8, 1);",
437            "  effect.rate = 30;",
438            "}",
439            ""
440         ],
441         "Code": [
442            "float2 circle() {",
443            "  float2 xy;",
444            "  do {",
445            "    xy.x = 2 * rand - 1;",
446            "    xy.y = 2 * rand - 1;",
447            "  } while (dot(xy, xy) > 1);",
448            "  return xy;",
449            "}",
450            "",
451            "void spawn(inout Particle p) {",
452            "  p.lifetime = 2.5;",
453            "  p.pos = circle() * float2(50, 10);",
454            "  p.vel.x = mix(-10, 10, rand);",
455            "  p.vel.y = mix(-10, 10, rand);",
456            "}",
457            "",
458            "void update(inout Particle p) {",
459            "  p.color.a = 1 - (length(p.pos) / 150);",
460            "}",
461            ""
462         ],
463         "Bindings": []
464      },
465      {
466         "Type": "SkEffectBinding",
467         "Name": "splash",
468         "MaxCount": 8,
469         "Drawable": {
470            "Type": "SkCircleDrawable",
471            "Radius": 1
472         },
473         "EffectCode": [
474            "void effectSpawn(inout Effect effect) {",
475            "  effect.burst = 8;",
476            "  effect.scale = 1;",
477            "}",
478            ""
479         ],
480         "Code": [
481            "void spawn(inout Particle p) {",
482            "  p.lifetime = rand;",
483            "  float a = radians(mix(-80, 80, rand) - 90);",
484            "  p.vel.x = cos(a) * 20;",
485            "  p.vel.y = sin(a) * 20;",
486            "}",
487            "",
488            "void update(inout Particle p) {",
489            "  p.vel.y += dt * 20;",
490            "}",
491            ""
492         ],
493         "Bindings": []
494      }
495   ]
496};
497
498const text = {
499   "MaxCount": 2000,
500   "Drawable": {
501      "Type": "SkCircleDrawable",
502      "Radius": 1
503   },
504   "EffectCode": [
505      "void effectSpawn(inout Effect effect) {",
506      "  effect.rate = 1000;",
507      "}",
508      ""
509   ],
510   "Code": [
511      "void spawn(inout Particle p) {",
512      "  p.lifetime = mix(1, 3, rand);",
513      "  float a = radians(mix(250, 290, rand));",
514      "  float s = mix(10, 30, rand);",
515      "  p.vel.x = cos(a) * s;",
516      "  p.vel.y = sin(a) * s;",
517      "  p.pos = text(rand).xy;",
518      "}",
519      "",
520      "void update(inout Particle p) {",
521      "  float4 startColor = float4(1, 0.196, 0.078, 1);",
522      "  float4 endColor   = float4(1, 0.784, 0.078, 1);",
523      "  p.color = mix(startColor, endColor, p.age);",
524      "}",
525      ""
526   ],
527   "Bindings": [
528      {
529         "Type": "SkTextBinding",
530         "Name": "text",
531         "Text": "SKIA",
532         "FontSize": 96
533      }
534   ]
535};
536
537  function preventScrolling(canvas) {
538    canvas.addEventListener('touchmove', (e) => {
539      // Prevents touch events in the canvas from scrolling the canvas.
540      e.preventDefault();
541      e.stopPropagation();
542    });
543  }
544
545  function TrailExample(CanvasKit, id, jsonData) {
546    if (!CanvasKit || !jsonData) {
547      return;
548    }
549    const surface = CanvasKit.MakeCanvasSurface(id);
550    if (!surface) {
551      console.error('Could not make surface');
552      return;
553    }
554    const context = CanvasKit.currentContext();
555    const canvas = surface.getCanvas();
556
557    const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
558    particles.start(Date.now() / 1000.0, true);
559
560    function drawFrame(canvas) {
561      particles.update(Date.now() / 1000.0);
562
563      canvas.clear(CanvasKit.WHITE);
564      particles.draw(canvas);
565      surface.requestAnimationFrame(drawFrame);
566    }
567    surface.requestAnimationFrame(drawFrame);
568
569    let interact = (e) => {
570      particles.setPosition([e.offsetX, e.offsetY]);
571      particles.setRate(e.pressure * 1000);
572    };
573    document.getElementById('trail').addEventListener('pointermove', interact);
574    document.getElementById('trail').addEventListener('pointerdown', interact);
575    document.getElementById('trail').addEventListener('pointerup', interact);
576    preventScrolling(document.getElementById('trail'));
577  }
578
579const trail = {
580   "MaxCount": 2000,
581   "Drawable": {
582      "Type": "SkCircleDrawable",
583      "Radius": 4
584   },
585   "EffectCode": "",
586   "Code": [
587      "void spawn(inout Particle p) {",
588      "  p.lifetime = 2 + rand;",
589      "  float a = radians(rand * 360);",
590      "  p.vel = float2(cos(a), sin(a)) * mix(5, 15, rand);",
591      "  p.scale = mix(0.25, 0.75, rand);",
592      "}",
593      "",
594      "void update(inout Particle p) {",
595      "  p.color.r = p.age;",
596      "  p.color.g = 1 - p.age;",
597      "}",
598      ""
599   ],
600   "Bindings": []
601};
602
603  }
604  document.head.appendChild(s);
605})();
606</script>
607