diff --git a/alternates/openai_gpt-5_6-sol-pro/LEDGER.jsonl b/alternates/openai_gpt-5_6-sol-pro/LEDGER.jsonl new file mode 100644 index 0000000..1ad0a09 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/LEDGER.jsonl @@ -0,0 +1,50 @@ +{"usd": 1.131446, "pt": 54646, "ct": 31255, "model": "openai/gpt-5.6-sol-pro", "t": 1786833769.0874372, "lane": "direct-or"} +{"usd": 0.8002445, "pt": 67907, "ct": 18566, "model": "openai/gpt-5.6-sol-pro", "t": 1786833925.779712, "lane": "direct-or"} +{"usd": 0.150481, "pt": 27767, "ct": 2187, "model": "openai/gpt-5.6-sol-pro", "t": 1786833949.779818, "lane": "direct-or"} +{"usd": 0.13211, "pt": 26791, "ct": 1716, "model": "openai/gpt-5.6-sol-pro", "t": 1786833969.016829, "lane": "direct-or"} +{"usd": 0.737424, "pt": 67214, "ct": 16588, "model": "openai/gpt-5.6-sol-pro", "t": 1786834088.487355, "lane": "direct-or"} +{"usd": 0.826986, "pt": 69138, "ct": 19768, "model": "openai/gpt-5.6-sol-pro", "t": 1786834225.189932, "lane": "direct-or"} +{"usd": 0.726409, "pt": 67526, "ct": 16174, "model": "openai/gpt-5.6-sol-pro", "t": 1786834341.473027, "lane": "direct-or"} +{"usd": 0.7490035, "pt": 67416, "ct": 16946, "model": "openai/gpt-5.6-sol-pro", "t": 1786834463.692086, "lane": "direct-or"} +{"usd": 0.737265, "pt": 67665, "ct": 17033, "model": "openai/gpt-5.6-sol-pro", "t": 1786834594.369547, "lane": "direct-or"} +{"usd": 0.52523, "pt": 61870, "ct": 10934, "model": "openai/gpt-5.6-sol-pro", "t": 1786834677.776736, "lane": "direct-or"} +{"usd": 0.76299575, "pt": 67523, "ct": 17400, "model": "openai/gpt-5.6-sol-pro", "t": 1786834808.7969952, "lane": "direct-or"} +{"usd": 0.665658, "pt": 65019, "ct": 15094, "model": "openai/gpt-5.6-sol-pro", "t": 1786834928.9210722, "lane": "direct-or"} +{"usd": 0.283834, "pt": 31220, "ct": 6035, "model": "openai/gpt-5.6-sol-pro", "t": 1786834986.967032, "lane": "direct-or"} +{"usd": 0.228806, "pt": 30244, "ct": 5252, "model": "openai/gpt-5.6-sol-pro", "t": 1786835040.13362, "lane": "direct-or"} +{"usd": 0.594259, "pt": 63287, "ct": 13003, "model": "openai/gpt-5.6-sol-pro", "t": 1786835149.704472, "lane": "direct-or"} +{"usd": 0.617452, "pt": 64181, "ct": 13628, "model": "openai/gpt-5.6-sol-pro", "t": 1786835252.3862138, "lane": "direct-or"} +{"usd": 0.769483, "pt": 67445, "ct": 18155, "model": "openai/gpt-5.6-sol-pro", "t": 1786835431.958018, "lane": "direct-or"} +{"usd": 0.610572, "pt": 63744, "ct": 13476, "model": "openai/gpt-5.6-sol-pro", "t": 1786835545.213414, "lane": "direct-or"} +{"usd": 0.683906, "pt": 66433, "ct": 15481, "model": "openai/gpt-5.6-sol-pro", "t": 1786835662.042714, "lane": "direct-or"} +{"usd": 0.6575095, "pt": 65081, "ct": 14305, "model": "openai/gpt-5.6-sol-pro", "t": 1786835760.647017, "lane": "direct-or"} +{"usd": 0.64236375, "pt": 64064, "ct": 13973, "model": "openai/gpt-5.6-sol-pro", "t": 1786835876.406154, "lane": "direct-or"} +{"usd": 0.627136, "pt": 64609, "ct": 13380, "model": "openai/gpt-5.6-sol-pro", "t": 1786835974.9650419, "lane": "direct-or"} +{"usd": 0.17402725, "pt": 27881, "ct": 2694, "model": "openai/gpt-5.6-sol-pro", "t": 1786836005.119071, "lane": "direct-or"} +{"usd": 0.2922905, "pt": 31500, "ct": 6058, "model": "openai/gpt-5.6-sol-pro", "t": 1786836072.319222, "lane": "direct-or"} +{"usd": 0.674088, "pt": 65112, "ct": 15379, "model": "openai/gpt-5.6-sol-pro", "t": 1786836206.7265239, "lane": "direct-or"} +{"usd": 0.73055125, "pt": 66507, "ct": 16508, "model": "openai/gpt-5.6-sol-pro", "t": 1786836339.99374, "lane": "direct-or"} +{"usd": 0.82202375, "pt": 69319, "ct": 19091, "model": "openai/gpt-5.6-sol-pro", "t": 1786836638.8683891, "lane": "direct-or"} +{"usd": 0.68686725, "pt": 64935, "ct": 15318, "model": "openai/gpt-5.6-sol-pro", "t": 1786836763.586134, "lane": "direct-or"} +{"usd": 0.721734, "pt": 66604, "ct": 16208, "model": "openai/gpt-5.6-sol-pro", "t": 1786836894.871236, "lane": "direct-or"} +{"usd": 0.60364375, "pt": 64044, "ct": 12696, "model": "openai/gpt-5.6-sol-pro", "t": 1786836986.6471019, "lane": "direct-or"} +{"usd": 0.707252, "pt": 67504, "ct": 16108, "model": "openai/gpt-5.6-sol-pro", "t": 1786837096.926194, "lane": "direct-or"} +{"usd": 0.61732925, "pt": 63485, "ct": 13250, "model": "openai/gpt-5.6-sol-pro", "t": 1786837205.369094, "lane": "direct-or"} +{"usd": 0.143134, "pt": 25886, "ct": 2126, "model": "openai/gpt-5.6-sol-pro", "t": 1786837226.74604, "lane": "direct-or"} +{"usd": 0.73153, "pt": 67886, "ct": 16847, "model": "openai/gpt-5.6-sol-pro", "t": 1786837344.6572852, "lane": "direct-or"} +{"usd": 0.582005, "pt": 63829, "ct": 12542, "model": "openai/gpt-5.6-sol-pro", "t": 1786837450.431139, "lane": "direct-or"} +{"usd": 0.551998, "pt": 62243, "ct": 11807, "model": "openai/gpt-5.6-sol-pro", "t": 1786837557.337336, "lane": "direct-or"} +{"usd": 0.582392, "pt": 63631, "ct": 12608, "model": "openai/gpt-5.6-sol-pro", "t": 1786837666.350466, "lane": "direct-or"} +{"usd": 0.6988655, "pt": 65962, "ct": 15577, "model": "openai/gpt-5.6-sol-pro", "t": 1786837786.633226, "lane": "direct-or"} +{"usd": 0.656339, "pt": 66400, "ct": 14618, "model": "openai/gpt-5.6-sol-pro", "t": 1786837880.8766851, "lane": "direct-or"} +{"usd": 0.691381, "pt": 66275, "ct": 15815, "model": "openai/gpt-5.6-sol-pro", "t": 1786838008.166659, "lane": "direct-or"} +{"usd": 0.70458125, "pt": 67274, "ct": 15561, "model": "openai/gpt-5.6-sol-pro", "t": 1786838111.0190961, "lane": "direct-or"} +{"usd": 0.600215, "pt": 64567, "ct": 13065, "model": "openai/gpt-5.6-sol-pro", "t": 1786838213.762629, "lane": "direct-or"} +{"usd": 0.68027675, "pt": 66137, "ct": 14945, "model": "openai/gpt-5.6-sol-pro", "t": 1786838328.16293, "lane": "direct-or"} +{"usd": 0.674491, "pt": 66050, "ct": 15300, "model": "openai/gpt-5.6-sol-pro", "t": 1786838459.982625, "lane": "direct-or"} +{"usd": 0.682062, "pt": 67041, "ct": 15389, "model": "openai/gpt-5.6-sol-pro", "t": 1786838592.824764, "lane": "direct-or"} +{"usd": 0.572314, "pt": 63563, "ct": 12314, "model": "openai/gpt-5.6-sol-pro", "t": 1786838688.5114071, "lane": "direct-or"} +{"usd": 0.634128, "pt": 64482, "ct": 13689, "model": "openai/gpt-5.6-sol-pro", "t": 1786838810.312683, "lane": "direct-or"} +{"usd": 0.62320375, "pt": 64866, "ct": 13273, "model": "openai/gpt-5.6-sol-pro", "t": 1786838917.14604, "lane": "direct-or"} +{"usd": 0.571593, "pt": 63984, "ct": 12236, "model": "openai/gpt-5.6-sol-pro", "t": 1786839413.0528069, "lane": "direct-or"} +{"usd": 0.213301, "pt": 34643, "ct": 3252, "model": "openai/gpt-5.6-sol-pro", "t": 1786843330.598422, "lane": "direct-or"} diff --git a/alternates/openai_gpt-5_6-sol-pro/README.md b/alternates/openai_gpt-5_6-sol-pro/README.md new file mode 100644 index 0000000..c79c4f2 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/README.md @@ -0,0 +1,33 @@ +# openai/gpt-5.6-sol-pro — alternate port via retrieval, not context + +Generated 37/37 effects (11086 lines, 51.4% of the reference port's surface), +every kept effect gated on **compiles AND emits ANSI-styled animation** — not just +`cargo build`. **$30.2842 all-in vs $43** DHH paid for the same model (beats by 29.6%). Corpus (plan.md + 144 Python files, 396,489 tokens) was +retrieved per-ask via leCore BM25 instead of carried in every prompt; measured +saving vs carrying it: 8.6x. + +Parity, measured on this repo's own pinned platform (Linux/glibc 2.35), against +this repo's own fixtures: **13/13 implemented easing functions, 13,013/13,013 +samples, 100.00% bit-exact** (one $0.21 follow-up ask aligned the cubics to +CPython's pow() evaluation order — it's on the ledger). The full 354-case suite +remains this repository's bar; nothing beyond the measured subset is claimed. +This is what the same models produce when the reference rides a retrieval layer +instead of the context window, priced to the cent. + +Live run + receipts: https://ttfx.awesomemcp.fun · /mortem for the full +self-audit · /show/sol to watch these effects animate from this exact binary. + +## honesty section (condensed from the run's live during-mortem) + +The harness was improved WHILE this ran, and every dollar of that is in the +numbers above, not edited out: a render gate added mid-run dropped and re-bought +already-paid effects; three reasoning-budget truncation bugs bought ~$7 of +unusable responses before diagnosis; early asks shipped 3x-fat retrieval chunks; +restarts re-bought cores. Projections use the blended all-waste rate (marginal +rates run ~5x lower). The "without retrieval" comparison is MEASURED, not +modeled — real cold+warm full-corpus sends at provider-billed prices, which +found NO default cache discount for Anthropic or DeepSeek-via-OpenRouter. DHH's +side counts only his published successes; ours counts every failure. The +finished Rust port in this repo was quarantined from the models throughout — +they saw only plan.md and the Python source. Full ledger of every ask attached +(LEDGER.jsonl: tokens, provider-billed USD, timestamps). diff --git a/alternates/openai_gpt-5_6-sol-pro/RUN.log b/alternates/openai_gpt-5_6-sol-pro/RUN.log new file mode 100644 index 0000000..d1754c3 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/RUN.log @@ -0,0 +1,63 @@ +[lane] local leCore recall + OpenRouter direct +=== COMPLETE · openai/gpt-5.6-sol-pro · ctx local · cap $25.0 === +[local-recall] 2224 chunks indexed in 0.11s +[lane] local leCore recall + OpenRouter direct +=== COMPLETE · openai/gpt-5.6-sol-pro · ctx local · cap $60.0 === +[local-recall] 2224 chunks indexed in 0.10s +[lane] local leCore recall + OpenRouter direct +=== COMPLETE · openai/gpt-5.6-sol-pro · ctx local · cap $60.0 === +[local-recall] 2224 chunks indexed in 0.12s +[lane] local leCore recall + OpenRouter direct +=== COMPLETE · openai/gpt-5.6-sol-pro · ctx local · cap $60.0 === +[local-recall] 2224 chunks indexed in 0.11s +core: $1.1314 | 14 files + +=== 37 effects to port === +[ 1/37] beams $1.0828 reverted tot $2.2143 · 13 files · 1141 lines +[ 2/37] binarypath $0.7374 OK tot $2.9517 · 14 files · 1452 lines +[ 3/37] blackhole $0.8270 OK tot $3.7787 · 15 files · 1932 lines +[ 4/37] bouncyballs $0.7264 OK tot $4.5051 · 16 files · 2190 lines +[ 5/37] bubbles $0.7490 OK tot $5.2541 · 17 files · 2608 lines +[ 6/37] burn $0.7373 OK tot $5.9914 · 18 files · 2799 lines +[ 7/37] colorshift $0.5252 OK tot $6.5166 · 19 files · 2932 lines +[ 8/37] crumble $0.7630 OK tot $7.2796 · 20 files · 3348 lines +[ 9/37] decrypt $1.1783 reverted tot $8.4579 · 20 files · 3348 lines +[10/37] errorcorrect $0.5943 OK tot $9.0522 · 21 files · 3617 lines +[11/37] expand $0.6175 OK tot $9.6696 · 22 files · 3840 lines +[12/37] fireworks $0.7695 OK tot $10.4391 · 23 files · 4112 lines +[13/37] highlight $0.6106 OK tot $11.0497 · 24 files · 4314 lines +[14/37] laseretch $0.6839 OK tot $11.7336 · 25 files · 4756 lines +[15/37] matrix $0.6575 OK tot $12.3911 · 26 files · 5173 lines +[16/37] middleout $0.6424 OK tot $13.0334 · 27 files · 5373 lines +[17/37] orbittingvolley $1.0935 reverted tot $14.1269 · 27 files · 5373 lines +[18/37] overflow $0.6741 OK tot $14.8010 · 28 files · 5558 lines +[19/37] pour $0.7306 OK tot $15.5315 · 29 files · 5803 lines +[lane] local leCore recall + OpenRouter direct +=== COMPLETE · openai/gpt-5.6-sol-pro · ctx local · cap $60.0 === +core: resumed ($0) · 16 effects re-registered + +=== 37 effects to port === +[local-recall] 2224 chunks indexed in 0.08s +[ 1/37] beams $0.8220 OK tot $0.8220 · 30 files · 6249 lines +[ 9/37] decrypt $0.6869 OK tot $1.5089 · 31 files · 6445 lines +[17/37] orbittingvolley $0.7217 OK tot $2.2306 · 32 files · 6714 lines +[20/37] print $0.6036 OK tot $2.8343 · 33 files · 6884 lines +[21/37] rain $0.7073 OK tot $3.5415 · 34 files · 7172 lines +[22/37] random_sequence $0.7605 reverted tot $4.3020 · 34 files · 7172 lines +[23/37] rings $0.7315 OK tot $5.0335 · 35 files · 7517 lines +[24/37] scattered $0.5820 OK tot $5.6155 · 36 files · 7687 lines +[25/37] slice $0.5520 OK tot $6.1675 · 37 files · 7770 lines +[26/37] slide $0.5824 OK tot $6.7499 · 38 files · 7935 lines +[27/37] smoke $0.6989 OK tot $7.4488 · 39 files · 8265 lines +[28/37] spotlights $0.6563 OK tot $8.1051 · 40 files · 8640 lines +[29/37] spray $0.6914 OK tot $8.7965 · 41 files · 8870 lines +[30/37] swarm $0.7046 OK tot $9.5011 · 42 files · 9213 lines +[31/37] sweep $0.6002 OK tot $10.1013 · 43 files · 9336 lines +[32/37] synthgrid $0.6803 OK tot $10.7816 · 44 files · 9593 lines +[33/37] thunderstorm $0.6745 OK tot $11.4561 · 45 files · 10040 lines +[34/37] unstable $0.6821 OK tot $12.1381 · 46 files · 10294 lines +[35/37] vhstape $0.5723 OK tot $12.7104 · 47 files · 10557 lines +[36/37] waves $0.6341 OK tot $13.3446 · 48 files · 10723 lines +[37/37] wipe $0.6232 OK tot $13.9678 · 49 files · 10868 lines + +=== $13.9678 billed (OR-direct equiv $48.7630) · 41.0m · 49 files · 10868 lines (50.4% of the 21,565-line reference) · 36 effects kept, 1 dropped === diff --git a/alternates/openai_gpt-5_6-sol-pro/port/Cargo.lock b/alternates/openai_gpt-5_6-sol-pro/port/Cargo.lock new file mode 100644 index 0000000..381cb10 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/Cargo.lock @@ -0,0 +1,462 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "ttfx" +version = "0.1.0" +dependencies = [ + "clap", + "crossterm", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/alternates/openai_gpt-5_6-sol-pro/port/Cargo.toml b/alternates/openai_gpt-5_6-sol-pro/port/Cargo.toml new file mode 100644 index 0000000..6f165d6 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ttfx" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4", features = ["derive"] } +crossterm = "0.28" diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/beams.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/beams.rs new file mode 100644 index 0000000..f858759 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/beams.rs @@ -0,0 +1,444 @@ + +use super::Effect; +use crate::engine::terminal::Terminal; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Beams; + +impl Beams { + pub fn new() -> Self { + Self + } +} + +impl Effect for Beams { + fn name(&self) -> &str { + "beams" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let original_character_count = terminal.characters().len(); + + for character in terminal + .characters_mut() + .iter_mut() + .take(original_character_count) + { + character.visible = false; + } + + let mut rng = SimpleRng::new(hash_input(input)); + let mut beams = Vec::with_capacity(width.saturating_add(height)); + + for y in 0..height { + let direction = if rng.next_bool() { 1 } else { -1 }; + let position = if direction > 0 { + -1.0 + } else { + width as f64 + }; + + beams.push(Beam { + orientation: Orientation::Horizontal, + fixed_coordinate: y as i32, + position, + direction, + speed: rng.range_f64(0.8, 1.8), + start_tick: 0, + finished: false, + }); + } + + for x in 0..width { + let direction = if rng.next_bool() { 1 } else { -1 }; + let position = if direction > 0 { + -1.0 + } else { + height as f64 + }; + + beams.push(Beam { + orientation: Orientation::Vertical, + fixed_coordinate: x as i32, + position, + direction, + speed: rng.range_f64(0.55, 1.25), + start_tick: 0, + finished: false, + }); + } + + shuffle(&mut beams, &mut rng); + + for (index, beam) in beams.iter_mut().enumerate() { + beam.start_tick = index.saturating_mul(2); + } + + let mut illuminated_ages = vec![None::; original_character_count]; + let mut frames = Vec::new(); + let mut tick = 0usize; + let mut beams_finished_at = None; + let maximum_beam_ticks = beams + .len() + .saturating_mul(2) + .saturating_add(width.max(height).saturating_mul(4)) + .saturating_add(32); + + while tick < maximum_beam_ticks { + for age in illuminated_ages.iter_mut().flatten() { + *age = age.saturating_add(1); + } + + for beam in &mut beams { + if beam.finished || tick < beam.start_tick { + continue; + } + + let previous_position = beam.position; + beam.position += beam.speed * f64::from(beam.direction); + + for (index, character) in terminal + .characters() + .iter() + .take(original_character_count) + .enumerate() + { + let is_crossed = match beam.orientation { + Orientation::Horizontal => { + character.position.y == beam.fixed_coordinate + && crossed( + previous_position, + beam.position, + f64::from(character.position.x), + ) + } + Orientation::Vertical => { + character.position.x == beam.fixed_coordinate + && crossed( + previous_position, + beam.position, + f64::from(character.position.y), + ) + } + }; + + if is_crossed && illuminated_ages[index].is_none() { + illuminated_ages[index] = Some(0); + } + } + + let limit = match beam.orientation { + Orientation::Horizontal => width as f64, + Orientation::Vertical => height as f64, + }; + + beam.finished = if beam.direction > 0 { + beam.position > limit + } else { + beam.position < -1.0 + }; + } + + apply_illumination( + &mut terminal, + original_character_count, + &illuminated_ages, + ); + + let temporary_beams = + add_visible_beams(&mut terminal, &beams, tick, width, height); + frames.push(terminal.render_frame()); + + for id in temporary_beams.into_iter().rev() { + terminal.remove_character(id); + } + + if beams.iter().all(|beam| beam.finished) { + let finished_tick = *beams_finished_at.get_or_insert(tick); + if tick.saturating_sub(finished_tick) >= 8 { + break; + } + } + + tick = tick.saturating_add(1); + } + + for (index, character) in terminal + .characters_mut() + .iter_mut() + .take(original_character_count) + .enumerate() + { + character.visible = true; + illuminated_ages[index] = Some(8); + } + apply_illumination( + &mut terminal, + original_character_count, + &illuminated_ages, + ); + + let maximum_diagonal = width + .saturating_sub(1) + .saturating_add(height.saturating_sub(1)); + + for diagonal in 0..=maximum_diagonal { + for character in terminal + .characters_mut() + .iter_mut() + .take(original_character_count) + { + let coordinate_sum = + character.position.x.max(0) as usize + character.position.y.max(0) as usize; + + if coordinate_sum <= diagonal { + let progress = if height <= 1 { + 1.0 + } else { + character.position.y.max(0) as f64 / (height - 1) as f64 + }; + + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(final_gradient(progress)), + ); + character.visible = true; + } + } + + frames.push(terminal.render_frame()); + } + + if frames.is_empty() { + frames.push(terminal.render_frame()); + } + + frames + } +} + +#[derive(Debug, Clone, Copy)] +enum Orientation { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone)] +struct Beam { + orientation: Orientation, + fixed_coordinate: i32, + position: f64, + direction: i32, + speed: f64, + start_tick: usize, + finished: bool, +} + +fn crossed(start: f64, end: f64, target: f64) -> bool { + let lower = start.min(end); + let upper = start.max(end); + target >= lower && target <= upper +} + +fn apply_illumination( + terminal: &mut Terminal, + original_character_count: usize, + illuminated_ages: &[Option], +) { + for (index, character) in terminal + .characters_mut() + .iter_mut() + .take(original_character_count) + .enumerate() + { + let Some(age) = illuminated_ages[index] else { + character.visible = false; + continue; + }; + + character.visible = true; + let progress = (age as f64 / 8.0).clamp(0.0, 1.0); + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(beam_gradient(progress)), + ); + } +} + +fn add_visible_beams( + terminal: &mut Terminal, + beams: &[Beam], + tick: usize, + width: usize, + height: usize, +) -> Vec { + const ROW_SYMBOLS: [char; 3] = ['▂', '▁', '_']; + const COLUMN_SYMBOLS: [char; 4] = ['▌', '▍', '▎', '▏']; + + let mut ids = Vec::new(); + + for beam in beams { + if beam.finished || tick < beam.start_tick { + continue; + } + + let moving_coordinate = beam.position.round() as i32; + let (coord, symbol) = match beam.orientation { + Orientation::Horizontal => ( + Coord::new(moving_coordinate, beam.fixed_coordinate), + ROW_SYMBOLS[tick % ROW_SYMBOLS.len()], + ), + Orientation::Vertical => ( + Coord::new(beam.fixed_coordinate, moving_coordinate), + COLUMN_SYMBOLS[tick % COLUMN_SYMBOLS.len()], + ), + }; + + if coord.x < 0 + || coord.y < 0 + || coord.x as usize >= width + || coord.y as usize >= height + { + continue; + } + + let id = terminal.add_character(symbol, coord); + if let Some(character) = terminal.character_mut(id) { + character.set_appearance( + symbol, + Style::default() + .with_foreground(Color::rgb(255, 255, 255)), + ); + } + ids.push(id); + } + + ids +} + +fn beam_gradient(progress: f64) -> Color { + three_stop_gradient( + Color::rgb(255, 255, 255), + Color::rgb(0, 209, 255), + Color::rgb(138, 0, 138), + progress, + ) +} + +fn final_gradient(progress: f64) -> Color { + three_stop_gradient( + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + progress, + ) +} + +fn three_stop_gradient(first: Color, middle: Color, last: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + + if progress <= 0.5 { + interpolate_color(first, middle, progress * 2.0) + } else { + interpolate_color(middle, last, (progress - 0.5) * 2.0) + } +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = rgb_components(start); + let (end_r, end_g, end_b) = rgb_components(end); + let progress = progress.clamp(0.0, 1.0); + + let interpolate = |start: u8, end: u8| { + (f64::from(start) + (f64::from(end) - f64::from(start)) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) +} + +fn rgb_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn hash_input(input: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + if hash == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + hash + } +} + +fn shuffle(values: &mut [T], rng: &mut SimpleRng) { + for upper in (1..values.len()).rev() { + let index = rng.range_usize(upper + 1); + values.swap(index, upper); + } +} + +#[derive(Debug, Clone)] +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn new(seed: u64) -> Self { + Self { + state: if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + }, + } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn next_bool(&mut self) -> bool { + self.next_u64() & 1 == 0 + } + + fn next_f64(&mut self) -> f64 { + let value = self.next_u64() >> 11; + value as f64 / ((1_u64 << 53) as f64) + } + + fn range_f64(&mut self, minimum: f64, maximum: f64) -> f64 { + minimum + (maximum - minimum) * self.next_f64() + } + + fn range_usize(&mut self, upper_exclusive: usize) -> usize { + if upper_exclusive <= 1 { + 0 + } else { + (self.next_u64() % upper_exclusive as u64) as usize + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/binarypath.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/binarypath.rs new file mode 100644 index 0000000..856277b --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/binarypath.rs @@ -0,0 +1,311 @@ + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +pub struct Binarypath; + +impl Binarypath { + pub fn new() -> Self { + Self + } +} + +impl Default for Binarypath { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Binarypath { + fn name(&self) -> &str { + "binarypath" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let source_characters = terminal + .characters() + .iter() + .map(|character| { + ( + character.id, + character.input_symbol, + character.position, + ) + }) + .collect::>(); + + if source_characters.is_empty() { + return vec![terminal.render_frame()]; + } + + let mut seed = seed_from_input(input); + let mut groups = Vec::with_capacity(source_characters.len()); + + for (source_id, source_symbol, destination) in source_characters { + let final_style = final_style(destination, width, height); + + if let Some(source) = terminal.character_mut(source_id) { + source.visible = false; + source.set_appearance(source_symbol, final_style); + } + + let route = make_route(destination, width, height, &mut seed); + let binary_string = format!("{:08b}", source_symbol as u32); + let mut bit_ids = Vec::with_capacity(binary_string.len()); + + for bit in binary_string.chars() { + let bit_id = terminal.add_character(bit, route[0]); + + if let Some(character) = terminal.character_mut(bit_id) { + character.visible = false; + character.set_appearance(bit, binary_style(bit)); + } + + bit_ids.push(bit_id); + } + + groups.push(BinaryGroup { + source_id, + bit_ids, + route, + next_bit: 0, + started: false, + complete: false, + }); + } + + shuffle(&mut groups, &mut seed); + + let active_limit = ((groups.len() as f64 * 0.05).ceil() as usize).max(1); + let mut frames = Vec::new(); + + while groups.iter().any(|group| !group.complete) { + let mut active_count = groups + .iter() + .filter(|group| group.started && !group.complete) + .count(); + + if active_count < active_limit { + for group in &mut groups { + if active_count >= active_limit { + break; + } + + if !group.started && !group.complete { + group.started = true; + active_count += 1; + } + } + } + + for group in &mut groups { + if !group.started || group.complete { + continue; + } + + if let Some(&bit_id) = group.bit_ids.get(group.next_bit) { + if let Some(bit) = terminal.character_mut(bit_id) { + bit.visible = true; + bit.set_position(group.route[0]); + + let waypoints = group + .route + .iter() + .copied() + .map(Waypoint::new) + .collect::>(); + let path = Path::with_waypoints(waypoints, 1.0); + bit.motion.activate_path(path); + } + + group.next_bit += 1; + } + } + + terminal.step(); + + for group in &mut groups { + if !group.started || group.complete || group.next_bit < group.bit_ids.len() { + continue; + } + + let travel_complete = group.bit_ids.iter().all(|&bit_id| { + terminal + .character(bit_id) + .and_then(|character| character.motion.active_path()) + .map(|path| !path.is_active()) + .unwrap_or(true) + }); + + if travel_complete { + for &bit_id in &group.bit_ids { + if let Some(bit) = terminal.character_mut(bit_id) { + bit.visible = false; + bit.motion.deactivate(); + } + } + + if let Some(source) = terminal.character_mut(group.source_id) { + source.visible = true; + } + + group.complete = true; + } + } + + frames.push(terminal.render_frame()); + } + + if frames.is_empty() { + frames.push(terminal.render_frame()); + } + + frames + } +} + +struct BinaryGroup { + source_id: CharacterId, + bit_ids: Vec, + route: Vec, + next_bit: usize, + started: bool, + complete: bool, +} + +fn binary_style(bit: char) -> Style { + let color = if bit == '1' { + Color::rgb(0, 209, 255) + } else { + Color::rgb(138, 0, 138) + }; + + Style::default().with_foreground(color) +} + +fn final_style(coord: Coord, width: usize, height: usize) -> Style { + let maximum = width.saturating_sub(1) + height.saturating_sub(1); + let progress = if maximum == 0 { + 1.0 + } else { + let x = coord.x.max(0) as usize; + let y = coord.y.max(0) as usize; + (x + y) as f64 / maximum as f64 + }; + + let color = if progress < 0.5 { + interpolate_color( + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + progress * 2.0, + ) + } else { + interpolate_color( + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + (progress - 0.5) * 2.0, + ) + }; + + Style::default().with_foreground(color) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) = (start, end) + else { + return end; + }; + + let progress = progress.clamp(0.0, 1.0); + let channel = |start: u8, end: u8| { + (start as f64 + (end as f64 - start as f64) * progress).round() as u8 + }; + + Color::rgb( + channel(start_r, end_r), + channel(start_g, end_g), + channel(start_b, end_b), + ) +} + +fn make_route( + destination: Coord, + width: usize, + height: usize, + seed: &mut u64, +) -> Vec { + let right = width.saturating_sub(1) as i32; + let bottom = height.saturating_sub(1) as i32; + let edge = next_random(seed) % 4; + + let start = match edge { + 0 => Coord::new(0, random_coordinate(seed, height)), + 1 => Coord::new(right, random_coordinate(seed, height)), + 2 => Coord::new(random_coordinate(seed, width), 0), + _ => Coord::new(random_coordinate(seed, width), bottom), + }; + + let corner = if edge < 2 { + Coord::new(destination.x, start.y) + } else { + Coord::new(start.x, destination.y) + }; + + vec![start, corner, destination] +} + +fn random_coordinate(seed: &mut u64, extent: usize) -> i32 { + if extent <= 1 { + 0 + } else { + (next_random(seed) % extent as u64) as i32 + } +} + +fn seed_from_input(input: &str) -> u64 { + let mut seed = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + seed ^= u64::from(byte); + seed = seed.wrapping_mul(0x0000_0100_0000_01b3); + } + + if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + } +} + +fn next_random(seed: &mut u64) -> u64 { + let mut value = *seed; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + *seed = value; + value +} + +fn shuffle(values: &mut [T], seed: &mut u64) { + for index in (1..values.len()).rev() { + let swap_index = (next_random(seed) % (index as u64 + 1)) as usize; + values.swap(index, swap_index); + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/blackhole.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/blackhole.rs new file mode 100644 index 0000000..f729fa1 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/blackhole.rs @@ -0,0 +1,478 @@ +use std::collections::HashSet; + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::easing::{in_expo, in_out_sine, out_expo, out_quad}; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; +use crate::utils::EasingFn; + +pub struct Blackhole; + +impl Blackhole { + pub fn new() -> Self { + Self + } +} + +impl Default for Blackhole { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Blackhole { + fn name(&self) -> &str { + "blackhole" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let mut frames = Vec::new(); + + let ids: Vec = terminal + .characters() + .iter() + .map(|character| character.id) + .collect(); + + if ids.is_empty() { + frames.push(terminal.render_frame()); + return frames; + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let center = Coord::new( + (width.saturating_sub(1) / 2) as i32, + (height.saturating_sub(1) / 2) as i32, + ); + + let original_positions: Vec<(CharacterId, Coord)> = terminal + .characters() + .iter() + .map(|character| (character.id, character.position)) + .collect(); + + let seed = input + .bytes() + .fold(0x9e37_79b9_7f4a_7c15_u64, |state, byte| { + state + .wrapping_mul(0x100_0000_01b3) + .wrapping_add(u64::from(byte) + 1) + }); + let mut rng = SmallRng::new(seed); + + let radius_limit = width.min(height).saturating_sub(1) / 2; + let radius = radius_limit.max(1) as i32; + let desired_ring_size = + ((2.0 * std::f64::consts::PI * f64::from(radius)).round() as usize).max(1); + let ring_size = desired_ring_size.min(ids.len()); + let ring_ids = ids[ids.len() - ring_size..].to_vec(); + let ring_set: HashSet = ring_ids.iter().copied().collect(); + let ring_positions = circle_positions(center, radius, ring_size, width, height); + + let star_symbols = ['.', '`', '\'', ',', '*']; + let star_colors = [ + Color::rgb(255, 204, 0), + Color::rgb(255, 153, 0), + Color::rgb(255, 102, 0), + Color::rgb(255, 255, 255), + ]; + + for id in &ids { + let position = random_coord(&mut rng, width, height); + let symbol = star_symbols[rng.index(star_symbols.len())]; + let color = star_colors[rng.index(star_colors.len())]; + + if let Some(character) = terminal.character_mut(*id) { + character.set_position(position); + character.visible = true; + character.set_appearance(symbol, colored_style(color)); + } + } + + frames.push(terminal.render_frame()); + + for (index, id) in ring_ids.iter().enumerate() { + let speed = 0.55 + rng.unit() * 0.25; + activate_move( + &mut terminal, + *id, + ring_positions[index], + speed, + in_out_sine, + ); + } + + run_until_still( + &mut terminal, + &ring_ids, + phase_limit(width, height, 4.0, 40), + &mut frames, + ); + + let blackhole_color = Color::rgb(255, 255, 255); + for (index, id) in ring_ids.iter().enumerate() { + if let Some(character) = terminal.character_mut(*id) { + character.visible = true; + character.set_appearance('*', colored_style(blackhole_color)); + + let mut path = Path::new(0.45); + path.set_looping(true); + path.set_easing(in_out_sine); + + for offset in 0..=ring_positions.len() { + let position_index = (index + offset) % ring_positions.len(); + path.add_waypoint(Waypoint::new(ring_positions[position_index])); + } + + character.motion.activate_path(path); + } + } + + let mut pending: Vec = ids + .iter() + .copied() + .filter(|id| !ring_set.contains(id)) + .collect(); + shuffle(&mut pending, &mut rng); + + let mut consuming = HashSet::new(); + let launch_count = (pending.len() / 40).max(1); + let consume_limit = phase_limit(width, height, 8.0, pending.len() + 100); + + for _ in 0..consume_limit { + for _ in 0..launch_count { + let Some(id) = pending.pop() else { + break; + }; + + let speed = 0.17 + rng.unit() * 0.13; + activate_move(&mut terminal, id, center, speed, in_expo); + consuming.insert(id); + } + + terminal.step(); + + let completed: Vec = consuming + .iter() + .copied() + .filter(|id| !character_is_moving(&terminal, *id)) + .collect(); + + for id in completed { + consuming.remove(&id); + if let Some(character) = terminal.character_mut(id) { + character.visible = false; + character.set_position(center); + } + } + + frames.push(terminal.render_frame()); + + if pending.is_empty() && consuming.is_empty() { + break; + } + } + + for id in &ring_ids { + if let Some(character) = terminal.character_mut(*id) { + character.motion.deactivate(); + } + activate_move(&mut terminal, *id, center, 0.3, in_expo); + } + + run_until_still( + &mut terminal, + &ring_ids, + phase_limit(width, height, 6.0, 40), + &mut frames, + ); + + for id in &ring_ids { + if let Some(character) = terminal.character_mut(*id) { + character.set_position(center); + character.set_appearance('*', colored_style(blackhole_color)); + } + } + + for pulse in 0..8 { + let color = if pulse % 2 == 0 { + Color::rgb(255, 255, 255) + } else { + Color::rgb(255, 153, 0) + }; + + for id in &ring_ids { + if let Some(character) = terminal.character_mut(*id) { + character.set_appearance('*', colored_style(color)); + } + } + + frames.push(terminal.render_frame()); + } + + let explosion_colors = [ + Color::rgb(255, 255, 255), + Color::rgb(255, 204, 0), + Color::rgb(255, 102, 0), + Color::rgb(255, 51, 0), + ]; + + for id in &ids { + let destination = random_coord(&mut rng, width, height); + let color = explosion_colors[rng.index(explosion_colors.len())]; + let symbol = star_symbols[rng.index(star_symbols.len())]; + + if let Some(character) = terminal.character_mut(*id) { + character.visible = true; + character.set_position(center); + character.set_appearance(symbol, colored_style(color)); + } + + activate_move( + &mut terminal, + *id, + destination, + 0.55 + rng.unit() * 0.35, + out_expo, + ); + } + + run_until_still( + &mut terminal, + &ids, + phase_limit(width, height, 5.0, 80), + &mut frames, + ); + + for (id, original_position) in &original_positions { + activate_move(&mut terminal, *id, *original_position, 0.8, out_quad); + } + + let home_limit = phase_limit(width, height, 4.0, 80); + for _ in 0..home_limit { + terminal.step(); + + for (id, original_position) in &original_positions { + if !character_is_moving(&terminal, *id) { + let color = final_color(*original_position, center, width, height); + if let Some(character) = terminal.character_mut(*id) { + character.set_position(*original_position); + character.set_appearance( + character.input_symbol, + colored_style(color), + ); + } + } + } + + frames.push(terminal.render_frame()); + + if !ids + .iter() + .any(|id| character_is_moving(&terminal, *id)) + { + break; + } + } + + for (id, original_position) in original_positions { + let color = final_color(original_position, center, width, height); + if let Some(character) = terminal.character_mut(id) { + character.motion.deactivate(); + character.visible = true; + character.set_position(original_position); + character.set_appearance(character.input_symbol, colored_style(color)); + } + } + + frames.push(terminal.render_frame()); + frames + } +} + +fn activate_move( + terminal: &mut Terminal, + id: CharacterId, + destination: Coord, + speed: f64, + easing: EasingFn, +) { + let Some(start) = terminal.character(id).map(|character| character.position) else { + return; + }; + + let mut path = Path::new(speed); + path.set_easing(easing); + path.add_waypoint(Waypoint::new(start)); + path.add_waypoint(Waypoint::new(destination)); + + if let Some(character) = terminal.character_mut(id) { + character.motion.activate_path(path); + } +} + +fn character_is_moving(terminal: &Terminal, id: CharacterId) -> bool { + terminal + .character(id) + .and_then(|character| character.motion.active_path()) + .is_some_and(|path| path.is_active()) +} + +fn run_until_still( + terminal: &mut Terminal, + ids: &[CharacterId], + limit: usize, + frames: &mut Vec, +) { + for _ in 0..limit { + terminal.step(); + frames.push(terminal.render_frame()); + + if !ids + .iter() + .any(|id| character_is_moving(terminal, *id)) + { + break; + } + } +} + +fn circle_positions( + center: Coord, + radius: i32, + count: usize, + width: usize, + height: usize, +) -> Vec { + let max_x = width.saturating_sub(1) as i32; + let max_y = height.saturating_sub(1) as i32; + + (0..count.max(1)) + .map(|index| { + let angle = + std::f64::consts::TAU * index as f64 / count.max(1) as f64; + let x = center.x + (angle.cos() * f64::from(radius)).round() as i32; + let y = center.y + (angle.sin() * f64::from(radius)).round() as i32; + Coord::new(x.clamp(0, max_x), y.clamp(0, max_y)) + }) + .collect() +} + +fn random_coord(rng: &mut SmallRng, width: usize, height: usize) -> Coord { + Coord::new(rng.index(width.max(1)) as i32, rng.index(height.max(1)) as i32) +} + +fn colored_style(color: Color) -> Style { + Style::default().with_foreground(color) +} + +fn final_color( + position: Coord, + center: Coord, + width: usize, + height: usize, +) -> Color { + let max_distance = ((width.max(1) as f64).powi(2) + + (height.max(1) as f64).powi(2)) + .sqrt() + .max(1.0); + let progress = (position.distance(center) / max_distance).clamp(0.0, 1.0); + + if progress < 0.5 { + interpolate_color( + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + progress * 2.0, + ) + } else { + interpolate_color( + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + (progress - 0.5) * 2.0, + ) + } +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) = (start, end) + else { + return start; + }; + + let progress = progress.clamp(0.0, 1.0); + let interpolate = |from: u8, to: u8| { + (f64::from(from) + (f64::from(to) - f64::from(from)) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) +} + +fn phase_limit(width: usize, height: usize, multiplier: f64, minimum: usize) -> usize { + let diagonal = + ((width.max(1) as f64).powi(2) + (height.max(1) as f64).powi(2)).sqrt(); + ((diagonal * multiplier).ceil() as usize).max(minimum) +} + +fn shuffle(values: &mut [T], rng: &mut SmallRng) { + for index in (1..values.len()).rev() { + let swap_index = rng.index(index + 1); + values.swap(index, swap_index); + } +} + +struct SmallRng { + state: u64, +} + +impl SmallRng { + fn new(seed: u64) -> Self { + Self { + state: if seed == 0 { + 0xa5a5_5a5a_d3c1_b2e7 + } else { + seed + }, + } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn unit(&mut self) -> f64 { + self.next_u64() as f64 / u64::MAX as f64 + } + + fn index(&mut self, upper_bound: usize) -> usize { + if upper_bound <= 1 { + 0 + } else { + (self.next_u64() % upper_bound as u64) as usize + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bouncyballs.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bouncyballs.rs new file mode 100644 index 0000000..3e1aea8 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bouncyballs.rs @@ -0,0 +1,256 @@ + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::{Color, Coord, Style}; + +const BALL_DELAY: u32 = 7; +const MOVEMENT_SPEED: f64 = 0.25; + +const BALL_COLORS: [Color; 5] = [ + Color::rgb(0xd1, 0xf4, 0xa5), + Color::rgb(0x96, 0xe2, 0xa4), + Color::rgb(0x5a, 0xcd, 0xa9), + Color::rgb(0x3a, 0xb8, 0xb0), + Color::rgb(0x2e, 0x9d, 0xb2), +]; + +const FINAL_START: (u8, u8, u8) = (0xc1, 0xf8, 0x0a); +const FINAL_END: (u8, u8, u8) = (0x00, 0xd1, 0xff); +const FINAL_GRADIENT_STEPS: usize = 12; + +#[derive(Debug, Clone)] +struct PendingBall { + id: CharacterId, + start: Coord, + target: Coord, + ball_style: Style, + final_style: Style, +} + +#[derive(Debug, Clone)] +struct ActiveBall { + id: CharacterId, + target: Coord, + final_style: Style, +} + +#[derive(Debug, Clone)] +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn new(seed: u64) -> Self { + Self { + state: if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + }, + } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn index(&mut self, upper_bound: usize) -> usize { + if upper_bound <= 1 { + 0 + } else { + (self.next_u64() % upper_bound as u64) as usize + } + } + + fn shuffle(&mut self, values: &mut [T]) { + for index in (1..values.len()).rev() { + let swap_index = self.index(index + 1); + values.swap(index, swap_index); + } + } +} + +pub struct Bouncyballs; + +impl Bouncyballs { + pub fn new() -> Self { + Self + } +} + +impl Default for Bouncyballs { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Bouncyballs { + fn name(&self) -> &str { + "bouncyballs" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let mut rng = SimpleRng::new(seed_from_input(input)); + let character_data = terminal + .characters() + .iter() + .filter(|character| !character.input_symbol.is_whitespace()) + .map(|character| { + ( + character.id, + character.position, + BALL_COLORS[rng.index(BALL_COLORS.len())], + ) + }) + .collect::>(); + + let mut pending = Vec::with_capacity(character_data.len()); + + for (id, target, ball_color) in character_data { + let start = Coord::new(rng.index(width) as i32, 0); + let final_color = final_gradient_color(target, width, height); + + pending.push(PendingBall { + id, + start, + target, + ball_style: Style::default().with_foreground(ball_color), + final_style: Style::default().with_foreground(final_color), + }); + + if let Some(character) = terminal.character_mut(id) { + character.visible = false; + } + } + + rng.shuffle(&mut pending); + + if pending.is_empty() { + return Vec::new(); + } + + let mut active = Vec::::new(); + let mut frames = Vec::new(); + let mut launch_delay = 0_u32; + + while !pending.is_empty() || !active.is_empty() { + if launch_delay == 0 { + if let Some(ball) = pending.pop() { + if let Some(character) = terminal.character_mut(ball.id) { + character.visible = true; + character.set_position(ball.start); + character.set_appearance('●', ball.ball_style); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(ball.start), Waypoint::new(ball.target)], + MOVEMENT_SPEED, + ); + path.set_easing(out_bounce); + character.motion.activate_path(path); + + active.push(ActiveBall { + id: ball.id, + target: ball.target, + final_style: ball.final_style, + }); + } + + launch_delay = BALL_DELAY; + } + } else { + launch_delay -= 1; + } + + terminal.step(); + + let mut index = 0; + while index < active.len() { + let finished = terminal + .character(active[index].id) + .and_then(|character| character.motion.active_path()) + .map(|path| !path.is_active()) + .unwrap_or(true); + + if finished { + let ball = active.swap_remove(index); + + if let Some(character) = terminal.character_mut(ball.id) { + let symbol = character.input_symbol; + character.set_position(ball.target); + character.set_appearance(symbol, ball.final_style); + } + } else { + index += 1; + } + } + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn out_bounce(progress: f64) -> f64 { + let progress = progress.clamp(0.0, 1.0); + const N1: f64 = 7.5625; + const D1: f64 = 2.75; + + if progress < 1.0 / D1 { + N1 * progress * progress + } else if progress < 2.0 / D1 { + let shifted = progress - 1.5 / D1; + N1 * shifted * shifted + 0.75 + } else if progress < 2.5 / D1 { + let shifted = progress - 2.25 / D1; + N1 * shifted * shifted + 0.9375 + } else { + let shifted = progress - 2.625 / D1; + N1 * shifted * shifted + 0.984375 + } +} + +fn final_gradient_color(coord: Coord, width: usize, height: usize) -> Color { + let maximum = width.saturating_sub(1) + height.saturating_sub(1); + let diagonal = coord.x.max(0) as usize + coord.y.max(0) as usize; + + let progress = if maximum == 0 { + 0.0 + } else { + diagonal.min(maximum) as f64 / maximum as f64 + }; + + let intervals = FINAL_GRADIENT_STEPS.saturating_sub(1).max(1); + let quantized = (progress * intervals as f64).round() / intervals as f64; + + Color::rgb( + interpolate_channel(FINAL_START.0, FINAL_END.0, quantized), + interpolate_channel(FINAL_START.1, FINAL_END.1, quantized), + interpolate_channel(FINAL_START.2, FINAL_END.2, quantized), + ) +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress.clamp(0.0, 1.0); + value.round().clamp(0.0, 255.0) as u8 +} + +fn seed_from_input(input: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + hash +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bubbles.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bubbles.rs new file mode 100644 index 0000000..9b8116c --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/bubbles.rs @@ -0,0 +1,416 @@ +use std::f64::consts::TAU; + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::easing::{out_expo, out_quad}; +use crate::utils::{Color, Coord, Style}; + +const BUBBLE_DELAY: usize = 50; +const BUBBLE_SPEED: f64 = 0.1; +const POP_SPEED: f64 = 0.3; +const RETURN_SPEED: f64 = 0.3; + +const BUBBLE_COLORS: [Color; 4] = [ + Color::rgb(0xd3, 0x3a, 0xff), + Color::rgb(0x43, 0xc2, 0xff), + Color::rgb(0x2d, 0xff, 0x8a), + Color::rgb(0xff, 0xf7, 0x00), +]; + +const POP_COLOR: Color = Color::rgb(0xff, 0xff, 0xff); +const FINAL_COLOR_TOP: Color = Color::rgb(0x43, 0xc2, 0xff); +const FINAL_COLOR_BOTTOM: Color = Color::rgb(0xd3, 0x3a, 0xff); + +pub struct Bubbles; + +impl Bubbles { + pub fn new() -> Self { + Self + } +} + +impl Default for Bubbles { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Bubbles { + fn name(&self) -> &str { + "bubbles" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let mut members = terminal + .characters() + .iter() + .filter(|character| !character.input_symbol.is_whitespace()) + .map(|character| Member { + id: character.id, + home: character.position, + symbol: character.input_symbol, + }) + .collect::>(); + + for character in terminal.characters_mut() { + character.visible = false; + } + + if members.is_empty() { + return Vec::new(); + } + + let mut rng = SimpleRng::new(seed_from_input(input)); + shuffle(&mut members, &mut rng); + + let mut bubbles = Vec::new(); + let mut remaining = members.as_slice(); + + while !remaining.is_empty() { + let maximum = remaining.len().min(20); + let minimum = maximum.min(5); + let group_size = if minimum == maximum { + maximum + } else { + rng.range_inclusive(minimum, maximum) + }; + + let (group, rest) = remaining.split_at(group_size); + remaining = rest; + + let radius = ((group_size as f64 / TAU).ceil() as i32).max(1); + let start_x = rng.range_i32(0, width.saturating_sub(1) as i32); + let start_center = Coord::new(start_x, height as i32 + radius + 1); + + let average_home_y = group + .iter() + .map(|member| i64::from(member.home.y)) + .sum::() as f64 + / group.len() as f64; + + let target_x = rng.range_i32(0, width.saturating_sub(1) as i32); + let target_center = Coord::new( + target_x, + average_home_y.round().clamp(0.0, height.saturating_sub(1) as f64) as i32, + ); + + let color = BUBBLE_COLORS[rng.range(0, BUBBLE_COLORS.len())]; + + bubbles.push(Bubble { + members: group.to_vec(), + radius, + start_center, + target_center, + color, + phase: BubblePhase::Pending, + }); + } + + let mut frames = Vec::new(); + let mut next_release = 0usize; + let dimension_allowance = width + .saturating_add(height) + .saturating_mul(20) + .saturating_add(1_000); + let max_steps = bubbles + .len() + .saturating_mul(BUBBLE_DELAY) + .saturating_add(dimension_allowance); + + for tick in 0..max_steps { + if tick >= next_release { + if let Some(bubble) = bubbles + .iter_mut() + .find(|bubble| bubble.phase == BubblePhase::Pending) + { + activate_bubble(bubble, &mut terminal); + next_release = next_release.saturating_add(BUBBLE_DELAY); + } + } + + terminal.step(); + + for bubble in &mut bubbles { + match bubble.phase { + BubblePhase::Rising if members_have_stopped(bubble, &terminal) => { + begin_pop(bubble, &mut terminal); + } + BubblePhase::Popping if members_have_stopped(bubble, &terminal) => { + begin_return(bubble, &mut terminal, height); + } + BubblePhase::Returning if members_have_stopped(bubble, &terminal) => { + finish_bubble(bubble, &mut terminal, height); + } + _ => {} + } + } + + frames.push(terminal.render_frame()); + + if bubbles + .iter() + .all(|bubble| bubble.phase == BubblePhase::Done) + { + break; + } + } + + if bubbles + .iter() + .any(|bubble| bubble.phase != BubblePhase::Done) + { + for bubble in &mut bubbles { + finish_bubble(bubble, &mut terminal, height); + } + frames.push(terminal.render_frame()); + } + + frames + } +} + +#[derive(Debug, Clone)] +struct Member { + id: CharacterId, + home: Coord, + symbol: char, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BubblePhase { + Pending, + Rising, + Popping, + Returning, + Done, +} + +#[derive(Debug, Clone)] +struct Bubble { + members: Vec, + radius: i32, + start_center: Coord, + target_center: Coord, + color: Color, + phase: BubblePhase, +} + +fn activate_bubble(bubble: &mut Bubble, terminal: &mut Terminal) { + let count = bubble.members.len(); + + for (index, member) in bubble.members.iter().enumerate() { + let offset = circle_offset(bubble.radius, index, count); + let start = bubble.start_center + offset; + let destination = bubble.target_center + offset; + + if let Some(character) = terminal.character_mut(member.id) { + character.visible = true; + character.set_position(start); + character.set_appearance( + member.symbol, + Style::default().with_foreground(bubble.color), + ); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(destination)], + BUBBLE_SPEED, + ); + path.set_easing(out_quad); + character.motion.activate_path(path); + } + } + + bubble.phase = BubblePhase::Rising; +} + +fn begin_pop(bubble: &mut Bubble, terminal: &mut Terminal) { + let count = bubble.members.len(); + let pop_radius = bubble.radius.saturating_add(3); + + for (index, member) in bubble.members.iter().enumerate() { + let offset = circle_offset(pop_radius, index, count); + let destination = bubble.target_center + offset; + + if let Some(character) = terminal.character_mut(member.id) { + let start = character.position; + character.set_appearance( + member.symbol, + Style::default().with_foreground(POP_COLOR), + ); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(destination)], + POP_SPEED, + ); + path.set_easing(out_expo); + character.motion.activate_path(path); + } + } + + bubble.phase = BubblePhase::Popping; +} + +fn begin_return(bubble: &mut Bubble, terminal: &mut Terminal, height: usize) { + for member in &bubble.members { + if let Some(character) = terminal.character_mut(member.id) { + let start = character.position; + let color = final_color(member.home.y, height); + + character.set_appearance(member.symbol, Style::default().with_foreground(color)); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(member.home)], + RETURN_SPEED, + ); + path.set_easing(out_expo); + character.motion.activate_path(path); + } + } + + bubble.phase = BubblePhase::Returning; +} + +fn finish_bubble(bubble: &mut Bubble, terminal: &mut Terminal, height: usize) { + for member in &bubble.members { + if let Some(character) = terminal.character_mut(member.id) { + character.motion.deactivate(); + character.set_position(member.home); + character.set_appearance( + member.symbol, + Style::default().with_foreground(final_color(member.home.y, height)), + ); + character.visible = true; + } + } + + bubble.phase = BubblePhase::Done; +} + +fn members_have_stopped(bubble: &Bubble, terminal: &Terminal) -> bool { + bubble.members.iter().all(|member| { + terminal + .character(member.id) + .and_then(|character| character.motion.active_path()) + .map(|path| !path.is_active()) + .unwrap_or(true) + }) +} + +fn circle_offset(radius: i32, index: usize, count: usize) -> Coord { + if count == 0 { + return Coord::ZERO; + } + + let angle = TAU * index as f64 / count as f64; + Coord::new( + (angle.cos() * radius as f64).round() as i32, + (angle.sin() * radius as f64).round() as i32, + ) +} + +fn final_color(row: i32, height: usize) -> Color { + let denominator = height.saturating_sub(1).max(1) as f64; + let progress = (row.max(0) as f64 / denominator).clamp(0.0, 1.0); + + interpolate_color(FINAL_COLOR_TOP, FINAL_COLOR_BOTTOM, progress) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) = (start, end) + else { + return start; + }; + + let interpolate = |from: u8, to: u8| { + (f64::from(from) + (f64::from(to) - f64::from(from)) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) +} + +fn seed_from_input(input: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + if hash == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + hash + } +} + +fn shuffle(values: &mut [T], rng: &mut SimpleRng) { + for index in (1..values.len()).rev() { + let other = rng.range_inclusive(0, index); + values.swap(index, other); + } +} + +#[derive(Debug, Clone)] +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn new(seed: u64) -> Self { + Self { state: seed } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn range(&mut self, start: usize, end: usize) -> usize { + if end <= start { + return start; + } + + start + (self.next_u64() as usize % (end - start)) + } + + fn range_inclusive(&mut self, start: usize, end: usize) -> usize { + if end <= start { + return start; + } + + start + (self.next_u64() as usize % (end - start + 1)) + } + + fn range_i32(&mut self, start: i32, end: i32) -> i32 { + if end <= start { + return start; + } + + start + (self.next_u64() % (end - start + 1) as u64) as i32 + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/burn.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/burn.rs new file mode 100644 index 0000000..decc470 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/burn.rs @@ -0,0 +1,189 @@ + +use std::collections::BTreeMap; + +use super::Effect; +use crate::engine::animation::{CharacterVisual, Frame, Scene}; +use crate::engine::character::{CharacterId, EffectCharacter}; +use crate::engine::terminal::Terminal; +use crate::utils::graphics::{Color, Style}; + +const STARTING_COLOR: Color = Color::rgb(0x83, 0x73, 0x73); +const BURN_COLORS: [Color; 4] = [ + Color::rgb(0xff, 0xff, 0xff), + Color::rgb(0xff, 0xf7, 0x5d), + Color::rgb(0xfe, 0x65, 0x0d), + Color::rgb(0x8a, 0x00, 0x3c), +]; +const FINAL_GRADIENT_START: Color = Color::rgb(0x00, 0xc3, 0xff); +const FINAL_GRADIENT_END: Color = Color::rgb(0xff, 0xff, 0x1c); +const FINAL_GRADIENT_STEPS: usize = 12; +const BURN_DELAY: usize = 2; +const FLAME_SYMBOLS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + +pub struct Burn; + +impl Burn { + pub fn new() -> Self { + Self + } +} + +impl Default for Burn { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Burn { + fn name(&self) -> &str { + "burn" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let canvas_height = terminal.canvas().height(); + let mut rows: BTreeMap> = BTreeMap::new(); + + for character in terminal.characters_mut() { + rows.entry(character.position.y) + .or_default() + .push(character.id); + + prepare_character(character, canvas_height); + } + + let pending_rows: Vec> = rows.into_iter().rev().map(|(_, ids)| ids).collect(); + let mut next_row = 0; + let mut active = Vec::::new(); + let mut delay_remaining = 0; + let mut frames = Vec::new(); + + while next_row < pending_rows.len() || !active.is_empty() { + if next_row < pending_rows.len() { + if delay_remaining == 0 { + for &id in &pending_rows[next_row] { + if let Some(character) = terminal.character_mut(id) { + if let Some(scene) = burn_scene(character, canvas_height) { + character.animation.activate_scene(scene); + active.push(id); + } + } + } + + next_row += 1; + delay_remaining = BURN_DELAY; + } else { + delay_remaining -= 1; + } + } + + terminal.step(); + + active.retain(|&id| { + terminal + .character(id) + .and_then(|character| character.animation.active_scene()) + .is_some_and(|scene| !scene.is_finished()) + }); + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn prepare_character(character: &mut EffectCharacter, canvas_height: usize) { + let style = Style::default().with_foreground(STARTING_COLOR); + character.set_appearance(character.input_symbol, style); + character.visible = true; + + let _ = canvas_height; +} + +fn burn_scene(character: &EffectCharacter, canvas_height: usize) -> Option { + let mut frames = Vec::with_capacity(BURN_COLORS.len() + 1); + + for (index, color) in BURN_COLORS.iter().copied().enumerate() { + let symbol_index = (character.id.0 as usize) + .wrapping_mul(5) + .wrapping_add(index * 3) + % FLAME_SYMBOLS.len(); + + frames.push(Frame::new( + CharacterVisual::new( + FLAME_SYMBOLS[symbol_index], + Style::default().with_foreground(color), + ), + 2, + )); + } + + frames.push(Frame::new( + CharacterVisual::new( + character.input_symbol, + Style::default().with_foreground(final_color( + character.position.y, + canvas_height, + )), + ), + 1, + )); + + if frames.is_empty() { + None + } else { + Some(Scene::with_frames(frames, false)) + } +} + +fn final_color(row: i32, canvas_height: usize) -> Color { + if canvas_height <= 1 { + return FINAL_GRADIENT_START; + } + + let maximum_row = (canvas_height - 1) as f64; + let row = f64::from(row).clamp(0.0, maximum_row); + + // The original canvas coordinates increase upward. Rust canvas coordinates + // increase downward, so invert the ratio to retain the original gradient. + let vertical_ratio = 1.0 - row / maximum_row; + let step = (vertical_ratio * (FINAL_GRADIENT_STEPS - 1) as f64).round(); + let ratio = step / (FINAL_GRADIENT_STEPS - 1) as f64; + + interpolate_color(FINAL_GRADIENT_START, FINAL_GRADIENT_END, ratio) +} + +fn interpolate_color(start: Color, end: Color, ratio: f64) -> Color { + let ratio = ratio.clamp(0.0, 1.0); + + match (start, end) { + ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) => Color::rgb( + interpolate_channel(start_r, end_r, ratio), + interpolate_channel(start_g, end_g, ratio), + interpolate_channel(start_b, end_b, ratio), + ), + _ if ratio < 0.5 => start, + _ => end, + } +} + +fn interpolate_channel(start: u8, end: u8, ratio: f64) -> u8 { + (f64::from(start) + (f64::from(end) - f64::from(start)) * ratio).round() as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/colorshift.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/colorshift.rs new file mode 100644 index 0000000..74177f7 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/colorshift.rs @@ -0,0 +1,131 @@ + +use super::Effect; +use crate::engine::{CharacterVisual, Frame, Scene, Terminal}; +use crate::utils::{Color, Style}; + +const GRADIENT_STEPS: usize = 12; +const GRADIENT_FRAMES: u32 = 5; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Colorshift; + +impl Colorshift { + pub fn new() -> Self { + Self + } +} + +impl Effect for Colorshift { + fn name(&self) -> &str { + "colorshift" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let spectrum = build_spectrum(); + let canvas_width = terminal.canvas().width(); + let final_spectrum_index = spectrum.len().saturating_sub(1); + + for character in terminal.characters_mut() { + let phase = if canvas_width <= 1 { + 0 + } else { + let x = character + .position + .x + .clamp(0, canvas_width.saturating_sub(1) as i32) + as usize; + + x * final_spectrum_index / canvas_width.saturating_sub(1) + }; + + let mut scene = Scene::new(false); + + for offset in 0..spectrum.len() { + let color = spectrum[(phase + offset) % spectrum.len()]; + let style = Style::default().with_foreground(color); + + scene.add_frame(Frame::new( + CharacterVisual::new(character.input_symbol, style), + GRADIENT_FRAMES, + )); + } + + character.animation.activate_scene(scene); + } + + let mut frames = Vec::new(); + + while terminal.characters().iter().any(|character| { + character + .animation + .active_scene() + .map_or(false, Scene::is_active) + }) { + terminal.step(); + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn build_spectrum() -> Vec { + const STOPS: [Color; 7] = [ + Color::rgb(0xe8, 0x14, 0x16), + Color::rgb(0xff, 0xa5, 0x00), + Color::rgb(0xfa, 0xeb, 0x36), + Color::rgb(0x79, 0xc3, 0x14), + Color::rgb(0x48, 0x7d, 0xe7), + Color::rgb(0x4b, 0x36, 0x9d), + Color::rgb(0x70, 0x36, 0x9d), + ]; + + let mut spectrum = Vec::with_capacity(STOPS.len() * GRADIENT_STEPS); + + for index in 0..STOPS.len() { + let start = STOPS[index]; + let end = STOPS[(index + 1) % STOPS.len()]; + + for step in 0..GRADIENT_STEPS { + let progress = step as f64 / GRADIENT_STEPS as f64; + spectrum.push(interpolate_color(start, end, progress)); + } + } + + spectrum +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) = (start, end) + else { + return start; + }; + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = f64::from(start) + (f64::from(end) - f64::from(start)) * progress; + value.round().clamp(0.0, 255.0) as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/crumble.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/crumble.rs new file mode 100644 index 0000000..1516e5c --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/crumble.rs @@ -0,0 +1,414 @@ + +use super::Effect; +use crate::engine::{CharacterId, CharacterVisual, Frame, Path, Scene, Terminal, Waypoint}; +use crate::utils::easing::{in_quad, out_expo}; +use crate::utils::{Color, Coord, Style}; + +pub struct Crumble; + +impl Crumble { + pub fn new() -> Self { + Self + } +} + +impl Default for Crumble { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum CharacterPhase { + Waiting, + Crumbling, + Falling, + Hidden, + Returning, + Reforming, + Done, +} + +struct CrumbleCharacter { + id: CharacterId, + original_position: Coord, + phase: CharacterPhase, + fall_speed: f64, + return_speed: f64, +} + +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn from_input(input: &str) -> Self { + let mut state = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + state ^= u64::from(byte); + state = state.wrapping_mul(0x0000_0100_0000_01b3); + } + + if state == 0 { + state = 0x9e37_79b9_7f4a_7c15; + } + + Self { state } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn range_f64(&mut self, minimum: f64, maximum: f64) -> f64 { + let ratio = self.next_u64() as f64 / u64::MAX as f64; + minimum + (maximum - minimum) * ratio + } + + fn shuffle(&mut self, values: &mut [T]) { + for index in (1..values.len()).rev() { + let target = (self.next_u64() % (index as u64 + 1)) as usize; + values.swap(index, target); + } + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = f64::from(start) + (f64::from(end) - f64::from(start)) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +fn final_style(position: Coord, canvas_height: usize) -> Style { + let progress = if canvas_height <= 1 { + 0.0 + } else { + (position.y.max(0) as f64 / (canvas_height - 1) as f64).clamp(0.0, 1.0) + }; + + let start = (0x5c, 0xe1, 0xe6); + let end = (0xff, 0x8c, 0x00); + + Style::default().with_foreground(Color::rgb( + interpolate_channel(start.0, end.0, progress), + interpolate_channel(start.1, end.1, progress), + interpolate_channel(start.2, end.2, progress), + )) +} + +fn crumble_scene(symbol: char, style: &Style) -> Scene { + let mut scene = Scene::new(false); + + scene.add_frame(Frame::new( + CharacterVisual::new(symbol, style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▉', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▓', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▒', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('░', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new(' ', style.clone()), + 1, + )); + + scene +} + +fn dust_scene() -> Scene { + let dust_style = Style::default().with_foreground(Color::rgb(0xb2, 0xa1, 0x8f)); + let mut scene = Scene::new(true); + + scene.add_frame(Frame::new( + CharacterVisual::new('░', dust_style.clone()), + 2, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▒', dust_style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('░', dust_style), + 2, + )); + + scene +} + +fn reform_scene(symbol: char, style: &Style) -> Scene { + let mut scene = Scene::new(false); + + scene.add_frame(Frame::new( + CharacterVisual::new('░', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▒', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new('▓', style.clone()), + 1, + )); + scene.add_frame(Frame::new( + CharacterVisual::new(symbol, style.clone()), + 1, + )); + + scene +} + +fn scene_finished(terminal: &Terminal, id: CharacterId) -> bool { + terminal + .character(id) + .and_then(|character| character.animation.active_scene()) + .map(|scene| scene.is_finished()) + .unwrap_or(true) +} + +fn motion_finished(terminal: &Terminal, id: CharacterId) -> bool { + terminal + .character(id) + .and_then(|character| character.motion.active_path()) + .map(|path| !path.is_active()) + .unwrap_or(true) +} + +fn restore_final_frame( + terminal: &mut Terminal, + characters: &mut [CrumbleCharacter], + canvas_height: usize, +) { + for record in characters { + if let Some(character) = terminal.character_mut(record.id) { + let style = final_style(record.original_position, canvas_height); + character.motion.deactivate(); + character.animation.deactivate(); + character.set_position(record.original_position); + character.set_appearance(character.input_symbol, style); + character.visible = true; + } + + record.phase = CharacterPhase::Done; + } +} + +impl Effect for Crumble { + fn name(&self) -> &str { + "crumble" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let canvas_height = terminal.canvas().height(); + + if terminal.characters().is_empty() { + return vec![terminal.render_frame()]; + } + + let mut rng = SimpleRng::from_input(input); + let mut characters = terminal + .characters() + .iter() + .map(|character| CrumbleCharacter { + id: character.id, + original_position: character.position, + phase: CharacterPhase::Waiting, + fall_speed: rng.range_f64(0.35, 0.75), + return_speed: rng.range_f64(0.55, 1.0), + }) + .collect::>(); + + let mut crumble_order = (0..characters.len()).collect::>(); + rng.shuffle(&mut crumble_order); + + let mut return_order = crumble_order.clone(); + return_order.reverse(); + + let mut crumble_cursor = 0; + let mut return_cursor = 0; + let mut stage = 0_u8; + let mut pause_frames = 4_u8; + let mut tick = 0_usize; + let mut frames = Vec::new(); + + let maximum_frames = characters + .len() + .saturating_mul(12) + .saturating_add(canvas_height.saturating_mul(8)) + .saturating_add(128); + + while frames.len() < maximum_frames { + if stage == 0 { + for record in &mut characters { + match record.phase { + CharacterPhase::Crumbling if scene_finished(&terminal, record.id) => { + if let Some(character) = terminal.character_mut(record.id) { + character.animation.deactivate(); + character.set_appearance( + '░', + Style::default() + .with_foreground(Color::rgb(0xb2, 0xa1, 0x8f)), + ); + + let destination = + Coord::new(record.original_position.x, canvas_height as i32 - 1); + let mut path = Path::with_waypoints( + vec![ + Waypoint::new(character.position), + Waypoint::new(destination), + ], + record.fall_speed, + ); + path.set_easing(in_quad); + character.motion.activate_path(path); + } + + record.phase = CharacterPhase::Falling; + } + CharacterPhase::Falling if motion_finished(&terminal, record.id) => { + if let Some(character) = terminal.character_mut(record.id) { + character.motion.deactivate(); + character.animation.deactivate(); + character.visible = false; + } + + record.phase = CharacterPhase::Hidden; + } + _ => {} + } + } + + if crumble_cursor < crumble_order.len() && tick % 2 == 0 { + let record_index = crumble_order[crumble_cursor]; + crumble_cursor += 1; + + let record = &mut characters[record_index]; + if let Some(character) = terminal.character_mut(record.id) { + let style = final_style(record.original_position, canvas_height); + character + .animation + .activate_scene(crumble_scene(character.input_symbol, &style)); + } + record.phase = CharacterPhase::Crumbling; + } + + if crumble_cursor == crumble_order.len() + && characters + .iter() + .all(|record| record.phase == CharacterPhase::Hidden) + { + stage = 1; + } + } else if stage == 1 { + if pause_frames > 0 { + pause_frames -= 1; + } else { + stage = 2; + } + } else { + for record in &mut characters { + match record.phase { + CharacterPhase::Returning if motion_finished(&terminal, record.id) => { + if let Some(character) = terminal.character_mut(record.id) { + let style = final_style(record.original_position, canvas_height); + character.motion.deactivate(); + character.set_position(record.original_position); + character.animation.activate_scene(reform_scene( + character.input_symbol, + &style, + )); + } + + record.phase = CharacterPhase::Reforming; + } + CharacterPhase::Reforming if scene_finished(&terminal, record.id) => { + if let Some(character) = terminal.character_mut(record.id) { + let style = final_style(record.original_position, canvas_height); + character.animation.deactivate(); + character.set_position(record.original_position); + character.set_appearance(character.input_symbol, style); + character.visible = true; + } + + record.phase = CharacterPhase::Done; + } + _ => {} + } + } + + if return_cursor < return_order.len() { + let record_index = return_order[return_cursor]; + return_cursor += 1; + + let record = &mut characters[record_index]; + if record.phase == CharacterPhase::Hidden { + if let Some(character) = terminal.character_mut(record.id) { + let bottom = Coord::new( + record.original_position.x, + canvas_height as i32 - 1, + ); + + character.set_position(bottom); + character.visible = true; + character.animation.activate_scene(dust_scene()); + + let mut path = Path::with_waypoints( + vec![ + Waypoint::new(bottom), + Waypoint::new(record.original_position), + ], + record.return_speed, + ); + path.set_easing(out_expo); + character.motion.activate_path(path); + } + + record.phase = CharacterPhase::Returning; + } + } + + if return_cursor == return_order.len() + && characters + .iter() + .all(|record| record.phase == CharacterPhase::Done) + { + restore_final_frame(&mut terminal, &mut characters, canvas_height); + frames.push(terminal.render_frame()); + break; + } + } + + terminal.step(); + frames.push(terminal.render_frame()); + tick = tick.saturating_add(1); + } + + if characters + .iter() + .any(|record| record.phase != CharacterPhase::Done) + { + restore_final_frame(&mut terminal, &mut characters, canvas_height); + frames.push(terminal.render_frame()); + } + + frames + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/decrypt.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/decrypt.rs new file mode 100644 index 0000000..97e00d7 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/decrypt.rs @@ -0,0 +1,194 @@ + +use super::Effect; +use crate::engine::{CharacterVisual, Frame, Scene, Terminal}; +use crate::utils::graphics::{Color, Style}; + +const TYPING_SPEED: usize = 2; +const CIPHERTEXT: &[char] = &[ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!', '@', '#', '$', '%', '^', '&', '*', '+', '=', + '?', '/', '\\', '|', ':', ';', '<', '>', '~', +]; +const BLOCKS: &[char] = &['█', '▓', '▒', '░']; + +pub struct Decrypt; + +impl Decrypt { + pub fn new() -> Self { + Self + } +} + +impl Default for Decrypt { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Decrypt { + fn name(&self) -> &str { + "decrypt" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let mut rng = LocalRng::from_input(input); + let mut frames = Vec::new(); + + for character in terminal.characters_mut() { + character.visible = false; + } + + let character_count = terminal.characters().len(); + let mut typing_index = 0; + + while typing_index < character_count { + let end = (typing_index + TYPING_SPEED).min(character_count); + + for character in &mut terminal.characters_mut()[typing_index..end] { + let symbol = CIPHERTEXT[rng.index(CIPHERTEXT.len())]; + let color = ciphertext_color(rng.index(3)); + character.visible = true; + character.set_appearance(symbol, Style::default().with_foreground(color)); + } + + typing_index = end; + frames.push(terminal.render_frame()); + } + + let canvas_height = terminal.canvas().height(); + + for character in terminal.characters_mut() { + let mut scene = Scene::new(false); + let cipher_frames = rng.range_inclusive(5, 10); + + for _ in 0..cipher_frames { + let symbol = CIPHERTEXT[rng.index(CIPHERTEXT.len())]; + let color = ciphertext_color(rng.index(3)); + let duration = rng.range_inclusive(1, 2) as u32; + scene.add_frame(Frame::new( + CharacterVisual::new(symbol, Style::default().with_foreground(color)), + duration, + )); + } + + for &symbol in BLOCKS { + let color = ciphertext_color(rng.index(3)); + scene.add_frame(Frame::new( + CharacterVisual::new(symbol, Style::default().with_foreground(color)), + 1, + )); + } + + let final_style = + Style::default().with_foreground(final_color(character.position.y, canvas_height)); + scene.add_frame(Frame::new( + CharacterVisual::new(character.input_symbol, final_style), + 1, + )); + + character.animation.activate_scene(scene); + } + + if character_count == 0 { + frames.push(terminal.render_frame()); + return frames; + } + + loop { + terminal.step(); + frames.push(terminal.render_frame()); + + let any_active = terminal.characters().iter().any(|character| { + character + .animation + .active_scene() + .is_some_and(|scene| scene.is_active()) + }); + + if !any_active { + break; + } + } + + frames + } +} + +fn ciphertext_color(index: usize) -> Color { + match index { + 0 => Color::rgb(0x00, 0x80, 0x00), + 1 => Color::rgb(0x00, 0xcb, 0x00), + _ => Color::rgb(0x00, 0xff, 0x00), + } +} + +fn final_color(row: i32, canvas_height: usize) -> Color { + let progress = if canvas_height <= 1 { + 0.0 + } else { + (row.max(0) as f64 / (canvas_height - 1) as f64).clamp(0.0, 1.0) + }; + + let start = (0_u8, 0xd5_u8, 0_u8); + let end = (0xff_u8, 0xff_u8, 0xff_u8); + + Color::rgb( + interpolate(start.0, end.0, progress), + interpolate(start.1, end.1, progress), + interpolate(start.2, end.2, progress), + ) +} + +fn interpolate(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +struct LocalRng { + state: u64, +} + +impl LocalRng { + fn from_input(input: &str) -> Self { + let mut state = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + state ^= u64::from(byte); + state = state.wrapping_mul(0x0000_0100_0000_01b3); + } + + if state == 0 { + state = 0x9e37_79b9_7f4a_7c15; + } + + Self { state } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn index(&mut self, length: usize) -> usize { + if length <= 1 { + 0 + } else { + (self.next_u64() % length as u64) as usize + } + } + + fn range_inclusive(&mut self, minimum: usize, maximum: usize) -> usize { + if maximum <= minimum { + minimum + } else { + minimum + self.index(maximum - minimum + 1) + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/errorcorrect.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/errorcorrect.rs new file mode 100644 index 0000000..1ed7331 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/errorcorrect.rs @@ -0,0 +1,267 @@ + +use std::collections::{BTreeMap, HashSet}; + +use super::Effect; +use crate::engine::character::CharacterId; +use crate::engine::terminal::Terminal; +use crate::utils::easing::out_quad; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const ERROR_PAIR_RATIO: f64 = 0.1; +const ERROR_HOLD_FRAMES: usize = 10; +const SWAP_FRAMES: usize = 6; +const CORRECT_HOLD_FRAMES: usize = 4; + +const ERROR_COLOR: Color = Color::rgb(255, 0, 0); +const CORRECT_COLOR: Color = Color::rgb(0, 255, 0); + +#[derive(Debug, Clone, Copy)] +struct ErrorPair { + left_id: CharacterId, + right_id: CharacterId, + left_coord: Coord, + right_coord: Coord, +} + +#[derive(Debug, Clone, Copy)] +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn from_input(input: &str) -> Self { + let mut state = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + state ^= u64::from(byte); + state = state.wrapping_mul(0x0000_0100_0000_01b3); + } + + if state == 0 { + state = 0x9e37_79b9_7f4a_7c15; + } + + Self { state } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn shuffle(&mut self, values: &mut [T]) { + for index in (1..values.len()).rev() { + let other = (self.next_u64() as usize) % (index + 1); + values.swap(index, other); + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Errorcorrect; + +impl Errorcorrect { + pub fn new() -> Self { + Self + } +} + +impl Effect for Errorcorrect { + fn name(&self) -> &str { + "errorcorrect" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return vec![terminal.render_frame()]; + } + + let mut pairs = build_error_pairs(&terminal, input); + + if pairs.is_empty() { + return vec![terminal.render_frame()]; + } + + let error_style = Style::default().with_foreground(ERROR_COLOR); + let correct_style = Style::default().with_foreground(CORRECT_COLOR); + + for pair in &pairs { + set_character( + &mut terminal, + pair.left_id, + pair.right_coord, + error_style.clone(), + ); + set_character( + &mut terminal, + pair.right_id, + pair.left_coord, + error_style.clone(), + ); + } + + let mut frames = Vec::new(); + + push_repeated_frame(&mut terminal, &mut frames, ERROR_HOLD_FRAMES); + + for pair in pairs.drain(..) { + for step in 1..=SWAP_FRAMES { + let raw_progress = step as f64 / SWAP_FRAMES as f64; + let progress = out_quad(raw_progress); + + let left_position = pair.right_coord.lerp(pair.left_coord, progress); + let right_position = pair.left_coord.lerp(pair.right_coord, progress); + + set_character( + &mut terminal, + pair.left_id, + left_position, + error_style.clone(), + ); + set_character( + &mut terminal, + pair.right_id, + right_position, + error_style.clone(), + ); + + frames.push(terminal.render_frame()); + } + + set_character( + &mut terminal, + pair.left_id, + pair.left_coord, + correct_style.clone(), + ); + set_character( + &mut terminal, + pair.right_id, + pair.right_coord, + correct_style.clone(), + ); + + push_repeated_frame(&mut terminal, &mut frames, CORRECT_HOLD_FRAMES); + + set_character( + &mut terminal, + pair.left_id, + pair.left_coord, + Style::default(), + ); + set_character( + &mut terminal, + pair.right_id, + pair.right_coord, + Style::default(), + ); + + frames.push(terminal.render_frame()); + } + + if frames.is_empty() { + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn build_error_pairs(terminal: &Terminal, input: &str) -> Vec { + let mut rows: BTreeMap> = BTreeMap::new(); + let mut eligible_count = 0_usize; + + for character in terminal.characters() { + if character.input_symbol.is_whitespace() { + continue; + } + + eligible_count += 1; + rows.entry(character.position.y) + .or_default() + .push((character.id, character.position)); + } + + let mut candidates = Vec::new(); + + for row in rows.values_mut() { + row.sort_by_key(|(_, coord)| coord.x); + + for adjacent in row.windows(2) { + let (left_id, left_coord) = adjacent[0]; + let (right_id, right_coord) = adjacent[1]; + + if right_coord.x == left_coord.x + 1 { + candidates.push(ErrorPair { + left_id, + right_id, + left_coord, + right_coord, + }); + } + } + } + + if candidates.is_empty() { + return Vec::new(); + } + + let requested_pairs = ((eligible_count as f64 * ERROR_PAIR_RATIO).round() as usize) + .max(1) + .min(candidates.len()); + + let mut rng = SimpleRng::from_input(input); + rng.shuffle(&mut candidates); + + let mut selected = Vec::with_capacity(requested_pairs); + let mut used = HashSet::new(); + + for pair in candidates { + if selected.len() >= requested_pairs { + break; + } + + if used.contains(&pair.left_id) || used.contains(&pair.right_id) { + continue; + } + + used.insert(pair.left_id); + used.insert(pair.right_id); + selected.push(pair); + } + + rng.shuffle(&mut selected); + selected +} + +fn set_character( + terminal: &mut Terminal, + id: CharacterId, + position: Coord, + style: Style, +) { + if let Some(character) = terminal.character_mut(id) { + character.set_position(position); + character.set_appearance(character.input_symbol, style); + } +} + +fn push_repeated_frame( + terminal: &mut Terminal, + frames: &mut Vec, + count: usize, +) { + if count == 0 { + return; + } + + let frame = terminal.render_frame(); + frames.extend(std::iter::repeat(frame).take(count)); +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/expand.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/expand.rs new file mode 100644 index 0000000..0b729da --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/expand.rs @@ -0,0 +1,221 @@ +use super::Effect; +use crate::engine::{Path, Terminal, Waypoint}; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const MOVEMENT_SPEED: f64 = 0.35; +const FINAL_GRADIENT_STEPS: usize = 12; +const EXPAND_GRADIENT_STEPS: usize = 10; + +const FINAL_GRADIENT: [Color; 3] = [ + Color::rgb(0x8a, 0x00, 0x8a), + Color::rgb(0x00, 0xd1, 0xff), + Color::rgb(0xff, 0xff, 0xff), +]; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Expand; + +impl Expand { + pub fn new() -> Self { + Self + } +} + +impl Effect for Expand { + fn name(&self) -> &str { + "expand" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let center = Coord::new( + (width.saturating_sub(1) / 2) as i32, + (height.saturating_sub(1) / 2) as i32, + ); + + let targets: Vec = terminal + .characters() + .iter() + .map(|character| character.position) + .collect(); + + let distances: Vec = targets + .iter() + .map(|target| center.distance(*target)) + .collect(); + + let final_colors: Vec = targets + .iter() + .map(|target| final_color_for_row(target.y, height)) + .collect(); + + for character in terminal.characters_mut() { + character.set_position(center); + character.set_style(Style::default().with_foreground(Color::rgb( + 0xff, 0xff, 0xff, + ))); + } + + for (character, target) in terminal + .characters_mut() + .iter_mut() + .zip(targets.iter().copied()) + { + let mut path = Path::with_waypoints( + vec![Waypoint::new(center), Waypoint::new(target)], + MOVEMENT_SPEED, + ); + path.set_easing(in_out_quart); + character.motion.activate_path(path); + } + + let mut frames = Vec::new(); + let mut elapsed_steps = 0usize; + + loop { + let has_active_character = terminal.characters().iter().any(|character| { + character + .motion + .active_path() + .map(|path| path.is_active()) + .unwrap_or(false) + }); + + if !has_active_character { + break; + } + + terminal.step(); + elapsed_steps += 1; + + for (index, character) in terminal.characters_mut().iter_mut().enumerate() { + let distance = distances[index]; + let progress = if distance <= f64::EPSILON { + 1.0 + } else { + let raw = + (elapsed_steps as f64 * MOVEMENT_SPEED / distance).clamp(0.0, 1.0); + in_out_quart(raw) + }; + + let color = stepped_color( + Color::rgb(0xff, 0xff, 0xff), + final_colors[index], + progress, + EXPAND_GRADIENT_STEPS, + ); + + character.set_style(Style::default().with_foreground(color)); + } + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn in_out_quart(progress: f64) -> f64 { + let progress = progress.clamp(0.0, 1.0); + + if progress < 0.5 { + 8.0 * progress.powi(4) + } else { + 1.0 - (-2.0 * progress + 2.0).powi(4) / 2.0 + } +} + +fn final_color_for_row(row: i32, height: usize) -> Color { + if height <= 1 { + return FINAL_GRADIENT[0]; + } + + // The original effect's vertical gradient runs from the first stop at + // the bottom of the canvas to the last stop at the top. + let vertical_progress = + 1.0 - (row.max(0) as f64 / height.saturating_sub(1) as f64); + + multi_stop_color( + &FINAL_GRADIENT, + vertical_progress, + FINAL_GRADIENT_STEPS, + ) +} + +fn multi_stop_color(stops: &[Color], progress: f64, steps_per_segment: usize) -> Color { + if stops.is_empty() { + return Color::rgb(0xff, 0xff, 0xff); + } + + if stops.len() == 1 { + return stops[0]; + } + + let steps_per_segment = steps_per_segment.max(2); + let segment_count = stops.len() - 1; + let spectrum_len = segment_count * steps_per_segment; + let index = rounded_index(progress, spectrum_len.saturating_sub(1)); + + let segment = (index / steps_per_segment).min(segment_count - 1); + let segment_index = index.saturating_sub(segment * steps_per_segment); + let segment_progress = segment_index as f64 / (steps_per_segment - 1) as f64; + + interpolate_color(stops[segment], stops[segment + 1], segment_progress) +} + +fn stepped_color(start: Color, end: Color, progress: f64, steps: usize) -> Color { + let steps = steps.max(2); + let index = rounded_index(progress, steps - 1); + let quantized_progress = index as f64 / (steps - 1) as f64; + interpolate_color(start, end, quantized_progress) +} + +fn rounded_index(progress: f64, final_index: usize) -> usize { + let value = progress.clamp(0.0, 1.0) * final_index as f64; + let lower = value.floor(); + let fraction = value - lower; + + let rounded = if fraction < 0.5 { + lower + } else if fraction > 0.5 { + lower + 1.0 + } else if lower as usize % 2 == 0 { + lower + } else { + lower + 1.0 + }; + + (rounded as usize).min(final_index) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = rgb_components(start); + let (end_r, end_g, end_b) = rgb_components(end); + let progress = progress.clamp(0.0, 1.0); + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn rgb_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/fireworks.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/fireworks.rs new file mode 100644 index 0000000..e290b1d --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/fireworks.rs @@ -0,0 +1,270 @@ + +use std::f64::consts::TAU; + +use super::Effect; +use crate::engine::character::CharacterId; +use crate::engine::terminal::Terminal; +use crate::utils::easing::{in_out_sine, out_expo, out_quad}; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +pub struct Fireworks; + +impl Fireworks { + pub fn new() -> Self { + Self + } +} + +impl Default for Fireworks { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy)] +struct Particle { + id: CharacterId, + symbol: char, + home: Coord, + launch: Coord, + apex: Coord, + burst: Coord, + start_tick: usize, + shell_color: Color, +} + +impl Effect for Fireworks { + fn name(&self) -> &str { + "fireworks" + } + + fn frames(&self, input: &str) -> Vec { + const LAUNCH_STEPS: usize = 14; + const BURST_STEPS: usize = 9; + const RETURN_STEPS: usize = 22; + const LAUNCH_DELAY: usize = 7; + + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let characters = terminal + .characters() + .iter() + .filter(|character| !character.input_symbol.is_whitespace()) + .map(|character| { + ( + character.id, + character.input_symbol, + character.position, + ) + }) + .collect::>(); + + if characters.is_empty() { + return vec![terminal.render_frame()]; + } + + let shell_size = characters.len().saturating_add(49) / 50; + let shell_size = shell_size.max(1); + let mut particles = Vec::with_capacity(characters.len()); + + for (shell_index, shell) in characters.chunks(shell_size).enumerate() { + let shell_seed = mix_seed(shell_index as u64 + 1); + let apex_x = (shell_seed as usize % width) as i32; + + let apex_region = ((height as f64 * 0.65).ceil() as usize) + .max(1) + .min(height); + let apex_y = (mix_seed(shell_seed) as usize % apex_region) as i32; + let launch = Coord::new(apex_x, height.saturating_sub(1) as i32); + let apex = Coord::new(apex_x, apex_y); + let shell_color = FIREWORK_COLORS[shell_index % FIREWORK_COLORS.len()]; + let phase = unit_value(mix_seed(shell_seed ^ 0xa5a5_a5a5)) * TAU; + + let base_radius = ((width.max(height) as f64) * 0.12).round().max(1.0); + + for (particle_index, &(id, symbol, home)) in shell.iter().enumerate() { + let angle = phase + TAU * particle_index as f64 / shell.len() as f64; + let radius_seed = mix_seed(shell_seed ^ particle_index as u64); + let radius = base_radius * (0.7 + unit_value(radius_seed) * 0.6); + + let burst_x = apex.x + (angle.cos() * radius).round() as i32; + let burst_y = apex.y + (angle.sin() * radius * 0.55).round() as i32; + let burst = Coord::new( + burst_x.clamp(0, width.saturating_sub(1) as i32), + burst_y.clamp(0, height.saturating_sub(1) as i32), + ); + + particles.push(Particle { + id, + symbol, + home, + launch, + apex, + burst, + start_tick: shell_index * LAUNCH_DELAY, + shell_color, + }); + } + } + + for particle in &particles { + if let Some(character) = terminal.character_mut(particle.id) { + character.visible = false; + } + } + + let last_start = particles + .iter() + .map(|particle| particle.start_tick) + .max() + .unwrap_or(0); + let final_tick = last_start + LAUNCH_STEPS + BURST_STEPS + RETURN_STEPS; + let mut frames = Vec::with_capacity(final_tick + 1); + + for tick in 0..=final_tick { + for particle in &particles { + let Some(character) = terminal.character_mut(particle.id) else { + continue; + }; + + if tick < particle.start_tick { + character.visible = false; + continue; + } + + let local_tick = tick - particle.start_tick; + character.visible = true; + + if local_tick < LAUNCH_STEPS { + let progress = local_tick as f64 / LAUNCH_STEPS as f64; + let position = particle.launch.lerp(particle.apex, out_quad(progress)); + let launch_symbol = launch_symbol(progress); + + character.set_position(position); + character.set_appearance( + launch_symbol, + Style::default().with_foreground(particle.shell_color), + ); + } else if local_tick < LAUNCH_STEPS + BURST_STEPS { + let burst_tick = local_tick - LAUNCH_STEPS; + let progress = burst_tick as f64 / BURST_STEPS as f64; + let position = particle.apex.lerp(particle.burst, out_expo(progress)); + + character.set_position(position); + character.set_appearance( + particle.symbol, + Style::default().with_foreground(particle.shell_color), + ); + } else if local_tick < LAUNCH_STEPS + BURST_STEPS + RETURN_STEPS { + let return_tick = local_tick - LAUNCH_STEPS - BURST_STEPS; + let progress = return_tick as f64 / RETURN_STEPS as f64; + let position = particle + .burst + .lerp(particle.home, in_out_sine(progress)); + let color = blend_color( + particle.shell_color, + final_color(particle.home, height), + progress, + ); + + character.set_position(position); + character.set_appearance( + particle.symbol, + Style::default().with_foreground(color), + ); + } else { + character.set_position(particle.home); + character.set_appearance( + particle.symbol, + Style::default() + .with_foreground(final_color(particle.home, height)), + ); + } + } + + frames.push(terminal.render_frame()); + } + + frames + } +} + +const FIREWORK_COLORS: [Color; 6] = [ + Color::rgb(136, 206, 235), + Color::rgb(255, 255, 255), + Color::rgb(254, 95, 85), + Color::rgb(240, 182, 127), + Color::rgb(214, 209, 177), + Color::rgb(199, 239, 207), +]; + +fn launch_symbol(progress: f64) -> char { + const SYMBOLS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + let index = (progress.clamp(0.0, 1.0) * (SYMBOLS.len() - 1) as f64).round() as usize; + SYMBOLS[index] +} + +fn final_color(coord: Coord, height: usize) -> Color { + let denominator = height.saturating_sub(1).max(1) as f64; + let progress = (coord.y.max(0) as f64 / denominator).clamp(0.0, 1.0); + + if progress < 0.5 { + blend_color( + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + progress * 2.0, + ) + } else { + blend_color( + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + (progress - 0.5) * 2.0, + ) + } +} + +fn blend_color(start: Color, end: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + + match (start, end) { + ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) => Color::rgb( + blend_channel(start_r, end_r, progress), + blend_channel(start_g, end_g, progress), + blend_channel(start_b, end_b, progress), + ), + _ if progress < 0.5 => start, + _ => end, + } +} + +fn blend_channel(start: u8, end: u8, progress: f64) -> u8 { + (start as f64 + (end as f64 - start as f64) * progress) + .round() + .clamp(0.0, 255.0) as u8 +} + +fn mix_seed(mut value: u64) -> u64 { + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} + +fn unit_value(value: u64) -> f64 { + (value >> 11) as f64 / ((1_u64 << 53) - 1) as f64 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/highlight.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/highlight.rs new file mode 100644 index 0000000..bff4b30 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/highlight.rs @@ -0,0 +1,200 @@ + +use std::collections::{BTreeMap, BTreeSet}; + +use super::Effect; +use crate::engine::{CharacterId, CharacterVisual, Frame, Scene, Terminal}; +use crate::utils::{Color, Style}; + +const HIGHLIGHT_DURATION: u32 = 3; +const FINAL_GRADIENT_STEPS: f64 = 24.0; + +const GRADIENT_START: Color = Color::rgb(0x8a, 0x00, 0x8a); +const GRADIENT_MIDDLE: Color = Color::rgb(0x00, 0xd1, 0xff); +const GRADIENT_END: Color = Color::rgb(0xff, 0xff, 0xff); +const HIGHLIGHT_COLOR: Color = Color::rgb(0xff, 0xff, 0xff); + +#[derive(Debug, Clone, Copy, Default)] +pub struct Highlight; + +impl Highlight { + pub fn new() -> Self { + Self + } +} + +impl Effect for Highlight { + fn name(&self) -> &str { + "highlight" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let mut groups: BTreeMap> = BTreeMap::new(); + + for character in terminal.characters_mut() { + let x_progress = axis_progress(character.position.x, width); + let y_progress = if height <= 1 { + 0.0 + } else { + 1.0 - axis_progress(character.position.y, height) + }; + + let diagonal_progress = if width <= 1 { + y_progress + } else if height <= 1 { + x_progress + } else { + (x_progress + y_progress) / 2.0 + }; + + let final_color = final_gradient(diagonal_progress); + let final_style = Style::default().with_foreground(final_color); + let highlight_style = Style::default().with_foreground(HIGHLIGHT_COLOR); + + character.set_style(final_style.clone()); + + let scene = Scene::with_frames( + vec![ + Frame::new( + CharacterVisual::new(character.input_symbol, highlight_style), + HIGHLIGHT_DURATION, + ), + Frame::new( + CharacterVisual::new(character.input_symbol, final_style), + 1, + ), + ], + false, + ); + + character.animation.activate_scene(scene); + character.animation.deactivate(); + + groups + .entry(character.position.x) + .or_default() + .push(character.id); + } + + let groups: Vec> = groups.into_values().collect(); + let mut next_group = 0; + let mut active = BTreeSet::new(); + let mut frames = Vec::new(); + + while next_group < groups.len() || !active.is_empty() { + if let Some(group) = groups.get(next_group) { + for &id in group { + if let Some(character) = terminal.character_mut(id) { + let final_style = character.style.clone(); + let highlight_style = + Style::default().with_foreground(HIGHLIGHT_COLOR); + + let scene = Scene::with_frames( + vec![ + Frame::new( + CharacterVisual::new( + character.input_symbol, + highlight_style, + ), + HIGHLIGHT_DURATION, + ), + Frame::new( + CharacterVisual::new( + character.input_symbol, + final_style, + ), + 1, + ), + ], + false, + ); + + if character.animation.activate_scene(scene) { + active.insert(id); + } + } + } + + next_group += 1; + } + + terminal.step(); + + active.retain(|id| { + terminal + .character(*id) + .and_then(|character| character.animation.active_scene()) + .is_some_and(|scene| !scene.is_finished()) + }); + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn axis_progress(position: i32, size: usize) -> f64 { + if size <= 1 { + 0.0 + } else { + (position.max(0) as f64 / (size - 1) as f64).clamp(0.0, 1.0) + } +} + +fn final_gradient(progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + let quantized = (progress * FINAL_GRADIENT_STEPS).round() / FINAL_GRADIENT_STEPS; + + if quantized <= 0.5 { + interpolate_color(GRADIENT_START, GRADIENT_MIDDLE, quantized * 2.0) + } else { + interpolate_color( + GRADIENT_MIDDLE, + GRADIENT_END, + (quantized - 0.5) * 2.0, + ) + } +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + + match (start, end) { + ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) => Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ), + (Color::Ansi(start), Color::Ansi(end)) => { + Color::ansi(interpolate_channel(start, end, progress)) + } + _ if progress < 0.5 => start, + _ => end, + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + (f64::from(start) + (f64::from(end) - f64::from(start)) * progress) + .round() + .clamp(0.0, 255.0) as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/laseretch.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/laseretch.rs new file mode 100644 index 0000000..fb1cd1b --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/laseretch.rs @@ -0,0 +1,440 @@ + +use super::Effect; +use crate::engine::character::CharacterId; +use crate::engine::terminal::Terminal; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const COOLING_FRAMES: u32 = 12; + +pub struct Laseretch; + +impl Laseretch { + pub fn new() -> Self { + Self + } +} + +impl Default for Laseretch { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Laseretch { + fn name(&self) -> &str { + "laseretch" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + + let originals = terminal + .characters() + .iter() + .filter(|character| !character.input_symbol.is_whitespace()) + .map(|character| OriginalCharacter { + id: character.id, + symbol: character.input_symbol, + coord: character.position, + final_color: final_color(character.position, height), + }) + .collect::>(); + + for character in terminal.characters_mut() { + if !character.input_symbol.is_whitespace() { + character.visible = false; + } + } + + if originals.is_empty() { + return vec![terminal.render_frame()]; + } + + let mut beam_ids = Vec::with_capacity(width); + for x in 0..width { + let id = terminal.add_character('━', Coord::new(x as i32, 0)); + if let Some(character) = terminal.character_mut(id) { + character.visible = false; + character.set_style(laser_style(x, width)); + } + beam_ids.push(id); + } + + let head_id = terminal.add_character('█', Coord::ZERO); + if let Some(head) = terminal.character_mut(head_id) { + head.visible = false; + head.set_style(Style { + bold: true, + ..Style::default().with_foreground(Color::rgb(255, 255, 180)) + }); + } + + let spark_symbols = ['*', '+', '·', '•']; + let mut spark_ids = Vec::with_capacity(spark_symbols.len()); + for symbol in spark_symbols { + let id = terminal.add_character(symbol, Coord::ZERO); + if let Some(spark) = terminal.character_mut(id) { + spark.visible = false; + } + spark_ids.push(id); + } + + let mut frames = Vec::new(); + let mut cooling = Vec::::new(); + + for row in 0..height { + let row_y = row as i32; + let leftmost = originals + .iter() + .filter(|character| character.coord.y == row_y) + .map(|character| character.coord.x) + .min(); + + let Some(leftmost) = leftmost else { + continue; + }; + + let rightmost = originals + .iter() + .filter(|character| character.coord.y == row_y) + .map(|character| character.coord.x) + .max() + .unwrap_or(leftmost); + + let entry_x = (width.saturating_sub(1)) as i32; + + for x in (leftmost..=entry_x).rev() { + advance_cooling(&mut terminal, &mut cooling); + hide_sparks(&mut terminal, &spark_ids); + position_laser(&mut terminal, &beam_ids, head_id, Coord::new(x, row_y)); + + if x <= rightmost { + if let Some(target) = originals + .iter() + .find(|character| character.coord == Coord::new(x, row_y)) + { + if let Some(character) = terminal.character_mut(target.id) { + character.visible = true; + character.set_appearance( + target.symbol, + Style { + bold: true, + ..Style::default() + .with_foreground(Color::rgb(255, 255, 255)) + }, + ); + } + + cooling.push(CoolingCharacter { + id: target.id, + symbol: target.symbol, + final_color: target.final_color, + age: 0, + }); + + position_sparks( + &mut terminal, + &spark_ids, + target.coord, + 0, + width, + height, + ); + frames.push(terminal.render_frame()); + + advance_cooling(&mut terminal, &mut cooling); + position_sparks( + &mut terminal, + &spark_ids, + target.coord, + 1, + width, + height, + ); + frames.push(terminal.render_frame()); + continue; + } + } + + frames.push(terminal.render_frame()); + } + + hide_laser(&mut terminal, &beam_ids, head_id, &spark_ids); + advance_cooling(&mut terminal, &mut cooling); + frames.push(terminal.render_frame()); + } + + hide_laser(&mut terminal, &beam_ids, head_id, &spark_ids); + + while !cooling.is_empty() { + advance_cooling(&mut terminal, &mut cooling); + frames.push(terminal.render_frame()); + } + + for original in &originals { + if let Some(character) = terminal.character_mut(original.id) { + character.visible = true; + character.set_appearance( + original.symbol, + Style::default().with_foreground(original.final_color), + ); + } + } + + let final_frame = terminal.render_frame(); + if frames.last() != Some(&final_frame) { + frames.push(final_frame); + } + + frames + } +} + +#[derive(Clone, Copy)] +struct OriginalCharacter { + id: CharacterId, + symbol: char, + coord: Coord, + final_color: Color, +} + +struct CoolingCharacter { + id: CharacterId, + symbol: char, + final_color: Color, + age: u32, +} + +fn position_laser( + terminal: &mut Terminal, + beam_ids: &[CharacterId], + head_id: CharacterId, + position: Coord, +) { + for (x, id) in beam_ids.iter().copied().enumerate() { + if let Some(beam) = terminal.character_mut(id) { + beam.position = Coord::new(x as i32, position.y); + beam.visible = x as i32 > position.x; + } + } + + if let Some(head) = terminal.character_mut(head_id) { + head.position = position; + head.visible = true; + } +} + +fn hide_laser( + terminal: &mut Terminal, + beam_ids: &[CharacterId], + head_id: CharacterId, + spark_ids: &[CharacterId], +) { + for id in beam_ids { + if let Some(character) = terminal.character_mut(*id) { + character.visible = false; + } + } + + if let Some(head) = terminal.character_mut(head_id) { + head.visible = false; + } + + hide_sparks(terminal, spark_ids); +} + +fn hide_sparks(terminal: &mut Terminal, spark_ids: &[CharacterId]) { + for id in spark_ids { + if let Some(spark) = terminal.character_mut(*id) { + spark.visible = false; + } + } +} + +fn position_sparks( + terminal: &mut Terminal, + spark_ids: &[CharacterId], + origin: Coord, + phase: usize, + width: usize, + height: usize, +) { + let offsets = if phase == 0 { + [ + Coord::new(0, -1), + Coord::new(1, 0), + Coord::new(0, 1), + Coord::new(-1, 0), + ] + } else { + [ + Coord::new(-1, -1), + Coord::new(1, -1), + Coord::new(1, 1), + Coord::new(-1, 1), + ] + }; + + let colors = [ + Color::rgb(255, 255, 255), + Color::rgb(255, 255, 0), + Color::rgb(255, 128, 0), + Color::rgb(255, 32, 0), + ]; + + for (index, id) in spark_ids.iter().copied().enumerate() { + let coord = origin + offsets[index]; + let is_visible = coord.x >= 0 + && coord.y >= 0 + && (coord.x as usize) < width + && (coord.y as usize) < height; + + if let Some(spark) = terminal.character_mut(id) { + spark.position = coord; + spark.visible = is_visible; + spark.set_style( + Style { + bold: index < 2, + ..Style::default() + } + .with_foreground(colors[index]), + ); + } + } +} + +fn advance_cooling(terminal: &mut Terminal, cooling: &mut Vec) { + let mut index = 0; + + while index < cooling.len() { + cooling[index].age = cooling[index].age.saturating_add(1); + + let age = cooling[index].age; + let id = cooling[index].id; + let symbol = cooling[index].symbol; + let final_color = cooling[index].final_color; + let color = cooling_color(age, final_color); + + if let Some(character) = terminal.character_mut(id) { + character.visible = true; + character.set_appearance( + symbol, + Style { + bold: age < COOLING_FRAMES / 2, + ..Style::default().with_foreground(color) + }, + ); + } + + if age >= COOLING_FRAMES { + cooling.remove(index); + } else { + index += 1; + } + } +} + +fn cooling_color(age: u32, final_color: Color) -> Color { + let progress = (age as f64 / COOLING_FRAMES as f64).clamp(0.0, 1.0); + + if progress < 0.2 { + mix_color( + Color::rgb(255, 255, 255), + Color::rgb(255, 255, 0), + progress / 0.2, + ) + } else if progress < 0.5 { + mix_color( + Color::rgb(255, 255, 0), + Color::rgb(255, 128, 0), + (progress - 0.2) / 0.3, + ) + } else if progress < 0.75 { + mix_color( + Color::rgb(255, 128, 0), + Color::rgb(255, 0, 0), + (progress - 0.5) / 0.25, + ) + } else { + mix_color( + Color::rgb(255, 0, 0), + final_color, + (progress - 0.75) / 0.25, + ) + } +} + +fn laser_style(x: usize, width: usize) -> Style { + let progress = if width <= 1 { + 1.0 + } else { + x as f64 / (width - 1) as f64 + }; + + Style { + bold: true, + ..Style::default().with_foreground(mix_color( + Color::rgb(255, 32, 0), + Color::rgb(255, 255, 0), + progress, + )) + } +} + +fn final_color(coord: Coord, height: usize) -> Color { + let progress = if height <= 1 { + 1.0 + } else { + (coord.y as f64 / (height - 1) as f64).clamp(0.0, 1.0) + }; + + if progress < 0.5 { + mix_color( + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + progress * 2.0, + ) + } else { + mix_color( + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + (progress - 0.5) * 2.0, + ) + } +} + +fn mix_color(start: Color, end: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + + match (start, end) { + ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) => { + let interpolate = |start: u8, end: u8| { + (start as f64 + (end as f64 - start as f64) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) + } + _ if progress < 0.5 => start, + _ => end, + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/matrix.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/matrix.rs new file mode 100644 index 0000000..458c35f --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/matrix.rs @@ -0,0 +1,415 @@ +use std::collections::BTreeMap; + +use super::Effect; +use crate::engine::{CharacterId, Terminal}; +use crate::utils::{Color, Coord, Style}; + +const MATRIX_SYMBOLS: &[u8] = + b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@#$%&*+-=<>"; + +#[derive(Debug, Clone)] +pub struct Matrix; + +impl Matrix { + pub fn new() -> Self { + Self + } +} + +impl Default for Matrix { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Matrix { + fn name(&self) -> &str { + "matrix" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let mut rng = MatrixRng::new(seed_from_input(input)); + + let mut input_characters = BTreeMap::new(); + for character in terminal.characters_mut() { + input_characters.insert(character.position, character.id); + character.visible = false; + } + + let mut rain_characters = vec![CharacterId(0); width * height]; + let mut rain_symbols = vec!['0'; width * height]; + + for x in 0..width { + for y in 0..height { + let index = y * width + x; + let symbol = random_matrix_symbol(&mut rng); + let id = terminal.add_character( + symbol, + Coord::new(x as i32, y as i32), + ); + + if let Some(character) = terminal.character_mut(id) { + character.visible = false; + character.set_appearance(symbol, rain_style(0, 1)); + } + + rain_characters[index] = id; + rain_symbols[index] = symbol; + } + } + + let mut columns = (0..width) + .map(|_| RainColumn::new(height, &mut rng)) + .collect::>(); + let mut frames = Vec::new(); + + let rain_duration = (height.saturating_mul(4) + width / 2 + 24).clamp(32, 120); + + for _ in 0..rain_duration { + for column in &mut columns { + column.step(height, &mut rng); + } + + for x in 0..width { + let column = &columns[x]; + + for y in 0..height { + let index = y * width + x; + let distance = column.head - y as i32; + let visible = distance >= 0 && distance < column.length as i32; + + if visible && rng.chance(1, 12) { + rain_symbols[index] = random_matrix_symbol(&mut rng); + } + + if let Some(character) = terminal.character_mut(rain_characters[index]) { + character.visible = visible; + + if visible { + character.set_appearance( + rain_symbols[index], + rain_style(distance as usize, column.length), + ); + } + } + } + } + + frames.push(terminal.render_frame()); + } + + // Fill the canvas with code before resolving it back into the input. + for reveal_row in 0..height { + for y in 0..=reveal_row { + for x in 0..width { + let index = y * width + x; + + if rng.chance(1, 8) { + rain_symbols[index] = random_matrix_symbol(&mut rng); + } + + if let Some(character) = terminal.character_mut(rain_characters[index]) { + character.visible = true; + character.set_appearance( + rain_symbols[index], + rain_style(reveal_row - y + 1, height.max(1)), + ); + } + } + } + + frames.push(terminal.render_frame()); + } + + let mut resolve_coords = Vec::with_capacity(width * height); + for y in 0..height { + for x in 0..width { + resolve_coords.push(Coord::new(x as i32, y as i32)); + } + } + rng.shuffle(&mut resolve_coords); + + let resolve_steps = (width * height).clamp(12, 36); + let resolve_batch = resolve_coords.len().div_ceil(resolve_steps).max(1); + + for coords in resolve_coords.chunks(resolve_batch) { + for character in terminal.characters_mut() { + if character.visible + && rain_characters.contains(&character.id) + && rng.chance(1, 10) + { + let symbol = random_matrix_symbol(&mut rng); + character.set_appearance(symbol, character.style.clone()); + } + } + + for coord in coords { + let index = coord.y as usize * width + coord.x as usize; + + if let Some(character) = terminal.character_mut(rain_characters[index]) { + character.visible = false; + } + + if let Some(input_id) = input_characters.get(coord).copied() { + let final_color = final_color_for_row(coord.y as usize, height); + + if let Some(character) = terminal.character_mut(input_id) { + character.visible = true; + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(mix_color( + Color::rgb(235, 255, 235), + final_color, + 0.25, + )), + ); + } + } + } + + frames.push(terminal.render_frame()); + } + + // Fade the newly resolved text from the bright rain head color to its + // final green gradient. + for fade_step in 1..=8 { + let progress = fade_step as f64 / 8.0; + + for character in terminal.characters_mut() { + if input_characters + .get(&character.position) + .is_some_and(|id| *id == character.id) + { + let final_color = + final_color_for_row(character.position.y as usize, height); + character.visible = true; + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(mix_color( + Color::rgb(235, 255, 235), + final_color, + progress, + )), + ); + } + } + + frames.push(terminal.render_frame()); + } + + for id in rain_characters { + if let Some(character) = terminal.character_mut(id) { + character.visible = false; + } + } + + for character in terminal.characters_mut() { + if input_characters + .get(&character.position) + .is_some_and(|id| *id == character.id) + { + character.visible = true; + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(final_color_for_row( + character.position.y as usize, + height, + )), + ); + } + } + + let final_frame = terminal.render_frame(); + if frames.last() != Some(&final_frame) { + frames.push(final_frame); + } + + frames + } +} + +#[derive(Debug, Clone)] +struct RainColumn { + head: i32, + length: usize, + fall_delay: usize, + delay_tick: usize, + restart_delay: usize, +} + +impl RainColumn { + fn new(height: usize, rng: &mut MatrixRng) -> Self { + let maximum_length = height.max(3); + let minimum_length = maximum_length.min(3); + + Self { + head: -(rng.range(height.saturating_add(1)) as i32), + length: minimum_length + + rng.range(maximum_length.saturating_sub(minimum_length) + 1), + fall_delay: 1 + rng.range(4), + delay_tick: 0, + restart_delay: rng.range(10), + } + } + + fn step(&mut self, height: usize, rng: &mut MatrixRng) { + if self.restart_delay > 0 { + self.restart_delay -= 1; + return; + } + + self.delay_tick += 1; + if self.delay_tick < self.fall_delay { + return; + } + + self.delay_tick = 0; + self.head += 1; + + if self.head - self.length as i32 > height as i32 { + let maximum_length = height.max(3); + let minimum_length = maximum_length.min(3); + + self.head = -(rng.range(height.saturating_add(1)) as i32); + self.length = minimum_length + + rng.range(maximum_length.saturating_sub(minimum_length) + 1); + self.fall_delay = 1 + rng.range(4); + self.restart_delay = 2 + rng.range(14); + } + } +} + +fn rain_style(distance_from_head: usize, tail_length: usize) -> Style { + let progress = if tail_length <= 1 { + 0.0 + } else { + distance_from_head as f64 / (tail_length - 1) as f64 + } + .clamp(0.0, 1.0); + + let color = if distance_from_head == 0 { + Color::rgb(225, 255, 225) + } else { + mix_color( + Color::rgb(80, 255, 105), + Color::rgb(0, 55, 12), + progress, + ) + }; + + let mut style = Style::default().with_foreground(color); + style.bold = distance_from_head <= 1; + style.dim = progress > 0.72; + style +} + +fn final_color_for_row(row: usize, height: usize) -> Color { + let progress = if height <= 1 { + 0.0 + } else { + row as f64 / (height - 1) as f64 + }; + + mix_color( + Color::rgb(85, 255, 115), + Color::rgb(0, 145, 45), + progress, + ) +} + +fn mix_color(start: Color, end: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + + match (start, end) { + ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) => Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ), + (_, color) => color, + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + (start as f64 + (end as f64 - start as f64) * progress) + .round() + .clamp(0.0, 255.0) as u8 +} + +fn random_matrix_symbol(rng: &mut MatrixRng) -> char { + MATRIX_SYMBOLS[rng.range(MATRIX_SYMBOLS.len())] as char +} + +fn seed_from_input(input: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + if hash == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + hash + } +} + +#[derive(Debug, Clone)] +struct MatrixRng { + state: u64, +} + +impl MatrixRng { + fn new(seed: u64) -> Self { + Self { + state: if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + }, + } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn range(&mut self, upper_bound: usize) -> usize { + if upper_bound <= 1 { + 0 + } else { + (self.next_u64() % upper_bound as u64) as usize + } + } + + fn chance(&mut self, numerator: usize, denominator: usize) -> bool { + denominator != 0 && self.range(denominator) < numerator + } + + fn shuffle(&mut self, values: &mut [T]) { + for index in (1..values.len()).rev() { + let other = self.range(index + 1); + values.swap(index, other); + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/middleout.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/middleout.rs new file mode 100644 index 0000000..5020210 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/middleout.rs @@ -0,0 +1,198 @@ + +use super::Effect; +use crate::engine::animation::{CharacterVisual, Frame, Scene}; +use crate::engine::motion::{Path, Waypoint}; +use crate::engine::terminal::Terminal; +use crate::utils::easing::in_out_sine; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const STARTING_COLOR: Color = Color::rgb(255, 255, 255); +const CENTER_MOVEMENT_SPEED: f64 = 0.35; +const FULL_MOVEMENT_SPEED: f64 = 0.35; +const COLOR_STEPS: usize = 10; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CharacterPhase { + Center, + Full, + Done, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Middleout; + +impl Middleout { + pub fn new() -> Self { + Self + } +} + +impl Effect for Middleout { + fn name(&self) -> &str { + "middleout" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let center = Coord::new( + (width.saturating_sub(1) / 2) as i32, + (height.saturating_sub(1) / 2) as i32, + ); + + let targets = terminal + .characters() + .iter() + .map(|character| character.position) + .collect::>(); + let final_colors = targets + .iter() + .map(|target| final_color_for_row(target.y, height)) + .collect::>(); + let mut phases = vec![CharacterPhase::Center; targets.len()]; + + for (index, character) in terminal.characters_mut().iter_mut().enumerate() { + let target = targets[index]; + let middle_coord = Coord::new(center.x, target.y); + + character.set_position(center); + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(STARTING_COLOR), + ); + + let mut center_path = Path::with_waypoints( + vec![Waypoint::new(center), Waypoint::new(middle_coord)], + CENTER_MOVEMENT_SPEED, + ); + center_path.set_easing(in_out_sine); + character.motion.activate_path(center_path); + } + + let mut frames = Vec::new(); + + while phases + .iter() + .any(|phase| *phase != CharacterPhase::Done) + { + terminal.step(); + + for (index, character) in terminal.characters_mut().iter_mut().enumerate() { + let motion_active = character + .motion + .active_path() + .is_some_and(|path| path.is_active()); + + match phases[index] { + CharacterPhase::Center if !motion_active => { + let mut full_path = Path::with_waypoints( + vec![ + Waypoint::new(character.position), + Waypoint::new(targets[index]), + ], + FULL_MOVEMENT_SPEED, + ); + full_path.set_easing(in_out_sine); + character.motion.activate_path(full_path); + + let scene = + color_scene(character.input_symbol, final_colors[index], COLOR_STEPS); + character.animation.activate_scene(scene); + phases[index] = CharacterPhase::Full; + } + CharacterPhase::Full => { + let animation_active = character + .animation + .active_scene() + .is_some_and(|scene| scene.is_active()); + + if !motion_active && !animation_active { + character.set_position(targets[index]); + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(final_colors[index]), + ); + phases[index] = CharacterPhase::Done; + } + } + CharacterPhase::Center + | CharacterPhase::Full + | CharacterPhase::Done => {} + } + } + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn color_scene(symbol: char, final_color: Color, steps: usize) -> Scene { + let steps = steps.max(1); + let mut scene = Scene::new(false); + + for index in 0..steps { + let progress = if steps == 1 { + 1.0 + } else { + index as f64 / (steps - 1) as f64 + }; + let color = interpolate_color(STARTING_COLOR, final_color, progress); + let style = Style::default().with_foreground(color); + scene.add_frame(Frame::new(CharacterVisual::new(symbol, style), 1)); + } + + scene +} + +fn final_color_for_row(row: i32, height: usize) -> Color { + const STOPS: [Color; 3] = [ + Color::rgb(138, 0, 138), + Color::rgb(0, 209, 255), + Color::rgb(255, 255, 255), + ]; + + let progress = if height <= 1 { + 0.0 + } else { + 1.0 - (row.max(0) as f64 / (height - 1) as f64) + } + .clamp(0.0, 1.0); + + let scaled = progress * (STOPS.len() - 1) as f64; + let segment = (scaled.floor() as usize).min(STOPS.len() - 2); + interpolate_color(STOPS[segment], STOPS[segment + 1], scaled - segment as f64) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = rgb_components(start); + let (end_r, end_g, end_b) = rgb_components(end); + let progress = progress.clamp(0.0, 1.0); + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn rgb_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + (f64::from(start) + (f64::from(end) - f64::from(start)) * progress) + .round() + .clamp(0.0, 255.0) as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/mod.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/mod.rs new file mode 100644 index 0000000..6b5c5f2 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/mod.rs @@ -0,0 +1,84 @@ +pub mod binarypath; +pub mod blackhole; +pub mod bouncyballs; +pub mod bubbles; +pub mod burn; +pub mod colorshift; +pub mod crumble; +pub mod errorcorrect; +pub mod expand; +pub mod fireworks; +pub mod highlight; +pub mod laseretch; +pub mod matrix; +pub mod middleout; +pub mod overflow; +pub mod pour; +pub mod beams; +pub mod decrypt; +pub mod orbittingvolley; +pub mod print; +pub mod rain; +pub mod rings; +pub mod scattered; +pub mod slice; +pub mod slide; +pub mod smoke; +pub mod spotlights; +pub mod spray; +pub mod swarm; +pub mod sweep; +pub mod synthgrid; +pub mod thunderstorm; +pub mod unstable; +pub mod vhstape; +pub mod waves; +pub mod wipe; +pub mod random_sequence; + +pub trait Effect { + fn name(&self) -> &str; + fn frames(&self, input: &str) -> Vec; +} + +pub fn registry() -> Vec> { + vec![ + Box::new(binarypath::Binarypath::new()), + Box::new(blackhole::Blackhole::new()), + Box::new(bouncyballs::Bouncyballs::new()), + Box::new(bubbles::Bubbles::new()), + Box::new(burn::Burn::new()), + Box::new(colorshift::Colorshift::new()), + Box::new(crumble::Crumble::new()), + Box::new(errorcorrect::Errorcorrect::new()), + Box::new(expand::Expand::new()), + Box::new(fireworks::Fireworks::new()), + Box::new(highlight::Highlight::new()), + Box::new(laseretch::Laseretch::new()), + Box::new(matrix::Matrix::new()), + Box::new(middleout::Middleout::new()), + Box::new(overflow::Overflow::new()), + Box::new(pour::Pour::new()), + Box::new(beams::Beams::new()), + Box::new(decrypt::Decrypt::new()), + Box::new(orbittingvolley::Orbittingvolley::new()), + Box::new(print::Print::new()), + Box::new(rain::Rain::new()), + Box::new(rings::Rings::new()), + Box::new(scattered::Scattered::new()), + Box::new(slice::Slice::new()), + Box::new(slide::Slide::new()), + Box::new(smoke::Smoke::new()), + Box::new(spotlights::Spotlights::new()), + Box::new(spray::Spray::new()), + Box::new(swarm::Swarm::new()), + Box::new(sweep::Sweep::new()), + Box::new(synthgrid::Synthgrid::new()), + Box::new(thunderstorm::Thunderstorm::new()), + Box::new(unstable::Unstable::new()), + Box::new(vhstape::Vhstape::new()), + Box::new(waves::Waves::new()), + Box::new(wipe::Wipe::new()), + Box::new(random_sequence::RandomSequence::new()) + ] +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/orbittingvolley.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/orbittingvolley.rs new file mode 100644 index 0000000..01d5ff6 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/orbittingvolley.rs @@ -0,0 +1,267 @@ + +use std::collections::VecDeque; + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::easing::out_sine; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +#[derive(Debug, Clone)] +pub struct Orbittingvolley { + launcher_symbol: char, + launcher_color: Color, + movement_speed: f64, + launcher_movement_speed: f64, + volley_size: usize, + launch_delay: usize, +} + +impl Orbittingvolley { + pub fn new() -> Self { + Self { + launcher_symbol: '█', + launcher_color: Color::rgb(255, 255, 255), + movement_speed: 1.0, + launcher_movement_speed: 0.5, + volley_size: 4, + launch_delay: 2, + } + } +} + +impl Default for Orbittingvolley { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Orbittingvolley { + fn name(&self) -> &str { + "orbittingvolley" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let max_x = terminal.canvas().width().saturating_sub(1) as i32; + let max_y = terminal.canvas().height().saturating_sub(1) as i32; + + let corners = [ + Coord::new(0, 0), + Coord::new(max_x, 0), + Coord::new(max_x, max_y), + Coord::new(0, max_y), + ]; + + let original_characters: Vec<(CharacterId, Coord)> = terminal + .characters() + .iter() + .map(|character| (character.id, character.position)) + .collect(); + + for character in terminal.characters_mut() { + character.visible = false; + } + + let launcher_ids: [CharacterId; 4] = std::array::from_fn(|index| { + let id = terminal.add_character(self.launcher_symbol, corners[index]); + + if let Some(launcher) = terminal.character_mut(id) { + launcher.set_appearance( + self.launcher_symbol, + Style::default().with_foreground(self.launcher_color), + ); + launcher.visible = true; + } + + id + }); + + let mut magazines: [VecDeque; 4] = + std::array::from_fn(|_| VecDeque::new()); + + for &(id, destination) in &original_characters { + let launcher_index = nearest_corner(destination, &corners); + magazines[launcher_index].push_back(id); + } + + for magazine in &mut magazines { + let slice = magazine.make_contiguous(); + slice.sort_by_key(|id| { + terminal + .character(*id) + .map(|character| { + let center_distance = distance_squared_from_center( + character.position, + max_x, + max_y, + ); + (center_distance, character.id) + }) + .unwrap_or((0, *id)) + }); + } + + let mut frames = Vec::new(); + let mut launcher_progress = 0.0; + let mut launch_delay_remaining = 0usize; + + loop { + if launch_delay_remaining == 0 { + for launcher_index in 0..launcher_ids.len() { + let launcher_position = terminal + .character(launcher_ids[launcher_index]) + .map(|launcher| launcher.position) + .unwrap_or(corners[launcher_index]); + + for _ in 0..self.volley_size { + let Some(character_id) = magazines[launcher_index].pop_front() else { + break; + }; + + let Some(destination) = original_characters + .iter() + .find_map(|(id, coord)| (*id == character_id).then_some(*coord)) + else { + continue; + }; + + let style = final_style(destination, max_x, max_y); + let mut path = Path::with_waypoints( + vec![ + Waypoint::new(launcher_position), + Waypoint::new(destination), + ], + self.movement_speed, + ); + path.set_easing(out_sine); + + if let Some(character) = terminal.character_mut(character_id) { + character.set_position(launcher_position); + character.set_appearance(character.input_symbol, style); + character.visible = true; + character.motion.activate_path(path); + } + } + } + + launch_delay_remaining = self.launch_delay; + } else { + launch_delay_remaining -= 1; + } + + terminal.step(); + + let orbit_span = f64::from(max_x.max(1)); + launcher_progress += self.launcher_movement_speed / orbit_span; + if launcher_progress > 1.0 { + launcher_progress -= 1.0; + } + + update_launchers( + &mut terminal, + launcher_ids, + launcher_progress, + max_x, + max_y, + ); + + frames.push(terminal.render_frame()); + + let magazines_empty = magazines.iter().all(VecDeque::is_empty); + let characters_at_rest = original_characters.iter().all(|(id, _)| { + terminal + .character(*id) + .and_then(|character| character.motion.active_path()) + .is_none_or(|path| !path.is_active()) + }); + + if magazines_empty && characters_at_rest { + for launcher_id in launcher_ids { + if let Some(launcher) = terminal.character_mut(launcher_id) { + launcher.visible = false; + } + } + + for &(id, destination) in &original_characters { + if let Some(character) = terminal.character_mut(id) { + character.set_position(destination); + character.visible = true; + } + } + + let final_frame = terminal.render_frame(); + if frames.last() != Some(&final_frame) { + frames.push(final_frame); + } + break; + } + } + + if frames.is_empty() { + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn nearest_corner(coord: Coord, corners: &[Coord; 4]) -> usize { + corners + .iter() + .enumerate() + .min_by_key(|(index, corner)| (coord.manhattan_distance(**corner), *index)) + .map(|(index, _)| index) + .unwrap_or(0) +} + +fn distance_squared_from_center(coord: Coord, max_x: i32, max_y: i32) -> i64 { + let doubled_x = i64::from(coord.x) * 2 - i64::from(max_x); + let doubled_y = i64::from(coord.y) * 2 - i64::from(max_y); + doubled_x * doubled_x + doubled_y * doubled_y +} + +fn final_style(coord: Coord, max_x: i32, max_y: i32) -> Style { + const START: (u8, u8, u8) = (49, 233, 129); + const END: (u8, u8, u8) = (27, 231, 255); + + let denominator = (max_x + max_y).max(1) as f64; + let progress = f64::from(coord.x + coord.y).clamp(0.0, denominator) / denominator; + + let interpolate = |start: u8, end: u8| { + (f64::from(start) + (f64::from(end) - f64::from(start)) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Style::default().with_foreground(Color::rgb( + interpolate(START.0, END.0), + interpolate(START.1, END.1), + interpolate(START.2, END.2), + )) +} + +fn update_launchers( + terminal: &mut Terminal, + launcher_ids: [CharacterId; 4], + progress: f64, + max_x: i32, + max_y: i32, +) { + let progress = progress.clamp(0.0, 1.0); + let x_forward = (f64::from(max_x) * progress).trunc() as i32; + let y_forward = (f64::from(max_y) * progress).trunc() as i32; + + let positions = [ + Coord::new(x_forward, 0), + Coord::new(max_x, y_forward), + Coord::new(max_x - x_forward, max_y), + Coord::new(0, max_y - y_forward), + ]; + + for (launcher_id, position) in launcher_ids.into_iter().zip(positions) { + if let Some(launcher) = terminal.character_mut(launcher_id) { + launcher.set_position(position); + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/overflow.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/overflow.rs new file mode 100644 index 0000000..1c3d3fb --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/overflow.rs @@ -0,0 +1,183 @@ + +use super::Effect; +use crate::engine::Terminal; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const OVERFLOW_CYCLES: usize = 2; +const OVERFLOW_SPEED: usize = 3; +const GRADIENT_STOPS: [(u8, u8, u8); 3] = [ + (0xf2, 0xeb, 0xc0), + (0x8d, 0xbf, 0xb3), + (0xf2, 0xeb, 0xc0), +]; + +#[derive(Debug, Clone)] +pub struct Overflow; + +impl Overflow { + pub fn new() -> Self { + Self + } +} + +impl Default for Overflow { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Overflow { + fn name(&self) -> &str { + "overflow" + } + + fn frames(&self, input: &str) -> Vec { + let rows = input_rows(input); + let width = rows + .iter() + .map(|row| row.len()) + .max() + .unwrap_or(0) + .max(1); + let height = rows.len().max(1); + + let mut pending_rows = Vec::with_capacity(height * (OVERFLOW_CYCLES + 1)); + let mut random_state = seed_from_input(input); + + for _ in 0..OVERFLOW_CYCLES { + let mut shuffled = rows.clone(); + shuffle(&mut shuffled, &mut random_state); + pending_rows.extend(shuffled); + } + + // The final pass is not shuffled, allowing the original text to settle + // into its proper row order. + pending_rows.extend(rows); + + let mut active_rows: Vec = Vec::with_capacity(height); + let mut frames = Vec::new(); + + for row in pending_rows { + for active_row in &mut active_rows { + active_row.y -= 1; + } + active_rows.retain(|active_row| active_row.y >= 0); + + active_rows.push(ActiveRow { + symbols: row, + y: height as i32 - 1, + }); + + let frame = render_rows(width, height, &active_rows); + + // Overflow speed controls the time between row advances. + for _ in 0..OVERFLOW_SPEED { + frames.push(frame.clone()); + } + } + + if frames.is_empty() { + frames.push(Terminal::new(width, height).render_frame()); + } + + frames + } +} + +#[derive(Debug, Clone)] +struct ActiveRow { + symbols: Vec, + y: i32, +} + +fn input_rows(input: &str) -> Vec> { + if input.is_empty() { + vec![Vec::new()] + } else { + input.lines().map(|line| line.chars().collect()).collect() + } +} + +fn render_rows(width: usize, height: usize, rows: &[ActiveRow]) -> String { + let mut terminal = Terminal::new(width, height); + + for row in rows { + if row.y < 0 || row.y >= height as i32 { + continue; + } + + let color = gradient_color(row.y as usize, height); + let style = Style::default().with_foreground(color); + + for (x, symbol) in row.symbols.iter().copied().enumerate() { + if x >= width { + break; + } + + let id = terminal.add_character(symbol, Coord::new(x as i32, row.y)); + if let Some(character) = terminal.character_mut(id) { + character.set_style(style.clone()); + } + } + } + + terminal.render_frame() +} + +fn gradient_color(row: usize, height: usize) -> Color { + if height <= 1 { + let (r, g, b) = GRADIENT_STOPS[0]; + return Color::rgb(r, g, b); + } + + let progress = row as f64 / (height - 1) as f64; + let scaled = progress * (GRADIENT_STOPS.len() - 1) as f64; + let start_index = (scaled.floor() as usize).min(GRADIENT_STOPS.len() - 2); + let local_progress = scaled - start_index as f64; + + let start = GRADIENT_STOPS[start_index]; + let end = GRADIENT_STOPS[start_index + 1]; + + Color::rgb( + interpolate(start.0, end.0, local_progress), + interpolate(start.1, end.1, local_progress), + interpolate(start.2, end.2, local_progress), + ) +} + +fn interpolate(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +fn seed_from_input(input: &str) -> u64 { + let mut seed = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + seed ^= u64::from(byte); + seed = seed.wrapping_mul(0x0000_0100_0000_01b3); + } + + if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + } +} + +fn next_random(state: &mut u64) -> u64 { + let mut value = *state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + *state = value; + value +} + +fn shuffle(values: &mut [T], state: &mut u64) { + for upper in (1..values.len()).rev() { + let index = (next_random(state) % (upper as u64 + 1)) as usize; + values.swap(upper, index); + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/pour.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/pour.rs new file mode 100644 index 0000000..048c32c --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/pour.rs @@ -0,0 +1,243 @@ + +use std::collections::{BTreeMap, VecDeque}; + +use super::Effect; +use crate::engine::animation::{CharacterVisual, Frame, Scene}; +use crate::engine::character::CharacterId; +use crate::engine::motion::{Path, Waypoint}; +use crate::engine::terminal::Terminal; +use crate::utils::easing::in_quad; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const MOVEMENT_SPEED: f64 = 0.2; +const POUR_GAP: usize = 1; +const COLOR_STEPS: usize = 12; +const STARTING_COLOR: Color = Color::rgb(0x00, 0xc2, 0xff); +const FINAL_GRADIENT: [Color; 3] = [ + Color::rgb(0x8a, 0x00, 0x8a), + Color::rgb(0x00, 0xd1, 0xff), + Color::rgb(0xff, 0xff, 0xff), +]; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Pour; + +impl Pour { + pub fn new() -> Self { + Self + } +} + +impl Effect for Pour { + fn name(&self) -> &str { + "pour" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let canvas_height = terminal.canvas().height(); + let canvas_width = terminal.canvas().width(); + + let mut rows: BTreeMap> = BTreeMap::new(); + for character in terminal.characters() { + rows.entry(character.position.y) + .or_default() + .push(character.id); + } + + let mut seed = initial_seed(&terminal); + let mut pending = VecDeque::new(); + + // Pouring downward must fill the rows furthest from the source first. + for (_, mut row) in rows.into_iter().rev() { + shuffle(&mut row, &mut seed); + pending.extend(row); + } + + for character in terminal.characters_mut() { + character.visible = false; + } + + let mut active = Vec::::new(); + let mut gap_remaining = 0usize; + let character_count = pending.len(); + let longest_path = canvas_height.saturating_sub(1); + let max_frames = character_count + .saturating_mul(POUR_GAP.saturating_add(2)) + .saturating_add(longest_path.saturating_mul(6)) + .saturating_add(COLOR_STEPS) + .saturating_add(16); + + let mut frames = Vec::new(); + + for _ in 0..max_frames { + if pending.is_empty() && active.is_empty() { + break; + } + + if gap_remaining == 0 { + if let Some(id) = pending.pop_front() { + activate_character(&mut terminal, id, canvas_width, canvas_height); + active.push(id); + gap_remaining = POUR_GAP; + } + } else { + gap_remaining -= 1; + } + + terminal.step(); + + active.retain(|id| { + terminal.character(*id).is_some_and(|character| { + let motion_active = character + .motion + .active_path() + .is_some_and(|path| path.is_active()); + let animation_active = character + .animation + .active_scene() + .is_some_and(|scene| scene.is_active()); + + motion_active || animation_active + }) + }); + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn activate_character( + terminal: &mut Terminal, + id: CharacterId, + canvas_width: usize, + canvas_height: usize, +) { + let Some(character) = terminal.character_mut(id) else { + return; + }; + + let destination = character.position; + let starting_position = Coord::new(destination.x, 0); + let final_color = gradient_color_for_coord( + destination, + canvas_width, + canvas_height, + &FINAL_GRADIENT, + ); + + character.position = starting_position; + character.visible = true; + + let mut path = Path::with_waypoints( + vec![ + Waypoint::new(starting_position), + Waypoint::new(destination), + ], + MOVEMENT_SPEED, + ); + path.set_easing(in_quad); + character.motion.activate_path(path); + + let mut scene = Scene::new(false); + for step in 0..COLOR_STEPS { + let denominator = COLOR_STEPS.saturating_sub(1).max(1); + let progress = step as f64 / denominator as f64; + let color = interpolate_color(STARTING_COLOR, final_color, progress); + let style = Style::default().with_foreground(color); + + scene.add_frame(Frame::new( + CharacterVisual::new(character.input_symbol, style), + 1, + )); + } + + character.animation.activate_scene(scene); +} + +fn gradient_color_for_coord( + coord: Coord, + _canvas_width: usize, + canvas_height: usize, + stops: &[Color], +) -> Color { + if stops.is_empty() { + return STARTING_COLOR; + } + + if stops.len() == 1 || canvas_height <= 1 { + return stops[0]; + } + + let y = coord.y.max(0) as usize; + let progress = y.min(canvas_height - 1) as f64 / (canvas_height - 1) as f64; + let scaled = progress * (stops.len() - 1) as f64; + let segment = (scaled.floor() as usize).min(stops.len() - 2); + let segment_progress = scaled - segment as f64; + + interpolate_color(stops[segment], stops[segment + 1], segment_progress) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let progress = progress.clamp(0.0, 1.0); + let (start_r, start_g, start_b) = color_components(start); + let (end_r, end_g, end_b) = color_components(end); + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn color_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = f64::from(start) + (f64::from(end) - f64::from(start)) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +fn initial_seed(terminal: &Terminal) -> u64 { + let mut seed = 0xcbf2_9ce4_8422_2325_u64; + + for character in terminal.characters() { + seed ^= u64::from(character.input_symbol as u32); + seed = seed.wrapping_mul(0x0000_0100_0000_01b3); + seed ^= u64::from(character.id.0); + seed = seed.wrapping_mul(0x0000_0100_0000_01b3); + } + + if seed == 0 { + 1 + } else { + seed + } +} + +fn shuffle(values: &mut [T], seed: &mut u64) { + if values.len() < 2 { + return; + } + + for index in (1..values.len()).rev() { + *seed ^= *seed << 13; + *seed ^= *seed >> 7; + *seed ^= *seed << 17; + + let swap_index = (*seed as usize) % (index + 1); + values.swap(index, swap_index); + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/print.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/print.rs new file mode 100644 index 0000000..0b8f24d --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/print.rs @@ -0,0 +1,168 @@ + +use std::collections::BTreeMap; + +use super::Effect; +use crate::engine::{CharacterId, Terminal}; +use crate::utils::easing::in_out_quad; +use crate::utils::{Color, Coord, Style}; + +const PRINT_HEAD_SYMBOL: char = '█'; +const PRINT_HEAD_RETURN_SPEED: f64 = 1.25; +const PRINT_SPEED: usize = 1; + +const GRADIENT_BOTTOM: Color = Color::rgb(0x02, 0xb8, 0xbd); +const GRADIENT_TOP: Color = Color::rgb(0xc1, 0xf0, 0xe3); + +#[derive(Debug, Clone, Copy, Default)] +pub struct Print; + +impl Print { + pub fn new() -> Self { + Self + } +} + +impl Effect for Print { + fn name(&self) -> &str { + "print" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let height = terminal.canvas().height(); + + let mut rows: BTreeMap> = BTreeMap::new(); + for character in terminal.characters() { + rows.entry(character.position.y) + .or_default() + .push(character.id); + } + + for row in rows.values_mut() { + row.sort_by_key(|id| { + terminal + .character(*id) + .map(|character| (character.position.x, character.id)) + .unwrap_or((i32::MAX, *id)) + }); + } + + for character in terminal.characters_mut() { + character.visible = false; + let color = gradient_color(character.position.y, height); + character.set_style(Style::default().with_foreground(color)); + } + + let rows: Vec<(i32, Vec)> = rows.into_iter().collect(); + if rows.is_empty() { + return vec![terminal.render_frame()]; + } + + let first_row = rows[0].0; + let print_head_id = terminal.add_character( + PRINT_HEAD_SYMBOL, + Coord::new(0, first_row), + ); + + if let Some(print_head) = terminal.character_mut(print_head_id) { + print_head.set_style( + Style::default().with_foreground(Color::rgb(0xff, 0xff, 0xff)), + ); + } + + let mut frames = Vec::new(); + + for (row_index, (row_y, character_ids)) in rows.iter().enumerate() { + for batch in character_ids.chunks(PRINT_SPEED) { + let mut print_head_x = 0; + + for id in batch { + if let Some(character) = terminal.character_mut(*id) { + character.visible = true; + print_head_x = character.position.x.saturating_add(1); + } + } + + if let Some(print_head) = terminal.character_mut(print_head_id) { + print_head.set_position(Coord::new(print_head_x, *row_y)); + } + + frames.push(terminal.render_frame()); + } + + let Some((next_row_y, _)) = rows.get(row_index + 1) else { + continue; + }; + + let start = terminal + .character(print_head_id) + .map(|character| character.position) + .unwrap_or(Coord::new(0, *row_y)); + let destination = Coord::new(0, *next_row_y); + let distance = start.distance(destination); + let return_steps = (distance / PRINT_HEAD_RETURN_SPEED) + .ceil() + .max(1.0) as usize; + + for step in 1..=return_steps { + let raw_progress = step as f64 / return_steps as f64; + let position = start.lerp(destination, in_out_quad(raw_progress)); + + if let Some(print_head) = terminal.character_mut(print_head_id) { + print_head.set_position(position); + } + + frames.push(terminal.render_frame()); + } + } + + if let Some(print_head) = terminal.character_mut(print_head_id) { + print_head.visible = false; + } + frames.push(terminal.render_frame()); + + frames + } +} + +fn gradient_color(row: i32, height: usize) -> Color { + if height <= 1 { + return GRADIENT_BOTTOM; + } + + let row = row.clamp(0, height.saturating_sub(1) as i32) as f64; + let progress = 1.0 - row / (height - 1) as f64; + + interpolate_color(GRADIENT_BOTTOM, GRADIENT_TOP, progress) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let ( + Color::Rgb { + r: start_r, + g: start_g, + b: start_b, + }, + Color::Rgb { + r: end_r, + g: end_g, + b: end_b, + }, + ) = (start, end) + else { + return start; + }; + + let progress = progress.clamp(0.0, 1.0); + let interpolate = |start: u8, end: u8| { + (start as f64 + (end as f64 - start as f64) * progress) + .round() + .clamp(0.0, 255.0) as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rain.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rain.rs new file mode 100644 index 0000000..67f3df8 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rain.rs @@ -0,0 +1,286 @@ + +use super::Effect; +use crate::engine::animation::{CharacterVisual, Frame, Scene}; +use crate::engine::motion::{Path, Waypoint}; +use crate::engine::terminal::Terminal; +use crate::utils::easing; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +pub struct Rain; + +impl Rain { + pub fn new() -> Self { + Self + } +} + +impl Default for Rain { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Rain { + fn name(&self) -> &str { + "rain" + } + + fn frames(&self, input: &str) -> Vec { + const RAIN_COLORS: [Color; 4] = [ + Color::rgb(0x00, 0x31, 0x5c), + Color::rgb(0x00, 0x4c, 0x8f), + Color::rgb(0x00, 0x75, 0xdb), + Color::rgb(0x3f, 0x91, 0xd9), + ]; + + let mut terminal = Terminal::from_text(input); + let character_count = terminal.characters().len(); + + if character_count == 0 { + return vec![terminal.render_frame()]; + } + + let canvas_height = terminal.canvas().height(); + let mut rng = RainRng::new(seed_from_input(input)); + + let targets: Vec = terminal + .characters() + .iter() + .map(|character| character.position) + .collect(); + + let final_colors: Vec = targets + .iter() + .map(|coord| final_gradient_color(coord.y, canvas_height)) + .collect(); + + for character in terminal.characters_mut() { + character.visible = false; + character.motion.deactivate(); + character.animation.deactivate(); + } + + let mut pending: Vec = (0..character_count).collect(); + shuffle(&mut pending, &mut rng); + + // 0 = pending, 1 = falling, 2 = fading, 3 = complete. + let mut states = vec![0_u8; character_count]; + let mut completed = 0_usize; + let mut frames = Vec::new(); + + let maximum_steps = character_count + .saturating_mul(4) + .saturating_add(canvas_height.saturating_mul(20)) + .saturating_add(64); + + for _ in 0..maximum_steps { + if completed == character_count { + break; + } + + let spawn_count = rng.range_usize(1, 3); + for _ in 0..spawn_count { + let Some(index) = pending.pop() else { + break; + }; + + let target = targets[index]; + let start = Coord::new(target.x, 0); + let rain_color = RAIN_COLORS[rng.range_usize(0, RAIN_COLORS.len() - 1)]; + let speed = 0.1 + rng.unit_f64() * 0.1; + let style = Style::default().with_foreground(rain_color); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(target)], + speed, + ); + path.set_easing(easing::in_quad); + + let character = &mut terminal.characters_mut()[index]; + character.visible = true; + character.set_position(start); + character.set_appearance(character.input_symbol, style); + character.motion.activate_path(path); + states[index] = 1; + } + + terminal.step(); + + for index in 0..character_count { + if states[index] == 1 { + let still_falling = terminal.characters()[index] + .motion + .active_path() + .is_some_and(Path::is_active); + + if !still_falling { + let symbol = terminal.characters()[index].input_symbol; + let rain_color = terminal.characters()[index] + .style + .colors + .foreground + .unwrap_or(RAIN_COLORS[0]); + let final_color = final_colors[index]; + let scene = make_fade_scene(symbol, rain_color, final_color); + + let character = &mut terminal.characters_mut()[index]; + character.set_position(targets[index]); + character.set_appearance( + symbol, + Style::default().with_foreground(rain_color), + ); + character.animation.activate_scene(scene); + states[index] = 2; + } + } else if states[index] == 2 { + let fade_finished = terminal.characters()[index] + .animation + .active_scene() + .is_some_and(Scene::is_finished); + + if fade_finished { + states[index] = 3; + completed += 1; + } + } + } + + frames.push(terminal.render_frame()); + } + + if completed < character_count { + for index in 0..character_count { + let symbol = terminal.characters()[index].input_symbol; + let character = &mut terminal.characters_mut()[index]; + character.visible = true; + character.set_position(targets[index]); + character.motion.deactivate(); + character.animation.deactivate(); + character.set_appearance( + symbol, + Style::default().with_foreground(final_colors[index]), + ); + } + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn make_fade_scene(symbol: char, start: Color, end: Color) -> Scene { + const FADE_STEPS: u32 = 8; + + let mut scene = Scene::new(false); + for step in 0..FADE_STEPS { + let progress = step as f64 / (FADE_STEPS - 1) as f64; + let color = interpolate_color(start, end, progress); + let style = Style::default().with_foreground(color); + scene.add_frame(Frame::new(CharacterVisual::new(symbol, style), 1)); + } + scene +} + +fn final_gradient_color(y: i32, height: usize) -> Color { + const STOPS: [Color; 3] = [ + Color::rgb(0x8a, 0x00, 0x8a), + Color::rgb(0x00, 0xd1, 0xff), + Color::rgb(0xff, 0xff, 0xff), + ]; + + let progress = if height <= 1 { + 0.0 + } else { + let y = y.clamp(0, height.saturating_sub(1) as i32) as f64; + 1.0 - y / (height - 1) as f64 + }; + + let scaled = progress.clamp(0.0, 1.0) * (STOPS.len() - 1) as f64; + let lower = scaled.floor() as usize; + let upper = (lower + 1).min(STOPS.len() - 1); + interpolate_color(STOPS[lower], STOPS[upper], scaled - lower as f64) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = color_rgb(start); + let (end_r, end_g, end_b) = color_rgb(end); + let progress = progress.clamp(0.0, 1.0); + + let interpolate = |start: u8, end: u8| { + (start as f64 + (end as f64 - start as f64) * progress).round() as u8 + }; + + Color::rgb( + interpolate(start_r, end_r), + interpolate(start_g, end_g), + interpolate(start_b, end_b), + ) +} + +fn color_rgb(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn seed_from_input(input: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + if hash == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + hash + } +} + +fn shuffle(values: &mut [T], rng: &mut RainRng) { + for index in (1..values.len()).rev() { + let swap_index = rng.range_usize(0, index); + values.swap(index, swap_index); + } +} + +struct RainRng { + state: u64, +} + +impl RainRng { + fn new(seed: u64) -> Self { + Self { + state: if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + }, + } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn unit_f64(&mut self) -> f64 { + const SCALE: f64 = 1.0 / ((1_u64 << 53) as f64); + ((self.next_u64() >> 11) as f64) * SCALE + } + + fn range_usize(&mut self, minimum: usize, maximum: usize) -> usize { + if minimum >= maximum { + return minimum; + } + + minimum + (self.next_u64() as usize % (maximum - minimum + 1)) + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/random_sequence.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/random_sequence.rs new file mode 100644 index 0000000..7121151 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/random_sequence.rs @@ -0,0 +1,216 @@ + +use std::time::{SystemTime, UNIX_EPOCH}; + +use super::Effect; +use crate::engine::{CharacterId, CharacterVisual, Frame, Scene, Terminal}; +use crate::utils::graphics::{Color, Style}; + +const REVEAL_SPEED: f64 = 0.004; +const FADE_STEPS: usize = 10; +const FADE_FRAME_DURATION: u32 = 5; + +const STARTING_COLOR: Color = Color::rgb(0x00, 0x00, 0x00); +const FINAL_GRADIENT_STOPS: [Color; 3] = [ + Color::rgb(0x8a, 0x00, 0x8a), + Color::rgb(0x00, 0xd1, 0xff), + Color::rgb(0xff, 0xff, 0xff), +]; + +pub struct RandomSequence; + +impl RandomSequence { + pub fn new() -> Self { + Self + } +} + +impl Default for RandomSequence { + fn default() -> Self { + Self::new() + } +} + +impl Effect for RandomSequence { + fn name(&self) -> &str { + "random_sequence" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let starting_style = Style::default().with_foreground(STARTING_COLOR); + + let mut pending: Vec = terminal + .characters() + .iter() + .map(|character| character.id) + .collect(); + + for character in terminal.characters_mut() { + character.visible = false; + character.set_appearance(character.input_symbol, starting_style.clone()); + } + + shuffle(&mut pending, seed_for(input)); + + let mut active = Vec::new(); + let mut frames = Vec::new(); + + while !pending.is_empty() || !active.is_empty() { + if !pending.is_empty() { + let activation_count = ((pending.len() as f64 * REVEAL_SPEED) as usize) + .max(1) + .min(pending.len()); + + for _ in 0..activation_count { + let Some(id) = pending.pop() else { + break; + }; + + let Some(character) = terminal.character_mut(id) else { + continue; + }; + + let final_color = + final_color_for(character.position.x, character.position.y, width, height); + let scene = fade_scene(character.input_symbol, final_color); + + character.visible = true; + character.animation.activate_scene(scene); + active.push(id); + } + } + + terminal.step(); + + active.retain(|id| { + terminal + .character(*id) + .and_then(|character| character.animation.active_scene()) + .is_some_and(|scene| scene.is_active()) + }); + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn fade_scene(symbol: char, final_color: Color) -> Scene { + let mut scene = Scene::new(false); + + for step in 0..FADE_STEPS { + let progress = if FADE_STEPS <= 1 { + 1.0 + } else { + step as f64 / (FADE_STEPS - 1) as f64 + }; + + let color = interpolate_color(STARTING_COLOR, final_color, progress); + let style = Style::default().with_foreground(color); + + scene.add_frame(Frame::new( + CharacterVisual::new(symbol, style), + FADE_FRAME_DURATION, + )); + } + + scene +} + +fn final_color_for(x: i32, y: i32, width: usize, height: usize) -> Color { + let horizontal = x.max(0) as f64; + let vertical = (height as i32 - 1 - y).max(0) as f64; + let denominator = (width.saturating_sub(1) + height.saturating_sub(1)) as f64; + + let progress = if denominator <= f64::EPSILON { + 0.0 + } else { + ((horizontal + vertical) / denominator).clamp(0.0, 1.0) + }; + + multi_stop_color(&FINAL_GRADIENT_STOPS, progress) +} + +fn multi_stop_color(stops: &[Color], progress: f64) -> Color { + if stops.is_empty() { + return Color::rgb(0, 0, 0); + } + + if stops.len() == 1 { + return stops[0]; + } + + let scaled = progress.clamp(0.0, 1.0) * (stops.len() - 1) as f64; + let segment = (scaled.floor() as usize).min(stops.len() - 2); + let local_progress = scaled - segment as f64; + + interpolate_color(stops[segment], stops[segment + 1], local_progress) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = rgb_components(start); + let (end_r, end_g, end_b) = rgb_components(end); + let progress = progress.clamp(0.0, 1.0); + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn rgb_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +fn seed_for(input: &str) -> u64 { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos() as u64) + .unwrap_or(0); + + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + let seed = timestamp ^ hash; + if seed == 0 { + 0x9e37_79b9_7f4a_7c15 + } else { + seed + } +} + +fn shuffle(values: &mut [T], mut state: u64) { + if values.len() < 2 { + return; + } + + for index in (1..values.len()).rev() { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + + let other = (state as usize) % (index + 1); + values.swap(index, other); + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rings.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rings.rs new file mode 100644 index 0000000..49477fd --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/rings.rs @@ -0,0 +1,343 @@ + +use std::collections::HashSet; + +use super::Effect; +use crate::engine::character::CharacterId; +use crate::engine::terminal::Terminal; +use crate::utils::easing::out_quad; +use crate::utils::geometry::Coord; +use crate::utils::graphics::{Color, Style}; + +const RING_COLORS: [Color; 3] = [ + Color::rgb(171, 72, 255), + Color::rgb(231, 178, 178), + Color::rgb(255, 254, 189), +]; + +const ENTRANCE_FRAMES: usize = 28; +const SPIN_FRAMES: usize = 200; +const RETURN_FRAMES: usize = 32; +const ROTATION_TICKS_PER_POINT: usize = 4; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Rings; + +impl Rings { + pub fn new() -> Self { + Self + } +} + +impl Effect for Rings { + fn name(&self) -> &str { + "rings" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let center = Coord::new( + (width.saturating_sub(1) / 2) as i32, + (height.saturating_sub(1) / 2) as i32, + ); + + let mut characters: Vec = terminal + .characters() + .iter() + .map(|character| CharacterAssignment { + id: character.id, + symbol: character.input_symbol, + home: character.position, + path: Vec::new(), + path_index: 0, + direction: 1, + ring_color: RING_COLORS[0], + final_color: final_color(character.position, width, height), + }) + .collect(); + + if characters.is_empty() { + return vec![terminal.render_frame()]; + } + + let mut rng = LocalRng::from_text(input); + shuffle(&mut characters, &mut rng); + + assign_rings(&mut characters, center); + + for assignment in &characters { + if let Some(character) = terminal.character_mut(assignment.id) { + character.visible = true; + character.set_appearance( + assignment.symbol, + Style::default().with_foreground(assignment.final_color), + ); + } + } + + let mut frames = Vec::with_capacity( + 1 + ENTRANCE_FRAMES + SPIN_FRAMES + RETURN_FRAMES + 1, + ); + + frames.push(terminal.render_frame()); + + for step in 1..=ENTRANCE_FRAMES { + let progress = step as f64 / ENTRANCE_FRAMES as f64; + + for assignment in &characters { + let target = assignment.path[assignment.path_index]; + let position = assignment.home.lerp(target, progress); + let color = interpolate_color( + assignment.final_color, + assignment.ring_color, + progress, + ); + + if let Some(character) = terminal.character_mut(assignment.id) { + character.set_position(position); + character.set_appearance( + assignment.symbol, + Style::default().with_foreground(color), + ); + } + } + + frames.push(terminal.render_frame()); + } + + for step in 0..SPIN_FRAMES { + let shift = step / ROTATION_TICKS_PER_POINT; + + for assignment in &characters { + let path_len = assignment.path.len(); + let offset = shift % path_len; + let index = if assignment.direction > 0 { + (assignment.path_index + offset) % path_len + } else { + (assignment.path_index + path_len - offset) % path_len + }; + + if let Some(character) = terminal.character_mut(assignment.id) { + character.set_position(assignment.path[index]); + character.set_appearance( + assignment.symbol, + Style::default().with_foreground(assignment.ring_color), + ); + } + } + + frames.push(terminal.render_frame()); + } + + let return_starts: Vec<(CharacterId, Coord)> = characters + .iter() + .map(|assignment| { + let position = terminal + .character(assignment.id) + .map(|character| character.position) + .unwrap_or(assignment.home); + (assignment.id, position) + }) + .collect(); + + for step in 1..=RETURN_FRAMES { + let raw_progress = step as f64 / RETURN_FRAMES as f64; + let progress = out_quad(raw_progress); + + for (assignment, (_, start)) in characters.iter().zip(&return_starts) { + let position = start.lerp(assignment.home, progress); + let color = interpolate_color( + assignment.ring_color, + assignment.final_color, + progress, + ); + + if let Some(character) = terminal.character_mut(assignment.id) { + character.set_position(position); + character.set_appearance( + assignment.symbol, + Style::default().with_foreground(color), + ); + } + } + + frames.push(terminal.render_frame()); + } + + for assignment in &characters { + if let Some(character) = terminal.character_mut(assignment.id) { + character.visible = true; + character.set_position(assignment.home); + character.set_appearance( + assignment.symbol, + Style::default().with_foreground(assignment.final_color), + ); + } + } + + frames.push(terminal.render_frame()); + frames + } +} + +#[derive(Debug, Clone)] +struct CharacterAssignment { + id: CharacterId, + symbol: char, + home: Coord, + path: Vec, + path_index: usize, + direction: i32, + ring_color: Color, + final_color: Color, +} + +fn assign_rings(characters: &mut [CharacterAssignment], center: Coord) { + let mut character_index = 0; + let mut radius = 1usize; + let mut ring_index = 0usize; + + while character_index < characters.len() { + let points = circle_points(center, radius as i32, radius.saturating_mul(7)); + + if points.is_empty() { + radius += 1; + continue; + } + + let count = points.len().min(characters.len() - character_index); + let direction = if ring_index % 2 == 0 { 1 } else { -1 }; + let ring_color = RING_COLORS[ring_index % RING_COLORS.len()]; + + for point_index in 0..count { + let assignment = &mut characters[character_index + point_index]; + assignment.path = points.clone(); + assignment.path_index = point_index; + assignment.direction = direction; + assignment.ring_color = ring_color; + } + + character_index += count; + radius += 1; + ring_index += 1; + } +} + +fn circle_points(center: Coord, radius: i32, count: usize) -> Vec { + let count = count.max(1); + let mut points = Vec::with_capacity(count); + let mut seen = HashSet::with_capacity(count); + + for index in 0..count { + let angle = std::f64::consts::TAU * index as f64 / count as f64; + let x = center.x + (angle.cos() * radius as f64).round() as i32; + let y = center.y + (angle.sin() * radius as f64).round() as i32; + let coord = Coord::new(x, y); + + if seen.insert(coord) { + points.push(coord); + } + } + + if points.is_empty() { + points.push(center); + } + + points +} + +fn final_color(coord: Coord, width: usize, height: usize) -> Color { + let progress = if height > 1 { + coord.y.max(0) as f64 / height.saturating_sub(1) as f64 + } else if width > 1 { + coord.x.max(0) as f64 / width.saturating_sub(1) as f64 + } else { + 0.0 + }; + + palette_color(&RING_COLORS, progress) +} + +fn palette_color(colors: &[Color], progress: f64) -> Color { + if colors.len() == 1 { + return colors[0]; + } + + let scaled = progress.clamp(0.0, 1.0) * (colors.len() - 1) as f64; + let lower = scaled.floor() as usize; + let upper = (lower + 1).min(colors.len() - 1); + let local_progress = scaled - lower as f64; + + interpolate_color(colors[lower], colors[upper], local_progress) +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + let (start_r, start_g, start_b) = color_components(start); + let (end_r, end_g, end_b) = color_components(end); + let progress = progress.clamp(0.0, 1.0); + + Color::rgb( + interpolate_channel(start_r, end_r, progress), + interpolate_channel(start_g, end_g, progress), + interpolate_channel(start_b, end_b, progress), + ) +} + +fn color_components(color: Color) -> (u8, u8, u8) { + match color { + Color::Rgb { r, g, b } => (r, g, b), + Color::Ansi(value) => (value, value, value), + } +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} + +fn shuffle(values: &mut [T], rng: &mut LocalRng) { + for index in (1..values.len()).rev() { + let other = rng.index(index + 1); + values.swap(index, other); + } +} + +#[derive(Debug, Clone)] +struct LocalRng { + state: u64, +} + +impl LocalRng { + fn from_text(input: &str) -> Self { + let mut state = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + state ^= u64::from(byte); + state = state.wrapping_mul(0x0000_0100_0000_01b3); + } + + if state == 0 { + state = 0x9e37_79b9_7f4a_7c15; + } + + Self { state } + } + + fn next_u64(&mut self) -> u64 { + let mut value = self.state; + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + self.state = value; + value + } + + fn index(&mut self, upper: usize) -> usize { + if upper <= 1 { + 0 + } else { + (self.next_u64() % upper as u64) as usize + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/scattered.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/scattered.rs new file mode 100644 index 0000000..6dc4878 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/scattered.rs @@ -0,0 +1,168 @@ +use super::Effect; + +use crate::engine::{Path, Terminal, Waypoint}; +use crate::utils::{Color, Coord, Style}; + +const MOVEMENT_SPEED: f64 = 0.3; +const GRADIENT_STEPS: usize = 12; +const GRADIENT_START: (u8, u8, u8) = (0xff, 0x90, 0x48); +const GRADIENT_END: (u8, u8, u8) = (0xab, 0x9d, 0xff); + +pub struct Scattered; + +impl Scattered { + pub fn new() -> Self { + Self + } +} + +impl Default for Scattered { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Scattered { + fn name(&self) -> &str { + "scattered" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let mut rng = LocalRng::new(seed_from_input(input, width, height)); + + for character in terminal.characters_mut() { + let destination = character.position; + let start = Coord::new( + rng.index(width) as i32, + rng.index(height) as i32, + ); + + character.set_position(start); + character.visible = true; + character.set_appearance( + character.input_symbol, + Style::default().with_foreground(gradient_color(destination.y, height)), + ); + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(destination)], + MOVEMENT_SPEED, + ); + path.set_easing(in_out_back); + character.motion.activate_path(path); + } + + let mut frames = Vec::new(); + + while terminal.characters().iter().any(|character| { + character + .motion + .active_path() + .is_some_and(|path| path.is_active()) + }) { + terminal.step(); + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn gradient_color(y: i32, height: usize) -> Color { + let step = if height <= 1 { + 0 + } else { + let y = y.clamp(0, height.saturating_sub(1) as i32) as usize; + let distance_from_bottom = height - 1 - y; + distance_from_bottom * (GRADIENT_STEPS - 1) / (height - 1) + }; + + let denominator = (GRADIENT_STEPS - 1) as u32; + let step = step as u32; + + Color::rgb( + interpolate_channel(GRADIENT_START.0, GRADIENT_END.0, step, denominator), + interpolate_channel(GRADIENT_START.1, GRADIENT_END.1, step, denominator), + interpolate_channel(GRADIENT_START.2, GRADIENT_END.2, step, denominator), + ) +} + +fn interpolate_channel(start: u8, end: u8, step: u32, denominator: u32) -> u8 { + let start = i64::from(start); + let difference = i64::from(end) - start; + let numerator = difference * i64::from(step); + let denominator = i64::from(denominator.max(1)); + + let offset = if numerator >= 0 { + (numerator + denominator / 2) / denominator + } else { + (numerator - denominator / 2) / denominator + }; + + (start + offset).clamp(0, 255) as u8 +} + +fn in_out_back(progress: f64) -> f64 { + let progress = progress.clamp(0.0, 1.0); + let c1 = 1.70158; + let c2 = c1 * 1.525; + + if progress < 0.5 { + let scaled = 2.0 * progress; + (scaled * scaled * ((c2 + 1.0) * scaled - c2)) / 2.0 + } else { + let scaled = 2.0 * progress - 2.0; + (scaled * scaled * ((c2 + 1.0) * scaled + c2) + 2.0) / 2.0 + } +} + +fn seed_from_input(input: &str, width: usize, height: usize) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + + for byte in input.bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + hash ^= width as u64; + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + hash ^= height as u64; + + hash +} + +struct LocalRng { + state: u64, +} + +impl LocalRng { + fn new(seed: u64) -> Self { + Self { + state: seed ^ 0x9e37_79b9_7f4a_7c15, + } + } + + fn next_u64(&mut self) -> u64 { + self.state = self + .state + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + self.state + } + + fn index(&mut self, upper_bound: usize) -> usize { + if upper_bound <= 1 { + 0 + } else { + (self.next_u64() % upper_bound as u64) as usize + } + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slice.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slice.rs new file mode 100644 index 0000000..a48b503 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slice.rs @@ -0,0 +1,81 @@ + +use std::collections::BTreeMap; + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::easing::in_out_expo; +use crate::utils::Coord; + +const MOVEMENT_SPEED: f64 = 0.15; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Slice; + +impl Slice { + pub fn new() -> Self { + Self + } +} + +impl Effect for Slice { + fn name(&self) -> &str { + "slice" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + let right_edge = terminal.canvas().width() as i32; + + // The upstream default is a vertical slice: each row is divided into + // left and right halves, which enter from opposite horizontal edges. + let mut rows: BTreeMap> = BTreeMap::new(); + + for character in terminal.characters() { + rows.entry(character.position.y) + .or_default() + .push((character.position.x, character.id)); + } + + for row in rows.values_mut() { + row.sort_by_key(|(x, _)| *x); + let midpoint = row.len() / 2; + + for (index, (_, id)) in row.iter().enumerate() { + let Some(character) = terminal.character_mut(*id) else { + continue; + }; + + let destination = character.position; + let start = if index < midpoint { + Coord::new(-1, destination.y) + } else { + Coord::new(right_edge, destination.y) + }; + + character.set_position(start); + character.visible = true; + + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(destination)], + MOVEMENT_SPEED, + ); + path.set_easing(in_out_expo); + character.motion.activate_path(path); + } + } + + let mut frames = Vec::new(); + + while terminal.characters().iter().any(|character| { + character + .motion + .active_path() + .map_or(false, |path| path.is_active()) + }) { + terminal.step(); + frames.push(terminal.render_frame()); + } + + frames + } +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slide.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slide.rs new file mode 100644 index 0000000..d64d3d1 --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/slide.rs @@ -0,0 +1,163 @@ + +use std::collections::{BTreeMap, VecDeque}; + +use super::Effect; +use crate::engine::{CharacterId, Path, Terminal, Waypoint}; +use crate::utils::easing::in_out_quad; +use crate::utils::graphics::{Color, Style}; +use crate::utils::Coord; + +#[derive(Debug, Clone)] +pub struct Slide { + movement_speed: f64, + group_gap: usize, +} + +impl Slide { + pub fn new() -> Self { + Self { + movement_speed: 0.5, + group_gap: 0, + } + } +} + +impl Default for Slide { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Slide { + fn name(&self) -> &str { + "slide" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return Vec::new(); + } + + let width = terminal.canvas().width() as i32; + let height = terminal.canvas().height(); + + let mut rows: BTreeMap> = BTreeMap::new(); + let mut destinations = BTreeMap::new(); + + for character in terminal.characters() { + rows.entry(character.position.y) + .or_default() + .push(character.id); + destinations.insert(character.id, character.position); + } + + let mut groups: VecDeque> = rows.into_values().collect(); + + for (row_index, group) in groups.iter().enumerate() { + let enter_from_left = row_index % 2 == 0; + + for id in group { + let Some(destination) = destinations.get(id).copied() else { + continue; + }; + + let start = if enter_from_left { + destination.offset(-width, 0) + } else { + destination.offset(width, 0) + }; + + if let Some(character) = terminal.character_mut(*id) { + character.set_position(start); + character.visible = true; + + let color = slide_color(destination.y, height); + character.set_style(Style::default().with_foreground(color)); + } + } + } + + let mut active = Vec::::new(); + let mut gap_remaining = 0usize; + let mut frames = Vec::new(); + + while !groups.is_empty() || !active.is_empty() { + if gap_remaining == 0 { + if let Some(group) = groups.pop_front() { + for id in group { + let Some(destination) = destinations.get(&id).copied() else { + continue; + }; + + if let Some(character) = terminal.character_mut(id) { + let start = character.position; + let mut path = Path::with_waypoints( + vec![Waypoint::new(start), Waypoint::new(destination)], + self.movement_speed, + ); + path.set_easing(in_out_quad); + + if character.motion.activate_path(path) { + active.push(id); + } else { + character.set_position(destination); + } + } + } + + gap_remaining = self.group_gap; + } + } else { + gap_remaining -= 1; + } + + terminal.step(); + + active.retain(|id| { + terminal + .character(*id) + .and_then(|character| character.motion.active_path()) + .is_some_and(|path| path.is_active()) + }); + + frames.push(terminal.render_frame()); + } + + frames + } +} + +fn slide_color(row: i32, height: usize) -> Color { + const STOPS: [(u8, u8, u8); 3] = [ + (0x12, 0xc2, 0xe9), + (0xc4, 0x71, 0xed), + (0xf6, 0x4f, 0x59), + ]; + + let progress = if height <= 1 { + 0.0 + } else { + (row.max(0) as f64 / (height - 1) as f64).clamp(0.0, 1.0) + }; + + let scaled = progress * (STOPS.len() - 1) as f64; + let lower = (scaled.floor() as usize).min(STOPS.len() - 1); + let upper = (lower + 1).min(STOPS.len() - 1); + let local_progress = scaled - lower as f64; + + let (r1, g1, b1) = STOPS[lower]; + let (r2, g2, b2) = STOPS[upper]; + + Color::rgb( + interpolate_channel(r1, r2, local_progress), + interpolate_channel(g1, g2, local_progress), + interpolate_channel(b1, b2, local_progress), + ) +} + +fn interpolate_channel(start: u8, end: u8, progress: f64) -> u8 { + let value = start as f64 + (end as f64 - start as f64) * progress; + value.round().clamp(0.0, 255.0) as u8 +} diff --git a/alternates/openai_gpt-5_6-sol-pro/port/src/effects/smoke.rs b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/smoke.rs new file mode 100644 index 0000000..48f7e7e --- /dev/null +++ b/alternates/openai_gpt-5_6-sol-pro/port/src/effects/smoke.rs @@ -0,0 +1,328 @@ + +use super::Effect; +use crate::engine::{CharacterVisual, Frame, Path, Scene, Terminal, Waypoint}; +use crate::utils::{Color, Coord, Style}; + +pub struct Smoke; + +impl Smoke { + pub fn new() -> Self { + Self + } +} + +impl Default for Smoke { + fn default() -> Self { + Self::new() + } +} + +impl Effect for Smoke { + fn name(&self) -> &str { + "smoke" + } + + fn frames(&self, input: &str) -> Vec { + let mut terminal = Terminal::from_text(input); + + if terminal.characters().is_empty() { + return vec![terminal.render_frame()]; + } + + let width = terminal.canvas().width(); + let height = terminal.canvas().height(); + let targets: Vec = terminal + .characters() + .iter() + .map(|character| character.position) + .collect(); + + let final_styles: Vec