1use crate::parser::ast::*;
10use std::collections::HashSet;
11use std::fmt::Write;
12
13const SVG_W: f32 = 1640.0;
16const SIDEBAR_W: f32 = 200.0;
17const CARD_W: f32 = 370.0;
18const CARD_GAP: f32 = 14.0;
19const GRID_X: f32 = SIDEBAR_W + 14.0;
20const COLS: usize = 3;
21const HEADER_H: f32 = 74.0;
22const LEGEND_H: f32 = 64.0;
23const CONTENT_Y: f32 = HEADER_H + LEGEND_H;
24const ICON_SZ: f32 = 22.0; const ICON_GAP: f32 = 4.0;
26const ICONS_ROW: usize = 11;
27const CARD_PAD: f32 = 12.0;
28const CARD_ROUNDING: f32 = 7.0;
29
30const BG: &str = "#0b0b1a";
33const CARD_BG: &str = "#11112a";
34const CARD_BD: &str = "#22225a";
35const TEXT: &str = "#c0c0e0";
36const TEXT_DIM: &str = "#52528a";
37const GOLD: &str = "#ffd700";
38const SIDEBAR_BG: &str = "#0d0d22";
39
40#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
43#[allow(dead_code)]
44pub(crate) enum Cat {
45 Rings,
47 Spiral,
48 Star,
49 Flower,
50 Lotus,
51 Chakra,
52 Yantra,
53 Hyper,
54 Tess,
55 Rain,
56 Grid,
57 Halftone,
58 Pagoda,
59 Torii,
60 Cog,
61 Fractal,
63 Tone,
65 Vol,
66 Listen,
67 Sfx,
68 Spectrum,
69 Music,
71 Note,
72 Hash,
74 Cipher,
75 Sign,
76 Kem,
77 Shard,
78 Rigid,
80 Soft,
81 Liquid,
82 Force,
83 Collide,
84 Mesh,
86 Draw3D,
87 Draw2D,
88 Trig,
90 MathFn,
91 Noise,
92 Net,
94 Neural,
95 Behavior,
96 Widget,
98 Hud,
99 Dialog,
100 Holo,
101 Str,
103 Font,
104 Vector,
105 Color,
107 Shade,
108 Camera,
109 Light,
110 Fill,
111 Present,
112 Window,
113 Key,
115 Mouse,
116 Print,
118 File,
119 Sys,
120 Anim,
121 User,
123 Loop,
124 Const,
125}
126
127impl Cat {
128 pub(crate) fn color(self) -> &'static str {
129 match self {
130 Cat::Rings => "#00e5ff",
132 Cat::Spiral => "#00ffb3",
133 Cat::Star => "#ffd700",
134 Cat::Flower => "#ff79c6",
135 Cat::Lotus => "#ff5e8a",
136 Cat::Chakra => "#bd93f9",
137 Cat::Yantra => "#ffb86c",
138 Cat::Hyper => "#5575c8",
139 Cat::Tess => "#50fa7b",
140 Cat::Rain => "#f1fa8c",
141 Cat::Grid => "#8be9fd",
142 Cat::Halftone => "#888899",
143 Cat::Pagoda => "#ff9a3c",
144 Cat::Torii => "#ff5e5e",
145 Cat::Cog => "#b0b0c8",
146 Cat::Fractal => "#c77dff",
148 Cat::Tone => "#ff1493",
150 Cat::Vol => "#ff69b4",
151 Cat::Listen => "#db77d9",
152 Cat::Sfx => "#ff85c0",
153 Cat::Spectrum => "#ff4fa3",
154 Cat::Music => "#b388ff",
156 Cat::Note => "#d8a7ff",
157 Cat::Hash => "#0fd9b0",
159 Cat::Cipher => "#12b48c",
160 Cat::Sign => "#5ce0c0",
161 Cat::Kem => "#2aa98a",
162 Cat::Shard => "#7ff0d8",
163 Cat::Rigid => "#ff8c42",
165 Cat::Soft => "#ffb37b",
166 Cat::Liquid => "#4aa3ff",
167 Cat::Force => "#ff6b35",
168 Cat::Collide => "#ff4d4d",
169 Cat::Mesh => "#6aa9ff",
171 Cat::Draw3D => "#8fb8ff",
172 Cat::Draw2D => "#5fd0ff",
173 Cat::Trig => "#b6e36b",
175 Cat::MathFn => "#d4e157",
176 Cat::Noise => "#9ccc65",
177 Cat::Net => "#18b6e0",
179 Cat::Neural => "#ffd54f",
180 Cat::Behavior => "#ffca6b",
181 Cat::Widget => "#7a8fb0",
183 Cat::Hud => "#92a8c8",
184 Cat::Dialog => "#e0b97d",
185 Cat::Holo => "#66ffe0",
186 Cat::Str => "#a0b4d0",
188 Cat::Font => "#c0c8e0",
189 Cat::Vector => "#88d8c0",
190 Cat::Color => "#ff7eb6",
192 Cat::Shade => "#9d8df1",
193 Cat::Camera => "#ffe066",
194 Cat::Light => "#ffe4b5",
195 Cat::Fill => "#3a3a6a",
196 Cat::Present => "#555580",
197 Cat::Window => "#7fd4ff",
198 Cat::Key => "#ff9e7d",
200 Cat::Mouse => "#ffbfa3",
201 Cat::Print => "#9aa0b5",
203 Cat::File => "#7d8aa8",
204 Cat::Sys => "#6b7488",
205 Cat::Anim => "#ffd28a",
206 Cat::User => "#6ab0f5",
208 Cat::Loop => "#ff7f50",
209 Cat::Const => "#50fa7b",
210 }
211 }
212
213 pub(crate) fn label(self) -> &'static str {
214 match self {
215 Cat::Rings => "rings",
216 Cat::Spiral => "spiral",
217 Cat::Star => "star",
218 Cat::Flower => "flower",
219 Cat::Lotus => "lotus",
220 Cat::Chakra => "chakra",
221 Cat::Yantra => "yantra",
222 Cat::Hyper => "hyperbolic",
223 Cat::Tess => "tessellated",
224 Cat::Rain => "letter rain",
225 Cat::Grid => "grid",
226 Cat::Halftone => "halftone",
227 Cat::Pagoda => "pagoda",
228 Cat::Torii => "torii",
229 Cat::Cog => "spiked cog",
230 Cat::Fractal => "fractal tex",
231 Cat::Tone => "audio tone",
232 Cat::Vol => "audio vol",
233 Cat::Listen => "listener",
234 Cat::Sfx => "sfx",
235 Cat::Spectrum => "spectrum",
236 Cat::Music => "music",
237 Cat::Note => "note",
238 Cat::Hash => "hash",
239 Cat::Cipher => "cipher",
240 Cat::Sign => "signature",
241 Cat::Kem => "key exch",
242 Cat::Shard => "secret share",
243 Cat::Rigid => "rigid body",
244 Cat::Soft => "soft body",
245 Cat::Liquid => "liquid",
246 Cat::Force => "force",
247 Cat::Collide => "collision",
248 Cat::Mesh => "mesh",
249 Cat::Draw3D => "draw 3d",
250 Cat::Draw2D => "draw 2d",
251 Cat::Trig => "trig",
252 Cat::MathFn => "math",
253 Cat::Noise => "noise",
254 Cat::Net => "network",
255 Cat::Neural => "neural net",
256 Cat::Behavior => "behavior tree",
257 Cat::Widget => "ui widget",
258 Cat::Hud => "hud",
259 Cat::Dialog => "dialog",
260 Cat::Holo => "hologram",
261 Cat::Str => "string",
262 Cat::Font => "font",
263 Cat::Vector => "svg vector",
264 Cat::Color => "color",
265 Cat::Shade => "shading",
266 Cat::Camera => "camera",
267 Cat::Light => "light",
268 Cat::Fill => "fill",
269 Cat::Present => "render",
270 Cat::Window => "window",
271 Cat::Key => "keyboard",
272 Cat::Mouse => "mouse",
273 Cat::Print => "print",
274 Cat::File => "file io",
275 Cat::Sys => "system",
276 Cat::Anim => "animation",
277 Cat::User => "fn call",
278 Cat::Loop => "loop",
279 Cat::Const => "const",
280 }
281 }
282
283 pub(crate) fn domain(self) -> &'static str {
285 if is_vtex(self) {
286 return "texture";
287 }
288 if is_audio(self) {
289 return "audio";
290 }
291 if is_crypto(self) {
292 return "crypto";
293 }
294 if is_physics(self) {
295 return "physics";
296 }
297 if is_ai(self) {
298 return "ai";
299 }
300 match self {
301 Cat::Music | Cat::Note => "music",
302 Cat::Mesh | Cat::Draw3D | Cat::Draw2D | Cat::Vector => "geometry",
303 Cat::Trig | Cat::MathFn | Cat::Noise | Cat::Fractal => "math",
304 Cat::Widget | Cat::Hud | Cat::Dialog | Cat::Holo => "ui",
305 Cat::Str | Cat::Font => "text",
306 Cat::Color
307 | Cat::Shade
308 | Cat::Camera
309 | Cat::Light
310 | Cat::Fill
311 | Cat::Present
312 | Cat::Window => "scene",
313 Cat::Key | Cat::Mouse => "input",
314 Cat::Print | Cat::File | Cat::Sys | Cat::Anim | Cat::Net => "system",
315 _ => "code",
316 }
317 }
318}
319
320pub(crate) fn categorize(name: &str) -> Cat {
321 if name.starts_with("vtex_rings")
323 || name == "ลายวงซ้อน"
324 || name == "纹环"
325 || name == "輪模様"
326 || name == "輪무늬"
327 {
328 return Cat::Rings;
329 }
330 if name.starts_with("vtex_spiral")
331 || name == "ลายก้นหอย"
332 || name == "ลายเกลียว"
333 || name == "ลายเกลียวหมุน"
334 || name == "纹螺"
335 || name == "螺旋模様"
336 || name == "나선무늬"
337 {
338 return Cat::Spiral;
339 }
340 if name.starts_with("vtex_star")
341 || name == "ลายดาว"
342 || name == "纹星"
343 || name == "星模様"
344 || name == "별무늬"
345 {
346 return Cat::Star;
347 }
348 if name.starts_with("vtex_flower")
349 || name == "ลายดอกไม้"
350 || name == "ลายดอก"
351 || name == "纹花"
352 || name == "花模様"
353 || name == "꽃무늬"
354 {
355 return Cat::Flower;
356 }
357 if name.starts_with("vtex_lotus")
358 || name == "ลายบัว"
359 || name == "ลายดอกบัว"
360 || name == "纹莲"
361 || name == "蓮模様"
362 || name == "연꽃무늬"
363 {
364 return Cat::Lotus;
365 }
366 if name.starts_with("vtex_chakra")
367 || name == "ลายจักร"
368 || name == "纹轮"
369 || name == "輪模様"
370 || name == "바퀴무늬"
371 {
372 return Cat::Chakra;
373 }
374 if name.starts_with("vtex_yantra")
375 || name == "ลายยันต์"
376 || name == "纹咒"
377 || name == "護符模様"
378 || name == "부적무늬"
379 {
380 return Cat::Yantra;
381 }
382 if name.starts_with("vtex_hyper")
383 || name == "ลายไฮเพอร์โบลิก"
384 || name == "ลายไฮเปอร์"
385 || name == "纹超"
386 || name == "超次元模様"
387 || name == "초차원무늬"
388 {
389 return Cat::Hyper;
390 }
391 if name.starts_with("vtex_tessellated")
392 || name == "ลายตาข่าย"
393 || name == "纹镶嵌"
394 || name == "網目模様"
395 {
396 return Cat::Tess;
397 }
398 if name.starts_with("vtex_letter_rain")
399 || name.starts_with("vtex_rain")
400 || name == "ลายอักษรไหล"
401 || name == "ลายฝน"
402 || name == "纹雨"
403 || name == "文字雨"
404 || name == "비무늬"
405 {
406 return Cat::Rain;
407 }
408 if name.starts_with("vtex_grid")
409 || name == "ลายตาราง"
410 || name == "纹格"
411 || name == "格子模様"
412 || name == "격자무늬"
413 {
414 return Cat::Grid;
415 }
416 if name.starts_with("vtex_halftone")
417 || name == "ลายจุด"
418 || name == "ลายฮาล์ฟโทน"
419 || name == "纹半调"
420 || name == "網点模様"
421 || name == "망점"
422 {
423 return Cat::Halftone;
424 }
425 if name.starts_with("vtex_pagoda")
426 || name == "ลายเจดีย์"
427 || name == "เจดีย์"
428 || name == "纹塔"
429 || name == "탑"
430 {
431 return Cat::Pagoda;
432 }
433 if name.starts_with("vtex_torii")
434 || name == "ประตูโทริอิ"
435 || name == "纹鸟居"
436 || name == "鳥居"
437 || name == "도리이"
438 {
439 return Cat::Torii;
440 }
441 if name.starts_with("vtex_spiked_cog")
442 || name == "ฟันเฟืองหนาม"
443 || name == "纹棘轮"
444 || name == "歯車模様"
445 || name == "톱니바퀴"
446 {
447 return Cat::Cog;
448 }
449 if name.starts_with("vtex_") {
450 return Cat::Tess;
451 }
452
453 if name.starts_with("tex_") {
455 return Cat::Fractal;
456 }
457
458 if name == "audio_tone"
460 || name == "เสียงโทน"
461 || name == "音調"
462 || name == "音调"
463 || name == "음조"
464 {
465 return Cat::Tone;
466 }
467 if name == "audio_volume"
468 || name == "audio_bgm_volume"
469 || name == "ระดับเสียง"
470 || name == "音量"
471 || name == "음량"
472 {
473 return Cat::Vol;
474 }
475 if name == "audio_listener"
476 || name == "音声リスナー"
477 || name == "오디오리스너"
478 || name == "音频监听"
479 {
480 return Cat::Listen;
481 }
482 if name.starts_with("audio_") {
483 return Cat::Sfx;
484 }
485 if name.starts_with("mic_") || name.starts_with("fft_") {
486 return Cat::Spectrum;
487 }
488
489 if name == "music_note_on"
491 || name == "music_note_off"
492 || name == "music_note"
493 || name == "music_note_name"
494 {
495 return Cat::Note;
496 }
497 if name.starts_with("music_") || name.starts_with("midi_") || name.starts_with("MIDI") {
498 return Cat::Music;
499 }
500
501 if name == "crypto_hash"
503 || name == "sha3_512"
504 || name == "blake3"
505 || name == "hash_int"
506 || name == "hash_str"
507 {
508 return Cat::Hash;
509 }
510 if name == "crypto_seal" || name == "crypto_open" || name == "encrypt" || name == "aes_gcm_256"
511 {
512 return Cat::Cipher;
513 }
514 if name == "ed25519"
515 || name == "schnorr_verify"
516 || name == "vrf_verify"
517 || name == "derive"
518 || name == "argon2id"
519 {
520 return Cat::Sign;
521 }
522 if name == "mlkem768" || name.starts_with("hybrid_") || name.starts_with("knot_") {
523 return Cat::Kem;
524 }
525 if name == "shamir_reconstruct" {
526 return Cat::Shard;
527 }
528
529 if name.starts_with("rb_") || name == "rigidbody" {
531 return Cat::Rigid;
532 }
533 if name.starts_with("soft_") {
534 return Cat::Soft;
535 }
536 if name.starts_with("liquid_") {
537 return Cat::Liquid;
538 }
539 if matches!(
540 name,
541 "force"
542 | "torque"
543 | "spring"
544 | "friction"
545 | "elasticity"
546 | "acceleration"
547 | "apply_impulse"
548 | "gyro"
549 ) {
550 return Cat::Force;
551 }
552 if matches!(
553 name,
554 "collision" | "raycast" | "aabb" | "AABB" | "constraint"
555 ) {
556 return Cat::Collide;
557 }
558
559 if matches!(
561 name,
562 "cube"
563 | "box"
564 | "capsule"
565 | "capsule_chain"
566 | "cylinder"
567 | "pyramid"
568 | "icosphere"
569 | "icosahedron"
570 | "octahedron"
571 | "orb_shell"
572 | "stairs"
573 | "frustum"
574 ) {
575 return Cat::Mesh;
576 }
577 if matches!(
578 name,
579 "draw_line_3d"
580 | "draw_triangle_3d"
581 | "line3d"
582 | "triangle3d"
583 | "project_3d"
584 | "render_3d"
585 | "flush_3d"
586 | "font_text_3d"
587 ) {
588 return Cat::Draw3D;
589 }
590
591 if name.starts_with("nn_") {
593 return Cat::Neural;
594 }
595 if name.starts_with("bt_") {
596 return Cat::Behavior;
597 }
598 if name.starts_with("dialog_") {
599 return Cat::Dialog;
600 }
601
602 if name.starts_with("net_") {
604 return Cat::Net;
605 }
606
607 if matches!(
609 name,
610 "ui_radar"
611 | "ui_radar3d"
612 | "ui_minimap"
613 | "ui_compass"
614 | "ui_healthbar"
615 | "ui_reticle"
616 | "ui_target"
617 | "ui_gauge"
618 | "ui_gauge3d"
619 | "ui_vu"
620 | "ui_battery"
621 | "ui_segbar"
622 | "ui_bar"
623 | "ui_progress"
624 | "ui_cooldown"
625 | "ui_scanlines"
626 | "ui_vignette"
627 | "ui_spark"
628 | "ui_ring"
629 | "ui_counter"
630 ) {
631 return Cat::Hud;
632 }
633 if name.starts_with("ui_") {
634 return Cat::Widget;
635 }
636
637 if matches!(
639 name,
640 "holo_points" | "holo_fragment_count" | "knot_points" | "sparkle" | "neon" | "psychedelic"
641 ) {
642 return Cat::Holo;
643 }
644
645 if name.starts_with("font_") {
647 return Cat::Font;
648 }
649 if name.starts_with("svg_") {
650 return Cat::Vector;
651 }
652
653 if name.starts_with("str_")
655 || matches!(
656 name,
657 "split"
658 | "join"
659 | "trim"
660 | "substr"
661 | "format"
662 | "to_str"
663 | "num_str"
664 | "starts_with"
665 | "ends_with"
666 )
667 {
668 return Cat::Str;
669 }
670
671 if matches!(
673 name,
674 "sin"
675 | "cos"
676 | "tan"
677 | "asin"
678 | "acos"
679 | "atan"
680 | "atan2"
681 | "arcsin"
682 | "arccos"
683 | "arctan"
684 | "arctan2"
685 | "tanh"
686 | "tanhf"
687 | "hypot"
688 ) {
689 return Cat::Trig;
690 }
691 if matches!(
692 name,
693 "perlin" | "perlin3" | "noise2" | "fbm" | "vnoise" | "smoothstep"
694 ) {
695 return Cat::Noise;
696 }
697 if matches!(
698 name,
699 "sqrt"
700 | "cbrt"
701 | "pow"
702 | "exp"
703 | "ln"
704 | "log"
705 | "log2"
706 | "log10"
707 | "abs"
708 | "floor"
709 | "ceil"
710 | "round"
711 | "trunc"
712 | "fract"
713 | "sign"
714 | "clamp"
715 | "lerp"
716 | "min"
717 | "max"
718 | "rand"
719 | "pi"
720 | "tau"
721 ) {
722 return Cat::MathFn;
723 }
724
725 if matches!(
727 name,
728 "hsl_color"
729 | "hsv_to_rgb"
730 | "set_color"
731 | "set_color_hsl"
732 | "color"
733 | "lerp_color"
734 | "gfx_color"
735 ) {
736 return Cat::Color;
737 }
738 if matches!(
739 name,
740 "set_shade_mode"
741 | "set_rim"
742 | "set_fog"
743 | "set_ambient"
744 | "set_cel_bands"
745 | "set_blend"
746 | "set_shadow_color"
747 | "set_projection"
748 | "set_zdist"
749 ) {
750 return Cat::Shade;
751 }
752
753 if matches!(name, "set_camera" | "set_camera_pos" | "move_camera") {
755 return Cat::Camera;
756 }
757 if matches!(name, "add_light" | "clear_lights") {
758 return Cat::Light;
759 }
760
761 if matches!(
763 name,
764 "open_window"
765 | "open_fullscreen"
766 | "gfx_window"
767 | "fullscreen"
768 | "windowed"
769 | "wait_window"
770 | "is_open"
771 | "window_is_open"
772 | "gfx_is_open"
773 | "gfx_wait"
774 | "resolution"
775 | "get_width"
776 | "get_height"
777 ) {
778 return Cat::Window;
779 }
780 if matches!(
781 name,
782 "present" | "แสดงผล" | "gfx_present" | "render" | "render_3d" | "flush_3d"
783 ) {
784 return Cat::Present;
785 }
786 if matches!(name, "fill" | "เติม" | "gfx_fill" | "clear") {
787 return Cat::Fill;
788 }
789
790 if name.starts_with("draw_")
792 || matches!(
793 name,
794 "gfx_line" | "gfx_pixel" | "gfx_triangle" | "line" | "triangle" | "pixel"
795 )
796 {
797 return Cat::Draw2D;
798 }
799
800 if name.starts_with("mouse_") || matches!(name, "capture_mouse" | "release_mouse") {
802 return Cat::Mouse;
803 }
804 if name.starts_with("key_") || matches!(name, "keys" | "text_poll" | "text_get") {
805 return Cat::Key;
806 }
807
808 if matches!(
810 name,
811 "print" | "println" | "print_file" | "imprimir" | "afficher" | "вывести" | "พิมพ์" | "打印"
812 ) {
813 return Cat::Print;
814 }
815 if matches!(name, "read_file" | "write_file" | "อ่านไฟล์" | "เขียนไฟล์")
816 {
817 return Cat::File;
818 }
819 if matches!(
820 name,
821 "system" | "get_args" | "time_now" | "sleep" | "sleep_ms"
822 ) {
823 return Cat::Sys;
824 }
825
826 if matches!(
828 name,
829 "animation"
830 | "ease"
831 | "tick"
832 | "delta_time"
833 | "frame_count"
834 | "frame"
835 | "record_frame"
836 | "record_count"
837 | "tween"
838 | "tween_ease"
839 | "breathe"
840 | "wobble"
841 | "gait_phase"
842 | "gait_swing"
843 | "gait_lift"
844 | "spring_to"
845 | "ik2"
846 ) {
847 return Cat::Anim;
848 }
849 if matches!(
851 name,
852 "gear_couple" | "gear_train" | "cam_lift" | "piston" | "rack"
853 ) {
854 return Cat::Cog;
855 }
856
857 Cat::User
858}
859
860pub(crate) fn is_vtex(c: Cat) -> bool {
861 matches!(
862 c,
863 Cat::Rings
864 | Cat::Spiral
865 | Cat::Star
866 | Cat::Flower
867 | Cat::Lotus
868 | Cat::Chakra
869 | Cat::Yantra
870 | Cat::Hyper
871 | Cat::Tess
872 | Cat::Rain
873 | Cat::Grid
874 | Cat::Halftone
875 | Cat::Pagoda
876 | Cat::Torii
877 | Cat::Cog
878 )
879}
880pub(crate) fn is_audio(c: Cat) -> bool {
881 matches!(
882 c,
883 Cat::Tone | Cat::Vol | Cat::Listen | Cat::Sfx | Cat::Spectrum
884 )
885}
886pub(crate) fn is_crypto(c: Cat) -> bool {
887 matches!(
888 c,
889 Cat::Hash | Cat::Cipher | Cat::Sign | Cat::Kem | Cat::Shard
890 )
891}
892pub(crate) fn is_physics(c: Cat) -> bool {
893 matches!(
894 c,
895 Cat::Rigid | Cat::Soft | Cat::Liquid | Cat::Force | Cat::Collide
896 )
897}
898pub(crate) fn is_ai(c: Cat) -> bool {
899 matches!(c, Cat::Neural | Cat::Behavior)
900}
901
902const ENTRY_NAMES: &[&str] = &[
903 "start",
904 "main",
905 "启",
906 "เริ่ม",
907 "시작",
908 "начать",
909 "начало",
910 "inicio",
911 "comenzar",
912 "début",
913 "commencer",
914 "anfang",
915 "starten",
916 "início",
917];
918fn is_entry(name: &str) -> bool {
919 ENTRY_NAMES.contains(&name)
920}
921
922struct GlobalConst {
925 name: String,
926 value: String,
927}
928
929#[derive(Clone)]
930struct CallItem {
931 name: String,
932 cat: Cat,
933 count: usize,
934}
935
936struct FuncCard {
937 name: String,
938 params: Vec<String>,
939 calls: Vec<CallItem>,
940 has_loop: bool,
941 is_entry: bool,
942 vtex_count: usize,
943 audio_count: usize,
944}
945
946struct Document {
947 filename: String,
948 globals: Vec<GlobalConst>,
949 funcs: Vec<FuncCard>,
950 #[allow(dead_code)]
951 fn_names: HashSet<String>,
952}
953
954struct RawCall {
957 name: String,
958 cat: Cat,
959}
960
961fn walk_stmts(stmts: &[Stmt], fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
962 for s in stmts {
963 let e = match s {
964 Stmt::Expr(e) | Stmt::Return(e) => e,
965 Stmt::Bind(_, e) => e,
966 };
967 walk_expr(e, fns, out, loop_);
968 }
969}
970
971fn walk_expr(e: &Expr, fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
972 match e {
973 Expr::Call(func, args) => {
974 if let Expr::Ident(name) = func.as_ref() {
975 let cat = if fns.contains(name.as_str()) {
976 let base = categorize(name);
977 if base == Cat::User {
978 Cat::User
979 } else {
980 base
981 }
982 } else {
983 categorize(name)
984 };
985 out.push(RawCall { name: name.clone(), cat });
986 }
987 for a in args {
988 walk_expr(a, fns, out, loop_);
989 }
990 },
991 Expr::While { cond, body } => {
992 *loop_ = true;
993 walk_expr(cond, fns, out, loop_);
994 walk_stmts(body, fns, out, loop_);
995 },
996 Expr::Do(ss) => walk_stmts(ss, fns, out, loop_),
997 Expr::If { cond, then, elseifs, else_body } => {
998 walk_expr(cond, fns, out, loop_);
999 walk_stmts(then, fns, out, loop_);
1000 for (c, b) in elseifs {
1001 walk_expr(c, fns, out, loop_);
1002 walk_stmts(b, fns, out, loop_);
1003 }
1004 if let Some(b) = else_body {
1005 walk_stmts(b, fns, out, loop_);
1006 }
1007 },
1008 Expr::For { iter, body, .. } => {
1009 walk_expr(iter, fns, out, loop_);
1010 walk_stmts(body, fns, out, loop_);
1011 },
1012 Expr::BinOp(_, a, b) => {
1013 walk_expr(a, fns, out, loop_);
1014 walk_expr(b, fns, out, loop_);
1015 },
1016 Expr::Array(es) => {
1017 for a in es {
1018 walk_expr(a, fns, out, loop_);
1019 }
1020 },
1021 _ => {},
1022 }
1023}
1024
1025fn aggregate(raw: Vec<RawCall>) -> Vec<CallItem> {
1026 let mut out: Vec<CallItem> = Vec::new();
1027 for r in raw {
1028 if let Some(last) = out.last_mut() {
1029 if last.name == r.name {
1030 last.count += 1;
1031 continue;
1032 }
1033 }
1034 out.push(CallItem { name: r.name, cat: r.cat, count: 1 });
1035 }
1036 out
1037}
1038
1039fn make_card(
1040 name: String,
1041 params: Vec<String>,
1042 raw: Vec<RawCall>,
1043 has_loop: bool,
1044 is_entry: bool,
1045) -> FuncCard {
1046 let calls = aggregate(raw);
1047 let vtex_count = calls
1048 .iter()
1049 .filter(|c| is_vtex(c.cat))
1050 .map(|c| c.count)
1051 .sum();
1052 let audio_count = calls
1053 .iter()
1054 .filter(|c| is_audio(c.cat))
1055 .map(|c| c.count)
1056 .sum();
1057 FuncCard {
1058 name,
1059 params,
1060 calls,
1061 has_loop,
1062 is_entry,
1063 vtex_count,
1064 audio_count,
1065 }
1066}
1067
1068impl Document {
1069 fn build(filename: &str, prog: &Program) -> Self {
1070 let fn_names: HashSet<String> = prog
1071 .items
1072 .iter()
1073 .filter_map(|i| {
1074 if let Item::Fn(f) = i {
1075 Some(f.name.clone())
1076 } else {
1077 None
1078 }
1079 })
1080 .collect();
1081
1082 let mut globals = Vec::new();
1083 let mut entries = Vec::new();
1084 let mut funcs = Vec::new();
1085
1086 for item in &prog.items {
1087 match item {
1088 Item::Bind(name, expr) => match expr {
1089 Expr::Number(n) => globals.push(GlobalConst {
1090 name: name.clone(),
1091 value: if n.fract() == 0.0 {
1092 format!("{}", *n as i64)
1093 } else {
1094 format!("{:.2}", n)
1095 },
1096 }),
1097 Expr::Do(body) => {
1098 let mut raw = Vec::new();
1099 let mut lp = false;
1100 for s in body {
1102 if let Stmt::Expr(Expr::While { body: wb, .. }) = s {
1103 lp = true;
1104 walk_stmts(wb, &fn_names, &mut raw, &mut lp);
1105 }
1106 }
1107 walk_stmts(body, &fn_names, &mut raw, &mut lp);
1108 entries.push(make_card(name.clone(), vec![], raw, lp, is_entry(name)));
1109 },
1110 _ => {},
1111 },
1112 Item::Fn(f) => {
1113 let mut raw = Vec::new();
1114 let mut lp = false;
1115 walk_stmts(&f.body, &fn_names, &mut raw, &mut lp);
1116 funcs.push(make_card(f.name.clone(), f.params.clone(), raw, lp, false));
1117 },
1118 _ => {},
1119 }
1120 }
1121
1122 entries.extend(funcs);
1123 Document {
1124 filename: filename.to_string(),
1125 globals,
1126 funcs: entries,
1127 fn_names,
1128 }
1129 }
1130}
1131
1132fn xe(s: &str) -> String {
1136 s.replace('&', "&")
1137 .replace('<', "<")
1138 .replace('>', ">")
1139}
1140fn p(v: f32) -> String {
1141 format!("{:.2}", v)
1142}
1143
1144pub(crate) fn icon(cat: Cat, cx: f32, cy: f32, r: f32) -> String {
1145 let c = cat.color();
1146 match cat {
1147 Cat::Rings => {
1148 format!(
1149 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.9"/>
1150 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.65"/>
1151 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.4"/>"#,
1152 p(cx),
1153 p(cy),
1154 p(r),
1155 p(cx),
1156 p(cy),
1157 p(r * 0.63),
1158 p(cx),
1159 p(cy),
1160 p(r * 0.33)
1161 )
1162 },
1163 Cat::Spiral => {
1164 let (x0, y0) = (cx, cy - r * 0.1);
1166 let (x1, y1) = (cx + r * 0.85, cy);
1167 let (x2, y2) = (cx, cy + r * 0.9);
1168 let (x3, y3) = (cx - r * 0.85, cy + r * 0.1);
1169 let (x4, y4) = (cx, cy - r * 0.9);
1170 format!(
1171 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{} S {},{} {},{} S {},{} {},{}"
1172 fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>"#,
1173 p(x0),
1174 p(y0),
1175 p(cx + r),
1176 p(y0),
1177 p(x1),
1178 p(cy - r * 0.5),
1179 p(x1),
1180 p(y1),
1181 p(cx + r * 0.3),
1182 p(y2),
1183 p(x2),
1184 p(y2),
1185 p(x3),
1186 p(y3 - r * 0.4),
1187 p(x3),
1188 p(y3),
1189 p(cx),
1190 p(y4),
1191 p(x4),
1192 p(y4)
1193 )
1194 },
1195 Cat::Star => {
1196 let pts: String = (0..5)
1198 .flat_map(|i| {
1199 let ao = (i as f32 * 72.0 - 90.0).to_radians();
1200 let ai = ao + 36.0_f32.to_radians();
1201 let ri = r * 0.42;
1202 vec![
1203 format!("{},{}", p(cx + r * ao.cos()), p(cy + r * ao.sin())),
1204 format!("{},{}", p(cx + ri * ai.cos()), p(cy + ri * ai.sin())),
1205 ]
1206 })
1207 .collect::<Vec<_>>()
1208 .join(" ");
1209 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.85"/>"#)
1210 },
1211 Cat::Flower => {
1212 (0..6)
1214 .map(|i| {
1215 let angle = i as f32 * 60.0;
1216 format!(
1217 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
1218 fill="{c}" opacity="0.5"
1219 transform="rotate({angle},{},{})"/>"#,
1220 p(cx),
1221 p(cy - r * 0.45),
1222 p(r * 0.28),
1223 p(r * 0.52),
1224 p(cx),
1225 p(cy)
1226 )
1227 })
1228 .collect::<String>()
1229 },
1230 Cat::Lotus => {
1231 (0..8)
1233 .map(|i| {
1234 let angle = i as f32 * 45.0;
1235 format!(
1236 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
1237 fill="{c}" opacity="0.45"
1238 transform="rotate({angle},{},{})"/>"#,
1239 p(cx),
1240 p(cy - r * 0.50),
1241 p(r * 0.22),
1242 p(r * 0.55),
1243 p(cx),
1244 p(cy)
1245 )
1246 })
1247 .collect::<String>()
1248 + &format!(
1249 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
1250 p(cx),
1251 p(cy),
1252 p(r * 0.22)
1253 )
1254 },
1255 Cat::Chakra => {
1256 let spokes: String = (0..8).map(|i| {
1258 let a = (i as f32 * 45.0).to_radians();
1259 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.8"/>"#,
1260 p(cx + r*0.28*a.cos()), p(cy + r*0.28*a.sin()),
1261 p(cx + r*0.88*a.cos()), p(cy + r*0.88*a.sin()))
1262 }).collect();
1263 spokes
1264 + &format!(
1265 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
1266 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
1267 p(cx),
1268 p(cy),
1269 p(r * 0.88),
1270 p(cx),
1271 p(cy),
1272 p(r * 0.2)
1273 )
1274 },
1275 Cat::Yantra => {
1276 let h = r * 0.866;
1278 let t1 = format!(
1279 "{},{} {},{} {},{}",
1280 p(cx),
1281 p(cy - r),
1282 p(cx + h),
1283 p(cy + r * 0.5),
1284 p(cx - h),
1285 p(cy + r * 0.5)
1286 );
1287 let t2 = format!(
1288 "{},{} {},{} {},{}",
1289 p(cx),
1290 p(cy + r),
1291 p(cx - h),
1292 p(cy - r * 0.5),
1293 p(cx + h),
1294 p(cy - r * 0.5)
1295 );
1296 format!(
1297 r#"<polygon points="{t1}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1298 <polygon points="{t2}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1299 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25"/>"#,
1300 p(cx),
1301 p(cy),
1302 p(r * 0.38)
1303 )
1304 },
1305 Cat::Hyper => {
1306 let rays: String = (0..6).map(|i| {
1308 let a = (i as f32 * 30.0).to_radians();
1309 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"#,
1310 p(cx - r*a.cos()), p(cy - r*a.sin()),
1311 p(cx + r*a.cos()), p(cy + r*a.sin()))
1312 }).collect();
1313 rays + &format!(
1314 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>
1315 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.5"/>
1316 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.3"/>"#,
1317 p(cx),
1318 p(cy),
1319 p(r),
1320 p(cx),
1321 p(cy),
1322 p(r * 0.6),
1323 p(cx),
1324 p(cy),
1325 p(r * 0.3)
1326 )
1327 },
1328 Cat::Tess => {
1329 (0..4)
1331 .map(|i| {
1332 let y = cy - r * 0.7 + i as f32 * r * 0.46;
1333 let amp = r * 0.15;
1334 format!(
1335 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
1336 fill="none" stroke="{c}" stroke-width="1.1" opacity="0.7"/>"#,
1337 p(cx - r),
1338 p(y),
1339 p(cx - r + r * 0.4),
1340 p(y - amp),
1341 p(cx - r * 0.1),
1342 p(y - amp),
1343 p(cx),
1344 p(y),
1345 p(cx + r * 0.5),
1346 p(y + amp),
1347 p(cx + r),
1348 p(y)
1349 )
1350 })
1351 .collect::<String>()
1352 },
1353 Cat::Rain => {
1354 (0..5).flat_map(|col| {
1356 let x = cx - r*0.9 + col as f32 * r*0.45;
1357 (0..3).map(move |row| {
1358 let y = cy - r*0.8 + row as f32 * r*0.55;
1359 let opa = 0.9 - row as f32 * 0.22;
1360 format!(r#"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="{:.2}"/>"#,
1361 p(x - r*0.06), p(y), p(r*0.12), p(r*0.30), opa)
1362 })
1363 }).collect::<String>()
1364 },
1365 Cat::Grid => {
1366 let n = 3;
1368 let step = r * 2.0 / n as f32;
1369 let mut s = String::new();
1370 for i in 0..=n {
1371 let off = -r + i as f32 * step;
1372 write!(s, r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>
1373 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>"#,
1374 p(cx+off),p(cy-r), p(cx+off),p(cy+r),
1375 p(cx-r),p(cy+off), p(cx+r),p(cy+off)).ok();
1376 }
1377 s
1378 },
1379 Cat::Halftone => {
1380 (0..4)
1382 .flat_map(|row| {
1383 (0..4).map(move |col| {
1384 let x = cx - r * 0.75 + col as f32 * r * 0.5;
1385 let y = cy - r * 0.75 + row as f32 * r * 0.5;
1386 format!(
1387 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.6"/>"#,
1388 p(x),
1389 p(y),
1390 p(r * 0.14)
1391 )
1392 })
1393 })
1394 .collect::<String>()
1395 },
1396 Cat::Tone => {
1397 let x0 = cx - r;
1399 let x3 = cx + r;
1400 let xm = cx;
1401 let amp = r * 0.65;
1402 format!(
1403 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
1404 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>"#,
1405 p(x0),
1406 p(cy),
1407 p(x0 + r * 0.5),
1408 p(cy - amp),
1409 p(xm - r * 0.1),
1410 p(cy - amp),
1411 p(xm),
1412 p(cy),
1413 p(xm + r * 0.6),
1414 p(cy + amp),
1415 p(x3),
1416 p(cy)
1417 )
1418 },
1419 Cat::Vol | Cat::Listen => {
1420 let arcs: String = (1..=3)
1422 .map(|i| {
1423 let ri = r * 0.3 * i as f32;
1424 format!(
1425 r#"<path d="M {},{} A {ri},{ri} 0 0,1 {},{}"
1426 fill="none" stroke="{c}" stroke-width="1.3" opacity="{:.2}"/>"#,
1427 p(cx),
1428 p(cy - ri),
1429 p(cx),
1430 p(cy + ri),
1431 1.0 - i as f32 * 0.22
1432 )
1433 })
1434 .collect();
1435 arcs + &format!(
1436 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.7"/>"#,
1437 p(cx - r * 0.15),
1438 p(cy),
1439 p(r * 0.22)
1440 )
1441 },
1442 Cat::Camera => {
1443 format!(
1445 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.8"/>
1446 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.55"/>
1447 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"#,
1448 p(cx),
1449 p(cy),
1450 p(r),
1451 p(cx),
1452 p(cy),
1453 p(r * 0.6),
1454 p(cx),
1455 p(cy),
1456 p(r * 0.18)
1457 )
1458 },
1459 Cat::Light => {
1460 let rays: String = (0..8).map(|i| {
1462 let a = (i as f32 * 45.0).to_radians();
1463 let r1 = r * 0.38;
1464 let r2 = r * 0.90;
1465 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.75"/>"#,
1466 p(cx + r1*a.cos()), p(cy + r1*a.sin()),
1467 p(cx + r2*a.cos()), p(cy + r2*a.sin()))
1468 }).collect();
1469 rays + &format!(
1470 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"#,
1471 p(cx),
1472 p(cy),
1473 p(r * 0.32)
1474 )
1475 },
1476 Cat::Fill => {
1477 format!(
1478 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"#,
1479 p(cx - r * 0.9),
1480 p(cy - r * 0.7),
1481 p(r * 1.8),
1482 p(r * 1.4)
1483 )
1484 },
1485 Cat::Present => {
1486 let pts = format!(
1488 "{},{} {},{} {},{}",
1489 p(cx - r * 0.7),
1490 p(cy - r * 0.8),
1491 p(cx + r * 0.8),
1492 p(cy),
1493 p(cx - r * 0.7),
1494 p(cy + r * 0.8)
1495 );
1496 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.7"/>"#)
1497 },
1498 Cat::User => {
1499 format!(
1501 r#"<path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
1502 stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
1503 <path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
1504 stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>"#,
1505 p(cx - r * 0.6),
1506 p(cy - r * 0.7),
1507 p(cx + r * 0.5),
1508 p(cy),
1509 p(cx - r * 0.6),
1510 p(cy + r * 0.7),
1511 p(cx),
1512 p(cy - r * 0.7),
1513 p(cx + r * 0.95),
1514 p(cy),
1515 p(cx),
1516 p(cy + r * 0.7)
1517 )
1518 },
1519 Cat::Loop => {
1520 format!(
1522 r#"<path d="M {},{} A {},{} 0 1,1 {},{}"
1523 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
1524 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"#,
1525 p(cx),
1526 p(cy - r * 0.9),
1527 p(r * 0.9),
1528 p(r * 0.9),
1529 p(cx + r * 0.4),
1530 p(cy - r * 0.9),
1531 p(cx + r * 0.4),
1532 p(cy - r * 0.9),
1533 p(cx + r * 0.1),
1534 p(cy - r * 0.55),
1535 p(cx + r * 0.8),
1536 p(cy - r * 0.62)
1537 )
1538 },
1539 Cat::Pagoda => {
1541 let mut s = String::new();
1542 for i in 0..3 {
1543 let yy = cy - r * 0.7 + i as f32 * r * 0.55;
1544 let hw = r * (0.4 + i as f32 * 0.22);
1545 write!(s, r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
1546 p(cx - hw), p(yy), p(cx), p(yy - r*0.35), p(cx + hw), p(yy)).ok();
1547 }
1548 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"##,
1549 p(cx), p(cy - r*1.05), p(cx), p(cy + r*0.55)).ok();
1550 s
1551 },
1552 Cat::Torii => {
1553 format!(
1554 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
1555 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1556 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>
1557 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
1558 p(cx - r * 0.95),
1559 p(cy - r * 0.6),
1560 p(cx + r * 0.95),
1561 p(cy - r * 0.6),
1562 p(cx - r * 0.75),
1563 p(cy - r * 0.2),
1564 p(cx + r * 0.75),
1565 p(cy - r * 0.2),
1566 p(cx - r * 0.55),
1567 p(cy - r * 0.6),
1568 p(cx - r * 0.55),
1569 p(cy + r * 0.85),
1570 p(cx + r * 0.55),
1571 p(cy - r * 0.6),
1572 p(cx + r * 0.55),
1573 p(cy + r * 0.85)
1574 )
1575 },
1576 Cat::Cog => {
1577 let mut s = String::new();
1578 for i in 0..8 {
1579 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.8" transform="rotate({},{},{})"/>"##,
1580 p(cx - r*0.13), p(cy - r*1.0), p(r*0.26), p(r*0.3), p(i as f32 * 45.0), p(cx), p(cy)).ok();
1581 }
1582 write!(
1583 s,
1584 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.55"/>
1585 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
1586 p(cx),
1587 p(cy),
1588 p(r * 0.7),
1589 p(cx),
1590 p(cy),
1591 p(r * 0.3)
1592 )
1593 .ok();
1594 s
1595 },
1596 Cat::Fractal => {
1597 let outer = format!(
1598 "{},{} {},{} {},{}",
1599 p(cx),
1600 p(cy - r * 0.9),
1601 p(cx + r * 0.85),
1602 p(cy + r * 0.6),
1603 p(cx - r * 0.85),
1604 p(cy + r * 0.6)
1605 );
1606 let inner = format!(
1607 "{},{} {},{} {},{}",
1608 p(cx),
1609 p(cy + r * 0.6),
1610 p(cx - r * 0.42),
1611 p(cy - r * 0.12),
1612 p(cx + r * 0.42),
1613 p(cy - r * 0.12)
1614 );
1615 format!(
1616 r##"<polygon points="{outer}" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
1617 <polygon points="{inner}" fill="#0b0b1a" opacity="0.9"/>"##
1618 )
1619 },
1620 Cat::Sfx => {
1622 format!(
1623 r##"<polygon points="{},{} {},{} {},{} {},{} {},{} {},{}" fill="{c}" opacity="0.85"/>"##,
1624 p(cx + r * 0.2),
1625 p(cy - r * 0.9),
1626 p(cx - r * 0.5),
1627 p(cy + r * 0.1),
1628 p(cx - r * 0.05),
1629 p(cy + r * 0.1),
1630 p(cx - r * 0.2),
1631 p(cy + r * 0.9),
1632 p(cx + r * 0.5),
1633 p(cy - r * 0.1),
1634 p(cx + r * 0.05),
1635 p(cy - r * 0.1)
1636 )
1637 },
1638 Cat::Spectrum => {
1639 let hs = [0.5f32, 0.9, 0.35, 0.7, 0.55];
1640 let mut s = String::new();
1641 for (i, &hh) in hs.iter().enumerate() {
1642 let xx = cx - r * 0.8 + i as f32 * r * 0.4;
1643 let bh = r * 1.6 * hh;
1644 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.8"/>"##,
1645 p(xx - r*0.12), p(cy + r*0.8 - bh), p(r*0.24), p(bh)).ok();
1646 }
1647 s
1648 },
1649 Cat::Music => {
1651 format!(
1652 r##"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{c}" opacity="0.85" transform="rotate(-20,{},{})"/>
1653 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1654 <path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1655 p(cx - r * 0.35),
1656 p(cy + r * 0.55),
1657 p(r * 0.32),
1658 p(r * 0.24),
1659 p(cx - r * 0.35),
1660 p(cy + r * 0.55),
1661 p(cx - r * 0.05),
1662 p(cy + r * 0.5),
1663 p(cx - r * 0.05),
1664 p(cy - r * 0.8),
1665 p(cx - r * 0.05),
1666 p(cy - r * 0.8),
1667 p(cx + r * 0.5),
1668 p(cy - r * 0.65),
1669 p(cx + r * 0.55),
1670 p(cy - r * 0.2),
1671 p(cx + r * 0.3),
1672 p(cy - r * 0.05)
1673 )
1674 },
1675 Cat::Note => {
1676 format!(
1677 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
1678 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
1679 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
1680 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
1681 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="2.0" opacity="0.85"/>"##,
1682 p(cx - r * 0.55),
1683 p(cy + r * 0.55),
1684 p(r * 0.24),
1685 p(cx + r * 0.55),
1686 p(cy + r * 0.3),
1687 p(r * 0.24),
1688 p(cx - r * 0.33),
1689 p(cy + r * 0.55),
1690 p(cx - r * 0.33),
1691 p(cy - r * 0.6),
1692 p(cx + r * 0.77),
1693 p(cy + r * 0.3),
1694 p(cx + r * 0.77),
1695 p(cy - r * 0.85),
1696 p(cx - r * 0.33),
1697 p(cy - r * 0.6),
1698 p(cx + r * 0.77),
1699 p(cy - r * 0.85)
1700 )
1701 },
1702 Cat::Hash => {
1704 let mut s = String::new();
1705 for i in 0..3 {
1706 let rr = r * (0.35 + i as f32 * 0.22);
1707 write!(s, r##"<path d="M {},{} A {},{} 0 1,1 {},{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="{:.2}"/>"##,
1708 p(cx - rr), p(cy), p(rr), p(rr), p(cx + rr), p(cy), 0.85 - i as f32*0.18).ok();
1709 }
1710 write!(
1711 s,
1712 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
1713 p(cx),
1714 p(cy),
1715 p(r * 0.12)
1716 )
1717 .ok();
1718 s
1719 },
1720 Cat::Cipher => {
1721 format!(
1722 r##"<path d="M {},{} A {},{} 0 0,1 {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.85"/>
1723 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.1"/>
1724 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
1725 p(cx - r * 0.45),
1726 p(cy - r * 0.05),
1727 p(r * 0.45),
1728 p(r * 0.45),
1729 p(cx + r * 0.45),
1730 p(cy - r * 0.05),
1731 p(cx - r * 0.7),
1732 p(cy - r * 0.05),
1733 p(r * 1.4),
1734 p(r * 0.95),
1735 p(cx),
1736 p(cy + r * 0.4),
1737 p(r * 0.14)
1738 )
1739 },
1740 Cat::Sign => {
1741 format!(
1742 r##"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>
1743 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.5"/>"##,
1744 p(cx - r * 0.9),
1745 p(cy + r * 0.4),
1746 p(cx - r * 0.4),
1747 p(cy - r * 0.7),
1748 p(cx),
1749 p(cy + r * 0.6),
1750 p(cx + r * 0.3),
1751 p(cy - r * 0.1),
1752 p(cx + r * 0.7),
1753 p(cy - r * 0.7),
1754 p(cx + r * 0.9),
1755 p(cy + r * 0.3),
1756 p(cx - r),
1757 p(cy + r * 0.75),
1758 p(cx + r),
1759 p(cy + r * 0.75)
1760 )
1761 },
1762 Cat::Kem => {
1763 format!(
1764 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1765 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1766 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1767 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1768 p(cx - r * 0.5),
1769 p(cy - r * 0.4),
1770 p(r * 0.4),
1771 p(cx - r * 0.18),
1772 p(cy - r * 0.12),
1773 p(cx + r * 0.8),
1774 p(cy + r * 0.75),
1775 p(cx + r * 0.55),
1776 p(cy + r * 0.5),
1777 p(cx + r * 0.8),
1778 p(cy + r * 0.25),
1779 p(cx + r * 0.8),
1780 p(cy + r * 0.75),
1781 p(cx + r * 1.05),
1782 p(cy + r * 0.5)
1783 )
1784 },
1785 Cat::Shard => {
1786 format!(
1787 r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.5"/>
1788 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3"/>
1789 <polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
1790 p(cx),
1791 p(cy - r),
1792 p(cx - r * 0.85),
1793 p(cy + r * 0.5),
1794 p(cx),
1795 p(cy + r * 0.2),
1796 p(cx),
1797 p(cy - r),
1798 p(cx + r * 0.85),
1799 p(cy + r * 0.5),
1800 p(cx),
1801 p(cy + r * 0.2),
1802 p(cx),
1803 p(cy - r),
1804 p(cx + r * 0.85),
1805 p(cy + r * 0.5),
1806 p(cx - r * 0.85),
1807 p(cy + r * 0.5)
1808 )
1809 },
1810 Cat::Rigid => {
1812 format!(
1813 r##"<polygon points="{},{} {},{} {},{} {},{}" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.0"/>
1814 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.2" stroke="{c}" stroke-width="1.0"/>
1815 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.28" stroke="{c}" stroke-width="1.0"/>"##,
1816 p(cx),
1817 p(cy - r * 0.9),
1818 p(cx + r * 0.85),
1819 p(cy - r * 0.4),
1820 p(cx),
1821 p(cy + r * 0.1),
1822 p(cx - r * 0.85),
1823 p(cy - r * 0.4),
1824 p(cx - r * 0.85),
1825 p(cy - r * 0.4),
1826 p(cx),
1827 p(cy + r * 0.1),
1828 p(cx),
1829 p(cy + r * 0.95),
1830 p(cx - r * 0.85),
1831 p(cy + r * 0.45),
1832 p(cx + r * 0.85),
1833 p(cy - r * 0.4),
1834 p(cx),
1835 p(cy + r * 0.1),
1836 p(cx),
1837 p(cy + r * 0.95),
1838 p(cx + r * 0.85),
1839 p(cy + r * 0.45)
1840 )
1841 },
1842 Cat::Soft => {
1843 format!(
1844 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.45" stroke="{c}" stroke-width="1.0"/>"##,
1845 p(cx),
1846 p(cy - r * 0.85),
1847 p(cx + r * 0.7),
1848 p(cy - r * 0.9),
1849 p(cx + r * 0.95),
1850 p(cy - r * 0.2),
1851 p(cx + r * 0.8),
1852 p(cy + r * 0.4),
1853 p(cx + r * 0.6),
1854 p(cy + r * 0.95),
1855 p(cx - r * 0.1),
1856 p(cy + r * 0.9),
1857 p(cx - r * 0.6),
1858 p(cy + r * 0.7),
1859 p(cx - r * 0.95),
1860 p(cy + r * 0.5),
1861 p(cx - r * 0.9),
1862 p(cy - r * 0.3),
1863 p(cx - r * 0.7),
1864 p(cy - r * 0.6),
1865 p(cx - r * 0.5),
1866 p(cy - r * 0.85),
1867 p(cx - r * 0.2),
1868 p(cy - r * 0.95),
1869 p(cx),
1870 p(cy - r * 0.85)
1871 )
1872 },
1873 Cat::Liquid => {
1874 format!(
1875 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.0"/>
1876 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#ffffff" opacity="0.25"/>"##,
1877 p(cx),
1878 p(cy - r * 0.9),
1879 p(cx + r * 0.75),
1880 p(cy - r * 0.1),
1881 p(cx + r * 0.6),
1882 p(cy + r * 0.8),
1883 p(cx),
1884 p(cy + r * 0.85),
1885 p(cx - r * 0.6),
1886 p(cy + r * 0.8),
1887 p(cx - r * 0.75),
1888 p(cy - r * 0.1),
1889 p(cx),
1890 p(cy - r * 0.9),
1891 p(cx - r * 0.25),
1892 p(cy + r * 0.25),
1893 p(r * 0.16),
1894 p(r * 0.28)
1895 )
1896 },
1897 Cat::Force => {
1898 format!(
1899 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.8" opacity="0.9" stroke-linecap="round"/>
1900 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"##,
1901 p(cx - r * 0.7),
1902 p(cy + r * 0.7),
1903 p(cx + r * 0.45),
1904 p(cy - r * 0.45),
1905 p(cx + r * 0.8),
1906 p(cy - r * 0.8),
1907 p(cx + r * 0.2),
1908 p(cy - r * 0.7),
1909 p(cx + r * 0.7),
1910 p(cy - r * 0.15)
1911 )
1912 },
1913 Cat::Collide => {
1914 let mut s = String::new();
1915 for i in 0..8 {
1916 let a = (i as f32 * 45.0).to_radians();
1917 let r1 = r * 0.3;
1918 let r2 = if i % 2 == 0 { r * 0.95 } else { r * 0.6 };
1919 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1920 p(cx + r1*a.cos()), p(cy + r1*a.sin()), p(cx + r2*a.cos()), p(cy + r2*a.sin())).ok();
1921 }
1922 write!(
1923 s,
1924 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
1925 p(cx),
1926 p(cy),
1927 p(r * 0.18)
1928 )
1929 .ok();
1930 s
1931 },
1932 Cat::Mesh => {
1934 format!(
1935 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
1936 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
1937 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
1938 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.9" opacity="0.6"/>"##,
1939 p(cx),
1940 p(cy),
1941 p(r * 0.9),
1942 p(cx),
1943 p(cy),
1944 p(r * 0.38),
1945 p(r * 0.9),
1946 p(cx),
1947 p(cy),
1948 p(r * 0.9),
1949 p(r * 0.38),
1950 p(cx - r * 0.9),
1951 p(cy),
1952 p(cx + r * 0.9),
1953 p(cy)
1954 )
1955 },
1956 Cat::Draw3D => {
1957 format!(
1958 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1959 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.75"/>
1960 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.6"/>
1961 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1962 p(cx),
1963 p(cy + r * 0.3),
1964 p(cx),
1965 p(cy - r * 0.9),
1966 p(cx),
1967 p(cy + r * 0.3),
1968 p(cx + r * 0.9),
1969 p(cy + r * 0.7),
1970 p(cx),
1971 p(cy + r * 0.3),
1972 p(cx - r * 0.85),
1973 p(cy + r * 0.6),
1974 p(cx),
1975 p(cy + r * 0.3),
1976 p(r * 0.13)
1977 )
1978 },
1979 Cat::Draw2D => {
1980 format!(
1981 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9" stroke-linecap="round"/>
1982 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>
1983 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1984 p(cx - r * 0.8),
1985 p(cy + r * 0.8),
1986 p(cx + r * 0.8),
1987 p(cy - r * 0.8),
1988 p(cx - r * 0.8),
1989 p(cy + r * 0.8),
1990 p(r * 0.16),
1991 p(cx + r * 0.8),
1992 p(cy - r * 0.8),
1993 p(r * 0.16)
1994 )
1995 },
1996 Cat::Trig => {
1998 format!(
1999 r##"<polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
2000 <path d="M {},{} A {},{} 0 0,0 {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2001 p(cx - r * 0.8),
2002 p(cy + r * 0.7),
2003 p(cx + r * 0.8),
2004 p(cy + r * 0.7),
2005 p(cx - r * 0.8),
2006 p(cy - r * 0.7),
2007 p(cx - r * 0.35),
2008 p(cy + r * 0.7),
2009 p(r * 0.45),
2010 p(r * 0.45),
2011 p(cx - r * 0.8),
2012 p(cy + r * 0.38)
2013 )
2014 },
2015 Cat::MathFn => {
2016 format!(
2017 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
2018 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
2019 <path d="M {},{} Q {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
2020 p(cx - r * 0.9),
2021 p(cy),
2022 p(cx + r * 0.9),
2023 p(cy),
2024 p(cx),
2025 p(cy - r * 0.9),
2026 p(cx),
2027 p(cy + r * 0.9),
2028 p(cx - r * 0.8),
2029 p(cy - r * 0.7),
2030 p(cx),
2031 p(cy + r * 1.1),
2032 p(cx + r * 0.8),
2033 p(cy - r * 0.7)
2034 )
2035 },
2036 Cat::Noise => {
2037 format!(
2038 r##"<polyline points="{},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
2039 p(cx - r * 0.9),
2040 p(cy + r * 0.1),
2041 p(cx - r * 0.65),
2042 p(cy - r * 0.6),
2043 p(cx - r * 0.4),
2044 p(cy + r * 0.5),
2045 p(cx - r * 0.15),
2046 p(cy - r * 0.4),
2047 p(cx + r * 0.1),
2048 p(cy + r * 0.7),
2049 p(cx + r * 0.35),
2050 p(cy - r * 0.5),
2051 p(cx + r * 0.6),
2052 p(cy + r * 0.3),
2053 p(cx + r * 0.8),
2054 p(cy - r * 0.3),
2055 p(cx + r * 0.95),
2056 p(cy + r * 0.2)
2057 )
2058 },
2059 Cat::Net => {
2061 let mut s = String::new();
2062 let nodes = [(0.0f32, -0.85f32), (0.8, 0.4), (-0.8, 0.4)];
2063 for &(nx, ny) in nodes.iter() {
2064 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
2065 p(cx), p(cy), p(cx + nx*r), p(cy + ny*r)).ok();
2066 }
2067 write!(
2068 s,
2069 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
2070 p(cx),
2071 p(cy),
2072 p(r * 0.2)
2073 )
2074 .ok();
2075 for &(nx, ny) in nodes.iter() {
2076 write!(
2077 s,
2078 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
2079 p(cx + nx * r),
2080 p(cy + ny * r),
2081 p(r * 0.18)
2082 )
2083 .ok();
2084 }
2085 s
2086 },
2087 Cat::Neural => {
2088 let mut s = String::new();
2089 let l0 = [-0.6f32, 0.0, 0.6];
2090 let l1 = [-0.3f32, 0.3];
2091 let x0 = cx - r * 0.7;
2092 let x1 = cx;
2093 let x2 = cx + r * 0.7;
2094 for &a in l0.iter() {
2095 for &b in l1.iter() {
2096 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
2097 p(x0), p(cy + a*r), p(x1), p(cy + b*r)).ok();
2098 }
2099 }
2100 for &b in l1.iter() {
2101 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
2102 p(x1), p(cy + b*r), p(x2), p(cy)).ok();
2103 }
2104 for &a in l0.iter() {
2105 write!(
2106 s,
2107 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2108 p(x0),
2109 p(cy + a * r),
2110 p(r * 0.14)
2111 )
2112 .ok();
2113 }
2114 for &b in l1.iter() {
2115 write!(
2116 s,
2117 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2118 p(x1),
2119 p(cy + b * r),
2120 p(r * 0.14)
2121 )
2122 .ok();
2123 }
2124 write!(
2125 s,
2126 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2127 p(x2),
2128 p(cy),
2129 p(r * 0.14)
2130 )
2131 .ok();
2132 s
2133 },
2134 Cat::Behavior => {
2135 format!(
2136 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2137 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2138 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
2139 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>
2140 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
2141 p(cx),
2142 p(cy - r * 0.6),
2143 p(cx - r * 0.7),
2144 p(cy + r * 0.6),
2145 p(cx),
2146 p(cy - r * 0.6),
2147 p(cx + r * 0.7),
2148 p(cy + r * 0.6),
2149 p(cx),
2150 p(cy - r * 0.6),
2151 p(r * 0.2),
2152 p(cx - r * 0.7),
2153 p(cy + r * 0.6),
2154 p(r * 0.18),
2155 p(cx + r * 0.7),
2156 p(cy + r * 0.6),
2157 p(r * 0.18)
2158 )
2159 },
2160 Cat::Widget => {
2162 format!(
2163 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
2164 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
2165 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"##,
2166 p(cx - r * 0.85),
2167 p(cy - r * 0.75),
2168 p(r * 1.7),
2169 p(r * 1.5),
2170 p(cx - r * 0.85),
2171 p(cy - r * 0.3),
2172 p(cx + r * 0.85),
2173 p(cy - r * 0.3),
2174 p(cx - r * 0.5),
2175 p(cy + r * 0.1),
2176 p(r * 1.0),
2177 p(r * 0.45)
2178 )
2179 },
2180 Cat::Hud => {
2181 format!(
2182 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
2183 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.6"/>
2184 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.6"/>
2185 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9"/>
2186 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
2187 p(cx),
2188 p(cy),
2189 p(r * 0.9),
2190 p(cx - r * 0.9),
2191 p(cy),
2192 p(cx + r * 0.9),
2193 p(cy),
2194 p(cx),
2195 p(cy - r * 0.9),
2196 p(cx),
2197 p(cy + r * 0.9),
2198 p(cx),
2199 p(cy),
2200 p(cx + r * 0.64),
2201 p(cy - r * 0.64),
2202 p(cx + r * 0.4),
2203 p(cy - r * 0.4),
2204 p(r * 0.13)
2205 )
2206 },
2207 Cat::Dialog => {
2208 format!(
2209 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="4" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
2210 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.4"/>
2211 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
2212 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
2213 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>"##,
2214 p(cx - r * 0.9),
2215 p(cy - r * 0.8),
2216 p(r * 1.8),
2217 p(r * 1.2),
2218 p(cx - r * 0.4),
2219 p(cy + r * 0.4),
2220 p(cx - r * 0.1),
2221 p(cy + r * 0.4),
2222 p(cx - r * 0.55),
2223 p(cy + r * 0.9),
2224 p(cx - r * 0.4),
2225 p(cy - r * 0.2),
2226 p(r * 0.1),
2227 p(cx),
2228 p(cy - r * 0.2),
2229 p(r * 0.1),
2230 p(cx + r * 0.4),
2231 p(cy - r * 0.2),
2232 p(r * 0.1)
2233 )
2234 },
2235 Cat::Holo => {
2236 let mut s = String::new();
2237 for i in 0..3 {
2238 let off = (i as f32 - 1.0) * r * 0.22;
2239 write!(s, r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3" stroke="{c}" stroke-width="0.8" stroke-opacity="0.7"/>"##,
2240 p(cx + off), p(cy - r*0.8), p(cx + off + r*0.8), p(cy + r*0.6), p(cx + off - r*0.8), p(cy + r*0.6)).ok();
2241 }
2242 s
2243 },
2244 Cat::Str => {
2246 let mut s = String::new();
2247 for &dx in [-0.5f32, 0.3].iter() {
2248 write!(
2249 s,
2250 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
2251 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2252 p(cx + dx * r),
2253 p(cy - r * 0.4),
2254 p(r * 0.16),
2255 p(cx + dx * r + r * 0.3),
2256 p(cy - r * 0.4),
2257 p(r * 0.16)
2258 )
2259 .ok();
2260 }
2261 for j in 0..2 {
2262 let yy = cy + r * 0.3 + j as f32 * r * 0.45;
2263 let ww = if j == 0 { r * 1.6 } else { r * 1.0 };
2264 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.6"/>"##,
2265 p(cx - r*0.8), p(yy), p(cx - r*0.8 + ww), p(yy)).ok();
2266 }
2267 s
2268 },
2269 Cat::Font => {
2270 format!(
2271 r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9" stroke-linejoin="round"/>
2272 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
2273 p(cx - r * 0.7),
2274 p(cy + r * 0.8),
2275 p(cx),
2276 p(cy - r * 0.85),
2277 p(cx + r * 0.7),
2278 p(cy + r * 0.8),
2279 p(cx - r * 0.38),
2280 p(cy + r * 0.15),
2281 p(cx + r * 0.38),
2282 p(cy + r * 0.15)
2283 )
2284 },
2285 Cat::Vector => {
2286 format!(
2287 r##"<path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
2288 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
2289 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
2290 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
2291 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
2292 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2293 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2294 p(cx - r * 0.8),
2295 p(cy + r * 0.7),
2296 p(cx - r * 0.3),
2297 p(cy - r * 0.9),
2298 p(cx + r * 0.3),
2299 p(cy - r * 0.9),
2300 p(cx + r * 0.8),
2301 p(cy + r * 0.7),
2302 p(cx - r * 0.8),
2303 p(cy + r * 0.7),
2304 p(cx - r * 0.3),
2305 p(cy - r * 0.9),
2306 p(cx + r * 0.8),
2307 p(cy + r * 0.7),
2308 p(cx + r * 0.3),
2309 p(cy - r * 0.9),
2310 p(cx - r * 0.9),
2311 p(cy + r * 0.6),
2312 p(r * 0.2),
2313 p(r * 0.2),
2314 p(cx + r * 0.7),
2315 p(cy + r * 0.6),
2316 p(r * 0.2),
2317 p(r * 0.2),
2318 p(cx - r * 0.3),
2319 p(cy - r * 0.9),
2320 p(r * 0.14),
2321 p(cx + r * 0.3),
2322 p(cy - r * 0.9),
2323 p(r * 0.14)
2324 )
2325 },
2326 Cat::Color => {
2328 format!(
2329 r##"<circle cx="{}" cy="{}" r="{}" fill="#ff5e8a" opacity="0.5"/>
2330 <circle cx="{}" cy="{}" r="{}" fill="#50fa7b" opacity="0.5"/>
2331 <circle cx="{}" cy="{}" r="{}" fill="#6ab0f5" opacity="0.5"/>"##,
2332 p(cx),
2333 p(cy - r * 0.4),
2334 p(r * 0.6),
2335 p(cx - r * 0.45),
2336 p(cy + r * 0.35),
2337 p(r * 0.6),
2338 p(cx + r * 0.45),
2339 p(cy + r * 0.35),
2340 p(r * 0.6)
2341 )
2342 },
2343 Cat::Shade => {
2344 format!(
2345 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25" stroke="{c}" stroke-width="1.0"/>
2346 <path d="M {},{} A {},{} 0 0,1 {},{} A {},{} 0 0,0 {},{} Z" fill="{c}" opacity="0.6"/>
2347 <circle cx="{}" cy="{}" r="{}" fill="#ffffff" opacity="0.3"/>"##,
2348 p(cx),
2349 p(cy),
2350 p(r * 0.9),
2351 p(cx),
2352 p(cy - r * 0.9),
2353 p(r * 0.9),
2354 p(r * 0.9),
2355 p(cx),
2356 p(cy + r * 0.9),
2357 p(r * 0.45),
2358 p(r * 0.9),
2359 p(cx),
2360 p(cy - r * 0.9),
2361 p(cx + r * 0.35),
2362 p(cy - r * 0.35),
2363 p(r * 0.18)
2364 )
2365 },
2366 Cat::Window => {
2367 format!(
2368 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
2369 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2370 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2371 p(cx - r * 0.85),
2372 p(cy - r * 0.85),
2373 p(r * 1.7),
2374 p(r * 1.7),
2375 p(cx),
2376 p(cy - r * 0.85),
2377 p(cx),
2378 p(cy + r * 0.85),
2379 p(cx - r * 0.85),
2380 p(cy),
2381 p(cx + r * 0.85),
2382 p(cy)
2383 )
2384 },
2385 Cat::Key => {
2387 format!(
2388 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="3" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.2"/>
2389 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2390 p(cx - r * 0.8),
2391 p(cy - r * 0.8),
2392 p(r * 1.6),
2393 p(r * 1.6),
2394 p(cx - r * 0.45),
2395 p(cy - r * 0.45),
2396 p(r * 0.9),
2397 p(r * 0.9)
2398 )
2399 },
2400 Cat::Mouse => {
2401 format!(
2402 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
2403 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
2404 <rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.85"/>"##,
2405 p(cx - r * 0.55),
2406 p(cy - r * 0.85),
2407 p(r * 1.1),
2408 p(r * 1.7),
2409 p(r * 0.55),
2410 p(cx),
2411 p(cy - r * 0.85),
2412 p(cx),
2413 p(cy - r * 0.1),
2414 p(cx - r * 0.08),
2415 p(cy - r * 0.7),
2416 p(r * 0.16),
2417 p(r * 0.4)
2418 )
2419 },
2420 Cat::Print => {
2422 let mut s = format!(
2423 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1.5" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>"##,
2424 p(cx - r * 0.6),
2425 p(cy - r * 0.85),
2426 p(r * 1.2),
2427 p(r * 1.7)
2428 );
2429 for j in 0..4 {
2430 let yy = cy - r * 0.45 + j as f32 * r * 0.35;
2431 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
2432 p(cx - r*0.4), p(yy), p(cx + r*0.4), p(yy)).ok();
2433 }
2434 s
2435 },
2436 Cat::File => {
2437 format!(
2438 r##"<path d="M {},{} L {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.1"/>
2439 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
2440 p(cx - r * 0.6),
2441 p(cy - r * 0.85),
2442 p(cx + r * 0.25),
2443 p(cy - r * 0.85),
2444 p(cx + r * 0.6),
2445 p(cy - r * 0.45),
2446 p(cx + r * 0.6),
2447 p(cy + r * 0.85),
2448 p(cx - r * 0.6),
2449 p(cy + r * 0.85),
2450 p(cx + r * 0.25),
2451 p(cy - r * 0.85),
2452 p(cx + r * 0.25),
2453 p(cy - r * 0.45),
2454 p(cx + r * 0.6),
2455 p(cy - r * 0.45)
2456 )
2457 },
2458 Cat::Sys => {
2459 format!(
2460 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.8"/>
2461 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round" stroke-linejoin="round"/>
2462 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round"/>"##,
2463 p(cx - r * 0.9),
2464 p(cy - r * 0.7),
2465 p(r * 1.8),
2466 p(r * 1.4),
2467 p(cx - r * 0.45),
2468 p(cy - r * 0.2),
2469 p(cx - r * 0.1),
2470 p(cy + r * 0.15),
2471 p(cx - r * 0.45),
2472 p(cy + r * 0.5),
2473 p(cx + r * 0.1),
2474 p(cy + r * 0.5),
2475 p(cx + r * 0.5),
2476 p(cy + r * 0.5)
2477 )
2478 },
2479 Cat::Anim => {
2480 let mut s = String::new();
2481 for i in 0..6 {
2482 let a = (i as f32 * 60.0).to_radians();
2483 let rad = r * 0.7;
2484 let dotr = r * (0.08 + 0.16 * (i as f32 / 6.0));
2485 write!(
2486 s,
2487 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="{:.2}"/>"##,
2488 p(cx + rad * a.cos()),
2489 p(cy + rad * a.sin()),
2490 p(dotr),
2491 0.35 + i as f32 * 0.1
2492 )
2493 .ok();
2494 }
2495 s
2496 },
2497 Cat::Const => {
2498 format!(
2499 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2"
2500 fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
2501 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.1" opacity="0.5"/>"#,
2502 p(cx - r * 0.7),
2503 p(cy - r * 0.55),
2504 p(r * 1.4),
2505 p(r * 1.1),
2506 p(cx - r * 0.4),
2507 p(cy),
2508 p(cx + r * 0.4),
2509 p(cy)
2510 )
2511 },
2512 }
2513}
2514
2515fn card_height(card: &FuncCard) -> f32 {
2518 let icon_rows = (card.calls.len() + ICONS_ROW - 1).max(1) / ICONS_ROW + 1;
2519 let base = CARD_PAD * 2.0 + 32.0 + 20.0; base + icon_rows as f32 * (ICON_SZ + ICON_GAP) + ICON_GAP
2523}
2524
2525fn render_card(card: &FuncCard, x: f32, y: f32) -> String {
2526 let h = card_height(card);
2527 let w = CARD_W;
2528 let r = CARD_ROUNDING;
2529
2530 let dominant = card
2532 .calls
2533 .iter()
2534 .max_by_key(|c| c.count)
2535 .map(|c| c.cat.color())
2536 .unwrap_or(Cat::User.color());
2537
2538 let border_color = if card.is_entry { GOLD } else { dominant };
2539 let border_w = if card.is_entry { 2.5 } else { 1.2 };
2540 let glow_filter = if card.is_entry {
2541 r#" filter="url(#glow-gold)""#
2542 } else {
2543 ""
2544 };
2545
2546 let mut s = String::new();
2547
2548 write!(s, r#"<rect x="{}" y="{}" width="{w}" height="{h}" rx="{r}"
2550 fill="{CARD_BG}" stroke="{border_color}" stroke-width="{border_w}"{glow_filter}/>
2551 <line x1="{}" y1="{}" x2="{}" y2="{}"
2552 stroke="{border_color}" stroke-width="3" opacity="0.6"/>
2553"#,
2554 p(x), p(y),
2555 p(x+r), p(y+h), p(x+r), p(y) ).ok();
2557
2558 let name_y = y + CARD_PAD + 18.0;
2560 let entry_badge = if card.is_entry {
2561 format!(
2562 r#" <text x="{}" y="{}" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.8">⬡ ENTRY</text>"#,
2563 p(x + w - CARD_PAD - 48.0),
2564 p(name_y)
2565 )
2566 } else {
2567 String::new()
2568 };
2569
2570 write!(
2571 s,
2572 r#"<text x="{}" y="{}" fill="{}" font-size="13" font-weight="bold">{}</text>{}"#,
2573 p(x + CARD_PAD + 10.0),
2574 p(name_y),
2575 if card.is_entry { GOLD } else { TEXT },
2576 xe(&card.name),
2577 entry_badge
2578 )
2579 .ok();
2580
2581 let stats_y = name_y + 18.0;
2583 let params_str = if card.params.is_empty() {
2584 String::new()
2585 } else {
2586 format!("({})", card.params.join(", "))
2587 };
2588 let stats_str = {
2589 let mut parts = Vec::new();
2590 if card.vtex_count > 0 {
2591 parts.push(format!("{} vtex", card.vtex_count));
2592 }
2593 if card.audio_count > 0 {
2594 parts.push(format!("{} audio", card.audio_count));
2595 }
2596 if card.has_loop {
2597 parts.push("↺ loop".into());
2598 }
2599 parts.join(" · ")
2600 };
2601 write!(
2602 s,
2603 r#"<text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{}</text>
2604 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10" text-anchor="end">{}</text>
2605"#,
2606 p(x + CARD_PAD + 10.0),
2607 p(stats_y),
2608 xe(¶ms_str),
2609 p(x + w - CARD_PAD),
2610 p(stats_y),
2611 xe(&stats_str)
2612 )
2613 .ok();
2614
2615 let div_y = stats_y + 6.0;
2617 write!(
2618 s,
2619 r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{CARD_BD}" stroke-width="1"/>"#,
2620 p(x + CARD_PAD),
2621 p(div_y),
2622 p(x + w - CARD_PAD),
2623 p(div_y)
2624 )
2625 .ok();
2626
2627 let icon_y0 = div_y + ICON_GAP + ICON_SZ / 2.0;
2629 let icon_x0 = x + CARD_PAD + ICON_SZ / 2.0 + 6.0;
2630 let ir = ICON_SZ / 2.0 * 0.82; for (i, call) in card.calls.iter().enumerate() {
2633 let row = i / ICONS_ROW;
2634 let col = i % ICONS_ROW;
2635 let ix = icon_x0 + col as f32 * (ICON_SZ + ICON_GAP);
2636 let iy = icon_y0 + row as f32 * (ICON_SZ + ICON_GAP);
2637
2638 write!(
2640 s,
2641 r#"<rect x="{}" y="{}" width="{ICON_SZ}" height="{ICON_SZ}" rx="3"
2642 fill="{}" opacity="0.12"/>
2643"#,
2644 p(ix - ICON_SZ / 2.0),
2645 p(iy - ICON_SZ / 2.0),
2646 call.cat.color()
2647 )
2648 .ok();
2649
2650 s.push_str(&icon(call.cat, ix, iy, ir));
2652
2653 if call.count > 1 {
2655 write!(s, r##"<rect x="{}" y="{}" width="13" height="10" rx="3" fill="{}" opacity="0.9"/>
2656 <text x="{}" y="{}" fill="#0a0a1a" font-size="8" font-weight="bold" text-anchor="middle">{}</text>"##,
2657 p(ix + ir - 2.0), p(iy - ir - 1.0), call.cat.color(),
2658 p(ix + ir + 4.5), p(iy - ir + 7.0), call.count
2659 ).ok();
2660 }
2661 }
2662
2663 s
2664}
2665
2666fn render_header(filename: &str, funcs: &[FuncCard]) -> String {
2669 let name = std::path::Path::new(filename)
2670 .file_name()
2671 .map(|n| n.to_string_lossy().into_owned())
2672 .unwrap_or_else(|| filename.to_string());
2673 let n_fns = funcs.len();
2674 let count = |pred: fn(Cat) -> bool| -> usize {
2675 funcs
2676 .iter()
2677 .flat_map(|f| &f.calls)
2678 .filter(|c| pred(c.cat))
2679 .map(|c| c.count)
2680 .sum()
2681 };
2682 let n_vtex = count(is_vtex);
2683 let n_audio = count(is_audio);
2684 let n_crypto = count(is_crypto);
2685 let n_physics = count(is_physics);
2686 let n_ai = count(is_ai);
2687 let n_calls: usize = funcs.iter().map(|f| f.calls.len()).sum();
2688
2689 let mut stats = vec![format!("{n_fns} fn"), format!("{n_calls} call types")];
2691 for (n, lbl) in [
2692 (n_vtex, "vtex"),
2693 (n_audio, "audio"),
2694 (n_crypto, "crypto"),
2695 (n_physics, "physics"),
2696 (n_ai, "ai"),
2697 ] {
2698 if n > 0 {
2699 stats.push(format!("{n} {lbl}"));
2700 }
2701 }
2702
2703 format!(
2704 r##"<rect x="0" y="0" width="{SVG_W}" height="{HEADER_H}" fill="#080814"/>
2705 <line x1="0" y1="{HEADER_H}" x2="{SVG_W}" y2="{HEADER_H}" stroke="#1a1a3a" stroke-width="1"/>
2706 <text x="20" y="22" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.6" letter-spacing="2">LING VISUALIZER</text>
2707 <text x="20" y="56" fill="{TEXT}" font-size="26" font-weight="bold">{}</text>
2708 <text x="{}" y="56" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"##,
2709 xe(&name),
2710 SVG_W - 20.0,
2711 stats.join(" · ")
2712 )
2713}
2714
2715fn render_legend(funcs: &[FuncCard]) -> String {
2716 let mut present: Vec<Cat> = Vec::new();
2719 let mut seen = HashSet::new();
2720 for f in funcs {
2721 for c in &f.calls {
2722 if seen.insert(c.cat) {
2723 present.push(c.cat);
2724 }
2725 }
2726 }
2727 present.sort_by(|a, b| a.domain().cmp(b.domain()).then(a.label().cmp(b.label())));
2728
2729 let mut s = format!(
2730 r##"<rect x="0" y="{HEADER_H}" width="{SVG_W}" height="{LEGEND_H}" fill="#0a0a18"/>
2731 <line x1="0" y1="{}" x2="{SVG_W}" y2="{}" stroke="#171730" stroke-width="1"/>"##,
2732 HEADER_H + LEGEND_H,
2733 HEADER_H + LEGEND_H
2734 );
2735
2736 let row_h = 21.0;
2737 let x_start = GRID_X + 8.0;
2738 let x_max = SVG_W - 24.0;
2739 let mut lx = x_start;
2740 let mut row = 0usize;
2741 let mut cur_domain = "";
2742 let row_y = |r: usize| HEADER_H + 17.0 + r as f32 * row_h;
2743
2744 for cat in present {
2745 let c = cat.color();
2746 let lbl = cat.label();
2747 let dom = cat.domain();
2748
2749 if dom != cur_domain {
2751 let tag_w = dom.len() as f32 * 6.0 + 12.0;
2752 if lx > x_start && lx + tag_w > x_max {
2753 row += 1;
2754 lx = x_start;
2755 }
2756 if row > 1 {
2757 break;
2758 } let ly = row_y(row);
2760 s.push_str(&format!(
2761 r##"<text x="{}" y="{}" fill="#3f3f66" font-size="8" font-weight="bold" letter-spacing="1">{}</text>"##,
2762 p(lx), p(ly + 3.0), dom.to_uppercase()
2763 ));
2764 lx += tag_w;
2765 cur_domain = dom;
2766 }
2767
2768 let item_w = 20.0 + lbl.len() as f32 * 6.2 + 12.0;
2769 if lx + item_w > x_max {
2770 row += 1;
2771 lx = x_start;
2772 }
2773 if row > 1 {
2774 break;
2775 }
2776 let ly = row_y(row);
2777 let icon_svg = icon(cat, lx + 7.0, ly, 6.0);
2778 s.push_str(&format!(
2779 r#"<rect x="{}" y="{}" width="14" height="14" rx="3" fill="{c}" opacity="0.12"/>
2780 {}
2781 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{lbl}</text>"#,
2782 p(lx),
2783 p(ly - 7.0),
2784 icon_svg,
2785 p(lx + 18.0),
2786 p(ly + 4.0)
2787 ));
2788 lx += item_w;
2789 }
2790 s
2791}
2792
2793fn render_sidebar(globals: &[GlobalConst], total_h: f32) -> String {
2794 let h = total_h - CONTENT_Y;
2795 let mut s = format!(
2796 r##"<rect x="0" y="{CONTENT_Y}" width="{SIDEBAR_W}" height="{h}" fill="{SIDEBAR_BG}"/>
2797 <line x1="{SIDEBAR_W}" y1="{CONTENT_Y}" x2="{SIDEBAR_W}" y2="{total_h}" stroke="#1a1a3a" stroke-width="1"/>
2798 <text x="14" y="{}" fill="{TEXT_DIM}" font-size="9" font-weight="bold" letter-spacing="2">CONSTANTS</text>"##,
2799 CONTENT_Y + 20.0
2800 );
2801
2802 let mut gy = CONTENT_Y + 38.0;
2803 for g in globals {
2804 write!(
2806 s,
2807 r#"<rect x="{}" y="{}" width="8" height="8" rx="1" fill="{}" opacity="0.7"
2808 transform="rotate(45,{},{})"/>
2809 <text x="{}" y="{}" fill="{}" font-size="12" font-weight="bold">{}</text>
2810 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"#,
2811 p(14.0),
2812 p(gy - 7.0),
2813 Cat::Star.color(),
2814 p(18.0),
2815 p(gy - 3.0),
2816 p(28.0),
2817 p(gy),
2818 Cat::Grid.color(),
2819 xe(&g.name),
2820 p(SIDEBAR_W - 10.0),
2821 p(gy),
2822 xe(&g.value)
2823 )
2824 .ok();
2825 gy += 22.0;
2826 }
2827 s
2828}
2829
2830const DEFS: &str = r##"<defs>
2833 <filter id="glow-gold" x="-30%" y="-30%" width="160%" height="160%">
2834 <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
2835 <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
2836 </filter>
2837 <pattern id="grid-pat" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
2838 <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#ffffff" stroke-width="0.4"/>
2839 </pattern>
2840</defs>"##;
2841
2842pub fn render(filename: &str, program: &Program) -> String {
2845 let doc = Document::build(filename, program);
2846
2847 let heights: Vec<f32> = doc.funcs.iter().map(|f| card_height(f)).collect();
2849 let mut col_y = vec![CONTENT_Y; COLS];
2850 let mut positions: Vec<(f32, f32)> = Vec::with_capacity(doc.funcs.len());
2851
2852 for &h in &heights {
2853 let (col, &cy) = col_y
2854 .iter()
2855 .enumerate()
2856 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2857 .unwrap();
2858 let cx = GRID_X + col as f32 * (CARD_W + CARD_GAP);
2859 positions.push((cx, cy));
2860 col_y[col] += h + CARD_GAP;
2861 }
2862
2863 let total_h = col_y.iter().cloned().fold(0.0f32, f32::max) + 40.0;
2864
2865 let mut svg = String::new();
2866 write!(
2867 svg,
2868 r#"<?xml version="1.0" encoding="UTF-8"?>
2869<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_W}" height="{h}" viewBox="0 0 {SVG_W} {h}"
2870 style="font-family:'JetBrains Mono','Fira Code',monospace,sans-serif;background:{BG}">"#,
2871 h = total_h
2872 )
2873 .ok();
2874
2875 svg.push_str(DEFS);
2876
2877 write!(svg, r#"<rect width="{SVG_W}" height="{total_h}" fill="{BG}"/>
2879 <rect width="{SVG_W}" height="{total_h}" fill="url(#grid-pat)" opacity="0.05"/>"#,
2880 total_h = total_h).ok();
2881
2882 svg.push_str(&render_header(&doc.filename, &doc.funcs));
2883 svg.push_str(&render_legend(&doc.funcs));
2884 svg.push_str(&render_sidebar(&doc.globals, total_h));
2885
2886 for (card, &(cx, cy)) in doc.funcs.iter().zip(positions.iter()) {
2887 svg.push_str(&render_card(card, cx, cy));
2888 }
2889
2890 svg.push_str("\n</svg>");
2891 svg
2892}