Skip to content

Draw & Trail ​

Instead of keeping a single live cursor trail, this demo records every stroke you paint and spawns replay followers along it. The scene opens with a deterministic infinity-stroke replay so the follower motion is visible before your first drag; every stroke you add gets its own colored brush followers.

Open the full Draw & Trail demo

Minimal Pattern ​

javascript
import { MeshLine } from 'makio-meshline'
import { Plane, Raycaster, Vector2, Vector3 } from 'three/webgpu'

const maxPoints = 120
const stroke = []
const positions = new Float32Array( maxPoints * 3 )
const target = new Vector3()
const pointer = new Vector2()
const plane = new Plane( new Vector3( 0, 0, 1 ), 0 )
const raycaster = new Raycaster()

const line = new MeshLine()
	.lines( positions )
	.lineWidth( 0.08 )
	.color( 0x00ff88 )
	.dynamic( true )
	.build()

let isDrawing = false

function projectPointer( event ) {
	pointer.x = event.clientX / innerWidth * 2 - 1
	pointer.y = -( event.clientY / innerHeight ) * 2 + 1
	raycaster.setFromCamera( pointer, camera )
	raycaster.ray.intersectPlane( plane, target )
	return target.clone()
}

function writeStroke() {
	const lastPoint = stroke[stroke.length - 1] ?? new Vector3()

	for ( let i = 0; i < maxPoints; i++ ) {
		const point = stroke[i] ?? lastPoint
		positions[i * 3] = point.x
		positions[i * 3 + 1] = point.y
		positions[i * 3 + 2] = point.z
	}

	line.setPositions( positions )
}

window.addEventListener( 'pointerdown', () => {
	isDrawing = true
	stroke.length = 0
} )

window.addEventListener( 'pointerup', () => {
	isDrawing = false
} )

window.addEventListener( 'pointermove', ( event ) => {
	if ( !isDrawing ) return

	const point = projectPointer( event )
	const lastPoint = stroke[0]

	if ( !lastPoint || lastPoint.distanceToSquared( point ) > 0.0025 ) {
		stroke.unshift( point )
		stroke.length = Math.min( stroke.length, maxPoints )
		writeStroke()
	}
} )

The live demo expands this into many follower lines that replay saved strokes, but the paintbrush effect starts with one dynamic line and one growing point array.

Keeping reserved capacity cheap ​

The showcase reserves 1,000 fixed-topology lines up front, then tracks only the five pointer followers and ten replay followers for each saved stroke. Each active line patches its existing point span with setPositionRange(positions, 0, { lineIndex }); inactive lines are neither simulated nor uploaded. Widths use one cached typed-array view over the active prefix and one ranged setVertexValues('width', activeWidths, 0) call per frame. This keeps later strokes instant to activate without paying the CPU and transfer cost of the unused capacity.