Skip to content

Commit acb10f0

Browse files
committed
Add StarWarsIntro custom element with auto-play animation
- Implements `StarWarsIntro` web component with customizable attributes. - Integrates and replaces content with the new animated Star Wars-themed introduction in `index.md`.
1 parent 58c731f commit acb10f0

File tree

2 files changed

+289
-22
lines changed

2 files changed

+289
-22
lines changed

src/_islands/starwars_intro.js

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
class StarWarsIntro extends HTMLElement {
2+
// 관찰할 속성 정의
3+
static get observedAttributes() {
4+
return ["auto-play", "duration", "height"];
5+
}
6+
7+
constructor() {
8+
super();
9+
this.attachShadow({ mode: "open" });
10+
this.isPlaying = false;
11+
}
12+
13+
connectedCallback() {
14+
this.render();
15+
this.init();
16+
17+
const replayButton = this.shadowRoot.querySelector(".replay-button");
18+
if (replayButton) {
19+
replayButton.addEventListener("click", () => {
20+
this.isPlaying = false; // Reset flag to allow restart
21+
this.startAnimation();
22+
});
23+
}
24+
}
25+
attributeChangedCallback(name, oldValue, newValue) {
26+
if (oldValue !== newValue) {
27+
this.render();
28+
// 속성 변경 시 애니메이션을 재시작할 필요가 있다면 (예: duration 변경 시)
29+
if (name === "duration" && this.isPlaying) {
30+
this.startAnimation();
31+
}
32+
}
33+
}
34+
35+
init() {
36+
const autoPlay = this.getAttribute("auto-play") === "true";
37+
38+
if (autoPlay) {
39+
// 1초 후 자동 시작
40+
setTimeout(() => this.startAnimation(), 1000);
41+
}
42+
}
43+
44+
/**
45+
* 애니메이션을 시작/재시작합니다.
46+
*/
47+
startAnimation() {
48+
if (this.isPlaying) return; // 이미 재생 중이라면 중복 실행 방지
49+
50+
this.isPlaying = true;
51+
const crawl = this.shadowRoot.querySelector(".starwars-crawl");
52+
const replayButton = this.shadowRoot.querySelector(".replay-button"); // Get button reference
53+
54+
// Hide button at start
55+
if (replayButton) {
56+
replayButton.style.display = "none";
57+
}
58+
59+
// 1. 애니메이션을 'none'으로 설정하여 초기 상태(0% 키프레임)로 리셋
60+
// (top: 100%, opacity: 0) 상태로 되돌아갑니다.
61+
crawl.style.animation = "none";
62+
63+
// 2. DOM이 변경된 것을 감지할 수 있도록 짧은 딜레이 후 애니메이션 재시작
64+
// (reflow/repaint를 유도하여 애니메이션을 처음부터 다시 재생)
65+
setTimeout(() => {
66+
crawl.style.animation = `starwarsCrawl ${this.duration}s linear`;
67+
}, 10);
68+
69+
// 3. 애니메이션 종료 시간 계산 후 재시작 (무한 반복)
70+
setTimeout(() => {
71+
this.isPlaying = false;
72+
// Show button when animation ends
73+
if (replayButton) {
74+
replayButton.style.display = "block"; // Or 'inline-block' depending on desired layout
75+
}
76+
// auto-play가 true인 경우에만 재시작
77+
if (this.getAttribute("auto-play") === "true") {
78+
this.startAnimation();
79+
}
80+
}, this.duration * 1000);
81+
}
82+
83+
get duration() {
84+
return parseInt(this.getAttribute("duration") || "40");
85+
}
86+
87+
get height() {
88+
return this.getAttribute("height") || "80vh";
89+
}
90+
91+
render() {
92+
this.shadowRoot.innerHTML = `
93+
<style>
94+
:host {
95+
display: block;
96+
width: 100%;
97+
}
98+
99+
* {
100+
margin: 0;
101+
padding: 0;
102+
box-sizing: border-box;
103+
}
104+
105+
.starwars-container {
106+
position: relative;
107+
width: 100%;
108+
height: ${this.height};
109+
min-height: 600px;
110+
background: transparent;
111+
overflow: hidden; /* 텍스트가 이 영역 밖으로 벗어나는 것을 숨김 */
112+
border-radius: 3px;
113+
}
114+
115+
116+
117+
.starwars-text {
118+
display: flex;
119+
justify-content: center;
120+
position: absolute;
121+
width: 100%;
122+
height: 100%;
123+
top: 0;
124+
left: 0;
125+
color: #444;
126+
font-family: 'Pathway Gothic One', 'Pretendard', sans-serif;
127+
font-size: calc(2rem + 4vw);
128+
font-weight: 600;
129+
letter-spacing: 4px;
130+
line-height: 150%;
131+
perspective: 1200px; /* 3D 효과의 깊이 */
132+
text-align: center;
133+
-webkit-mask-image: linear-gradient(to top, black 25%, transparent 100%);
134+
mask-image: linear-gradient(to top, black 25%, transparent 100%);
135+
}
136+
137+
.starwars-crawl {
138+
position: absolute;
139+
top: 100%; /* 초기 상태: 화면 아래쪽에 위치 */
140+
transform-origin: 50% 100%;
141+
/* !!! 여기서 animation 속성을 제거했습니다. !!! */
142+
/* !!! 이제 JS의 startAnimation()으로만 제어됩니다. !!! */
143+
width: 200%;
144+
145+
}
146+
147+
.intro-title {
148+
text-align: center;
149+
margin-bottom: 3rem;
150+
}
151+
152+
.episode {
153+
font-size: 0.6em;
154+
margin: 0;
155+
opacity: 0.8;
156+
letter-spacing: 2px;
157+
}
158+
159+
.intro-title h2 {
160+
margin: 1rem 0 3rem;
161+
text-transform: uppercase;
162+
font-size: 1.1em;
163+
font-weight: 900;
164+
letter-spacing: 6px;
165+
color: #444;
166+
}
167+
168+
.intro-text {
169+
font-size: 0.55em;
170+
margin-bottom: 3rem;
171+
text-align: center;
172+
line-height: 140%;
173+
}
174+
175+
.intro-text.emphasis {
176+
font-size: 0.6em;
177+
margin: 4rem 0;
178+
font-weight: 700;
179+
}
180+
181+
@keyframes starwarsCrawl {
182+
0% {
183+
top: 100%; /* 화면 아래쪽에서 시작 */
184+
transform: rotateX(40deg) translateZ(0);
185+
opacity: 0; /* 투명한 상태에서 시작 */
186+
}
187+
10% {
188+
opacity: 1;
189+
}
190+
100% {
191+
top: -1200%; /* 화면 위쪽으로 멀리 이동 */
192+
transform: rotateX(55deg) translateZ(-3000px);
193+
opacity: 1;
194+
}
195+
}
196+
197+
.replay-button {
198+
position: absolute;
199+
bottom: 20px;
200+
right: 20px;
201+
padding: 10px 20px;
202+
background-color: #feda4a;
203+
color: black;
204+
border: none;
205+
border-radius: 5px;
206+
cursor: pointer;
207+
font-size: 1em;
208+
z-index: 10; /* Ensure it's above other elements */
209+
display: none; /* Hidden by default */
210+
}
211+
212+
.replay-button:hover {
213+
background-color: #e0c030;
214+
}
215+
216+
/* 컨트롤 버튼 (옵션) - 이 예시에서는 사용되지 않으나 스타일은 유지 */
217+
.control-button {
218+
/* ... (스타일 유지) ... */
219+
display: none;
220+
}
221+
222+
@media (max-width: 768px) {
223+
/* ... (미디어 쿼리 스타일 유지) ... */
224+
}
225+
</style>
226+
227+
<div class="starwars-container">
228+
<div class="starwars-text">
229+
<div class="starwars-crawl">
230+
<div class="intro-title">
231+
<h2>KODINGWARRIOR QUEST</h2>
232+
</div>
233+
234+
<p class="intro-text"><i>한 개발자가 있습니다.</i><br/>
235+
<i>정해진 길을 걷지 않고,</i><br/>
236+
<i>자신만의 길을 고수하는 사람입니다.</i></p>
237+
238+
<p class="intro-text"><i>고행길인 걸 알면서도,</i><br/>
239+
<i>꿋꿋이 페이스를 유지하며,</i><br/>
240+
<i>조용히 자신의 일을 이어갑니다.</i></p>
241+
242+
<p class="intro-text">그는 화려한 기술보다<br/>
243+
꾸준함이 더 어렵다는 걸 알고 있습니다.<br/>
244+
그래서 오늘도 묵묵히 코드를 씁니다.</p>
245+
246+
<p class="intro-text">그는 Ruby로 시작해 Python, NestJS, Flutter로 이어갔습니다.<br/>
247+
언어나 프레임워크는 중요하지 않습니다.<br/>
248+
중요한 것은, 누군가에게 도움이 되는 결과입니다.</p>
249+
250+
<p class="intro-text emphasis">그의 하루는 단순합니다.<br/>
251+
코드를 다듬고, 환경을 정비하며,<br/>
252+
작은 불편을 발견하면 바로 고칩니다.<br/>
253+
그렇게 조금씩, 제품은 더 나아집니다.</p>
254+
255+
<p class="intro-text">그는 익숙한 것보다<br/>
256+
지금 필요한 것을 선택합니다.<br/>
257+
새로운 기술은 낯설지만,<br/>
258+
그 낯섦 속에서 배움을 즐깁니다.</p>
259+
260+
<p class="intro-text emphasis">그는 먼저 앞서가며,<br/>
261+
뒤따르는 이들이 같은 길을 걷기 쉽게 만들고 싶어합니다.<br/>
262+
그 길이 고행이 되지 않기를 바랍니다.</p>
263+
264+
<p class="intro-text">그는 완벽을 좇지 않습니다.<br/>
265+
대신, 어제보다 나은 오늘을 선택합니다.<br/>
266+
그게 프로라면, 그걸로 충분하다고 믿습니다.</p>
267+
268+
<p class="intro-text emphasis"><i>마이너한 스택을 다루는 개발자,</i><br/>
269+
<i>꾸준히 배우며 앞으로 나아가는 사람,</i><br/>
270+
<i>그의 여정은 오늘도 계속됩니다.</i></p>
271+
</div>
272+
</div>
273+
<button class="replay-button">Replay</button>
274+
</div>
275+
276+
277+
`;
278+
}
279+
}
280+
281+
// Custom Element 등록
282+
customElements.define("starwars-intro", StarWarsIntro);

