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 | "set_vertex_normals"
588 ) {
589 return Cat::Draw3D;
590 }
591
592 if name.starts_with("nn_") {
594 return Cat::Neural;
595 }
596 if name.starts_with("bt_") {
597 return Cat::Behavior;
598 }
599 if name.starts_with("dialog_") {
600 return Cat::Dialog;
601 }
602
603 if name.starts_with("net_") {
605 return Cat::Net;
606 }
607
608 if matches!(
610 name,
611 "ui_radar"
612 | "ui_radar3d"
613 | "ui_minimap"
614 | "ui_compass"
615 | "ui_healthbar"
616 | "ui_reticle"
617 | "ui_target"
618 | "ui_gauge"
619 | "ui_gauge3d"
620 | "ui_vu"
621 | "ui_battery"
622 | "ui_segbar"
623 | "ui_bar"
624 | "ui_progress"
625 | "ui_cooldown"
626 | "ui_scanlines"
627 | "ui_vignette"
628 | "ui_spark"
629 | "ui_ring"
630 | "ui_counter"
631 ) {
632 return Cat::Hud;
633 }
634 if name.starts_with("ui_") {
635 return Cat::Widget;
636 }
637
638 if matches!(
640 name,
641 "holo_points" | "holo_fragment_count" | "knot_points" | "sparkle" | "neon" | "psychedelic"
642 ) {
643 return Cat::Holo;
644 }
645
646 if name.starts_with("font_") {
648 return Cat::Font;
649 }
650 if name.starts_with("svg_") {
651 return Cat::Vector;
652 }
653
654 if name.starts_with("str_")
656 || matches!(
657 name,
658 "split"
659 | "join"
660 | "trim"
661 | "substr"
662 | "format"
663 | "to_str"
664 | "num_str"
665 | "starts_with"
666 | "ends_with"
667 )
668 {
669 return Cat::Str;
670 }
671
672 if matches!(
674 name,
675 "sin"
676 | "cos"
677 | "tan"
678 | "asin"
679 | "acos"
680 | "atan"
681 | "atan2"
682 | "arcsin"
683 | "arccos"
684 | "arctan"
685 | "arctan2"
686 | "tanh"
687 | "tanhf"
688 | "hypot"
689 ) {
690 return Cat::Trig;
691 }
692 if matches!(
693 name,
694 "perlin" | "perlin3" | "noise2" | "fbm" | "vnoise" | "smoothstep"
695 ) {
696 return Cat::Noise;
697 }
698 if matches!(
699 name,
700 "sqrt"
701 | "cbrt"
702 | "pow"
703 | "exp"
704 | "ln"
705 | "log"
706 | "log2"
707 | "log10"
708 | "abs"
709 | "floor"
710 | "ceil"
711 | "round"
712 | "trunc"
713 | "fract"
714 | "sign"
715 | "clamp"
716 | "lerp"
717 | "min"
718 | "max"
719 | "rand"
720 | "pi"
721 | "tau"
722 ) {
723 return Cat::MathFn;
724 }
725
726 if matches!(
728 name,
729 "hsl_color"
730 | "hsv_to_rgb"
731 | "set_color"
732 | "set_color_hsl"
733 | "color"
734 | "lerp_color"
735 | "gfx_color"
736 ) {
737 return Cat::Color;
738 }
739 if matches!(
740 name,
741 "set_shade_mode"
742 | "set_rim"
743 | "set_fog"
744 | "set_ambient"
745 | "set_cel_bands"
746 | "set_blend"
747 | "set_shadow_color"
748 | "set_projection"
749 | "set_zdist"
750 ) {
751 return Cat::Shade;
752 }
753
754 if matches!(name, "set_camera" | "set_camera_pos" | "move_camera") {
756 return Cat::Camera;
757 }
758 if matches!(name, "add_light" | "clear_lights" | "keep_lights") {
759 return Cat::Light;
760 }
761
762 if matches!(
764 name,
765 "open_window"
766 | "open_fullscreen"
767 | "gfx_window"
768 | "fullscreen"
769 | "windowed"
770 | "wait_window"
771 | "is_open"
772 | "window_is_open"
773 | "gfx_is_open"
774 | "gfx_wait"
775 | "resolution"
776 | "get_width"
777 | "get_height"
778 ) {
779 return Cat::Window;
780 }
781 if matches!(
782 name,
783 "present" | "แสดงผล" | "gfx_present" | "render" | "render_3d" | "flush_3d"
784 ) {
785 return Cat::Present;
786 }
787 if matches!(name, "fill" | "เติม" | "gfx_fill" | "clear") {
788 return Cat::Fill;
789 }
790
791 if name.starts_with("draw_")
793 || matches!(
794 name,
795 "gfx_line" | "gfx_pixel" | "gfx_triangle" | "line" | "triangle" | "pixel"
796 )
797 {
798 return Cat::Draw2D;
799 }
800
801 if name.starts_with("mouse_") || matches!(name, "capture_mouse" | "release_mouse") {
803 return Cat::Mouse;
804 }
805 if name.starts_with("key_") || matches!(name, "keys" | "text_poll" | "text_get") {
806 return Cat::Key;
807 }
808
809 if matches!(
811 name,
812 "print" | "println" | "print_file" | "imprimir" | "afficher" | "вывести" | "พิมพ์" | "打印"
813 ) {
814 return Cat::Print;
815 }
816 if matches!(name, "read_file" | "write_file" | "อ่านไฟล์" | "เขียนไฟล์")
817 {
818 return Cat::File;
819 }
820 if matches!(
821 name,
822 "system" | "get_args" | "time_now" | "sleep" | "sleep_ms"
823 ) {
824 return Cat::Sys;
825 }
826
827 if matches!(
829 name,
830 "animation"
831 | "ease"
832 | "tick"
833 | "delta_time"
834 | "frame_count"
835 | "frame"
836 | "record_frame"
837 | "record_count"
838 | "tween"
839 | "tween_ease"
840 | "breathe"
841 | "wobble"
842 | "gait_phase"
843 | "gait_swing"
844 | "gait_lift"
845 | "spring_to"
846 | "ik2"
847 ) {
848 return Cat::Anim;
849 }
850 if matches!(
852 name,
853 "gear_couple" | "gear_train" | "cam_lift" | "piston" | "rack"
854 ) {
855 return Cat::Cog;
856 }
857
858 Cat::User
859}
860
861pub(crate) fn is_vtex(c: Cat) -> bool {
862 matches!(
863 c,
864 Cat::Rings
865 | Cat::Spiral
866 | Cat::Star
867 | Cat::Flower
868 | Cat::Lotus
869 | Cat::Chakra
870 | Cat::Yantra
871 | Cat::Hyper
872 | Cat::Tess
873 | Cat::Rain
874 | Cat::Grid
875 | Cat::Halftone
876 | Cat::Pagoda
877 | Cat::Torii
878 | Cat::Cog
879 )
880}
881pub(crate) fn is_audio(c: Cat) -> bool {
882 matches!(
883 c,
884 Cat::Tone | Cat::Vol | Cat::Listen | Cat::Sfx | Cat::Spectrum
885 )
886}
887pub(crate) fn is_crypto(c: Cat) -> bool {
888 matches!(
889 c,
890 Cat::Hash | Cat::Cipher | Cat::Sign | Cat::Kem | Cat::Shard
891 )
892}
893pub(crate) fn is_physics(c: Cat) -> bool {
894 matches!(
895 c,
896 Cat::Rigid | Cat::Soft | Cat::Liquid | Cat::Force | Cat::Collide
897 )
898}
899pub(crate) fn is_ai(c: Cat) -> bool {
900 matches!(c, Cat::Neural | Cat::Behavior)
901}
902
903const ENTRY_NAMES: &[&str] = &[
904 "start",
905 "main",
906 "启",
907 "เริ่ม",
908 "시작",
909 "начать",
910 "начало",
911 "inicio",
912 "comenzar",
913 "début",
914 "commencer",
915 "anfang",
916 "starten",
917 "início",
918];
919fn is_entry(name: &str) -> bool {
920 ENTRY_NAMES.contains(&name)
921}
922
923struct GlobalConst {
926 name: String,
927 value: String,
928}
929
930#[derive(Clone)]
931struct CallItem {
932 name: String,
933 cat: Cat,
934 count: usize,
935}
936
937struct FuncCard {
938 name: String,
939 params: Vec<String>,
940 calls: Vec<CallItem>,
941 has_loop: bool,
942 is_entry: bool,
943 vtex_count: usize,
944 audio_count: usize,
945}
946
947struct Document {
948 filename: String,
949 globals: Vec<GlobalConst>,
950 funcs: Vec<FuncCard>,
951 #[allow(dead_code)]
952 fn_names: HashSet<String>,
953}
954
955struct RawCall {
958 name: String,
959 cat: Cat,
960}
961
962fn walk_stmts(stmts: &[Stmt], fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
963 for s in stmts {
964 let e = match s {
965 Stmt::Expr(e) | Stmt::Return(e) => e,
966 Stmt::Bind(_, e) => e,
967 };
968 walk_expr(e, fns, out, loop_);
969 }
970}
971
972fn walk_expr(e: &Expr, fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
973 match e {
974 Expr::Call(func, args) => {
975 if let Expr::Ident(name) = func.as_ref() {
976 let cat = if fns.contains(name.as_str()) {
977 let base = categorize(name);
978 if base == Cat::User {
979 Cat::User
980 } else {
981 base
982 }
983 } else {
984 categorize(name)
985 };
986 out.push(RawCall { name: name.clone(), cat });
987 }
988 for a in args {
989 walk_expr(a, fns, out, loop_);
990 }
991 },
992 Expr::While { cond, body } => {
993 *loop_ = true;
994 walk_expr(cond, fns, out, loop_);
995 walk_stmts(body, fns, out, loop_);
996 },
997 Expr::Do(ss) => walk_stmts(ss, fns, out, loop_),
998 Expr::If { cond, then, elseifs, else_body } => {
999 walk_expr(cond, fns, out, loop_);
1000 walk_stmts(then, fns, out, loop_);
1001 for (c, b) in elseifs {
1002 walk_expr(c, fns, out, loop_);
1003 walk_stmts(b, fns, out, loop_);
1004 }
1005 if let Some(b) = else_body {
1006 walk_stmts(b, fns, out, loop_);
1007 }
1008 },
1009 Expr::For { iter, body, .. } => {
1010 walk_expr(iter, fns, out, loop_);
1011 walk_stmts(body, fns, out, loop_);
1012 },
1013 Expr::BinOp(_, a, b) => {
1014 walk_expr(a, fns, out, loop_);
1015 walk_expr(b, fns, out, loop_);
1016 },
1017 Expr::Array(es) => {
1018 for a in es {
1019 walk_expr(a, fns, out, loop_);
1020 }
1021 },
1022 _ => {},
1023 }
1024}
1025
1026fn aggregate(raw: Vec<RawCall>) -> Vec<CallItem> {
1027 let mut out: Vec<CallItem> = Vec::new();
1028 for r in raw {
1029 if let Some(last) = out.last_mut() {
1030 if last.name == r.name {
1031 last.count += 1;
1032 continue;
1033 }
1034 }
1035 out.push(CallItem { name: r.name, cat: r.cat, count: 1 });
1036 }
1037 out
1038}
1039
1040fn make_card(
1041 name: String,
1042 params: Vec<String>,
1043 raw: Vec<RawCall>,
1044 has_loop: bool,
1045 is_entry: bool,
1046) -> FuncCard {
1047 let calls = aggregate(raw);
1048 let vtex_count = calls
1049 .iter()
1050 .filter(|c| is_vtex(c.cat))
1051 .map(|c| c.count)
1052 .sum();
1053 let audio_count = calls
1054 .iter()
1055 .filter(|c| is_audio(c.cat))
1056 .map(|c| c.count)
1057 .sum();
1058 FuncCard {
1059 name,
1060 params,
1061 calls,
1062 has_loop,
1063 is_entry,
1064 vtex_count,
1065 audio_count,
1066 }
1067}
1068
1069impl Document {
1070 fn build(filename: &str, prog: &Program) -> Self {
1071 let fn_names: HashSet<String> = prog
1072 .items
1073 .iter()
1074 .filter_map(|i| {
1075 if let Item::Fn(f) = i {
1076 Some(f.name.clone())
1077 } else {
1078 None
1079 }
1080 })
1081 .collect();
1082
1083 let mut globals = Vec::new();
1084 let mut entries = Vec::new();
1085 let mut funcs = Vec::new();
1086
1087 for item in &prog.items {
1088 match item {
1089 Item::Bind(name, expr) => match expr {
1090 Expr::Number(n) => globals.push(GlobalConst {
1091 name: name.clone(),
1092 value: if n.fract() == 0.0 {
1093 format!("{}", *n as i64)
1094 } else {
1095 format!("{:.2}", n)
1096 },
1097 }),
1098 Expr::Do(body) => {
1099 let mut raw = Vec::new();
1100 let mut lp = false;
1101 for s in body {
1103 if let Stmt::Expr(Expr::While { body: wb, .. }) = s {
1104 lp = true;
1105 walk_stmts(wb, &fn_names, &mut raw, &mut lp);
1106 }
1107 }
1108 walk_stmts(body, &fn_names, &mut raw, &mut lp);
1109 entries.push(make_card(name.clone(), vec![], raw, lp, is_entry(name)));
1110 },
1111 _ => {},
1112 },
1113 Item::Fn(f) => {
1114 let mut raw = Vec::new();
1115 let mut lp = false;
1116 walk_stmts(&f.body, &fn_names, &mut raw, &mut lp);
1117 funcs.push(make_card(f.name.clone(), f.params.clone(), raw, lp, false));
1118 },
1119 _ => {},
1120 }
1121 }
1122
1123 entries.extend(funcs);
1124 Document {
1125 filename: filename.to_string(),
1126 globals,
1127 funcs: entries,
1128 fn_names,
1129 }
1130 }
1131}
1132
1133fn xe(s: &str) -> String {
1137 s.replace('&', "&")
1138 .replace('<', "<")
1139 .replace('>', ">")
1140}
1141fn p(v: f32) -> String {
1142 format!("{:.2}", v)
1143}
1144
1145pub(crate) fn icon(cat: Cat, cx: f32, cy: f32, r: f32) -> String {
1146 let c = cat.color();
1147 match cat {
1148 Cat::Rings => {
1149 format!(
1150 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.9"/>
1151 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.65"/>
1152 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.4"/>"#,
1153 p(cx),
1154 p(cy),
1155 p(r),
1156 p(cx),
1157 p(cy),
1158 p(r * 0.63),
1159 p(cx),
1160 p(cy),
1161 p(r * 0.33)
1162 )
1163 },
1164 Cat::Spiral => {
1165 let (x0, y0) = (cx, cy - r * 0.1);
1167 let (x1, y1) = (cx + r * 0.85, cy);
1168 let (x2, y2) = (cx, cy + r * 0.9);
1169 let (x3, y3) = (cx - r * 0.85, cy + r * 0.1);
1170 let (x4, y4) = (cx, cy - r * 0.9);
1171 format!(
1172 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{} S {},{} {},{} S {},{} {},{}"
1173 fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>"#,
1174 p(x0),
1175 p(y0),
1176 p(cx + r),
1177 p(y0),
1178 p(x1),
1179 p(cy - r * 0.5),
1180 p(x1),
1181 p(y1),
1182 p(cx + r * 0.3),
1183 p(y2),
1184 p(x2),
1185 p(y2),
1186 p(x3),
1187 p(y3 - r * 0.4),
1188 p(x3),
1189 p(y3),
1190 p(cx),
1191 p(y4),
1192 p(x4),
1193 p(y4)
1194 )
1195 },
1196 Cat::Star => {
1197 let pts: String = (0..5)
1199 .flat_map(|i| {
1200 let ao = (i as f32 * 72.0 - 90.0).to_radians();
1201 let ai = ao + 36.0_f32.to_radians();
1202 let ri = r * 0.42;
1203 vec![
1204 format!("{},{}", p(cx + r * ao.cos()), p(cy + r * ao.sin())),
1205 format!("{},{}", p(cx + ri * ai.cos()), p(cy + ri * ai.sin())),
1206 ]
1207 })
1208 .collect::<Vec<_>>()
1209 .join(" ");
1210 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.85"/>"#)
1211 },
1212 Cat::Flower => {
1213 (0..6)
1215 .map(|i| {
1216 let angle = i as f32 * 60.0;
1217 format!(
1218 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
1219 fill="{c}" opacity="0.5"
1220 transform="rotate({angle},{},{})"/>"#,
1221 p(cx),
1222 p(cy - r * 0.45),
1223 p(r * 0.28),
1224 p(r * 0.52),
1225 p(cx),
1226 p(cy)
1227 )
1228 })
1229 .collect::<String>()
1230 },
1231 Cat::Lotus => {
1232 (0..8)
1234 .map(|i| {
1235 let angle = i as f32 * 45.0;
1236 format!(
1237 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
1238 fill="{c}" opacity="0.45"
1239 transform="rotate({angle},{},{})"/>"#,
1240 p(cx),
1241 p(cy - r * 0.50),
1242 p(r * 0.22),
1243 p(r * 0.55),
1244 p(cx),
1245 p(cy)
1246 )
1247 })
1248 .collect::<String>()
1249 + &format!(
1250 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
1251 p(cx),
1252 p(cy),
1253 p(r * 0.22)
1254 )
1255 },
1256 Cat::Chakra => {
1257 let spokes: String = (0..8).map(|i| {
1259 let a = (i as f32 * 45.0).to_radians();
1260 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.8"/>"#,
1261 p(cx + r*0.28*a.cos()), p(cy + r*0.28*a.sin()),
1262 p(cx + r*0.88*a.cos()), p(cy + r*0.88*a.sin()))
1263 }).collect();
1264 spokes
1265 + &format!(
1266 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
1267 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
1268 p(cx),
1269 p(cy),
1270 p(r * 0.88),
1271 p(cx),
1272 p(cy),
1273 p(r * 0.2)
1274 )
1275 },
1276 Cat::Yantra => {
1277 let h = r * 0.866;
1279 let t1 = format!(
1280 "{},{} {},{} {},{}",
1281 p(cx),
1282 p(cy - r),
1283 p(cx + h),
1284 p(cy + r * 0.5),
1285 p(cx - h),
1286 p(cy + r * 0.5)
1287 );
1288 let t2 = format!(
1289 "{},{} {},{} {},{}",
1290 p(cx),
1291 p(cy + r),
1292 p(cx - h),
1293 p(cy - r * 0.5),
1294 p(cx + h),
1295 p(cy - r * 0.5)
1296 );
1297 format!(
1298 r#"<polygon points="{t1}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1299 <polygon points="{t2}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1300 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25"/>"#,
1301 p(cx),
1302 p(cy),
1303 p(r * 0.38)
1304 )
1305 },
1306 Cat::Hyper => {
1307 let rays: String = (0..6).map(|i| {
1309 let a = (i as f32 * 30.0).to_radians();
1310 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"#,
1311 p(cx - r*a.cos()), p(cy - r*a.sin()),
1312 p(cx + r*a.cos()), p(cy + r*a.sin()))
1313 }).collect();
1314 rays + &format!(
1315 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>
1316 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.5"/>
1317 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.3"/>"#,
1318 p(cx),
1319 p(cy),
1320 p(r),
1321 p(cx),
1322 p(cy),
1323 p(r * 0.6),
1324 p(cx),
1325 p(cy),
1326 p(r * 0.3)
1327 )
1328 },
1329 Cat::Tess => {
1330 (0..4)
1332 .map(|i| {
1333 let y = cy - r * 0.7 + i as f32 * r * 0.46;
1334 let amp = r * 0.15;
1335 format!(
1336 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
1337 fill="none" stroke="{c}" stroke-width="1.1" opacity="0.7"/>"#,
1338 p(cx - r),
1339 p(y),
1340 p(cx - r + r * 0.4),
1341 p(y - amp),
1342 p(cx - r * 0.1),
1343 p(y - amp),
1344 p(cx),
1345 p(y),
1346 p(cx + r * 0.5),
1347 p(y + amp),
1348 p(cx + r),
1349 p(y)
1350 )
1351 })
1352 .collect::<String>()
1353 },
1354 Cat::Rain => {
1355 (0..5).flat_map(|col| {
1357 let x = cx - r*0.9 + col as f32 * r*0.45;
1358 (0..3).map(move |row| {
1359 let y = cy - r*0.8 + row as f32 * r*0.55;
1360 let opa = 0.9 - row as f32 * 0.22;
1361 format!(r#"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="{:.2}"/>"#,
1362 p(x - r*0.06), p(y), p(r*0.12), p(r*0.30), opa)
1363 })
1364 }).collect::<String>()
1365 },
1366 Cat::Grid => {
1367 let n = 3;
1369 let step = r * 2.0 / n as f32;
1370 let mut s = String::new();
1371 for i in 0..=n {
1372 let off = -r + i as f32 * step;
1373 write!(s, r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>
1374 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>"#,
1375 p(cx+off),p(cy-r), p(cx+off),p(cy+r),
1376 p(cx-r),p(cy+off), p(cx+r),p(cy+off)).ok();
1377 }
1378 s
1379 },
1380 Cat::Halftone => {
1381 (0..4)
1383 .flat_map(|row| {
1384 (0..4).map(move |col| {
1385 let x = cx - r * 0.75 + col as f32 * r * 0.5;
1386 let y = cy - r * 0.75 + row as f32 * r * 0.5;
1387 format!(
1388 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.6"/>"#,
1389 p(x),
1390 p(y),
1391 p(r * 0.14)
1392 )
1393 })
1394 })
1395 .collect::<String>()
1396 },
1397 Cat::Tone => {
1398 let x0 = cx - r;
1400 let x3 = cx + r;
1401 let xm = cx;
1402 let amp = r * 0.65;
1403 format!(
1404 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
1405 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>"#,
1406 p(x0),
1407 p(cy),
1408 p(x0 + r * 0.5),
1409 p(cy - amp),
1410 p(xm - r * 0.1),
1411 p(cy - amp),
1412 p(xm),
1413 p(cy),
1414 p(xm + r * 0.6),
1415 p(cy + amp),
1416 p(x3),
1417 p(cy)
1418 )
1419 },
1420 Cat::Vol | Cat::Listen => {
1421 let arcs: String = (1..=3)
1423 .map(|i| {
1424 let ri = r * 0.3 * i as f32;
1425 format!(
1426 r#"<path d="M {},{} A {ri},{ri} 0 0,1 {},{}"
1427 fill="none" stroke="{c}" stroke-width="1.3" opacity="{:.2}"/>"#,
1428 p(cx),
1429 p(cy - ri),
1430 p(cx),
1431 p(cy + ri),
1432 1.0 - i as f32 * 0.22
1433 )
1434 })
1435 .collect();
1436 arcs + &format!(
1437 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.7"/>"#,
1438 p(cx - r * 0.15),
1439 p(cy),
1440 p(r * 0.22)
1441 )
1442 },
1443 Cat::Camera => {
1444 format!(
1446 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.8"/>
1447 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.55"/>
1448 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"#,
1449 p(cx),
1450 p(cy),
1451 p(r),
1452 p(cx),
1453 p(cy),
1454 p(r * 0.6),
1455 p(cx),
1456 p(cy),
1457 p(r * 0.18)
1458 )
1459 },
1460 Cat::Light => {
1461 let rays: String = (0..8).map(|i| {
1463 let a = (i as f32 * 45.0).to_radians();
1464 let r1 = r * 0.38;
1465 let r2 = r * 0.90;
1466 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.75"/>"#,
1467 p(cx + r1*a.cos()), p(cy + r1*a.sin()),
1468 p(cx + r2*a.cos()), p(cy + r2*a.sin()))
1469 }).collect();
1470 rays + &format!(
1471 r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"#,
1472 p(cx),
1473 p(cy),
1474 p(r * 0.32)
1475 )
1476 },
1477 Cat::Fill => {
1478 format!(
1479 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"#,
1480 p(cx - r * 0.9),
1481 p(cy - r * 0.7),
1482 p(r * 1.8),
1483 p(r * 1.4)
1484 )
1485 },
1486 Cat::Present => {
1487 let pts = format!(
1489 "{},{} {},{} {},{}",
1490 p(cx - r * 0.7),
1491 p(cy - r * 0.8),
1492 p(cx + r * 0.8),
1493 p(cy),
1494 p(cx - r * 0.7),
1495 p(cy + r * 0.8)
1496 );
1497 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.7"/>"#)
1498 },
1499 Cat::User => {
1500 format!(
1502 r#"<path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
1503 stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
1504 <path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
1505 stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>"#,
1506 p(cx - r * 0.6),
1507 p(cy - r * 0.7),
1508 p(cx + r * 0.5),
1509 p(cy),
1510 p(cx - r * 0.6),
1511 p(cy + r * 0.7),
1512 p(cx),
1513 p(cy - r * 0.7),
1514 p(cx + r * 0.95),
1515 p(cy),
1516 p(cx),
1517 p(cy + r * 0.7)
1518 )
1519 },
1520 Cat::Loop => {
1521 format!(
1523 r#"<path d="M {},{} A {},{} 0 1,1 {},{}"
1524 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
1525 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"#,
1526 p(cx),
1527 p(cy - r * 0.9),
1528 p(r * 0.9),
1529 p(r * 0.9),
1530 p(cx + r * 0.4),
1531 p(cy - r * 0.9),
1532 p(cx + r * 0.4),
1533 p(cy - r * 0.9),
1534 p(cx + r * 0.1),
1535 p(cy - r * 0.55),
1536 p(cx + r * 0.8),
1537 p(cy - r * 0.62)
1538 )
1539 },
1540 Cat::Pagoda => {
1542 let mut s = String::new();
1543 for i in 0..3 {
1544 let yy = cy - r * 0.7 + i as f32 * r * 0.55;
1545 let hw = r * (0.4 + i as f32 * 0.22);
1546 write!(s, r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
1547 p(cx - hw), p(yy), p(cx), p(yy - r*0.35), p(cx + hw), p(yy)).ok();
1548 }
1549 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"##,
1550 p(cx), p(cy - r*1.05), p(cx), p(cy + r*0.55)).ok();
1551 s
1552 },
1553 Cat::Torii => {
1554 format!(
1555 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
1556 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1557 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>
1558 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
1559 p(cx - r * 0.95),
1560 p(cy - r * 0.6),
1561 p(cx + r * 0.95),
1562 p(cy - r * 0.6),
1563 p(cx - r * 0.75),
1564 p(cy - r * 0.2),
1565 p(cx + r * 0.75),
1566 p(cy - r * 0.2),
1567 p(cx - r * 0.55),
1568 p(cy - r * 0.6),
1569 p(cx - r * 0.55),
1570 p(cy + r * 0.85),
1571 p(cx + r * 0.55),
1572 p(cy - r * 0.6),
1573 p(cx + r * 0.55),
1574 p(cy + r * 0.85)
1575 )
1576 },
1577 Cat::Cog => {
1578 let mut s = String::new();
1579 for i in 0..8 {
1580 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.8" transform="rotate({},{},{})"/>"##,
1581 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();
1582 }
1583 write!(
1584 s,
1585 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.55"/>
1586 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
1587 p(cx),
1588 p(cy),
1589 p(r * 0.7),
1590 p(cx),
1591 p(cy),
1592 p(r * 0.3)
1593 )
1594 .ok();
1595 s
1596 },
1597 Cat::Fractal => {
1598 let outer = format!(
1599 "{},{} {},{} {},{}",
1600 p(cx),
1601 p(cy - r * 0.9),
1602 p(cx + r * 0.85),
1603 p(cy + r * 0.6),
1604 p(cx - r * 0.85),
1605 p(cy + r * 0.6)
1606 );
1607 let inner = format!(
1608 "{},{} {},{} {},{}",
1609 p(cx),
1610 p(cy + r * 0.6),
1611 p(cx - r * 0.42),
1612 p(cy - r * 0.12),
1613 p(cx + r * 0.42),
1614 p(cy - r * 0.12)
1615 );
1616 format!(
1617 r##"<polygon points="{outer}" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
1618 <polygon points="{inner}" fill="#0b0b1a" opacity="0.9"/>"##
1619 )
1620 },
1621 Cat::Sfx => {
1623 format!(
1624 r##"<polygon points="{},{} {},{} {},{} {},{} {},{} {},{}" fill="{c}" opacity="0.85"/>"##,
1625 p(cx + r * 0.2),
1626 p(cy - r * 0.9),
1627 p(cx - r * 0.5),
1628 p(cy + r * 0.1),
1629 p(cx - r * 0.05),
1630 p(cy + r * 0.1),
1631 p(cx - r * 0.2),
1632 p(cy + r * 0.9),
1633 p(cx + r * 0.5),
1634 p(cy - r * 0.1),
1635 p(cx + r * 0.05),
1636 p(cy - r * 0.1)
1637 )
1638 },
1639 Cat::Spectrum => {
1640 let hs = [0.5f32, 0.9, 0.35, 0.7, 0.55];
1641 let mut s = String::new();
1642 for (i, &hh) in hs.iter().enumerate() {
1643 let xx = cx - r * 0.8 + i as f32 * r * 0.4;
1644 let bh = r * 1.6 * hh;
1645 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.8"/>"##,
1646 p(xx - r*0.12), p(cy + r*0.8 - bh), p(r*0.24), p(bh)).ok();
1647 }
1648 s
1649 },
1650 Cat::Music => {
1652 format!(
1653 r##"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{c}" opacity="0.85" transform="rotate(-20,{},{})"/>
1654 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1655 <path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1656 p(cx - r * 0.35),
1657 p(cy + r * 0.55),
1658 p(r * 0.32),
1659 p(r * 0.24),
1660 p(cx - r * 0.35),
1661 p(cy + r * 0.55),
1662 p(cx - r * 0.05),
1663 p(cy + r * 0.5),
1664 p(cx - r * 0.05),
1665 p(cy - r * 0.8),
1666 p(cx - r * 0.05),
1667 p(cy - r * 0.8),
1668 p(cx + r * 0.5),
1669 p(cy - r * 0.65),
1670 p(cx + r * 0.55),
1671 p(cy - r * 0.2),
1672 p(cx + r * 0.3),
1673 p(cy - r * 0.05)
1674 )
1675 },
1676 Cat::Note => {
1677 format!(
1678 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
1679 <circle cx="{}" cy="{}" r="{}" fill="{c}" 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="1.2" opacity="0.85"/>
1682 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="2.0" opacity="0.85"/>"##,
1683 p(cx - r * 0.55),
1684 p(cy + r * 0.55),
1685 p(r * 0.24),
1686 p(cx + r * 0.55),
1687 p(cy + r * 0.3),
1688 p(r * 0.24),
1689 p(cx - r * 0.33),
1690 p(cy + r * 0.55),
1691 p(cx - r * 0.33),
1692 p(cy - r * 0.6),
1693 p(cx + r * 0.77),
1694 p(cy + r * 0.3),
1695 p(cx + r * 0.77),
1696 p(cy - r * 0.85),
1697 p(cx - r * 0.33),
1698 p(cy - r * 0.6),
1699 p(cx + r * 0.77),
1700 p(cy - r * 0.85)
1701 )
1702 },
1703 Cat::Hash => {
1705 let mut s = String::new();
1706 for i in 0..3 {
1707 let rr = r * (0.35 + i as f32 * 0.22);
1708 write!(s, r##"<path d="M {},{} A {},{} 0 1,1 {},{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="{:.2}"/>"##,
1709 p(cx - rr), p(cy), p(rr), p(rr), p(cx + rr), p(cy), 0.85 - i as f32*0.18).ok();
1710 }
1711 write!(
1712 s,
1713 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
1714 p(cx),
1715 p(cy),
1716 p(r * 0.12)
1717 )
1718 .ok();
1719 s
1720 },
1721 Cat::Cipher => {
1722 format!(
1723 r##"<path d="M {},{} A {},{} 0 0,1 {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.85"/>
1724 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.1"/>
1725 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
1726 p(cx - r * 0.45),
1727 p(cy - r * 0.05),
1728 p(r * 0.45),
1729 p(r * 0.45),
1730 p(cx + r * 0.45),
1731 p(cy - r * 0.05),
1732 p(cx - r * 0.7),
1733 p(cy - r * 0.05),
1734 p(r * 1.4),
1735 p(r * 0.95),
1736 p(cx),
1737 p(cy + r * 0.4),
1738 p(r * 0.14)
1739 )
1740 },
1741 Cat::Sign => {
1742 format!(
1743 r##"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>
1744 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.5"/>"##,
1745 p(cx - r * 0.9),
1746 p(cy + r * 0.4),
1747 p(cx - r * 0.4),
1748 p(cy - r * 0.7),
1749 p(cx),
1750 p(cy + r * 0.6),
1751 p(cx + r * 0.3),
1752 p(cy - r * 0.1),
1753 p(cx + r * 0.7),
1754 p(cy - r * 0.7),
1755 p(cx + r * 0.9),
1756 p(cy + r * 0.3),
1757 p(cx - r),
1758 p(cy + r * 0.75),
1759 p(cx + r),
1760 p(cy + r * 0.75)
1761 )
1762 },
1763 Cat::Kem => {
1764 format!(
1765 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1766 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1767 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1768 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1769 p(cx - r * 0.5),
1770 p(cy - r * 0.4),
1771 p(r * 0.4),
1772 p(cx - r * 0.18),
1773 p(cy - r * 0.12),
1774 p(cx + r * 0.8),
1775 p(cy + r * 0.75),
1776 p(cx + r * 0.55),
1777 p(cy + r * 0.5),
1778 p(cx + r * 0.8),
1779 p(cy + r * 0.25),
1780 p(cx + r * 0.8),
1781 p(cy + r * 0.75),
1782 p(cx + r * 1.05),
1783 p(cy + r * 0.5)
1784 )
1785 },
1786 Cat::Shard => {
1787 format!(
1788 r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.5"/>
1789 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3"/>
1790 <polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
1791 p(cx),
1792 p(cy - r),
1793 p(cx - r * 0.85),
1794 p(cy + r * 0.5),
1795 p(cx),
1796 p(cy + r * 0.2),
1797 p(cx),
1798 p(cy - r),
1799 p(cx + r * 0.85),
1800 p(cy + r * 0.5),
1801 p(cx),
1802 p(cy + r * 0.2),
1803 p(cx),
1804 p(cy - r),
1805 p(cx + r * 0.85),
1806 p(cy + r * 0.5),
1807 p(cx - r * 0.85),
1808 p(cy + r * 0.5)
1809 )
1810 },
1811 Cat::Rigid => {
1813 format!(
1814 r##"<polygon points="{},{} {},{} {},{} {},{}" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.0"/>
1815 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.2" stroke="{c}" stroke-width="1.0"/>
1816 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.28" stroke="{c}" stroke-width="1.0"/>"##,
1817 p(cx),
1818 p(cy - r * 0.9),
1819 p(cx + r * 0.85),
1820 p(cy - r * 0.4),
1821 p(cx),
1822 p(cy + r * 0.1),
1823 p(cx - r * 0.85),
1824 p(cy - r * 0.4),
1825 p(cx - r * 0.85),
1826 p(cy - r * 0.4),
1827 p(cx),
1828 p(cy + r * 0.1),
1829 p(cx),
1830 p(cy + r * 0.95),
1831 p(cx - r * 0.85),
1832 p(cy + r * 0.45),
1833 p(cx + r * 0.85),
1834 p(cy - r * 0.4),
1835 p(cx),
1836 p(cy + r * 0.1),
1837 p(cx),
1838 p(cy + r * 0.95),
1839 p(cx + r * 0.85),
1840 p(cy + r * 0.45)
1841 )
1842 },
1843 Cat::Soft => {
1844 format!(
1845 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.45" stroke="{c}" stroke-width="1.0"/>"##,
1846 p(cx),
1847 p(cy - r * 0.85),
1848 p(cx + r * 0.7),
1849 p(cy - r * 0.9),
1850 p(cx + r * 0.95),
1851 p(cy - r * 0.2),
1852 p(cx + r * 0.8),
1853 p(cy + r * 0.4),
1854 p(cx + r * 0.6),
1855 p(cy + r * 0.95),
1856 p(cx - r * 0.1),
1857 p(cy + r * 0.9),
1858 p(cx - r * 0.6),
1859 p(cy + r * 0.7),
1860 p(cx - r * 0.95),
1861 p(cy + r * 0.5),
1862 p(cx - r * 0.9),
1863 p(cy - r * 0.3),
1864 p(cx - r * 0.7),
1865 p(cy - r * 0.6),
1866 p(cx - r * 0.5),
1867 p(cy - r * 0.85),
1868 p(cx - r * 0.2),
1869 p(cy - r * 0.95),
1870 p(cx),
1871 p(cy - r * 0.85)
1872 )
1873 },
1874 Cat::Liquid => {
1875 format!(
1876 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.0"/>
1877 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#ffffff" opacity="0.25"/>"##,
1878 p(cx),
1879 p(cy - r * 0.9),
1880 p(cx + r * 0.75),
1881 p(cy - r * 0.1),
1882 p(cx + r * 0.6),
1883 p(cy + r * 0.8),
1884 p(cx),
1885 p(cy + r * 0.85),
1886 p(cx - r * 0.6),
1887 p(cy + r * 0.8),
1888 p(cx - r * 0.75),
1889 p(cy - r * 0.1),
1890 p(cx),
1891 p(cy - r * 0.9),
1892 p(cx - r * 0.25),
1893 p(cy + r * 0.25),
1894 p(r * 0.16),
1895 p(r * 0.28)
1896 )
1897 },
1898 Cat::Force => {
1899 format!(
1900 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.8" opacity="0.9" stroke-linecap="round"/>
1901 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"##,
1902 p(cx - r * 0.7),
1903 p(cy + r * 0.7),
1904 p(cx + r * 0.45),
1905 p(cy - r * 0.45),
1906 p(cx + r * 0.8),
1907 p(cy - r * 0.8),
1908 p(cx + r * 0.2),
1909 p(cy - r * 0.7),
1910 p(cx + r * 0.7),
1911 p(cy - r * 0.15)
1912 )
1913 },
1914 Cat::Collide => {
1915 let mut s = String::new();
1916 for i in 0..8 {
1917 let a = (i as f32 * 45.0).to_radians();
1918 let r1 = r * 0.3;
1919 let r2 = if i % 2 == 0 { r * 0.95 } else { r * 0.6 };
1920 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1921 p(cx + r1*a.cos()), p(cy + r1*a.sin()), p(cx + r2*a.cos()), p(cy + r2*a.sin())).ok();
1922 }
1923 write!(
1924 s,
1925 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
1926 p(cx),
1927 p(cy),
1928 p(r * 0.18)
1929 )
1930 .ok();
1931 s
1932 },
1933 Cat::Mesh => {
1935 format!(
1936 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
1937 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
1938 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
1939 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.9" opacity="0.6"/>"##,
1940 p(cx),
1941 p(cy),
1942 p(r * 0.9),
1943 p(cx),
1944 p(cy),
1945 p(r * 0.38),
1946 p(r * 0.9),
1947 p(cx),
1948 p(cy),
1949 p(r * 0.9),
1950 p(r * 0.38),
1951 p(cx - r * 0.9),
1952 p(cy),
1953 p(cx + r * 0.9),
1954 p(cy)
1955 )
1956 },
1957 Cat::Draw3D => {
1958 format!(
1959 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1960 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.75"/>
1961 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.6"/>
1962 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1963 p(cx),
1964 p(cy + r * 0.3),
1965 p(cx),
1966 p(cy - r * 0.9),
1967 p(cx),
1968 p(cy + r * 0.3),
1969 p(cx + r * 0.9),
1970 p(cy + r * 0.7),
1971 p(cx),
1972 p(cy + r * 0.3),
1973 p(cx - r * 0.85),
1974 p(cy + r * 0.6),
1975 p(cx),
1976 p(cy + r * 0.3),
1977 p(r * 0.13)
1978 )
1979 },
1980 Cat::Draw2D => {
1981 format!(
1982 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9" stroke-linecap="round"/>
1983 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>
1984 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1985 p(cx - r * 0.8),
1986 p(cy + r * 0.8),
1987 p(cx + r * 0.8),
1988 p(cy - r * 0.8),
1989 p(cx - r * 0.8),
1990 p(cy + r * 0.8),
1991 p(r * 0.16),
1992 p(cx + r * 0.8),
1993 p(cy - r * 0.8),
1994 p(r * 0.16)
1995 )
1996 },
1997 Cat::Trig => {
1999 format!(
2000 r##"<polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
2001 <path d="M {},{} A {},{} 0 0,0 {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2002 p(cx - r * 0.8),
2003 p(cy + r * 0.7),
2004 p(cx + r * 0.8),
2005 p(cy + r * 0.7),
2006 p(cx - r * 0.8),
2007 p(cy - r * 0.7),
2008 p(cx - r * 0.35),
2009 p(cy + r * 0.7),
2010 p(r * 0.45),
2011 p(r * 0.45),
2012 p(cx - r * 0.8),
2013 p(cy + r * 0.38)
2014 )
2015 },
2016 Cat::MathFn => {
2017 format!(
2018 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
2019 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
2020 <path d="M {},{} Q {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
2021 p(cx - r * 0.9),
2022 p(cy),
2023 p(cx + r * 0.9),
2024 p(cy),
2025 p(cx),
2026 p(cy - r * 0.9),
2027 p(cx),
2028 p(cy + r * 0.9),
2029 p(cx - r * 0.8),
2030 p(cy - r * 0.7),
2031 p(cx),
2032 p(cy + r * 1.1),
2033 p(cx + r * 0.8),
2034 p(cy - r * 0.7)
2035 )
2036 },
2037 Cat::Noise => {
2038 format!(
2039 r##"<polyline points="{},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
2040 p(cx - r * 0.9),
2041 p(cy + r * 0.1),
2042 p(cx - r * 0.65),
2043 p(cy - r * 0.6),
2044 p(cx - r * 0.4),
2045 p(cy + r * 0.5),
2046 p(cx - r * 0.15),
2047 p(cy - r * 0.4),
2048 p(cx + r * 0.1),
2049 p(cy + r * 0.7),
2050 p(cx + r * 0.35),
2051 p(cy - r * 0.5),
2052 p(cx + r * 0.6),
2053 p(cy + r * 0.3),
2054 p(cx + r * 0.8),
2055 p(cy - r * 0.3),
2056 p(cx + r * 0.95),
2057 p(cy + r * 0.2)
2058 )
2059 },
2060 Cat::Net => {
2062 let mut s = String::new();
2063 let nodes = [(0.0f32, -0.85f32), (0.8, 0.4), (-0.8, 0.4)];
2064 for &(nx, ny) in nodes.iter() {
2065 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
2066 p(cx), p(cy), p(cx + nx*r), p(cy + ny*r)).ok();
2067 }
2068 write!(
2069 s,
2070 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
2071 p(cx),
2072 p(cy),
2073 p(r * 0.2)
2074 )
2075 .ok();
2076 for &(nx, ny) in nodes.iter() {
2077 write!(
2078 s,
2079 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
2080 p(cx + nx * r),
2081 p(cy + ny * r),
2082 p(r * 0.18)
2083 )
2084 .ok();
2085 }
2086 s
2087 },
2088 Cat::Neural => {
2089 let mut s = String::new();
2090 let l0 = [-0.6f32, 0.0, 0.6];
2091 let l1 = [-0.3f32, 0.3];
2092 let x0 = cx - r * 0.7;
2093 let x1 = cx;
2094 let x2 = cx + r * 0.7;
2095 for &a in l0.iter() {
2096 for &b in l1.iter() {
2097 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
2098 p(x0), p(cy + a*r), p(x1), p(cy + b*r)).ok();
2099 }
2100 }
2101 for &b in l1.iter() {
2102 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
2103 p(x1), p(cy + b*r), p(x2), p(cy)).ok();
2104 }
2105 for &a in l0.iter() {
2106 write!(
2107 s,
2108 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2109 p(x0),
2110 p(cy + a * r),
2111 p(r * 0.14)
2112 )
2113 .ok();
2114 }
2115 for &b in l1.iter() {
2116 write!(
2117 s,
2118 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2119 p(x1),
2120 p(cy + b * r),
2121 p(r * 0.14)
2122 )
2123 .ok();
2124 }
2125 write!(
2126 s,
2127 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2128 p(x2),
2129 p(cy),
2130 p(r * 0.14)
2131 )
2132 .ok();
2133 s
2134 },
2135 Cat::Behavior => {
2136 format!(
2137 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2138 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2139 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
2140 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>
2141 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
2142 p(cx),
2143 p(cy - r * 0.6),
2144 p(cx - r * 0.7),
2145 p(cy + r * 0.6),
2146 p(cx),
2147 p(cy - r * 0.6),
2148 p(cx + r * 0.7),
2149 p(cy + r * 0.6),
2150 p(cx),
2151 p(cy - r * 0.6),
2152 p(r * 0.2),
2153 p(cx - r * 0.7),
2154 p(cy + r * 0.6),
2155 p(r * 0.18),
2156 p(cx + r * 0.7),
2157 p(cy + r * 0.6),
2158 p(r * 0.18)
2159 )
2160 },
2161 Cat::Widget => {
2163 format!(
2164 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
2165 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
2166 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"##,
2167 p(cx - r * 0.85),
2168 p(cy - r * 0.75),
2169 p(r * 1.7),
2170 p(r * 1.5),
2171 p(cx - r * 0.85),
2172 p(cy - r * 0.3),
2173 p(cx + r * 0.85),
2174 p(cy - r * 0.3),
2175 p(cx - r * 0.5),
2176 p(cy + r * 0.1),
2177 p(r * 1.0),
2178 p(r * 0.45)
2179 )
2180 },
2181 Cat::Hud => {
2182 format!(
2183 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
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="0.8" opacity="0.6"/>
2186 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9"/>
2187 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
2188 p(cx),
2189 p(cy),
2190 p(r * 0.9),
2191 p(cx - r * 0.9),
2192 p(cy),
2193 p(cx + r * 0.9),
2194 p(cy),
2195 p(cx),
2196 p(cy - r * 0.9),
2197 p(cx),
2198 p(cy + r * 0.9),
2199 p(cx),
2200 p(cy),
2201 p(cx + r * 0.64),
2202 p(cy - r * 0.64),
2203 p(cx + r * 0.4),
2204 p(cy - r * 0.4),
2205 p(r * 0.13)
2206 )
2207 },
2208 Cat::Dialog => {
2209 format!(
2210 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="4" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
2211 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.4"/>
2212 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
2213 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
2214 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>"##,
2215 p(cx - r * 0.9),
2216 p(cy - r * 0.8),
2217 p(r * 1.8),
2218 p(r * 1.2),
2219 p(cx - r * 0.4),
2220 p(cy + r * 0.4),
2221 p(cx - r * 0.1),
2222 p(cy + r * 0.4),
2223 p(cx - r * 0.55),
2224 p(cy + r * 0.9),
2225 p(cx - r * 0.4),
2226 p(cy - r * 0.2),
2227 p(r * 0.1),
2228 p(cx),
2229 p(cy - r * 0.2),
2230 p(r * 0.1),
2231 p(cx + r * 0.4),
2232 p(cy - r * 0.2),
2233 p(r * 0.1)
2234 )
2235 },
2236 Cat::Holo => {
2237 let mut s = String::new();
2238 for i in 0..3 {
2239 let off = (i as f32 - 1.0) * r * 0.22;
2240 write!(s, r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3" stroke="{c}" stroke-width="0.8" stroke-opacity="0.7"/>"##,
2241 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();
2242 }
2243 s
2244 },
2245 Cat::Str => {
2247 let mut s = String::new();
2248 for &dx in [-0.5f32, 0.3].iter() {
2249 write!(
2250 s,
2251 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
2252 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
2253 p(cx + dx * r),
2254 p(cy - r * 0.4),
2255 p(r * 0.16),
2256 p(cx + dx * r + r * 0.3),
2257 p(cy - r * 0.4),
2258 p(r * 0.16)
2259 )
2260 .ok();
2261 }
2262 for j in 0..2 {
2263 let yy = cy + r * 0.3 + j as f32 * r * 0.45;
2264 let ww = if j == 0 { r * 1.6 } else { r * 1.0 };
2265 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.6"/>"##,
2266 p(cx - r*0.8), p(yy), p(cx - r*0.8 + ww), p(yy)).ok();
2267 }
2268 s
2269 },
2270 Cat::Font => {
2271 format!(
2272 r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9" stroke-linejoin="round"/>
2273 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
2274 p(cx - r * 0.7),
2275 p(cy + r * 0.8),
2276 p(cx),
2277 p(cy - r * 0.85),
2278 p(cx + r * 0.7),
2279 p(cy + r * 0.8),
2280 p(cx - r * 0.38),
2281 p(cy + r * 0.15),
2282 p(cx + r * 0.38),
2283 p(cy + r * 0.15)
2284 )
2285 },
2286 Cat::Vector => {
2287 format!(
2288 r##"<path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
2289 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
2290 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
2291 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
2292 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
2293 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2294 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2295 p(cx - r * 0.8),
2296 p(cy + r * 0.7),
2297 p(cx - r * 0.3),
2298 p(cy - r * 0.9),
2299 p(cx + r * 0.3),
2300 p(cy - r * 0.9),
2301 p(cx + r * 0.8),
2302 p(cy + r * 0.7),
2303 p(cx - r * 0.8),
2304 p(cy + r * 0.7),
2305 p(cx - r * 0.3),
2306 p(cy - r * 0.9),
2307 p(cx + r * 0.8),
2308 p(cy + r * 0.7),
2309 p(cx + r * 0.3),
2310 p(cy - r * 0.9),
2311 p(cx - r * 0.9),
2312 p(cy + r * 0.6),
2313 p(r * 0.2),
2314 p(r * 0.2),
2315 p(cx + r * 0.7),
2316 p(cy + r * 0.6),
2317 p(r * 0.2),
2318 p(r * 0.2),
2319 p(cx - r * 0.3),
2320 p(cy - r * 0.9),
2321 p(r * 0.14),
2322 p(cx + r * 0.3),
2323 p(cy - r * 0.9),
2324 p(r * 0.14)
2325 )
2326 },
2327 Cat::Color => {
2329 format!(
2330 r##"<circle cx="{}" cy="{}" r="{}" fill="#ff5e8a" opacity="0.5"/>
2331 <circle cx="{}" cy="{}" r="{}" fill="#50fa7b" opacity="0.5"/>
2332 <circle cx="{}" cy="{}" r="{}" fill="#6ab0f5" opacity="0.5"/>"##,
2333 p(cx),
2334 p(cy - r * 0.4),
2335 p(r * 0.6),
2336 p(cx - r * 0.45),
2337 p(cy + r * 0.35),
2338 p(r * 0.6),
2339 p(cx + r * 0.45),
2340 p(cy + r * 0.35),
2341 p(r * 0.6)
2342 )
2343 },
2344 Cat::Shade => {
2345 format!(
2346 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25" stroke="{c}" stroke-width="1.0"/>
2347 <path d="M {},{} A {},{} 0 0,1 {},{} A {},{} 0 0,0 {},{} Z" fill="{c}" opacity="0.6"/>
2348 <circle cx="{}" cy="{}" r="{}" fill="#ffffff" opacity="0.3"/>"##,
2349 p(cx),
2350 p(cy),
2351 p(r * 0.9),
2352 p(cx),
2353 p(cy - r * 0.9),
2354 p(r * 0.9),
2355 p(r * 0.9),
2356 p(cx),
2357 p(cy + r * 0.9),
2358 p(r * 0.45),
2359 p(r * 0.9),
2360 p(cx),
2361 p(cy - r * 0.9),
2362 p(cx + r * 0.35),
2363 p(cy - r * 0.35),
2364 p(r * 0.18)
2365 )
2366 },
2367 Cat::Window => {
2368 format!(
2369 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
2370 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
2371 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2372 p(cx - r * 0.85),
2373 p(cy - r * 0.85),
2374 p(r * 1.7),
2375 p(r * 1.7),
2376 p(cx),
2377 p(cy - r * 0.85),
2378 p(cx),
2379 p(cy + r * 0.85),
2380 p(cx - r * 0.85),
2381 p(cy),
2382 p(cx + r * 0.85),
2383 p(cy)
2384 )
2385 },
2386 Cat::Key => {
2388 format!(
2389 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="3" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.2"/>
2390 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
2391 p(cx - r * 0.8),
2392 p(cy - r * 0.8),
2393 p(r * 1.6),
2394 p(r * 1.6),
2395 p(cx - r * 0.45),
2396 p(cy - r * 0.45),
2397 p(r * 0.9),
2398 p(r * 0.9)
2399 )
2400 },
2401 Cat::Mouse => {
2402 format!(
2403 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
2404 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
2405 <rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.85"/>"##,
2406 p(cx - r * 0.55),
2407 p(cy - r * 0.85),
2408 p(r * 1.1),
2409 p(r * 1.7),
2410 p(r * 0.55),
2411 p(cx),
2412 p(cy - r * 0.85),
2413 p(cx),
2414 p(cy - r * 0.1),
2415 p(cx - r * 0.08),
2416 p(cy - r * 0.7),
2417 p(r * 0.16),
2418 p(r * 0.4)
2419 )
2420 },
2421 Cat::Print => {
2423 let mut s = format!(
2424 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1.5" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>"##,
2425 p(cx - r * 0.6),
2426 p(cy - r * 0.85),
2427 p(r * 1.2),
2428 p(r * 1.7)
2429 );
2430 for j in 0..4 {
2431 let yy = cy - r * 0.45 + j as f32 * r * 0.35;
2432 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
2433 p(cx - r*0.4), p(yy), p(cx + r*0.4), p(yy)).ok();
2434 }
2435 s
2436 },
2437 Cat::File => {
2438 format!(
2439 r##"<path d="M {},{} L {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.1"/>
2440 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
2441 p(cx - r * 0.6),
2442 p(cy - r * 0.85),
2443 p(cx + r * 0.25),
2444 p(cy - r * 0.85),
2445 p(cx + r * 0.6),
2446 p(cy - r * 0.45),
2447 p(cx + r * 0.6),
2448 p(cy + r * 0.85),
2449 p(cx - r * 0.6),
2450 p(cy + r * 0.85),
2451 p(cx + r * 0.25),
2452 p(cy - r * 0.85),
2453 p(cx + r * 0.25),
2454 p(cy - r * 0.45),
2455 p(cx + r * 0.6),
2456 p(cy - r * 0.45)
2457 )
2458 },
2459 Cat::Sys => {
2460 format!(
2461 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.8"/>
2462 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round" stroke-linejoin="round"/>
2463 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round"/>"##,
2464 p(cx - r * 0.9),
2465 p(cy - r * 0.7),
2466 p(r * 1.8),
2467 p(r * 1.4),
2468 p(cx - r * 0.45),
2469 p(cy - r * 0.2),
2470 p(cx - r * 0.1),
2471 p(cy + r * 0.15),
2472 p(cx - r * 0.45),
2473 p(cy + r * 0.5),
2474 p(cx + r * 0.1),
2475 p(cy + r * 0.5),
2476 p(cx + r * 0.5),
2477 p(cy + r * 0.5)
2478 )
2479 },
2480 Cat::Anim => {
2481 let mut s = String::new();
2482 for i in 0..6 {
2483 let a = (i as f32 * 60.0).to_radians();
2484 let rad = r * 0.7;
2485 let dotr = r * (0.08 + 0.16 * (i as f32 / 6.0));
2486 write!(
2487 s,
2488 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="{:.2}"/>"##,
2489 p(cx + rad * a.cos()),
2490 p(cy + rad * a.sin()),
2491 p(dotr),
2492 0.35 + i as f32 * 0.1
2493 )
2494 .ok();
2495 }
2496 s
2497 },
2498 Cat::Const => {
2499 format!(
2500 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2"
2501 fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
2502 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.1" opacity="0.5"/>"#,
2503 p(cx - r * 0.7),
2504 p(cy - r * 0.55),
2505 p(r * 1.4),
2506 p(r * 1.1),
2507 p(cx - r * 0.4),
2508 p(cy),
2509 p(cx + r * 0.4),
2510 p(cy)
2511 )
2512 },
2513 }
2514}
2515
2516fn card_height(card: &FuncCard) -> f32 {
2519 let icon_rows = (card.calls.len() + ICONS_ROW - 1).max(1) / ICONS_ROW + 1;
2520 let base = CARD_PAD * 2.0 + 32.0 + 20.0; base + icon_rows as f32 * (ICON_SZ + ICON_GAP) + ICON_GAP
2524}
2525
2526fn render_card(card: &FuncCard, x: f32, y: f32) -> String {
2527 let h = card_height(card);
2528 let w = CARD_W;
2529 let r = CARD_ROUNDING;
2530
2531 let dominant = card
2533 .calls
2534 .iter()
2535 .max_by_key(|c| c.count)
2536 .map(|c| c.cat.color())
2537 .unwrap_or(Cat::User.color());
2538
2539 let border_color = if card.is_entry { GOLD } else { dominant };
2540 let border_w = if card.is_entry { 2.5 } else { 1.2 };
2541 let glow_filter = if card.is_entry {
2542 r#" filter="url(#glow-gold)""#
2543 } else {
2544 ""
2545 };
2546
2547 let mut s = String::new();
2548
2549 write!(s, r#"<rect x="{}" y="{}" width="{w}" height="{h}" rx="{r}"
2551 fill="{CARD_BG}" stroke="{border_color}" stroke-width="{border_w}"{glow_filter}/>
2552 <line x1="{}" y1="{}" x2="{}" y2="{}"
2553 stroke="{border_color}" stroke-width="3" opacity="0.6"/>
2554"#,
2555 p(x), p(y),
2556 p(x+r), p(y+h), p(x+r), p(y) ).ok();
2558
2559 let name_y = y + CARD_PAD + 18.0;
2561 let entry_badge = if card.is_entry {
2562 format!(
2563 r#" <text x="{}" y="{}" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.8">⬡ ENTRY</text>"#,
2564 p(x + w - CARD_PAD - 48.0),
2565 p(name_y)
2566 )
2567 } else {
2568 String::new()
2569 };
2570
2571 write!(
2572 s,
2573 r#"<text x="{}" y="{}" fill="{}" font-size="13" font-weight="bold">{}</text>{}"#,
2574 p(x + CARD_PAD + 10.0),
2575 p(name_y),
2576 if card.is_entry { GOLD } else { TEXT },
2577 xe(&card.name),
2578 entry_badge
2579 )
2580 .ok();
2581
2582 let stats_y = name_y + 18.0;
2584 let params_str = if card.params.is_empty() {
2585 String::new()
2586 } else {
2587 format!("({})", card.params.join(", "))
2588 };
2589 let stats_str = {
2590 let mut parts = Vec::new();
2591 if card.vtex_count > 0 {
2592 parts.push(format!("{} vtex", card.vtex_count));
2593 }
2594 if card.audio_count > 0 {
2595 parts.push(format!("{} audio", card.audio_count));
2596 }
2597 if card.has_loop {
2598 parts.push("↺ loop".into());
2599 }
2600 parts.join(" · ")
2601 };
2602 write!(
2603 s,
2604 r#"<text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{}</text>
2605 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10" text-anchor="end">{}</text>
2606"#,
2607 p(x + CARD_PAD + 10.0),
2608 p(stats_y),
2609 xe(¶ms_str),
2610 p(x + w - CARD_PAD),
2611 p(stats_y),
2612 xe(&stats_str)
2613 )
2614 .ok();
2615
2616 let div_y = stats_y + 6.0;
2618 write!(
2619 s,
2620 r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{CARD_BD}" stroke-width="1"/>"#,
2621 p(x + CARD_PAD),
2622 p(div_y),
2623 p(x + w - CARD_PAD),
2624 p(div_y)
2625 )
2626 .ok();
2627
2628 let icon_y0 = div_y + ICON_GAP + ICON_SZ / 2.0;
2630 let icon_x0 = x + CARD_PAD + ICON_SZ / 2.0 + 6.0;
2631 let ir = ICON_SZ / 2.0 * 0.82; for (i, call) in card.calls.iter().enumerate() {
2634 let row = i / ICONS_ROW;
2635 let col = i % ICONS_ROW;
2636 let ix = icon_x0 + col as f32 * (ICON_SZ + ICON_GAP);
2637 let iy = icon_y0 + row as f32 * (ICON_SZ + ICON_GAP);
2638
2639 write!(
2641 s,
2642 r#"<rect x="{}" y="{}" width="{ICON_SZ}" height="{ICON_SZ}" rx="3"
2643 fill="{}" opacity="0.12"/>
2644"#,
2645 p(ix - ICON_SZ / 2.0),
2646 p(iy - ICON_SZ / 2.0),
2647 call.cat.color()
2648 )
2649 .ok();
2650
2651 s.push_str(&icon(call.cat, ix, iy, ir));
2653
2654 if call.count > 1 {
2656 write!(s, r##"<rect x="{}" y="{}" width="13" height="10" rx="3" fill="{}" opacity="0.9"/>
2657 <text x="{}" y="{}" fill="#0a0a1a" font-size="8" font-weight="bold" text-anchor="middle">{}</text>"##,
2658 p(ix + ir - 2.0), p(iy - ir - 1.0), call.cat.color(),
2659 p(ix + ir + 4.5), p(iy - ir + 7.0), call.count
2660 ).ok();
2661 }
2662 }
2663
2664 s
2665}
2666
2667fn render_header(filename: &str, funcs: &[FuncCard]) -> String {
2670 let name = std::path::Path::new(filename)
2671 .file_name()
2672 .map(|n| n.to_string_lossy().into_owned())
2673 .unwrap_or_else(|| filename.to_string());
2674 let n_fns = funcs.len();
2675 let count = |pred: fn(Cat) -> bool| -> usize {
2676 funcs
2677 .iter()
2678 .flat_map(|f| &f.calls)
2679 .filter(|c| pred(c.cat))
2680 .map(|c| c.count)
2681 .sum()
2682 };
2683 let n_vtex = count(is_vtex);
2684 let n_audio = count(is_audio);
2685 let n_crypto = count(is_crypto);
2686 let n_physics = count(is_physics);
2687 let n_ai = count(is_ai);
2688 let n_calls: usize = funcs.iter().map(|f| f.calls.len()).sum();
2689
2690 let mut stats = vec![format!("{n_fns} fn"), format!("{n_calls} call types")];
2692 for (n, lbl) in [
2693 (n_vtex, "vtex"),
2694 (n_audio, "audio"),
2695 (n_crypto, "crypto"),
2696 (n_physics, "physics"),
2697 (n_ai, "ai"),
2698 ] {
2699 if n > 0 {
2700 stats.push(format!("{n} {lbl}"));
2701 }
2702 }
2703
2704 format!(
2705 r##"<rect x="0" y="0" width="{SVG_W}" height="{HEADER_H}" fill="#080814"/>
2706 <line x1="0" y1="{HEADER_H}" x2="{SVG_W}" y2="{HEADER_H}" stroke="#1a1a3a" stroke-width="1"/>
2707 <text x="20" y="22" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.6" letter-spacing="2">LING VISUALIZER</text>
2708 <text x="20" y="56" fill="{TEXT}" font-size="26" font-weight="bold">{}</text>
2709 <text x="{}" y="56" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"##,
2710 xe(&name),
2711 SVG_W - 20.0,
2712 stats.join(" · ")
2713 )
2714}
2715
2716fn render_legend(funcs: &[FuncCard]) -> String {
2717 let mut present: Vec<Cat> = Vec::new();
2720 let mut seen = HashSet::new();
2721 for f in funcs {
2722 for c in &f.calls {
2723 if seen.insert(c.cat) {
2724 present.push(c.cat);
2725 }
2726 }
2727 }
2728 present.sort_by(|a, b| a.domain().cmp(b.domain()).then(a.label().cmp(b.label())));
2729
2730 let mut s = format!(
2731 r##"<rect x="0" y="{HEADER_H}" width="{SVG_W}" height="{LEGEND_H}" fill="#0a0a18"/>
2732 <line x1="0" y1="{}" x2="{SVG_W}" y2="{}" stroke="#171730" stroke-width="1"/>"##,
2733 HEADER_H + LEGEND_H,
2734 HEADER_H + LEGEND_H
2735 );
2736
2737 let row_h = 21.0;
2738 let x_start = GRID_X + 8.0;
2739 let x_max = SVG_W - 24.0;
2740 let mut lx = x_start;
2741 let mut row = 0usize;
2742 let mut cur_domain = "";
2743 let row_y = |r: usize| HEADER_H + 17.0 + r as f32 * row_h;
2744
2745 for cat in present {
2746 let c = cat.color();
2747 let lbl = cat.label();
2748 let dom = cat.domain();
2749
2750 if dom != cur_domain {
2752 let tag_w = dom.len() as f32 * 6.0 + 12.0;
2753 if lx > x_start && lx + tag_w > x_max {
2754 row += 1;
2755 lx = x_start;
2756 }
2757 if row > 1 {
2758 break;
2759 } let ly = row_y(row);
2761 s.push_str(&format!(
2762 r##"<text x="{}" y="{}" fill="#3f3f66" font-size="8" font-weight="bold" letter-spacing="1">{}</text>"##,
2763 p(lx), p(ly + 3.0), dom.to_uppercase()
2764 ));
2765 lx += tag_w;
2766 cur_domain = dom;
2767 }
2768
2769 let item_w = 20.0 + lbl.len() as f32 * 6.2 + 12.0;
2770 if lx + item_w > x_max {
2771 row += 1;
2772 lx = x_start;
2773 }
2774 if row > 1 {
2775 break;
2776 }
2777 let ly = row_y(row);
2778 let icon_svg = icon(cat, lx + 7.0, ly, 6.0);
2779 s.push_str(&format!(
2780 r#"<rect x="{}" y="{}" width="14" height="14" rx="3" fill="{c}" opacity="0.12"/>
2781 {}
2782 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{lbl}</text>"#,
2783 p(lx),
2784 p(ly - 7.0),
2785 icon_svg,
2786 p(lx + 18.0),
2787 p(ly + 4.0)
2788 ));
2789 lx += item_w;
2790 }
2791 s
2792}
2793
2794fn render_sidebar(globals: &[GlobalConst], total_h: f32) -> String {
2795 let h = total_h - CONTENT_Y;
2796 let mut s = format!(
2797 r##"<rect x="0" y="{CONTENT_Y}" width="{SIDEBAR_W}" height="{h}" fill="{SIDEBAR_BG}"/>
2798 <line x1="{SIDEBAR_W}" y1="{CONTENT_Y}" x2="{SIDEBAR_W}" y2="{total_h}" stroke="#1a1a3a" stroke-width="1"/>
2799 <text x="14" y="{}" fill="{TEXT_DIM}" font-size="9" font-weight="bold" letter-spacing="2">CONSTANTS</text>"##,
2800 CONTENT_Y + 20.0
2801 );
2802
2803 let mut gy = CONTENT_Y + 38.0;
2804 for g in globals {
2805 write!(
2807 s,
2808 r#"<rect x="{}" y="{}" width="8" height="8" rx="1" fill="{}" opacity="0.7"
2809 transform="rotate(45,{},{})"/>
2810 <text x="{}" y="{}" fill="{}" font-size="12" font-weight="bold">{}</text>
2811 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"#,
2812 p(14.0),
2813 p(gy - 7.0),
2814 Cat::Star.color(),
2815 p(18.0),
2816 p(gy - 3.0),
2817 p(28.0),
2818 p(gy),
2819 Cat::Grid.color(),
2820 xe(&g.name),
2821 p(SIDEBAR_W - 10.0),
2822 p(gy),
2823 xe(&g.value)
2824 )
2825 .ok();
2826 gy += 22.0;
2827 }
2828 s
2829}
2830
2831const DEFS: &str = r##"<defs>
2834 <filter id="glow-gold" x="-30%" y="-30%" width="160%" height="160%">
2835 <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
2836 <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
2837 </filter>
2838 <pattern id="grid-pat" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
2839 <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#ffffff" stroke-width="0.4"/>
2840 </pattern>
2841</defs>"##;
2842
2843pub fn render(filename: &str, program: &Program) -> String {
2846 let doc = Document::build(filename, program);
2847
2848 let heights: Vec<f32> = doc.funcs.iter().map(card_height).collect();
2850 let mut col_y = [CONTENT_Y; COLS];
2851 let mut positions: Vec<(f32, f32)> = Vec::with_capacity(doc.funcs.len());
2852
2853 for &h in &heights {
2854 let (col, &cy) = col_y
2855 .iter()
2856 .enumerate()
2857 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2858 .unwrap();
2859 let cx = GRID_X + col as f32 * (CARD_W + CARD_GAP);
2860 positions.push((cx, cy));
2861 col_y[col] += h + CARD_GAP;
2862 }
2863
2864 let total_h = col_y.iter().cloned().fold(0.0f32, f32::max) + 40.0;
2865
2866 let mut svg = String::new();
2867 write!(
2868 svg,
2869 r#"<?xml version="1.0" encoding="UTF-8"?>
2870<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_W}" height="{h}" viewBox="0 0 {SVG_W} {h}"
2871 style="font-family:'JetBrains Mono','Fira Code',monospace,sans-serif;background:{BG}">"#,
2872 h = total_h
2873 )
2874 .ok();
2875
2876 svg.push_str(DEFS);
2877
2878 write!(svg, r#"<rect width="{SVG_W}" height="{total_h}" fill="{BG}"/>
2880 <rect width="{SVG_W}" height="{total_h}" fill="url(#grid-pat)" opacity="0.05"/>"#,
2881 total_h = total_h).ok();
2882
2883 svg.push_str(&render_header(&doc.filename, &doc.funcs));
2884 svg.push_str(&render_legend(&doc.funcs));
2885 svg.push_str(&render_sidebar(&doc.globals, total_h));
2886
2887 for (card, &(cx, cy)) in doc.funcs.iter().zip(positions.iter()) {
2888 svg.push_str(&render_card(card, cx, cy));
2889 }
2890
2891 svg.push_str("\n</svg>");
2892 svg
2893}