blob: 527069c956dbf0cda4a755351ae4ebe7c662278e [file] [log] [blame]
/**
* @module particles-sk
* @description <h2><code>particles-sk</code></h2>
*
* <p>
* The main application element for particles in Skia.
* </p>
*
*/
import { $$ } from 'common-sk/modules/dom'
import { html, render } from 'lit-html'
import 'elements-sk/spinner-sk'
const CanvasKitInit = require('../../build/canvaskit/canvaskit.js');
const ZOOM_IN_FACTOR = 1.1; // 10%
const ZOOM_OUT_FACTOR = 1/ZOOM_IN_FACTOR;
// 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.src).origin;
const kitReady = CanvasKitInit({
locateFile: (file) => {
return `${scriptOrigin}/static/${file}`;
},
}).ready();
const loadingTemplate = (ele) => html`
<div class=player-loading title="Loading particles and engine."
style='width: ${ele._config.width}px; height: ${ele._config.height}px;'>
<div>Loading</div>
<spinner-sk active></spinner-sk>
</div>`;
const runningTemplate = (ele) => html`
<div class=container>
<!-- It would be more mobile friendly to use pointermove, but Safari doesn't support it-->
<canvas id=player
@wheel=${ele._wheel}
@mousemove=${ele._drag}
width=${ele._config.width * window.devicePixelRatio}
height=${ele._config.height * window.devicePixelRatio}
style='width: ${ele._config.width}px; height: ${ele._config.height}px;'>
Your browser does not support the canvas tag.
</canvas>
</div>`;
window.customElements.define('particles-player-sk', class extends HTMLElement {
constructor() {
super();
this._engine = {
kit: null, // CanvasKit instance
context: null, // CK context.
animation: null, // Particles instance
surface: null, // SkSurface
canvas: null, // Cached SkCanvas (surface.getCanvas()).
};
this._state = {
loading: true,
paused: this.hasAttribute('paused'),
time: 0, // a monotonically increasing amount of ms
lastTs: 0, // last time stamp we had a frame
};
this._lastDrag = null;
this._zoomLevel = 1.0;
}
connectedCallback() {
this._config = {
width: this.hasAttribute('width') ? +this.getAttribute('width') : 256,
height: this.hasAttribute('height') ? +this.getAttribute('height') : 256,
bgcolor: this.hasAttribute('bgcolor') ? +this.getAttribute('bgcolor') : 4278190080, // black
};
this.render();
}
_drag(e) {
if (!e.buttons || !e.shiftKey) { // ignore movements unless shift is held
this._lastDrag = null;
return;
}
if (this._lastDrag) {
const dx = e.clientX - this._lastDrag[0];
const dy = e.clientY - this._lastDrag[1];
const canvas = this._engine.canvas.translate(dx / this._zoomLevel,
dy / this._zoomLevel);
}
this._lastDrag = [e.clientX, e.clientY];
}
_drawFrame() {
if (!this._engine.animation || !this._engine.canvas) {
return;
}
window.requestAnimationFrame(this._drawFrame.bind(this));
if (!this._state.lastTs) {
this._engine.animation.start(0, true);
this._state.lastTs = Date.now();
}
if (this.isPlaying()) {
this._state.time += (Date.now() - this._state.lastTs);
}
this._state.lastTs = Date.now();
this._engine.kit.setCurrentContext(this._engine.context);
this._engine.canvas.clear(this._config.bgcolor);
this._engine.animation.update(this._state.time / 1000.0);
this._engine.animation.draw(this._engine.canvas);
this._engine.surface.flush();
}
initialize(config) {
this._config.width = config.width;
this._config.height = config.height;
this.render();
return kitReady.then((ck) => {
this._engine.kit = ck;
this._initializeParticles(config.json);
this.render();
});
}
_initializeParticles(particlesJSON, assets) {
this._state.loading = false;
// Rebuild the surface only if needed.
if (!this._engine.surface ||
this._engine.surface.width != this._config.width ||
this._engine.surface.height != this._config.height) {
this.render();
this._engine.surface && this._engine.surface.delete();
let canvasEle = $$('#player', this);
this._engine.surface = this._engine.kit.MakeCanvasSurface(canvasEle);
if (!this._engine.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._engine.canvas = this._engine.surface.getCanvas();
this._engine.context = this._engine.kit.currentContext();
}
this._engine.animation && this._engine.animation.delete();
this._engine.animation = this._engine.kit.MakeParticles(
JSON.stringify(particlesJSON));
if (!this._engine.animation) {
throw new Error('Could not parse Particles JSON.');
}
this._engine.canvas.clear(this._config.bgcolor);
// Center the animation
this.resetView();
this.restartAnimation();
this._drawFrame();
}
isPlaying() {
return !this._state.paused;
}
play() {
if (!this.isPlaying()) {
this._state.paused = false;
}
this.render();
}
pause() {
if (this.isPlaying()) {
this._state.paused = true;
}
}
render() {
render(this._state.loading
? loadingTemplate(this)
: runningTemplate(this),
this, {eventContext: this});
}
resetView() {
const ck = this._engine.kit;
const canvas = this._engine.canvas;
// Reset to identity
const tt = canvas.getTotalMatrix();
const itt = ck.SkMatrix.invert(tt);
canvas.concat(itt);
// Zoom to the middle of the animation
canvas.translate(this._config.width/2, this._config.height/2);
this._zoomLevel = 1.0;
}
restartAnimation() {
this._state.time = 0;
this._state.lastTs = 0;
}
_wheel(e) {
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._engine.kit;
const canvas = this._engine.canvas;
const tt = canvas.getTotalMatrix();
const itt = ck.SkMatrix.invert(tt);
const pts = [e.clientX, e.clientY];
ck.SkMatrix.mapPoints(itt, pts); // Transform DOM pts into canvas space
let matr = ck.SkMatrix.scaled(zoom, zoom, pts[0], pts[1]);
canvas.concat(matr);
}
});