src/index.md

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
11
---
2-
# Feel free to add content and custom Front Matter to this file.
3-
42
layout: default
53
---
64

7-
# KODINGWARRIOR QUEST
8-
{:class="text-center mb-20"}
5+
<is-land on:visible import="<%= asset_path 'islands/starwars_intro.js' %>">
6+
<starwars-intro
7+
auto-play="true"
8+
duration="45"
9+
height="80vh">
10+
</starwars-intro>
11+
</is-land>
912

10-
<i>한 개발자가 있습니다</i><br/>
11-
<br/>
12-
<i>정도의 길을 걷지 않고</i><br/>
13-
<i>자신만의 길을 고수하는</i><br/>
14-
<i>그러면서도 프로가 되고자 하는</i><br/>
15-
<br/>
16-
<i>고행길인 것을 알면서도</i><br/>
17-
<i>꿋꿋이 페이스를 유지하고자 하는</i><br/>
18-
<i>그러면서 새로운 길을 개척하고자 하는</i><br/>
19-
<br/>
20-
<i>먼저 앞서가는 그 길</i><br/>
21-
<i>동료와 뒤따르는 이에게는</i><br/>
22-
<i>고행길이 되지 않기를 바라며</i><br/>
23-
<br/>
24-
<i>마이너한 스택을 가진 개발자의</i><br/>
25-
<i>롱런하는 프로 개발자가 되기 위한 여정은</i><br/>
26-
<i>앞으로도 계속 됩니다</i><br/>
27-
{:class="text-right"}

0 commit comments

Comments
 (0)