threlte logo
@threlte/extras

<RadialGradientTexture>

A reactive radial gradient texture that attaches to the “map” property of its parent. The underlying texture uses an OffscreenCanvas and a CanvasTexture and is assigned the same colorspace as the renderer.

<script lang="ts">
  import Scene from './Scene.svelte'
  import type { ToneMapping, Wrapping } from 'three'
  import { Canvas } from '@threlte/core'
  import {
    ClampToEdgeWrapping,
    MirroredRepeatWrapping,
    RepeatWrapping,
    ACESFilmicToneMapping,
    AgXToneMapping,
    CineonToneMapping,
    LinearToneMapping,
    NeutralToneMapping,
    NoToneMapping,
    ReinhardToneMapping
  } from 'three'
  import { Checkbox, Color, Folder, List, Pane, Slider } from 'svelte-tweakpane-ui'

  const toneMappingOptions: Record<PropertyKey, ToneMapping> = {
    ACESFilmic: ACESFilmicToneMapping,
    AgX: AgXToneMapping,
    Cineon: CineonToneMapping,
    Linear: LinearToneMapping,
    NeutralToneMapping,
    None: NoToneMapping,
    Reinhard: ReinhardToneMapping
  }

  const wrappingOptions: Record<PropertyKey, Wrapping> = {
    ClampToEdge: ClampToEdgeWrapping,
    MirroredRepeat: MirroredRepeatWrapping,
    Repeat: RepeatWrapping
  }

  const canvasSize = 1024
  const halfCanvasSize = 0.5 * 1024

  // from center to one of the corners
  const diagonal = Math.floor(Math.hypot(halfCanvasSize, halfCanvasSize))

  let sceneClearColor = $state('#000000')
  let sceneToneMapping = $state(AgXToneMapping)

  let gradientStartColor = $state('#ffff00')
  let gradientEndColor = $state('#ff00ff')
  let gradientInnerRadius = $state(0)
  let gradientOuterRadiusNumber = $state(diagonal)
  let gradientUseOuterRadiusAuto = $state(true)

  let textureCenterX = $state(0)
  let textureCenterY = $state(0)
  let textureOffsetX = $state(0)
  let textureOffsetY = $state(0)
  let textureRepeatX = $state(1)
  let textureRepeatY = $state(1)
  let textureRotationDegrees = $state(0)
  let textureWrapS: Wrapping = $state(ClampToEdgeWrapping)
  let textureWrapT: Wrapping = $state(ClampToEdgeWrapping)

  let textureRotation = $derived((Math.PI / 180) * textureRotationDegrees)
  let gradientOuterRadius = $derived(
    gradientUseOuterRadiusAuto ? 'auto' : gradientOuterRadiusNumber
  )
</script>

