5f827ee657
No-Try: true Docs-Preview: https://skia.org/?cl=249229 Bug: skia:9513 Change-Id: If72d809d843af73debb8cb2d797283d9aa65f314 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/249229 Commit-Queue: Brian Osman <brianosman@google.com> Commit-Queue: Kevin Lubick <kjlubick@google.com> Auto-Submit: Brian Osman <brianosman@google.com> Reviewed-by: Kevin Lubick <kjlubick@google.com>
607 lines
16 KiB
Markdown
607 lines
16 KiB
Markdown
Particles
|
||
=========
|
||
|
||
Skia’s particle module provides a way to quickly generate large numbers of
|
||
drawing primitives with dynamic, animated behavior. Particles can be used to
|
||
create effects like fireworks, spark trails, ambient “weather”, and much more.
|
||
Nearly all properties and behavior are controlled by scripts written in Skia’s
|
||
custom language, SkSL.
|
||
|
||
|
||
Samples
|
||
-------
|
||
|
||
<style>
|
||
#demo canvas {
|
||
border: 1px dashed #AAA;
|
||
margin: 2px;
|
||
}
|
||
|
||
figure {
|
||
display: inline-block;
|
||
margin: 0;
|
||
}
|
||
|
||
figcaption > a {
|
||
margin: 2px 10px;
|
||
}
|
||
</style>
|
||
|
||
<div id=demo>
|
||
<figure>
|
||
<canvas id=trail width=400 height=400></canvas>
|
||
<figcaption>
|
||
Trail (Click and Drag!)
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=cube width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/f03ffe333483db27fb045d0f3f508db3"
|
||
target=_blank rel=noopener>Cuboid</a>
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=confetti width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/84a757d92c424b3d378b55481a4b2394"
|
||
target=_blank rel=noopener>Confetti</a>
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=curves width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/63b1970cc212740e5a44870691c49307"
|
||
target=_blank rel=noopener>Curves</a>
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=fireworks width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/b986ed92759cd66a36a3d589e6130931"
|
||
target=_blank rel=noopener>Fireworks</a>
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=raincloud width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/030d1403bbe69f7c4506d6f688e53487"
|
||
target=_blank rel=noopener>Raincloud</a>
|
||
</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<canvas id=text width=400 height=400></canvas>
|
||
<figcaption>
|
||
<a href="https://particles.skia.org/7c13116e4b61c18b828bfc281903efe8"
|
||
target=_blank rel=noopener>Text</a>
|
||
</figcaption>
|
||
</figure>
|
||
|
||
</div>
|
||
|
||
<script type="text/javascript" charset="utf-8">
|
||
(function() {
|
||
// Tries to load the WASM version if supported, shows error otherwise
|
||
let s = document.createElement('script');
|
||
var locate_file = '';
|
||
if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
|
||
console.log('WebAssembly is supported!');
|
||
locate_file = 'https://particles.skia.org/static/';
|
||
} else {
|
||
console.log('WebAssembly is not supported (yet) on this browser.');
|
||
document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>";
|
||
return;
|
||
}
|
||
s.src = locate_file + 'canvaskit.js';
|
||
s.onload = () => {
|
||
var CanvasKit = null;
|
||
CanvasKitInit({
|
||
locateFile: (file) => locate_file + file,
|
||
}).ready().then((CK) => {
|
||
CanvasKit = CK;
|
||
TrailExample(CanvasKit, 'trail', trail);
|
||
ParticleExample(CanvasKit, 'confetti', confetti, 200, 200);
|
||
ParticleExample(CanvasKit, 'curves', curves, 200, 300);
|
||
ParticleExample(CanvasKit, 'cube', cube, 200, 200);
|
||
ParticleExample(CanvasKit, 'fireworks', fireworks, 200, 300);
|
||
ParticleExample(CanvasKit, 'raincloud', raincloud, 200, 100);
|
||
ParticleExample(CanvasKit, 'text', text, 75, 250);
|
||
});
|
||
|
||
function ParticleExample(CanvasKit, id, jsonData, cx, cy) {
|
||
if (!CanvasKit || !jsonData) {
|
||
return;
|
||
}
|
||
const surface = CanvasKit.MakeCanvasSurface(id);
|
||
if (!surface) {
|
||
console.error('Could not make surface');
|
||
return;
|
||
}
|
||
const context = CanvasKit.currentContext();
|
||
const canvas = surface.getCanvas();
|
||
canvas.translate(cx, cy);
|
||
|
||
const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
|
||
particles.start(Date.now() / 1000.0, true);
|
||
|
||
function drawFrame(canvas) {
|
||
particles.update(Date.now() / 1000.0);
|
||
|
||
canvas.clear(CanvasKit.WHITE);
|
||
particles.draw(canvas);
|
||
surface.requestAnimationFrame(drawFrame);
|
||
}
|
||
surface.requestAnimationFrame(drawFrame);
|
||
}
|
||
|
||
const confetti ={
|
||
"MaxCount": 200,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 8
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.lifetime = 2;",
|
||
"}",
|
||
"",
|
||
"void effectUpdate(inout Effect effect) {",
|
||
" if (effect.age < 0.25 || effect.age > 0.75) { effect.rate = 0; }",
|
||
" else { effect.rate = 200; }",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" float3 colors[4];",
|
||
" colors[0] = float3(0.87, 0.24, 0.11);",
|
||
" colors[1] = float3(1, 0.9, 0.2);",
|
||
" colors[2] = float3(0.44, 0.73, 0.24);",
|
||
" colors[3] = float3(0.38, 0.54, 0.95);",
|
||
" int idx = int(rand * 4);",
|
||
" p.color.rgb = colors[idx];",
|
||
"",
|
||
" p.lifetime = (1 - effect.age) * effect.lifetime;",
|
||
" p.scale = mix(0.6, 1, rand);",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.color.a = 1 - p.age;",
|
||
"",
|
||
" float a = radians(rand * 360);",
|
||
" float invAge = 1 - p.age;",
|
||
" p.vel = float2(cos(a), sin(a)) * mix(250, 550, rand) * invAge * invAge;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
};
|
||
|
||
const cube = {
|
||
"MaxCount": 2000,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 4
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.lifetime = 2;",
|
||
" effect.rate = 200;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 10;",
|
||
"}",
|
||
"",
|
||
"float4x4 rx(float rad) {",
|
||
" float c = cos(rad);",
|
||
" float s = sin(rad);",
|
||
" return float4x4(1, 0, 0, 0,",
|
||
" 0, c, -s, 0,",
|
||
" 0, s, c, 0,",
|
||
" 0, 0, 0, 1);",
|
||
"}",
|
||
"",
|
||
"float4x4 ry(float rad) {",
|
||
" float c = cos(rad);",
|
||
" float s = sin(rad);",
|
||
" return float4x4(c, 0, -s, 0,",
|
||
" 0, 1, 0, 0,",
|
||
" s, 0, c, 0,",
|
||
" 0, 0, 0, 1);",
|
||
"}",
|
||
"",
|
||
"float4x4 rz(float rad) {",
|
||
" float c = cos(rad);",
|
||
" float s = sin(rad);",
|
||
" return float4x4( c, s, 0, 0,",
|
||
" -s, c, 0, 0,",
|
||
" 0, 0, 1, 0,",
|
||
" 0, 0, 0, 1);",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" float3 pos = float3(rand, rand, rand);",
|
||
" if (rand < 0.33) {",
|
||
" if (pos.x > 0.5) {",
|
||
" pos.x = 1;",
|
||
" p.color.rgb = float3(1, 0.2, 0.2);",
|
||
" } else {",
|
||
" pos.x = 0;",
|
||
" p.color.rgb = float3(0.2, 1, 1);",
|
||
" }",
|
||
" } else if (rand < 0.5) {",
|
||
" if (pos.y > 0.5) {",
|
||
" pos.y = 1;",
|
||
" p.color.rgb = float3(0.2, 0.2, 1);",
|
||
" } else {",
|
||
" pos.y = 0;",
|
||
" p.color.rgb = float3(1, 1, 0.2);",
|
||
" }",
|
||
" } else {",
|
||
" if (pos.z > 0.5) {",
|
||
" pos.z = 1;",
|
||
" p.color.rgb = float3(0.2, 1, 0.2);",
|
||
" } else {",
|
||
" pos.z = 0;",
|
||
" p.color.rgb = float3(1, 0.2, 1);",
|
||
" }",
|
||
" }",
|
||
"",
|
||
" float s = effect.age * 2 - 1;",
|
||
" s = s < 0 ? -s : s;",
|
||
"",
|
||
" pos = pos * 2 - 1;",
|
||
" pos = mix(pos, normalize(pos), s);",
|
||
" pos = pos * 100;",
|
||
"",
|
||
" float age = effect.loop + effect.age;",
|
||
" float4x4 mat = rx(age * radians(60))",
|
||
" * ry(age * radians(70))",
|
||
" * rz(age * radians(80));",
|
||
" pos = (mat * float4(pos, 1)).xyz;",
|
||
"",
|
||
" p.pos.x = pos.x;",
|
||
" p.pos.y = pos.y;",
|
||
" p.scale = ((pos.z + 50) / 100 + 0.5) / 2;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
};
|
||
|
||
const curves = {
|
||
"MaxCount": 1000,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 2
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.rate = 200;",
|
||
" effect.color = float4(1, 0, 0, 1);",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 3 + rand;",
|
||
" p.vel.y = -50;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" float w = mix(15, 3, p.age);",
|
||
" p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand);",
|
||
" if (rand < 0.5) { p.pos.x = -p.pos.x; }",
|
||
"",
|
||
" p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand)) / 255;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
};
|
||
|
||
const fireworks = {
|
||
"MaxCount": 1000,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 1
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.lifetime = 2;",
|
||
" effect.rate = 120;",
|
||
" float a = radians(mix(-20, 20, rand) - 90);",
|
||
" float s = mix(200, 220, rand);",
|
||
" effect.vel.x = cos(a) * s;",
|
||
" effect.vel.y = sin(a) * s;",
|
||
" effect.color.rgb = float3(rand, rand, rand);",
|
||
" effect.pos.x = 0;",
|
||
" effect.pos.y = 0;",
|
||
"}",
|
||
"",
|
||
"void effectUpdate(inout Effect effect) {",
|
||
" effect.vel.y += dt * 90;",
|
||
"}",
|
||
"",
|
||
"void effectDeath(inout Effect effect) {",
|
||
" explode(false);",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 0.5;",
|
||
" float a = radians(rand * 360);",
|
||
" float s = mix(5, 10, rand);",
|
||
" p.vel.x = cos(a) * s;",
|
||
" p.vel.y = sin(a) * s;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.color.a = 1 - p.age;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": [
|
||
{
|
||
"Type": "SkEffectBinding",
|
||
"Name": "explode",
|
||
"MaxCount": 50,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 3
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.burst = 50;",
|
||
" effect.lifetime = 2.5;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 2 + rand * 0.5;",
|
||
" float a = radians(rand * 360);",
|
||
" float s = mix(90, 100, rand);",
|
||
" p.vel.x = cos(a) * s;",
|
||
" p.vel.y = sin(a) * s;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.color.a = 1 - p.age;",
|
||
" p.vel.y += dt * 50;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
}
|
||
]
|
||
};
|
||
|
||
const raincloud = {
|
||
"MaxCount": 128,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 2
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" if (effect.loop == 0) {",
|
||
" cloud(true);",
|
||
" }",
|
||
" effect.color = float4(0.1, 0.1, 1.0, 1.0);",
|
||
" effect.rate = 10;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 4;",
|
||
" p.pos.x = mix(-50, 50, rand);",
|
||
" p.vel.y = 50;",
|
||
"}",
|
||
"",
|
||
"bool once(bool cond, inout uint flags, uint flag) {",
|
||
" bool result = false;",
|
||
" if (cond && (flags & flag) == 0) {",
|
||
" flags |= flag;",
|
||
" result = true;",
|
||
" }",
|
||
" return result;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.vel.y += 20 * dt;",
|
||
" if (once(p.pos.y > 150, p.flags, 0x1)) {",
|
||
" p.scale = 0;",
|
||
" splash(false);",
|
||
" }",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": [
|
||
{
|
||
"Type": "SkEffectBinding",
|
||
"Name": "cloud",
|
||
"MaxCount": 60,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 16
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.color = float4(0.8, 0.8, 0.8, 1);",
|
||
" effect.rate = 30;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"float2 circle() {",
|
||
" float2 xy;",
|
||
" do {",
|
||
" xy.x = 2 * rand - 1;",
|
||
" xy.y = 2 * rand - 1;",
|
||
" } while (dot(xy, xy) > 1);",
|
||
" return xy;",
|
||
"}",
|
||
"",
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 2.5;",
|
||
" p.pos = circle() * float2(50, 10);",
|
||
" p.vel.x = mix(-10, 10, rand);",
|
||
" p.vel.y = mix(-10, 10, rand);",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.color.a = 1 - (length(p.pos) / 150);",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
},
|
||
{
|
||
"Type": "SkEffectBinding",
|
||
"Name": "splash",
|
||
"MaxCount": 8,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 1
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.burst = 8;",
|
||
" effect.scale = 1;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = rand;",
|
||
" float a = radians(mix(-80, 80, rand) - 90);",
|
||
" p.vel.x = cos(a) * 20;",
|
||
" p.vel.y = sin(a) * 20;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.vel.y += dt * 20;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
}
|
||
]
|
||
};
|
||
|
||
const text = {
|
||
"MaxCount": 2000,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 1
|
||
},
|
||
"EffectCode": [
|
||
"void effectSpawn(inout Effect effect) {",
|
||
" effect.rate = 1000;",
|
||
"}",
|
||
""
|
||
],
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = mix(1, 3, rand);",
|
||
" float a = radians(mix(250, 290, rand));",
|
||
" float s = mix(10, 30, rand);",
|
||
" p.vel.x = cos(a) * s;",
|
||
" p.vel.y = sin(a) * s;",
|
||
" p.pos = text(rand).xy;",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" float4 startColor = float4(1, 0.196, 0.078, 1);",
|
||
" float4 endColor = float4(1, 0.784, 0.078, 1);",
|
||
" p.color = mix(startColor, endColor, p.age);",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": [
|
||
{
|
||
"Type": "SkTextBinding",
|
||
"Name": "text",
|
||
"Text": "SKIA",
|
||
"FontSize": 96
|
||
}
|
||
]
|
||
};
|
||
|
||
function preventScrolling(canvas) {
|
||
canvas.addEventListener('touchmove', (e) => {
|
||
// Prevents touch events in the canvas from scrolling the canvas.
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
}
|
||
|
||
function TrailExample(CanvasKit, id, jsonData) {
|
||
if (!CanvasKit || !jsonData) {
|
||
return;
|
||
}
|
||
const surface = CanvasKit.MakeCanvasSurface(id);
|
||
if (!surface) {
|
||
console.error('Could not make surface');
|
||
return;
|
||
}
|
||
const context = CanvasKit.currentContext();
|
||
const canvas = surface.getCanvas();
|
||
|
||
const particles = CanvasKit.MakeParticles(JSON.stringify(jsonData));
|
||
particles.start(Date.now() / 1000.0, true);
|
||
|
||
function drawFrame(canvas) {
|
||
particles.update(Date.now() / 1000.0);
|
||
|
||
canvas.clear(CanvasKit.WHITE);
|
||
particles.draw(canvas);
|
||
surface.requestAnimationFrame(drawFrame);
|
||
}
|
||
surface.requestAnimationFrame(drawFrame);
|
||
|
||
let interact = (e) => {
|
||
particles.setPosition([e.offsetX, e.offsetY]);
|
||
particles.setRate(e.pressure * 1000);
|
||
};
|
||
document.getElementById('trail').addEventListener('pointermove', interact);
|
||
document.getElementById('trail').addEventListener('pointerdown', interact);
|
||
document.getElementById('trail').addEventListener('pointerup', interact);
|
||
preventScrolling(document.getElementById('trail'));
|
||
}
|
||
|
||
const trail = {
|
||
"MaxCount": 2000,
|
||
"Drawable": {
|
||
"Type": "SkCircleDrawable",
|
||
"Radius": 4
|
||
},
|
||
"EffectCode": "",
|
||
"Code": [
|
||
"void spawn(inout Particle p) {",
|
||
" p.lifetime = 2 + rand;",
|
||
" float a = radians(rand * 360);",
|
||
" p.vel = float2(cos(a), sin(a)) * mix(5, 15, rand);",
|
||
" p.scale = mix(0.25, 0.75, rand);",
|
||
"}",
|
||
"",
|
||
"void update(inout Particle p) {",
|
||
" p.color.r = p.age;",
|
||
" p.color.g = 1 - p.age;",
|
||
"}",
|
||
""
|
||
],
|
||
"Bindings": []
|
||
};
|
||
|
||
}
|
||
document.head.appendChild(s);
|
||
})();
|
||
</script>
|