threlte logo

Cursor Lines

Here we use <MeshLineGeometry> and <MeshLineMaterial> to create a scene where a group of lines follow the cursor. Inspired by the OGL PolyLines example.

<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 { T, useTask } from '@threlte/core'
  import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras'
  import { Vector3 } from 'three'
  import { spring } from 'svelte/motion'

  export let cursorPosition: { x: number; z: number }
  export let color: string
  export let width: number
  export let stiffness: number
  export let damping: number

  let sprungCursor = spring(
    { x: 0, z: 0 },
    {
      stiffness,
      damping
    }
  )

  let points: Vector3[] = []

  for (let j = 0; j < 50; j++) {
    points.push(new Vector3(0, 0, 0))
  }

  $: sprungCursor.set(cursorPosition)

  $: {
    points[0]?.set($sprungCursor.x, 0, $sprungCursor.z)
    points = points
  }

  useTask((delta) => {
    let [previousPoint] = points
    points.forEach((point, i) => {
      if (previousPoint && i > 0) {
        point.lerp(previousPoint, Math.pow(0.000001, delta))
        previousPoint = point
      }
    })
    points = points
  })
</script>

<T.Mesh {...$$restProps}>
  <MeshLineGeometry
    {points}
    shape={'taper'}
  />
  <MeshLineMaterial
    {width}
    {color}
    scaleDown={0.1}
    attenuate={false}
  />
</T.Mesh>
<script lang="ts">
  import { T } from '@threlte/core'
  import { interactivity } from '@threlte/extras'
  import CursorLine from './CursorLine.svelte'

  interactivity()

  let cursorPosition = { x: 0, z: 0 }
  let colors = ['#fc6435', '#ff541f', '#f53c02', '#261f9a', '#1e168d']
</script>

{#each colors as color, i}
  <CursorLine
    {color}
    {cursorPosition}
    position.y={5 - i}
    stiffness={0.02 * i + 0.02}
    damping={0.25 - 0.04 * i}
    width={15 + i * 10}
  />
{/each}

<T.OrthographicCamera
  zoom={50}
  makeDefault
  position.y={10}
  on:create={({ ref }) => ref.lookAt(0, 0, 0)}
/>

<T.Mesh
  visible={false}
  on:pointermove={(e) => {
    cursorPosition.x = e.point.x
    cursorPosition.z = e.point.z
  }}
>
  <T.BoxGeometry args={[20, 0.1, 20]} />
  <T.MeshBasicMaterial />
</T.Mesh>

How does it work?

First we create a scene with an OrthographicCamera and a Mesh that will act as our background. We move the camera up in the Y axis and use the lookAt property to point the camera back down at the Mesh. We use a BoxGeometry large enough in the X and Z axis to fill the screen.

<T.OrthographicCamera
  zoom={50}
  makeDefault
  position.y={10}
  on:create={({ ref }) => {
    ref.lookAt(new Vector3(0, 0, 0))
  }}
/>

<T.Mesh>
  <T.BoxGeometry args={[20, 0.1, 20]} />
  <T.MeshBasicMaterial />
</T.Mesh>

Get the cursor position

To get the cursor position we use Threlte’s interacivity plugin.

<script lang="ts">
  interactivity()
  let cursorPosition = { x: 0, z: 0 }
</script>

<T.Mesh
  visible={false}
  on:pointermove={(e) => {
    cursorPosition.x = e.point.x
    cursorPosition.z = e.point.z
  }}
>
  <T.BoxGeometry args={[20, 0.1, 20]} />
  <T.MeshBasicMaterial />
</T.Mesh>

Create our CursorLine component

We create a component called CursorLine where we will implement <MeshLineMaterial> and <MeshLineGeometry> to create out lines. We need to initialise the <MeshLineGeometry> component with an array of points so we create an array of points positioned at x:0, y:0, z:0. To give each line a different color and with we export these propertys to be set in our parent scene, along with the cursorPosition.

<script lang="ts">
  export let cursorPosition: { x: number; z: number }
  export let color: string
  export let width: number

  let points: Vector3[] = []

  for (let j = 0; j < 50; j++) {
    points.push(new Vector3(0, 0, 0))
  }
</script>

<T.Mesh {...$$restProps}>
  <MeshLineGeometry
    {points}
    shape={'taper'}
  />
  <MeshLineMaterial
    {width}
    {color}
    scaleDown={0.1}
    attenuate={false}
  />
</T.Mesh>

To give each line a bit of individual character we can use Svelte’s spring function. We will set the stiffness and dampening values in the parent scene.

export let stiffness: number
export let damping: number

// Here we create our new svelte store that will
// automatically 'spring' any future values we set
let sprungCursor = spring(
  { x: 0, z: 0 },
  {
    stiffness,
    damping
  }
)

// We set the sprungCursor value to the
// cursorPosition whenever it updates
$: sprungCursor.set(cursorPosition)

Now we need to move our line!

We do this by linking the first point in the line to our cursor position. For the rest of the points in the line we use the lerp function to move each point towards the one before it. The lerp function is a method of THREE.Vector3 and is useful for moving an object towards a set point each frame.

$: {
  if (points[0]) {
    // We set the first point in the array to equal
    // our sprungCursor store whenever it updates
    points[0].x = $sprungCursor.x
    points[0].z = $sprungCursor.z
    points = points
  }
}

useTask(() => {
  let previousPoint = points[0]
  // Every frame we loop through each point ...
  points.forEach((point, i) => {
    if (previousPoint && i > 0) {
      // ... and for every point (except the first)
      // we lerp it towards the point before it
      point.lerp(previousPoint, 0.75)
      previousPoint = point
    }
  })
  points = points
})

All that is left to do is to is implement our new CursorLine component in our scene. To create our five lines we create an array of five colors, then loop through them to create out CursorLine components. We use the index to give each line slightly different position, stiffness, dampening and width propertys.

<script lang="ts">
  let colors = ['#fc6435', '#ff541f', '#f53c02', '#261f79', '#1e165c']
</script>

{#each colors as color, i}
  <CursorLine
    {color}
    {cursorPosition}
    position.y={5 - i}
    stiffness={0.02 * i + 0.02}
    damping={0.25 - 0.04 * i}
    width={15 + i * 10}
  />
{/each}