JavaScript · 4859 bytes Raw Blame History
1 // dougk - a cozy pond simulator featuring Doug the duck
2 // Three.js version with Wind Waker cel-shading
3
4 import * as THREE from 'three'
5 import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
6 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
7 import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
8 import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
9 import { createDoug } from './duck.js'
10 import { createPond } from './pond.js'
11 import { BreadManager } from './bread.js'
12
13 // Scene setup
14 const scene = new THREE.Scene()
15 scene.background = new THREE.Color(0x55a04b) // Vibrant grass green
16
17 // Isometric-style orthographic camera
18 const aspect = window.innerWidth / window.innerHeight
19 const frustumSize = 12
20 const camera = new THREE.OrthographicCamera(
21 -frustumSize * aspect / 2,
22 frustumSize * aspect / 2,
23 frustumSize / 2,
24 -frustumSize / 2,
25 0.1,
26 100
27 )
28
29 // Classic isometric angle
30 camera.position.set(10, 10, 10)
31 camera.lookAt(0, 0, 0)
32
33 // Renderer
34 const renderer = new THREE.WebGLRenderer({ antialias: true })
35 renderer.setSize(window.innerWidth, window.innerHeight)
36 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
37 document.body.appendChild(renderer.domElement)
38
39 // Cel-shading lighting setup
40 // Main directional light (sun)
41 const sunLight = new THREE.DirectionalLight(0xffffee, 1.5)
42 sunLight.position.set(5, 10, 5)
43 scene.add(sunLight)
44
45 // Hemisphere light for ambient (sky/ground colors like Wind Waker)
46 const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x55a04b, 0.6)
47 scene.add(hemiLight)
48
49 // Subtle fill light
50 const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
51 fillLight.position.set(-5, 5, -5)
52 scene.add(fillLight)
53
54 // Create gradient texture for toon shading
55 function createToonGradient() {
56 const canvas = document.createElement('canvas')
57 canvas.width = 4
58 canvas.height = 1
59 const ctx = canvas.getContext('2d')
60
61 // 3-step gradient for Wind Waker style
62 ctx.fillStyle = '#444444'
63 ctx.fillRect(0, 0, 1, 1)
64 ctx.fillStyle = '#888888'
65 ctx.fillRect(1, 0, 1, 1)
66 ctx.fillStyle = '#cccccc'
67 ctx.fillRect(2, 0, 1, 1)
68 ctx.fillStyle = '#ffffff'
69 ctx.fillRect(3, 0, 1, 1)
70
71 const texture = new THREE.CanvasTexture(canvas)
72 texture.minFilter = THREE.NearestFilter
73 texture.magFilter = THREE.NearestFilter
74 return texture
75 }
76
77 const toonGradient = createToonGradient()
78
79 // Create the pond
80 const pond = createPond(scene, toonGradient)
81
82 // Create Doug the duck
83 const doug = createDoug(scene, toonGradient)
84
85 // Bread manager
86 const breadManager = new BreadManager(scene, toonGradient)
87
88 // Post-processing for outlines
89 const composer = new EffectComposer(renderer)
90 const renderPass = new RenderPass(scene, camera)
91 composer.addPass(renderPass)
92
93 // Outline pass for that bold Wind Waker look
94 const outlinePass = new OutlinePass(
95 new THREE.Vector2(window.innerWidth, window.innerHeight),
96 scene,
97 camera
98 )
99 outlinePass.edgeStrength = 3
100 outlinePass.edgeGlow = 0
101 outlinePass.edgeThickness = 1.5
102 outlinePass.visibleEdgeColor.set(0x191410)
103 outlinePass.hiddenEdgeColor.set(0x191410)
104 outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
105 composer.addPass(outlinePass)
106
107 const outputPass = new OutputPass()
108 composer.addPass(outputPass)
109
110 // Raycaster for mouse interaction
111 const raycaster = new THREE.Raycaster()
112 const mouse = new THREE.Vector2()
113
114 // Handle clicks for bread dropping
115 renderer.domElement.addEventListener('click', (event) => {
116 mouse.x = (event.clientX / window.innerWidth) * 2 - 1
117 mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
118
119 raycaster.setFromCamera(mouse, camera)
120 const intersects = raycaster.intersectObject(pond.water)
121
122 if (intersects.length > 0) {
123 const point = intersects[0].point
124 breadManager.spawnBread(point.x, point.z)
125 pond.addRipple(point.x, point.z)
126
127 // Update outline objects
128 outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
129 }
130 })
131
132 // Handle window resize
133 window.addEventListener('resize', () => {
134 const aspect = window.innerWidth / window.innerHeight
135 camera.left = -frustumSize * aspect / 2
136 camera.right = frustumSize * aspect / 2
137 camera.top = frustumSize / 2
138 camera.bottom = -frustumSize / 2
139 camera.updateProjectionMatrix()
140
141 renderer.setSize(window.innerWidth, window.innerHeight)
142 composer.setSize(window.innerWidth, window.innerHeight)
143 })
144
145 // Animation loop
146 const clock = new THREE.Clock()
147
148 function animate() {
149 requestAnimationFrame(animate)
150
151 const delta = clock.getDelta()
152 const elapsed = clock.getElapsedTime()
153
154 // Update Doug
155 doug.update(delta, elapsed, breadManager.getActiveBits(), pond)
156
157 // Update bread
158 breadManager.update(delta, elapsed)
159
160 // Update pond (ripples, etc)
161 pond.update(delta, elapsed)
162
163 // Render with post-processing
164 composer.render()
165 }
166
167 animate()