threlte logo
@threlte/extras

<Gizmo>

webgl <Gizmo> works with WebGL and is available from the @threlte/extras export. webgpu <Gizmo> works with WebGPU and is available from the @threlte/extras/webgpu export.

A gizmo for snap-to camera controls.

<script lang="ts">
  import type { Vector3Tuple } from 'three'
  import { Canvas, T } from '@threlte/core'
  import { Gizmo, type GizmoOptions, OrbitControls } from '@threlte/extras'
  import { Folder, List, Pane, Slider, ThemeUtils } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'

  let type = $state<'sphere' | 'cube' | undefined>('sphere')
  let speed = $state(1)
  let placement = $state<GizmoOptions['placement']>('bottom-left')
  let size = $state(86)
  let left = $state(10)
  let top = $state(10)
  let right = $state(10)
  let bottom = $state(10)
  let center = $state<Vector3Tuple>([0, 0, 0])
</script>

<Pane
  theme={ThemeUtils.presets.light}
  position="fixed"
  title="Gizmo"
>
  <List
    label="type"
    bind:value={type}
    options={{
      sphere: 'sphere',
      cube: 'cube'
    }}
  />
  <Slider
    label="speed"
    bind:value={speed}
    min={0.1}
    max={1}
  />
  <List
    label="placement"
    bind:value={placement}
    options={[
      'top-left',
      'top-center',
      'top-right',
      'center-left',
      'center-center',
      'center-right',
      'bottom-left',
      'bottom-center',
      'bottom-right'
    ]}
  />
  <Slider
    label="size"
    bind:value={size}
    min={20}
    max={350}
    step={1}
  />

  <Folder
    title="offset"
    expanded={false}
  >
    <Slider
      label="top"
      bind:value={top}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="left"
      bind:value={left}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="right"
      bind:value={right}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="bottom"
      bind:value={bottom}
      min={0}
      max={50}
      step={1}
    />
  </Folder>
</Pane>

<div>
  <Canvas>
    <T.PerspectiveCamera
      makeDefault
      position={[20, 20, 20]}
      fov={36}
      target={[0, 0, 0]}
    >
      <OrbitControls
        onchange={(event) => {
          center = event.target.target.toArray()
        }}
      >
        <Gizmo
          {type}
          {speed}
          {placement}
          {size}
          offset={{
            top,
            left,
            bottom,
            right
          }}
        />
      </OrbitControls>
    </T.PerspectiveCamera>

    <Scene {center} />
  </Canvas>
</div>

<style>
  div {
    position: relative;
    height: 100%;
    width: 100%;
    background-color: rgb(14, 22, 37);
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Grid } from '@threlte/extras'
  import { BufferAttribute } from 'three'

  type Props = {
    center: [number, number, number]
  }

  let { center }: Props = $props()

  const red = [1, 0, 0]
  const green = [0, 1, 0]
  const blue = [0, 0, 1]

  const colors = new Float32Array([
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue
  ])
</script>

<T.AxesHelper
  args={[5]}
  renderOrder={1}
/>

<Grid
  sectionSize={0}
  cellColor="#eee"
/>

<T.Mesh position={center}>
  <T.BoxGeometry
    oncreate={(ref) => {
      ref.setAttribute('color', new BufferAttribute(colors, 3))
    }}
  />
  <T.MeshBasicMaterial vertexColors />
</T.Mesh>
<script lang="ts">
  import type { Vector3Tuple } from 'three/webgpu'
  import { Canvas, T } from '@threlte/core/webgpu'
  import { Gizmo, type GizmoOptions, OrbitControls } from '@threlte/extras/webgpu'
  import { Folder, List, Pane, Slider, ThemeUtils } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'

  let type = $state<'sphere' | 'cube' | undefined>('sphere')
  let speed = $state(1)
  let placement = $state<GizmoOptions['placement']>('bottom-left')
  let size = $state(86)
  let left = $state(10)
  let top = $state(10)
  let right = $state(10)
  let bottom = $state(10)
  let center = $state<Vector3Tuple>([0, 0, 0])
</script>

<Pane
  theme={ThemeUtils.presets.light}
  position="fixed"
  title="Gizmo"
>
  <List
    label="type"
    bind:value={type}
    options={{
      sphere: 'sphere',
      cube: 'cube'
    }}
  />
  <Slider
    label="speed"
    bind:value={speed}
    min={0.1}
    max={1}
  />
  <List
    label="placement"
    bind:value={placement}
    options={[
      'top-left',
      'top-center',
      'top-right',
      'center-left',
      'center-center',
      'center-right',
      'bottom-left',
      'bottom-center',
      'bottom-right'
    ]}
  />
  <Slider
    label="size"
    bind:value={size}
    min={20}
    max={350}
    step={1}
  />

  <Folder
    title="offset"
    expanded={false}
  >
    <Slider
      label="top"
      bind:value={top}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="left"
      bind:value={left}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="right"
      bind:value={right}
      min={0}
      max={50}
      step={1}
    />
    <Slider
      label="bottom"
      bind:value={bottom}
      min={0}
      max={50}
      step={1}
    />
  </Folder>
</Pane>

<div>
  <Canvas>
    <T.PerspectiveCamera
      makeDefault
      position={[20, 20, 20]}
      fov={36}
      target={[0, 0, 0]}
    >
      <OrbitControls
        onchange={(event) => {
          center = event.target.target.toArray()
        }}
      >
        <Gizmo
          {type}
          {speed}
          {placement}
          {size}
          offset={{
            top,
            left,
            bottom,
            right
          }}
        />
      </OrbitControls>
    </T.PerspectiveCamera>

    <Scene {center} />
  </Canvas>
