Show HN: Threlte – A Three.js component library for Svelte

91
Show HN: Threlte – A Three.js component library for Svelte
Threlte Logo

A three.js component library for svelte.

npm-license
npm-version

Index

  • What is threlte?
  • Getting started
  • Concepts
    • Interactivity
    • Viewport Awareness
    • Reactivity
  • Conventions
  • Reference
    • Types
      • Property Types
      • Context Types
    • Components
      • 📋
      • 🌐 Objects
        • 🌐
        • 🌐
        • 🌐
        • 🌐
      • ♻️ Object Instances
        • ♻️
        • ♻️
        • ♻️
        • ♻️
      • 🔆 Lights
        • 🔆
        • 🔆
        • 🔆
        • 🔆
        • 🔆
      • 🎥 Cameras
        • 🎥
        • 🎥
      • 🔁 Controls
        • 🔁
      • 💄 Post Processing
        • 💄
      • Misc
        • 💭
        • 💭
        • 🔤
        • 🔲
        • 🔗
    • ↩️ Hooks
      • ↩️ useThrelte
      • ↩️ useThrelteRoot
      • ↩️ useFrame
      • ↩️ useLoader
      • ↩️ useTexture
  • Credits
  • Thank you
  • License

What is threlte?

Threlte is a component library for svelte to build and render three.js scenes declaratively and state-driven in Svelte apps.

It’s inspired by the sensible defaults of react-three-fiber, the simplicity and effectiveness of Sveltes reactivity model and Svelte Cubed.

It provides strictly typed components to quickly and easily build three.js scenes with deep reactivity and interactivity out-of-the-box.

It also aims to provide the building blocks to quickly extend threlte when it’s needed.

⚠️ threlte is still in development and you should expect breaking changes. Check the release notes before updating. If you want to be in the safe side, install threlte with npm i threlte three --save-exact to lock the version.

Getting started

Install threlte and three.js:

npm install threlte three

For TypeScript users, install three.js types:

npm install -D @types/three

Build your first scene:

Open in CodeSandbox

three"; import { Canvas, DirectionalLight, HemisphereLight, Mesh, OrbitControls, PerspectiveCamera } from "threlte"; script> <div> <Canvas> <PerspectiveCamera position={{ x: 10, y: 10, z: 10 }}> <OrbitControls autoRotate /> PerspectiveCamera> <DirectionalLight shadow color={'white'} position={{ x: -15, y: 45, z: 20 }} /> <HemisphereLight skyColor={'white'} groundColor={'#ac844c'} intensity={0.4} /> <Mesh castShadow geometry={new BoxBufferGeometry(1, 1, 1)} material={new MeshStandardMaterial({ color: '#ff3e00' })} /> <Mesh receiveShadow position={{ y: -0.5 }} rotation={{ x: -90 * (Math.PI / 180) }} geometry={new CircleBufferGeometry(3, 72)} material={new MeshStandardMaterial({ side: DoubleSide, color: 'white' })} /> Canvas> div> <style> div { position: fixed; top: 0; left: 0; width: 100%; height: 100%; } style>

It should look something like this:

getting-started-screenshot

Congratulations 🎉


Orbit around the cube and have fun using threlte.

Check out the slightly advanced example scene including interactivity on CodeSandbox.

Concepts

