| 1 | import { escapeHtmlText } from './utils' |
| 2 | |
| 3 | export const ART_TEXT_TEMPLATES = [ |
| 4 | { id: 'gradient-shine', label: '01 流光溢彩', defaultText: 'LIGHT FLOW' }, |
| 5 | { id: 'neon-glow', label: '02 赛博霓虹', defaultText: 'NEON CYBER' }, |
| 6 | { id: 'shadow-3d', label: '03 硬核3D', defaultText: '3D DEPTH' }, |
| 7 | { id: 'wave-text', label: '04 波浪律动', defaultText: 'WAVE 动感' }, |
| 8 | { id: 'stroke-text', label: '05 科技描边', defaultText: 'HOLLOW OUT' }, |
| 9 | { id: 'typewriter', label: '06 经典打字机', defaultText: 'Typing...' }, |
| 10 | { id: 'glitch', label: '07 故障干扰', defaultText: 'GLITCH FX' }, |
| 11 | { id: 'fire-text', label: '08 烈焰灼烧', defaultText: 'BLAZING HOT' }, |
| 12 | { id: 'glass-text', label: '09 毛玻璃高级感', defaultText: 'Frosted Glass' }, |
| 13 | { id: 'rotate-3d', label: '10 立体旋转', defaultText: '3D ROTATE' }, |
| 14 | { id: 'rainbow', label: '11 彩虹变色龙', defaultText: 'RAINBOW' }, |
| 15 | { id: 'liquid', label: '12 液态幻彩', defaultText: 'LIQUID ART' }, |
| 16 | { id: 'bounce-text', label: '13 弹性跳跃', defaultText: 'BOUNCE!' }, |
| 17 | { id: 'float-shadow', label: '14 悬浮梦境', defaultText: 'FLOAT SOFTLY' }, |
| 18 | { id: 'metal-text', label: '15 金属质感', defaultText: 'METAL BRUSH' } |
| 19 | ] as const |
| 20 | |
| 21 | export type ArtTextTemplateId = (typeof ART_TEXT_TEMPLATES)[number]['id'] |
| 22 | |
| 23 | type ArtTextTemplate = (typeof ART_TEXT_TEMPLATES)[number] |
| 24 | |
| 25 | export type ArtTextLayout = { |
| 26 | blockId: string |
| 27 | left: number |
| 28 | top: number |
| 29 | width: number |
| 30 | minHeight: number |
| 31 | zIndex: number |
| 32 | text?: string |
| 33 | } |
| 34 | |
| 35 | const templateById = new Map<ArtTextTemplateId, ArtTextTemplate>( |
| 36 | ART_TEXT_TEMPLATES.map((template) => [template.id, template]) |
| 37 | ) |
| 38 | |
| 39 | export function isArtTextTemplateId(value: string | undefined): value is ArtTextTemplateId { |
| 40 | return Boolean(value && templateById.has(value as ArtTextTemplateId)) |
| 41 | } |
| 42 | |
| 43 | export function getArtTextTemplateLabel(value: string | undefined): string { |
| 44 | return isArtTextTemplateId(value) ? templateById.get(value)?.label || '' : '' |
| 45 | } |
| 46 | |
| 47 | function escapeCssAttribute(value: string): string { |
| 48 | return value |
| 49 | .replace(/\\/g, '\\\\') |
| 50 | .replace(/"/g, '\\"') |
| 51 | .replace(/\r?\n/g, ' ') |
| 52 | .replace(/</g, '\\3C ') |
| 53 | .replace(/>/g, '\\3E ') |
| 54 | } |
| 55 | |
| 56 | function styleAttr(styles: string[]): string { |
| 57 | return escapeHtmlText(styles.join('; ')) |
| 58 | } |
| 59 | |
| 60 | function buildBaseStyle(layout: ArtTextLayout): string { |
| 61 | return styleAttr([ |
| 62 | 'position:absolute', |
| 63 | `left:${layout.left}px`, |
| 64 | `top:${layout.top}px`, |
| 65 | `width:${layout.width}px`, |
| 66 | `min-height:${layout.minHeight}px`, |
| 67 | 'margin:0', |
| 68 | 'padding:0', |
| 69 | `z-index:${layout.zIndex}`, |
| 70 | 'box-sizing:border-box', |
| 71 | 'display:flex', |
| 72 | 'align-items:center', |
| 73 | 'justify-content:center', |
| 74 | 'overflow:visible', |
| 75 | 'text-align:center', |
| 76 | 'line-height:1.15', |
| 77 | 'letter-spacing:0', |
| 78 | 'white-space:normal', |
| 79 | 'overflow-wrap:anywhere', |
| 80 | 'font-family:inherit', |
| 81 | 'font-size:54px', |
| 82 | 'font-weight:800', |
| 83 | 'color:#34402c' |
| 84 | ]) |
| 85 | } |
| 86 | |
| 87 | function buildWaveText(text: string): string { |
| 88 | const chars = Array.from(text) |
| 89 | return chars |
| 90 | .map((char, index) => { |
| 91 | if (char === ' ') return '<span class="ppt-art-wave-space"> </span>' |
| 92 | return `<span class="ppt-art-wave-char" style="animation-delay:${index * 0.08}s">${escapeHtmlText(char)}</span>` |
| 93 | }) |
| 94 | .join('') |
| 95 | } |
| 96 | |
| 97 | function escapeStyleText(value: string): string { |
| 98 | return value.replace(/<\/style/gi, '<\\/style') |
| 99 | } |
| 100 | |
| 101 | function buildTextNode(template: ArtTextTemplate, text: string): string { |
| 102 | const safeText = escapeHtmlText(text) |
| 103 | |
| 104 | if (template.id === 'wave-text') { |
| 105 | return `<span class="ppt-art-text ppt-art-wave-line">${buildWaveText(text)}</span>` |
| 106 | } |
| 107 | |
| 108 | if (template.id === 'typewriter') { |
| 109 | return `<span class="ppt-art-typewriter-wrapper"><span class="ppt-art-text">${safeText}</span></span>` |
| 110 | } |
| 111 | |
| 112 | if (template.id === 'glitch') { |
| 113 | return `<span class="ppt-art-text" data-text="${safeText}">${safeText}</span>` |
| 114 | } |
| 115 | |
| 116 | return `<span class="ppt-art-text">${safeText}</span>` |
| 117 | } |
| 118 | |
| 119 | export function buildArtTextInnerHtml(templateId: ArtTextTemplateId, text: string): string { |
| 120 | const template = templateById.get(templateId) || ART_TEXT_TEMPLATES[0] |
| 121 | return buildTextNode(template, text || template.defaultText) |
| 122 | } |
| 123 | |
| 124 | function buildTemplateCss(templateId: ArtTextTemplateId, blockId: string): string { |
| 125 | const scope = `[data-block-id="${escapeCssAttribute(blockId)}"]` |
| 126 | const base = ` |
| 127 | ${scope} .ppt-art-text { |
| 128 | display: inline-block; |
| 129 | max-width: 100%; |
| 130 | box-sizing: border-box; |
| 131 | } |
| 132 | ${scope} .ppt-art-wave-line { |
| 133 | display: flex; |
| 134 | flex-wrap: wrap; |
| 135 | justify-content: center; |
| 136 | gap: 6px; |
| 137 | } |
| 138 | ${scope} .ppt-art-wave-char, |
| 139 | ${scope} .ppt-art-wave-space { |
| 140 | display: inline-block; |
| 141 | font-size: inherit; |
| 142 | font-weight: inherit; |
| 143 | } |
| 144 | ` |
| 145 | |
| 146 | switch (templateId) { |
| 147 | case 'gradient-shine': |
| 148 | return `${base} |
| 149 | ${scope} .ppt-art-text { |
| 150 | background: linear-gradient(135deg, #ffd89b, #c7e9fb, #f6d5f7, #a0e9ff); |
| 151 | background-size: 300% 300%; |
| 152 | background-clip: text; |
| 153 | -webkit-background-clip: text; |
| 154 | color: transparent; |
| 155 | animation: pptArtFlowGradient 6s ease infinite; |
| 156 | } |
| 157 | @keyframes pptArtFlowGradient { |
| 158 | 0% { background-position: 0% 50%; } |
| 159 | 50% { background-position: 100% 50%; } |
| 160 | 100% { background-position: 0% 50%; } |
| 161 | } |
| 162 | ` |
| 163 | case 'neon-glow': |
| 164 | return `${base} |
| 165 | ${scope} .ppt-art-text { |
| 166 | color: #0ff; |
| 167 | text-shadow: 0 0 5px #0ff, 0 0 10px #0ff, 0 0 20px #0ff, 0 0 40px #00aaff; |
| 168 | animation: pptArtNeonPulse 1.2s ease-in-out infinite alternate; |
| 169 | } |
| 170 | @keyframes pptArtNeonPulse { |
| 171 | 0% { text-shadow: 0 0 2px #0ff, 0 0 5px #0ff, 0 0 10px #0aa; opacity: 0.9; } |
| 172 | 100% { text-shadow: 0 0 8px #0ff, 0 0 20px #0ff, 0 0 35px #0cf; opacity: 1; } |
| 173 | } |
| 174 | ` |
| 175 | case 'shadow-3d': |
| 176 | return `${base} |
| 177 | ${scope} .ppt-art-text { |
| 178 | color: #f3b33d; |
| 179 | text-shadow: 1px 1px 0 #7a4c1a, 2px 2px 0 #6b4215, 3px 3px 0 #5a3710, 4px 4px 0 #482d0c, 5px 5px 0 #362208, 6px 6px 0 #241705; |
| 180 | } |
| 181 | ` |
| 182 | case 'wave-text': |
| 183 | return `${base} |
| 184 | ${scope} .ppt-art-text { |
| 185 | color: #34402c; |
| 186 | text-shadow: 0 2px 8px rgba(52, 64, 44, 0.18); |
| 187 | } |
| 188 | ${scope} .ppt-art-wave-char { |
| 189 | animation: pptArtWaveFloat 1.4s infinite ease-in-out; |
| 190 | } |
| 191 | @keyframes pptArtWaveFloat { |
| 192 | 0%, 100% { transform: translateY(0); } |
| 193 | 50% { transform: translateY(-10px); } |
| 194 | } |
| 195 | ` |
| 196 | case 'stroke-text': |
| 197 | return `${base} |
| 198 | ${scope} .ppt-art-text { |
| 199 | color: transparent; |
| 200 | -webkit-text-stroke: 2px #00e0ff; |
| 201 | text-stroke: 2px #00e0ff; |
| 202 | text-shadow: 0 0 12px rgba(0, 255, 255, 0.3); |
| 203 | } |
| 204 | ` |
| 205 | case 'typewriter': |
| 206 | return `${base} |
| 207 | ${scope} .ppt-art-typewriter-wrapper { |
| 208 | display: inline-block; |
| 209 | max-width: 100%; |
| 210 | overflow: hidden; |
| 211 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| 212 | } |
| 213 | ${scope} .ppt-art-text { |
| 214 | overflow: hidden; |
| 215 | white-space: nowrap; |
| 216 | border-right: 3px solid #f0f; |
| 217 | color: #34402c; |
| 218 | text-shadow: 0 2px 8px rgba(52, 64, 44, 0.18); |
| 219 | animation: pptArtTyping 3s steps(20, end) infinite, pptArtBlinkCursor 0.75s step-end infinite; |
| 220 | } |
| 221 | @keyframes pptArtTyping { |
| 222 | 0% { width: 0; } |
| 223 | 40% { width: 100%; } |
| 224 | 80% { width: 100%; } |
| 225 | 100% { width: 0; } |
| 226 | } |
| 227 | @keyframes pptArtBlinkCursor { |
| 228 | 0%, 100% { border-color: #f0f; } |
| 229 | 50% { border-color: transparent; } |
| 230 | } |
| 231 | ` |
| 232 | case 'glitch': |
| 233 | return `${base} |
| 234 | ${scope} .ppt-art-text { |
| 235 | position: relative; |
| 236 | color: #1f2633; |
| 237 | text-shadow: 0.05em 0 0 rgba(255, 0, 0, 0.75), -0.05em -0.025em 0 rgba(0, 180, 255, 0.75), 0 2px 8px rgba(31, 38, 51, 0.18); |
| 238 | animation: pptArtGlitchShake 0.3s infinite; |
| 239 | } |
| 240 | ${scope} .ppt-art-text::before, |
| 241 | ${scope} .ppt-art-text::after { |
| 242 | content: attr(data-text); |
| 243 | position: absolute; |
| 244 | top: 0; |
| 245 | left: 0; |
| 246 | width: 100%; |
| 247 | height: 100%; |
| 248 | background: transparent; |
| 249 | } |
| 250 | ${scope} .ppt-art-text::before { |
| 251 | left: 2px; |
| 252 | text-shadow: -2px 0 red; |
| 253 | clip: rect(24px, 550px, 44px, 0); |
| 254 | animation: pptArtGlitchClip 3s infinite linear alternate-reverse; |
| 255 | } |
| 256 | ${scope} .ppt-art-text::after { |
| 257 | left: -2px; |
| 258 | text-shadow: -2px 0 blue; |
| 259 | clip: rect(85px, 550px, 140px, 0); |
| 260 | animation: pptArtGlitchClip 2.5s infinite linear alternate-reverse; |
| 261 | } |
| 262 | @keyframes pptArtGlitchShake { |
| 263 | 0% { transform: translate(0); } |
| 264 | 20% { transform: translate(-1px, 1px); } |
| 265 | 40% { transform: translate(-1px, -1px); } |
| 266 | 60% { transform: translate(1px, 1px); } |
| 267 | 80% { transform: translate(1px, -1px); } |
| 268 | 100% { transform: translate(0); } |
| 269 | } |
| 270 | @keyframes pptArtGlitchClip { |
| 271 | 0% { clip: rect(31px, 9999px, 44px, 0); } |
| 272 | 50% { clip: rect(78px, 9999px, 112px, 0); } |
| 273 | 100% { clip: rect(12px, 9999px, 70px, 0); } |
| 274 | } |
| 275 | ` |
| 276 | case 'fire-text': |
| 277 | return `${base} |
| 278 | ${scope} .ppt-art-text { |
| 279 | background-image: linear-gradient(0deg, #ff4d00, #ff9900, #ffcc00); |
| 280 | background-clip: text; |
| 281 | -webkit-background-clip: text; |
| 282 | color: transparent; |
| 283 | text-shadow: 0 0 10px #ff7b00, 0 0 20px #ff4400; |
| 284 | animation: pptArtFireFlicker 0.2s infinite alternate; |
| 285 | } |
| 286 | @keyframes pptArtFireFlicker { |
| 287 | 0% { opacity: 0.95; text-shadow: 0 0 8px #ff5500; } |
| 288 | 100% { opacity: 1; text-shadow: 0 0 15px #ffaa33, 0 0 5px #ff3300; } |
| 289 | } |
| 290 | ` |
| 291 | case 'glass-text': |
| 292 | return `${base} |
| 293 | ${scope} .ppt-art-text { |
| 294 | background: rgba(31, 38, 51, 0.54); |
| 295 | backdrop-filter: blur(8px); |
| 296 | -webkit-backdrop-filter: blur(8px); |
| 297 | padding: 10px 20px; |
| 298 | border-radius: 60px; |
| 299 | color: #fff3cf; |
| 300 | font-weight: 700; |
| 301 | box-shadow: 0 6px 22px rgba(31, 38, 51, 0.22); |
| 302 | border: 1px solid rgba(31, 38, 51, 0.18); |
| 303 | } |
| 304 | ` |
| 305 | case 'rotate-3d': |
| 306 | return `${base} |
| 307 | ${scope} { |
| 308 | perspective: 800px; |
| 309 | } |
| 310 | ${scope} .ppt-art-text { |
| 311 | transform-style: preserve-3d; |
| 312 | animation: pptArtRotateText 5s infinite linear; |
| 313 | color: #fe9c8f; |
| 314 | text-shadow: 0 2px 8px rgba(31, 38, 51, 0.18); |
| 315 | } |
| 316 | @keyframes pptArtRotateText { |
| 317 | 0% { transform: rotateY(0deg); } |
| 318 | 100% { transform: rotateY(360deg); } |
| 319 | } |
| 320 | ` |
| 321 | case 'rainbow': |
| 322 | return `${base} |
| 323 | ${scope} .ppt-art-text { |
| 324 | animation: pptArtRainbowColor 2s linear infinite; |
| 325 | } |
| 326 | @keyframes pptArtRainbowColor { |
| 327 | 0% { color: #ff0000; } |
| 328 | 17% { color: #ff9900; } |
| 329 | 33% { color: #ffff00; } |
| 330 | 50% { color: #00ff00; } |
| 331 | 67% { color: #0099ff; } |
| 332 | 83% { color: #8b00ff; } |
| 333 | 100% { color: #ff0000; } |
| 334 | } |
| 335 | ` |
| 336 | case 'liquid': |
| 337 | return `${base} |
| 338 | ${scope} .ppt-art-text { |
| 339 | background: linear-gradient(45deg, #ff6bcb, #ffb86c, #2ed8ff, #ff6bcb); |
| 340 | background-size: 400% 400%; |
| 341 | background-clip: text; |
| 342 | -webkit-background-clip: text; |
| 343 | color: transparent; |
| 344 | animation: pptArtLiquidMove 5s ease infinite; |
| 345 | } |
| 346 | @keyframes pptArtLiquidMove { |
| 347 | 0% { background-position: 0% 50%; } |
| 348 | 50% { background-position: 100% 50%; } |
| 349 | 100% { background-position: 0% 50%; } |
| 350 | } |
| 351 | ` |
| 352 | case 'bounce-text': |
| 353 | return `${base} |
| 354 | ${scope} .ppt-art-text { |
| 355 | animation: pptArtTextBounce 1.2s ease infinite; |
| 356 | transform-origin: center; |
| 357 | color: #34402c; |
| 358 | text-shadow: 0 2px 8px rgba(52, 64, 44, 0.18); |
| 359 | } |
| 360 | @keyframes pptArtTextBounce { |
| 361 | 0%, 100% { transform: scale(1); letter-spacing: 0; } |
| 362 | 50% { transform: scale(1.08); letter-spacing: 4px; color: #b8860b; } |
| 363 | } |
| 364 | ` |
| 365 | case 'float-shadow': |
| 366 | return `${base} |
| 367 | ${scope} .ppt-art-text { |
| 368 | color: #34402c; |
| 369 | animation: pptArtFloatUp 2.6s infinite alternate; |
| 370 | text-shadow: 0 10px 15px rgba(52, 64, 44, 0.22); |
| 371 | } |
| 372 | @keyframes pptArtFloatUp { |
| 373 | 0% { transform: translateY(0); text-shadow: 0 5px 12px rgba(52, 64, 44, 0.18); } |
| 374 | 100% { transform: translateY(-8px); text-shadow: 0 20px 25px rgba(52, 64, 44, 0.34); } |
| 375 | } |
| 376 | ` |
| 377 | case 'metal-text': |
| 378 | return `${base} |
| 379 | ${scope} .ppt-art-text { |
| 380 | background: linear-gradient(135deg, #e8e8e8 0%, #a0a0b0 40%, #f5f5ff 50%, #b0b0c0 80%, #dfdfdf 100%); |
| 381 | background-clip: text; |
| 382 | -webkit-background-clip: text; |
| 383 | color: transparent; |
| 384 | filter: drop-shadow(0 1px 1px rgba(31, 38, 51, 0.42)) drop-shadow(0 3px 7px rgba(31, 38, 51, 0.18)); |
| 385 | } |
| 386 | ` |
| 387 | default: |
| 388 | return base |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | export function buildArtTextHtmlFragment( |
| 393 | templateId: ArtTextTemplateId, |
| 394 | layout: ArtTextLayout |
| 395 | ): string { |
| 396 | const template = templateById.get(templateId) || ART_TEXT_TEMPLATES[0] |
| 397 | const text = layout.text || template.defaultText |
| 398 | const baseStyle = buildBaseStyle(layout) |
| 399 | const css = escapeStyleText(buildTemplateCss(template.id, layout.blockId).trim()) |
| 400 | const content = buildArtTextInnerHtml(template.id, text) |
| 401 | |
| 402 | return [ |
| 403 | `<style data-ppt-art-text-style="${escapeHtmlText(layout.blockId)}">${css}</style>`, |
| 404 | `<div data-block-id="${escapeHtmlText(layout.blockId)}" data-ppt-art-text="${template.id}" style="${baseStyle};">${content}</div>` |
| 405 | ].join('') |
| 406 | } |
| 407 |