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.
323 lines (322 loc) • 13.2 kB
JavaScript
import { Vec3 } from "../../math/Vec3.mjs";
import { isCurtainsRenderer } from "../../core/renderers/utils.mjs";
import { Vec2 } from "../../math/Vec2.mjs";
import { Box3 } from "../../math/Box3.mjs";
import { ProjectedObject3D } from "../../core/objects3D/ProjectedObject3D.mjs";
import { DOMElement } from "../../core/DOM/DOMElement.mjs";
//#region src/curtains/objects3D/DOMObject3D.ts
/**
* This special kind of {@link ProjectedObject3D} uses an {@link HTMLElement} to convert the corresponding X and Y {@link DOMObject3D#scale | scale} and {@link DOMObject3D#position | position} relative to the 3D world space.
*
* Internally used by the {@link curtains/meshes/DOMMesh.DOMMesh | DOMMesh} and {@link curtains/meshes/Plane.Plane | Plane}, but can also be used as any {@link core/meshes/Mesh.Mesh | Mesh} {@link parent} to map it with an {@link HTMLElement} size and position values.
*/
var DOMObject3D = class extends ProjectedObject3D {
/** Private {@link Vec3 | vector} used to keep track of the actual {@link DOMObject3DTransforms#position.world | world position} accounting the {@link DOMObject3DTransforms#position.document | additional document translation} converted into world space. */
#DOMObjectWorldPosition = new Vec3();
/** Private {@link Vec3 | vector} used to keep track of the actual {@link DOMObject3D} world scale accounting the {@link DOMObject3D#size.world | DOMObject3D world size}. */
#DOMObjectWorldScale = new Vec3(1);
/** Private number representing the scale ratio of the {@link DOMObject3D} along Z axis to apply. Since it can be difficult to guess the most accurate scale along the Z axis of an object mapped to 2D coordinates, this helps with adjusting the scale along the Z axis. */
#DOMObjectDepthScaleRatio = 1;
/**
* DOMObject3D constructor
* @param renderer - {@link GPUCurtainsRenderer} object or {@link GPUCurtains} class object used to create this {@link DOMObject3D}.
* @param element - {@link HTMLElement} or string representing an {@link HTMLElement} selector used to scale and position the {@link DOMObject3D}.
* @param parameters - {@link DOMObject3DParams | parameters} used to create this {@link DOMObject3D}.
*/
constructor(renderer, element, parameters = {}) {
super(renderer);
this.boundingBox = new Box3(new Vec3(-1), new Vec3(1));
this._onAfterDOMElementResizeCallback = () => {};
renderer = isCurtainsRenderer(renderer, "DOMObject3D");
this.renderer = renderer;
this.size = {
shouldUpdate: true,
normalizedWorld: {
size: new Vec2(1),
position: new Vec2()
},
cameraWorld: { size: new Vec2(1) },
scaledWorld: {
size: new Vec3(1),
position: new Vec3()
}
};
this.watchScroll = parameters.watchScroll;
this.camera = this.renderer.camera;
this.boundingBox.min.onChange(() => this.shouldUpdateComputedSizes());
this.boundingBox.max.onChange(() => this.shouldUpdateComputedSizes());
this.setDOMElement(element);
this.renderer.domObjects.push(this);
}
/**
* Set or reset this {@link DOMObject3D} {@link DOMObject3D.renderer | renderer}.
* @param renderer - New {@link GPUCurtainsRenderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.renderer) this.renderer.domObjects = this.renderer.domObjects.filter((object) => object.object3DIndex !== this.object3DIndex);
renderer = isCurtainsRenderer(renderer, "DOMObject3D");
this.renderer = renderer;
this.renderer.domObjects.push(this);
}
/**
* Set the {@link domElement | DOM Element}.
* @param element - {@link HTMLElement} or string representing an {@link HTMLElement} selector to use.
*/
setDOMElement(element) {
this.domElement = new DOMElement({
element,
onSizeChanged: (boundingRect) => this.resize(boundingRect),
onPositionChanged: () => this.onPositionChanged()
});
this.updateSizeAndPosition();
}
/**
* Update size and position when the {@link domElement | DOM Element} position changed.
*/
onPositionChanged() {
if (this.watchScroll) this.shouldUpdateComputedSizes();
}
/**
* Reset the {@link domElement | DOMElement}.
* @param element - The new {@link HTMLElement} or string representing an {@link HTMLElement} selector to use.
*/
resetDOMElement(element) {
if (this.domElement) this.domElement.destroy();
this.setDOMElement(element);
}
/**
* Resize the {@link DOMObject3D}.
* @param boundingRect - New {@link domElement | DOM Element} {@link DOMElement#boundingRect | bounding rectangle}.
*/
resize(boundingRect = null) {
if (!boundingRect && (!this.domElement || this.domElement?.isResizing)) return;
this.updateSizeAndPosition();
this._onAfterDOMElementResizeCallback && this._onAfterDOMElementResizeCallback();
}
/**
* Get the {@link domElement | DOM Element} {@link DOMElement#boundingRect | bounding rectangle}.
* @readonly
*/
get boundingRect() {
return this.domElement?.boundingRect ?? {
width: 1,
height: 1,
top: 0,
right: 0,
bottom: 0,
left: 0,
x: 0,
y: 0
};
}
/**
* Set our transforms properties and {@link Vec3#onChange | onChange vector} callbacks.
*/
setTransforms() {
super.setTransforms();
this.transforms.origin.model.set(.5, .5, 0);
this.transforms.origin.world = new Vec3();
this.transforms.position.document = new Vec3();
this.documentPosition.onChange(() => this.applyPosition());
this.transformOrigin.onChange(() => this.setWorldTransformOrigin());
}
/**
* Get the {@link DOMObject3DTransforms#position.document | additional translation relative to the document}.
*/
get documentPosition() {
return this.transforms.position.document;
}
/**
* Set the {@link DOMObject3DTransforms#position.document | additional translation relative to the document}.
* @param value - Additional translation relative to the document to apply.
*/
set documentPosition(value) {
this.transforms.position.document = value;
this.applyPosition();
}
/**
* Get the {@link domElement | DOM element} scale in world space.
* @readonly
*/
get DOMObjectWorldScale() {
return this.#DOMObjectWorldScale.clone();
}
/**
* Get the {@link DOMObject3D} scale in world space (accounting for {@link scale}).
* @readonly
*/
get worldScale() {
return this.DOMObjectWorldScale.multiply(this.scale);
}
/**
* Get the {@link DOMObject3D} position in world space.
* @readonly
*/
get worldPosition() {
return this.#DOMObjectWorldPosition.clone();
}
/**
* Get the {@link DOMObject3D} transform origin relative to the {@link DOMObject3D}.
*/
get transformOrigin() {
return this.transforms.origin.model;
}
/**
* Set the {@link DOMObject3D} transform origin relative to the {@link DOMObject3D}.
* @param value - New transform origin.
*/
set transformOrigin(value) {
this.transforms.origin.model = value;
this.setWorldTransformOrigin();
}
/**
* Get the {@link DOMObject3D} transform origin in world space.
*/
get worldTransformOrigin() {
return this.transforms.origin.world;
}
/**
* Set the {@link DOMObject3D} transform origin in world space.
* @param value - New world space transform origin.
*/
set worldTransformOrigin(value) {
this.transforms.origin.world = value;
}
/**
* Check whether at least one of the matrix should be updated.
*/
shouldUpdateMatrices() {
super.shouldUpdateMatrices();
if (this.matricesNeedUpdate || this.size.shouldUpdate) {
this.updateSizeAndPosition();
this.matricesNeedUpdate = true;
}
this.size.shouldUpdate = false;
}
/**
* Set the {@link DOMObject3D#size.shouldUpdate | size shouldUpdate} flag to true to compute the new sizes before next matrices calculations.
*/
shouldUpdateComputedSizes() {
this.size.shouldUpdate = true;
}
/**
* Update the {@link DOMObject3D} sizes and position.
*/
updateSizeAndPosition() {
this.setWorldSizes();
this.applyDocumentPosition();
this.shouldUpdateModelMatrix();
}
/**
* Compute the {@link DOMObject3D} world position using its world position and document translation converted to world space.
*/
applyDocumentPosition() {
let worldPosition = new Vec3(0, 0, 0);
if (!this.documentPosition.equals(worldPosition)) worldPosition = this.documentToWorldSpace(this.documentPosition);
this.#DOMObjectWorldPosition.set(this.position.x + this.size.scaledWorld.position.x + worldPosition.x, this.position.y + this.size.scaledWorld.position.y + worldPosition.y, this.position.z + this.size.scaledWorld.position.z + this.documentPosition.z / this.camera.CSSPerspective);
}
/**
* Apply the transform origin and set the {@link DOMObject3D} world transform origin.
*/
applyTransformOrigin() {
if (!this.size) return;
this.setWorldTransformOrigin();
super.applyTransformOrigin();
}
/**
* Update the {@link modelMatrix | model matrix} accounting the {@link DOMObject3D} world position and {@link DOMObject3D} world scale.
*/
updateModelMatrix() {
this.modelMatrix.composeFromOrigin(this.#DOMObjectWorldPosition, this.quaternion, this.scale, this.worldTransformOrigin);
this.modelMatrix.scale(this.DOMObjectWorldScale);
this.shouldUpdateWorldMatrix();
}
/**
* Convert a document position {@link Vec3 | vector} to a world position {@link Vec3 | vector}.
* @param vector - Document position {@link Vec3 | vector} converted to world space.
*/
documentToWorldSpace(vector = new Vec3()) {
return new Vec3(vector.x * this.renderer.pixelRatio / this.renderer.boundingRect.width * this.camera.visibleSize.width, -(vector.y * this.renderer.pixelRatio / this.renderer.boundingRect.height) * this.camera.visibleSize.height, vector.z);
}
/**
* Compute the {@link DOMObject3D#size | world sizes}.
*/
computeWorldSizes() {
const containerBoundingRect = this.renderer.boundingRect;
const planeCenter = {
x: this.boundingRect.width / 2 + this.boundingRect.left,
y: this.boundingRect.height / 2 + this.boundingRect.top
};
const containerCenter = {
x: containerBoundingRect.width / 2 + containerBoundingRect.left,
y: containerBoundingRect.height / 2 + containerBoundingRect.top
};
const { size, center } = this.boundingBox;
if (size.x !== 0 && size.y !== 0 && size.z !== 0) center.divide(size);
this.size.normalizedWorld.size.set(this.boundingRect.width / containerBoundingRect.width, this.boundingRect.height / containerBoundingRect.height);
this.size.normalizedWorld.position.set((planeCenter.x - containerCenter.x) / containerBoundingRect.width, (containerCenter.y - planeCenter.y) / containerBoundingRect.height);
this.size.cameraWorld.size.set(this.size.normalizedWorld.size.x * this.camera.visibleSize.width, this.size.normalizedWorld.size.y * this.camera.visibleSize.height);
this.size.scaledWorld.size.set(this.size.cameraWorld.size.x / size.x, this.size.cameraWorld.size.y / size.y, 1);
this.size.scaledWorld.size.z = this.size.scaledWorld.size.y * (size.x / size.y / (this.boundingRect.width / this.boundingRect.height));
this.size.scaledWorld.position.set(this.size.normalizedWorld.position.x * this.camera.visibleSize.width, this.size.normalizedWorld.position.y * this.camera.visibleSize.height, 0);
}
/**
* Compute and set the {@link DOMObject3D#size.world | world size} and set the {@link DOMObject3D} world transform origin.
*/
setWorldSizes() {
this.computeWorldSizes();
this.setWorldScale();
this.setWorldTransformOrigin();
}
/**
* Set the {@link worldScale} accounting for scaled world size and {@link DOMObjectDepthScaleRatio}.
*/
setWorldScale() {
this.#DOMObjectWorldScale.set(this.size.scaledWorld.size.x, this.size.scaledWorld.size.y, this.size.scaledWorld.size.z * this.#DOMObjectDepthScaleRatio);
this.shouldUpdateMatrixStack();
}
/**
* Set {@link DOMObjectDepthScaleRatio}. Since it can be difficult to guess the most accurate scale along the Z axis of an object mapped to 2D coordinates, this helps with adjusting the scale along the Z axis.
* @param value - Depth scale ratio value to use.
*/
set DOMObjectDepthScaleRatio(value) {
this.#DOMObjectDepthScaleRatio = value;
this.setWorldScale();
}
/**
* Set the {@link DOMObject3D} world transform origin and tell the matrices to update.
*/
setWorldTransformOrigin() {
this.transforms.origin.world = new Vec3((this.transformOrigin.x * 2 - 1) * this.#DOMObjectWorldScale.x, -(this.transformOrigin.y * 2 - 1) * this.#DOMObjectWorldScale.y, this.transformOrigin.z * this.#DOMObjectWorldScale.z);
this.shouldUpdateMatrixStack();
}
/**
* Update the {@link domElement | DOM Element} scroll position.
* @param delta - Last {@link utils/ScrollManager.ScrollManager.delta | scroll delta values}.
*/
updateScrollPosition(delta = {
x: 0,
y: 0
}) {
if (delta.x || delta.y) this.domElement.updateScrollPosition(delta);
}
/**
* Callback to execute just after the {@link domElement} has been resized.
* @param callback - Callback to run just after {@link domElement} has been resized.
* @returns - Our {@link DOMObject3D}.
*/
onAfterDOMElementResize(callback) {
if (callback) this._onAfterDOMElementResizeCallback = callback;
return this;
}
/**
* Destroy our {@link DOMObject3D}.
*/
destroy() {
super.destroy();
this.renderer.domObjects = this.renderer.domObjects.filter((object) => object.object3DIndex !== this.object3DIndex);
this.domElement?.destroy();
}
};
//#endregion
export { DOMObject3D };