kimhyunwoo commited on
Commit
d20b9cd
ยท
verified ยท
1 Parent(s): 9b68ef7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +284 -106
index.html CHANGED
@@ -1,123 +1,301 @@
1
  <!DOCTYPE html>
2
- <html lang="ko">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>3D ๋ง๊ณ  ํด๋ฆฌ์ปค</title>
7
- <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
8
- <script>
9
- // ํด๋ฆญ ์‹œ ๋ง๊ณ  ๋ฐ˜์‘ ๋ฐ ์ ์ˆ˜ ์ฒ˜๋ฆฌ ๋กœ์ง
10
- AFRAME.registerComponent('clickable-mango', {
11
- schema: {
12
- scoreDisplay: {type: 'selector'}
13
- },
14
- init: function () {
15
- this.score = 0;
16
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
17
-
18
- this.el.addEventListener('click', () => {
19
- // 1. ์ ์ˆ˜ ์ฆ๊ฐ€ ๋ฐ ํ‘œ์‹œ
20
- this.score++;
21
- if (this.data.scoreDisplay) {
22
- this.data.scoreDisplay.setAttribute('value', `์ ์ˆ˜: ${this.score}`);
23
- }
24
-
25
- // 2. ๋ง๊ณ  ์ ํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜ (๊ฐ„๋‹จํ•œ ์œ„์น˜ ๋ณ€๊ฒฝ)
26
- let currentPosition = this.el.getAttribute('position');
27
- this.el.setAttribute('animation', {
28
- property: 'position',
29
- to: {x: currentPosition.x, y: currentPosition.y + 0.5, z: currentPosition.z},
30
- dur: 150,
31
- dir: 'alternate', // ์˜ฌ๋ผ๊ฐ”๋‹ค ๋‚ด๋ ค์˜ค๊ธฐ
32
- loop: 1
33
- });
34
-
35
- // 3. ํด๋ฆญ ์‚ฌ์šด๋“œ ์žฌ์ƒ
36
- this.playSound(440, 0.1, 'triangle'); // A4 note
37
-
38
- // 4. ๋ง๊ณ  ์œ„์น˜ ๋žœ๋ค ๋ณ€๊ฒฝ (ํด๋ฆญ ํ›„ ์ž ์‹œ ๋’ค)
39
- setTimeout(() => {
40
- const newX = (Math.random() - 0.5) * 4; // -2 to 2
41
- const newY = Math.random() * 1 + 1; // 1 to 2 (height)
42
- const newZ = (Math.random() - 0.5) * 4 - 2; // -4 to 0 (depth)
43
- this.el.setAttribute('position', `${newX} ${newY} ${newZ}`);
44
- // ์ด์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ œ๊ฑฐ (A-Frame์€ ๋™์ผ property ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ฎ์–ด์“ฐ์ง€๋งŒ, ๋ช…์‹œ์  ์ œ๊ฑฐ๋„ ๊ฐ€๋Šฅ)
45
- this.el.removeAttribute('animation');
46
- }, 300);
47
- });
48
- },
49
- playSound: function(frequency, duration, type) {
50
- if (!this.audioContext) return;
51
- const oscillator = this.audioContext.createOscillator();
52
- const gainNode = this.audioContext.createGain();
53
- oscillator.type = type;
54
- oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
55
- gainNode.gain.setValueAtTime(0.2, this.audioContext.currentTime);
56
- gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + duration);
57
- oscillator.connect(gainNode);
58
- gainNode.connect(this.audioContext.destination);
59
- oscillator.start();
60
- oscillator.stop(this.audioContext.currentTime + duration);
61
- }
62
- });
63
- </script>
64
  <style>
65
- /* UI๋ฅผ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ์Šคํƒ€์ผ (A-Frame์€ ์บ”๋ฒ„์Šค ์œ„์— HTML ์š”์†Œ ์˜ค๋ฒ„๋ ˆ์ด ๊ฐ€๋Šฅ) */
66
- #ui-overlay {
 
