返回 oh-my-ppt
artTextTemplates.ts
根目录 / src / renderer / src / lib / artTextTemplates.ts
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">&nbsp;</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
407 lines TYPESCRIPT