Add Tree of Life static HTML5 page

This commit is contained in:
2026-07-20 00:38:09 +09:00
parent e204b6d4eb
commit d7fdc42821
2 changed files with 176 additions and 1 deletions

View File

@@ -1,3 +1,17 @@
# tree-of-life # tree-of-life
Tree of Life - HTML5 page for the podman pages service Tree of Life - HTML5 page for the podman pages service
- 순수 정적 HTML5 (`index.html` 단일 파일, 외부 의존성 없음)
- 봄/여름/가을/겨울 색상 전환이 가능한 SVG 나무 애니메이션
## pages 서비스 연동
1. `https://pages.du5t.xyz` 관리자 대시보드에서 새 페이지 생성 (name, slug 지정)
2. 해당 페이지의 Git 연동 설정에서 다음 값 입력
- repo owner: `gm`
- repo name: `tree-of-life`
- branch: `main`
- site root: (비워둠, 저장소 루트가 그대로 서빙됨)
3. 저장하면 Gitea 웹훅이 자동 등록되고, 이후 `main` 브랜치에 push할 때마다 자동 재배포됨
4. 최초 배포는 페이지 상세의 "수동 재배포" 버튼으로 트리거 가능

161
index.html Normal file
View File

@@ -0,0 +1,161 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tree of Life</title>
<style>
:root{
--bg: #0b1410;
--bg-2: #0f1d16;
--trunk: #6b4a35;
--trunk-dark: #4a3324;
--leaf-1: #4f9d6e;
--leaf-2: #79c28d;
--leaf-3: #a7dcb5;
--text: #eaf3ec;
--muted: #9db3a6;
}
@media (prefers-color-scheme: light){
:root{
--bg: #f4f8f5;
--bg-2: #e7f0e9;
--text: #16241c;
--muted: #4c6155;
}
}
*{ box-sizing: border-box; }
html,body{ height:100%; }
body{
margin:0;
min-height:100%;
background: radial-gradient(ellipse at 50% 20%, var(--bg-2), var(--bg));
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Pretendard, Roboto, sans-serif;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap: 1.5rem;
padding: 2.5rem 1.5rem;
text-align:center;
overflow-x:hidden;
}
h1{
margin:0;
font-size: clamp(1.8rem, 5vw, 2.8rem);
letter-spacing: 0.02em;
font-weight: 700;
}
p.tagline{
margin:0;
color: var(--muted);
font-size: 1rem;
max-width: 32rem;
}
.tree-wrap{
width: min(90vw, 480px);
aspect-ratio: 4/5;
}
svg{ width:100%; height:100%; overflow:visible; }
.trunk{
stroke: var(--trunk-dark);
stroke-width: 3;
fill: var(--trunk);
}
.branch{
stroke: var(--trunk-dark);
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-dasharray: 200;
stroke-dashoffset: 200;
animation: grow 1.6s ease-out forwards;
}
.leaf{
fill: var(--leaf-2);
opacity: 0;
transform-origin: center;
animation: bloom 0.9s ease-out forwards;
}
.leaf.a{ fill: var(--leaf-1); }
.leaf.c{ fill: var(--leaf-3); }
@keyframes grow{ to{ stroke-dashoffset: 0; } }
@keyframes bloom{
from{ opacity:0; transform: scale(0.3); }
to{ opacity:1; transform: scale(1); }
}
.season-btns{ display:flex; gap:0.5rem; flex-wrap:wrap; justify-content:center; }
button{
appearance:none;
border: 1px solid color-mix(in srgb, var(--text) 20%, transparent);
background: color-mix(in srgb, var(--text) 6%, transparent);
color: var(--text);
padding: 0.5rem 1rem;
border-radius: 999px;
font-size: 0.9rem;
cursor:pointer;
transition: background 0.15s ease, transform 0.15s ease;
}
button:hover{ background: color-mix(in srgb, var(--text) 14%, transparent); transform: translateY(-1px); }
button[aria-pressed="true"]{ background: var(--leaf-1); color:#0b1410; border-color: transparent; }
footer{ color: var(--muted); font-size: 0.8rem; }
</style>
</head>
<body>
<h1>Tree of Life</h1>
<p class="tagline">가지마다 새로운 잎이 돋아나는, 성장을 담은 작은 나무 한 그루.</p>
<div class="tree-wrap">
<svg viewBox="0 0 300 360" xmlns="http://www.w3.org/2000/svg" aria-label="애니메이션으로 자라나는 나무">
<rect x="132" y="300" width="36" height="40" rx="4" class="trunk"/>
<path class="branch" d="M150 300 C150 240, 150 220, 150 180" style="animation-delay:0s"/>
<path class="branch" d="M150 260 C150 250, 110 230, 80 190" style="animation-delay:0.15s"/>
<path class="branch" d="M150 260 C150 250, 190 230, 220 190" style="animation-delay:0.15s"/>
<path class="branch" d="M150 210 C150 200, 120 175, 95 140" style="animation-delay:0.3s"/>
<path class="branch" d="M150 210 C150 200, 180 175, 205 140" style="animation-delay:0.3s"/>
<path class="branch" d="M150 180 C150 165, 150 150, 150 120" style="animation-delay:0.45s"/>
<g class="leaves">
<circle class="leaf a" cx="80" cy="185" r="26" style="animation-delay:1.1s"/>
<circle class="leaf b" cx="220" cy="185" r="26" style="animation-delay:1.2s"/>
<circle class="leaf c" cx="95" cy="135" r="24" style="animation-delay:1.3s"/>
<circle class="leaf a" cx="205" cy="135" r="24" style="animation-delay:1.4s"/>
<circle class="leaf b" cx="150" cy="110" r="30" style="animation-delay:1.5s"/>
<circle class="leaf c" cx="150" cy="185" r="22" style="animation-delay:1.6s"/>
</g>
</svg>
</div>
<div class="season-btns" role="group" aria-label="계절 선택">
<button data-season="spring" aria-pressed="true"></button>
<button data-season="summer">여름</button>
<button data-season="autumn">가을</button>
<button data-season="winter">겨울</button>
</div>
<footer>tree-of-life &middot; static HTML5 page</footer>
<script>
const palettes = {
spring: ['#79c28d', '#4f9d6e', '#a7dcb5'],
summer: ['#2f8f4f', '#1f6e3a', '#4fae6d'],
autumn: ['#d99a3d', '#c46a2b', '#e8c15a'],
winter: ['#cfe3e0', '#9fb8b4', '#ffffff'],
};
const buttons = document.querySelectorAll('.season-btns button');
const leafClasses = { a: 0, b: 1, c: 2 };
function applySeason(season){
const colors = palettes[season];
document.querySelectorAll('.leaf').forEach(leaf => {
const key = ['a','b','c'].find(k => leaf.classList.contains(k)) || 'b';
leaf.style.fill = colors[leafClasses[key]];
});
buttons.forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.season === season)));
}
buttons.forEach(btn => btn.addEventListener('click', () => applySeason(btn.dataset.season)));
</script>
</body>
</html>