67
  position: absolute;
68
- top: 20px;
69
- left: 20px;
70
- z-index: 10;
71
  background-color: rgba(0,0,0,0.5);
72
  color: white;
73
- padding: 10px;
74
  border-radius: 5px;
75
- font-family: Arial, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
  </style>
78
  </head>
79
  <body>
80
- <!-- A-Frame Scene -->
81
- <a-scene background="color: #87CEEB"> <!-- ํ•˜๋Š˜์ƒ‰ ๋ฐฐ๊ฒฝ -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- <!-- ์นด๋ฉ”๋ผ + ์ปค์„œ (ํด๋ฆญ ์ธํ„ฐ๋ž™์…˜์šฉ) -->
84
- <a-entity camera look-controls position="0 1.6 0">
85
- <a-entity cursor="fuse: false; rayOrigin: mouse;"
86
- position="0 0 -1"
87
- geometry="primitive: ring; radiusInner: 0.01; radiusOuter: 0.015"
88
- material="color: white; shader: flat; transparent: true; opacity: 0.5">
89
- </a-entity>
90
- </a-entity>
91
-
92
- <!-- ๋ง๊ณ  (๊ตฌ์ฒด๋กœ ํ‘œํ˜„) -->
93
- <a-sphere id="mango"
94
- position="0 1.5 -3"
95
- radius="0.5"
96
- color="#FFBF00" <!-- ๋ง๊ณ  ์ƒ‰์ƒ -->
97
- shadow="cast: true"
98
- clickable-mango="scoreDisplay: #scoreText"> <!-- ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ์ ์šฉ -->
99
- <a-light type="point" color="yellow" intensity="0.3" position="0 0.2 0"></a-light> <!-- ๋ง๊ณ  ์ž์ฒด์—์„œ ์•ฝ๊ฐ„์˜ ๋น› -->
100
- </a-sphere>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- <!-- ๋ฐ”๋‹ฅ -->
103
- <a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7CFC00" shadow="receive: true"></a-plane> <!-- ์ž”๋””์ƒ‰ ๋ฐ”๋‹ฅ -->
104
-
105
- <!-- ์กฐ๋ช… -->
106
- <a-light type="ambient" color="#888"></a-light>
107
- <a-light type="directional" color="#FFF" intensity="0.6" position="-1 1 1"></a-light>
108
-
109
- <!-- UI ํ…์ŠคํŠธ (A-Frame์˜ a-text ์‚ฌ์šฉ) -->
110
- <a-text id="scoreText" value="์ ์ˆ˜: 0" position="-1.8 2.5 -3" color="black" width="4"></a-text>
111
- <a-text value="๋ง๊ณ ๋ฅผ ํด๋ฆญํ•˜์„ธ์š”!" position="-1.8 2.8 -3" color="black" width="5"></a-text>
112
-
113
- </a-scene>
114
-
115
- <!-- HTML UI Overlay (์„ ํƒ ์‚ฌํ•ญ, A-Frame ์™ธ๋ถ€ HTML๋กœ UI ๊ตฌ์„ฑ) -->
116
- <!--
117
- <div id="ui-overlay">
118
- <h2 id="score-html">์ ์ˆ˜: 0</h2>
119
- <p>๋ง๊ณ ๋ฅผ ํด๋ฆญํ•˜์„ธ์š”!</p>
120
- </div>
121
- -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </body>
123
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Fruit Fall 3D</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  <style>
9
+ body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; }
10
+ #gameCanvas { display: block; }
11
+ #ui-container {
12
  position: absolute;
13
+ top: 10px;
14
+ left: 10px;
15
+ padding: 10px;
16
  background-color: rgba(0,0,0,0.5);
17
  color: white;
 
18
  border-radius: 5px;
