Created
February 6, 2026 22:08
-
-
Save alexanderson1993/3d0619954ba968b43209d95b9f9697e1 to your computer and use it in GitHub Desktop.
Simple Remix Three's demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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