three-layout-box
v0.2.1
Published
A small helper for binding Three.js objects to HTML elements.
Downloads
3
Readme
Three Layout Box
A small helper for binding Three.js objects to HTML elements.
What it looks like
The library exports the ThreeLayoutBox
class and the resizeCamera
function. The resizeCamera
function resizes the camera so that the sizes of objects correspond to CSS pixels. The class binds a Three.js object to an HTML element, obtaining its dimensions, position, rotation, and transformation.
import * as THREE from 'three'
import { ThreeLayoutBox, resizeCamera } from 'three-layout-box'
...
const camera = THREE.PerspectiveCamera()
const geo = new THREE.BoxGeometry()
const mat = new THREE.MeshStandardMaterial()
const mesh = new THREE.Mesh()
const box = new ThreeLayoutBox({
element: '.element-selector',
object3d: mesh,
})
function resize() {
...
resizeCamera(camera)
box.resize()
...
}
function animationFrame() {
...
box.update()
...
}
After binding the object, ThreeLayoutBox
starts managing its position, size, and rotation. To intervene in these processes without disrupting the current values, the library implements a system of steps.
// 🛑
mesh.position.x = 100
// ✅
box.setPositionStep('🍓', '+', {
x: 100,
})
After calling the box.update()
method, the values of all steps will be calculated and assigned to the corresponding properties of the object.
ThreeLayoutBox
Constructor parameters
interface ThreeLayotBoxParameters {
element: HTMLElement | string
object3d?: Object3D
containerElement?: HTMLElement | string
scrollAxis?: 'x' | 'y' | 'z' | 'auto'
frontSide?: 'left' | 'top'
sizeStep?: boolean
positionStep?: boolean
}
element
HTML element or element selector (uses querySelector
).
object3d?
Any Three.js object that inherits from THREE.Object3D
.
containerElement?
This element should correspond to the element containing the Three.js canvas. By default, document.body
is used.
scrollAxis?
The axis along which the Three.js object will scroll. By default, auto
is used.
frontSide?
If z
is specified in scrollAxis
and you have horizontal scrolling, you need to specify left
in this parameter for correct position calculations. By default, top
is used.
sizeStep?
If set to false
, the Three.js object will not receive the element's size (CSS scale
will still affect the object's size). By default, true
is used.
positionStep?
If set to false
, the Three.js object will not receive the element's position (CSS translate
will still affect the object's position). By default, true
is used.
Instance properties
class ThreeLayoutBox {
get element(): HTMLElement
get containerElement(): HTMLElement
get position(): {
x: number
y: number
z: number
}
get rotation(): {
x: number
y: number
z: number
}
get scale(): {
x: number
y: number
z: number
}
}
element
The element used to obtain dimensions and position.
containerElement
The element used to obtain viewport dimensions.
position
An object that stores the calculated position of the element.
scale
An object that stores the calculated size of the element.
rotation
An object that stores the calculated rotation of the element.
Instance methods
class ThreeLayoutBox {
bindObject(object: Object3D): void
unbindObject(object: Object3D): void
resize: () => void
update: () => void
destroy(): void
setScrollStep(callback: () => { axis: 'x' | 'y'; value: number }): () => void
deleteScrollStep(callback: () => { axis: 'x' | 'y'; value: number }): void
setPositionStep(
stepName: string,
action: '+' | '-' | '*' | '/',
value: { x?: number; y?: number; z: number }
): void
setRotationStep(
stepName: string,
action: '+' | '-' | '*' | '/',
value: { x?: number; y?: number; z: number }
): void
setScaleStep(
stepName: string,
action: '+' | '-' | '*' | '/',
value: { x?: number; y?: number; z: number }
): void
deletePositionStep(stepName: string): void
deleteRotationStep(stepName: string): void
deleteScaleStep(stepName: string): void
}
bindObject(object)
Binds the object to the element and, for convenience, writes the current instance to userData.box. Same as passing the object to the constructor.
unbindObject(object)
Unbinds the object from the element.
resize()
After calling this method, the dimensions, position, rotation, and transformation of the element are read, and these values are written to the steps. This method should be called every time the window is resized.
update()
After calling this method, all steps are calculated, and the results are written to the position
, scale
, rotation
properties of the object. This method should be called before rendering.
destroy()
This method should be called when the box is no longer needed.
set[Position | Rotation | Scale]Step(stepName, operation, object)
After binding the object, ThreeLayoutBox
starts managing its position, size, and rotation. To intervene in these processes without disrupting the current values, the library implements a system of steps. The results of your created steps and the steps set by the library will be calculated and written to the corresponding object properties.
box.setPositionStep('any name', '+', {
x: 100,
y: 100,
z: 100,
})
box.setRotationStep('any name', '+', {
z: Math.PI / 2,
})
box.setScaleStep('any name', '/', {
y: 2,
})
delete[Position | Rotation | Scale]Step(stepName)
Deletes the specified step.
setScrollStep(callback)
Sets a scroll step slightly differently, but the same mechanism is used inside the library.
box.setScrollStep(() => {
return {
axis: 'y',
value: scrollY,
}
})
❗️ NOTE ❗️
Native scrolling is not always synchronized with requestAnimationFrame
, and if you render the scene inside the animation frame, you may notice a slight offset from the scroll.
The problem can be solved by updating and rendering everything inside the scroll event:
addEventListener('scroll', () => {
// Render
})
But this approach is suitable only in very rare cases when there is no dynamics, so use scroll libraries that implement requestAnimationFrame
.
Here is an example with the Lenis library:
...
const lenis = new Lenis()
box.setScrollStep(() => {
return {
axis: lenis.isHorizontal ? 'x' : 'y',
value: lenis.animatedScroll,
}
})
function animationFrame(t: number) {
lenis.raf(t)
box.update()
// Render
...
}
deleteScrollStep(callback)
Deletes the scroll step.
box.deleteScrollStep(callback)
Static methods
For convenience, several methods have been made to control all boxes at once.
class LayoutBox {
static resizeBoxes(): void
static updateBoxes(): void
static destroyBoxes(): void
}
resizeBoxes()
Resizes all created boxes at once.
function resize() {
LayoutBox.resizeBoxes()
}
updateBoxes()
Updates all created boxes at once.
function animationFrame() {
LayoutBox.updateBoxes()
}
destroyBoxes()
Destroys all created boxes at once.
LayoutBox.destroyBoxes()
resizeCamera
function resizeCamera(
camera: PerspectiveCamera | OrthographicCamera,
options?: {
width?: number
height?: number
cameraDistance?: number
cameraFar?: number
}
)
Resizes the camera so that the sizes of objects correspond to CSS pixels. By default, after such a call:
resizeCamera(camera)
the following values are set:
camera.userData.distance = 1000
camera.position.z = 1000
camera.near = 1
camera.far = 11000
CSS
You can use any CSS values for positioning or setting sizes, but you should understand that they are applied to the object only after calling the resize
method and then update
. Since the resize
method is most often called once after resizing the window, CSS animations and transition effects will not affect the bound object.
Since CSS does not have a property for creating depth, you can use the --depth
variable for these purposes:
.element {
width: 100px;
height: 100px;
--depth: 100px;
}
The object in depth is always in the middle of the element. In the example above, the object will be in front of the element by 50px. To move it back to match positions, use transform
:
.element {
width: 100px;
height: 100px;
--depth: 100px;
transform: translateZ(-50px);
}