19
+ z-index: 100;
20
+ }
21
+ #score { font-size: 1.5em; margin-bottom: 5px; }
22
+ #start-button, #reset-button {
23
+ padding: 8px 15px;
24
+ font-size: 1em;
25
+ background-color: #4CAF50;
26
+ color: white;
27
+ border: none;
28
+ border-radius: 3px;
29
+ cursor: pointer;
30
+ margin-top: 5px;
31
+ }
32
+ #reset-button { background-color: #f44336; display: none;}
33
+ #game-over-message {
34
+ position: absolute;
35
+ top: 50%;
36
+ left: 50%;
37
+ transform: translate(-50%, -50%);
38
+ color: white;
39
+ background-color: rgba(0,0,0,0.7);
40
+ padding: 20px;
41
+ border-radius: 10px;
42
+ font-size: 2em;
43
+ text-align: center;
44
+ z-index: 101;
45
+ display: none; /* Hidden by default */
46
  }
47
  </style>
48
  </head>
49
  <body>
50
+ <div id="ui-container">
51
+ <div id="score">Score: 0</div>
52
+ <button id="start-button">Start Game</button>
53
+ <button id="reset-button">Play Again</button>
54
+ </div>
55
+ <div id="game-over-message">
56
+ Game Over!<br>
57
+ <span id="final-score-message"></span>
58
+ </div>
59
+ <canvas id="gameCanvas"></canvas>
60
+
61
+ <script>
62
+ let scene, camera, renderer;
63
+ let fruits = [];
64
+ let score = 0;
65
+ let gameActive = false;
66
+ let raycaster, mouse; // For click detection
67
+ let audioContext;
68
+ let gameTimer, timeLeft;
69
+ const GAME_DURATION = 30; // seconds
70
+
71
+ const scoreElement = document.getElementById('score');
72
+ const startButton = document.getElementById('start-button');
73
+ const resetButton = document.getElementById('reset-button');
74
+ const gameOverMessageDiv = document.getElementById('game-over-message');
75
+ const finalScoreMessageSpan = document.getElementById('final-score-message');
76
+
77
+
78
+ const fruitTypes = [
79
+ { name: 'Mango', color: 0xFFBF00, size: 0.5, points: 10 },
80
+ { name: 'Apple', color: 0xFF0000, size: 0.4, points: 10 },
81
+ { name: 'Banana', color: 0xFFFF00, size: 0.35, points: 15 }, // Represented as sphere
82
+ { name: 'Grapes', color: 0x800080, size: 0.3, points: 20 },
83
+ { name: 'Watermelon', color: 0x00FF00, size: 0.7, points: 5 }
84
+ ];
85
+
86
+ function initAudio() {
87
+ if (!audioContext) {
88
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
89
+ }
90
+ }
91
+
92
+ function playSound(type = 'click', freq = 440, duration = 0.05) {
93
+ if (!audioContext) return;
94
+ const oscillator = audioContext.createOscillator();
95
+ const gainNode = audioContext.createGain();
96
+ oscillator.connect(gainNode);
97
+ gainNode.connect(audioContext.destination);
98
+
99
+ oscillator.type = (type === 'miss') ? 'sawtooth' : 'triangle';
100
+ oscillator.frequency.setValueAtTime(freq, audioContext.currentTime);
101
+ gainNode.gain.setValueAtTime(0.15, audioContext.currentTime); // Lowered volume
102
+ gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration);
103
+
104
+ oscillator.start();
105
+ oscillator.stop(audioContext.currentTime + duration);
106
+ }
107
+
108
+ function init() {
109
+ // Scene
110
+ scene = new THREE.Scene();
111
+ scene.background = new THREE.Color(0x87CEEB); // Sky blue
112
+
113
+ // Camera
114
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
115
+ camera.position.set(0, 2, 7); // Positioned to see the falling fruits well
116
+ camera.lookAt(0, 0, 0);
117
+
118
+ // Renderer
119
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gameCanvas'), antialias: true });
120
+ renderer.setSize(window.innerWidth, window.innerHeight);
121
+ renderer.shadowMap.enabled = true;
122
+
123
+ // Lighting
124
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
125
+ scene.add(ambientLight);
126
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
127
+ directionalLight.position.set(5, 10, 7);
128
+ directionalLight.castShadow = true;
129
+ scene.add(directionalLight);
130
+
131
+ // Ground (optional, for visual reference)
132
+ const groundGeometry = new THREE.PlaneGeometry(20, 20);
133
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, side: THREE.DoubleSide }); // Forest green
134
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
135
+ ground.rotation.x = -Math.PI / 2;
136
+ ground.position.y = -5; // Fruits fall past this
137
+ ground.receiveShadow = true;
138
+ scene.add(ground);
139
+
140
+ // Raycaster for mouse clicks
141
+ raycaster = new THREE.Raycaster();
142
+ mouse = new THREE.Vector2();
143
+
144
+ // Event Listeners
145
+ window.addEventListener('resize', onWindowResize, false);
146
+ renderer.domElement.addEventListener('click', onClick, false); // For desktop
147
+ renderer.domElement.addEventListener('touchstart', onTouch, false); // For mobile
148
+
149
+ startButton.addEventListener('click', startGame);
150
+ resetButton.addEventListener('click', startGame); // Reset button also starts the game
151
+ }
152
+
153
+ function startGame() {
154
+ initAudio();
155
+ score = 0;
156
+ timeLeft = GAME_DURATION;
157
+ updateScoreDisplay();
158
+ gameActive = true;
159
+ fruits.forEach(fruit => scene.remove(fruit.mesh)); // Clear existing fruits
160
+ fruits = [];
161
+
162
+ startButton.style.display = 'none';
163
+ resetButton.style.display = 'none';
164
+ gameOverMessageDiv.style.display = 'none';
165
+
166
+ if (gameTimer) clearInterval(gameTimer);
167
+ gameTimer = setInterval(() => {
168
+ timeLeft--;
169
+ // Optionally display timer: scoreElement.textContent = `Score: ${score} | Time: ${timeLeft}`;
170
+ if (timeLeft <= 0) {
171
+ endGame();
172
+ }
173
+ }, 1000);
174
+
175
+ spawnFruitLoop(); // Start spawning fruits
176
+ if (!renderer.xr.isPresenting) animate(); // Ensure animate isn't called twice if in XR
177
+ }
178
 
