HH/.agents/skills/huashu-design/demos/c2-slides-pptx.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

1056 lines
32 KiB
HTML
Raw Permalink 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.

<!doctype html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<title>c2-slides-pptx · 中文版 · v2</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #000000;
--ink: #FFFFFF;
--ink-80: rgba(255,255,255,0.82);
--ink-60: rgba(255,255,255,0.58);
--muted: rgba(255,255,255,0.40);
--dim: rgba(255,255,255,0.18);
--hairline: rgba(255,255,255,0.12);
--accent: #D97757;
--accent-deep: #B85D3D;
--cd-bg: #F5F4F0;
--cd-panel: #FFFFFF;
--cd-ink: #1A1918;
--cd-dim: #8B867E;
--cd-hair: rgba(0,0,0,0.08);
--serif-cn: "Noto Serif SC", "Source Han Serif SC", serif;
--serif-en: "Source Serif 4", Georgia, serif;
--sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
--mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
}
html, body {
margin: 0; padding: 0;
background: #000;
overflow: hidden;
font-family: var(--sans);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
* { box-sizing: border-box; }
.stage {
position: fixed;
top: 50%; left: 50%;
width: 1920px; height: 1080px;
transform-origin: center center;
background: var(--bg);
overflow: hidden;
}
/* Film grain (2% opacity) */
.stage::after {
content: '';
position: absolute; inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
opacity: 0.025;
pointer-events: none;
mix-blend-mode: overlay;
z-index: 200;
}
.watermark-tl {
position: absolute;
top: 40px; left: 56px;
font-family: var(--mono);
font-size: 14px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255,255,255,0.16);
z-index: 180;
pointer-events: none;
}
/* ====== Beat 1: browser-fullscreen deck ====== */
.beat1 {
position: absolute; inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
}
.deck-window {
width: 1400px;
height: 788px;
border-radius: 14px;
background: #101010;
border: 1px solid var(--hairline);
box-shadow: 0 40px 120px -30px rgba(217,119,87,0.18),
0 0 0 1px rgba(255,255,255,0.03);
position: relative;
will-change: transform, opacity;
}
.deck-window .deck-body-wrap {
position: absolute;
top: 44px; left: 0; right: 0; bottom: 0;
border-radius: 0 0 14px 14px;
overflow: hidden;
background: #0A0A0A;
}
.deck-chrome {
height: 44px;
background: #161616;
border-bottom: 1px solid var(--hairline);
display: flex;
align-items: center;
padding: 0 18px;
gap: 14px;
}
.deck-chrome .traffic {
display: flex; gap: 8px;
}
.deck-chrome .traffic .d {
width: 11px; height: 11px; border-radius: 50%;
background: var(--hairline);
}
.deck-chrome .url {
flex: 1;
text-align: center;
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
letter-spacing: 0.02em;
}
.deck-chrome .page-count {
font-family: var(--mono);
font-size: 13px;
color: var(--accent);
letter-spacing: 0.08em;
min-width: 60px;
text-align: right;
}
.deck-slide {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background: #0A0A0A;
display: flex;
flex-direction: column;
justify-content: center;
padding: 96px 120px;
will-change: transform, opacity;
}
.deck-slide .eyebrow {
font-family: var(--mono);
font-size: 14px;
color: var(--accent);
letter-spacing: 0.24em;
text-transform: uppercase;
margin-bottom: 24px;
}
.deck-slide h1 {
font-family: var(--serif-cn);
font-size: 92px;
font-weight: 500;
line-height: 1.08;
color: var(--ink);
margin: 0 0 28px 0;
letter-spacing: -0.01em;
}
.deck-slide .sub {
font-family: var(--sans);
font-size: 22px;
color: var(--ink-60);
line-height: 1.5;
max-width: 780px;
}
.deck-slide .hairline {
margin-top: 48px;
width: 80px;
height: 2px;
background: var(--accent);
}
/* Key press indicator — sits below the window */
.key-hint {
position: absolute;
top: calc(50% + 440px);
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 14px;
font-family: var(--mono);
font-size: 13px;
color: var(--muted);
letter-spacing: 0.14em;
opacity: 0;
will-change: opacity;
z-index: 30;
}
.key-hint .kbd {
display: inline-flex;
align-items: center; justify-content: center;
width: 36px; height: 36px;
border: 1px solid var(--hairline);
border-radius: 6px;
background: rgba(255,255,255,0.04);
color: var(--ink-80);
font-size: 14px;
will-change: background, color, transform;
}
/* ====== Beat 2: split screen — HTML left, PowerPoint right ====== */
.beat2 {
position: absolute; inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 56px;
opacity: 0;
padding: 0 96px;
will-change: opacity;
}
.split-window {
width: 820px;
height: 580px;
border-radius: 12px;
overflow: hidden;
position: relative;
will-change: transform, opacity;
}
/* Left: HTML deck shrunk */
.split-left {
background: #0A0A0A;
border: 1px solid var(--hairline);
box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
}
.split-left .mini-chrome {
height: 30px;
background: #161616;
border-bottom: 1px solid var(--hairline);
display: flex;
align-items: center;
padding: 0 12px;
gap: 8px;
}
.split-left .mini-chrome .d {
width: 8px; height: 8px; border-radius: 50%;
background: var(--hairline);
}
.split-left .mini-chrome .label {
margin-left: 10px;
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 0.08em;
}
.split-left .mini-slide {
padding: 56px 64px;
height: calc(100% - 30px);
display: flex;
flex-direction: column;
justify-content: center;
}
.split-left .mini-eye {
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
letter-spacing: 0.22em;
text-transform: uppercase;
margin-bottom: 16px;
}
.split-left .mini-title {
font-family: var(--serif-cn);
font-size: 54px;
font-weight: 500;
line-height: 1.1;
color: var(--ink);
letter-spacing: -0.01em;
}
.split-left .mini-sub {
margin-top: 20px;
font-family: var(--sans);
font-size: 15px;
color: var(--ink-60);
line-height: 1.5;
}
.split-left .mini-hair {
margin-top: 28px;
width: 52px; height: 2px;
background: var(--accent);
}
/* Right: PowerPoint chrome */
.split-right {
background: #F3F2EE;
border: 1px solid rgba(0,0,0,0.2);
box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
}
.ppt-titlebar {
height: 32px;
background: #C44A36;
display: flex;
align-items: center;
padding: 0 14px;
gap: 10px;
color: #fff;
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.02em;
}
.ppt-titlebar .pp-logo {
width: 18px; height: 18px;
background: #fff;
border-radius: 2px;
display: inline-flex;
align-items: center; justify-content: center;
color: #C44A36;
font-weight: 700;
font-size: 11px;
font-family: var(--sans);
}
.ppt-titlebar .title-text { opacity: 0.92; }
.ppt-titlebar .win-dots {
margin-left: auto;
display: flex; gap: 10px;
opacity: 0.7;
}
.ppt-titlebar .win-dots span {
width: 10px; height: 10px; border: 1px solid rgba(255,255,255,0.7);
border-radius: 1px;
}
.ppt-toolbar {
height: 40px;
background: #EAE8E3;
border-bottom: 1px solid rgba(0,0,0,0.08);
display: flex;
align-items: center;
padding: 0 14px;
gap: 14px;
font-family: var(--sans);
font-size: 12px;
color: #4A4843;
}
.ppt-toolbar .tool {
display: flex; align-items: center; gap: 6px;
padding: 4px 10px;
border-radius: 4px;
}
.ppt-toolbar .tool.active {
background: #fff;
border: 1px solid rgba(0,0,0,0.08);
color: var(--cd-ink);
}
.ppt-toolbar .tool .ico {
width: 14px; height: 14px;
border: 1px solid currentColor;
border-radius: 2px;
opacity: 0.7;
}
.ppt-toolbar .font-name {
padding: 4px 10px;
background: #fff;
border: 1px solid rgba(0,0,0,0.12);
border-radius: 3px;
min-width: 140px;
font-size: 12px;
color: var(--cd-ink);
display: flex; align-items: center; justify-content: space-between;
}
.ppt-toolbar .divider {
width: 1px; height: 20px;
background: rgba(0,0,0,0.08);
}
/* PPT canvas (the actual slide) */
.ppt-canvas {
height: calc(100% - 32px - 40px);
background: #D8D4CB;
padding: 24px;
position: relative;
overflow: hidden;
}
.ppt-slide {
background: #0A0A0A;
border-radius: 3px;
width: 100%;
height: 100%;
padding: 56px 64px;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
}
.ppt-slide .ppt-eye {
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
letter-spacing: 0.22em;
text-transform: uppercase;
margin-bottom: 16px;
}
.ppt-slide .ppt-title-frame {
position: relative;
display: inline-block;
padding: 6px 10px;
margin: -6px -10px;
border-radius: 2px;
transition: box-shadow 0.12s ease;
align-self: flex-start;
max-width: fit-content;
min-width: 160px;
}
.ppt-slide .ppt-title-frame.selected {
box-shadow:
0 0 0 1px rgba(217,119,87,0.0),
inset 0 0 0 0 rgba(217,119,87,0.0);
}
.ppt-slide .ppt-title-frame.editing {
box-shadow:
0 0 0 1.5px var(--accent),
0 0 0 3px rgba(217,119,87,0.2);
}
.ppt-slide .ppt-title {
font-family: var(--serif-cn);
font-size: 54px;
font-weight: 500;
line-height: 1.1;
color: var(--ink);
letter-spacing: -0.01em;
display: inline;
position: relative;
}
.ppt-slide .edit-caret {
display: inline-block;
width: 2px;
height: 52px;
background: var(--accent);
vertical-align: -8px;
margin: 0 2px;
opacity: 0;
}
.ppt-slide .ppt-sub {
margin-top: 20px;
font-family: var(--sans);
font-size: 15px;
color: var(--ink-60);
line-height: 1.5;
}
.ppt-slide .ppt-hair {
margin-top: 28px;
width: 52px; height: 2px;
background: var(--accent);
}
/* Selection handles (corners) */
.ppt-slide .ppt-title-frame .handle {
position: absolute;
width: 8px; height: 8px;
background: var(--accent);
border: 1.5px solid #fff;
border-radius: 1px;
opacity: 0;
pointer-events: none;
}
.ppt-slide .ppt-title-frame .handle.tl { top: -4px; left: -4px; }
.ppt-slide .ppt-title-frame .handle.tr { top: -4px; right: -4px; }
.ppt-slide .ppt-title-frame .handle.bl { bottom: -4px; left: -4px; }
.ppt-slide .ppt-title-frame .handle.br { bottom: -4px; right: -4px; }
.ppt-slide .ppt-title-frame.selected .handle { opacity: 1; }
.ppt-slide .ppt-title-frame.editing .handle { opacity: 0; }
/* Mouse cursor */
.cursor {
position: absolute;
top: 0; left: 0;
width: 22px; height: 30px;
pointer-events: none;
z-index: 50;
opacity: 0;
will-change: transform, opacity;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.cursor svg { width: 100%; height: 100%; }
/* Double-click ripple */
.dblclick-ripple {
position: absolute;
top: 0; left: 0;
width: 20px; height: 20px;
border: 2px solid var(--accent);
border-radius: 50%;
pointer-events: none;
z-index: 45;
opacity: 0;
will-change: transform, opacity;
}
/* Connection line between two windows */
.connector {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
will-change: opacity;
z-index: 10;
}
.connector svg { width: 100%; height: 100%; }
.connector-label {
position: absolute;
top: calc(50% + 72px);
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 12px;
color: var(--accent);
letter-spacing: 0.12em;
white-space: nowrap;
opacity: 0;
will-change: opacity;
}
/* Stage labels above windows */
.split-label {
position: absolute;
top: -48px;
left: 0;
font-family: var(--mono);
font-size: 16px;
color: var(--ink-60);
letter-spacing: 0.18em;
text-transform: uppercase;
opacity: 0;
will-change: opacity;
white-space: nowrap;
}
.split-label .em { color: var(--accent); }
/* ====== Brand Reveal (米色面板 · hero-v10 系列 signature) ====== */
.brand-panel {
position: absolute;
inset: 0;
background: var(--cd-bg);
transform: translateY(100%);
will-change: transform;
z-index: 80;
}
.brand-reveal {
position: absolute;
inset: 0;
z-index: 81;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
will-change: opacity;
}
.brand-reveal .brand-wordmark {
font-family: var(--serif-en);
font-size: 72px;
font-weight: 100;
font-variation-settings: "wght" 100;
letter-spacing: -0.01em;
color: var(--cd-ink);
line-height: 1;
opacity: 0;
will-change: opacity, transform, font-variation-settings;
}
.brand-reveal .brand-wordmark .accent {
color: var(--accent);
font-weight: inherit;
}
.brand-reveal .brand-line {
width: 0;
height: 2px;
background: var(--accent);
margin-top: 60px;
will-change: width;
}
</style>
</head>
<body>
<div class="stage" id="stage">
<div class="watermark-tl">HUASHU · DESIGN</div>
<!-- ====== Beat 1 ====== -->
<div class="beat1" id="beat1">
<div class="deck-window" id="deckWindow">
<div class="deck-chrome">
<div class="traffic"><span class="d"></span><span class="d"></span><span class="d"></span></div>
<div class="url">localhost:8080 / deck · 全屏演讲</div>
<div class="page-count" id="pageCount">3 / 12</div>
</div>
<div class="deck-body-wrap">
<div class="deck-slide" id="slideA">
<div class="eyebrow">AI 心理学 · 第 3 节</div>
<h1>心智的<br/>可塑性</h1>
<div class="sub">Agent 不是工具,它有自己的偏好。</div>
<div class="hairline"></div>
</div>
<div class="deck-slide" id="slideB" style="opacity:0; transform: translateX(60px);">
<div class="eyebrow">AI 心理学 · 第 4 节</div>
<h1>注入与引导</h1>
<div class="sub">参数里藏着一个世界。</div>
<div class="hairline"></div>
</div>
</div>
</div>
<div class="key-hint" id="keyHint">
<span>键盘翻页</span>
<span class="kbd" id="kbdKey"></span>
</div>
</div>
<!-- ====== Beat 2: Split Screen ====== -->
<div class="beat2" id="beat2">
<!-- LEFT: HTML deck -->
<div class="split-col" style="position: relative;">
<div class="split-label" id="labelLeft">HTML · <span class="em">只读演示</span></div>
<div class="split-window split-left" id="splitLeft">
<div class="mini-chrome">
<span class="d"></span><span class="d"></span><span class="d"></span>
<span class="label">localhost:8080/deck</span>
</div>
<div class="mini-slide">
<div class="mini-eye">AI 心理学 · 第 3 节</div>
<div class="mini-title">心智的<br/>可塑性</div>
<div class="mini-sub">Agent 不是工具,它有自己的偏好。</div>
<div class="mini-hair"></div>
</div>
</div>
</div>
<!-- Connector -->
<div class="connector" id="connector">
<svg viewBox="0 0 56 120" fill="none">
<line x1="4" y1="60" x2="52" y2="60" stroke="#D97757" stroke-width="1.5" stroke-dasharray="4 4"/>
<polygon points="44,54 54,60 44,66" fill="#D97757"/>
</svg>
</div>
<div class="connector-label" id="connectorLabel">html2pptx.js</div>
<!-- RIGHT: PowerPoint -->
<div class="split-col" style="position: relative;">
<div class="split-label" id="labelRight">PowerPoint · <span class="em">真文本框可改</span></div>
<div class="split-window split-right" id="splitRight">
<div class="ppt-titlebar">
<div class="pp-logo">P</div>
<div class="title-text">AI-心理学-演讲.pptx - PowerPoint</div>
<div class="win-dots"><span></span><span></span><span></span></div>
</div>
<div class="ppt-toolbar">
<div class="tool">
<span class="ico"></span>
<span class="font-name"><span id="fontName">Noto Serif SC</span><span style="opacity:0.5"></span></span>
</div>
<div class="divider"></div>
<div class="tool"><span style="font-weight:700">B</span></div>
<div class="tool" style="font-style:italic">I</div>
<div class="tool" style="text-decoration:underline">U</div>
<div class="divider"></div>
<div class="tool active"><span class="ico" style="background:#D97757;border-color:#D97757"></span></div>
</div>
<div class="ppt-canvas">
<div class="ppt-slide">
<div class="ppt-eye">AI 心理学 · 第 3 节</div>
<div class="ppt-title-frame" id="titleFrame">
<span class="handle tl"></span>
<span class="handle tr"></span>
<span class="handle bl"></span>
<span class="handle br"></span>
<span class="ppt-title" id="titleText">心智的可塑性</span><span class="edit-caret" id="caret"></span>
</div>
<div class="ppt-sub">Agent 不是工具,它有自己的偏好。</div>
<div class="ppt-hair"></div>
</div>
<!-- Cursor arrow -->
<div class="cursor" id="cursor">
<svg viewBox="0 0 22 30" fill="none">
<path d="M2 2 L2 22 L8 17 L12 26 L16 24 L12 15 L20 14 Z"
fill="#1A1918" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
<!-- Double-click ripple -->
<div class="dblclick-ripple" id="ripple"></div>
</div>
</div>
</div>
</div>
<!-- ====== Brand Reveal (米色面板 · hero-v10 signature) ====== -->
<div class="brand-panel" id="brandPanel"></div>
<div class="brand-reveal" id="brandReveal">
<div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
<div class="brand-line" id="brandLine"></div>
</div>
</div>
<script>
(function() {
// ---------- Fit stage ----------
const stage = document.getElementById('stage');
function rescale() {
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
stage.style.transform = `translate(-50%, -50%) scale(${s})`;
}
rescale();
window.addEventListener('resize', rescale);
// ---------- Easings ----------
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
const expoIn = t => (t <= 0) ? 0 : (t >= 1) ? 1 : Math.pow(2, 10 * (t - 1));
const easeOut = t => 1 - Math.pow(1 - t, 3);
const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
function lerp(time, start, end, fromV, toV, ease) {
if (time <= start) return fromV;
if (time >= end) return toV;
let p = (time - start) / (end - start);
if (ease) p = ease(p);
return fromV + (toV - fromV) * p;
}
function clampLerp(time, start, end) {
if (time <= start) return 0;
if (time >= end) return 1;
return (time - start) / (end - start);
}
// ---------- Timeline (10s total) ----------
const T = {
DURATION: 10.0,
// Beat 1: 0 - 2s
deckIn: [0.15, 0.9], // browser fade+rise
keyHintIn: [0.6, 1.1],
keyPress: [1.25, 1.4], // arrow key highlight
slideFlip: [1.3, 1.9], // slide A→B
beat1Out: [2.0, 2.4],
// Beat 2: split screen: 2.2 - 8.0s
beat2In: [2.3, 2.9],
labelsIn: [3.0, 3.5],
cursorIn: [3.1, 3.4], // cursor arrives on right side
cursorMove1: [3.4, 4.1], // cursor moves to title
dblclick: [4.1, 4.3], // double click
frameSelect: [4.15, 4.35], // frame shows handles
frameEdit: [4.4, 4.55], // frame enters edit mode
caretShowStart: 4.5,
textDelete: [4.6, 5.4], // delete original text char by char
textRetype: [5.5, 7.2], // type new text char by char
commitEdit: [7.3, 7.5], // exit edit mode
connectorIn: [3.3, 3.9],
beat2Out: [8.0, 8.3], // main scene fades to 0 (0.3s)
// Brand Reveal (米色面板 · hero-v10 signature): 8.3 - 10s
// panelRise 与 beat2Out 微重叠 0.05s,避免黑屏间隙
panelRise: [8.25, 8.7], // 米色面板 translateY 100%→0 (expoOut)
wordmarkIn: [8.7, 9.3], // wordmark opacity 0→1 + translateY 20→0 + weight 100→500 (0.6s, expoOut)
brandLineIn: [9.3, 9.7], // brand-line expand 0→280px (0.4s, cubicOut)
brandHold: [9.7, 10.0], // hold (0.3s)
};
// ---------- Elements ----------
const beat1 = document.getElementById('beat1');
const beat2 = document.getElementById('beat2');
const brandReveal = document.getElementById('brandReveal');
const deckWindow = document.getElementById('deckWindow');
const pageCount = document.getElementById('pageCount');
const slideA = document.getElementById('slideA');
const slideB = document.getElementById('slideB');
const keyHint = document.getElementById('keyHint');
const kbdKey = document.getElementById('kbdKey');
const splitLeft = document.getElementById('splitLeft');
const splitRight = document.getElementById('splitRight');
const labelLeft = document.getElementById('labelLeft');
const labelRight = document.getElementById('labelRight');
const connector = document.getElementById('connector');
const connectorLabel = document.getElementById('connectorLabel');
const cursor = document.getElementById('cursor');
const ripple = document.getElementById('ripple');
const titleFrame = document.getElementById('titleFrame');
const titleText = document.getElementById('titleText');
const caret = document.getElementById('caret');
const panel = document.getElementById('brandPanel');
const wordmark = document.getElementById('wordmark');
const brandLine = document.getElementById('brandLine');
// Text to animate
const ORIG_TEXT = '心智的可塑性';
const NEW_TEXT = '心智 · 可塑性';
// ---------- Render ----------
function render(t) {
/* ======= Beat 1 ======= */
let beat1Op;
if (t < T.beat1Out[0]) {
beat1Op = lerp(t, T.deckIn[0], T.deckIn[1], 0, 1, expoOut);
} else {
beat1Op = 1 - clampLerp(t, T.beat1Out[0], T.beat1Out[1]);
}
beat1.style.opacity = beat1Op;
beat1.style.visibility = beat1Op > 0.01 ? 'visible' : 'hidden';
// Deck window rise
const deckRise = lerp(t, T.deckIn[0], T.deckIn[1], 24, 0, expoOut);
deckWindow.style.transform = `translate3d(0, ${deckRise}px, 0)`;
// Key hint appear
const khOp = clampLerp(t, T.keyHintIn[0], T.keyHintIn[1]);
keyHint.style.opacity = khOp;
// Key press flash
const kpActive = t >= T.keyPress[0] && t < T.keyPress[1] + 0.2;
if (kpActive) {
const kp = clampLerp(t, T.keyPress[0], T.keyPress[1]);
kbdKey.style.background = `rgba(217,119,87,${0.9 * (1 - kp * 0.4)})`;
kbdKey.style.color = '#fff';
kbdKey.style.transform = `scale(${1 - 0.08 * kp})`;
} else {
kbdKey.style.background = '';
kbdKey.style.color = '';
kbdKey.style.transform = '';
}
// Slide flip A→B
if (t >= T.slideFlip[0] && t < T.slideFlip[1] + 0.2) {
const sp = clampLerp(t, T.slideFlip[0], T.slideFlip[1]);
const eased = expoOut(sp);
slideA.style.opacity = 1 - eased;
slideA.style.transform = `translateX(${-60 * eased}px)`;
slideB.style.opacity = eased;
slideB.style.transform = `translateX(${60 * (1 - eased)}px)`;
// Update page count at midway
if (sp > 0.5) pageCount.textContent = '4 / 12';
else pageCount.textContent = '3 / 12';
} else if (t >= T.slideFlip[1]) {
slideA.style.opacity = 0;
slideB.style.opacity = 1;
slideB.style.transform = 'translateX(0)';
pageCount.textContent = '4 / 12';
} else {
slideA.style.opacity = 1;
slideA.style.transform = 'translateX(0)';
slideB.style.opacity = 0;
pageCount.textContent = '3 / 12';
}
/* ======= Beat 2 ======= */
let beat2Op = 0;
if (t >= T.beat2In[0] && t < T.beat2Out[1]) {
if (t < T.beat2In[1]) beat2Op = clampLerp(t, T.beat2In[0], T.beat2In[1]);
else if (t < T.beat2Out[0]) beat2Op = 1;
else beat2Op = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
}
beat2.style.opacity = beat2Op;
beat2.style.visibility = beat2Op > 0.01 ? 'visible' : 'hidden';
// Windows rise in
const splitInP = clampLerp(t, T.beat2In[0], T.beat2In[1]);
const splitRise = lerp(t, T.beat2In[0], T.beat2In[1], 28, 0, expoOut);
splitLeft.style.transform = `translate3d(${-8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
splitRight.style.transform = `translate3d(${8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
// Labels
const labelOp = clampLerp(t, T.labelsIn[0], T.labelsIn[1]);
labelLeft.style.opacity = labelOp * 0.7;
labelRight.style.opacity = labelOp * 0.85;
// Connector
const connOp = clampLerp(t, T.connectorIn[0], T.connectorIn[1]);
connector.style.opacity = connOp;
connectorLabel.style.opacity = connOp * 0.9;
/* === Cursor movement === */
// Cursor positions (relative to .ppt-canvas, which is inside split-right)
// Canvas starts at (0,0), size ~820 × 508 (580 - 32 - 40)
// Title sits around x=84 y=110 (inside .ppt-slide padding 56/64)
// We'll place cursor with absolute positioning inside .ppt-canvas.
// Entry point: off to the right bottom of canvas
const P_ENTER = { x: 720, y: 420 };
const P_TITLE = { x: 250, y: 170 }; // on the title
let cursorOp = 0;
let cx = P_ENTER.x, cy = P_ENTER.y;
if (t >= T.cursorIn[0] && t < T.beat2Out[0]) {
cursorOp = 1;
// Phase 1: appear (pop in with slight scale)
const inP = clampLerp(t, T.cursorIn[0], T.cursorIn[1]);
cursorOp = expoOut(inP);
// Phase 2: move to title
if (t >= T.cursorMove1[0]) {
const mp = clampLerp(t, T.cursorMove1[0], T.cursorMove1[1]);
const e = easeInOut(mp);
cx = P_ENTER.x + (P_TITLE.x - P_ENTER.x) * e;
cy = P_ENTER.y + (P_TITLE.y - P_ENTER.y) * e;
} else {
cx = P_ENTER.x;
cy = P_ENTER.y;
}
// After double-click, slight jitter toward caret position during typing
if (t >= T.textRetype[0] && t < T.textRetype[1]) {
cx = P_TITLE.x + 6;
cy = P_TITLE.y - 2;
}
} else if (t >= T.beat2Out[0]) {
cursorOp = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
}
cursor.style.opacity = cursorOp;
cursor.style.transform = `translate(${cx}px, ${cy}px)`;
/* === Double-click ripple === */
// Ripple pulses twice at T.dblclick start
let rippleVisible = false;
if (t >= T.dblclick[0] && t < T.dblclick[0] + 0.7) {
const dt = t - T.dblclick[0];
// Two rapid pulses
const pulse1 = clamp(dt / 0.25, 0, 1);
const pulse2 = clamp((dt - 0.15) / 0.25, 0, 1);
const scale1 = 0.4 + pulse1 * 1.4;
const scale2 = 0.4 + pulse2 * 1.4;
const op1 = 1 - pulse1;
const op2 = dt > 0.15 ? (1 - pulse2) : 0;
// Render as single element: use larger of the two
const scale = Math.max(scale1, scale2);
const op = Math.max(op1, op2);
ripple.style.opacity = op;
ripple.style.transform = `translate(-50%, -50%) translate(${P_TITLE.x + 6}px, ${P_TITLE.y + 26}px) scale(${scale})`;
rippleVisible = true;
}
if (!rippleVisible) ripple.style.opacity = 0;
/* === Frame states: selected → editing === */
titleFrame.classList.remove('selected', 'editing');
if (t >= T.frameSelect[0] && t < T.frameEdit[0]) {
titleFrame.classList.add('selected');
} else if (t >= T.frameEdit[0] && t < T.commitEdit[1]) {
titleFrame.classList.add('editing');
}
/* === Text animation: delete → retype === */
let displayedText = ORIG_TEXT;
let caretOp = 0;
if (t < T.textDelete[0]) {
displayedText = ORIG_TEXT;
caretOp = t >= T.caretShowStart ? 1 : 0;
} else if (t < T.textDelete[1]) {
// Delete: remove chars from end
const dp = clampLerp(t, T.textDelete[0], T.textDelete[1]);
const charsToRemove = Math.floor(dp * ORIG_TEXT.length);
displayedText = ORIG_TEXT.slice(0, ORIG_TEXT.length - charsToRemove);
caretOp = 1;
} else if (t < T.textRetype[0]) {
displayedText = '';
caretOp = 1;
} else if (t < T.textRetype[1]) {
// Retype new text
const rp = clampLerp(t, T.textRetype[0], T.textRetype[1]);
const charsToShow = Math.floor(rp * NEW_TEXT.length);
displayedText = NEW_TEXT.slice(0, charsToShow);
caretOp = 1;
} else if (t < T.commitEdit[1]) {
displayedText = NEW_TEXT;
// Caret blinks while still in edit mode
caretOp = (Math.floor(t * 2) % 2 === 0) ? 1 : 0.3;
} else {
displayedText = NEW_TEXT;
caretOp = 0;
}
// Blinking during idle-in-edit phases (when not actively typing/deleting)
if (t >= T.caretShowStart && t < T.textDelete[0]) {
caretOp = (Math.floor((t - T.caretShowStart) * 3) % 2 === 0) ? 1 : 0.35;
}
titleText.textContent = displayedText;
caret.style.opacity = caretOp;
/* ======= Brand Reveal (米色面板 · hero-v10 signature) ======= */
// Panel rises from bottom (米色面板 #F5F4F0)
const panelP = clampLerp(t, T.panelRise[0], T.panelRise[1]);
panel.style.transform = `translateY(${(1 - expoOut(panelP)) * 100}%)`;
// brand-reveal container visible once panel starts rising
brandReveal.style.opacity = panelP > 0.01 ? 1 : 0;
// Wordmark: opacity 0→1 + translateY 20→0 + weight 100→500 (expoOut)
const wmP = clampLerp(t, T.wordmarkIn[0], T.wordmarkIn[1]);
const wmEased = expoOut(wmP);
wordmark.style.opacity = wmEased;
const wmRise = (1 - wmEased) * 20;
wordmark.style.transform = `translate3d(0, ${wmRise}px, 0)`;
const w = 100 + (500 - 100) * wmEased;
wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
wordmark.style.fontWeight = Math.round(w);
// Brand line expand 0→280px (cubicOut)
const lineP = clampLerp(t, T.brandLineIn[0], T.brandLineIn[1]);
const cubicOut = x => 1 - Math.pow(1 - x, 3);
brandLine.style.width = (280 * cubicOut(lineP)) + 'px';
}
// ---------- Driver ----------
let manualT = null;
let startMs = null;
let hasFinished = false;
function tick(now) {
if (manualT != null) render(manualT);
else {
if (startMs == null) startMs = now;
const elapsed = (now - startMs) / 1000;
const recording = window.__recording === true;
let t;
if (recording) {
t = Math.min(elapsed, T.DURATION - 0.001);
if (elapsed >= T.DURATION) hasFinished = true;
} else {
t = elapsed % T.DURATION;
}
render(t);
}
requestAnimationFrame(tick);
}
// Force first-frame render synchronously, THEN set ready
render(0);
requestAnimationFrame(tick);
window.__setTime = function(t) { manualT = t; render(t); };
window.__resume = function() { manualT = null; startMs = null; };
window.__duration = T.DURATION;
window.__render = render;
window.__ready = true;
})();
</script>
</body>
</html>