다람쥐 모델에 눈/코/주둥이/다리와 곡선형 꼬리를 더해 디테일을 높이고, 바구니가 꽉 찼을 때 quotes.txt에서 랜덤 명언을 골라 축하 메시지와 함께 보여주도록 추가
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ Tree of Life - HTML5 page for the podman pages service
|
||||
- 저사양 모바일 GPU에서도 부드럽게 동작하도록 로우폴리 지오메트리, 실시간 그림자 대신 블롭 섀도우, 픽셀비 상한(2x) 적용
|
||||
- 나무를 마우스/터치로 드래그하면 스프링-댐퍼 물리로 자연스럽게 흔들리고, 일정 세기 이상 흔들면 사과(8개)가 하나씩 떨어짐
|
||||
- 가지는 사과가 떨어지고 약 2.6초 후 원래 자리에서 새 사과가 다시 자라남(pop-in 애니메이션) — 떨어진 사과가 바구니에 들어갔는지 여부와 무관하게 독립적으로 재생됨
|
||||
- 나무 아래 바구니에 사과가 떨어지면 자동으로 담기고(빗나가면 잔디에 튕겼다가 사라짐), 5개가 모이면 축하 메시지 + 컨페티가 뜨고 약 2.8초 후 바구니가 비워지며 반복 가능
|
||||
- 나무 아래 바구니에 사과가 떨어지면 자동으로 담기고(빗나가면 잔디에 튕겼다가 사라짐), 5개가 모이면 축하 메시지 + 컨페티와 함께 `quotes.txt`에서 뽑은 랜덤 명언이 뜨고 약 2.8초 후 바구니가 비워지며 반복 가능
|
||||
|
||||
## pages 서비스 연동
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
5
quotes.txt
Normal file
5
quotes.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
삶이 있는 한 희망은 있다. - 키케로
|
||||
성공은 최종적인 것이 아니고 실패도 치명적인 것이 아니다. 중요한 것은 계속할 용기다. - 윈스턴 처칠
|
||||
천 리 길도 한 걸음부터. - 노자
|
||||
오늘 할 수 있는 일에 최선을 다하라. - 랄프 왈도 에머슨
|
||||
네가 상상할 수 있다면, 이룰 수도 있다. - 월트 디즈니
|
||||
Reference in New Issue
Block a user