179
+ function endGame() {
180
+ gameActive = false;
181
+ clearInterval(gameTimer);
182
+ clearTimeout(fruitSpawnTimeout); // Stop spawning new fruits
183
+
184
+ finalScoreMessageSpan.textContent = `Your Score: ${score}`;
185
+ gameOverMessageDiv.style.display = 'flex';
186
+ resetButton.style.display = 'inline-block';
187
+ }
188
+
189
+
190
+ let fruitSpawnTimeout;
191
+ function spawnFruitLoop() {
192
+ if (!gameActive) return;
193
+ createFruit();
194
+ const spawnInterval = Math.random() * 1500 + 500; // 0.5 to 2 seconds
195
+ fruitSpawnTimeout = setTimeout(spawnFruitLoop, spawnInterval);
196
+ }
197
+
198
+ function createFruit() {
199
+ if (!gameActive) return;
200
+
201
+ const type = fruitTypes[Math.floor(Math.random() * fruitTypes.length)];
202
+ const geometry = new THREE.SphereGeometry(type.size, 16, 12);
203
+ const material = new THREE.MeshStandardMaterial({ color: type.color, roughness: 0.5, metalness: 0.1 });
204
+ const fruitMesh = new THREE.Mesh(geometry, material);
205
+ fruitMesh.castShadow = true;
206
+
207
+ // Random spawn position at the top
208
+ fruitMesh.position.x = (Math.random() - 0.5) * 10; // Range: -5 to 5
209
+ fruitMesh.position.y = 7 + Math.random() * 3; // Start above camera view
210
+ fruitMesh.position.z = (Math.random() - 0.5) * 4; // Some depth variation
211
+
212
+ fruitMesh.userData = { type: type.name, points: type.points, fallSpeed: 0.02 + Math.random() * 0.03 };
213
+
214
+ scene.add(fruitMesh);
215
+ fruits.push({ mesh: fruitMesh, data: fruitMesh.userData });
216
+ }
217
+
218
+ function onWindowResize() {
219
+ camera.aspect = window.innerWidth / window.innerHeight;
220
+ camera.updateProjectionMatrix();
221
+ renderer.setSize(window.innerWidth, window.innerHeight);
222
+ }
223
 
