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.
386 lines (383 loc) • 14.9 kB
JavaScript
import { ProjectedObject3D } from '../../core/objects3D/ProjectedObject3D.mjs';
import { isCurtainsRenderer } from '../../core/renderers/utils.mjs';
import { DOMElement } from '../../core/DOM/DOMElement.mjs';
import { Vec2 } from '../../math/Vec2.mjs';
import { Vec3 } from '../../math/Vec3.mjs';
import { Box3 } from '../../math/Box3.mjs';
var __typeError = (msg) => {
throw TypeError(msg);
};
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
var _DOMObjectWorldPosition, _DOMObjectWorldScale, _DOMObjectDepthScaleRatio;
class DOMObject3D extends ProjectedObject3D {
/**
* 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);
/** 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. */
__privateAdd(this, _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}. */
__privateAdd(this, _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. */
__privateAdd(this, _DOMObjectDepthScaleRatio, 1);
/** Helper {@link Box3 | bounding box} used to map the 3D object onto the 2D DOM element. */
this.boundingBox = new Box3(new Vec3(-1), new Vec3(1));
/** function assigned to the {@link onAfterDOMElementResize} callback. */
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();
}
/* BOUNDING BOXES GETTERS */
/**
* 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
};
}
/* TRANSFOMS */
/**
* Set our transforms properties and {@link Vec3#onChange | onChange vector} callbacks.
*/
setTransforms() {
super.setTransforms();
this.transforms.origin.model.set(0.5, 0.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 __privateGet(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 __privateGet(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);
}
__privateGet(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();
}
/* MATRICES */
/**
* Update the {@link modelMatrix | model matrix} accounting the {@link DOMObject3D} world position and {@link DOMObject3D} world scale.
*/
updateModelMatrix() {
this.modelMatrix.composeFromOrigin(
__privateGet(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() {
__privateGet(this, _DOMObjectWorldScale).set(
this.size.scaledWorld.size.x,
this.size.scaledWorld.size.y,
this.size.scaledWorld.size.z * __privateGet(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) {
__privateSet(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) * // between -1 and 1
__privateGet(this, _DOMObjectWorldScale).x,
-(this.transformOrigin.y * 2 - 1) * // between -1 and 1
__privateGet(this, _DOMObjectWorldScale).y,
this.transformOrigin.z * __privateGet(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();
}
}
_DOMObjectWorldPosition = new WeakMap();
_DOMObjectWorldScale = new WeakMap();
_DOMObjectDepthScaleRatio = new WeakMap();
export { DOMObject3D };