Yes, there are already three.js component libraries for svelte, threlte is different in some ways:

  • Sensible defaults


    Much like react-three-fiber, threlte will set sensible defaults to three.js WebGLRenderer, all colors and textures and more. This makes it easy for you to follow best practices in terms of color reception and accuracy.


    threlte also makes visibility management a breeze with its component.
  • Unified frame loop


    By default, threlte only renders the scene if there’s need for it: If a prop changes that makes rendering the scene necessary, if there are any interactive objects in the scene or if threlte or you use useFrame in any of your components.
  • Interactivity


    threlte makes it possible to use events on three.js objects as if they were regular DOM elements:





    You can also listen to your object leaving or entering the viewport:


  • TypeScript


    All threlte components are written in TypeScript, so type support is a first-class citizen.
  • EffectComposer support


    Add a Pass with


    and threlte will take care of setting up the default RenderPass and render to the EffectComposer instead of the WebGLRenderer.
  • Text rendering


    Render text using the fantastic troika-three-text library with:
  • Access All Areas
    • Bind to three.js object instances


    • Access the renderer


      const { renderer, invalidate } = useThrelte()
  • Easily extendable


    Build objects that didn’t yet make it to threlte yourself by plugging together functional components.
  • Tree-shakeble


    react-three-fiber is great at making it possible to use three.js classes as JSX components. This means that there is no hard dependency on a certain three.js version and everything that is possible in three.js is covered with react-three-fiber as well. There is however a downside: react-three-fiber looks up three.js classes at runtime. This means that even if your react-three-fiber app only uses a fraction of three.js, you will need to ship three.js in its entirety.


    threlte does not look up three.js classes at runtime and as such is limited in features compared to three.js itself. It tries however to cover most use cases of three.js and provides functional components to make extending threlte as easy as possible. As such, your bundler is able to tree-shake threlte and limit what parts of three.js get shipped.
  • Does not get in your way


    Everything is accessible. Objects are instantiated without any default values, so that threlte will not get in your way of setting up or altering objects manually while also relying on the defaults set by three.js.

Interactivity

Open the interactivity example in CodeSandbox

Listen to events of a and a as if it would be a regular DOM element:

These events are supported:
  • click
  • contextmenu
  • pointerup
  • pointerdown
  • pointerenter
  • pointerleave
  • pointermove

All events include the raycast Intersection object:

ts"> import { Mesh, ThreltePointerEvent } from 'threlte' const onClick = (e: CustomEvent<ThreltePointerEvent>) => { const distanceToMesh = e.detail.distance } script> <Meshinteractive on:click={onClick}>

All events but pointerleave and pointerenter also include the underlying PointerEvent or MouseEvent:

⚠️ You must add interactive to your component to be able to listen to pointer events

Be aware that this will make the frameloop render on every frame.

Viewport Awareness

Open the viewport awareness example in CodeSandbox

Additionally, most Objects (Lights, Cameras, Meshes, …) can be made viewport aware. Use it to lazily load textures, models and more.

ts"> import { PointLight } from 'threlte' import type { Object3D } from 'three' let inViewport const onViewportEnter = (e: CustomEvent<Object3D>) => { console.log('PointLight entered the viewport.') } const onViewportLeave = (e: CustomEvent<Object3D>) => { console.log('PointLight left the viewport.') } script> <PointLight viewportAware bind:inViewport on:viewportenter={onViewportEnter} on:viewportleave={onViewportLeave} />

These events are supported:

  • viewportenter
  • viewportleave

Bind inViewport if you wish to not use events.

⚠️ You must add viewportAware to your component to make it viewport aware

Reactivity

Open the reactivity example in CodeSandbox

Just like Svelte Cubed and much unlike react-three-fiber it is encouraged to use your component state to drive your three.js scene.
By using props instead of manipulating three.js objects directly, the unified render loop is able to tell that your scene needs rerendering and svelte can make use of component optimizations.

Conventions

Threlte components follow the principles of three.js whereever possible and useful in terms of available properties and events and their respective naming.

Most component properties are undefined by default. Therefore, new three.js objects are instantiated without any optional default values. This way, three.js will provide the defaults and threltes reactivity will not kick in, allowing you to take complete control over objects.

On top of that, threlte adds some functionality to make objects even more reactive.

lookAt