224
+ function handleInteraction(clientX, clientY) {
225
+ if (!gameActive) return;
226
+
227
+ mouse.x = (clientX / window.innerWidth) * 2 - 1;
228
+ mouse.y = -(clientY / window.innerHeight) * 2 + 1;
229
+ raycaster.setFromCamera(mouse, camera);
230
+
231
+ const fruitMeshes = fruits.map(f => f.mesh);
232
+ const intersects = raycaster.intersectObjects(fruitMeshes);
233
+
234
+ if (intersects.length > 0) {
235
+ const clickedFruitMesh = intersects[0].object;
236
+ const fruitIndex = fruits.findIndex(f => f.mesh === clickedFruitMesh);
237
+
238
+ if (fruitIndex !== -1) {
239
+ const fruitData = fruits[fruitIndex].data;
240
+ score += fruitData.points;
241
+ updateScoreDisplay();
242
+ playSound('click', 300 + Math.random()*200);
243
+
244
+ // Simple "pop" effect: shrink and remove
245
+ // More complex particle effects are possible but add overhead
246
+ clickedFruitMesh.scale.set(0.1,0.1,0.1); // Visually shrink
247
+ setTimeout(() => { // Remove after slight delay
248
+ scene.remove(clickedFruitMesh);
249
+ }, 50);
250
+ fruits.splice(fruitIndex, 1);
251
+ }
252
+ }
253
+ }
254
+
255
+ function onClick(event) {
256
+ handleInteraction(event.clientX, event.clientY);
257
+ }
258
+
259
+ function onTouch(event) {
260
+ if (event.touches.length > 0) {
261
+ handleInteraction(event.touches[0].clientX, event.touches[0].clientY);
262
+ }
263
+ }
264
+
265
+ function updateScoreDisplay() {
266
+ scoreElement.textContent = `Score: ${score} | Time: ${timeLeft}`;
267
+ }
268
+
269
+ function animate() {
270
+ if (!gameActive && timeLeft <= 0) { // If game ended, stop animation loop
271
+ return;
272
+ }
273
+ requestAnimationFrame(animate);
274
+
275
+ if (gameActive) {
276
+ // Fruit falling logic
277
+ for (let i = fruits.length - 1; i >= 0; i--) {
278
+ const fruitObj = fruits[i];
279
+ fruitObj.mesh.position.y -= fruitObj.data.fallSpeed;
280
+ fruitObj.mesh.rotation.x += 0.01;
281
+ fruitObj.mesh.rotation.y += 0.01;
282
+
283
+ // Remove fruit if it falls below a certain point (miss)
284
+ if (fruitObj.mesh.position.y < -6) { // Below the ground plane
285
+ scene.remove(fruitObj.mesh);
286
+ fruits.splice(i, 1);
287
+ // playSound('miss', 150); // Optional miss sound
288
+ // score -= 5; // Optional penalty
289
+ updateScoreDisplay();
290
+ }
291
+ }
292
+ }
293
+ renderer.render(scene, camera);
294
+ }
295
+
296
+ // --- Start ---
297
+ init();
298
+ // startGame(); // Initial call to animate if not waiting for button
299
+ </script>
300
  </body>
301
  </html>