blob: 170d4a4610936fb40a39372b174bff2fdbfb0417 [file] [log] [blame]
/**
* @module particles-sk
* @description <h2><code>particles-player-sk</code></h2>
*
* <p>
* Handles the bulk of the work displaying Particles.
* </p>
*
*/
import { $$ } from 'common-sk/modules/dom';
import { define } from 'elements-sk/define';
import { errorMessage } from 'elements-sk/errorMessage';
import { html, TemplateResult } from 'lit-html';
import type {
Particles, CanvasKit, Surface, Canvas, CanvasKitInit as CKInit,
} from 'canvaskit-wasm';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
// It is assumed that canvaskit.js has been loaded and this symbol is available globally.
declare const CanvasKitInit: typeof CKInit;
const DEFAULT_SIZE = 256;
const ZOOM_IN_FACTOR = 1.1; // 10%
const ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR;
export interface PlayerConfig {
body: any;
width: number;
height: number;
}
// This element might be loaded from a different site, and that means we need
// to be careful about how we construct the URL back to the canvas.wasm file.
// Start by recording the script origin.
const scriptOrigin = new URL((document!.currentScript as HTMLScriptElement).src).origin;
const kitReady = CanvasKitInit({
locateFile: (file: any) => `${scriptOrigin}/dist/${file}`,
});
/**
* Information needed to construct a single HTML control for a uniform. Note
* that some uniforms actually represent more than one control, such as a
* 'float3', in which case code will need to create three instances of
* UniformControl.
*/
interface UniformControl {
id: string;
uniformSlot: number;
}
interface Point {
x: number;
y: number;
}
export function floatSlider(uniform: UniformControl | null): TemplateResult {
if (!uniform) {
return html``;
}
return html` <div class="widget">
<input
name=${uniform.id}
id=${uniform.id}
min="0"
max="1"
step="0.00001"
type="range"
/>
<label for=${uniform.id}>${uniform.id}</label>
</div>`;
}
export class ParticlesPlayerSk extends ElementSk {
private sliders: UniformControl[] = [];
private zoomLevel: number = 1.0;
private kit: CanvasKit | null = null; // CanvasKit instance
private animation: Particles | null = null; // Particles instance
private surface: Surface | null = null; // Surface
private canvas: Canvas | null = null; // Cached Canvas (surface.getCanvas()).
private time: number = 0;
private lastTime: number = 0;
private lastDrag: Point | null = null;
constructor() {
super(ParticlesPlayerSk.template);
}
private static template = (ele: ParticlesPlayerSk) => html`
<div class="container">
${ele.sliders.map(floatSlider)}
<canvas
id="player"
@wheel=${ele.wheelHandler}
@mousemove=${ele.dragHandler}
width=${ele.width * window.devicePixelRatio}
height=${ele.height * window.devicePixelRatio}
style="width: ${ele.width}px; height: ${ele.height}px;"
>
Your browser does not support the canvas tag.
</canvas>
</div>`;
connectedCallback(): void {
super.connectedCallback();
this._render();
}
attributeChangedCallback(): void {
this._render();
}
initialize(config: PlayerConfig): Promise<void> {
this.width = config.width;
this.height = config.height;
this._render();
return kitReady.then((ck: CanvasKit) => {
this.kit = ck;
try {
this._initializeParticles(config.body);
} catch (error) {
errorMessage(error);
}
this._render();
});
}
isPlaying(): boolean {
return !this.paused;
}
play(): void {
if (!this.isPlaying()) {
this.paused = false;
}
this._render();
}
pause(): void {
if (this.isPlaying()) {
this.paused = true;
}
}
resetView(): void {
const ck = this.kit;
const canvas = this.canvas;
// Reset to identity
const tt = canvas!.getTotalMatrix();
const itt = ck!.Matrix.invert(tt)!;
canvas!.concat(itt);
// Zoom to the middle of the animation
canvas!.translate(this.width / 2, this.height / 2);
this.zoomLevel = 1.0;
}
restartAnimation(): void {
this.time = 0;
this.lastTime = 0;
}
private dragHandler(e: MouseEvent) {
if (!e.buttons) {
this.lastDrag = null;
return;
}
if (this.lastDrag) {
const dx = e.clientX - this.lastDrag.x;
const dy = e.clientY - this.lastDrag.y;
this.canvas!.translate(dx / this.zoomLevel, dy / this.zoomLevel);
}
this.lastDrag = {
x: e.clientX,
y: e.clientY,
};
}
private drawFrame() {
if (!this.animation || !this.canvas) {
return;
}
// Go through all the sliders on the page that we created and poll those inputs for their
// value. Plug those values (range [0.0, 1.0]) into the uniforms.
const uniforms = this.animation.uniforms();
this.sliders.forEach((slider) => {
const s = $$<HTMLInputElement>(`input#${slider.id}`, this);
if (!s) {
return;
}
uniforms[slider.uniformSlot] = s.valueAsNumber;
});
window.requestAnimationFrame(() => this.drawFrame());
if (!this.lastTime) {
this.animation.start(0, true);
this.lastTime = Date.now();
}
if (this.isPlaying()) {
this.time += Date.now() - this.lastTime;
}
this.lastTime = Date.now();
this.canvas.clear(this.kit!.BLACK);
this.animation.update(this.time / 1000.0);
this.animation.draw(this.canvas);
this.surface!.flush();
}
private _initializeParticles(particlesJSON: any) {
// Rebuild the surface only if needed.
if (
!this.surface
|| (this.surface!.width() !== this.width)
|| (this.surface!.height() !== this.height)
) {
this._render();
// eslint-disable-next-line no-unused-expressions
this.surface?.delete();
const canvasEle = $$<HTMLCanvasElement>('#player', this)!;
this.surface = this.kit!.MakeCanvasSurface(canvasEle);
if (!this.surface) {
throw new Error('Could not make SkSurface.');
}
// We don't need to call .delete() on the canvas because
// the parent surface will do that for us.
this.canvas = this.surface.getCanvas();
}
// eslint-disable-next-line no-unused-expressions
this.animation?.delete();
this.animation = this.kit!.MakeParticles(
JSON.stringify(particlesJSON),
);
if (!this.animation) {
throw new Error('Could not parse Particles JSON.');
}
// Go through all uniforms this animation has and look for those with the
// prefix 'slider_' For those uniforms, we will make a slider on the UI and
// then every frame, we will poll those inputs for their value and plug the
// values into the uniforms. The sliders will be in range [0.0, 1.0]. Note
// that the matrices are column major.
// TODO(jcgregorio) Group rows together on the display so matrices look like
// matrices.
// TODO(jgrergorio) If the name contains "color" then either display a color
// picker or at the very least change the postfixes to _r, _g_, and _b.
// TODO(jcgregorio) Break out the uniforms handling into its own element
// to be re-used on shaders.skia.org.
this.sliders = [];
const an = this.animation;
for (let i = 0; i < an.getUniformCount(); i++) {
const name = an.getUniformName(i);
if (name.startsWith('slider_')) {
const uniform = an.getUniform(i);
for (let row = 0; row < uniform.rows; row++) {
for (let col = 0; col < uniform.columns; col++) {
let id = `${name.substring('slider_'.length)}`;
if (uniform.rows > 1) {
id += `_${row}`;
}
if (uniform.columns > 1) {
id += `_${col}`;
}
this.sliders.push({
id: id,
uniformSlot: uniform.slot + row + col * uniform.rows,
});
}
}
}
}
this._render();
this.canvas!.clear(this.kit!.BLACK);
this.resetView();
this.restartAnimation();
this.drawFrame();
}
private wheelHandler(e: WheelEvent) {
e.preventDefault();
e.stopPropagation();
let zoom = 0;
if (e.deltaY < 0) {
zoom = ZOOM_IN_FACTOR;
} else {
zoom = ZOOM_OUT_FACTOR;
}
this.zoomLevel *= zoom;
const ck = this.kit;
const canvas = this.canvas;
const tt = canvas!.getTotalMatrix();
const itt = ck!.Matrix.invert(tt)!;
const pts = [e.clientX, e.clientY];
ck!.Matrix.mapPoints(itt, pts); // Transform DOM pts into canvas space
const matr = ck!.Matrix.scaled(zoom, zoom, pts[0], pts[1]);
canvas!.concat(matr);
}
static get observedAttributes(): string[] {
return ['width', 'height', 'paused'];
}
get width(): number { return +(this.getAttribute('width') || DEFAULT_SIZE); }
set width(val: number) { this.setAttribute('width', val.toFixed(0)); }
get height(): number { return +(this.getAttribute('height') || DEFAULT_SIZE); }
set height(val: number) { this.setAttribute('height', val.toFixed(0)); }
get paused(): boolean { return this.hasAttribute('paused'); }
set paused(val: boolean) {
if (val) {
this.setAttribute('paused', '');
} else {
this.removeAttribute('paused');
}
}
}
define('particles-player-sk', ParticlesPlayerSk);