다람쥐 모델에 눈/코/주둥이/다리와 곡선형 꼬리를 더해 디테일을 높이고, 바구니가 꽉 찼을 때 quotes.txt에서 랜덤 명언을 골라 축하 메시지와 함께 보여주도록 추가
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
86
index.html
86
index.html
@@ -88,7 +88,20 @@
|
||||
padding:0.85rem 1.3rem; border-radius:14px;
|
||||
font-weight:700; font-size:1.05rem;
|
||||
box-shadow:0 10px 28px rgba(0,0,0,0.22);
|
||||
max-width: 80vw;
|
||||
}
|
||||
.celebrate .quote{
|
||||
display:none;
|
||||
margin-top:0.5rem;
|
||||
font-weight:500; font-size:0.85rem;
|
||||
color:#4c6155;
|
||||
}
|
||||
.celebrate .quote.show{ display:block; }
|
||||
@media (prefers-color-scheme: dark){
|
||||
.celebrate .quote{ color:#9db3a6; }
|
||||
}
|
||||
:root[data-theme="dark"] .celebrate .quote{ color:#9db3a6; }
|
||||
:root[data-theme="light"] .celebrate .quote{ color:#4c6155; }
|
||||
@media (prefers-color-scheme: dark){
|
||||
.celebrate .msg{ background: rgba(20,28,22,0.92); color:#eaf3ec; }
|
||||
}
|
||||
@@ -107,7 +120,12 @@
|
||||
<div class="stage" id="stage">
|
||||
<div class="hud">🧺 <span id="basketCount">0 / 5</span></div>
|
||||
<div class="confetti-layer" id="confettiLayer"></div>
|
||||
<div class="celebrate" id="celebrate"><div class="msg">🎉 사과 5개를 모았어요!</div></div>
|
||||
<div class="celebrate" id="celebrate">
|
||||
<div class="msg">
|
||||
🎉 사과 5개를 모았어요!
|
||||
<div class="quote" id="quoteText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">나무를 드래그해서 흔들면 사과가 떨어져요. 바구니에 5개를 모아보세요 🧺</p>
|
||||
|
||||
@@ -117,6 +135,13 @@ const stage = document.getElementById('stage');
|
||||
const celebrateEl = document.getElementById('celebrate');
|
||||
const confettiLayer = document.getElementById('confettiLayer');
|
||||
const basketCountEl = document.getElementById('basketCount');
|
||||
const quoteEl = document.getElementById('quoteText');
|
||||
|
||||
let quotes = [];
|
||||
fetch('./quotes.txt')
|
||||
.then(r => (r.ok ? r.text() : ''))
|
||||
.then(text => { quotes = text.split('\n').map(s => s.trim()).filter(Boolean); })
|
||||
.catch(() => {});
|
||||
|
||||
// ---------- renderer / scene / camera ----------
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'low-power' });
|
||||
@@ -340,6 +365,8 @@ function knockOffLeaf(){
|
||||
function createSquirrelMesh(){
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8a6642, roughness: 0.9, flatShading: true });
|
||||
const bellyMat = new THREE.MeshStandardMaterial({ color: 0xe4c9a0, roughness: 0.9, flatShading: true });
|
||||
const darkMat = new THREE.MeshStandardMaterial({ color: 0x231a14, roughness: 0.5, flatShading: true });
|
||||
const noseMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1e, roughness: 0.5, flatShading: true });
|
||||
|
||||
const g = new THREE.Group();
|
||||
|
||||
@@ -356,6 +383,23 @@ function createSquirrelMesh(){
|
||||
head.position.set(0, 0.06, 0.22);
|
||||
g.add(head);
|
||||
|
||||
const snout = new THREE.Mesh(new THREE.SphereGeometry(0.055, 7, 6), bellyMat);
|
||||
snout.scale.set(0.9, 0.75, 1);
|
||||
snout.position.set(0, 0.02, 0.31);
|
||||
g.add(snout);
|
||||
|
||||
const nose = new THREE.Mesh(new THREE.SphereGeometry(0.017, 5, 4), noseMat);
|
||||
nose.position.set(0, 0.035, 0.36);
|
||||
g.add(nose);
|
||||
|
||||
const eyeGeo = new THREE.SphereGeometry(0.02, 5, 4);
|
||||
const eyeL = new THREE.Mesh(eyeGeo, darkMat);
|
||||
eyeL.position.set(-0.045, 0.11, 0.28);
|
||||
g.add(eyeL);
|
||||
const eyeR = eyeL.clone();
|
||||
eyeR.position.x = 0.045;
|
||||
g.add(eyeR);
|
||||
|
||||
const earGeo = new THREE.ConeGeometry(0.035, 0.07, 6);
|
||||
const earL = new THREE.Mesh(earGeo, bodyMat);
|
||||
earL.position.set(-0.06, 0.15, 0.26);
|
||||
@@ -365,11 +409,35 @@ function createSquirrelMesh(){
|
||||
earR.position.x = 0.06;
|
||||
g.add(earR);
|
||||
|
||||
const tail = new THREE.Mesh(new THREE.SphereGeometry(0.14, 8, 6), bodyMat);
|
||||
tail.scale.set(0.7, 1.4, 0.7);
|
||||
tail.position.set(0, 0.2, -0.24);
|
||||
tail.rotation.x = 0.7;
|
||||
g.add(tail);
|
||||
const legGeo = new THREE.SphereGeometry(0.042, 6, 5);
|
||||
const legSpots = [
|
||||
[-0.08, -0.1, 0.16], [0.08, -0.1, 0.16],
|
||||
[-0.09, -0.1, -0.12], [0.09, -0.1, -0.12],
|
||||
];
|
||||
for (const [x, y, z] of legSpots){
|
||||
const leg = new THREE.Mesh(legGeo, bodyMat);
|
||||
leg.scale.set(0.75, 0.65, 0.75);
|
||||
leg.position.set(x, y, z);
|
||||
g.add(leg);
|
||||
}
|
||||
|
||||
// bushy curved tail made of tapering, gradually offset segments
|
||||
const tailGroup = new THREE.Group();
|
||||
tailGroup.position.set(0, 0.1, -0.22);
|
||||
tailGroup.rotation.x = 0.45;
|
||||
g.add(tailGroup);
|
||||
const tailSegs = [
|
||||
{ r: 0.1, y: 0, z: 0 },
|
||||
{ r: 0.095, y: 0.13, z: -0.02 },
|
||||
{ r: 0.085, y: 0.25, z: -0.02 },
|
||||
{ r: 0.07, y: 0.35, z: 0.02 },
|
||||
{ r: 0.05, y: 0.43, z: 0.08 },
|
||||
];
|
||||
for (const seg of tailSegs){
|
||||
const s = new THREE.Mesh(new THREE.SphereGeometry(seg.r, 7, 6), bodyMat);
|
||||
s.position.set(0, seg.y, seg.z);
|
||||
tailGroup.add(s);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
@@ -525,6 +593,12 @@ function catchInBasket(apple){
|
||||
|
||||
function celebrate(){
|
||||
celebrateEl.classList.add('show');
|
||||
if (quotes.length){
|
||||
quoteEl.textContent = quotes[Math.floor(Math.random() * quotes.length)];
|
||||
quoteEl.classList.add('show');
|
||||
} else {
|
||||
quoteEl.classList.remove('show');
|
||||
}
|
||||
spawnConfetti();
|
||||
setTimeout(clearBasket, 2800);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user