- 사용 가이드 옆 '▶ 소개 영상 1분' 버튼, 네이티브 dialog에서 재생 - 자체 호스팅 mp4(56초·1920×1080·H.264, 무음), 열 때만 로드(preload none), 무음 자동재생 - Esc·배경·닫기로 종료 시 일시정지·처음으로·포커스 복귀, 모션 감소 시 자동재생 안 함 - 자막 원본(.srt)은 보관만, 화면 미연결 - verify-intro-video.cjs: Chrome 17개 검사 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
64 lines
3.2 KiB
TypeScript
64 lines
3.2 KiB
TypeScript
import './landing-video.css';
|
||
|
||
/** Self-hosted intro video (56 s, 1920×1080 H.264, no audio track). Fetched only when opened. */
|
||
export const introVideoUrl = new URL('./assets/pierzainer-intro.mp4', import.meta.url).href;
|
||
|
||
export const introVideoTriggerMarkup = `<button class="landing-video-trigger" type="button" aria-haspopup="dialog" aria-controls="landing-video"><span aria-hidden="true">▶</span>소개 영상 <span class="landing-video-trigger-time">1분</span></button>`;
|
||
|
||
/** Native dialog: Esc/backdrop close, pause + rewind on close, focus returns to the opener. */
|
||
export function createIntroVideo(root: HTMLElement, poster: string): { open: () => void; close: (restoreFocus?: boolean) => void } {
|
||
const trigger = root.querySelector<HTMLButtonElement>('.landing-video-trigger')!;
|
||
const dialog = document.createElement('dialog');
|
||
dialog.className = 'landing-video';
|
||
dialog.id = 'landing-video';
|
||
dialog.setAttribute('aria-labelledby', 'landing-video-title');
|
||
dialog.innerHTML = `
|
||
<div class="landing-video-head">
|
||
<div><span class="landing-video-kicker">PRODUCT TOUR / PIER & ABUT</span><h2 id="landing-video-title">Pierzainer 소개 영상</h2></div>
|
||
<button type="button" class="landing-video-close" aria-label="소개 영상 닫기">×</button>
|
||
</div>
|
||
<div class="landing-video-frame">
|
||
<video controls playsinline muted preload="none" poster="${poster}" aria-describedby="landing-video-desc"></video>
|
||
</div>
|
||
<p id="landing-video-desc" class="landing-video-desc">설계정보 입력부터 3D 모델링, 철근배근, 도면작성, 수량산출까지의 흐름을 1분 동안 소개합니다. (음성 없음)</p>`;
|
||
root.append(dialog);
|
||
const video = dialog.querySelector('video')!;
|
||
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
||
|
||
const open = (): void => {
|
||
if (dialog.open) return;
|
||
if (!video.currentSrc && !video.querySelector('source')) {
|
||
const source = document.createElement('source');
|
||
source.src = introVideoUrl;
|
||
source.type = 'video/mp4';
|
||
video.append(source);
|
||
video.load();
|
||
}
|
||
dialog.showModal();
|
||
dialog.querySelector<HTMLButtonElement>('.landing-video-close')!.focus();
|
||
if (!reducedMotion?.matches) void video.play().catch(() => undefined);
|
||
};
|
||
const stop = (): void => {
|
||
video.pause();
|
||
if (video.readyState > 0) video.currentTime = 0;
|
||
};
|
||
const close = (restoreFocus = true): void => {
|
||
if (!dialog.open) return;
|
||
dialog.close();
|
||
if (restoreFocus && trigger.isConnected && !root.hidden) trigger.focus();
|
||
};
|
||
|
||
trigger.addEventListener('click', open);
|
||
dialog.querySelector('.landing-video-close')!.addEventListener('click', () => close());
|
||
dialog.addEventListener('close', stop);
|
||
dialog.addEventListener('cancel', (event) => { event.preventDefault(); close(); });
|
||
// Clicking the backdrop (outside the panel box) closes the dialog.
|
||
dialog.addEventListener('click', (event) => {
|
||
if (event.target !== dialog) return;
|
||
const box = dialog.getBoundingClientRect();
|
||
const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;
|
||
if (!inside) close();
|
||
});
|
||
return { open, close };
|
||
}
|