1use rustc_hash::FxHashMap;
2use std::f32::consts::PI;
3use crate::color::Color;
4
5#[derive(Debug, Clone)]
6pub struct StyledSegment {
7 pub text: String,
8 pub styles: Vec<String>,
9}
10
11#[derive(Debug, Clone, Copy)]
12pub struct Transform {
13 pub x: f32,
14 pub y: f32,
15 pub scale_x: f32,
16 pub scale_y: f32,
17 pub rotation: f32,
18}
19
20impl Default for Transform {
21 fn default() -> Self {
22 Self {
23 x: 0.0,
24 y: 0.0,
25 scale_x: 1.0,
26 scale_y: 1.0,
27 rotation: 0.0
28 }
29 }
30}
31
32pub fn parse_text_lines(lines: Vec<String>) -> Result<Vec<Vec<StyledSegment>>, String> {
33 let mut result_lines: Vec<Vec<StyledSegment>> = Vec::new();
34 let mut style_stack: Vec<String> = Vec::new();
35
36 let mut in_style_def = false;
37 let mut escaped = false;
38
39 let mut text_buffer = String::new();
40 let mut style_buffer = String::new();
41
42 for line in lines {
43 let mut line_segments: Vec<StyledSegment> = Vec::new();
44
45 for c in line.chars() {
46 if escaped {
47 if in_style_def {
48 style_buffer.push(c);
49 } else {
50 text_buffer.push(c);
51 }
52 escaped = false;
53 continue;
54 }
55
56 match c {
57 '\\' => {
58 escaped = true;
59 }
60 '{' => {
61 if in_style_def {
62 style_buffer.push(c);
63 } else {
64 if !text_buffer.is_empty() {
65 line_segments.push(StyledSegment {
66 text: text_buffer.clone(),
67 styles: style_stack.clone(),
68 });
69 text_buffer.clear();
70 }
71 in_style_def = true;
72 }
73 }
74 '|' => {
75 if in_style_def {
76 style_stack.push(style_buffer.clone());
77 style_buffer.clear();
78 in_style_def = false;
79 } else {
80 text_buffer.push(c);
81 }
82 }
83 '}' => {
84 if in_style_def {
85 style_buffer.push(c);
86 } else {
87 if !text_buffer.is_empty() {
88 line_segments.push(StyledSegment {
89 text: text_buffer.clone(),
90 styles: style_stack.clone(),
91 });
92 text_buffer.clear();
93 }
94
95 if style_stack.pop().is_none() {
96 return Err(format!("Error: '}}' found with no open style on this line: {}", line));
97 }
98 }
99 }
100 ' ' => {
101 if in_style_def {
102 return Err(format!("Error: Whitespace not allowed in style definition on this line: {}", line));
103 } else {
104 text_buffer.push(c);
105 }
106 }
107 _ => {
108 if in_style_def {
109 style_buffer.push(c);
110 } else {
111 text_buffer.push(c);
112 }
113 }
114 }
115 }
116
117 if !text_buffer.is_empty() {
118 line_segments.push(StyledSegment {
119 text: text_buffer.clone(),
120 styles: style_stack.clone(),
121 });
122 text_buffer.clear();
123 }
124
125 result_lines.push(line_segments);
126 }
127
128 if in_style_def {
129 return Err("Error: Ended inside a style definition.".to_string());
130 }
131 if !style_stack.is_empty() {
132 return Err(format!("Error: Ended with {} unclosed styles.", style_stack.len()));
133 }
134
135 Ok(result_lines)
136}
137
138pub fn render_styled_text<F1, F2>(
140 segments: &[StyledSegment],
141 time: f64,
142 font_size: f32,
143 default_color: Color,
144 animation_tracker: &mut FxHashMap<String, (usize, f64)>,
145 total_char_index: &mut usize,
146 mut render_fn: F1,
147 mut render_shadow_fn: F2
148) where
149 F1: FnMut(&str, Transform, Color),
150 F2: FnMut(&str, Transform, Color)
151{
152 let named_colors: FxHashMap<String, Color> = [
153 ("white", (255.0, 255.0, 255.0)), ("black", (0.0, 0.0, 0.0)),
154 ("lightgray", (191.25, 191.25, 191.25)), ("darkgray", (94.35, 94.35, 94.35)),
155 ("red", (229.5, 0.0, 0.0)), ("orange", (255.0, 140.25, 0.0)),
156 ("yellow", (255.0, 214.2, 0.0)), ("lime", (0.0, 204.0, 0.0)),
157 ("green", (0.0, 127.5, 0.0)), ("cyan", (0.0, 204.0, 204.0)),
158 ("lightblue", (51.0, 153.0, 255.0)), ("blue", (0.0, 51.0, 204.0)),
159 ("purple", (114.75, 38.25, 196.35)), ("magenta", (204.0, 0.0, 204.0)),
160 ("brown", (137.7, 68.85, 17.85)), ("pink", (255.0, 102.0, 168.3)),
161 ].iter().map(|(k, (r,g,b))| (k.to_string(), Color::rgba(*r, *g, *b, 255.0))).collect();
162
163 let parse_float = |s: &str| s.parse::<f32>().unwrap_or(0.0);
164 let parse_color = |s: &str| -> Color {
165 if let Some(c) = named_colors.get(&s.to_lowercase()) { return *c; }
166 if s.starts_with('#') {
167 let hex = s.trim_start_matches('#');
168 if let Ok(val) = u32::from_str_radix(hex, 16) {
169 let r = ((val >> 16) & 0xFF) as f32;
170 let g = ((val >> 8) & 0xFF) as f32;
171 let b = (val & 0xFF) as f32;
172 return Color::rgba(r, g, b, 255.0);
173 }
174 }
175 if s.starts_with('(') && s.ends_with(')') {
176 let inner = &s[1..s.len()-1];
177 let parts: Vec<f32> = inner.split(',').map(|p| parse_float(p.trim())).collect();
178 if parts.len() >= 3 {
179 return Color::rgba(parts[0], parts[1], parts[2], 255.0);
180 }
181 }
182 Color::rgba(255.0, 255.0, 255.0, 255.0)
183 };
184
185 for segment in segments {
186 let mut has_effects = false;
187 for style_str in &segment.styles {
188 let mut parts = style_str.split('_');
189 let first_part = parts.next().unwrap_or("");
190 let (cmd, _) = if let Some(idx) = first_part.find('=') {
191 (&first_part[..idx], Some(&first_part[idx+1..]))
192 } else {
193 (first_part, None)
194 };
195 if cmd != "color" && cmd != "opacity" && !cmd.is_empty() {
196 has_effects = true;
197 break;
198 }
199 }
200
201 if !has_effects {
202 let mut color = default_color;
203 let mut opacity_mult = 1.0;
204
205 for style_str in &segment.styles {
206 let mut parts = style_str.split('_');
207 let first_part = parts.next().unwrap_or("");
208 let (cmd, first_arg_val) = if let Some(idx) = first_part.find('=') {
209 (&first_part[..idx], Some(&first_part[idx+1..]))
210 } else {
211 (first_part, None)
212 };
213
214 let val = if let Some(v) = first_arg_val {
215 v
216 } else {
217 parts.next().unwrap_or("")
218 };
219
220 if cmd == "color" {
221 color = parse_color(val);
222 } else if cmd == "opacity" {
223 opacity_mult *= parse_float(val);
224 }
225 }
226
227 color.a *= opacity_mult;
228 render_fn(&segment.text, Transform::default(), color);
229 *total_char_index += segment.text.chars().count();
230 continue;
231 }
232
233 for char_obj in segment.text.chars() {
234 let global_char_idx = *total_char_index as f32;
235
236 let mut tr = Transform::default();
237 let mut color = default_color;
238 let mut opacity_mult = 1.0;
239 let mut shadow_opts: Option<(Color, f32, f32, f32, f32)> = None;
240 let mut skip_render = false;
241 let mut render_char = char_obj.to_string();
242
243 for style_str in &segment.styles {
244 let mut parts = style_str.split('_');
245 let first_part = parts.next().unwrap_or("");
246
247 let (cmd, first_arg_val) = if let Some(idx) = first_part.find('=') {
248 (&first_part[..idx], Some(&first_part[idx+1..]))
249 } else {
250 (first_part, None)
251 };
252
253 let mut args: FxHashMap<&str, &str> = parts.map(|arg| {
254 let mut kv = arg.split('=');
255 (kv.next().unwrap_or(""), kv.next().unwrap_or(""))
256 }).collect();
257
258 if let Some(val) = first_arg_val {
259 args.insert("", val);
260 }
261
262 let get_f = |k: &str, def: f32| args.get(k).map(|v| parse_float(v)).unwrap_or(def);
263
264 if cmd == "hide" { skip_render = true; break; }
265
266 let anim_id = args.get("id").unwrap_or(&"");
267 if !anim_id.is_empty() {
268 let anim_key = anim_id.to_string();
269 let (start_index, start_time) = {
270 let entry = animation_tracker.entry(anim_key.clone()).or_insert((*total_char_index, time));
271 (entry.0, entry.1)
272 };
273 if *total_char_index < start_index {
274 animation_tracker.insert(anim_key, (*total_char_index, start_time));
275 }
276 let delay = get_f("delay", 0.0);
277
278 let elapsed = ((time - start_time) as f32 - delay).max(0.0);
279 let relative_idx = global_char_idx - start_index as f32;
280
281 let is_in = args.contains_key("in");
282 let is_out = args.contains_key("out");
283
284 if is_in || is_out {
285 match cmd {
286 "type" => {
287 let speed = get_f("speed", 8.0);
288 let chars_processed = elapsed * speed;
289 let cursor = args.get("cursor").unwrap_or(&"");
290 if is_in {
291 if relative_idx >= chars_processed {
292 if !cursor.is_empty() && relative_idx > 0.0 && (relative_idx - 1.0) < chars_processed {
293 render_char = cursor.to_string();
294 } else {
295 skip_render = true;
296 }
297 }
298 } else {
299 if relative_idx < chars_processed { skip_render = true; }
300 }
301 },
302 "fade" => {
303 let speed = get_f("speed", 3.0);
304 let trail = get_f("trail", 3.0);
305 let progress = (elapsed * speed - relative_idx) / trail;
306 let mut alpha = progress.clamp(0.0, 1.0);
307 if is_out { alpha = 1.0 - alpha; }
308 opacity_mult *= alpha;
309 },
310 "scale" => {
311 let speed = get_f("speed", 3.0);
312 let trail = get_f("trail", 3.0);
313 let progress = (elapsed * speed - relative_idx) / trail;
314 let mut s = progress.clamp(0.0, 1.0);
315 if is_out { s = 1.0 - s; }
316 tr.scale_x *= s;
317 tr.scale_y *= s;
318 }
319 _ => {}
320 }
321 } else {
322 panic!("Animation style '{}' requires either 'in' or 'out' argument.", cmd);
323 }
324 }
325
326 if skip_render { break; }
327
328 if cmd == "transform" {
329 if let Some(v) = args.get("translate") {
330 let nums: Vec<f32> = v.split(',').map(parse_float).collect();
331 tr.x += nums.get(0).unwrap_or(&0.0) * font_size;
332 tr.y += nums.get(1).unwrap_or(&0.0) * font_size;
333 }
334 if let Some(v) = args.get("scale") {
335 let nums: Vec<f32> = v.split(',').map(parse_float).collect();
336 tr.scale_x *= nums.get(0).unwrap_or(&1.0);
337 tr.scale_y *= nums.get(1).unwrap_or(nums.get(0).unwrap_or(&1.0));
338 }
339 tr.rotation += get_f("rotate", 0.0);
340 }
341
342 if cmd == "wave" {
343 let w = get_f("w", 3.0);
344 let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.5) };
345 let a = get_f("a", 0.3) * font_size;
346 let p = get_f("p", 0.0);
347 let r = get_f("r", 0.0);
348
349 let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
350 let disp = arg.cos() * a;
351
352 let rad = r.to_radians();
353 tr.x += -disp * rad.sin();
354 tr.y += disp * rad.cos();
355 }
356
357 if cmd == "pulse" {
358 let w = get_f("w", 2.0);
359 let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.6) };
360 let a = get_f("a", 0.15);
361 let p = get_f("p", 0.0);
362
363 let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
364 let scale_delta = 1.0 + arg.cos() * a;
365 tr.scale_x *= scale_delta;
366 tr.scale_y *= scale_delta;
367 }
368
369 if cmd == "swing" {
370 let w = get_f("w", 3.0);
371 let f = if args.contains_key("s") { get_f("s", 0.0) / w } else { get_f("f", 0.5) };
372 let a = get_f("a", 8.0);
373 let p = get_f("p", 0.0);
374
375 let arg = 2.0 * PI * (f * time as f32 + global_char_idx / w + p);
376 tr.rotation += arg.sin() * a;
377 }
378
379 if cmd == "jitter" {
380 let seed = (time as f32 * 20.0).floor() + global_char_idx * 13.37;
381 let rand_x = (seed.sin() * 43758.5453).fract();
382 let rand_y = ((seed + 7.1).cos() * 23421.632).fract();
383
384 let radii_str = args.get("radii").unwrap_or(&"0.1,0.1");
385 let rads: Vec<f32> = radii_str.split(',').map(parse_float).collect();
386 let rx = rads.get(0).unwrap_or(&0.5) * font_size;
387 let ry = rads.get(1).unwrap_or(rads.get(0).unwrap_or(&0.5)) * font_size;
388 let rot = get_f("rotation", 0.0).to_radians();
389
390 let jx = (rand_x - 0.5) * 2.0 * rx;
391 let jy = (rand_y - 0.5) * 2.0 * ry;
392
393 tr.x += jx * rot.cos() - jy * rot.sin();
394 tr.y += jx * rot.sin() + jy * rot.cos();
395 }
396
397 if cmd == "gradient" {
398 let speed = get_f("speed", 1.0);
399 let stops_str = args.get("stops").unwrap_or(&"0:#FF0000,1:#FF9A00,2:#D0DE21,3:#4FDC4A,4:#3FDAD8,5:#2FC9E2,6:#1C7FEE,7:#5F15F2,8:#BA0CF8,9:#FB07D9,10:#FF0000");
400
401 let stops: Vec<(f32, Color)> = stops_str.split(',').map(|pair| {
402 let mut kv = pair.split(':');
403 let pos = kv.next().unwrap_or("0").parse::<f32>().unwrap_or(0.0);
404 let col = parse_color(kv.next().unwrap_or("white"));
405 (pos, col)
406 }).collect();
407
408 if !stops.is_empty() {
409 let cycle_len = stops.last().unwrap().0;
410 let current_pos = (global_char_idx - time as f32 * speed).rem_euclid(cycle_len);
411
412 let mut c1 = stops[0].1;
413 let mut c2 = stops[0].1;
414 let mut t = 0.0;
415
416 for i in 0..stops.len()-1 {
417 if current_pos >= stops[i].0 && current_pos <= stops[i+1].0 {
418 c1 = stops[i].1;
419 c2 = stops[i+1].1;
420 let span = stops[i+1].0 - stops[i].0;
421 t = if span > 0.0 { (current_pos - stops[i].0) / span } else { 0.0 };
422 break;
423 }
424 }
425
426 if current_pos > stops.last().unwrap().0 {
427 c1 = stops.last().unwrap().1;
428 c2 = stops[0].1;
429 let span = cycle_len - stops.last().unwrap().0;
430 t = (current_pos - stops.last().unwrap().0) / span;
431 }
432
433 color.r = c1.r + (c2.r - c1.r) * t;
434 color.g = c1.g + (c2.g - c1.g) * t;
435 color.b = c1.b + (c2.b - c1.b) * t;
436 }
437 }
438
439 if cmd == "opacity" {
440 if let Some(v) = args.get("") {
441 opacity_mult *= parse_float(v);
442 }
443 }
444
445 if cmd == "color" {
446 if let Some(v) = args.get("") {
447 color = parse_color(v);
448 }
449 }
450
451 if cmd == "shadow" {
452 let color_str = args.get("color").unwrap_or(&"black");
453 let sc = parse_color(color_str);
454 let off_str = args.get("offset").unwrap_or(&"-0.3,0.3");
455 let offs: Vec<f32> = off_str.split(',').map(parse_float).collect();
456 let ox = offs.get(0).unwrap_or(&-0.3) * font_size;
457 let oy = offs.get(1).unwrap_or(&0.3) * font_size;
458
459 let scl_str = args.get("scale").unwrap_or(&"1");
460 let scls: Vec<f32> = scl_str.split(',').map(parse_float).collect();
461 let sx = *scls.get(0).unwrap_or(&1.0);
462 let sy = *scls.get(1).unwrap_or(&sx);
463
464 shadow_opts = Some((sc, ox, oy, sx, sy));
465 }
466 }
467
468 if !skip_render {
469 color.a *= opacity_mult;
470
471 if let Some((sc, ox, oy, ssx, ssy)) = shadow_opts {
472 let mut shadow_tr = tr;
473 shadow_tr.x += ox;
474 shadow_tr.y += oy;
475 shadow_tr.scale_x *= ssx;
476 shadow_tr.scale_y *= ssy;
477
478 let shadow_final_color = Color::rgba(sc.r, sc.g, sc.b, sc.a * opacity_mult);
479 render_shadow_fn(&render_char, shadow_tr, shadow_final_color);
480 }
481
482 render_fn(&render_char, tr, color);
483 }
484 *total_char_index += 1;
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 const WHITE: Color = Color { r: 255.0, g: 255.0, b: 255.0, a: 255.0 };
494
495 #[test]
496 fn test_render_simple_text() {
497 let lines = vec!["Hello".to_string()];
498 let segments = parse_text_lines(lines).unwrap();
499 let mut tracker = FxHashMap::default();
500 let mut rendered = Vec::new();
501
502 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
503 |c, tr, col| rendered.push((c.to_string(), tr, col)),
504 |_, _, _| {});
505
506 assert_eq!(rendered.len(), 1, "Rendered length should be 1 for 'Hello' (optimized)");
507 assert_eq!(rendered[0].0, "Hello", "First text should be 'Hello'");
508 assert_eq!(rendered[0].1.scale_x, 1.0, "Default scale_x should be 1.0");
509 assert_eq!(rendered[0].1.scale_y, 1.0, "Default scale_y should be 1.0");
510 }
511
512 #[test]
513 fn test_render_color_named() {
514 let lines = vec!["{color=red|R}".to_string()];
515 let segments = parse_text_lines(lines).unwrap();
516 let mut tracker = FxHashMap::default();
517 let mut rendered = Vec::new();
518
519 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
520 |c, tr, col| rendered.push((c.to_string(), tr, col)),
521 |_, _, _| {});
522
523 assert_eq!(rendered[0].0, "R", "Char should be 'R'");
524 assert!((rendered[0].2.r - 230.0).abs() < 1.0, "Named color red r value wrong? {:?}", rendered);
525 assert!(rendered[0].2.g < 1.0, "Named color red g value wrong? {:?}", rendered);
526 assert!(rendered[0].2.b < 1.0, "Named color red b value wrong? {:?}", rendered);
527 }
528
529 #[test]
530 fn test_render_color_hex() {
531 let lines = vec!["{color=#FF0000|R}".to_string()];
532 let segments = parse_text_lines(lines).unwrap();
533 let mut tracker = FxHashMap::default();
534 let mut rendered = Vec::new();
535
536 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
537 |c, tr, col| rendered.push((c.to_string(), tr, col)),
538 |_, _, _| {});
539
540 assert!((rendered[0].2.r - 255.0).abs() < 1.0, "Hex color r value wrong? {:?}", rendered);
541 assert!(rendered[0].2.g < 1.0, "Hex color g value wrong? {:?}", rendered);
542 assert!(rendered[0].2.b < 1.0, "Hex color b value wrong? {:?}", rendered);
543 }
544
545 #[test]
546 fn test_render_color_rgb() {
547 let lines = vec!["{color=(255,128,0)|O}".to_string()];
548 let segments = parse_text_lines(lines).unwrap();
549 let mut tracker = FxHashMap::default();
550 let mut rendered = Vec::new();
551
552 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
553 |c, tr, col| rendered.push((c.to_string(), tr, col)),
554 |_, _, _| {});
555
556 assert!((rendered[0].2.r - 255.0).abs() < 1.0, "RGB color r value wrong? {:?}", rendered);
557 assert!((rendered[0].2.g - 128.0).abs() < 1.0, "RGB color g value wrong? {:?}", rendered);
558 assert!(rendered[0].2.b < 1.0, "RGB color b value wrong? {:?}", rendered);
559 }
560
561 #[test]
562 fn test_render_opacity() {
563 let lines = vec!["{opacity=0.5|A}".to_string()];
564 let segments = parse_text_lines(lines).unwrap();
565 let mut tracker = FxHashMap::default();
566 let mut rendered = Vec::new();
567
568 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
569 |c, tr, col| rendered.push((c.to_string(), tr, col)),
570 |_, _, _| {});
571
572 assert!((rendered[0].2.a - 127.5).abs() < 1.0, "Opacity value wrong? {:?}", rendered);
573 }
574
575 #[test]
576 fn test_render_transform_translate() {
577 let lines = vec!["{transform_translate=0.5,0.5|A}".to_string()];
578 let segments = parse_text_lines(lines).unwrap();
579 let mut tracker = FxHashMap::default();
580 let mut rendered = Vec::new();
581
582 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
583 |c, tr, col| rendered.push((c.to_string(), tr, col)),
584 |_, _, _| {});
585
586 assert!((rendered[0].1.x - 8.0).abs() < 0.01, "Translate x wrong? {:?}", rendered); assert!((rendered[0].1.y - 8.0).abs() < 0.01, "Translate y wrong? {:?}", rendered);
588 }
589
590 #[test]
591 fn test_render_transform_scale() {
592 let lines = vec!["{transform_scale=2.0|A}".to_string()];
593 let segments = parse_text_lines(lines).unwrap();
594 let mut tracker = FxHashMap::default();
595 let mut rendered = Vec::new();
596
597 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
598 |c, tr, col| rendered.push((c.to_string(), tr, col)),
599 |_, _, _| {});
600
601 assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Scale x wrong? {:?}", rendered);
602 assert!((rendered[0].1.scale_y - 2.0).abs() < 0.01, "Scale y wrong? {:?}", rendered);
603 }
604
605 #[test]
606 fn test_render_transform_scale_xy() {
607 let lines = vec!["{transform_scale=2.0,0.5|A}".to_string()];
608 let segments = parse_text_lines(lines).unwrap();
609 let mut tracker = FxHashMap::default();
610 let mut rendered = Vec::new();
611
612 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
613 |c, tr, col| rendered.push((c.to_string(), tr, col)),
614 |_, _, _| {});
615
616 assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Scale x wrong? {:?}", rendered);
617 assert!((rendered[0].1.scale_y - 0.5).abs() < 0.01, "Scale y wrong? {:?}", rendered);
618 }
619
620 #[test]
621 fn test_render_transform_rotate() {
622 let lines = vec!["{transform_rotate=45|A}".to_string()];
623 let segments = parse_text_lines(lines).unwrap();
624 let mut tracker = FxHashMap::default();
625 let mut rendered = Vec::new();
626
627 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
628 |c, tr, col| rendered.push((c.to_string(), tr, col)),
629 |_, _, _| {});
630
631 assert!((rendered[0].1.rotation - 45.0).abs() < 0.01, "Rotate value wrong? {:?}", rendered);
632 }
633
634 #[test]
635 fn test_render_wave_effect() {
636 let lines = vec!["{wave|ABC}".to_string()];
637 let segments = parse_text_lines(lines).unwrap();
638 let mut tracker = FxHashMap::default();
639 let mut rendered = Vec::new();
640
641 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
642 |c, tr, col| rendered.push((c.to_string(), tr, col)),
643 |_, _, _| {});
644
645 assert_eq!(rendered.len(), 3, "Wave effect rendered length wrong? {:?}", rendered);
646 assert_ne!(rendered[0].1.y, rendered[1].1.y, "Wave effect Y position not different? {:?}", rendered);
647 }
648
649 #[test]
650 fn test_render_wave_with_params() {
651 let lines = vec!["{wave_w=2.0_f=1.0_a=0.5|AB}".to_string()];
652 let segments = parse_text_lines(lines).unwrap();
653 let mut tracker = FxHashMap::default();
654 let mut rendered = Vec::new();
655
656 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
657 |c, tr, col| rendered.push((c.to_string(), tr, col)),
658 |_, _, _| {});
659
660 assert_eq!(rendered.len(), 2, "Wave effect rendered length wrong? {:?}", rendered);
661 assert!(rendered[0].1.y.abs() <= 8.0, "Wave effect Y position amplitude wrong? {:?}", rendered);
662 }
663
664 #[test]
665 fn test_render_pulse_effect() {
666 let lines = vec!["{pulse|ABC}".to_string()];
667 let segments = parse_text_lines(lines).unwrap();
668 let mut tracker = FxHashMap::default();
669 let mut rendered = Vec::new();
670
671 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
672 |c, tr, col| rendered.push((c.to_string(), tr, col)),
673 |_, _, _| {});
674
675 assert_eq!(rendered.len(), 3, "Pulse effect rendered length wrong? {:?}", rendered);
676 assert_ne!(rendered[0].1.scale_x, rendered[1].1.scale_x, "Pulse effect scale not different? {:?}", rendered);
677 }
678
679 #[test]
680 fn test_render_swing_effect() {
681 let lines = vec!["{swing|ABC}".to_string()];
682 let segments = parse_text_lines(lines).unwrap();
683 let mut tracker = FxHashMap::default();
684 let mut rendered = Vec::new();
685
686 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
687 |c, tr, col| rendered.push((c.to_string(), tr, col)),
688 |_, _, _| {});
689
690 assert_eq!(rendered.len(), 3, "Swing effect rendered length wrong? {:?}", rendered);
691 assert_ne!(rendered[0].1.rotation, rendered[1].1.rotation, "Swing effect rotation not different? {:?}", rendered);
692 }
693
694 #[test]
695 fn test_render_jitter_effect() {
696 let lines = vec!["{jitter_radii=0.1,0.1|A}".to_string()];
697 let segments = parse_text_lines(lines).unwrap();
698 let mut tracker = FxHashMap::default();
699 let mut rendered_t1 = Vec::new();
700 let mut rendered_t2 = Vec::new();
701
702 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
703 |c, tr, col| rendered_t1.push((c.to_string(), tr, col)),
704 |_, _, _| {});
705
706 render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
707 |c, tr, col| rendered_t2.push((c.to_string(), tr, col)),
708 |_, _, _| {});
709
710 assert_ne!(rendered_t1[0].1.x, rendered_t2[0].1.x, "Jitter effect X position not different? {:?} {:?}", rendered_t1, rendered_t2);
711 }
712
713 #[test]
714 fn test_render_gradient_effect() {
715 let lines = vec!["{gradient_stops=0:#FF0000,3:#0000FF|ABC}".to_string()];
716 let segments = parse_text_lines(lines).unwrap();
717 let mut tracker = FxHashMap::default();
718 let mut rendered = Vec::new();
719
720 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
721 |c, tr, col| rendered.push((c.to_string(), tr, col)),
722 |_, _, _| {});
723
724 assert_eq!(rendered.len(), 3, "Gradient effect rendered length wrong? {:?}", rendered);
725 assert!(rendered[0].2.r > 0.5, "Gradient effect first char color not correct? {:?}", rendered);
726 assert!(rendered[2].2.b > rendered[0].2.b, "Gradient effect color not correct? {:?}", rendered);
727 }
728
729 #[test]
730 fn test_render_hide_effect() {
731 let lines = vec!["{hide|ABC}".to_string()];
732 let segments = parse_text_lines(lines).unwrap();
733 let mut tracker = FxHashMap::default();
734 let mut rendered = Vec::new();
735
736 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
737 |c, tr, col| rendered.push((c.to_string(), tr, col)),
738 |_, _, _| {});
739
740 assert_eq!(rendered.len(), 0, "Hide effect rendered length wrong? {:?}", rendered);
741 }
742
743 #[test]
744 fn test_render_shadow_effect() {
745 let lines = vec!["{shadow|A}".to_string()];
746 let segments = parse_text_lines(lines).unwrap();
747 let mut tracker = FxHashMap::default();
748 let mut rendered = Vec::new();
749 let mut shadows = Vec::new();
750
751 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
752 |c, tr, col| rendered.push((c.to_string(), tr, col)),
753 |c, tr, col| shadows.push((c.to_string(), tr, col)));
754
755 assert_eq!(rendered.len(), 1, "Shadow effect rendered length wrong? {:?}", rendered);
756 assert_eq!(shadows.len(), 1, "Shadow effect shadows length wrong? {:?}", shadows);
757 assert_eq!(shadows[0].0, "A", "Shadow effect char wrong? {:?}", shadows);
758 assert!(shadows[0].2.r < 0.1, "Shadow effect color r value wrong? {:?}", shadows);
759 }
760
761 #[test]
762 fn test_render_shadow_with_color() {
763 let lines = vec!["{shadow_color=red|A}".to_string()];
764 let segments = parse_text_lines(lines).unwrap();
765 let mut tracker = FxHashMap::default();
766 let mut shadows = Vec::new();
767
768 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
769 |_, _, _| {},
770 |c, tr, col| shadows.push((c.to_string(), tr, col)));
771
772 assert!(shadows[0].2.r > 0.5, "Shadow color r value wrong? {:?}", shadows);
773 }
774
775 #[test]
776 fn test_render_shadow_offset() {
777 let lines = vec!["{shadow_offset=0.5,0.5|A}".to_string()];
778 let segments = parse_text_lines(lines).unwrap();
779 let mut tracker = FxHashMap::default();
780 let mut rendered = Vec::new();
781 let mut shadows = Vec::new();
782
783 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
784 |c, tr, col| rendered.push((c.to_string(), tr, col)),
785 |c, tr, col| shadows.push((c.to_string(), tr, col)));
786
787 assert!((shadows[0].1.x - 8.0).abs() < 0.01, "Shadow offset x wrong? {:?}", shadows);
788 assert!((shadows[0].1.y - 8.0).abs() < 0.01, "Shadow offset y wrong? {:?}", shadows);
789 }
790
791 #[test]
792 fn test_render_type_animation() {
793 let lines = vec!["{type_in_id=t1_cursor=\\||ABC}".to_string()];
794 let segments = parse_text_lines(lines).unwrap();
795 let mut tracker = FxHashMap::default();
796 let mut rendered = Vec::new();
797
798 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
799 |c, tr, col| rendered.push((c.to_string(), tr, col)),
800 |_, _, _| {});
801
802 assert_eq!(rendered.len(), 0, "Type animation at time 0 should show nothing");
803
804 rendered.clear();
805 render_styled_text(&segments[0], 0.1, 16.0, WHITE, &mut tracker, &mut 0,
806 |c, tr, col| rendered.push((c.to_string(), tr, col)),
807 |_, _, _| {});
808
809 assert!(rendered.len() > 0, "Type animation after time should show chars");
810
811 assert!(rendered[rendered.len()-1].0 == "|", "Type animation cursor should be present? {:?}", rendered);
812 }
813
814 #[test]
815 fn test_render_fade_animation() {
816 let lines = vec!["{fade_in_id=f1|A}".to_string()];
817 let segments = parse_text_lines(lines).unwrap();
818 let mut tracker = FxHashMap::default();
819 let mut rendered = Vec::new();
820
821 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
822 |c, tr, col| rendered.push((c.to_string(), tr, col)),
823 |_, _, _| {});
824
825 assert!(rendered.len() > 0, "Fade animation should render something");
826 assert!(rendered[0].2.a < 0.1, "Fade animation alpha at time 0 should be low? {:?}", rendered);
827
828 rendered.clear();
829 render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
830 |c, tr, col| rendered.push((c.to_string(), tr, col)),
831 |_, _, _| {});
832
833 assert!(rendered[0].2.a > 0.9, "Fade animation alpha after time should be high? {:?}", rendered);
834 }
835
836 #[test]
837 fn test_render_scale_animation() {
838 let lines = vec!["{scale_in_id=s1|A}".to_string()];
839 let segments = parse_text_lines(lines).unwrap();
840 let mut tracker = FxHashMap::default();
841 let mut rendered = Vec::new();
842
843 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
844 |c, tr, col| rendered.push((c.to_string(), tr, col)),
845 |_, _, _| {});
846
847 assert!(rendered[0].1.scale_x < 0.1, "Scale animation scale_x at time 0 should be small? {:?}", rendered);
848
849 rendered.clear();
850 render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
851 |c, tr, col| rendered.push((c.to_string(), tr, col)),
852 |_, _, _| {});
853
854 assert!(rendered[0].1.scale_x > 0.9, "Scale animation scale_x after time should be large? {:?}", rendered);
855 }
856
857 #[test]
858 fn test_render_nested_wave_pulse() {
859 let lines = vec!["{wave|{pulse|ABC}}".to_string()];
860 let segments = parse_text_lines(lines).unwrap();
861 let mut tracker = FxHashMap::default();
862 let mut rendered = Vec::new();
863
864 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
865 |c, tr, col| rendered.push((c.to_string(), tr, col)),
866 |_, _, _| {});
867
868 assert_eq!(rendered.len(), 3, "Nested wave/pulse rendered length wrong? {:?}", rendered);
869 assert_ne!(rendered[0].1.y, 0.0, "Wave effect y not applied? {:?}", rendered);
870 assert_ne!(rendered[0].1.scale_x, 1.0, "Pulse effect scale_x not applied? {:?}", rendered);
871 }
872
873 #[test]
874 fn test_render_nested_color_wave() {
875 let lines = vec!["{color=red|{wave|ABC}}".to_string()];
876 let segments = parse_text_lines(lines).unwrap();
877 let mut tracker = FxHashMap::default();
878 let mut rendered = Vec::new();
879
880 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
881 |c, tr, col| rendered.push((c.to_string(), tr, col)),
882 |_, _, _| {});
883
884 assert_eq!(rendered.len(), 3, "Nested color/wave rendered length wrong? {:?}", rendered);
885 assert!(rendered[0].2.r > 0.5, "Nested color effect r value wrong? {:?}", rendered);
886 assert!(rendered[1].2.r > 0.5, "Nested color effect r value wrong? {:?}", rendered);
887 assert_ne!(rendered[0].1.y, rendered[1].1.y, "Nested wave effect y not different? {:?}", rendered);
888 }
889
890 #[test]
891 fn test_render_multiple_same_effect_nested() {
892 let lines = vec!["{wave|A{wave|B}C}".to_string()];
893 let segments = parse_text_lines(lines).unwrap();
894 let mut tracker = FxHashMap::default();
895 let mut rendered = Vec::new();
896
897 render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
898 |c, tr, col| rendered.push((c.to_string(), tr, col)),
899 |_, _, _| {});
900
901 assert_eq!(rendered.len(), 3, "Multiple nested wave rendered length wrong? {:?}", rendered);
902 let b_offset = rendered[1].1.y;
903 let a_offset = rendered[0].1.y;
904 assert_ne!(b_offset, a_offset, "Nested wave offsets not different? {:?}", rendered);
905 }
906
907 #[test]
908 fn test_render_gradient_over_time() {
909 let lines = vec!["{gradient_speed=10|AB}".to_string()];
910 let segments = parse_text_lines(lines).unwrap();
911 let mut tracker = FxHashMap::default();
912 let mut rendered_t1 = Vec::new();
913 let mut rendered_t2 = Vec::new();
914
915 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
916 |c, tr, col| rendered_t1.push((c.to_string(), tr, col)),
917 |_, _, _| {});
918
919 render_styled_text(&segments[0], 0.1, 16.0, WHITE, &mut tracker, &mut 0,
920 |c, tr, col| rendered_t2.push((c.to_string(), tr, col)),
921 |_, _, _| {});
922
923 assert_ne!(rendered_t1[0].2.r, rendered_t2[0].2.r, "Gradient color r value should change over time");
924 }
925
926 #[test]
927 fn test_render_all_effects_combined() {
928 let lines = vec!["{wave|{pulse|{swing|{color=cyan|ABC}}}}".to_string()];
929 let segments = parse_text_lines(lines).unwrap();
930 let mut tracker = FxHashMap::default();
931 let mut rendered = Vec::new();
932
933 render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
934 |c, tr, col| rendered.push((c.to_string(), tr, col)),
935 |_, _, _| {});
936
937 assert_eq!(rendered.len(), 3, "All effects combined rendered length wrong? {:?}", rendered);
938 assert_ne!(rendered[0].1.y, 0.0, "Wave effect y not applied? {:?}", rendered);
939 assert_ne!(rendered[0].1.scale_x, 1.0, "Pulse effect scale_x not applied? {:?}", rendered);
940 assert_ne!(rendered[0].1.rotation, 0.0, "Swing effect rotation not applied? {:?}", rendered);
941 assert!(rendered[0].2.g > 0.5 && rendered[0].2.b > 0.5, "Cyan color effect not applied? {:?}", rendered);
942 }
943
944 #[test]
945 fn test_render_color_overwrite_nested() {
946 let lines = vec!["{color=red|A{color=blue|B}C}".to_string()];
947 let segments = parse_text_lines(lines).unwrap();
948 let mut tracker = FxHashMap::default();
949 let mut rendered = Vec::new();
950
951 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
952 |c, tr, col| rendered.push((c.to_string(), tr, col)),
953 |_, _, _| {});
954
955 assert_eq!(rendered.len(), 3, "Color overwrite nested rendered length wrong? {:?}", rendered);
956 assert!(rendered[0].2.r > 0.5 && rendered[0].2.b < 0.5, "Outer color red not applied to A? {:?}", rendered);
957 assert!(rendered[1].2.b > 0.5 && rendered[1].2.r < 0.5, "Inner color blue not applied to B? {:?}", rendered);
958 assert!(rendered[2].2.r > 0.5 && rendered[2].2.b < 0.5, "Outer color red not applied to C? {:?}", rendered);
959 }
960
961 #[test]
962 fn test_render_transform_accumulation() {
963 let lines = vec!["{transform_translate=0.5,0|{transform_translate=0,0.5|A}}".to_string()];
964 let segments = parse_text_lines(lines).unwrap();
965 let mut tracker = FxHashMap::default();
966 let mut rendered = Vec::new();
967
968 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
969 |c, tr, col| rendered.push((c.to_string(), tr, col)),
970 |_, _, _| {});
971
972 assert!((rendered[0].1.x - 8.0).abs() < 0.01, "Transform accumulation x wrong? {:?}", rendered);
973 assert!((rendered[0].1.y - 8.0).abs() < 0.01, "Transform accumulation y wrong? {:?}", rendered);
974 }
975
976 #[test]
977 fn test_render_opacity_accumulation() {
978 let lines = vec!["{opacity=0.5|{opacity=0.5|A}}".to_string()];
979 let segments = parse_text_lines(lines).unwrap();
980 let mut tracker = FxHashMap::default();
981 let mut rendered = Vec::new();
982
983 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
984 |c, tr, col| rendered.push((c.to_string(), tr, col)),
985 |_, _, _| {});
986
987 assert!((rendered[0].2.a - 63.75).abs() < 1.0, "Opacity accumulation wrong? {:?}", rendered);
988 }
989
990 #[test]
991 fn test_render_shadow_with_transform() {
992 let lines = vec!["{transform_scale=2|{shadow|A}}".to_string()];
993 let segments = parse_text_lines(lines).unwrap();
994 let mut tracker = FxHashMap::default();
995 let mut rendered = Vec::new();
996 let mut shadows = Vec::new();
997
998 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
999 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1000 |c, tr, col| shadows.push((c.to_string(), tr, col)));
1001
1002 assert!((rendered[0].1.scale_x - 2.0).abs() < 0.01, "Shadow with transform scale_x wrong? {:?}", rendered);
1003 assert!((shadows[0].1.scale_x - 2.0).abs() < 0.01, "Shadow with transform scale_x wrong? {:?}", shadows);
1004 }
1005
1006 #[test]
1007 fn test_render_empty_text() {
1008 let lines = vec!["".to_string()];
1009 let segments = parse_text_lines(lines).unwrap();
1010 let mut tracker = FxHashMap::default();
1011 let mut rendered = Vec::new();
1012
1013 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1014 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1015 |_, _, _| {});
1016
1017 assert_eq!(rendered.len(), 0, "Empty text rendered length wrong? {:?}", rendered);
1018 }
1019
1020 #[test]
1021 fn test_render_unicode_with_effects() {
1022 let lines = vec!["{wave|你好🌍}".to_string()];
1023 let segments = parse_text_lines(lines).unwrap();
1024 let mut tracker = FxHashMap::default();
1025 let mut rendered = Vec::new();
1026
1027 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1028 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1029 |_, _, _| {});
1030
1031 assert_eq!(rendered.len(), 3, "Unicode with effects rendered length wrong? {:?}", rendered);
1032 assert_eq!(rendered[0].0, "你", "First unicode char wrong? {:?}", rendered);
1033 assert_eq!(rendered[1].0, "好", "Second unicode char wrong? {:?}", rendered);
1034 assert_eq!(rendered[2].0, "🌍", "Third unicode char wrong? {:?}", rendered);
1035 }
1036
1037 #[test]
1038 fn test_render_type_out_animation() {
1039 let lines = vec!["{type_out_id=t2|ABC}".to_string()];
1040 let segments = parse_text_lines(lines).unwrap();
1041 let mut tracker = FxHashMap::default();
1042 let mut rendered = Vec::new();
1043
1044 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1045 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1046 |_, _, _| {});
1047
1048 assert_eq!(rendered.len(), 3, "Type out animation at time 0 should show all chars");
1049
1050 rendered.clear();
1051 render_styled_text(&segments[0], 0.5, 16.0, WHITE, &mut tracker, &mut 0,
1052 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1053 |_, _, _| {});
1054
1055 assert_eq!(rendered.len(), 0, "Type out animation after time should hide chars");
1056 }
1057
1058 #[test]
1059 fn test_render_fade_out_animation() {
1060 let lines = vec!["{fade_out_id=f2|A}".to_string()];
1061 let segments = parse_text_lines(lines).unwrap();
1062 let mut tracker = FxHashMap::default();
1063 let mut rendered = Vec::new();
1064
1065 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1066 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1067 |_, _, _| {});
1068
1069 assert!(rendered.len() > 0);
1070 assert!(rendered[0].2.a > 0.9, "Fade out animation alpha at time 0 should be high? {:?}", rendered);
1071
1072 rendered.clear();
1073 render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
1074 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1075 |_, _, _| {});
1076
1077 assert!(rendered[0].2.a < 0.1, "Fade out animation alpha after time should be low? {:?}", rendered);
1078 }
1079
1080 #[test]
1081 fn test_render_scale_out_animation() {
1082 let lines = vec!["{scale_out_id=s2|A}".to_string()];
1083 let segments = parse_text_lines(lines).unwrap();
1084 let mut tracker = FxHashMap::default();
1085 let mut rendered = Vec::new();
1086
1087 render_styled_text(&segments[0], 0.0, 16.0, WHITE, &mut tracker, &mut 0,
1088 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1089 |_, _, _| {});
1090
1091 assert!(rendered[0].1.scale_x > 0.9, "Scale out animation scale_x at time 0 should be large? {:?}", rendered);
1092
1093 rendered.clear();
1094 render_styled_text(&segments[0], 2.0, 16.0, WHITE, &mut tracker, &mut 0,
1095 |c, tr, col| rendered.push((c.to_string(), tr, col)),
1096 |_, _, _| {});
1097
1098 assert!(rendered[0].1.scale_x < 0.1, "Scale out animation scale_x after time should be small? {:?}", rendered);
1099 }
1100}