@threlte/extras
<HTML>
This component is a port of drei’s <Html>
component. It allows you to tie HTML content to any object of your scene. It will be projected to the objects whereabouts automatically.
The container of your <Canvas>
component needs to be set to position: relative | absolute | sticky | fixed
. This is because the DOM element will be mounted as a sibling to the <canvas>
element.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<div>
<Canvas>
<Scene />
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { T } from '@threlte/core'
import { HTML, OrbitControls } from '@threlte/extras'
import { spring } from 'svelte/motion'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
const getRandomColor = () =>
`#${Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, '0')}`
let color = $state(getRandomColor())
let isHovering = $state(false)
let isPointerDown = $state(false)
let htmlPosZ = spring(0)
$effect(() => {
htmlPosZ.set(isPointerDown ? -0.15 : isHovering ? -0.075 : 0, {
hard: isPointerDown
})
})
</script>
<T.PerspectiveCamera
position={[10, 5, 10]}
makeDefault
fov={30}
oncreate={(ref) => ref.lookAt(0, 0.75, 0)}
>
<OrbitControls
target.y={0.75}
maxPolarAngle={85 * DEG2RAD}
minPolarAngle={20 * DEG2RAD}
maxAzimuthAngle={45 * DEG2RAD}
minAzimuthAngle={-45 * DEG2RAD}
enableZoom={false}
/>
</T.PerspectiveCamera>
<T.DirectionalLight position={[0, 10, 10]} />
<T.AmbientLight intensity={0.3} />
<T.GridHelper />
<T.Mesh position.y={0.5}>
<T.MeshStandardMaterial {color} />
<T.SphereGeometry args={[0.5]} />
<HTML
position.y={1.25}
position.z={$htmlPosZ}
transform
>
<button
onpointerenter={() => (isHovering = true)}
onpointerleave={() => {
isPointerDown = false
isHovering = false
}}
onpointerdown={() => {
isPointerDown = true
color = getRandomColor()
}}
onpointerup={() => (isPointerDown = false)}
onpointercancel={() => {
isPointerDown = false
isHovering = false
}}
class="rounded-full bg-orange-500 px-3 text-white hover:opacity-90 active:opacity-70"
>
I'm a regular HTML button
</button>
</HTML>
<HTML
position.x={0.75}
transform
pointerEvents="none"
>
<p
class="w-auto translate-x-1/2 text-xs drop-shadow-lg"
style="color: {color}"
>
color: {color}
</p>
</HTML>
</T.Mesh>
Examples
Basic Example
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML>
<h1>Hello World</h1>
</HTML>
Transform
transform
applies matrix3d transformations.
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML transform>
<h1>Hello World</h1>
</HTML>
Occlude
<Html>
can be occluded behind geometry using the occlude occlude
property.
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML
transform
occlude
>
<h1>Hello World</h1>
</HTML>
Setting occlude
to "blending"
will allow objects to partially occlude the <HTML>
component.
This occlusion mode requires the <canvas>
element to have pointer-events
set to none
.
Therefore, any events like those in OrbitControls
must be set on the canvas parent. Extras components
like <OrbitControls>
do this automatically.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<div>
<Canvas>
<Scene />
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import {
MeshStandardMaterial,
TetrahedronGeometry,
CylinderGeometry,
ConeGeometry,
SphereGeometry,
IcosahedronGeometry,
TorusGeometry,
OctahedronGeometry,
BoxGeometry,
MathUtils
} from 'three'
import { T } from '@threlte/core'
import { Float } from '@threlte/extras'
const material = new MeshStandardMaterial()
const geometries = [
{ geometry: new TetrahedronGeometry(2) },
{ geometry: new CylinderGeometry(0.8, 0.8, 2, 32) },
{ geometry: new ConeGeometry(1.1, 1.7, 32) },
{ geometry: new SphereGeometry(1.5, 32, 32) },
{ geometry: new IcosahedronGeometry(2) },
{ geometry: new TorusGeometry(1.1, 0.35, 16, 32) },
{ geometry: new OctahedronGeometry(2) },
{ geometry: new SphereGeometry(1.5, 32, 32) },
{ geometry: new BoxGeometry(2.5, 2.5, 2.5) }
] as const
const n = 40
const randProps = Array.from(
{ length: n },
() => geometries[Math.floor(Math.random() * geometries.length)]
)
</script>
{#each randProps as prop}
<Float
floatIntensity={0}
rotationIntensity={2}
rotationSpeed={2}
>
<T.Mesh
scale={MathUtils.randFloat(0.25, 0.5)}
position={[
MathUtils.randFloat(-8, 8),
MathUtils.randFloat(-8, 8),
MathUtils.randFloat(-8, 8)
]}
geometry={prop.geometry}
{material}
/>
</Float>
{/each}
// From: https://discourse.threejs.org/t/roundedrectangle-squircle/28645/20
import { BufferGeometry, BufferAttribute } from 'three'
export class RoundedPlaneGeometry extends BufferGeometry {
parameters: {
width: number
height: number
radius: number
segments: number
}
constructor(width = 1, height = 1, radius = 0.2, segments = 16) {
super()
this.parameters = {
width,
height,
radius,
segments
}
console.log(width, height)
// helper consts
const wi = width / 2 - radius // inner width
const hi = height / 2 - radius // inner height
const ul = radius / width // u left
const ur = (width - radius) / width // u right
const vl = radius / height // v low
const vh = (height - radius) / height // v high
let positions = [wi, hi, 0, -wi, hi, 0, -wi, -hi, 0, wi, -hi, 0]
let uvs = [ur, vh, ul, vh, ul, vl, ur, vl]
let n = [
3 * (segments + 1) + 3,
3 * (segments + 1) + 4,
segments + 4,
segments + 5,
2 * (segments + 1) + 4,
2,
1,
2 * (segments + 1) + 3,
3,
4 * (segments + 1) + 3,
4,
0
] as const
const indices: number[] = [
n[0],
n[1],
n[2],
n[0],
n[2],
n[3],
n[4],
n[5],
n[6],
n[4],
n[6],
n[7],
n[8],
n[9],
n[10],
n[8],
n[10],
n[11]
]
let phi, cos, sin, xc, yc, uc, vc, idx
for (let i = 0; i < 4; i++) {
xc = i < 1 || i > 2 ? wi : -wi
yc = i < 2 ? hi : -hi
uc = i < 1 || i > 2 ? ur : ul
vc = i < 2 ? vh : vl
for (let j = 0; j <= segments; j++) {
phi = (Math.PI / 2) * (i + j / segments)
cos = Math.cos(phi)
sin = Math.sin(phi)
positions.push(xc + radius * cos, yc + radius * sin, 0)
uvs.push(uc + ul * cos, vc + vl * sin)
if (j < segments) {
idx = (segments + 1) * i + j + 4
indices.push(i, idx, idx + 1)
}
}
}
this.setIndex(new BufferAttribute(new Uint32Array(indices), 1))
this.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3))
this.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2))
}
}
<script lang="ts">
import { T } from '@threlte/core'
import { Environment, Float, HTML, useGltf, OrbitControls } from '@threlte/extras'
import { derived } from 'svelte/store'
import { type Mesh, MathUtils } from 'three'
import Geometries from './Geometries.svelte'
import { RoundedPlaneGeometry } from './RoundedPlaneGeometry'
const gltf = useGltf<{
nodes: {
phone: Mesh
}
materials: {}
}>('/models/phone/phone.glb')
const phoneGeometry = derived(gltf, (gltf) => {
if (!gltf) return
return gltf.nodes.phone.geometry
})
const url = window.origin
</script>
<T.PerspectiveCamera
position={[50, -30, 30]}
fov={20}
oncreate={(ref) => {
ref.lookAt(0, 0, 0)
}}
makeDefault
>
<OrbitControls
enableDamping
enableZoom={false}
/>
</T.PerspectiveCamera>
<T.AmbientLight intensity={0.3} />
<Environment
path="/hdr/"
files="shanghai_riverside_1k.hdr"
/>
<Float
scale={0.7}
floatIntensity={5}
>
<HTML
rotation.y={90 * MathUtils.DEG2RAD}
position.x={1.2}
transform
occlude="blending"
geometry={new RoundedPlaneGeometry(10.5, 21.3, 1.6)}
>
<div
class="phone-wrapper"
style="border-radius:1rem"
>
<iframe
title=""
src={url}
width="100%"
height="100%"
frameborder="0"
></iframe>
</div>
</HTML>
{#if $phoneGeometry}
<T.Mesh
scale={5.65}
geometry={$phoneGeometry}
>
<T.MeshStandardMaterial
color="#FF3F00"
metalness={0.9}
roughness={0.1}
/>
</T.Mesh>
{/if}
</Float>
<Geometries />
<style>
.phone-wrapper {
height: 848px;
width: 420px;
border-radius: 63px;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.1);
}
</style>
Visibility Change Event
Use the property occlude
and bind to the event visibilitychange
to implement a custom hide/show behaviour.
<script lang="ts">
import { HTML } from '@threlte/extras'
const onVisibilityChange = (isVisible: boolean) => {
console.log(isVisible)
}
</script>
<HTML
transform
occlude
onvisibilitychange={onVisibilityChange}
>
<h1>Hello World</h1>
</HTML>
When binding to the event visibilitychange
the contents of <HTML>
is not automatically hidden when it’s occluded.
Sprite Rendering
Use the property sprite
in transform
mode to render the contents of <HTML>
as a sprite.
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML
transform
sprite
>
<h1>Hello World</h1>
</HTML>
Center
Add a -50%/-50% css transform with center
when not in transform
mode.
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML center>
<h1>Hello World</h1>
</HTML>
Portal
Use the property portal
to mount the contents of the <HTML>
component on another HTMLElement
.
By default the contents are mounted as a sibling to the rendering <canvas>
.
<script lang="ts">
import { HTML } from '@threlte/extras'
</script>
<HTML portal={document.body}>
<h1>Hello World</h1>
</HTML>