Use the property lookAt on an Object to

  • reactively orient an Object3D towards another Object3D
  • to reactively orient an Object towards a Position
  • Reference

    Types

    TypeScript users should install @types/three in order to get type support for three.js.

    npm install -D @types/three

    Property Types

    To make working with component props easier, threlte includes special types for position, scale, rotation and lookAt:

    type Position = THREE.Vector3 | { x?: number, y?: number, z?: number }
    
    const positionA = new Vector3()
    const positionB = { x: 1, z: 1 }
    
    
    type Scale = THREE.Vector3 | number | { x?: number, y?: number, z?: number }
    
    const scaleA = new Vector3()
    const scaleB = 2
    const scaleC = { x: 1.5, z: 1 }
    
    
    type Rotation = THREE.Euler | { x?: number, y?: number, z?: number, order?: THREE.Euler['order'] }
    
    const rotationA = new Euler()
    const rotationB = { x: 1.5, z: 1 }
    
    
    type LookAt = THREE.Vector3 | { x?: number, y?: number, z?: number } | THREE.Object3D
    
    const lookAtA = new Vector3()
    const lookAtB = { x: 2, y: 3 }
    const lookAtC = someMesh

    Context Types

    The component provides two very useful contexts: ThrelteContext and ThrelteRootContext

    See useThrelte and useThrelteRoot on how to use these.

    Components

    Type information for threlte component properties, bindings and events are detailed below in the following form:

    // optional
    name: type = default
    
    // required
    name: type

    📋

    The component is the root of your three.js scene.

    By default, the element and the renderer will resize to fit the parent element whenever the window resizes. Provide the property size to set a fixed size.

    also provides a default camera, located at { z: 5 }.

    Properties
    // optional
    dpr: number = browser ? window.devicePixelRatio : 1
    flat: boolean = false
    linear: boolean = false
    frameloop: 'always' | 'demand' = 'demand'
    debugFrameloop: boolean = false
    shadows: boolean = true
    shadowMapType: THREE.ShadowMapType = THREE.PCFSoftShadowMap
    size: { width: number, height: number } | undefined = undefined
    rendererParameters: THREE.WebGLRendererParameters | undefined = undefined
    Bindings
    ctx: ThrelteContext
    rootCtx: ThrelteRootContext

    See Context Types for details

    🌐 Objects

    🌐
    Example
    Properties
    // required
    geometry: THREE.BufferGeometry
    material: THREE.Material | THREE.Material[]
    
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    interactive: boolean = false
    ignorePointerEvents: boolean = false
    Bindings
    inViewport: boolean
    mesh: THREE.Mesh
    Events
    🌐
    Example


    “>
    <script>
      import { Group, GLTF } from 'threlte'
    script>
    
    <Group position={{ x: 5 }}>
      <GLTF url={"/models/modelA.glb"} />
      <GLTF url={"/models/modelB.glb"} />
    Group>
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    Bindings
    inViewport: boolean
    group: THREE.Group
    Events
    viewportenter: undefined
    viewportleave: undefined
    🌐
    Example

    You might want to use this component to pass as a reference to other components:

    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    Bindings
    inViewport: boolean
    object: THREE.Object3D
    Events
    viewportenter: undefined
    viewportleave: undefined
    🌐

    To use DRACO compression, provide a path to the DRACO decoder.


    To use KTX2 compressed textures, provide a path to the KTX2 transcoder.

    You are able to change the property url to load new 3D content. New content will be swapped as soon as loading is finished.

    Example
    Properties
    // required
    url: string
    
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    dracoDecoderPath: string | undefined = undefined
    ktxTranscoderPath: string | undefined = undefined
    Bindings
    gltf: GLTF
    scene: THREE.Group
    Events
    load: GLTF                 // The content finished loading
    unload: undefined          // New content finished loading and the old content is unloaded and disposed
    error: string              // An error occured while loading and parsing
    viewportenter: undefined
    viewportleave: undefined

    ♻️ Object Instances

    While object components like create a new object for you (in the case of it’s a THREE.Mesh), an object instance component takes an existing object instance (THREE.Mesh, THREE.Object3D, THREE.Light or THREE.Camera) as a property and applies reactivity to it. It’s used internally but can also be used to introduce reactivity to objects that need to be instanced manually, imported models or objects that did not yet make it into threlte.

    Object instance components intentionally have no default values on properties even if they can be undefined. That way, your IDE will tell you what properties need to be implemented to properly set them up.

    Example
    ♻️
    Example
    Properties
    // required
    object: THREE.Object3D
    viewportAware: boolean
    
    // optional
    position: Position | undefined
    scale: Scale | undefined
    rotation: Rotation | undefined
    lookAt: LookAt | undefined
    castShadow: boolean | undefined
    receiveShadow: boolean | undefined
    frustumCulled: boolean | undefined
    renderOrder: number | undefined
    Bindings
    Events
    viewportenter: undefined
    viewportleave: undefined
    ♻️
    Example
    Properties
    // required
    mesh: THREE.Mesh
    interactive: boolean
    ignorePointerEvents: boolean
    viewportAware: boolean
    
    // optional
    position: Position | undefined
    scale: Scale | undefined
    rotation: Rotation | undefined
    lookAt: LookAt | undefined
    castShadow: boolean | undefined
    receiveShadow: boolean | undefined
    frustumCulled: boolean | undefined
    renderOrder: number | undefined
    Bindings
    Events
    ♻️
    Example
    Properties
    // required
    camera: THREE.Camera
    viewportAware: boolean
    useCamera: boolean
    
    // optional
    position: Position | undefined
    scale: Scale | undefined
    rotation: Rotation | undefined
    lookAt: LookAt | undefined
    castShadow: boolean | undefined
    receiveShadow: boolean | undefined
    frustumCulled: boolean | undefined
    renderOrder: number | undefined
    Bindings
    Events
    viewportenter: undefined
    viewportleave: undefined
    ♻️
    Example
    Properties
    // required
    light: THREE.Light
    viewportAware: boolean
    
    // optional
    position: Position | undefined
    scale: Scale | undefined
    rotation: Rotation | undefined
    lookAt: LookAt | undefined
    castShadow: boolean | undefined
    receiveShadow: boolean | undefined
    frustumCulled: boolean | undefined
    renderOrder: number | undefined
    color: THREE.ColorRepresentation | undefined
    intensity: number | undefined
    Bindings
    Events
    viewportenter: undefined
    viewportleave: undefined

    🔆 Lights

    🔆
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    color: THREE.ColorRepresentation | undefined = undefined
    intensity: number | undefined = undefined
    Bindings
    inViewport: boolean
    light: THREE.AmbientLight
    Events
    viewportenter: undefined
    viewportleave: undefined
    🔆
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    color: THREE.ColorRepresentation | undefined = undefined
    intensity: number | undefined = undefined
    shadow:
      | boolean
      | {
          mapSize?: [number, number] | undefined
          camera?:
            | {
                left?: number | undefined
                right?: number | undefined
                top?: number | undefined
                bottom?: number | undefined
                near?: number | undefined
                far?: number | undefined
              }
            | undefined
          bias?: number | undefined
          radius?: number | undefined
        }
      | undefined = undefined
    Bindings
    inViewport: boolean
    light: THREE.DirectionalLight
    Events
    viewportenter: undefined
    viewportleave: undefined
    🔆
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    receiveShadow: boolean | undefined = undefined
    castShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    intensity: number | undefined = undefined
    skyColor: THREE.ColorRepresentation | undefined = undefined
    groundColor: THREE.ColorRepresentation | undefined = undefined
    Bindings
    inViewport: boolean
    light: THREE.HemisphereLight
    Events
    viewportenter: undefined
    viewportleave: undefined
    🔆
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    color: THREE.ColorRepresentation | undefined = undefined
    intensity: number | undefined = undefined
    distance: number | undefined = undefined
    decay: number | undefined = undefined
    power: number | undefined = undefined
    shadow:
      | boolean
      | {
          mapSize?: [number, number]
          camera?: { near?: number; far?: number }
          bias?: number
          radius?: number
        }
      | undefined = undefined
    Bindings
    inViewport: boolean
    light: THREE.PointLight
    Events
    viewportenter: undefined
    viewportleave: undefined
    🔆
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    color: THREE.ColorRepresentation | undefined = undefined
    intensity: number | undefined = undefined
    angle: number | undefined = undefined
    decay: number | undefined = undefined
    distance: number | undefined = undefined
    penumbra: number | undefined = undefined
    power: number | undefined = undefined
    target: LookAt | undefined = undefined
    shadow:
      | boolean
      | {
          mapSize?: [number, number]
          camera?: { near?: number; far?: number }
          bias?: number
          radius?: number
        }
      | undefined = undefined
    Bindings
    inViewport: boolean
    light: THREE.SpotLight
    Events
    viewportenter: undefined
    viewportleave: undefined

    🎥 Cameras

    🎥
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    useCamera: boolean = true
    near: number = undefined
    far: number = undefined
    zoom: number = undefined
    Bindings
    inViewport: boolean
    camera: THREE.OrthographicCamera
    Events
    viewportenter: undefined
    viewportleave: undefined
    🎥
    Example
    Properties
    // optional
    position: Position | undefined = undefined
    scale: Scale | undefined = undefined
    rotation: Rotation | undefined = undefined
    lookAt: LookAt | undefined = undefined
    viewportAware: boolean = false
    castShadow: boolean | undefined = undefined
    receiveShadow: boolean | undefined = undefined
    frustumCulled: boolean | undefined = undefined
    renderOrder: number | undefined = undefined
    useCamera: boolean = true
    near: number = undefined
    far: number = undefined
    fov: number = undefined
    Bindings
    inViewport: boolean
    camera: THREE.PerspectiveCamera
    Events
    viewportenter: undefined
    viewportleave: undefined

    🔁 Controls

    🔁

    The component must be a direct child of a camera component and will mount itself to that camera.


    If the properties autoRotate or enableDamping are set to true, the frame loop will run continously.

    Example
    Properties
    // optional
    autoRotate: boolean | undefined = undefined
    autoRotateSpeed: number | undefined = undefined
    dampingFactor: number | undefined = undefined
    enableDamping: boolean | undefined = undefined
    enabled: boolean | undefined = undefined
    enablePan: boolean | undefined = undefined
    enableRotate: boolean | undefined = undefined
    enableZoom: boolean | undefined = undefined
    keyPanSpeed: number | undefined = undefined
    keys:
      | {
          LEFT: string
          UP: string
          RIGHT: string
          BOTTOM: string
        }
      | undefined = undefined
    maxAzimuthAngle: number | undefined = undefined
    maxDistance: number | undefined = undefined
    maxPolarAngle: number | undefined = undefined
    maxZoom: number | undefined = undefined
    minAzimuthAngle: number | undefined = undefined
    minDistance: number | undefined = undefined
    minPolarAngle: number | undefined = undefined
    minZoom: number | undefined = undefined
    mouseButtons:
      | {
            LEFT: MOUSE
            MIDDLE: MOUSE
            RIGHT: MOUSE
        }
      | undefined = undefined
    panSpeed: number | undefined = undefined
    rotateSpeed: number | undefined = undefined
    screenSpacePanning: boolean | undefined = undefined
    touches: 
      | {
            ONE: TOUCH
            TWO: TOUCH
        }
      | undefined = undefined
    zoomSpeed: number | undefined = undefined
    target: Position | undefined = undefined
    Bindings
    controls: 





















    NOW WITH OVER +8500 USERS. people can Join Knowasiak for free. Sign up on Knowasiak.com
    Read More

    Charlie Layers
    WRITTEN BY

    Charlie Layers

    Fill your life with experiences so you always have a great story to tell