Skip to content

Instantly share code, notes, and snippets.

@alexanderson1993
Created February 6, 2026 22:08
Show Gist options
  • Select an option

  • Save alexanderson1993/3d0619954ba968b43209d95b9f9697e1 to your computer and use it in GitHub Desktop.

Select an option

Save alexanderson1993/3d0619954ba968b43209d95b9f9697e1 to your computer and use it in GitHub Desktop.
Simple Remix Three's demo
import { createRoot, type Handle, type RemixNode } from 'remix/component'
import * as THREE from 'three'
function ThreeDemo(handle: Handle) {
let color = '#00ff00'
let cubeHidden = false
return () => (
<div
css={{
position: 'fixed',
inset: 0,
backgroundColor: '#1a1a2e',
overflow: 'hidden',
}}
>
<div
css={{
position: 'absolute',
}}
>
<input
type="color"
value={color}
on={{
input: (event) => {
color = event.currentTarget.value
handle.update()
},
}}
/>
<button
on={{
click: () => {
cubeHidden = !cubeHidden
handle.update()
},
}}
>
Toggle Cube
</button>
</div>
<Canvas>
<DirectionLight />
{cubeHidden ? null : <Cube color={color} />}
<AmbientLight />
</Canvas>
</div>
)
}
createRoot(document.body).render(<ThreeDemo />)
function Canvas(
handle: Handle<{
scene: THREE.Scene
camera: THREE.Camera
renderer: THREE.WebGLRenderer
scheduleAnimation: (callback: (delta: number) => void) => () => void
}>,
) {
let scene = new THREE.Scene()
let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
let renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
let animations = new Set<(delta: number) => void>()
function scheduleAnimation(callback: (delta: number) => void) {
animations.add(callback)
return () => animations.delete(callback)
}
handle.context.set({ scene, camera, renderer, scheduleAnimation })
camera.position.z = 5
let lastTime = 0
let raycaster = new THREE.Raycaster()
let intersects: THREE.Intersection[] = []
function animate(time: number) {
let delta = time - lastTime
lastTime = time
for (let animation of animations) {
animation(delta)
}
raycaster.setFromCamera(mouse, camera)
let newIntersects = raycaster.intersectObjects(scene.children)
for (let newIntersect of newIntersects) {
if (!intersects.some((i) => i.object === newIntersect.object)) {
newIntersect.object.dispatchEvent({ type: 'pointerEnter' })
}
}
for (let oldIntersect of intersects) {
if (!newIntersects.some((i) => i.object === oldIntersect.object)) {
oldIntersect.object.dispatchEvent({ type: 'pointerLeave' })
}
}
intersects = newIntersects
renderer.render(scene, camera)
}
renderer.setAnimationLoop(animate)
let mouse = new THREE.Vector2()
renderer.domElement.addEventListener('pointermove', (event) => {
let rect = renderer.domElement.getBoundingClientRect()
mouse.x = (event.clientX / rect.width) * 2 - 1
mouse.y = -(event.clientY / rect.height) * 2 + 1
})
renderer.domElement.addEventListener('click', (event) => {
for (let object of intersects) {
object.object.dispatchEvent({ type: 'click' })
}
})
return (props: { children: RemixNode }) => (
<div connect={(node) => node.appendChild(renderer.domElement)}>{props.children}</div>
)
}
function DirectionLight(handle: Handle) {
let { scene, scheduleAnimation } = handle.context.get(Canvas)
let color = 0xffffff
let intensity = 1
let light = new THREE.DirectionalLight(color, intensity)
light.position.set(0, 10, 5)
light.target.position.set(0, 0, 0)
scene.add(light)
scene.add(light.target)
return () => null
}
function AmbientLight(handle: Handle) {
let { scene, scheduleAnimation } = handle.context.get(Canvas)
let color = 0xffffff
let intensity = 0.5
let light = new THREE.AmbientLight(color, intensity)
scene.add(light)
return () => null
}
function Cube(handle: Handle) {
let geometry = new THREE.BoxGeometry(1, 1, 1)
let material = new THREE.MeshPhysicalMaterial({ color: 0x00ff00 })
let cube = new THREE.Mesh(geometry, material)
let scale = 1
handle.on(cube, {
click: () => {
scale = scale === 1 ? 2 : 1
handle.update()
},
pointerEnter: () => {
material.color.set('red')
document.body.style.cursor = 'crosshair'
},
pointerLeave: () => {
material.color.set(0x00ff00)
document.body.style.cursor = 'auto'
},
})
let { scene, scheduleAnimation } = handle.context.get(Canvas)
scene.add(cube)
let cancelAnimation = scheduleAnimation(() => {
cube.rotation.x += 0.01
cube.rotation.y += 0.01
})
handle.signal.addEventListener(
'abort',
() => {
scene.remove(cube)
cancelAnimation()
},
{ once: true },
)
return (props: { color: string; position?: [number, number, number] }) => {
material.color.set(props.color)
cube.scale.set(scale, scale, scale)
if (props.position) {
cube.position.set(...props.position)
}
return null
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment