1
0

c2-slides-pptx.html 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. <!doctype html>
  2. <html lang="zh-Hans">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>c2-slides-pptx · 中文版 · v2</title>
  6. <link rel="preconnect" href="https://fonts.googleapis.com">
  7. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  8. <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">
  9. <style>
  10. :root {
  11. --bg: #000000;
  12. --ink: #FFFFFF;
  13. --ink-80: rgba(255,255,255,0.82);
  14. --ink-60: rgba(255,255,255,0.58);
  15. --muted: rgba(255,255,255,0.40);
  16. --dim: rgba(255,255,255,0.18);
  17. --hairline: rgba(255,255,255,0.12);
  18. --accent: #D97757;
  19. --accent-deep: #B85D3D;
  20. --cd-bg: #F5F4F0;
  21. --cd-panel: #FFFFFF;
  22. --cd-ink: #1A1918;
  23. --cd-dim: #8B867E;
  24. --cd-hair: rgba(0,0,0,0.08);
  25. --serif-cn: "Noto Serif SC", "Source Han Serif SC", serif;
  26. --serif-en: "Source Serif 4", Georgia, serif;
  27. --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
  28. --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
  29. }
  30. html, body {
  31. margin: 0; padding: 0;
  32. background: #000;
  33. overflow: hidden;
  34. font-family: var(--sans);
  35. color: var(--ink);
  36. -webkit-font-smoothing: antialiased;
  37. }
  38. * { box-sizing: border-box; }
  39. .stage {
  40. position: fixed;
  41. top: 50%; left: 50%;
  42. width: 1920px; height: 1080px;
  43. transform-origin: center center;
  44. background: var(--bg);
  45. overflow: hidden;
  46. }
  47. /* Film grain (2% opacity) */
  48. .stage::after {
  49. content: '';
  50. position: absolute; inset: 0;
  51. 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>");
  52. opacity: 0.025;
  53. pointer-events: none;
  54. mix-blend-mode: overlay;
  55. z-index: 200;
  56. }
  57. .watermark-tl {
  58. position: absolute;
  59. top: 40px; left: 56px;
  60. font-family: var(--mono);
  61. font-size: 14px;
  62. letter-spacing: 0.2em;
  63. text-transform: uppercase;
  64. color: rgba(255,255,255,0.16);
  65. z-index: 180;
  66. pointer-events: none;
  67. }
  68. /* ====== Beat 1: browser-fullscreen deck ====== */
  69. .beat1 {
  70. position: absolute; inset: 0;
  71. display: flex;
  72. align-items: center;
  73. justify-content: center;
  74. opacity: 1;
  75. }
  76. .deck-window {
  77. width: 1400px;
  78. height: 788px;
  79. border-radius: 14px;
  80. background: #101010;
  81. border: 1px solid var(--hairline);
  82. box-shadow: 0 40px 120px -30px rgba(217,119,87,0.18),
  83. 0 0 0 1px rgba(255,255,255,0.03);
  84. position: relative;
  85. will-change: transform, opacity;
  86. }
  87. .deck-window .deck-body-wrap {
  88. position: absolute;
  89. top: 44px; left: 0; right: 0; bottom: 0;
  90. border-radius: 0 0 14px 14px;
  91. overflow: hidden;
  92. background: #0A0A0A;
  93. }
  94. .deck-chrome {
  95. height: 44px;
  96. background: #161616;
  97. border-bottom: 1px solid var(--hairline);
  98. display: flex;
  99. align-items: center;
  100. padding: 0 18px;
  101. gap: 14px;
  102. }
  103. .deck-chrome .traffic {
  104. display: flex; gap: 8px;
  105. }
  106. .deck-chrome .traffic .d {
  107. width: 11px; height: 11px; border-radius: 50%;
  108. background: var(--hairline);
  109. }
  110. .deck-chrome .url {
  111. flex: 1;
  112. text-align: center;
  113. font-family: var(--mono);
  114. font-size: 12px;
  115. color: var(--muted);
  116. letter-spacing: 0.02em;
  117. }
  118. .deck-chrome .page-count {
  119. font-family: var(--mono);
  120. font-size: 13px;
  121. color: var(--accent);
  122. letter-spacing: 0.08em;
  123. min-width: 60px;
  124. text-align: right;
  125. }
  126. .deck-slide {
  127. position: absolute;
  128. top: 0; left: 0;
  129. width: 100%;
  130. height: 100%;
  131. background: #0A0A0A;
  132. display: flex;
  133. flex-direction: column;
  134. justify-content: center;
  135. padding: 96px 120px;
  136. will-change: transform, opacity;
  137. }
  138. .deck-slide .eyebrow {
  139. font-family: var(--mono);
  140. font-size: 14px;
  141. color: var(--accent);
  142. letter-spacing: 0.24em;
  143. text-transform: uppercase;
  144. margin-bottom: 24px;
  145. }
  146. .deck-slide h1 {
  147. font-family: var(--serif-cn);
  148. font-size: 92px;
  149. font-weight: 500;
  150. line-height: 1.08;
  151. color: var(--ink);
  152. margin: 0 0 28px 0;
  153. letter-spacing: -0.01em;
  154. }
  155. .deck-slide .sub {
  156. font-family: var(--sans);
  157. font-size: 22px;
  158. color: var(--ink-60);
  159. line-height: 1.5;
  160. max-width: 780px;
  161. }
  162. .deck-slide .hairline {
  163. margin-top: 48px;
  164. width: 80px;
  165. height: 2px;
  166. background: var(--accent);
  167. }
  168. /* Key press indicator — sits below the window */
  169. .key-hint {
  170. position: absolute;
  171. top: calc(50% + 440px);
  172. left: 50%;
  173. transform: translateX(-50%);
  174. display: flex;
  175. align-items: center;
  176. gap: 14px;
  177. font-family: var(--mono);
  178. font-size: 13px;
  179. color: var(--muted);
  180. letter-spacing: 0.14em;
  181. opacity: 0;
  182. will-change: opacity;
  183. z-index: 30;
  184. }
  185. .key-hint .kbd {
  186. display: inline-flex;
  187. align-items: center; justify-content: center;
  188. width: 36px; height: 36px;
  189. border: 1px solid var(--hairline);
  190. border-radius: 6px;
  191. background: rgba(255,255,255,0.04);
  192. color: var(--ink-80);
  193. font-size: 14px;
  194. will-change: background, color, transform;
  195. }
  196. /* ====== Beat 2: split screen — HTML left, PowerPoint right ====== */
  197. .beat2 {
  198. position: absolute; inset: 0;
  199. display: flex;
  200. align-items: center;
  201. justify-content: center;
  202. gap: 56px;
  203. opacity: 0;
  204. padding: 0 96px;
  205. will-change: opacity;
  206. }
  207. .split-window {
  208. width: 820px;
  209. height: 580px;
  210. border-radius: 12px;
  211. overflow: hidden;
  212. position: relative;
  213. will-change: transform, opacity;
  214. }
  215. /* Left: HTML deck shrunk */
  216. .split-left {
  217. background: #0A0A0A;
  218. border: 1px solid var(--hairline);
  219. box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
  220. }
  221. .split-left .mini-chrome {
  222. height: 30px;
  223. background: #161616;
  224. border-bottom: 1px solid var(--hairline);
  225. display: flex;
  226. align-items: center;
  227. padding: 0 12px;
  228. gap: 8px;
  229. }
  230. .split-left .mini-chrome .d {
  231. width: 8px; height: 8px; border-radius: 50%;
  232. background: var(--hairline);
  233. }
  234. .split-left .mini-chrome .label {
  235. margin-left: 10px;
  236. font-family: var(--mono);
  237. font-size: 11px;
  238. color: var(--muted);
  239. letter-spacing: 0.08em;
  240. }
  241. .split-left .mini-slide {
  242. padding: 56px 64px;
  243. height: calc(100% - 30px);
  244. display: flex;
  245. flex-direction: column;
  246. justify-content: center;
  247. }
  248. .split-left .mini-eye {
  249. font-family: var(--mono);
  250. font-size: 11px;
  251. color: var(--accent);
  252. letter-spacing: 0.22em;
  253. text-transform: uppercase;
  254. margin-bottom: 16px;
  255. }
  256. .split-left .mini-title {
  257. font-family: var(--serif-cn);
  258. font-size: 54px;
  259. font-weight: 500;
  260. line-height: 1.1;
  261. color: var(--ink);
  262. letter-spacing: -0.01em;
  263. }
  264. .split-left .mini-sub {
  265. margin-top: 20px;
  266. font-family: var(--sans);
  267. font-size: 15px;
  268. color: var(--ink-60);
  269. line-height: 1.5;
  270. }
  271. .split-left .mini-hair {
  272. margin-top: 28px;
  273. width: 52px; height: 2px;
  274. background: var(--accent);
  275. }
  276. /* Right: PowerPoint chrome */
  277. .split-right {
  278. background: #F3F2EE;
  279. border: 1px solid rgba(0,0,0,0.2);
  280. box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
  281. }
  282. .ppt-titlebar {
  283. height: 32px;
  284. background: #C44A36;
  285. display: flex;
  286. align-items: center;
  287. padding: 0 14px;
  288. gap: 10px;
  289. color: #fff;
  290. font-family: var(--sans);
  291. font-size: 12px;
  292. font-weight: 500;
  293. letter-spacing: 0.02em;
  294. }
  295. .ppt-titlebar .pp-logo {
  296. width: 18px; height: 18px;
  297. background: #fff;
  298. border-radius: 2px;
  299. display: inline-flex;
  300. align-items: center; justify-content: center;
  301. color: #C44A36;
  302. font-weight: 700;
  303. font-size: 11px;
  304. font-family: var(--sans);
  305. }
  306. .ppt-titlebar .title-text { opacity: 0.92; }
  307. .ppt-titlebar .win-dots {
  308. margin-left: auto;
  309. display: flex; gap: 10px;
  310. opacity: 0.7;
  311. }
  312. .ppt-titlebar .win-dots span {
  313. width: 10px; height: 10px; border: 1px solid rgba(255,255,255,0.7);
  314. border-radius: 1px;
  315. }
  316. .ppt-toolbar {
  317. height: 40px;
  318. background: #EAE8E3;
  319. border-bottom: 1px solid rgba(0,0,0,0.08);
  320. display: flex;
  321. align-items: center;
  322. padding: 0 14px;
  323. gap: 14px;
  324. font-family: var(--sans);
  325. font-size: 12px;
  326. color: #4A4843;
  327. }
  328. .ppt-toolbar .tool {
  329. display: flex; align-items: center; gap: 6px;
  330. padding: 4px 10px;
  331. border-radius: 4px;
  332. }
  333. .ppt-toolbar .tool.active {
  334. background: #fff;
  335. border: 1px solid rgba(0,0,0,0.08);
  336. color: var(--cd-ink);
  337. }
  338. .ppt-toolbar .tool .ico {
  339. width: 14px; height: 14px;
  340. border: 1px solid currentColor;
  341. border-radius: 2px;
  342. opacity: 0.7;
  343. }
  344. .ppt-toolbar .font-name {
  345. padding: 4px 10px;
  346. background: #fff;
  347. border: 1px solid rgba(0,0,0,0.12);
  348. border-radius: 3px;
  349. min-width: 140px;
  350. font-size: 12px;
  351. color: var(--cd-ink);
  352. display: flex; align-items: center; justify-content: space-between;
  353. }
  354. .ppt-toolbar .divider {
  355. width: 1px; height: 20px;
  356. background: rgba(0,0,0,0.08);
  357. }
  358. /* PPT canvas (the actual slide) */
  359. .ppt-canvas {
  360. height: calc(100% - 32px - 40px);
  361. background: #D8D4CB;
  362. padding: 24px;
  363. position: relative;
  364. overflow: hidden;
  365. }
  366. .ppt-slide {
  367. background: #0A0A0A;
  368. border-radius: 3px;
  369. width: 100%;
  370. height: 100%;
  371. padding: 56px 64px;
  372. display: flex;
  373. flex-direction: column;
  374. justify-content: center;
  375. position: relative;
  376. box-shadow: 0 4px 16px rgba(0,0,0,0.18);
  377. }
  378. .ppt-slide .ppt-eye {
  379. font-family: var(--mono);
  380. font-size: 11px;
  381. color: var(--accent);
  382. letter-spacing: 0.22em;
  383. text-transform: uppercase;
  384. margin-bottom: 16px;
  385. }
  386. .ppt-slide .ppt-title-frame {
  387. position: relative;
  388. display: inline-block;
  389. padding: 6px 10px;
  390. margin: -6px -10px;
  391. border-radius: 2px;
  392. transition: box-shadow 0.12s ease;
  393. align-self: flex-start;
  394. max-width: fit-content;
  395. min-width: 160px;
  396. }
  397. .ppt-slide .ppt-title-frame.selected {
  398. box-shadow:
  399. 0 0 0 1px rgba(217,119,87,0.0),
  400. inset 0 0 0 0 rgba(217,119,87,0.0);
  401. }
  402. .ppt-slide .ppt-title-frame.editing {
  403. box-shadow:
  404. 0 0 0 1.5px var(--accent),
  405. 0 0 0 3px rgba(217,119,87,0.2);
  406. }
  407. .ppt-slide .ppt-title {
  408. font-family: var(--serif-cn);
  409. font-size: 54px;
  410. font-weight: 500;
  411. line-height: 1.1;
  412. color: var(--ink);
  413. letter-spacing: -0.01em;
  414. display: inline;
  415. position: relative;
  416. }
  417. .ppt-slide .edit-caret {
  418. display: inline-block;
  419. width: 2px;
  420. height: 52px;
  421. background: var(--accent);
  422. vertical-align: -8px;
  423. margin: 0 2px;
  424. opacity: 0;
  425. }
  426. .ppt-slide .ppt-sub {
  427. margin-top: 20px;
  428. font-family: var(--sans);
  429. font-size: 15px;
  430. color: var(--ink-60);
  431. line-height: 1.5;
  432. }
  433. .ppt-slide .ppt-hair {
  434. margin-top: 28px;
  435. width: 52px; height: 2px;
  436. background: var(--accent);
  437. }
  438. /* Selection handles (corners) */
  439. .ppt-slide .ppt-title-frame .handle {
  440. position: absolute;
  441. width: 8px; height: 8px;
  442. background: var(--accent);
  443. border: 1.5px solid #fff;
  444. border-radius: 1px;
  445. opacity: 0;
  446. pointer-events: none;
  447. }
  448. .ppt-slide .ppt-title-frame .handle.tl { top: -4px; left: -4px; }
  449. .ppt-slide .ppt-title-frame .handle.tr { top: -4px; right: -4px; }
  450. .ppt-slide .ppt-title-frame .handle.bl { bottom: -4px; left: -4px; }
  451. .ppt-slide .ppt-title-frame .handle.br { bottom: -4px; right: -4px; }
  452. .ppt-slide .ppt-title-frame.selected .handle { opacity: 1; }
  453. .ppt-slide .ppt-title-frame.editing .handle { opacity: 0; }
  454. /* Mouse cursor */
  455. .cursor {
  456. position: absolute;
  457. top: 0; left: 0;
  458. width: 22px; height: 30px;
  459. pointer-events: none;
  460. z-index: 50;
  461. opacity: 0;
  462. will-change: transform, opacity;
  463. filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
  464. }
  465. .cursor svg { width: 100%; height: 100%; }
  466. /* Double-click ripple */
  467. .dblclick-ripple {
  468. position: absolute;
  469. top: 0; left: 0;
  470. width: 20px; height: 20px;
  471. border: 2px solid var(--accent);
  472. border-radius: 50%;
  473. pointer-events: none;
  474. z-index: 45;
  475. opacity: 0;
  476. will-change: transform, opacity;
  477. }
  478. /* Connection line between two windows */
  479. .connector {
  480. position: absolute;
  481. top: 50%;
  482. left: 50%;
  483. transform: translate(-50%, -50%);
  484. width: 56px;
  485. height: 120px;
  486. display: flex;
  487. align-items: center;
  488. justify-content: center;
  489. opacity: 0;
  490. will-change: opacity;
  491. z-index: 10;
  492. }
  493. .connector svg { width: 100%; height: 100%; }
  494. .connector-label {
  495. position: absolute;
  496. top: calc(50% + 72px);
  497. left: 50%;
  498. transform: translateX(-50%);
  499. font-family: var(--mono);
  500. font-size: 12px;
  501. color: var(--accent);
  502. letter-spacing: 0.12em;
  503. white-space: nowrap;
  504. opacity: 0;
  505. will-change: opacity;
  506. }
  507. /* Stage labels above windows */
  508. .split-label {
  509. position: absolute;
  510. top: -48px;
  511. left: 0;
  512. font-family: var(--mono);
  513. font-size: 16px;
  514. color: var(--ink-60);
  515. letter-spacing: 0.18em;
  516. text-transform: uppercase;
  517. opacity: 0;
  518. will-change: opacity;
  519. white-space: nowrap;
  520. }
  521. .split-label .em { color: var(--accent); }
  522. /* ====== Brand Reveal (米色面板 · hero-v10 系列 signature) ====== */
  523. .brand-panel {
  524. position: absolute;
  525. inset: 0;
  526. background: var(--cd-bg);
  527. transform: translateY(100%);
  528. will-change: transform;
  529. z-index: 80;
  530. }
  531. .brand-reveal {
  532. position: absolute;
  533. inset: 0;
  534. z-index: 81;
  535. display: flex;
  536. flex-direction: column;
  537. align-items: center;
  538. justify-content: center;
  539. opacity: 0;
  540. pointer-events: none;
  541. will-change: opacity;
  542. }
  543. .brand-reveal .brand-wordmark {
  544. font-family: var(--serif-en);
  545. font-size: 72px;
  546. font-weight: 100;
  547. font-variation-settings: "wght" 100;
  548. letter-spacing: -0.01em;
  549. color: var(--cd-ink);
  550. line-height: 1;
  551. opacity: 0;
  552. will-change: opacity, transform, font-variation-settings;
  553. }
  554. .brand-reveal .brand-wordmark .accent {
  555. color: var(--accent);
  556. font-weight: inherit;
  557. }
  558. .brand-reveal .brand-line {
  559. width: 0;
  560. height: 2px;
  561. background: var(--accent);
  562. margin-top: 60px;
  563. will-change: width;
  564. }
  565. </style>
  566. </head>
  567. <body>
  568. <div class="stage" id="stage">
  569. <div class="watermark-tl">HUASHU · DESIGN</div>
  570. <!-- ====== Beat 1 ====== -->
  571. <div class="beat1" id="beat1">
  572. <div class="deck-window" id="deckWindow">
  573. <div class="deck-chrome">
  574. <div class="traffic"><span class="d"></span><span class="d"></span><span class="d"></span></div>
  575. <div class="url">localhost:8080 / deck · 全屏演讲</div>
  576. <div class="page-count" id="pageCount">3 / 12</div>
  577. </div>
  578. <div class="deck-body-wrap">
  579. <div class="deck-slide" id="slideA">
  580. <div class="eyebrow">AI 心理学 · 第 3 节</div>
  581. <h1>心智的<br/>可塑性</h1>
  582. <div class="sub">Agent 不是工具,它有自己的偏好。</div>
  583. <div class="hairline"></div>
  584. </div>
  585. <div class="deck-slide" id="slideB" style="opacity:0; transform: translateX(60px);">
  586. <div class="eyebrow">AI 心理学 · 第 4 节</div>
  587. <h1>注入与引导</h1>
  588. <div class="sub">参数里藏着一个世界。</div>
  589. <div class="hairline"></div>
  590. </div>
  591. </div>
  592. </div>
  593. <div class="key-hint" id="keyHint">
  594. <span>键盘翻页</span>
  595. <span class="kbd" id="kbdKey">→</span>
  596. </div>
  597. </div>
  598. <!-- ====== Beat 2: Split Screen ====== -->
  599. <div class="beat2" id="beat2">
  600. <!-- LEFT: HTML deck -->
  601. <div class="split-col" style="position: relative;">
  602. <div class="split-label" id="labelLeft">HTML · <span class="em">只读演示</span></div>
  603. <div class="split-window split-left" id="splitLeft">
  604. <div class="mini-chrome">
  605. <span class="d"></span><span class="d"></span><span class="d"></span>
  606. <span class="label">localhost:8080/deck</span>
  607. </div>
  608. <div class="mini-slide">
  609. <div class="mini-eye">AI 心理学 · 第 3 节</div>
  610. <div class="mini-title">心智的<br/>可塑性</div>
  611. <div class="mini-sub">Agent 不是工具,它有自己的偏好。</div>
  612. <div class="mini-hair"></div>
  613. </div>
  614. </div>
  615. </div>
  616. <!-- Connector -->
  617. <div class="connector" id="connector">
  618. <svg viewBox="0 0 56 120" fill="none">
  619. <line x1="4" y1="60" x2="52" y2="60" stroke="#D97757" stroke-width="1.5" stroke-dasharray="4 4"/>
  620. <polygon points="44,54 54,60 44,66" fill="#D97757"/>
  621. </svg>
  622. </div>
  623. <div class="connector-label" id="connectorLabel">html2pptx.js</div>
  624. <!-- RIGHT: PowerPoint -->
  625. <div class="split-col" style="position: relative;">
  626. <div class="split-label" id="labelRight">PowerPoint · <span class="em">真文本框可改</span></div>
  627. <div class="split-window split-right" id="splitRight">
  628. <div class="ppt-titlebar">
  629. <div class="pp-logo">P</div>
  630. <div class="title-text">AI-心理学-演讲.pptx - PowerPoint</div>
  631. <div class="win-dots"><span></span><span></span><span></span></div>
  632. </div>
  633. <div class="ppt-toolbar">
  634. <div class="tool">
  635. <span class="ico"></span>
  636. <span class="font-name"><span id="fontName">Noto Serif SC</span><span style="opacity:0.5">▾</span></span>
  637. </div>
  638. <div class="divider"></div>
  639. <div class="tool"><span style="font-weight:700">B</span></div>
  640. <div class="tool" style="font-style:italic">I</div>
  641. <div class="tool" style="text-decoration:underline">U</div>
  642. <div class="divider"></div>
  643. <div class="tool active"><span class="ico" style="background:#D97757;border-color:#D97757"></span></div>
  644. </div>
  645. <div class="ppt-canvas">
  646. <div class="ppt-slide">
  647. <div class="ppt-eye">AI 心理学 · 第 3 节</div>
  648. <div class="ppt-title-frame" id="titleFrame">
  649. <span class="handle tl"></span>
  650. <span class="handle tr"></span>
  651. <span class="handle bl"></span>
  652. <span class="handle br"></span>
  653. <span class="ppt-title" id="titleText">心智的可塑性</span><span class="edit-caret" id="caret"></span>
  654. </div>
  655. <div class="ppt-sub">Agent 不是工具,它有自己的偏好。</div>
  656. <div class="ppt-hair"></div>
  657. </div>
  658. <!-- Cursor arrow -->
  659. <div class="cursor" id="cursor">
  660. <svg viewBox="0 0 22 30" fill="none">
  661. <path d="M2 2 L2 22 L8 17 L12 26 L16 24 L12 15 L20 14 Z"
  662. fill="#1A1918" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
  663. </svg>
  664. </div>
  665. <!-- Double-click ripple -->
  666. <div class="dblclick-ripple" id="ripple"></div>
  667. </div>
  668. </div>
  669. </div>
  670. </div>
  671. <!-- ====== Brand Reveal (米色面板 · hero-v10 signature) ====== -->
  672. <div class="brand-panel" id="brandPanel"></div>
  673. <div class="brand-reveal" id="brandReveal">
  674. <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
  675. <div class="brand-line" id="brandLine"></div>
  676. </div>
  677. </div>
  678. <script>
  679. (function() {
  680. // ---------- Fit stage ----------
  681. const stage = document.getElementById('stage');
  682. function rescale() {
  683. const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  684. stage.style.transform = `translate(-50%, -50%) scale(${s})`;
  685. }
  686. rescale();
  687. window.addEventListener('resize', rescale);
  688. // ---------- Easings ----------
  689. const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  690. const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
  691. const expoIn = t => (t <= 0) ? 0 : (t >= 1) ? 1 : Math.pow(2, 10 * (t - 1));
  692. const easeOut = t => 1 - Math.pow(1 - t, 3);
  693. const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
  694. function lerp(time, start, end, fromV, toV, ease) {
  695. if (time <= start) return fromV;
  696. if (time >= end) return toV;
  697. let p = (time - start) / (end - start);
  698. if (ease) p = ease(p);
  699. return fromV + (toV - fromV) * p;
  700. }
  701. function clampLerp(time, start, end) {
  702. if (time <= start) return 0;
  703. if (time >= end) return 1;
  704. return (time - start) / (end - start);
  705. }
  706. // ---------- Timeline (10s total) ----------
  707. const T = {
  708. DURATION: 10.0,
  709. // Beat 1: 0 - 2s
  710. deckIn: [0.15, 0.9], // browser fade+rise
  711. keyHintIn: [0.6, 1.1],
  712. keyPress: [1.25, 1.4], // arrow key highlight
  713. slideFlip: [1.3, 1.9], // slide A→B
  714. beat1Out: [2.0, 2.4],
  715. // Beat 2: split screen: 2.2 - 8.0s
  716. beat2In: [2.3, 2.9],
  717. labelsIn: [3.0, 3.5],
  718. cursorIn: [3.1, 3.4], // cursor arrives on right side
  719. cursorMove1: [3.4, 4.1], // cursor moves to title
  720. dblclick: [4.1, 4.3], // double click
  721. frameSelect: [4.15, 4.35], // frame shows handles
  722. frameEdit: [4.4, 4.55], // frame enters edit mode
  723. caretShowStart: 4.5,
  724. textDelete: [4.6, 5.4], // delete original text char by char
  725. textRetype: [5.5, 7.2], // type new text char by char
  726. commitEdit: [7.3, 7.5], // exit edit mode
  727. connectorIn: [3.3, 3.9],
  728. beat2Out: [8.0, 8.3], // main scene fades to 0 (0.3s)
  729. // Brand Reveal (米色面板 · hero-v10 signature): 8.3 - 10s
  730. // panelRise 与 beat2Out 微重叠 0.05s,避免黑屏间隙
  731. panelRise: [8.25, 8.7], // 米色面板 translateY 100%→0 (expoOut)
  732. wordmarkIn: [8.7, 9.3], // wordmark opacity 0→1 + translateY 20→0 + weight 100→500 (0.6s, expoOut)
  733. brandLineIn: [9.3, 9.7], // brand-line expand 0→280px (0.4s, cubicOut)
  734. brandHold: [9.7, 10.0], // hold (0.3s)
  735. };
  736. // ---------- Elements ----------
  737. const beat1 = document.getElementById('beat1');
  738. const beat2 = document.getElementById('beat2');
  739. const brandReveal = document.getElementById('brandReveal');
  740. const deckWindow = document.getElementById('deckWindow');
  741. const pageCount = document.getElementById('pageCount');
  742. const slideA = document.getElementById('slideA');
  743. const slideB = document.getElementById('slideB');
  744. const keyHint = document.getElementById('keyHint');
  745. const kbdKey = document.getElementById('kbdKey');
  746. const splitLeft = document.getElementById('splitLeft');
  747. const splitRight = document.getElementById('splitRight');
  748. const labelLeft = document.getElementById('labelLeft');
  749. const labelRight = document.getElementById('labelRight');
  750. const connector = document.getElementById('connector');
  751. const connectorLabel = document.getElementById('connectorLabel');
  752. const cursor = document.getElementById('cursor');
  753. const ripple = document.getElementById('ripple');
  754. const titleFrame = document.getElementById('titleFrame');
  755. const titleText = document.getElementById('titleText');
  756. const caret = document.getElementById('caret');
  757. const panel = document.getElementById('brandPanel');
  758. const wordmark = document.getElementById('wordmark');
  759. const brandLine = document.getElementById('brandLine');
  760. // Text to animate
  761. const ORIG_TEXT = '心智的可塑性';
  762. const NEW_TEXT = '心智 · 可塑性';
  763. // ---------- Render ----------
  764. function render(t) {
  765. /* ======= Beat 1 ======= */
  766. let beat1Op;
  767. if (t < T.beat1Out[0]) {
  768. beat1Op = lerp(t, T.deckIn[0], T.deckIn[1], 0, 1, expoOut);
  769. } else {
  770. beat1Op = 1 - clampLerp(t, T.beat1Out[0], T.beat1Out[1]);
  771. }
  772. beat1.style.opacity = beat1Op;
  773. beat1.style.visibility = beat1Op > 0.01 ? 'visible' : 'hidden';
  774. // Deck window rise
  775. const deckRise = lerp(t, T.deckIn[0], T.deckIn[1], 24, 0, expoOut);
  776. deckWindow.style.transform = `translate3d(0, ${deckRise}px, 0)`;
  777. // Key hint appear
  778. const khOp = clampLerp(t, T.keyHintIn[0], T.keyHintIn[1]);
  779. keyHint.style.opacity = khOp;
  780. // Key press flash
  781. const kpActive = t >= T.keyPress[0] && t < T.keyPress[1] + 0.2;
  782. if (kpActive) {
  783. const kp = clampLerp(t, T.keyPress[0], T.keyPress[1]);
  784. kbdKey.style.background = `rgba(217,119,87,${0.9 * (1 - kp * 0.4)})`;
  785. kbdKey.style.color = '#fff';
  786. kbdKey.style.transform = `scale(${1 - 0.08 * kp})`;
  787. } else {
  788. kbdKey.style.background = '';
  789. kbdKey.style.color = '';
  790. kbdKey.style.transform = '';
  791. }
  792. // Slide flip A→B
  793. if (t >= T.slideFlip[0] && t < T.slideFlip[1] + 0.2) {
  794. const sp = clampLerp(t, T.slideFlip[0], T.slideFlip[1]);
  795. const eased = expoOut(sp);
  796. slideA.style.opacity = 1 - eased;
  797. slideA.style.transform = `translateX(${-60 * eased}px)`;
  798. slideB.style.opacity = eased;
  799. slideB.style.transform = `translateX(${60 * (1 - eased)}px)`;
  800. // Update page count at midway
  801. if (sp > 0.5) pageCount.textContent = '4 / 12';
  802. else pageCount.textContent = '3 / 12';
  803. } else if (t >= T.slideFlip[1]) {
  804. slideA.style.opacity = 0;
  805. slideB.style.opacity = 1;
  806. slideB.style.transform = 'translateX(0)';
  807. pageCount.textContent = '4 / 12';
  808. } else {
  809. slideA.style.opacity = 1;
  810. slideA.style.transform = 'translateX(0)';
  811. slideB.style.opacity = 0;
  812. pageCount.textContent = '3 / 12';
  813. }
  814. /* ======= Beat 2 ======= */
  815. let beat2Op = 0;
  816. if (t >= T.beat2In[0] && t < T.beat2Out[1]) {
  817. if (t < T.beat2In[1]) beat2Op = clampLerp(t, T.beat2In[0], T.beat2In[1]);
  818. else if (t < T.beat2Out[0]) beat2Op = 1;
  819. else beat2Op = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
  820. }
  821. beat2.style.opacity = beat2Op;
  822. beat2.style.visibility = beat2Op > 0.01 ? 'visible' : 'hidden';
  823. // Windows rise in
  824. const splitInP = clampLerp(t, T.beat2In[0], T.beat2In[1]);
  825. const splitRise = lerp(t, T.beat2In[0], T.beat2In[1], 28, 0, expoOut);
  826. splitLeft.style.transform = `translate3d(${-8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
  827. splitRight.style.transform = `translate3d(${8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
  828. // Labels
  829. const labelOp = clampLerp(t, T.labelsIn[0], T.labelsIn[1]);
  830. labelLeft.style.opacity = labelOp * 0.7;
  831. labelRight.style.opacity = labelOp * 0.85;
  832. // Connector
  833. const connOp = clampLerp(t, T.connectorIn[0], T.connectorIn[1]);
  834. connector.style.opacity = connOp;
  835. connectorLabel.style.opacity = connOp * 0.9;
  836. /* === Cursor movement === */
  837. // Cursor positions (relative to .ppt-canvas, which is inside split-right)
  838. // Canvas starts at (0,0), size ~820 × 508 (580 - 32 - 40)
  839. // Title sits around x=84 y=110 (inside .ppt-slide padding 56/64)
  840. // We'll place cursor with absolute positioning inside .ppt-canvas.
  841. // Entry point: off to the right bottom of canvas
  842. const P_ENTER = { x: 720, y: 420 };
  843. const P_TITLE = { x: 250, y: 170 }; // on the title
  844. let cursorOp = 0;
  845. let cx = P_ENTER.x, cy = P_ENTER.y;
  846. if (t >= T.cursorIn[0] && t < T.beat2Out[0]) {
  847. cursorOp = 1;
  848. // Phase 1: appear (pop in with slight scale)
  849. const inP = clampLerp(t, T.cursorIn[0], T.cursorIn[1]);
  850. cursorOp = expoOut(inP);
  851. // Phase 2: move to title
  852. if (t >= T.cursorMove1[0]) {
  853. const mp = clampLerp(t, T.cursorMove1[0], T.cursorMove1[1]);
  854. const e = easeInOut(mp);
  855. cx = P_ENTER.x + (P_TITLE.x - P_ENTER.x) * e;
  856. cy = P_ENTER.y + (P_TITLE.y - P_ENTER.y) * e;
  857. } else {
  858. cx = P_ENTER.x;
  859. cy = P_ENTER.y;
  860. }
  861. // After double-click, slight jitter toward caret position during typing
  862. if (t >= T.textRetype[0] && t < T.textRetype[1]) {
  863. cx = P_TITLE.x + 6;
  864. cy = P_TITLE.y - 2;
  865. }
  866. } else if (t >= T.beat2Out[0]) {
  867. cursorOp = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
  868. }
  869. cursor.style.opacity = cursorOp;
  870. cursor.style.transform = `translate(${cx}px, ${cy}px)`;
  871. /* === Double-click ripple === */
  872. // Ripple pulses twice at T.dblclick start
  873. let rippleVisible = false;
  874. if (t >= T.dblclick[0] && t < T.dblclick[0] + 0.7) {
  875. const dt = t - T.dblclick[0];
  876. // Two rapid pulses
  877. const pulse1 = clamp(dt / 0.25, 0, 1);
  878. const pulse2 = clamp((dt - 0.15) / 0.25, 0, 1);
  879. const scale1 = 0.4 + pulse1 * 1.4;
  880. const scale2 = 0.4 + pulse2 * 1.4;
  881. const op1 = 1 - pulse1;
  882. const op2 = dt > 0.15 ? (1 - pulse2) : 0;
  883. // Render as single element: use larger of the two
  884. const scale = Math.max(scale1, scale2);
  885. const op = Math.max(op1, op2);
  886. ripple.style.opacity = op;
  887. ripple.style.transform = `translate(-50%, -50%) translate(${P_TITLE.x + 6}px, ${P_TITLE.y + 26}px) scale(${scale})`;
  888. rippleVisible = true;
  889. }
  890. if (!rippleVisible) ripple.style.opacity = 0;
  891. /* === Frame states: selected → editing === */
  892. titleFrame.classList.remove('selected', 'editing');
  893. if (t >= T.frameSelect[0] && t < T.frameEdit[0]) {
  894. titleFrame.classList.add('selected');
  895. } else if (t >= T.frameEdit[0] && t < T.commitEdit[1]) {
  896. titleFrame.classList.add('editing');
  897. }
  898. /* === Text animation: delete → retype === */
  899. let displayedText = ORIG_TEXT;
  900. let caretOp = 0;
  901. if (t < T.textDelete[0]) {
  902. displayedText = ORIG_TEXT;
  903. caretOp = t >= T.caretShowStart ? 1 : 0;
  904. } else if (t < T.textDelete[1]) {
  905. // Delete: remove chars from end
  906. const dp = clampLerp(t, T.textDelete[0], T.textDelete[1]);
  907. const charsToRemove = Math.floor(dp * ORIG_TEXT.length);
  908. displayedText = ORIG_TEXT.slice(0, ORIG_TEXT.length - charsToRemove);
  909. caretOp = 1;
  910. } else if (t < T.textRetype[0]) {
  911. displayedText = '';
  912. caretOp = 1;
  913. } else if (t < T.textRetype[1]) {
  914. // Retype new text
  915. const rp = clampLerp(t, T.textRetype[0], T.textRetype[1]);
  916. const charsToShow = Math.floor(rp * NEW_TEXT.length);
  917. displayedText = NEW_TEXT.slice(0, charsToShow);
  918. caretOp = 1;
  919. } else if (t < T.commitEdit[1]) {
  920. displayedText = NEW_TEXT;
  921. // Caret blinks while still in edit mode
  922. caretOp = (Math.floor(t * 2) % 2 === 0) ? 1 : 0.3;
  923. } else {
  924. displayedText = NEW_TEXT;
  925. caretOp = 0;
  926. }
  927. // Blinking during idle-in-edit phases (when not actively typing/deleting)
  928. if (t >= T.caretShowStart && t < T.textDelete[0]) {
  929. caretOp = (Math.floor((t - T.caretShowStart) * 3) % 2 === 0) ? 1 : 0.35;
  930. }
  931. titleText.textContent = displayedText;
  932. caret.style.opacity = caretOp;
  933. /* ======= Brand Reveal (米色面板 · hero-v10 signature) ======= */
  934. // Panel rises from bottom (米色面板 #F5F4F0)
  935. const panelP = clampLerp(t, T.panelRise[0], T.panelRise[1]);
  936. panel.style.transform = `translateY(${(1 - expoOut(panelP)) * 100}%)`;
  937. // brand-reveal container visible once panel starts rising
  938. brandReveal.style.opacity = panelP > 0.01 ? 1 : 0;
  939. // Wordmark: opacity 0→1 + translateY 20→0 + weight 100→500 (expoOut)
  940. const wmP = clampLerp(t, T.wordmarkIn[0], T.wordmarkIn[1]);
  941. const wmEased = expoOut(wmP);
  942. wordmark.style.opacity = wmEased;
  943. const wmRise = (1 - wmEased) * 20;
  944. wordmark.style.transform = `translate3d(0, ${wmRise}px, 0)`;
  945. const w = 100 + (500 - 100) * wmEased;
  946. wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
  947. wordmark.style.fontWeight = Math.round(w);
  948. // Brand line expand 0→280px (cubicOut)
  949. const lineP = clampLerp(t, T.brandLineIn[0], T.brandLineIn[1]);
  950. const cubicOut = x => 1 - Math.pow(1 - x, 3);
  951. brandLine.style.width = (280 * cubicOut(lineP)) + 'px';
  952. }
  953. // ---------- Driver ----------
  954. let manualT = null;
  955. let startMs = null;
  956. let hasFinished = false;
  957. function tick(now) {
  958. if (manualT != null) render(manualT);
  959. else {
  960. if (startMs == null) startMs = now;
  961. const elapsed = (now - startMs) / 1000;
  962. const recording = window.__recording === true;
  963. let t;
  964. if (recording) {
  965. t = Math.min(elapsed, T.DURATION - 0.001);
  966. if (elapsed >= T.DURATION) hasFinished = true;
  967. } else {
  968. t = elapsed % T.DURATION;
  969. }
  970. render(t);
  971. }
  972. requestAnimationFrame(tick);
  973. }
  974. // Force first-frame render synchronously, THEN set ready
  975. render(0);
  976. requestAnimationFrame(tick);
  977. window.__setTime = function(t) { manualT = t; render(t); };
  978. window.__resume = function() { manualT = null; startMs = null; };
  979. window.__duration = T.DURATION;
  980. window.__render = render;
  981. window.__ready = true;
  982. })();
  983. </script>
  984. </body>
  985. </html>