</div>

<style>
  div {
    position: relative;
    height: 100%;
    width: 100%;
    background-color: rgb(14, 22, 37);
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core/webgpu'
  import { Grid } from '@threlte/extras/webgpu'
  import { BufferAttribute } from 'three/webgpu'

  type Props = {
    center: [number, number, number]
  }

  let { center }: Props = $props()

  const red = [1, 0, 0]
  const green = [0, 1, 0]
  const blue = [0, 0, 1]

  const colors = new Float32Array([
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...red,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...green,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue,
    ...blue
  ])
</script>

<T.AxesHelper
  args={[5]}
  renderOrder={1}
/>

<Grid
  sectionSize={0}
  cellColor="#eee"
/>

<T.Mesh position={center}>
  <T.BoxGeometry
    oncreate={(ref) => {
      ref.setAttribute('color', new BufferAttribute(colors, 3))
    }}
  />
  <T.MeshBasicMaterial vertexColors />
</T.Mesh>

Three’s OrbitControls or yomotsu’s CameraControls can be provided as a controls prop.

Alternatively, the <Gizmo> can be placed as a child of a controls component.

<OrbitControls>
  <!-- Will attach itself to this OrbitControls -->
  <Gizmo />
</OrbitControls>

Configuring an axis

Each of x, y, z, nx, ny and nz takes a GizmoAxisOptions object:

type GizmoAxisOptions = {
  enabled?: boolean
  label?: string
  color?: ColorRepresentation
  labelColor?: ColorRepresentation
  opacity?: number
  scale?: number
  line?: boolean
  border?: {
    size: number
    color: ColorRepresentation
  }
  hover?: {
    color?: ColorRepresentation
    labelColor?: ColorRepresentation
    opacity?: number
    scale?: number
    border?: {
      size: number
      color: ColorRepresentation
    }
  }
}

The cube type accepts right, top, front, left, bottom and back as aliases for x, y, z, nx, ny and nz respectively, which reads more naturally when labelling faces.

<Gizmo
  type="cube"
  right={{ label: 'R', color: '#ff3653' }}
  top={{ label: 'U', color: '#8adb00' }}
/>

Component Signature

Props

name
type
required
default
description

animated
boolean
no
true
Whether clicking an axis animates the camera to it rather than snapping.

background
{ enabled?: boolean, color?: ColorRepresentation, opacity?: number, hover?: { color?: ColorRepresentation, opacity?: number } }
no
The sphere or cube drawn behind the indicators. Hidden by default on the sphere type until hovered.

className
string
no
HTML `class` attribute for the widget's container element.

container
HTMLElement | string
no
the <Canvas> element
Parent element for the widget, as an element or a CSS selector.

controls
OrbitControls | CameraControls
no

corners
{ enabled?: boolean, color?: ColorRepresentation, opacity?: number, scale?: number, radius?: number, smoothness?: number, hover?: { color?: ColorRepresentation, opacity?: number, scale?: number } }
no
The eight corner handles. Enabled by default on the cube type only.

edges
{ enabled?: boolean, color?: ColorRepresentation, opacity?: number, scale?: number, radius?: number, smoothness?: number, hover?: { color?: ColorRepresentation, opacity?: number, scale?: number } }
no
The twelve edge handles. Enabled by default on the cube type only.

font
{ family?: string, weight?: string | number }
no
{ family: 'sans-serif', weight: 900 }
Font used for the axis labels.

id
string
no
HTML `id` attribute for the widget's container element.

lineWidth
number
no
4
Width of the axis lines in pixels.

nx
GizmoAxisOptions
no
The negative X axis. Aliased as `left` for the cube type.

ny
GizmoAxisOptions
no
The negative Y axis. Aliased as `bottom` for the cube type.

nz
GizmoAxisOptions
no
The negative Z axis. Aliased as `back` for the cube type.

offset
{ top?: number, right?: number, bottom?: number, left?: number }
no
{ top: 10, right: 10, bottom: 10, left: 10 }
Distance in pixels between the widget and the container edges.

placement
'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
no
'bottom-left'
Where the widget sits within its container.

radius
number
no
1 sphere, 0.2 cube
Corner roundness applied to every axis indicator, in the range [0, 1].

renderTask
TaskOptions
no
Options for the task to render the gizmo scene in the viewport. By default, this happens after the `autoRenderTask`.

resolution
number
no
64 sphere, 128 cube
Texture resolution of the indicators. Higher values look sharper at a higher cost.

size
number
no
86
Size of the widget in pixels.

smoothness
number
no
18
Number of segments used to round the axis indicators.

speed
number
no
1
Animation speed multiplier.

type
'sphere' | 'cube'
no
'sphere'
The overall shape of the gizmo. The cube type labels its faces and adds corner and edge handles.

x
GizmoAxisOptions
no
The positive X axis. Aliased as `right` for the cube type.

y
GizmoAxisOptions
no
The positive Y axis. Aliased as `top` for the cube type.

z
GizmoAxisOptions
no
The positive Z axis. Aliased as `front` for the cube type.

Events

name
payload
description

start
Event<'start', ViewportGizmo>
Triggered when a view change interaction begins.

change
Event<'change', ViewportGizmo>
Triggered during view changes.

end
Event<'end', ViewportGizmo>
Triggered when a view change interaction ends.