|
| 1 | +package dev.cheleb.scalajswebgl.samples.three |
| 2 | + |
| 3 | +import com.raquo.laminar.api.L.* |
| 4 | + |
| 5 | +import THREE.* |
| 6 | + |
| 7 | +import org.scalajs.dom |
| 8 | +import org.scalajs.dom.window |
| 9 | +import scala.scalajs.js |
| 10 | + |
| 11 | +object TrackballControlsSample { |
| 12 | + |
| 13 | + def apply() = |
| 14 | + |
| 15 | + val trackballControlsDiv = div( |
| 16 | + h1("TrackballControls Demo"), |
| 17 | + p("Demonstrating trackball-style camera manipulation similar to CAD applications."), |
| 18 | + div( |
| 19 | + cls := "info-panel", |
| 20 | + p("TrackballControls provides intuitive camera control without maintaining a constant up vector."), |
| 21 | + p("Unlike OrbitControls, the camera can freely rotate over the poles without flipping."), |
| 22 | + ul( |
| 23 | + li("Left mouse button: Rotate camera around target"), |
| 24 | + li("Middle mouse button: Zoom in/out"), |
| 25 | + li("Right mouse button: Pan camera"), |
| 26 | + li("Mouse wheel: Zoom in/out"), |
| 27 | + li("A/S/D keys: Rotate, zoom, pan respectively (modifier keys)") |
| 28 | + ) |
| 29 | + ), |
| 30 | + div( |
| 31 | + cls := "canvas-container" |
| 32 | + ) |
| 33 | + ) |
| 34 | + |
| 35 | + // Create scene |
| 36 | + val scene = Scene() |
| 37 | + |
| 38 | + // Create camera |
| 39 | + val camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) |
| 40 | + camera.position.set(0, 0, 10) |
| 41 | + |
| 42 | + // Create WebGL renderer |
| 43 | + val renderer = WebGLRenderer(antialias = true) |
| 44 | + renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.8) |
| 45 | + renderer.setClearColor("#0a0a0a", 1) |
| 46 | + |
| 47 | + // Create a scene with multiple objects arranged in a sphere |
| 48 | + val geometries = js.Array( |
| 49 | + new SphereGeometry(1.0, 16, 12), |
| 50 | + new BoxGeometry(2.0, 2.0, 2.0), |
| 51 | + new ConeGeometry(1.0, 3.0, 8), |
| 52 | + new CylinderGeometry(0.8, 0.8, 2.0, 16), |
| 53 | + new TorusGeometry(1.5, 0.4, 8, 16), |
| 54 | + new OctahedronGeometry(1.5, 0), |
| 55 | + new IcosahedronGeometry(1.5, 0), |
| 56 | + new DodecahedronGeometry(1.5, 0) |
| 57 | + ) |
| 58 | + |
| 59 | + // Create materials with different colors |
| 60 | + val materials = js.Array( |
| 61 | + MeshStandardMaterial(color = 0xff4444, roughness = 0.3, metalness = 0.7), |
| 62 | + MeshStandardMaterial(color = 0x44ff44, roughness = 0.5, metalness = 0.3), |
| 63 | + MeshStandardMaterial(color = 0x4444ff, roughness = 0.2, metalness = 0.8), |
| 64 | + MeshStandardMaterial(color = 0xffff44, roughness = 0.4, metalness = 0.5), |
| 65 | + MeshStandardMaterial(color = 0xff44ff, roughness = 0.6, metalness = 0.2), |
| 66 | + MeshStandardMaterial(color = 0x44ffff, roughness = 0.1, metalness = 0.9), |
| 67 | + MeshStandardMaterial(color = 0xff8844, roughness = 0.7, metalness = 0.1), |
| 68 | + MeshStandardMaterial(color = 0x8844ff, roughness = 0.8, metalness = 0.4) |
| 69 | + ) |
| 70 | + |
| 71 | + // Create meshes and position them in a spherical arrangement |
| 72 | + val meshes = geometries.zipWithIndex.map { case (geometry, i) => |
| 73 | + val mesh = new Mesh(geometry, materials(i)) |
| 74 | + |
| 75 | + // Arrange objects in a sphere around the origin |
| 76 | + val phi = scala.math.Pi * 2 * i / geometries.length |
| 77 | + val theta = scala.math.Pi * (i % 2) * 0.5 |
| 78 | + val radius = 8.0 |
| 79 | + |
| 80 | + mesh.position.set( |
| 81 | + radius * scala.math.sin(theta) * scala.math.cos(phi), |
| 82 | + radius * scala.math.cos(theta), |
| 83 | + radius * scala.math.sin(theta) * scala.math.sin(phi) |
| 84 | + ) |
| 85 | + |
| 86 | + scene.add(mesh) |
| 87 | + mesh |
| 88 | + } |
| 89 | + |
| 90 | + // Add a central object as the target |
| 91 | + val centerGeometry = new SphereGeometry(0.5, 16, 12) |
| 92 | + val centerMaterial = MeshStandardMaterial(color = 0xffffff, roughness = 0.1, metalness = 0.9) |
| 93 | + val centerMesh = new Mesh(centerGeometry, centerMaterial) |
| 94 | + scene.add(centerMesh) |
| 95 | + |
| 96 | + // Add lighting |
| 97 | + val directionalLight = DirectionalLight(0xffffff, 1) |
| 98 | + directionalLight.position.set(10, 10, 5) |
| 99 | + scene.add(directionalLight) |
| 100 | + |
| 101 | + val ambientLight = AmbientLight(0x404040, 0.3) |
| 102 | + scene.add(ambientLight) |
| 103 | + |
| 104 | + // Add some point lights for atmosphere |
| 105 | + val pointLight1 = PointLight(0xff0000, 0.5, 30) |
| 106 | + pointLight1.position.set(-10, 5, 0) |
| 107 | + scene.add(pointLight1) |
| 108 | + |
| 109 | + val pointLight2 = PointLight(0x0000ff, 0.5, 30) |
| 110 | + pointLight2.position.set(10, 5, 0) |
| 111 | + scene.add(pointLight2) |
| 112 | + |
| 113 | + val pointLight3 = PointLight(0x00ff00, 0.5, 30) |
| 114 | + pointLight3.position.set(0, 0, 10) |
| 115 | + scene.add(pointLight3) |
| 116 | + |
| 117 | + // Create TrackballControls |
| 118 | + val controls = TrackballControls(camera, renderer.domElement) |
| 119 | + controls.rotateSpeed = 1.0 |
| 120 | + controls.zoomSpeed = 1.2 |
| 121 | + controls.panSpeed = 0.8 |
| 122 | + controls.noRotate = false |
| 123 | + controls.noZoom = false |
| 124 | + controls.noPan = false |
| 125 | + controls.staticMoving = false |
| 126 | + controls.dynamicDampingFactor = 0.3 |
| 127 | + controls.minDistance = 1.0 |
| 128 | + controls.maxDistance = 100.0 |
| 129 | + |
| 130 | + // Set target to center |
| 131 | + controls.target.set(0, 0, 0) |
| 132 | + |
| 133 | + // Animation loop |
| 134 | + val animate: () => Unit = () => { |
| 135 | + val time = js.Date.now() * 0.001 |
| 136 | + |
| 137 | + // Animate objects for visual interest |
| 138 | + meshes.zipWithIndex.foreach { case (mesh, i) => |
| 139 | + mesh.rotation.x = time * (0.5 + i * 0.1) |
| 140 | + mesh.rotation.y = time * (0.3 + i * 0.15) |
| 141 | + } |
| 142 | + |
| 143 | + // Rotate center object |
| 144 | + centerMesh.rotation.x = time * 0.5 |
| 145 | + centerMesh.rotation.y = time * 0.8 |
| 146 | + |
| 147 | + // Update controls |
| 148 | + controls.update() |
| 149 | + |
| 150 | + // Render the scene |
| 151 | + renderer.clear() |
| 152 | + renderer.render(scene, camera) |
| 153 | + } |
| 154 | + renderer.setAnimationLoop(animate) |
| 155 | + |
| 156 | + // Handle window resize |
| 157 | + val onWindowResize: dom.Event => Unit = { _ => |
| 158 | + camera.aspect = window.innerWidth / window.innerHeight |
| 159 | + camera.updateProjectionMatrix() |
| 160 | + renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.8) |
| 161 | + controls.handleResize() |
| 162 | + } |
| 163 | + window.addEventListener("resize", onWindowResize) |
| 164 | + |
| 165 | + // Cleanup on unmount |
| 166 | + trackballControlsDiv.amend( |
| 167 | + onUnmountCallback { _ => |
| 168 | + controls.dispose() |
| 169 | + renderer.dispose() |
| 170 | + window.removeEventListener("resize", onWindowResize) |
| 171 | + } |
| 172 | + ) |
| 173 | + |
| 174 | + // Append renderer to the canvas container |
| 175 | + trackballControlsDiv.ref.querySelector(".canvas-container").appendChild(renderer.domElement) |
| 176 | + |
| 177 | + trackballControlsDiv |
| 178 | +} |
0 commit comments