@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.