JavaScript · 4823 bytes Raw Blame History
1 // Three.js 3D renderer for dougk
2 import * as THREE from 'three'
3 import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
4 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
5 import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
6 import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
7 import { createDoug } from './duck.js'
8 import { createPond } from './pond.js'
9 import { BreadManager } from './bread.js'
10
11 let scene, camera, renderer, composer, outlinePass
12 let doug, pond, breadManager
13 let clock
14 let animationId = null
15
16 function createToonGradient() {
17 const canvas = document.createElement('canvas')
18 canvas.width = 4
19 canvas.height = 1
20 const ctx = canvas.getContext('2d')
21
22 ctx.fillStyle = '#444444'
23 ctx.fillRect(0, 0, 1, 1)
24 ctx.fillStyle = '#888888'
25 ctx.fillRect(1, 0, 1, 1)
26 ctx.fillStyle = '#cccccc'
27 ctx.fillRect(2, 0, 1, 1)
28 ctx.fillStyle = '#ffffff'
29 ctx.fillRect(3, 0, 1, 1)
30
31 const texture = new THREE.CanvasTexture(canvas)
32 texture.minFilter = THREE.NearestFilter
33 texture.magFilter = THREE.NearestFilter
34 return texture
35 }
36
37 export function start(container) {
38 if (animationId) return
39
40 // Scene
41 scene = new THREE.Scene()
42 scene.background = new THREE.Color(0x55a04b)
43
44 // Camera
45 const aspect = window.innerWidth / window.innerHeight
46 const frustumSize = 12
47 camera = new THREE.OrthographicCamera(
48 -frustumSize * aspect / 2,
49 frustumSize * aspect / 2,
50 frustumSize / 2,
51 -frustumSize / 2,
52 0.1,
53 100
54 )
55 camera.position.set(10, 10, 10)
56 camera.lookAt(0, 0, 0)
57
58 // Renderer
59 renderer = new THREE.WebGLRenderer({ antialias: true })
60 renderer.setSize(window.innerWidth, window.innerHeight)
61 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
62 container.appendChild(renderer.domElement)
63
64 // Lighting
65 const sunLight = new THREE.DirectionalLight(0xffffee, 1.5)
66 sunLight.position.set(5, 10, 5)
67 scene.add(sunLight)
68
69 const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x55a04b, 0.6)
70 scene.add(hemiLight)
71
72 const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
73 fillLight.position.set(-5, 5, -5)
74 scene.add(fillLight)
75
76 const toonGradient = createToonGradient()
77
78 // Create objects
79 pond = createPond(scene, toonGradient)
80 doug = createDoug(scene, toonGradient)
81 breadManager = new BreadManager(scene, toonGradient)
82
83 // Post-processing
84 composer = new EffectComposer(renderer)
85 composer.addPass(new RenderPass(scene, camera))
86
87 outlinePass = new OutlinePass(
88 new THREE.Vector2(window.innerWidth, window.innerHeight),
89 scene,
90 camera
91 )
92 outlinePass.edgeStrength = 3
93 outlinePass.edgeGlow = 0
94 outlinePass.edgeThickness = 1.5
95 outlinePass.visibleEdgeColor.set(0x191410)
96 outlinePass.hiddenEdgeColor.set(0x191410)
97 outlinePass.selectedObjects = [doug.group, pond.group]
98 composer.addPass(outlinePass)
99 composer.addPass(new OutputPass())
100
101 // Raycaster
102 const raycaster = new THREE.Raycaster()
103 const mouse = new THREE.Vector2()
104
105 renderer.domElement.addEventListener('click', (event) => {
106 mouse.x = (event.clientX / window.innerWidth) * 2 - 1
107 mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
108
109 raycaster.setFromCamera(mouse, camera)
110 const intersects = raycaster.intersectObject(pond.water)
111
112 if (intersects.length > 0) {
113 const point = intersects[0].point
114 breadManager.spawnBread(point.x, point.z)
115 pond.addRipple(point.x, point.z)
116 outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
117 }
118 })
119
120 // Resize handler
121 window.addEventListener('resize', onResize)
122
123 clock = new THREE.Clock()
124 animate()
125 }
126
127 function onResize() {
128 const aspect = window.innerWidth / window.innerHeight
129 const frustumSize = 12
130 camera.left = -frustumSize * aspect / 2
131 camera.right = frustumSize * aspect / 2
132 camera.top = frustumSize / 2
133 camera.bottom = -frustumSize / 2
134 camera.updateProjectionMatrix()
135
136 renderer.setSize(window.innerWidth, window.innerHeight)
137 composer.setSize(window.innerWidth, window.innerHeight)
138 }
139
140 function animate() {
141 animationId = requestAnimationFrame(animate)
142
143 const delta = clock.getDelta()
144 const elapsed = clock.getElapsedTime()
145
146 doug.update(delta, elapsed, breadManager.getActiveBits(), pond)
147 breadManager.update(delta, elapsed)
148 pond.update(delta, elapsed)
149
150 composer.render()
151 }
152
153 export function stop() {
154 if (animationId) {
155 cancelAnimationFrame(animationId)
156 animationId = null
157 }
158
159 window.removeEventListener('resize', onResize)
160
161 if (renderer) {
162 renderer.domElement.remove()
163 renderer.dispose()
164 }
165
166 if (composer) {
167 composer.dispose()
168 }
169
170 scene = null
171 camera = null
172 renderer = null
173 composer = null
174 doug = null
175 pond = null
176 breadManager = null
177 }
178
179 export const name = '3D'