Add Tree of Life static HTML5 page
This commit is contained in:
16
README.md
16
README.md
@@ -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
161
index.html
Normal 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 · 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>
|
||||||
Reference in New Issue
Block a user