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