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