@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 Scene from './Scene.svelte'
import { Canvas } from '@threlte/core'
import { Checkbox, Pane } from 'svelte-tweakpane-ui'
let autoRender = $state(true)
</script>
<Pane position="fixed">
<Checkbox
label="auto render"
bind:value={autoRender}
/>
</Pane>
<div>
<Canvas>
<Scene {autoRender} />
</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'
type Props = {
autoRender?: boolean
}
let { autoRender = true }: Props = $props()
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
{autoRender}
>
<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"
{autoRender}
>
<p
class="w-auto translate-x-1/2 text-xs drop-shadow-lg"
style="color: {color}"
>
color: {color}
</p>
</HTML>
</T.Mesh>
Stopping and Starting the Task
<HTML>
has an autoRender
prop that you can use to turn off and on its
render task. If at some point in your application, you no longer need to update
the hmtl, you can set autoRender
to false
. If you need to resume the task,
set autoRender
back to true
.
<HTML>
also exports it’s internal render task and the startRendering
,
stopRendering
, and render
functions so you can either manually render the
html or start and stop the internal task at your will.
<script>
let html = $state()
$effect(() => {
// if (shouldRender) {
html?.render()
// }
})
</script>
<HTML
autoRender={false}
bind:this={html}
>
<h1>Hello World</h1>
</HTML>
<script>
let html = $state()
// turn this on and off in accordance with your application
let renderWhileTrue = $state(false)
$effect(() => {
if (html !== undefined) {
if (renderWhileTrue) {
html.startRendering()
// always stop rendering if it was started
return () => {
html.stopRendering()
}
}
}
})
</script>
<HTML
autoRender={false}
bind:this={html}
>
<h1>Hello World</h1>
</HTML>
In both cases you should set autoRender
to false
so that the render task
doesn’t automatically begin.
Lastly, you can access these functions from the <HTML>
’s children snippet.
<HTML autoRender={false}>
{#snippet children({ render, startRendering, stopRendering })}
<button onclick={startRendering}>start rendering</button>
<button onclick={stopRendering}>stop rendering</button>
<button onclick={render}>render a single frame</button>
{/snippet}
</HTML>
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
}
// 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 url="/textures/equirectangular/hdr/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>
uikit
An alternative to using HTML for UI is
uikit. The
vanilla code has be wrapped into
threlte-uikit for use in
threlte projects. There are situations where this package is necessary, for
instance the <HTML/>
component cannot be used within XR sessions.