gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
237 lines (236 loc) • 9.27 kB
JavaScript
import { throwWarning } from "../../utils/utils.mjs";
import { isCurtainsRenderer } from "../../core/renderers/utils.mjs";
import { DOMTexture } from "../textures/DOMTexture.mjs";
import { ProjectedMeshBaseMixin } from "../../core/meshes/mixins/ProjectedMeshBaseMixin.mjs";
import { DOMObject3D } from "../objects3D/DOMObject3D.mjs";
//#region src/curtains/meshes/DOMMesh.ts
/** @const - default {@link DOMMesh} parameters. */
const defaultDOMMeshParams = {
autoloadSources: true,
watchScroll: true,
domTextures: []
};
/**
* Create a {@link core/meshes/Mesh.Mesh | Mesh} based on a {@link DOMObject3D}, which allow the {@link core/meshes/Mesh.Mesh | Mesh} to be scaled and positioned based on a {@link HTMLElement} {@link DOMElementBoundingRect | bounding rectangle}.
*
* @example
* ```javascript
* // set our main GPUCurtains instance
* const gpuCurtains = new GPUCurtains({
* container: '#canvas' // selector of our WebGPU canvas container
* })
*
* // set the GPU device
* // note this is asynchronous
* await gpuCurtains.setDevice()
*
* // create a DOMMesh with a box geometry,
* // assuming there's a HTML element with the "mesh" ID in the DOM
* // will use the normals colors as default shading
* const domMesh = new DOMMesh(gpuCurtains, '#mesh', {
* label: 'My DOM Mesh',
* geometry: new BoxGeometry(),
* })
* ```
*/
var DOMMesh = class extends ProjectedMeshBaseMixin(DOMObject3D) {
/**
* DOMMesh constructor
* @param renderer - {@link GPUCurtainsRenderer} object or {@link GPUCurtains} class object used to create this {@link DOMMesh}.
* @param element - {@link HTMLElement} or string representing an {@link HTMLElement} selector used to scale and position the {@link DOMMesh}.
* @param parameters - {@link DOMMeshParams | parameters} used to create this {@link DOMMesh}.
*/
constructor(renderer, element, parameters) {
parameters = {
...defaultDOMMeshParams,
...parameters
};
const { autoloadSources, watchScroll, domTextures, ...projectedMeshParams } = parameters;
super(renderer, element, parameters);
this._onLoadingCallback = (texture) => {};
isCurtainsRenderer(renderer, parameters.label ? parameters.label + " DOMMesh" : "DOMMesh");
this.type = "DOMMesh";
this.domTextures = [];
domTextures.forEach((texture) => {
this.addTexture(texture);
this.onDOMTextureAdded(texture);
});
this.autoloadSources = autoloadSources;
this.sourcesReady = false;
this.setInitSources();
}
/**
* Set or reset this {@link DOMMesh} {@link DOMMesh.renderer | renderer}.
* @param renderer - New {@link GPUCurtainsRenderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.renderer) this.renderer.domMeshes = this.renderer.domMeshes.filter((m) => m.uuid !== this.uuid);
renderer = isCurtainsRenderer(renderer, this.options.label + " DOMMesh");
super.setRenderer(renderer);
this.renderer = renderer;
this.renderer.domMeshes.push(this);
}
/**
* Get/set whether our {@link material} and {@link geometry} are ready.
* @readonly
*/
get ready() {
return this._ready;
}
set ready(value) {
if (value && !this._ready && this.sourcesReady) this._onReadyCallback && this._onReadyCallback();
this._ready = value;
}
/**
* Get/set whether all the initial {@link DOMMesh} sources have been successfully loaded.
* @readonly
*/
get sourcesReady() {
return this._sourcesReady;
}
set sourcesReady(value) {
if (value && !this._sourcesReady && this.ready) this._onReadyCallback && this._onReadyCallback();
this._sourcesReady = value;
}
/**
* Add a {@link DOMMesh} to the {@link core/scenes/Scene.Scene | Scene} and optionally to the renderer.
* @param addToRenderer - whether to add this {@link DOMMesh} to the {@link GPUCurtainsRenderer#meshes | renderer meshes array} and {@link GPUCurtainsRenderer#domMeshes | renderer domMeshes array}.
*/
addToScene(addToRenderer = false) {
super.addToScene(addToRenderer);
if (addToRenderer) this.renderer.domMeshes.push(this);
}
/**
* Remove a {@link DOMMesh} from the {@link core/scenes/Scene.Scene | Scene} and optionally from the renderer as well.
* @param removeFromRenderer - whether to remove this {@link DOMMesh} from the {@link GPUCurtainsRenderer#meshes | renderer meshes array} and {@link GPUCurtainsRenderer#domMeshes | renderer domMeshes array}.
*/
removeFromScene(removeFromRenderer = false) {
super.removeFromScene(removeFromRenderer);
if (removeFromRenderer) this.renderer.domMeshes = this.renderer.domMeshes.filter((m) => m.uuid !== this.uuid);
}
/**
* Resize the {@link textures} and {@link domTextures}.
*/
resizeTextures() {
super.resizeTextures();
this.domTextures?.forEach((texture) => {
texture.resize();
});
}
/**
* Apply scale and update {@link DOMTexture#modelMatrix | DOMTexture modelMatrix}.
*/
applyScale() {
super.applyScale();
this.domTextures?.forEach((texture) => {
texture.updateModelMatrix();
});
}
/**
* Create a new {@link DOMTexture}.
* @param options - {@link DOMTextureParams | DOMTexture parameters}.
* @returns - newly created {@link DOMTexture}.
*/
createDOMTexture(options) {
const defaultName = "texture" + this.textures.length;
if (!options.label) options.label = this.options.label + " " + (options.name ?? defaultName);
if (!options.name) options.name = defaultName;
const { viewDimension, useTransform, ...domTextureParams } = this.options.texturesOptions;
const texturesOptions = {
...options,
...domTextureParams
};
if (this.renderBundle) texturesOptions.useExternalTextures = false;
const domTexture = new DOMTexture(this.renderer, texturesOptions);
this.addTexture(domTexture);
this.onDOMTextureAdded(domTexture);
return domTexture;
}
/**
* Callback run when a new {@link DOMTexture} has been added.
* @param domTexture - newly created DOMTexture.
*/
onDOMTextureAdded(domTexture) {
domTexture.mesh = this;
this.domTextures.push(domTexture);
}
/**
* Load initial {@link DOMMesh} sources if needed and create associated {@link DOMTexture}.
*/
setInitSources() {
let loaderSize = 0;
let sourcesLoaded = 0;
if (this.autoloadSources) {
const images = this.domElement.element.querySelectorAll("img");
const videos = this.domElement.element.querySelectorAll("video");
const canvases = this.domElement.element.querySelectorAll("canvas");
loaderSize = images.length + videos.length + canvases.length;
const onSourceUploaded = (texture) => {
sourcesLoaded++;
this._onLoadingCallback && this._onLoadingCallback(texture);
if (sourcesLoaded === loaderSize) this.sourcesReady = true;
};
if (!loaderSize) this.sourcesReady = true;
if (images.length) images.forEach((image) => {
const texture = this.createDOMTexture({ name: image.getAttribute("data-texture-name") ?? "texture" + this.domTextures.length });
texture.onAllSourcesUploaded(() => onSourceUploaded(texture)).loadImage(image.src);
});
if (videos.length) videos.forEach((video) => {
const texture = this.createDOMTexture({ name: video.getAttribute("data-texture-name") ?? "texture" + this.domTextures.length });
texture.onAllSourcesUploaded(() => onSourceUploaded(texture)).loadVideo(video);
});
if (canvases.length) canvases.forEach((canvas) => {
const texture = this.createDOMTexture({ name: canvas.getAttribute("data-texture-name") ?? "texture" + this.domTextures.length });
texture.onAllSourcesUploaded(() => onSourceUploaded(texture)).loadCanvas(canvas);
});
} else this.sourcesReady = true;
}
/**
* Reset/change the {@link domElement | DOM Element}.
* @param element - new {@link HTMLElement} or string representing an {@link HTMLElement} selector to use.
*/
resetDOMElement(element) {
if (!!element) {
super.resetDOMElement(element);
this.domTextures.forEach((texture) => texture.resize());
} else if (!element && !this.renderer.production) throwWarning(`${this.options.label}: You are trying to reset a ${this.type} with a HTML element that does not exist. The old HTML element will be kept instead.`);
}
/**
* Get our {@link DOMMesh#domElement | DOM Element} {@link core/DOM/DOMElement.DOMElement#boundingRect | bounding rectangle} accounting for current {@link core/renderers/GPURenderer.GPURenderer#pixelRatio | renderer pixel ratio}.
*/
get pixelRatioBoundingRect() {
const devicePixelRatio = window.devicePixelRatio ?? 1;
const scaleBoundingRect = this.renderer.pixelRatio / devicePixelRatio;
return Object.keys(this.domElement.boundingRect).reduce((a, key) => ({
...a,
[key]: this.domElement.boundingRect[key] * scaleBoundingRect
}), {
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0
});
}
/**
* Compute the Mesh geometry if needed.
*/
computeGeometry() {
super.computeGeometry();
this.boundingBox.copy(this.geometry.boundingBox);
}
/**
* Called each time one of the initial sources associated {@link DOMTexture#texture | GPU texture} has been uploaded to the GPU.
* @param callback - callback to call each time a {@link DOMTexture#texture | GPU texture} has been uploaded to the GPU.
* @returns - our {@link DOMMesh}.
*/
onLoading(callback) {
if (callback) this._onLoadingCallback = callback;
return this;
}
};
//#endregion
export { DOMMesh };