Files
huashu-design/assets/deck_stage.js
T
alchainandClaude Opus 4.7 7d7bab4a82 init: huashu-design skill v1.0
花叔设计工作台 —— AI 驱动的 HTML 设计工作流,支持 Claude Code / Codex /
Cursor / Trae / OpenClaw / Hermes Agent 等任意 skill-compatible agent。

四条主干能力:
- HTML 高保真视觉产出(原型/App/幻灯片/动画/变体/信息图)
- 设计方向顾问 Fallback(5 流派 × 20 设计哲学 × 24 预制 showcase)
- Junior Designer 工作流(先 show 假设再迭代)
- 专家级 5 维度评审(哲学/层级/执行/功能/创新)

工程工具链:
- HTML → 25fps/60fps MP4 + palette GIF,含 6 首场景化 BGM
- HTML → PDF / PPTX 导出
- Playwright 截图 + 控制台错误验证
- iOS/Android/macOS/Browser 设备边框 + Stage/Sprite 动画引擎

初版包含 SKILL.md + 13 份 references + 7 个 scripts + 18 类 assets。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:41:51 +08:00

406 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* <deck-stage> — HTML幻灯片外壳web component
*
* 提供功能:
* - 固定尺寸canvas(默认1920×1080)+ auto-scale + letterbox
* - 键盘导航(←/→/Space/Home/End/Esc)
* - 左右点击区域导航
* - slide counter (当前/总数)
* - localStorage持久化当前slide
* - Speaker notes postMessage (支持外层渲染)
* - Hash导航 (#slide-5 跳到第5张)
* - Print-to-PDF支持 (Cmd+P / Ctrl+P 一页一slide)
* - 自动给每个slide添加 data-screen-label
*
* 用法:
* <deck-stage>
* <section>Slide 1</section>
* <section>Slide 2</section>
* </deck-stage>
*
* 自定义尺寸:
* <deck-stage width="1080" height="1920">...</deck-stage>
*
* Speaker notes:在<head>加
* <script type="application/json" id="speaker-notes">
* ["slide 1 notes", "slide 2 notes"]
* </script>
*/
(function() {
const STORAGE_KEY_PREFIX = 'deck-stage-slide-';
class DeckStage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._currentSlide = 0;
this._slides = [];
this._storageKey = STORAGE_KEY_PREFIX + (location.pathname || 'default');
}
connectedCallback() {
this._width = parseInt(this.getAttribute('width')) || 1920;
this._height = parseInt(this.getAttribute('height')) || 1080;
this._render();
this._collectSlides();
this._setupEventListeners();
this._restoreSlide();
this._updateDisplay();
this._setupPrintStyles();
}
_render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
position: fixed;
inset: 0;
background: #000;
overflow: hidden;
font-family: -apple-system, 'SF Pro Text', 'PingFang SC', sans-serif;
}
:host([noscale]) .stage {
transform: none !important;
top: 0 !important;
left: 0 !important;
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform-origin: top left;
will-change: transform;
background: #fff;
}
.slide-wrapper {
width: 100%;
height: 100%;
position: relative;
}
::slotted(section) {
display: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
::slotted(section.active) {
display: block;
}
.counter {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
font-variant-numeric: tabular-nums;
z-index: 100;
user-select: none;
opacity: 0.6;
transition: opacity 0.2s;
}
.counter:hover {
opacity: 1;
}
.nav-zone {
position: fixed;
top: 0;
bottom: 0;
width: 15%;
cursor: pointer;
z-index: 50;
}
.nav-zone.left { left: 0; }
.nav-zone.right { right: 0; }
.nav-hint {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
opacity: 0;
transition: opacity 0.2s;
}
.nav-zone.left .nav-hint { left: 20px; }
.nav-zone.right .nav-hint { right: 20px; }
.nav-zone:hover .nav-hint {
opacity: 1;
}
@media print {
:host {
position: static;
background: #fff;
}
.counter, .nav-zone {
display: none !important;
}
.stage {
position: static;
transform: none !important;
page-break-after: always;
}
::slotted(section) {
display: block !important;
position: relative !important;
page-break-after: always;
width: 100%;
height: 100%;
}
}
</style>
<div class="stage" id="stage" style="width: ${this._width}px; height: ${this._height}px;">
<div class="slide-wrapper">
<slot></slot>
</div>
</div>
<div class="nav-zone left" id="navLeft">
<div class="nav-hint">‹</div>
</div>
<div class="nav-zone right" id="navRight">
<div class="nav-hint">›</div>
</div>
<div class="counter" id="counter">1 / 1</div>
`;
}
_collectSlides() {
this._slides = Array.from(this.querySelectorAll(':scope > section'));
this._slides.forEach((slide, idx) => {
if (!slide.hasAttribute('data-screen-label')) {
const num = String(idx + 1).padStart(2, '0');
slide.setAttribute('data-screen-label', num);
}
if (!slide.hasAttribute('data-om-validate')) {
slide.setAttribute('data-om-validate', '');
}
});
}
_setupEventListeners() {
window.addEventListener('resize', () => this._updateScale());
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, [contenteditable]')) return;
switch (e.key) {
case 'ArrowRight':
case ' ':
case 'PageDown':
e.preventDefault();
this.next();
break;
case 'ArrowLeft':
case 'PageUp':
e.preventDefault();
this.prev();
break;
case 'Home':
e.preventDefault();
this.goTo(0);
break;
case 'End':
e.preventDefault();
this.goTo(this._slides.length - 1);
break;
}
});
this.shadowRoot.getElementById('navLeft').addEventListener('click', () => this.prev());
this.shadowRoot.getElementById('navRight').addEventListener('click', () => this.next());
window.addEventListener('hashchange', () => this._handleHash());
if (location.hash) {
setTimeout(() => this._handleHash(), 0);
}
const observer = new MutationObserver(() => {
if (this.hasAttribute('noscale')) {
this._updateScale();
}
});
observer.observe(this, { attributes: true, attributeFilter: ['noscale'] });
}
_handleHash() {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1]) - 1;
if (idx >= 0 && idx < this._slides.length) {
this.goTo(idx);
}
}
}
_restoreSlide() {
try {
const stored = localStorage.getItem(this._storageKey);
if (stored !== null) {
const idx = parseInt(stored);
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
}
}
} catch (e) {}
}
_saveSlide() {
try {
localStorage.setItem(this._storageKey, String(this._currentSlide));
} catch (e) {}
}
_updateScale() {
if (this.hasAttribute('noscale')) {
const stage = this.shadowRoot.getElementById('stage');
stage.style.transform = 'none';
stage.style.top = '0';
stage.style.left = '0';
return;
}
const stage = this.shadowRoot.getElementById('stage');
if (!stage) return;
const viewportW = window.innerWidth;
const viewportH = window.innerHeight;
const scale = Math.min(viewportW / this._width, viewportH / this._height);
const scaledW = this._width * scale;
const scaledH = this._height * scale;
const offsetX = (viewportW - scaledW) / 2;
const offsetY = (viewportH - scaledH) / 2;
stage.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
stage.style.top = '0';
stage.style.left = '0';
}
_updateDisplay() {
this._slides.forEach((slide, idx) => {
slide.classList.toggle('active', idx === this._currentSlide);
});
const counter = this.shadowRoot.getElementById('counter');
if (counter) {
counter.textContent = `${this._currentSlide + 1} / ${this._slides.length}`;
}
this._updateScale();
try {
window.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
} catch (e) {}
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
}
} catch (e) {}
}
_setupPrintStyles() {
const printStyle = document.createElement('style');
printStyle.textContent = `
@media print {
@page {
size: ${this._width}px ${this._height}px;
margin: 0;
}
body {
margin: 0;
padding: 0;
}
deck-stage {
position: static !important;
}
deck-stage > section {
display: block !important;
position: relative !important;
width: ${this._width}px !important;
height: ${this._height}px !important;
page-break-after: always;
overflow: hidden;
}
deck-stage > section:last-child {
page-break-after: auto;
}
}
`;
document.head.appendChild(printStyle);
}
next() {
if (this._currentSlide < this._slides.length - 1) {
this._currentSlide++;
this._saveSlide();
this._updateDisplay();
}
}
prev() {
if (this._currentSlide > 0) {
this._currentSlide--;
this._saveSlide();
this._updateDisplay();
}
}
goTo(idx) {
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
this._saveSlide();
this._updateDisplay();
}
}
get currentSlide() {
return this._currentSlide;
}
get totalSlides() {
return this._slides.length;
}
}
customElements.define('deck-stage', DeckStage);
window.DeckStage = DeckStage;
})();