<Pane position="fixed">
  <List
    bind:value={sceneToneMapping}
    options={toneMappingOptions}
    label="tone mapping"
  />
  <Color
    bind:value={sceneClearColor}
    label="clear color"
  />
  <Folder title="gradient props">
    <Color
      bind:value={gradientStartColor}
      label="start color"
    />
    <Color
      bind:value={gradientEndColor}
      label="end color"
    />
    <Slider
      bind:value={gradientInnerRadius}
      label="inner radius"
      min={0}
      max={gradientUseOuterRadiusAuto ? diagonal : gradientOuterRadiusNumber}
      step={1}
    />
    <Checkbox
      bind:value={gradientUseOuterRadiusAuto}
      label="use 'auto' outer radius"
    />
    {#if !gradientUseOuterRadiusAuto}
      <Slider
        bind:value={gradientOuterRadiusNumber}
        label="outer radius"
        min={0}
        max={canvasSize}
        step={1}
        on:change={({ detail }) => {
          gradientInnerRadius = Math.min(detail.value, gradientInnerRadius)
        }}
      />
    {/if}
  </Folder>
  <Folder title="texture props">
    <List
      bind:value={textureWrapS}
      label="wrapS"
      options={wrappingOptions}
    />
    <List
      bind:value={textureWrapT}
      label="wrapT"
      options={wrappingOptions}
    />
    <Slider
      label="centerX"
      bind:value={textureCenterX}
      min={-0.5}
      max={1.5}
      step={0.5}
    />
    <Slider
      label="centerY"
      bind:value={textureCenterY}
      min={-0.5}
      max={1.5}
      step={0.5}
    />
    <Slider
      label="offsetX"
      bind:value={textureOffsetX}
      min={-2}
      max={2}
      step={1}
    />
    <Slider
      label="offsetY"
      bind:value={textureOffsetY}
      min={-2}
      max={2}
      step={1}
    />
    <Slider
      label="repeatX"
      bind:value={textureRepeatX}
      min={0}
      max={5}
      step={1}
    />
    <Slider
      label="repeatY"
      bind:value={textureRepeatY}
      min={0}
      max={5}
      step={1}
    />
    <Slider
      label="rotation (degrees)"
      bind:value={textureRotationDegrees}
      min={-360}
      max={360}
      step={1}
    />
  </Folder>
</Pane>

<div>
  <Canvas>
    <Scene
      {gradientEndColor}
      {gradientInnerRadius}
      {gradientOuterRadius}
      {gradientStartColor}
      {sceneClearColor}
      {sceneToneMapping}
      {textureCenterX}
      {textureCenterY}
      {textureOffsetX}
      {textureOffsetY}
      {textureRepeatX}
      {textureRepeatY}
      {textureRotation}
      {textureWrapS}
      {textureWrapT}
      {canvasSize}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import type { ColorRepresentation, ToneMapping, Wrapping } from 'three'
  import type { GradientStop } from '@threlte/extras'
  import { DoubleSide } from 'three'
  import { RadialGradientTexture, OrbitControls } from '@threlte/extras'
  import { T, useThrelte } from '@threlte/core'

  type SceneProps = {
    canvasSize: number
    gradientEndColor: ColorRepresentation
    gradientInnerRadius: number
    gradientOuterRadius: number
    gradientStartColor: ColorRepresentation
    sceneClearColor: ColorRepresentation
    sceneToneMapping: ToneMapping
    textureCenterX: number
    textureCenterY: number
    textureOffsetX: number
    textureOffsetY: number
    textureRepeatX: number
    textureRepeatY: number
    textureRotation: number
    textureWrapS: Wrapping
    textureWrapT: Wrapping
  }

  let {
    canvasSize,
    gradientInnerRadius,
    gradientOuterRadius,
    gradientEndColor,
    gradientStartColor,
    sceneClearColor,
    sceneToneMapping,
    textureCenterX,
    textureCenterY,
    textureOffsetX,
    textureOffsetY,
    textureRepeatX,
    textureRepeatY,
    textureRotation,
    textureWrapS,
    textureWrapT
  }: SceneProps = $props()

  let stops: GradientStop[] = $derived([
    { color: gradientStartColor, offset: 0 },
    { color: gradientEndColor, offset: 1 }
  ])

  const { invalidate, renderer, toneMapping } = useThrelte()

  $effect(() => {
    toneMapping.set(sceneToneMapping)
    invalidate()
  })

  $effect(() => {
    renderer.setClearColor(sceneClearColor)
    invalidate()
  })
</script>

<T.PerspectiveCamera
  makeDefault
  position.z={5}
>
  <OrbitControls />
</T.PerspectiveCamera>

<T.Mesh scale={2}>
  <T.PlaneGeometry />
  <T.MeshBasicMaterial side={DoubleSide}>
    <RadialGradientTexture
      width={canvasSize}
      height={canvasSize}
      innerRadius={gradientInnerRadius}
      outerRadius={gradientOuterRadius}
      center.x={textureCenterX}
      center.y={textureCenterY}
      offset.x={textureOffsetX}
      offset.y={textureOffsetY}
      repeat.x={textureRepeatX}
      repeat.y={textureRepeatY}
      rotation={textureRotation}
      wrapS={textureWrapS}
      wrapT={textureWrapT}
      {stops}
    />
  </T.MeshBasicMaterial>
</T.Mesh>

Attaching the Texture

The texture is automatically attached to the map property of its parent. You can disable this behaviour by setting the attach prop to false. This may be useful if you want to create the texture but use it somewhere else.

<script>
  let texture = null
</script>

<RadialGradientTexture
  attach={false}
  bind:ref={texture}
/>

<SomeComponent {texture} />

Radius Props

The innerRadius and outerRadius props control the size of the gradient. The innerRadius prop should be less than the outerRadius prop but it is not enforced. If outerRadius is set to 'auto' the outerRadius is effectively set to the radius of the circle that circumscribes the canvas. For example, if the canvas’s width and height are 1024, and outerRadius is set to 'auto', the radius that will be used is sqrt(1024**2 + 1024**2) or roughly 724.

It is also not enforced that innerRadius and outerRadius are both positive.

Gradient Stops

<RadialGradientTexture> accepts a stops prop which is an array of color stops that define the gradient. A stop is defined by two things; an offset and a color. Gradient stops are identical to how you would use them with a 2D context, notably the offset should be a number between 0 and 1 inclusive. Stop colors can be any valid color representation in ThreeJS. Here are a couple examples of valid stops.

<RadialGradientTexture
  stops={[
    { color: 'black', offset: 0 },
    { color: 'white', offset: 1 }
  ]}
/>

<RadialGradientTexture
  stops={[
    { color: '#00ffff', offset: 0 },
    { color: '#ff00ff', offset: 0.5 },
    { color: '#ffff00', offset: 1 }
  ]}
/>

You can even mix and match color representations

<RadialGradientTexture
  stops={[
    { color: 'red', offset: 0 },
    { color: 0xff_00_00, offset: 0.25 },
    { color: 'rgb(255, 0, 0)', offset: 0.5 },
    { color: '#ff0000', offset: 0.75 },
    { color: new Color(new Color(new Color())).set(1, 0, 0), offset: 1 }
  ]}
/>

Adjusting Scene Colors

If the colors in your scene do not match the color in your stops, you may need to adjust the tone mapping of the scene. ToneMapping constants are imported from the three library.

<script>
  import { useThrelte } from '@threlte/core'
  import { LinearToneMapping } from 'three'

  const { toneMapping } = useThrelte()

  toneMapping.set(LinearToneMapping)
</script>

Component Signature

<RadialGradientTexture> extends <T . CanvasTexture> and supports all its props, slot props, bindings and events.

Props

name
type
required
default
description

height
number
no
1024
height of the texture's canvas

innerRadius
number
no
0
inner radius of the gradient

outerRadius
RadialGradientOuterRadius
no
auto
outer radius of the gradient. if set to 'auto', becomes the radius of the circle that circumscribes the canvas

stops
GradientStop[]
no
[{color: '#000000', offset: 0}, {color: '#ffffff', offset: 1}]
list of stops applied to the gradient. more info at https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient

width
number
no
1024
width of the texture's canvas