Skip to content

Commit 0391f50

Browse files
Merge branch '3D'
2 parents 522c7d0 + 2c98b30 commit 0391f50

File tree

5 files changed

+370
-5
lines changed

5 files changed

+370
-5
lines changed

3d/cca_3d.ts

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
import * as THREE from "three"
2+
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
3+
import type { Cell } from "../types/ Cell"
4+
import { nextCellColorId } from "../utils/nextCellColorId"
5+
import { pickColors } from "../utils/pickColors"
6+
7+
interface Cell3D extends Cell {
8+
mesh?: THREE.Mesh
9+
}
10+
11+
export class CCA3D {
12+
private canvasEl: HTMLCanvasElement
13+
private width: number
14+
private height: number
15+
private cubeDimension: number
16+
private readonly halfCubeDimension: number
17+
private cellSize: number
18+
private cellFilling: number
19+
private threshold: number
20+
private colors: Cell[]
21+
private readonly colorMap: Map<number, Cell>
22+
private state: Cell3D[][][]
23+
private initialRotationSpeed: number
24+
private rotationDirectionX: number
25+
private rotationDirectionY: number
26+
private rotationDirectionZ: number
27+
private scene: THREE.Scene
28+
private cellGroup: THREE.Group
29+
private camera: THREE.PerspectiveCamera
30+
private controls: OrbitControls
31+
private isUserInteracting = false
32+
private renderer: THREE.WebGLRenderer
33+
private animationFrameId?: number
34+
private animationFrameCount = 0
35+
renderInterval: NodeJS.Timer
36+
37+
constructor(
38+
canvasEl: HTMLCanvasElement,
39+
width: number,
40+
height: number,
41+
resolution: number,
42+
threshold: number,
43+
colorsCount: number,
44+
) {
45+
// Clean everything
46+
this.clear()
47+
48+
this.canvasEl = canvasEl
49+
this.width = width
50+
this.height = height
51+
this.cubeDimension = resolution
52+
this.cellSize = Math.min(width, height) / resolution / 4
53+
this.cellFilling = 1.0
54+
this.threshold = threshold // 4 is good
55+
this.colors = pickColors(colorsCount) // 10 is good
56+
this.state = []
57+
this.initialRotationSpeed = 0.004
58+
59+
// Pre-calculate values that are used often for performance
60+
this.halfCubeDimension = this.cubeDimension / 2
61+
this.colorMap = new Map(this.colors.map((color) => [color.id, color]))
62+
63+
// Initialize scene first
64+
this.scene = new THREE.Scene()
65+
66+
// Create group for cells and center it
67+
this.cellGroup = new THREE.Group()
68+
this.cellGroup.position.set(0, 0, 0)
69+
this.scene.add(this.cellGroup)
70+
71+
// Set up camera
72+
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
73+
this.camera.position.set(200, 200, 300)
74+
this.camera.lookAt(0, 0, 0) // Look at center
75+
76+
// Initialize renderer
77+
this.renderer = new THREE.WebGLRenderer()
78+
this.renderer.setSize(this.width, this.height)
79+
80+
// Replace the canvas with renderer's domElement
81+
if (this.canvasEl) {
82+
this.canvasEl.replaceWith(this.renderer.domElement)
83+
}
84+
85+
// Add OrbitControls
86+
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
87+
this.controls.enableDamping = true // Add smooth damping
88+
this.controls.dampingFactor = 0.05
89+
this.controls.rotateSpeed = 0.5
90+
91+
// Add event listeners for user interaction
92+
this.controls.addEventListener("start", () => {
93+
this.isUserInteracting = true
94+
})
95+
96+
// Initial random populating
97+
this.setInitialState()
98+
99+
// Initial render
100+
this.renderer.render(this.scene, this.camera)
101+
102+
// Initialize random rotation directions
103+
this.setRandomRotationDirections()
104+
105+
// Animate
106+
this.animate()
107+
}
108+
109+
clear = (): void => {
110+
if (this.animationFrameId) {
111+
cancelAnimationFrame(this.animationFrameId)
112+
this.animationFrameId = undefined
113+
}
114+
115+
if (this.renderInterval) {
116+
clearInterval(this.renderInterval)
117+
}
118+
119+
if (this.cellGroup) {
120+
this.cellGroup.traverse((object) => {
121+
if (object instanceof THREE.Mesh) {
122+
object.geometry.dispose()
123+
if (Array.isArray(object.material)) {
124+
for (const material of object.material) {
125+
material.dispose()
126+
}
127+
} else {
128+
object.material.dispose()
129+
}
130+
}
131+
})
132+
}
133+
134+
if (this.scene) {
135+
this.scene.remove(this.cellGroup)
136+
}
137+
138+
if (this.renderer) {
139+
this.renderer.dispose()
140+
const rendererDomElement = this.renderer.domElement
141+
if (rendererDomElement.parentElement) {
142+
const newCanvas = document.createElement("canvas")
143+
newCanvas.id = "canvas"
144+
rendererDomElement.parentElement.replaceChild(
145+
newCanvas,
146+
rendererDomElement,
147+
)
148+
}
149+
}
150+
}
151+
152+
private setRandomRotationDirections(): void {
153+
// Will be either 1 or -1 for each axis
154+
this.rotationDirectionX = Math.random() < 0.5 ? 1 : -1
155+
this.rotationDirectionY = Math.random() < 0.5 ? 1 : -1
156+
this.rotationDirectionZ = Math.random() < 0.5 ? 1 : -1
157+
}
158+
159+
private animate = (): void => {
160+
this.animationFrameId = requestAnimationFrame(this.animate)
161+
162+
// Update controls
163+
this.controls.update()
164+
165+
// Increment frame counter
166+
this.animationFrameCount++
167+
168+
// Only apply automatic rotation if user is not interacting
169+
if (!this.isUserInteracting) {
170+
this.cellGroup.rotation.x +=
171+
this.initialRotationSpeed * this.rotationDirectionX
172+
this.cellGroup.rotation.y +=
173+
this.initialRotationSpeed * this.rotationDirectionY
174+
this.cellGroup.rotation.z +=
175+
this.initialRotationSpeed * this.rotationDirectionZ
176+
}
177+
178+
// Render every frame
179+
this.renderer.render(this.scene, this.camera)
180+
}
181+
182+
start = (stateUpdatesPerSecond: number): void => {
183+
// Clear any existing interval
184+
if (this.renderInterval) {
185+
clearInterval(this.renderInterval)
186+
}
187+
188+
const intervalMs = 1000 / stateUpdatesPerSecond
189+
this.renderInterval = setInterval(() => {
190+
this.update()
191+
}, intervalMs)
192+
}
193+
194+
private setInitialState = (): void => {
195+
for (let z = 0; z < this.cubeDimension; z++) {
196+
this.state[z] = []
197+
for (let y = 0; y < this.cubeDimension; y++) {
198+
this.state[z][y] = []
199+
for (let x = 0; x < this.cubeDimension; x++) {
200+
const randomColor =
201+
this.colors[Math.floor(Math.random() * this.colors.length)]
202+
this.state[z][y][x] = this.createCell(randomColor, x, y, z, false)
203+
}
204+
}
205+
}
206+
}
207+
208+
private createCell(
209+
colorObj: Cell,
210+
x: number,
211+
y: number,
212+
z: number,
213+
wireframe: boolean,
214+
): Cell3D {
215+
const geometry = new THREE.BoxGeometry(
216+
this.cellSize * this.cellFilling,
217+
this.cellSize * this.cellFilling,
218+
this.cellSize * this.cellFilling,
219+
)
220+
221+
const material = new THREE.MeshBasicMaterial({
222+
transparent: true,
223+
opacity: 0.1,
224+
depthWrite: false,
225+
// side: THREE.DoubleSide, // Render both sides of faces
226+
})
227+
228+
const mesh = new THREE.Mesh(geometry, material)
229+
230+
if (wireframe) {
231+
// Add wireframe
232+
const wireframeGeometry = new THREE.EdgesGeometry(geometry)
233+
const wireframeMaterial = new THREE.LineBasicMaterial({
234+
color: 0xffffff,
235+
transparent: true,
236+
opacity: 0.2,
237+
})
238+
const wireframe = new THREE.LineSegments(
239+
wireframeGeometry,
240+
wireframeMaterial,
241+
)
242+
mesh.add(wireframe)
243+
}
244+
245+
this.updateCellColor(mesh, colorObj.colorRgb)
246+
247+
const posX = (x - this.halfCubeDimension) * this.cellSize
248+
const posY = (y - this.halfCubeDimension) * this.cellSize
249+
const posZ = (z - this.halfCubeDimension) * this.cellSize
250+
251+
mesh.position.set(posX, posY, posZ)
252+
this.cellGroup.add(mesh)
253+
254+
// Simply return the combined object
255+
return {
256+
...colorObj,
257+
mesh,
258+
}
259+
}
260+
261+
private updateCellColor(mesh: THREE.Mesh, colorRgb: number[]): void {
262+
// Type check only once
263+
if (mesh.material instanceof THREE.Material) {
264+
// Pre-calculate RGB values once
265+
const r = colorRgb[0] / 255
266+
const g = colorRgb[1] / 255
267+
const b = colorRgb[2] / 255
268+
mesh.material.color.setRGB(r, g, b)
269+
}
270+
}
271+
272+
private update = (): void => {
273+
const newState = this.state.map((zLayer, z) =>
274+
zLayer.map((yRow, y) =>
275+
yRow.map((currentCell, x) => {
276+
const nextColorId = nextCellColorId(currentCell, this.colors)
277+
const successorNeighboursCount = this.getNeighbours(x, y, z).filter(
278+
(neighbour) => neighbour.id === nextColorId,
279+
).length
280+
281+
if (successorNeighboursCount >= this.threshold) {
282+
const newColor = this.colorMap.get(nextColorId) ?? currentCell
283+
284+
if (newColor.id !== currentCell.id && currentCell.mesh) {
285+
// Use the utility method instead of inline color update
286+
this.updateCellColor(currentCell.mesh, newColor.colorRgb)
287+
}
288+
return { ...newColor, mesh: currentCell.mesh }
289+
}
290+
return currentCell
291+
}),
292+
),
293+
)
294+
295+
this.state = newState
296+
}
297+
298+
private getNeighbours(x: number, y: number, z: number): Cell[] {
299+
const neighbours: Cell[] = []
300+
const offsets = [-1, 0, 1]
301+
for (const dz of offsets) {
302+
for (const dy of offsets) {
303+
for (const dx of offsets) {
304+
if (dx === 0 && dy === 0 && dz === 0) continue
305+
neighbours.push(this.getCellColor(x + dx, y + dy, z + dz))
306+
}
307+
}
308+
}
309+
return neighbours
310+
}
311+
312+
private getCellColor(x: number, y: number, z: number): Cell {
313+
const modifiedX = (x + this.cubeDimension) % this.cubeDimension
314+
const modifiedY = (y + this.cubeDimension) % this.cubeDimension
315+
const modifiedZ = (z + this.cubeDimension) % this.cubeDimension
316+
return this.state[modifiedZ][modifiedY][modifiedX]
317+
}
318+
}

bun.lockb

340 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)