From a48c72357ae020a0769ad8dfc934703902141d66 Mon Sep 17 00:00:00 2001 From: shweet Date: Sun, 10 Aug 2025 01:27:06 -0400 Subject: [PATCH] The Omega Update(TM) Part 3 (Rendering) --- assets/atlas.kra | Bin 375239 -> 0 bytes assets/atlas.png | Bin 1698 -> 1132 bytes assets/bleh.png | Bin 88 -> 0 bytes assets/bleh.txt | 256 -------------------- src/COMMON.h | 45 ++++ src/PACKED.h | 436 ++++++++++++++------------------- src/canvas.cpp | 15 -- src/dialog.cpp | 82 ++----- src/dialog.h | 44 ++-- src/editor.cpp | 2 +- src/ffmpeg.cpp | 60 +++++ src/ffmpeg.h | 32 +++ src/imgui.cpp | 604 ++++++++++++++++++++++++++++++++-------------- src/imgui.h | 348 +++++++++++++++++--------- src/preview.cpp | 157 ++++++++---- src/preview.h | 8 +- src/render.h | 25 ++ src/resources.cpp | 20 +- src/resources.h | 2 +- src/settings.cpp | 15 +- src/settings.h | 15 +- src/snapshots.cpp | 7 + src/snapshots.h | 3 +- src/state.cpp | 4 +- src/texture.cpp | 62 +++-- src/texture.h | 12 +- 26 files changed, 1250 insertions(+), 1004 deletions(-) delete mode 100644 assets/atlas.kra delete mode 100644 assets/bleh.png delete mode 100644 assets/bleh.txt create mode 100644 src/ffmpeg.cpp create mode 100644 src/ffmpeg.h create mode 100644 src/render.h diff --git a/assets/atlas.kra b/assets/atlas.kra deleted file mode 100644 index 76039e467f588623b0222d30e612171abe5e1e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 375239 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 XQznNaS*$At00000NkvXXu0mjf&@S+R delta 1675 zcmV;626XxC2%-&;7Yb+y1^@s64$Jjbks%j<22@EzK~#9!?Olsb>o5#u)jkxy4E}VN zNCX96KaL%z?N(btY_w@oAHVb9?XBpX^dR)i?+|JG{{H^^?(Or+zEs9PBM=#5_qH9u z>flMhiGXSyYS%_-k?3b&Q<7*g^#Rff06OC>UiIs470P(=20;6nVFWjhN03&TAWryy z=rdgaWJw;CW>orTL5gZZL{QN2^meyVC2rrOsY}03?~nQhu0O7C>poSr1N}z&AMZuR z(VwXTz%hRX09OAN1E`7k{QP9^`}p|y^Zo1V>vjYHZ5+Mt4Ur~PVrFS%WdBT=hEQuW zwxi#@rVsT4blei(M&M18qM4;E4QPXZ?ow-Mg+~)4(j;FS%P6_1wv5l?+HbTYFY~>U z>1BwA`POsB>r~r1QRa`I+ci&Q)cfqDjt`9xX2VeF8C<;YReSFD7@yPM(RbH)kd{Fn zk%(nv-5MB@nK}&sXDCHv^n)-xl5}bJrSs<%ig6ytTvSw)Z4Qz4+bXq>Ml@%C>7OQI z5$4ad-`$BLk(9>Fu?`PJV~@x?%<^DY=6kMr(tW8yYE@c~(8e(1BPvWKjQ&{C;FU0~ z|JkT6%Y?cCKvmd^k1gg)adZ>_b)rKs2~z*ubRzv5DnN*^vE^T>X2aDbAFPv&S-$w z??|&(T=c?4Gmk`KZ71!B4vYh5RAG(zlD+l-fRq6chkObEkO>IuI%OzYxQl9lWowa$ zOJcO;=dAWqbtCB|tfWG7w31o!I$Kj&6kdAUH>V%S|*`Q-GdH z7}{?ir}ixB5UoD?{q0%p`QzkE`#d^Q&e?k=sMLG4P}Gw_Y7W_xh;gQkGmBCmh)+6m zY5lNL1obhRPx6!^5UqIHQpCz)Tt+7Ak^EWay~=#b%a@~z&PY)c!X-G`=j`^YYwsD) zre^Tw?L?}#%&eZ9ZI9G{gfan$KA#G43u`wo!dcP4tTbh2(yKM0TM*a6ENSIfMo5+o zfT#9?E<4HG=)u*?LLrl$! z-Hqb8m#X0kAK*>Wq-yT$yStc=)XpOspiPFX3LT?ck21F`xtVW&nILWN6QaJT3Gp)6 zwF5B`0B;Tj?Q_$rl}I!ZwUI}q_E7*(fLzQ+oMm*EWL8|ASzVm*0UrN=GXJJALfUYq zZRSHK9>(m~%7i1&w}3_80nL0l-XXwkkw)2Br`2ztm6N`op>{UT*m=gbNiRDiT}XkA zlfX#v%}dhm#rC&<$c{`+P>PDKMG~&STKE{T^mmgN!m=Vm!C1%_9Vn zxepHjcS}{dlShza0mcOD``$k)Z-sl2P_8crr%T`^yRVIZA$m}Wbg%pT3u|KqY7aG^ zVD)Kzmn2^rJGv$)@~){qfK$~B=2M@L34r&sF0Z-tnys`jFN66s>d-dXMMFrWG>(Ll zGxN#e?3vn7t{B(tC33=4blHmc<>;0l?d+ny+Sy~%IUgvsMT&)Lu})JxlsT5<&m>0l znswZaw%uxfro`;k9OU|DNj4hsa1xre;3}&a<{8UgN4DLmxzSYJD?NKLpX+Dbp(q{V zhiKgDcerylf3%%_Fb#-N8>nW`T`SR8KEp>um`=gd&WBs#gteC)nIAPtuU-Y6a>7N& z(d`T;N}WNYjyNY(tp)ADz&cLs2Y{%)}da@aRu>#;l?SE-N0Z>!`6aYm9Kmkxx z09-K>zA|Zh*7m3g-HY+|Ju89OuFiaJ8k7EQHkRiz&>EA5uqf7Mk0 zR1|c)GxfHc*#z!1aisy3h?NO?`A7zG!rG+#%6yyU8ZQ>!508YKgTe~DWM4fey= 0 && index < count) ? array[index] : ""; diff --git a/src/PACKED.h b/src/PACKED.h index e74980b..b826eb1 100644 --- a/src/PACKED.h +++ b/src/PACKED.h @@ -4,271 +4,209 @@ #include "COMMON.h" -const u32 TEXTURE_ATLAS_LENGTH = 1698; const u8 TEXTURE_ATLAS[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x68, - 0x08, 0x06, 0x00, 0x00, 0x00, 0x0e, 0xcb, 0xf5, 0x55, 0x00, 0x00, 0x00, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x78, + 0x04, 0x03, 0x00, 0x00, 0x00, 0xff, 0x33, 0xea, 0xfd, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, - 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x06, 0x54, 0x49, 0x44, - 0x41, 0x54, 0x78, 0xda, 0xed, 0x5d, 0x8b, 0x4e, 0xeb, 0x30, 0x0c, 0x65, - 0xd5, 0x3e, 0x14, 0xbe, 0x0c, 0xfe, 0x74, 0x97, 0x48, 0x04, 0x05, 0x5f, - 0x3f, 0x8e, 0x1d, 0xa7, 0xed, 0x56, 0x5b, 0x42, 0x6c, 0xb4, 0x69, 0x52, - 0x1f, 0xbf, 0xf3, 0xe0, 0xed, 0xad, 0xe8, 0x9c, 0xf4, 0x20, 0xf4, 0xcc, - 0xef, 0x10, 0x69, 0xfb, 0xfe, 0xfe, 0xfe, 0xfb, 0xee, 0xed, 0xf3, 0xca, - 0xbe, 0x54, 0xc6, 0x3f, 0x23, 0x10, 0x19, 0x63, 0xf7, 0xb6, 0x1d, 0xc1, - 0xea, 0xe0, 0x49, 0xc0, 0x89, 0x80, 0x6a, 0x1d, 0x6a, 0xd7, 0x46, 0x69, - 0x91, 0xe8, 0x67, 0x60, 0x53, 0x92, 0x68, 0x31, 0xf5, 0x01, 0xd2, 0x0a, - 0x00, 0x3a, 0xe3, 0x2d, 0x5e, 0xf5, 0xeb, 0x6e, 0x15, 0xca, 0x78, 0xf1, - 0x06, 0x40, 0xfb, 0x99, 0x61, 0x04, 0x37, 0x8e, 0x47, 0x90, 0x56, 0x99, - 0x20, 0x4e, 0xf8, 0xe8, 0x33, 0x5d, 0x00, 0x64, 0x49, 0x1e, 0x95, 0x66, - 0x54, 0xfa, 0x67, 0x41, 0x8a, 0x6a, 0x42, 0x44, 0x50, 0xd0, 0xf1, 0xf4, - 0x76, 0xb7, 0x51, 0x25, 0x6e, 0xdf, 0xa4, 0xa9, 0x4b, 0xbf, 0x4e, 0xef, - 0x8f, 0xfa, 0x06, 0xae, 0x3f, 0xae, 0x6f, 0xeb, 0x3e, 0x55, 0xb5, 0x03, - 0xfd, 0x46, 0xfa, 0x1f, 0xef, 0x45, 0xc6, 0xd1, 0x9f, 0xa9, 0x02, 0xc0, - 0x31, 0x7f, 0x05, 0x00, 0x56, 0xff, 0x16, 0x03, 0xa8, 0x89, 0xf8, 0xfc, - 0xfc, 0x64, 0xef, 0xfb, 0xf8, 0xf8, 0xf8, 0xf3, 0xfd, 0xeb, 0xeb, 0xeb, - 0x76, 0x04, 0x00, 0x6d, 0x1c, 0xbd, 0xef, 0x0d, 0x91, 0x26, 0x54, 0x62, - 0x66, 0x69, 0x64, 0x64, 0xff, 0x4c, 0x99, 0x86, 0x50, 0x6b, 0x33, 0xb6, - 0xa3, 0xdf, 0xbd, 0xa6, 0x0f, 0xf5, 0x03, 0x74, 0xdc, 0x12, 0xdf, 0x46, - 0xe0, 0x4d, 0x93, 0xa2, 0x99, 0xa5, 0x2c, 0x0d, 0x68, 0x83, 0xee, 0x52, - 0x6b, 0x69, 0x85, 0x47, 0x13, 0x24, 0xd2, 0x24, 0x5f, 0x1b, 0xcb, 0x28, - 0xb9, 0xa8, 0xb6, 0x8c, 0xcf, 0xe3, 0xda, 0x6f, 0xb4, 0x23, 0x2f, 0xf3, - 0xbd, 0x92, 0xe9, 0x65, 0x88, 0x87, 0xf9, 0xd6, 0x73, 0xc6, 0xeb, 0x54, - 0xdb, 0x3a, 0x51, 0xe6, 0x8f, 0x9f, 0xdb, 0x35, 0x4f, 0x64, 0xd4, 0xfb, - 0xec, 0xa4, 0x8e, 0x0f, 0x8d, 0x10, 0x66, 0xc3, 0x50, 0xe9, 0x19, 0x5c, - 0xbc, 0xef, 0x55, 0x7b, 0xee, 0xf7, 0x18, 0x9f, 0xd3, 0xdf, 0xd1, 0x77, - 0xd7, 0x78, 0x90, 0x96, 0x41, 0x1e, 0x91, 0x88, 0x65, 0x64, 0xdd, 0x1a, - 0x18, 0x92, 0x99, 0x3a, 0x0d, 0x00, 0x67, 0x28, 0x45, 0x64, 0xf4, 0x83, - 0x30, 0x3d, 0x92, 0x74, 0x69, 0xf7, 0xa5, 0xf3, 0xe7, 0x15, 0x8a, 0x71, - 0x1e, 0xc7, 0x5c, 0x54, 0x54, 0x94, 0x6d, 0x0e, 0x91, 0xf6, 0xdb, 0x2a, - 0xb5, 0x8f, 0x46, 0x34, 0x67, 0xe9, 0x9f, 0x26, 0x62, 0x11, 0xe6, 0xcf, - 0xb4, 0xdf, 0xdd, 0x89, 0x23, 0x91, 0x94, 0xc6, 0xcc, 0xb1, 0x0e, 0x0f, - 0x44, 0x63, 0x8f, 0xc8, 0x3b, 0xcc, 0xf2, 0x60, 0x57, 0xe6, 0x7b, 0xae, - 0x79, 0xd2, 0x7d, 0xa9, 0x42, 0x6a, 0x55, 0x5a, 0x8f, 0xd0, 0xc6, 0x30, - 0xe3, 0x23, 0x2a, 0x4c, 0x25, 0x8c, 0xfe, 0x58, 0xd2, 0xe0, 0x95, 0x30, - 0xad, 0xff, 0xd9, 0xa8, 0x2e, 0xcb, 0x84, 0xba, 0x01, 0x40, 0x55, 0xd8, - 0x8a, 0x8f, 0x2d, 0xe6, 0x4b, 0x71, 0x74, 0x14, 0x00, 0x75, 0xa2, 0x43, - 0x30, 0x09, 0x52, 0xff, 0xdc, 0x74, 0x22, 0xfd, 0x1b, 0x2a, 0x40, 0x88, - 0xb0, 0xb1, 0xe5, 0x5f, 0xa9, 0x66, 0xc3, 0xd5, 0x49, 0xac, 0x76, 0x63, - 0x69, 0xb8, 0x5f, 0xe7, 0x9e, 0xd3, 0x9e, 0xe1, 0xa9, 0xd3, 0x6b, 0xe5, - 0x71, 0xab, 0xaa, 0x2b, 0x15, 0x13, 0x7b, 0x29, 0x99, 0x2b, 0x9a, 0xf5, - 0xa2, 0xda, 0x58, 0x6e, 0xd6, 0x0a, 0x87, 0x5a, 0x11, 0xee, 0xbf, 0xfb, - 0x47, 0x84, 0xc7, 0xcf, 0x74, 0x26, 0x4b, 0x9a, 0x70, 0x96, 0xa4, 0x9f, - 0x73, 0x7e, 0x92, 0xb4, 0xce, 0x68, 0x80, 0xd7, 0xef, 0x48, 0xb3, 0x57, - 0x5c, 0xf4, 0xc2, 0x45, 0x33, 0x8f, 0x44, 0x62, 0x6d, 0x27, 0xed, 0x88, - 0x0e, 0x8c, 0x03, 0x67, 0x54, 0x61, 0x8d, 0xf9, 0x92, 0xbd, 0xf6, 0x00, - 0x80, 0x94, 0x01, 0x10, 0x87, 0x7c, 0x0a, 0x00, 0x90, 0x09, 0x08, 0xeb, - 0x3a, 0x65, 0x28, 0x5a, 0xb8, 0x8b, 0x6a, 0x80, 0x65, 0x6b, 0x91, 0x88, - 0x4b, 0x62, 0xb4, 0xe5, 0xe7, 0xac, 0xf6, 0x53, 0x75, 0x23, 0xe9, 0x25, - 0xac, 0xa4, 0x42, 0x73, 0xb4, 0x92, 0x59, 0xf2, 0x3a, 0x61, 0x2d, 0x8a, - 0x91, 0xfa, 0xd7, 0xf2, 0x84, 0xb3, 0x38, 0xe1, 0x8d, 0x4e, 0x98, 0xf4, - 0x8e, 0x6e, 0x84, 0xfa, 0x20, 0x38, 0x47, 0xcb, 0x4d, 0x34, 0x58, 0x53, - 0x80, 0x9e, 0x49, 0x18, 0xda, 0x6f, 0x1f, 0xa7, 0xf6, 0x2c, 0xea, 0x10, - 0xad, 0x3e, 0xfa, 0xfd, 0xed, 0x59, 0xed, 0xf9, 0xe3, 0xe4, 0x4b, 0xfb, - 0x3c, 0x3a, 0x52, 0xce, 0xd9, 0x7b, 0x26, 0xa8, 0xd4, 0x7b, 0xb5, 0x50, - 0xd4, 0x93, 0x41, 0x6a, 0x0e, 0xd9, 0x93, 0x88, 0x71, 0xa6, 0x8d, 0x33, - 0x8b, 0x52, 0x1f, 0x88, 0x4f, 0x3a, 0x73, 0x69, 0xfd, 0xb0, 0x52, 0x04, - 0xf5, 0x31, 0x9a, 0x4f, 0xf2, 0x94, 0x22, 0x10, 0xad, 0x78, 0xda, 0x52, - 0xc4, 0xca, 0x62, 0x5c, 0x46, 0x26, 0xeb, 0x91, 0xfc, 0x59, 0xe6, 0xbd, - 0xca, 0x7c, 0xca, 0xcb, 0x97, 0xa3, 0x8b, 0xce, 0x48, 0x51, 0x13, 0xc2, - 0x25, 0x38, 0xda, 0xe7, 0xec, 0xf6, 0xab, 0x6b, 0xef, 0x19, 0xcf, 0xa6, - 0x66, 0xf0, 0xe6, 0xed, 0x44, 0xaa, 0xb7, 0xcc, 0xac, 0x9e, 0x9b, 0x6d, - 0x8f, 0xd4, 0x84, 0x32, 0x01, 0x88, 0x3e, 0x9f, 0x0a, 0x71, 0x0b, 0x6b, - 0x37, 0x2f, 0xc2, 0x59, 0xd1, 0xc0, 0xac, 0x34, 0x65, 0x66, 0xd2, 0xab, - 0x35, 0xa1, 0x5b, 0x10, 0xd7, 0xc2, 0x2c, 0x69, 0xe5, 0x58, 0x46, 0x48, - 0x96, 0x0d, 0x80, 0xa7, 0xf6, 0x82, 0x2e, 0x3b, 0xc9, 0xdc, 0xe8, 0xc1, - 0xd5, 0xcb, 0x42, 0x21, 0x93, 0xa7, 0x3c, 0xe1, 0x01, 0x2f, 0x23, 0xd2, - 0x98, 0xdd, 0x1b, 0xb0, 0x62, 0x8f, 0x01, 0xb7, 0x27, 0x82, 0x15, 0x80, - 0x68, 0xb6, 0x8b, 0x30, 0x10, 0xd1, 0x80, 0x8c, 0x49, 0xef, 0x23, 0x00, - 0x88, 0x30, 0x5f, 0xdd, 0x1d, 0xe3, 0xdd, 0x8d, 0xe2, 0xb9, 0x97, 0xaa, - 0xe1, 0x0a, 0x1f, 0xe0, 0x4d, 0xd2, 0xa4, 0x6a, 0xee, 0xec, 0xbb, 0xbb, - 0x98, 0x8f, 0xd4, 0xce, 0x23, 0x1a, 0xa0, 0x4d, 0x86, 0xac, 0x0a, 0x1d, - 0xa3, 0x5b, 0x8f, 0x32, 0xb7, 0x2c, 0xb9, 0x99, 0x6f, 0x99, 0x20, 0x6d, - 0xef, 0x13, 0xa2, 0xbe, 0xa8, 0x09, 0xf1, 0x32, 0xd8, 0xb5, 0x03, 0x31, - 0x11, 0x00, 0x6f, 0x0e, 0x05, 0xed, 0x73, 0xd3, 0xaa, 0x95, 0x48, 0x34, - 0x11, 0xb5, 0x91, 0x47, 0xa5, 0xf6, 0x51, 0x00, 0x50, 0x80, 0x5c, 0xcc, - 0x47, 0x9c, 0x65, 0x74, 0x97, 0x64, 0x56, 0x5c, 0x9d, 0x59, 0x5d, 0x9c, - 0xf1, 0x01, 0x1e, 0xff, 0x80, 0x32, 0xff, 0xa6, 0x31, 0x42, 0xda, 0x70, - 0xa6, 0x6d, 0xe6, 0x43, 0x27, 0x1e, 0xc6, 0xec, 0xd7, 0xca, 0x84, 0x23, - 0xcf, 0xb7, 0x80, 0x45, 0xdf, 0x01, 0xcd, 0x7c, 0x39, 0xde, 0x21, 0xc0, - 0x6d, 0x91, 0x46, 0xd9, 0x59, 0xa7, 0xd5, 0x6f, 0x9f, 0x95, 0x93, 0xbe, - 0x9f, 0xa1, 0x76, 0x36, 0xce, 0xd8, 0x79, 0xc6, 0xb6, 0x49, 0x2f, 0x3b, - 0x23, 0x5d, 0x48, 0x81, 0x8d, 0x93, 0xc0, 0x48, 0xf1, 0xcd, 0x4b, 0xd2, - 0xee, 0xc5, 0xf6, 0xb7, 0xc8, 0x8e, 0x4c, 0x4d, 0x50, 0x8a, 0x8a, 0xae, - 0x45, 0x11, 0x6d, 0xdd, 0x9e, 0xe9, 0x05, 0xad, 0x35, 0xa0, 0x33, 0xa5, - 0x61, 0xa4, 0xdf, 0x23, 0x00, 0xfa, 0x1d, 0x40, 0x74, 0x75, 0x30, 0x77, - 0x52, 0x88, 0xe7, 0x59, 0xdc, 0x39, 0x3c, 0x33, 0xcc, 0xd0, 0xd6, 0xf6, - 0x58, 0xfd, 0xcf, 0x32, 0x38, 0x34, 0xf6, 0x99, 0x17, 0xe7, 0xd6, 0x99, - 0xa2, 0x49, 0x1a, 0xb7, 0x3e, 0x35, 0xa2, 0x29, 0x5a, 0xd2, 0x98, 0x59, - 0xf7, 0xd2, 0x18, 0x1c, 0xce, 0x63, 0xd0, 0xb5, 0xfd, 0x1e, 0x66, 0x58, - 0x83, 0x8c, 0x32, 0x5d, 0x62, 0x3c, 0xb7, 0xaa, 0xcd, 0x23, 0x04, 0x91, - 0xb9, 0x0f, 0x0f, 0x00, 0x77, 0x4b, 0x55, 0xb9, 0x93, 0x47, 0x90, 0x63, - 0x01, 0xc6, 0x04, 0xeb, 0xfb, 0xde, 0x3f, 0x2a, 0x6f, 0x85, 0x7b, 0x91, - 0x50, 0xae, 0x2f, 0x07, 0xa7, 0x4b, 0xe0, 0x25, 0xbb, 0xaf, 0x8d, 0x21, - 0x7a, 0x50, 0x89, 0x74, 0xaf, 0xfb, 0xfc, 0x0b, 0x6b, 0x63, 0x05, 0x6a, - 0x0f, 0x35, 0x9f, 0x60, 0xf5, 0x69, 0x7d, 0x97, 0x24, 0x5f, 0x1a, 0x3b, - 0xba, 0x26, 0x28, 0xf2, 0xae, 0xa9, 0x3e, 0x80, 0x53, 0xd5, 0x0c, 0xe6, - 0x53, 0x9f, 0x90, 0x09, 0x80, 0xf7, 0xb4, 0x2e, 0xaf, 0xb9, 0xf4, 0x9a, - 0xad, 0xb4, 0x30, 0x2f, 0x83, 0xf9, 0x34, 0xea, 0xd0, 0x36, 0xd9, 0x45, - 0x43, 0x48, 0xa4, 0x34, 0x8e, 0x84, 0x92, 0x33, 0xf3, 0xc9, 0xe1, 0xec, - 0x99, 0xda, 0x50, 0xae, 0x18, 0xd7, 0xed, 0x25, 0x72, 0xc2, 0x54, 0x74, - 0xd9, 0x8a, 0xf7, 0xe5, 0xe8, 0x96, 0x1f, 0xed, 0xec, 0xa2, 0xbe, 0xda, - 0xd9, 0x63, 0xd3, 0x39, 0x1f, 0x28, 0xb5, 0x45, 0x8a, 0x85, 0x6a, 0xb1, - 0x4e, 0x53, 0x3d, 0x94, 0x39, 0x96, 0xe4, 0xcf, 0x24, 0x46, 0xf4, 0x9a, - 0x75, 0xdc, 0x8c, 0xb6, 0xdd, 0x6a, 0xa6, 0xc4, 0xec, 0xd5, 0x1c, 0xe4, - 0xfa, 0x66, 0x49, 0x36, 0x1a, 0xf1, 0x70, 0x12, 0x9a, 0xb5, 0xe0, 0x2a, - 0xab, 0x18, 0xe6, 0x19, 0xcb, 0x5e, 0x47, 0xb6, 0xdd, 0xa9, 0xb9, 0xd1, - 0x54, 0xdd, 0x2b, 0x3d, 0x7b, 0x31, 0x9f, 0xeb, 0x67, 0xdc, 0xa1, 0x28, - 0x1d, 0xe2, 0x87, 0x68, 0xdc, 0xea, 0x77, 0xb8, 0x73, 0x36, 0x7f, 0xb4, - 0x9d, 0x7d, 0x30, 0x0d, 0x88, 0x51, 0x1b, 0xa8, 0x66, 0xd0, 0x5d, 0x2b, - 0xd1, 0x58, 0x3e, 0xc3, 0x47, 0x44, 0x98, 0x4e, 0xc1, 0xd3, 0xce, 0x87, - 0x5b, 0xe2, 0x84, 0xb5, 0x97, 0x1d, 0x99, 0x1f, 0x35, 0x49, 0xaf, 0x5e, - 0x05, 0x9d, 0x72, 0xc2, 0x45, 0xc7, 0xd1, 0xed, 0x0c, 0x27, 0x4a, 0x9d, - 0x41, 0xa3, 0x8e, 0x38, 0x27, 0x55, 0xad, 0x05, 0xed, 0xc1, 0xc0, 0x3a, - 0x4e, 0xec, 0x07, 0x80, 0xa8, 0xc3, 0x9a, 0x9d, 0x43, 0x2d, 0x7a, 0xb2, - 0x19, 0xb1, 0x02, 0xe0, 0x45, 0xed, 0x7f, 0x69, 0x40, 0x01, 0x50, 0x54, - 0x00, 0x14, 0x00, 0x45, 0x05, 0x40, 0x01, 0x50, 0x54, 0x00, 0x5c, 0x31, - 0x13, 0xbe, 0x32, 0x69, 0x7b, 0xd6, 0xf6, 0xa8, 0x09, 0xdd, 0x8b, 0xf1, - 0xf6, 0x3d, 0x2b, 0x81, 0xd8, 0xae, 0xce, 0x7c, 0x6e, 0x1a, 0x93, 0xfe, - 0x6d, 0xe5, 0x31, 0x08, 0x97, 0xf6, 0x01, 0x80, 0x64, 0x2f, 0x37, 0x41, - 0xdb, 0x55, 0xa5, 0x1f, 0x31, 0x2b, 0xed, 0x96, 0xd9, 0x7f, 0xd5, 0x55, - 0x00, 0x54, 0x14, 0x74, 0xbc, 0x33, 0xf5, 0xb6, 0x9b, 0xd9, 0x04, 0xee, - 0x34, 0x71, 0xa5, 0x01, 0x95, 0x88, 0x95, 0x09, 0x7a, 0xf9, 0x48, 0x06, - 0x72, 0xc2, 0xda, 0xa4, 0xfc, 0xca, 0x7c, 0xa0, 0x34, 0xa0, 0x4c, 0xd0, - 0x31, 0x1a, 0x03, 0x66, 0xc2, 0xcb, 0xb3, 0xe1, 0x4b, 0x6b, 0x00, 0x00, - 0xc2, 0xf2, 0x65, 0x33, 0x97, 0xac, 0x05, 0x8d, 0x6b, 0x60, 0x91, 0x03, - 0xa4, 0x76, 0x59, 0x1d, 0x5d, 0xa6, 0x68, 0xde, 0xc1, 0x17, 0x00, 0x49, - 0x40, 0xec, 0xb9, 0x34, 0xb1, 0xa2, 0xa0, 0x8a, 0x82, 0x0a, 0x80, 0xa2, - 0x02, 0xa0, 0x00, 0x28, 0x3a, 0x0a, 0x80, 0xfe, 0x5f, 0x83, 0xbc, 0xb4, - 0xd7, 0x1e, 0xaa, 0x4b, 0x68, 0x80, 0x17, 0x84, 0x62, 0xfe, 0x02, 0x13, - 0x84, 0x82, 0x90, 0xcd, 0xfc, 0xab, 0xef, 0x92, 0xf9, 0xe3, 0x03, 0x2c, - 0x10, 0x4a, 0xf2, 0x77, 0x70, 0xc2, 0x12, 0x08, 0xc5, 0xfc, 0x1d, 0xa3, - 0x20, 0x0a, 0x42, 0x31, 0x7f, 0x1d, 0x89, 0xb5, 0xa0, 0x1f, 0x86, 0x3f, - 0x86, 0xcf, 0x2f, 0x6d, 0xc7, 0x8f, 0xda, 0x38, 0xfd, 0x0f, 0xc3, 0x64, - 0xcf, 0x81, 0x94, 0xa2, 0x56, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, - 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 + 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x0f, 0x50, 0x4c, + 0x54, 0x45, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x60, 0x60, 0x60, 0xff, + 0xff, 0xff, 0x60, 0x60, 0x60, 0x15, 0x68, 0x14, 0xc2, 0x00, 0x00, 0x00, + 0x03, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x00, 0x00, 0xfa, 0x76, 0xc4, 0xde, + 0x00, 0x00, 0x03, 0xf4, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x98, + 0x6d, 0x6e, 0xde, 0x38, 0x0c, 0x84, 0x27, 0x20, 0x0f, 0xb0, 0xc1, 0x5e, + 0x60, 0x81, 0x5c, 0x80, 0x9b, 0xe1, 0x01, 0x08, 0x88, 0xf7, 0x3f, 0xd3, + 0x42, 0x94, 0x04, 0xa9, 0xad, 0x83, 0xd8, 0x0b, 0x34, 0xed, 0x8f, 0x4e, + 0x90, 0xc8, 0xb4, 0xf9, 0x84, 0x5f, 0xd2, 0xeb, 0x20, 0xf8, 0x3f, 0x22, + 0x71, 0x29, 0x31, 0x94, 0x9a, 0x97, 0xd7, 0xe1, 0x4f, 0xd6, 0x12, 0x42, + 0xc6, 0xe1, 0x4f, 0xda, 0xf1, 0x0b, 0x99, 0x0e, 0x68, 0xdd, 0x07, 0x68, + 0x90, 0x22, 0x87, 0x4b, 0x23, 0x09, 0x29, 0x73, 0x03, 0x29, 0x04, 0x64, + 0x86, 0x22, 0xeb, 0x72, 0x7b, 0x90, 0xc2, 0x92, 0xed, 0x94, 0x84, 0xae, + 0x1c, 0x80, 0x90, 0x38, 0x1c, 0xd4, 0x80, 0xd3, 0x16, 0x9b, 0x00, 0xc9, + 0x61, 0xd1, 0xc4, 0xa4, 0xb2, 0xd7, 0xed, 0x30, 0x53, 0xba, 0x02, 0x84, + 0x10, 0x13, 0x94, 0xac, 0xdb, 0x15, 0x26, 0x1b, 0xe9, 0x19, 0xdf, 0x02, + 0x5e, 0x16, 0xed, 0x04, 0x1c, 0x2d, 0x0a, 0xc8, 0xe6, 0xd9, 0x01, 0x1d, + 0xbd, 0x63, 0x0b, 0xd2, 0x3a, 0x20, 0xc4, 0x01, 0x28, 0x41, 0x60, 0x10, + 0xe5, 0x8f, 0x66, 0xe2, 0x1d, 0x58, 0xd5, 0x17, 0x46, 0x2c, 0x00, 0x0b, + 0x88, 0xee, 0x1f, 0xa1, 0x64, 0xcd, 0x08, 0x3c, 0xe7, 0x7c, 0x02, 0x8d, + 0x5e, 0x01, 0x54, 0x35, 0x55, 0x21, 0x95, 0x06, 0x6d, 0x02, 0x7b, 0x70, + 0xab, 0x4b, 0xf5, 0xb0, 0x14, 0x5d, 0x17, 0xc0, 0xdc, 0x1a, 0xca, 0x52, + 0xa0, 0x9e, 0xcc, 0x18, 0x00, 0x94, 0x31, 0x7e, 0xf0, 0xf3, 0xcd, 0x87, + 0xc0, 0x2f, 0x96, 0x92, 0x71, 0x65, 0x8b, 0x61, 0xd9, 0xd8, 0xda, 0xe7, + 0xa2, 0x71, 0x6c, 0x81, 0xec, 0x6b, 0x1e, 0x00, 0xb9, 0x89, 0xa3, 0xbd, + 0x70, 0x74, 0xaa, 0x83, 0x40, 0x07, 0x85, 0x7c, 0x07, 0x80, 0x37, 0x66, + 0xd0, 0x8e, 0xd0, 0xcc, 0x4c, 0x12, 0xb0, 0xa2, 0x01, 0xc6, 0xe8, 0x9f, + 0x8b, 0xb5, 0x0a, 0xa0, 0x91, 0xe4, 0xf0, 0xef, 0x84, 0x32, 0xb3, 0x91, + 0xb1, 0x00, 0x25, 0x20, 0x65, 0x8b, 0xc9, 0x04, 0xa4, 0x00, 0xf1, 0xcc, + 0x66, 0xb5, 0xd0, 0x9b, 0x2d, 0x60, 0x9d, 0x13, 0x96, 0x80, 0x56, 0xcf, + 0xc5, 0xa0, 0xde, 0x9a, 0x7b, 0x0b, 0xf1, 0x6c, 0xcc, 0x13, 0x80, 0x10, + 0x07, 0x50, 0xcf, 0xc5, 0xc0, 0x10, 0x31, 0x53, 0x36, 0x4b, 0x7a, 0x8a, + 0x0f, 0x40, 0xb1, 0xce, 0xc9, 0x4a, 0x49, 0x4c, 0xe9, 0xdd, 0x56, 0x93, + 0xe8, 0xdf, 0xe2, 0x42, 0x60, 0x45, 0xd8, 0xe7, 0x64, 0x15, 0xdd, 0x9f, + 0x67, 0xb7, 0x21, 0x20, 0x61, 0x50, 0xd2, 0x33, 0x67, 0xd1, 0x64, 0xb7, + 0x0d, 0xc9, 0xa8, 0xb6, 0x92, 0x00, 0xe9, 0x91, 0xd5, 0x25, 0x47, 0x1d, + 0x4a, 0x56, 0x5f, 0xd1, 0x01, 0x69, 0x6e, 0x05, 0x75, 0x00, 0x62, 0xd3, + 0x8e, 0xb2, 0x6b, 0xb4, 0x5e, 0x83, 0xcb, 0x9c, 0x83, 0xab, 0x2c, 0x1a, + 0x91, 0xf4, 0xec, 0x40, 0xd9, 0xc2, 0x08, 0xba, 0x5e, 0x6d, 0x8d, 0xe8, + 0x55, 0x21, 0xcb, 0x0e, 0xe0, 0xf5, 0x9f, 0xb2, 0x31, 0xec, 0xeb, 0xcd, + 0xb7, 0x26, 0x9a, 0x79, 0xd8, 0x58, 0xf6, 0x73, 0x89, 0xed, 0xf5, 0x96, + 0xc8, 0xbd, 0xee, 0xd4, 0xc4, 0x28, 0xc6, 0xbd, 0x96, 0x32, 0xf6, 0x7b, + 0xa0, 0xd6, 0x5d, 0xbc, 0xac, 0xf0, 0x38, 0xa4, 0x05, 0x9c, 0xa9, 0xfd, + 0x05, 0xfc, 0x4d, 0x76, 0x52, 0xc6, 0x6f, 0xb0, 0x09, 0x18, 0x5e, 0x01, + 0x89, 0x8c, 0xb3, 0x3b, 0x52, 0x77, 0xc9, 0x58, 0x87, 0xb1, 0x03, 0x98, + 0xc0, 0xcb, 0x2b, 0xe0, 0x54, 0x00, 0xd6, 0x42, 0x5d, 0x00, 0xeb, 0x80, + 0x60, 0x7d, 0x70, 0x49, 0x8d, 0xc4, 0xce, 0x94, 0xa4, 0x8d, 0xc1, 0x3b, + 0xd0, 0xc4, 0xc4, 0xd0, 0x1c, 0x62, 0x52, 0xe1, 0x06, 0x50, 0x29, 0xed, + 0x08, 0x4e, 0x37, 0xa0, 0xdf, 0x33, 0x11, 0x2e, 0x09, 0x51, 0x40, 0x75, + 0xa1, 0xed, 0x94, 0xf0, 0x2a, 0x8d, 0x0d, 0xd7, 0x80, 0x81, 0x26, 0x45, + 0x9d, 0x29, 0x55, 0x80, 0xeb, 0x94, 0x28, 0x34, 0x61, 0x55, 0x6e, 0x05, + 0x88, 0x19, 0x50, 0x01, 0x7e, 0x2c, 0x5a, 0x69, 0x20, 0x19, 0x73, 0x2a, + 0xb6, 0xfa, 0x07, 0x91, 0x0a, 0x70, 0xd5, 0x56, 0x23, 0xb9, 0xc7, 0xb9, + 0x46, 0x4a, 0xab, 0x00, 0x57, 0x83, 0x33, 0xd2, 0x16, 0x60, 0x1b, 0x20, + 0x4a, 0x17, 0x5b, 0x63, 0x47, 0x38, 0x52, 0xd2, 0x66, 0x57, 0x9b, 0x6f, + 0x5d, 0xd0, 0x3a, 0x40, 0x31, 0x80, 0x52, 0xab, 0xc6, 0xe5, 0xf6, 0xfe, + 0x1a, 0xd9, 0x11, 0x1b, 0x43, 0x4a, 0x4c, 0xc5, 0xb0, 0xc7, 0x22, 0xd1, + 0x7c, 0x27, 0x76, 0x24, 0xc9, 0x5c, 0x40, 0x58, 0x2d, 0xd3, 0x56, 0x27, + 0x01, 0x9e, 0x00, 0x01, 0x88, 0x33, 0x46, 0x18, 0x9f, 0x0f, 0xb5, 0x4d, + 0xa7, 0x56, 0xed, 0xec, 0x8a, 0xf5, 0x1e, 0x36, 0xb0, 0xf9, 0x4c, 0xeb, + 0xfd, 0x2d, 0x86, 0x63, 0x92, 0x3e, 0x32, 0xa6, 0x83, 0x43, 0xc0, 0xba, + 0x60, 0xa0, 0xd4, 0xf2, 0xfd, 0x2d, 0x19, 0x23, 0x00, 0x73, 0x54, 0x2b, + 0x01, 0xf9, 0x16, 0x30, 0x24, 0xfb, 0x17, 0x94, 0x49, 0x66, 0xc1, 0x41, + 0x7a, 0x54, 0x00, 0xa8, 0x03, 0xb4, 0xfd, 0x17, 0x8c, 0x10, 0x03, 0x98, + 0xa9, 0xd6, 0xb4, 0x95, 0xb2, 0x02, 0x84, 0xb5, 0xd9, 0x19, 0xb1, 0xb9, + 0xcc, 0x46, 0x2a, 0x04, 0x1d, 0x18, 0x95, 0xda, 0x0a, 0x20, 0xe8, 0x3a, + 0x01, 0x00, 0x59, 0x2a, 0xc0, 0xd7, 0x3b, 0x51, 0x57, 0x00, 0x94, 0x26, + 0xb0, 0x47, 0xa5, 0x50, 0x08, 0x99, 0x05, 0x60, 0x34, 0x11, 0xc7, 0x69, + 0xdc, 0x35, 0x9c, 0x80, 0x49, 0xfa, 0xda, 0xd5, 0x18, 0x80, 0x61, 0x48, + 0x56, 0x97, 0x6c, 0x03, 0x75, 0xbb, 0x0d, 0x0f, 0x7a, 0x2a, 0x02, 0x8d, + 0xb8, 0x04, 0xb2, 0x04, 0x40, 0x17, 0xa0, 0x19, 0x11, 0x38, 0x24, 0x6b, + 0xd2, 0x86, 0x2f, 0x95, 0x1c, 0xf1, 0xd4, 0x6e, 0x01, 0x1b, 0x62, 0x00, + 0x2f, 0x9c, 0x86, 0x30, 0x84, 0x31, 0xba, 0xf1, 0x01, 0x20, 0x44, 0x07, + 0x68, 0x9f, 0x01, 0xfb, 0x73, 0x65, 0x00, 0xb4, 0x7b, 0x80, 0x10, 0x13, + 0xa0, 0x5d, 0x03, 0x27, 0x54, 0xf7, 0x63, 0x01, 0xb4, 0x0d, 0x94, 0xed, + 0x6b, 0x3d, 0x6a, 0x50, 0x92, 0x5c, 0x00, 0xe3, 0x63, 0xe0, 0xdc, 0x39, + 0xbe, 0x00, 0xff, 0x10, 0x38, 0x95, 0xb4, 0x09, 0x78, 0xec, 0x94, 0x32, + 0xe9, 0x99, 0x40, 0x96, 0xf6, 0x07, 0xf2, 0x6e, 0x6b, 0xf9, 0x7f, 0x54, + 0x74, 0x01, 0x67, 0x5b, 0x81, 0xf2, 0xbf, 0x05, 0x28, 0xb1, 0x9a, 0x75, + 0x0f, 0x90, 0xe1, 0x98, 0x81, 0xb5, 0xdb, 0x75, 0x5c, 0x67, 0x5e, 0x01, + 0x37, 0xb5, 0x8b, 0x7e, 0xac, 0xfc, 0x4e, 0x81, 0xef, 0x44, 0xbb, 0x06, + 0x56, 0x05, 0x9f, 0x02, 0x8d, 0x25, 0xff, 0x3a, 0x00, 0x7f, 0x80, 0xdf, + 0x12, 0x20, 0x69, 0x4f, 0x00, 0x76, 0xd9, 0x7d, 0x40, 0x08, 0x9a, 0xf0, + 0x01, 0x60, 0xa0, 0xbd, 0x3c, 0x00, 0x58, 0x45, 0x8b, 0xfd, 0x0c, 0x80, + 0x3f, 0xea, 0xdf, 0x2f, 0x05, 0xbe, 0xbe, 0xe8, 0xe7, 0x73, 0x78, 0x3e, + 0xe9, 0xe7, 0x7b, 0x69, 0x13, 0xf6, 0xe7, 0x4c, 0x3f, 0x06, 0x94, 0xa5, + 0xb8, 0x05, 0xec, 0x10, 0x8e, 0xfb, 0x80, 0x56, 0x00, 0xdc, 0x7e, 0x3f, + 0x54, 0x08, 0xc7, 0x13, 0x40, 0xc9, 0x78, 0x04, 0xa0, 0x39, 0x9e, 0x01, + 0xe5, 0x94, 0x53, 0x9f, 0xfd, 0x33, 0xf0, 0x3f, 0x53, 0x26, 0x87, 0x23, + 0x59, 0xac, 0x2b, 0x06, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, + 0xae, 0x42, 0x60, 0x82 }; -enum TextureType +const u32 TEXTURE_ATLAS_LENGTH = (u32)std::size(TEXTURE_ATLAS); +const vec2 TEXTURE_ATLAS_SIZE = {96, 120}; + +enum AtlasType { - TEXTURE_NONE, - TEXTURE_RECORD, - TEXTURE_ROOT, - TEXTURE_LAYER, - TEXTURE_NULL, - TEXTURE_TRIGGERS, - TEXTURE_VISIBLE, - TEXTURE_INVISIBLE, - TEXTURE_SHOW_RECT, - TEXTURE_HIDE_RECT, - TEXTURE_PAN, - TEXTURE_MOVE, - TEXTURE_ROTATE, - TEXTURE_SCALE, - TEXTURE_CROP, - TEXTURE_UNDO, - TEXTURE_REDO, - TEXTURE_DRAW, - TEXTURE_ERASE, - TEXTURE_COLOR_PICKER, - TEXTURE_ANIMATION, - TEXTURE_SPRITESHEET, - TEXTURE_EVENT, - TEXTURE_TRIGGER, - TEXTURE_PIVOT, - TEXTURE_SQUARE, - TEXTURE_CIRCLE, - TEXTURE_PICKER, - TEXTURE_FRAME_ALT, - TEXTURE_FRAME, - TEXTURE_TARGET, - TEXTURE_COUNT + ATLAS_NONE, + ATLAS_FOLDER, + ATLAS_ROOT, + ATLAS_LAYER, + ATLAS_NULL, + ATLAS_TRIGGERS, + ATLAS_VISIBLE, + ATLAS_INVISIBLE, + ATLAS_SHOW_RECT, + ATLAS_HIDE_RECT, + ATLAS_SHOW_TARGETS, + ATLAS_HIDE_TARGETS, + ATLAS_PAN, + ATLAS_MOVE, + ATLAS_ROTATE, + ATLAS_SCALE, + ATLAS_CROP, + ATLAS_DRAW, + ATLAS_ERASE, + ATLAS_COLOR_PICKER, + ATLAS_UNDO, + ATLAS_REDO, + ATLAS_ANIMATION, + ATLAS_SPRITESHEET, + ATLAS_EVENT, + ATLAS_PLAY, + ATLAS_PAUSE, + ATLAS_ADD, + ATLAS_REMOVE, + ATLAS_TRIGGER, + ATLAS_PIVOT, + ATLAS_SQUARE, + ATLAS_CIRCLE, + ATLAS_PICKER, + ATLAS_FRAME, + ATLAS_FRAME_ALT, + ATLAS_TARGET, + ATLAS_COUNT }; -const vec2 ATLAS_SIZE = {96, 104}; -const vec2 TEXTURE_SIZE_SMALL = {8, 8}; -const vec2 TEXTURE_SIZE = {16, 16}; -const vec2 TEXTURE_SIZE_OBLONG = {16, 40}; -const vec2 TEXTURE_SIZE_BIG = {40, 40}; - -#define ATLAS_UV(x,y){(f32)x / ATLAS_SIZE[0], (f32) y / ATLAS_SIZE[1]} -const vec2 ATLAS_UVS[TEXTURE_COUNT][2] = +struct AtlasEntry { - { ATLAS_UV( 0, 0), ATLAS_UV( 16, 16) }, /* 16 x 16 v */ - { ATLAS_UV( 16, 0), ATLAS_UV( 32, 16) }, - { ATLAS_UV( 32, 0), ATLAS_UV( 48, 16) }, - { ATLAS_UV( 48, 0), ATLAS_UV( 64, 16) }, - { ATLAS_UV( 64, 0), ATLAS_UV( 80, 16) }, - { ATLAS_UV( 80, 0), ATLAS_UV( 96, 16) }, - { ATLAS_UV( 0, 16), ATLAS_UV( 16, 32) }, - { ATLAS_UV( 16, 16), ATLAS_UV( 32, 32) }, - { ATLAS_UV( 32, 16), ATLAS_UV( 48, 32) }, - { ATLAS_UV( 48, 16), ATLAS_UV( 64, 32) }, - { ATLAS_UV( 64, 16), ATLAS_UV( 80, 32) }, - { ATLAS_UV( 80, 16), ATLAS_UV( 96, 32) }, - { ATLAS_UV( 0, 32), ATLAS_UV( 16, 48) }, - { ATLAS_UV( 16, 32), ATLAS_UV( 32, 48) }, - { ATLAS_UV( 32, 32), ATLAS_UV( 48, 48) }, - { ATLAS_UV( 48, 32), ATLAS_UV( 64, 48) }, - { ATLAS_UV( 64, 32), ATLAS_UV( 80, 48) }, - { ATLAS_UV( 80, 32), ATLAS_UV( 96, 48) }, - { ATLAS_UV( 0, 48), ATLAS_UV( 16, 64) }, - { ATLAS_UV( 16, 48), ATLAS_UV( 32, 64) }, - { ATLAS_UV( 32, 48), ATLAS_UV( 48, 64) }, - { ATLAS_UV( 48, 48), ATLAS_UV( 64, 64) }, - { ATLAS_UV( 64, 48), ATLAS_UV( 80, 64) }, - { ATLAS_UV( 80, 48), ATLAS_UV( 88, 56) }, /* 8 x 8 v */ - { ATLAS_UV( 88, 48), ATLAS_UV( 96, 56) }, - { ATLAS_UV( 80, 56), ATLAS_UV( 88, 64) }, - { ATLAS_UV( 88, 56), ATLAS_UV( 96, 64) }, - { ATLAS_UV( 0, 64), ATLAS_UV( 16,104) }, /* 16 x 40 */ - { ATLAS_UV( 16, 64), ATLAS_UV( 32,104) }, - { ATLAS_UV( 32, 64), ATLAS_UV( 48,104) }, - { ATLAS_UV( 48, 64), ATLAS_UV( 88,104) } /* 40 x 40 */ + vec2 position; + vec2 size; }; -#define ATLAS_UV_ARGS(type) ATLAS_UVS[type][0], ATLAS_UVS[type][1] +const vec2 ATLAS_SIZE_SMALL = {8, 8}; +const vec2 ATLAS_SIZE_NORMAL = {16, 16}; +const vec2 ATLAS_SIZE_OBLONG = {16, 40}; +const vec2 ATLAS_SIZE_BIG = {40, 40}; -const vec2 ATLAS_SIZES[TEXTURE_COUNT] = +const inline AtlasEntry ATLAS_ENTRIES[ATLAS_COUNT] = { - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE, - TEXTURE_SIZE_SMALL, - TEXTURE_SIZE_SMALL, - TEXTURE_SIZE_SMALL, - TEXTURE_SIZE_SMALL, - TEXTURE_SIZE_OBLONG, - TEXTURE_SIZE_OBLONG, - TEXTURE_SIZE_OBLONG, - TEXTURE_SIZE_BIG + {{ 0, 0}, ATLAS_SIZE_NORMAL}, + {{ 16, 0}, ATLAS_SIZE_NORMAL}, + {{ 32, 0}, ATLAS_SIZE_NORMAL}, + {{ 48, 0}, ATLAS_SIZE_NORMAL}, + {{ 64, 0}, ATLAS_SIZE_NORMAL}, + {{ 80, 0}, ATLAS_SIZE_NORMAL}, + {{ 0, 16}, ATLAS_SIZE_NORMAL}, + {{ 16, 16}, ATLAS_SIZE_NORMAL}, + {{ 32, 16}, ATLAS_SIZE_NORMAL}, + {{ 48, 16}, ATLAS_SIZE_NORMAL}, + {{ 64, 16}, ATLAS_SIZE_NORMAL}, + {{ 80, 16}, ATLAS_SIZE_NORMAL}, + {{ 0, 32}, ATLAS_SIZE_NORMAL}, + {{ 16, 32}, ATLAS_SIZE_NORMAL}, + {{ 32, 32}, ATLAS_SIZE_NORMAL}, + {{ 48, 32}, ATLAS_SIZE_NORMAL}, + {{ 64, 32}, ATLAS_SIZE_NORMAL}, + {{ 80, 32}, ATLAS_SIZE_NORMAL}, + {{ 0, 48}, ATLAS_SIZE_NORMAL}, + {{ 16, 48}, ATLAS_SIZE_NORMAL}, + {{ 32, 48}, ATLAS_SIZE_NORMAL}, + {{ 48, 48}, ATLAS_SIZE_NORMAL}, + {{ 64, 48}, ATLAS_SIZE_NORMAL}, + {{ 80, 48}, ATLAS_SIZE_NORMAL}, + {{ 0, 64}, ATLAS_SIZE_NORMAL}, + {{ 16, 64}, ATLAS_SIZE_NORMAL}, + {{ 32, 64}, ATLAS_SIZE_NORMAL}, + {{ 48, 64}, ATLAS_SIZE_NORMAL}, + {{ 64, 64}, ATLAS_SIZE_NORMAL}, + {{ 80, 64}, ATLAS_SIZE_SMALL }, + {{ 88, 64}, ATLAS_SIZE_SMALL }, + {{ 80, 72}, ATLAS_SIZE_SMALL }, + {{ 88, 72}, ATLAS_SIZE_SMALL }, + {{ 0, 80}, ATLAS_SIZE_OBLONG}, + {{16, 80}, ATLAS_SIZE_OBLONG}, + {{32, 80}, ATLAS_SIZE_OBLONG}, + {{48, 80}, ATLAS_SIZE_BIG} }; -#define ATLAS_UV_VERTICES(type) UV_VERTICES(ATLAS_UVS[type][0], ATLAS_UVS[type][1]) +#define ATLAS_POSITION(type) ATLAS_ENTRIES[type].position +#define ATLAS_SIZE(type) ATLAS_ENTRIES[type].size +#define ATLAS_UV_MIN(type) (ATLAS_POSITION(type) / TEXTURE_ATLAS_SIZE) +#define ATLAS_UV_MAX(type) ((ATLAS_POSITION(type) + ATLAS_SIZE(type)) / TEXTURE_ATLAS_SIZE) +#define ATLAS_UV_ARGS(type) ATLAS_UV_MIN(type), ATLAS_UV_MAX(type) +#define ATLAS_UV_VERTICES(type) UV_VERTICES(ATLAS_UV_MIN(type), ATLAS_UV_MAX(type)) -/* Shaders */ struct ShaderData { std::string vertex; @@ -331,4 +269,4 @@ const ShaderData SHADER_DATA[SHADER_COUNT] = { {SHADER_VERTEX, SHADER_FRAGMENT}, {SHADER_VERTEX, SHADER_TEXTURE_FRAGMENT} -}; +}; \ No newline at end of file diff --git a/src/canvas.cpp b/src/canvas.cpp index 6225dc3..9d7fe2e 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -228,21 +228,6 @@ void canvas_rect_draw(Canvas* self, const GLuint& shader, const mat4& transform, glUseProgram(0); } -void canvas_rect_dotted_draw(Canvas* self, const GLuint& shader, const mat4& transform, const vec4& color) -{ - glUseProgram(shader); - - glBindVertexArray(self->rectVAO); - - glUniformMatrix4fv(glGetUniformLocation(shader, SHADER_UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform)); - glUniform4fv(glGetUniformLocation(shader, SHADER_UNIFORM_COLOR), 1, value_ptr(color)); - - glDrawArrays(GL_LINE_LOOP, 0, 4); - - glBindVertexArray(0); - glUseProgram(0); -} - void canvas_axes_draw(Canvas* self, GLuint& shader, mat4& transform, vec4& color) { glUseProgram(shader); diff --git a/src/dialog.cpp b/src/dialog.cpp index 52a32c2..7c66ad8 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -2,8 +2,6 @@ #include "dialog.h" -static void _dialog_callback(void* userdata, const char* const* filelist, s32 filter); - static void _dialog_callback(void* userdata, const char* const* filelist, s32 filter) { Dialog* self; @@ -23,91 +21,59 @@ static void _dialog_callback(void* userdata, const char* const* filelist, s32 fi } } -void dialog_init(Dialog* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, SDL_Window* window) +void dialog_init(Dialog* self, SDL_Window* window) { - self->anm2 = anm2; - self->reference = reference; - self->resources = resources; self->window = window; } void dialog_anm2_open(Dialog* self) { - SDL_ShowOpenFileDialog(_dialog_callback, self, nullptr, DIALOG_FILE_FILTER_ANM2, 1, nullptr, false); + SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, 1, nullptr, false); self->type = DIALOG_ANM2_OPEN; } void dialog_anm2_save(Dialog* self) { - SDL_ShowSaveFileDialog(_dialog_callback, self, nullptr, DIALOG_FILE_FILTER_ANM2, 1, nullptr); + SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, 1, nullptr); self->type = DIALOG_ANM2_SAVE; } -void dialog_png_open(Dialog* self) +void dialog_spritesheet_add(Dialog* self) { - SDL_ShowOpenFileDialog(_dialog_callback, self, nullptr, DIALOG_FILE_FILTER_PNG, 1, nullptr, false); - self->type = DIALOG_PNG_OPEN; + SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, 1, nullptr, false); + self->type = DIALOG_SPRITESHEET_ADD; } -void dialog_png_replace(Dialog* self) +void dialog_spritesheet_replace(Dialog* self, s32 id) { - SDL_ShowOpenFileDialog(_dialog_callback, self, nullptr, DIALOG_FILE_FILTER_PNG, 1, nullptr, false); - self->type = DIALOG_PNG_REPLACE; + SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, 1, nullptr, false); + self->replaceID = id; + self->type = DIALOG_SPRITESHEET_REPLACE; } -void dialog_update(Dialog* self) +void dialog_render_path_set(Dialog* self) { - self->isJustSelected = false; + SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_RENDER, 2, nullptr); + self->type = DIALOG_RENDER_PATH_SET; +} - if (self->isSelected) - { - Texture texture; - s32 id; - - switch (self->type) - { - case DIALOG_ANM2_OPEN: - *self->reference = Anm2Reference{}; - resources_textures_free(self->resources); - anm2_deserialize(self->anm2, self->resources, self->path); - window_title_from_path_set(self->window, self->path); - break; - case DIALOG_ANM2_SAVE: - anm2_serialize(self->anm2, self->path); - window_title_from_path_set(self->window, self->path); - break; - case DIALOG_PNG_OPEN: - id = map_next_id_get(self->resources->textures); - self->anm2->spritesheets[id] = Anm2Spritesheet{}; - self->anm2->spritesheets[id].path = self->path; - resources_texture_init(self->resources, self->path, id); - break; - case DIALOG_PNG_REPLACE: - self->anm2->spritesheets[self->replaceID].path = self->path; - resources_texture_init(self->resources, self->path, self->replaceID); - self->replaceID = -1; - break; - default: - break; - } +void dialog_render_directory_set(Dialog* self) +{ + SDL_ShowOpenFolderDialog(_dialog_callback, self, self->window, nullptr, false); + self->type = DIALOG_RENDER_DIRECTORY_SET; +} - self->lastType = self->type; - self->lastPath = self->path; - self->type = DIALOG_NONE; - self->path.clear(); - - self->isJustSelected = true; - self->isSelected = false; - } +void dialog_ffmpeg_path_set(Dialog* self) +{ + SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_FFMPEG, 1, nullptr, false); + self->type = DIALOG_FFMPEG_PATH_SET; } void dialog_reset(Dialog* self) { - self->lastType = DIALOG_NONE; + self->replaceID = ID_NONE; self->type = DIALOG_NONE; - self->lastPath.clear(); self->path.clear(); - self->isJustSelected = false; self->isSelected = false; } \ No newline at end of file diff --git a/src/dialog.h b/src/dialog.h index bdfcdc4..1fb15df 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -1,17 +1,26 @@ #pragma once -#include "anm2.h" -#include "resources.h" #include "window.h" const SDL_DialogFileFilter DIALOG_FILE_FILTER_ANM2[] = { - {"Anm2", "anm2;xml"} + {"Anm2 file", "anm2;xml"} }; const SDL_DialogFileFilter DIALOG_FILE_FILTER_PNG[] = { - {"png", "png"} + {"PNG image", "png"} +}; + +const SDL_DialogFileFilter DIALOG_FILE_FILTER_RENDER[] = +{ + {"GIF image", "gif"}, + {"WebM video", "webm"} +}; + +const SDL_DialogFileFilter DIALOG_FILE_FILTER_FFMPEG[] = +{ + {"Executable", ""} }; enum DialogType @@ -19,32 +28,29 @@ enum DialogType DIALOG_NONE, DIALOG_ANM2_OPEN, DIALOG_ANM2_SAVE, - DIALOG_PNG_OPEN, - DIALOG_PNG_REPLACE, - DIALOG_FRAME_DIRECTORY_OPEN, + DIALOG_SPRITESHEET_ADD, + DIALOG_SPRITESHEET_REPLACE, + DIALOG_RENDER_PATH_SET, + DIALOG_RENDER_DIRECTORY_SET, + DIALOG_FFMPEG_PATH_SET }; struct Dialog { - Anm2* anm2 = nullptr; - Anm2Reference* reference = nullptr; - Resources* resources = nullptr; SDL_Window* window = nullptr; + s32 selectedFilter = ID_NONE; std::string path{}; - std::string lastPath{}; s32 replaceID = ID_NONE; - s32 selectedFilter{}; DialogType type = DIALOG_NONE; - DialogType lastType = DIALOG_NONE; bool isSelected = false; - bool isJustSelected = false; }; -void dialog_init(Dialog* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, SDL_Window* window); +void dialog_init(Dialog* self, SDL_Window* window); void dialog_anm2_open(Dialog* self); -void dialog_png_open(Dialog* self); -void dialog_png_replace(Dialog* self); +void dialog_spritesheet_add(Dialog* self); +void dialog_spritesheet_replace(Dialog* self, s32 id); void dialog_anm2_save(Dialog* self); -void dialog_frame_directory_open(Dialog* self); -void dialog_update(Dialog* self); +void dialog_render_path_set(Dialog* self); +void dialog_render_directory_set(Dialog* self); +void dialog_ffmpeg_path_set(Dialog* self); void dialog_reset(Dialog* self); \ No newline at end of file diff --git a/src/editor.cpp b/src/editor.cpp index 293fb4d..cedd464 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -44,7 +44,7 @@ void editor_draw(Editor* self) canvas_rect_draw(&self->canvas, shaderLine, mvp, EDITOR_FRAME_COLOR); mvp = canvas_mvp_get(transform, CANVAS_PIVOT_SIZE, frame->crop + frame->pivot, CANVAS_PIVOT_SIZE * 0.5f); - f32 vertices[] = ATLAS_UV_VERTICES(TEXTURE_PIVOT); + f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT); canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, mvp, vertices, EDITOR_PIVOT_COLOR); } } diff --git a/src/ffmpeg.cpp b/src/ffmpeg.cpp new file mode 100644 index 0000000..281bab9 --- /dev/null +++ b/src/ffmpeg.cpp @@ -0,0 +1,60 @@ +#include "ffmpeg.h" + +bool +ffmpeg_render +( + const std::string& ffmpegPath, + const std::string& outputPath, + const std::vector& frames, + ivec2 size, + s32 fps, + enum RenderType type +) +{ + if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || outputPath.empty()) return false; + + std::string command{}; + + switch (type) + { + case RENDER_GIF: + command = std::format(FFMPEG_GIF_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath); + break; + case RENDER_WEBM: + command = std::format(FFMPEG_WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath); + break; + default: + return false; + } + + FILE* fp = POPEN(command.c_str(), PWRITE_MODE); + + if (!fp) + { + log_info(std::format(FFMPEG_POPEN_ERROR, strerror(errno))); + return false; + } + + size_t frameBytes = size.x * size.y * TEXTURE_CHANNELS; + + for (const auto& frame : frames) + { + std::vector rgba = texture_download(&frame); + + if (rgba.size() != frameBytes) + { + PCLOSE(fp); + return false; + } + + if (fwrite(rgba.data(), 1, frameBytes, fp) != frameBytes) + { + PCLOSE(fp); + return false; + } + } + + const int code = PCLOSE(fp); + + return (code == 0); +} \ No newline at end of file diff --git a/src/ffmpeg.h b/src/ffmpeg.h new file mode 100644 index 0000000..031ff27 --- /dev/null +++ b/src/ffmpeg.h @@ -0,0 +1,32 @@ +#pragma once + +#include "render.h" +#include "texture.h" + +#define FFMPEG_POPEN_ERROR "popen() (for FFmpeg) failed!\n{}" + +static constexpr const char* FFMPEG_GIF_FORMAT = +"\"{0}\" -y " +"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " +"-lavfi \"split[s0][s1];" +"[s0]palettegen=stats_mode=full[p];" +"[s1][p]paletteuse=dither=floyd_steinberg\" " +"-loop 0 \"{4}\""; + +static constexpr const char* FFMPEG_WEBM_FORMAT = +"\"{0}\" -y " +"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " +"-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 " +"-auto-alt-ref 0 -an \"{4}\""; + + +bool +ffmpeg_render +( + const std::string& ffmpegPath, + const std::string& outputPath, + const std::vector& frames, + ivec2 size, + s32 fps, + enum RenderType type +); \ No newline at end of file diff --git a/src/imgui.cpp b/src/imgui.cpp index e1bbf3b..e06aed6 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -35,9 +35,9 @@ static void _imgui_clipboard_hovered_item_set(Imgui* self, const T& data) self->clipboard->hoveredItem = ClipboardItem(data); } -static void _imgui_atlas_image(Imgui* self, TextureType type) +static void _imgui_atlas_image(Imgui* self, AtlasType type) { - ImGui::Image(self->resources->atlas.id, ATLAS_SIZES[type], ATLAS_UV_ARGS(type)); + ImGui::Image(self->resources->atlas.id, ATLAS_SIZE(type), ATLAS_UV_ARGS(type)); } static void _imgui_item_text(const ImguiItem& item) @@ -83,7 +83,7 @@ static void _imgui_item(Imgui* self, const ImguiItem& item, bool* isActivated) } } - if (isActivated && self->isHotkeysEnabled && (item.is_chord() && ImGui::IsKeyChordPressed(item.chord))) + if (isActivated && self->isContextualActionsEnabled && (item.is_chord() && ImGui::IsKeyChordPressed(item.chord))) if (item.is_focus_window() && (imgui_nav_window_root_get() == item.focusWindow)) *isActivated = true; @@ -97,25 +97,43 @@ static void _imgui_item(Imgui* self, const ImguiItem& item, bool* isActivated) if (item.is_popup()) { - ImGui::OpenPopup(item.popup.c_str()); - - switch (item.popupType) - { - case IMGUI_POPUP_CENTER_SCREEN: - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - break; - case IMGUI_POPUP_BY_ITEM: - default: - ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMin().y + ImGui::GetItemRectSize().y)); - break; - } + self->pendingPopup = item.popup; + self->pendingPopupType = item.popupType; + self->pendingPopupPosition = ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMin().y + ImGui::GetItemRectSize().y); } } } -static bool _imgui_item_combo(Imgui* self, const ImguiItem& item, s32* current, const char* const items[], s32 count) +static void _imgui_pending_popup_process(Imgui* self) { - bool isActivated = ImGui::Combo(item.label.c_str(), current, items, count); + if (self->pendingPopup.empty()) return; + + switch (self->pendingPopupType) + { + case IMGUI_POPUP_CENTER_SCREEN: + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + break; + case IMGUI_POPUP_BY_ITEM: + default: + ImGui::SetNextWindowPos(self->pendingPopupPosition); + break; + } + + ImGui::OpenPopup(self->pendingPopup.c_str()); + + self->pendingPopup.clear(); + self->pendingPopupType = IMGUI_POPUP_NONE; + self->pendingPopupPosition = ImVec2(); +} + +static bool _imgui_item_combo(Imgui* self, const ImguiItem& item, s32* current) +{ + std::vector cStrings; + cStrings.reserve(item.items.size()); + for (auto& string : item.items) + cStrings.push_back(string.c_str()); + + bool isActivated = ImGui::Combo(item.label.c_str(), current, cStrings.data(), (s32)item.items.size()); _imgui_item(self, item, &isActivated); return isActivated; } @@ -125,14 +143,15 @@ static bool _imgui_item_selectable(Imgui* self, const ImguiItem& item) const char* label = item.label.c_str(); s32 flags = item.flags; - if (item.isInactive || item.color.is_normal()) + if (item.isInactive) { - vec4 color = item.isInactive ? IMGUI_INACTIVE_COLOR : item.color.normal; - ImGui::PushStyleColor(ImGuiCol_Text, color); - flags |= ImGuiSelectableFlags_Disabled; - } + ImGui::PushStyleColor(ImGuiCol_Text, IMGUI_INACTIVE_COLOR); + flags |= ImGuiSelectableFlags_Disabled; + } + else if (item.color.is_normal()) + ImGui::PushStyleColor(ImGuiCol_Text, item.color.normal); - ImVec2 size = item.is_size() ? item.size : item.isSizeToText ? ImGui::CalcTextSize(label) :ImVec2(0, 0); + ImVec2 size = item.is_size() ? item.size : item.isSizeToText ? ImGui::CalcTextSize(label) : ImVec2(); bool isActivated = ImGui::Selectable(label, item.isSelected, flags, size); _imgui_item(self, item, &isActivated); @@ -173,14 +192,19 @@ static bool _imgui_item_inputint2(Imgui* self, const ImguiItem& item, ivec2& val static bool _imgui_item_inputtext(Imgui* self, const ImguiItem& item, std::string& buffer) { - if ((s32)buffer.size() < item.max) buffer.resize(item.max); + if ((s32)buffer.size() < (s32)item.max) buffer.resize(item.max); ImVec2 size = item.is_size() ? item.size : ImVec2(-FLT_MIN, 0); - + + if (item.isSizeToChild) + { + size.x = (ImGui::GetWindowSize().x - ImGui::GetStyle().ItemSpacing.x * (item.childRowItemCount + 1)) / item.childRowItemCount; + size.x -= ImGui::CalcTextSize(item.label.c_str()).x * 2; + } + ImGui::SetNextItemWidth(size.x); - ImGui::InputText(item.label.c_str(), &buffer[0], item.max, item.flags); - bool isActivated = ImGui::IsItemActivated(); + bool isActivated = ImGui::InputText(item.label.c_str(), &buffer[0], item.max, item.flags); _imgui_item(self, item, &isActivated); return isActivated; @@ -292,7 +316,7 @@ static bool _imgui_item_selectable_inputtext(Imgui* self, const ImguiItem& item, renameID = ID_NONE; itemID = ID_NONE; self->isRename = false; - + self->isContextualActionsEnabled = true; } ImGui::PopID(); @@ -307,6 +331,7 @@ static bool _imgui_item_selectable_inputtext(Imgui* self, const ImguiItem& item, renameID = id; itemID = item.id; ImGui::SetKeyboardFocusHere(-1); + self->isContextualActionsEnabled = false; } } @@ -340,6 +365,7 @@ static bool _imgui_item_selectable_inputint(Imgui* self, const ImguiItem& item, itemID = ID_NONE; changeID = ID_NONE; self->isChangeValue = false; + self->isContextualActionsEnabled = true; } ImGui::PopID(); @@ -353,6 +379,7 @@ static bool _imgui_item_selectable_inputint(Imgui* self, const ImguiItem& item, itemID = item.id; changeID = id; ImGui::SetKeyboardFocusHere(-1); + self->isContextualActionsEnabled = false; } } @@ -361,16 +388,9 @@ static bool _imgui_item_selectable_inputint(Imgui* self, const ImguiItem& item, return isActivated; } -static void _imgui_item_atlas_image_text(Imgui* self, const ImguiItem& item) -{ - _imgui_atlas_image(self, item.texture); - ImGui::SameLine(); - ImGui::Text(item.label.c_str()); -} - static bool _imgui_item_atlas_image_selectable(Imgui* self, const ImguiItem& item) { - _imgui_atlas_image(self, item.texture); + _imgui_atlas_image(self, item.atlas); ImGui::SameLine(); return _imgui_item_selectable(self, item); } @@ -388,14 +408,14 @@ static bool _imgui_item_text_inputtext(Imgui* self, const ImguiItem& item, std:: static bool _imgui_item_atlas_image_selectable_inputtext(Imgui* self, ImguiItem& item, std::string& string, s32 id) { - _imgui_atlas_image(self, item.texture); + _imgui_atlas_image(self, item.atlas); ImGui::SameLine(); return _imgui_item_selectable_inputtext(self, item, string, id); } static bool _imgui_item_atlas_image_selectable_inputint(Imgui* self, ImguiItem& item, s32& value, s32 id) { - _imgui_atlas_image(self, item.texture); + _imgui_atlas_image(self, item.atlas); ImGui::SameLine(); return _imgui_item_selectable_inputint(self, item, value, id); } @@ -407,7 +427,7 @@ static bool _imgui_item_atlas_image_checkbox_selectable(Imgui* self, const Imgui _imgui_item_checkbox(self, checkboxItem, value); ImGui::SameLine(); - _imgui_atlas_image(self, item.texture); + _imgui_atlas_image(self, item.atlas); ImGui::SameLine(); return _imgui_item_selectable(self, item); } @@ -415,7 +435,7 @@ static bool _imgui_item_atlas_image_checkbox_selectable(Imgui* self, const Imgui static bool _imgui_item_atlas_image_button(Imgui* self, const ImguiItem& item) { bool isActivated = false; - ImVec2 imageSize = (ATLAS_SIZES[item.texture]); + ImVec2 imageSize = (ATLAS_SIZE(item.atlas)); ImVec2 buttonSize = item.is_size() ? item.size : imageSize; if (item.color.is_normal()) ImGui::PushStyleColor(ImGuiCol_Button, item.color.normal); @@ -432,10 +452,10 @@ static bool _imgui_item_atlas_image_button(Imgui* self, const ImguiItem& item) ImVec2 imageMin = pos + item.contentOffset; ImVec2 imageMax = imageMin + imageSize; - ImGui::GetWindowDrawList()->AddImage(self->resources->atlas.id, imageMin, imageMax, ATLAS_UV_ARGS(item.texture)); + ImGui::GetWindowDrawList()->AddImage(self->resources->atlas.id, imageMin, imageMax, ATLAS_UV_ARGS(item.atlas)); } else - isActivated = ImGui::ImageButton(item.label.c_str(), self->resources->atlas.id, buttonSize, ATLAS_UV_ARGS(item.texture)); + isActivated = ImGui::ImageButton(item.label.c_str(), self->resources->atlas.id, buttonSize, ATLAS_UV_ARGS(item.atlas)); _imgui_item(self, item, &isActivated); if (item.color.is_normal()) ImGui::PopStyleColor(); @@ -481,26 +501,32 @@ static void _imgui_keyboard_navigation_set(bool value) if (!value) ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; } -static bool _imgui_item_yes_no_popup(Imgui* self, const ImguiItem& item) +static bool _imgui_item_option_popup(Imgui* self, const ImguiItem& item) { ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal(item.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + self->isContextualActionsEnabled = false; + ImGui::Text(item.label.c_str()); ImGui::Separator(); - if (_imgui_item_button(self, IMGUI_POPUP_YES_BUTTON)) + if (_imgui_item_button(self, IMGUI_POPUP_CONFIRM_BUTTON)) { ImGui::CloseCurrentPopup(); ImGui::EndPopup(); + self->isContextualActionsEnabled = true; return true; } ImGui::SameLine(); - if (_imgui_item_button(self, IMGUI_POPUP_NO_BUTTON)) + if (_imgui_item_button(self, IMGUI_POPUP_CANCEL_BUTTON)) + { ImGui::CloseCurrentPopup(); + self->isContextualActionsEnabled = true; + } ImGui::EndPopup(); } @@ -639,7 +665,7 @@ static void _imgui_timeline(Imgui* self) drawList->AddText(textPosition, textColor, buffer.c_str()); } - drawList->AddImage(self->resources->atlas.id, positionStart, positionEnd, ATLAS_UV_ARGS(TEXTURE_FRAME)); + drawList->AddImage(self->resources->atlas.id, positionStart, positionEnd, ATLAS_UV_ARGS(ATLAS_FRAME_ALT)); } _imgui_item_end_child(); // IMGUI_TIMELINE_HEADER @@ -656,7 +682,7 @@ static void _imgui_timeline(Imgui* self) drawList = ImGui::GetWindowDrawList(); drawList->PushClipRect(clipRectMin, clipRectMax, true); - drawList->AddImage(self->resources->atlas.id, pos, ImVec2(pos.x + frameSize.x, pos.y + frameSize.y), ATLAS_UV_ARGS(TEXTURE_PICKER)); + drawList->AddImage(self->resources->atlas.id, pos, ImVec2(pos.x + frameSize.x, pos.y + frameSize.y), ATLAS_UV_ARGS(ATLAS_PICKER)); drawList->AddRectFilled(lineStart, lineEnd, IMGUI_PICKER_LINE_COLOR); drawList->PopClipRect(); @@ -669,7 +695,7 @@ static void _imgui_timeline(Imgui* self) if (!item) return; - ImVec2 buttonSize = ImVec2(TEXTURE_SIZE) + (defaultFramePadding * ImVec2(2, 2)); + ImVec2 buttonSize = ImVec2(ATLAS_SIZE_NORMAL) + (defaultFramePadding * ImVec2(2, 2)); Anm2Type& type = reference.itemType; Anm2Layer* layer = nullptr; @@ -749,14 +775,14 @@ static void _imgui_timeline(Imgui* self) if (type == ANM2_NULL) { - ImguiItem rectItem = null->isShowRect ? IMGUI_TIMELINE_ITEM_SHOW_RECT : IMGUI_TIMELINE_ITEM_HIDE_RECT; + const ImguiItem& rectItem = null->isShowRect ? IMGUI_TIMELINE_ITEM_SHOW_RECT : IMGUI_TIMELINE_ITEM_HIDE_RECT; if (_imgui_item_atlas_image_button(self, rectItem)) null->isShowRect = !null->isShowRect; ImGui::SameLine(0.0f, defaultItemSpacing.x); } - ImguiItem visibleItem = item->isVisible ? IMGUI_TIMELINE_ITEM_VISIBLE : IMGUI_TIMELINE_ITEM_INVISIBLE; + const ImguiItem& visibleItem = item->isVisible ? IMGUI_TIMELINE_ITEM_VISIBLE : IMGUI_TIMELINE_ITEM_INVISIBLE; if (_imgui_item_atlas_image_button(self, visibleItem)) item->isVisible = !item->isVisible; @@ -875,7 +901,7 @@ static void _imgui_timeline(Imgui* self) ImVec2 endPosition(endX, cursorPos.y + frameSize.y); drawList->AddRectFilled(startPosition, endPosition, bgColor); - drawList->AddImage(self->resources->atlas.id, startPosition, endPosition, ATLAS_UV_ARGS(TEXTURE_FRAME_ALT)); + drawList->AddImage(self->resources->atlas.id, startPosition, endPosition, ATLAS_UV_ARGS(ATLAS_FRAME)); } ImGui::SetCursorPos(startPos); @@ -886,14 +912,14 @@ static void _imgui_timeline(Imgui* self) reference.frameIndex = i; ImguiItem frameButton = *IMGUI_TIMELINE_FRAMES[type]; ImVec2 framePos = ImGui::GetCursorPos(); - frameButton.texture = frame.isInterpolated ? TEXTURE_CIRCLE : TEXTURE_SQUARE; + frameButton.atlas = frame.isInterpolated ? ATLAS_CIRCLE : ATLAS_SQUARE; frameButton.size = {frameSize.x * frame.delay, frameSize.y}; frameButton.isSelected = reference == *self->reference; if (type == ANM2_TRIGGERS) { framePos.x = startPos.x + (frameSize.x * frame.atFrame); - frameButton.texture = TEXTURE_TRIGGER; + frameButton.atlas = ATLAS_TRIGGER; } ImGui::SetCursorPos(framePos); @@ -1017,21 +1043,7 @@ static void _imgui_timeline(Imgui* self) Anm2Frame* frame = anm2_frame_from_reference(self->anm2, self->reference); _imgui_item_begin_child(IMGUI_TIMELINE_FOOTER_ITEM_CHILD); - - if(_imgui_item_button(self, IMGUI_TIMELINE_ADD_ITEM)) - ImGui::OpenPopup(IMGUI_TIMELINE_ADD_ITEM.popup.c_str()); - - if (ImGui::BeginPopup(IMGUI_TIMELINE_ADD_ITEM.popup.c_str())) - { - if (_imgui_item_selectable(self, IMGUI_TIMELINE_ADD_ITEM_LAYER)) - anm2_layer_add(self->anm2); - - if (_imgui_item_selectable(self, IMGUI_TIMELINE_ADD_ITEM_NULL)) - anm2_null_add(self->anm2); - - ImGui::EndPopup(); - } - + _imgui_item_button(self, IMGUI_TIMELINE_ADD_ITEM); ImGui::SameLine(); if (_imgui_item_button(self, IMGUI_TIMELINE_REMOVE_ITEM)) @@ -1059,7 +1071,7 @@ static void _imgui_timeline(Imgui* self) _imgui_item_begin_child(IMGUI_TIMELINE_FOOTER_OPTIONS_CHILD); - ImguiItem playPauseItem = self->preview->isPlaying ? IMGUI_TIMELINE_PAUSE : IMGUI_TIMELINE_PLAY; + const ImguiItem& playPauseItem = self->preview->isPlaying ? IMGUI_TIMELINE_PAUSE : IMGUI_TIMELINE_PLAY; if (_imgui_item_button(self, playPauseItem)) self->preview->isPlaying = !self->preview->isPlaying; @@ -1082,12 +1094,35 @@ static void _imgui_timeline(Imgui* self) ImGui::SameLine(); - if (_imgui_item_button(self, IMGUI_TIMELINE_BAKE)) + _imgui_item_button(self, IMGUI_TIMELINE_BAKE); + + ImGui::SameLine(); + + if (_imgui_item_button(self, IMGUI_TIMELINE_FIT_ANIMATION_LENGTH)) + anm2_animation_length_set(animation); + + ImGui::SameLine(); + _imgui_item_inputint(self, IMGUI_TIMELINE_ANIMATION_LENGTH, animation->frameNum); + ImGui::SameLine(); + _imgui_item_inputint(self, IMGUI_TIMELINE_FPS, self->anm2->fps); + ImGui::SameLine(); + _imgui_item_checkbox(self, IMGUI_TIMELINE_LOOP, animation->isLoop); + ImGui::SameLine(); + _imgui_item_text_inputtext(self, IMGUI_TIMELINE_CREATED_BY, self->anm2->createdBy); + + _imgui_item_end_child(); //IMGUI_TIMELINE_FOOTER_OPTIONS_CHILD + + _imgui_pending_popup_process(self); + + if (ImGui::BeginPopup(IMGUI_TIMELINE_ADD_ITEM.popup.c_str())) { - if (frame) - ImGui::OpenPopup(IMGUI_TIMELINE_BAKE.popup.c_str()); - else - ImGui::CloseCurrentPopup(); + if (_imgui_item_selectable(self, IMGUI_TIMELINE_ADD_ITEM_LAYER)) + anm2_layer_add(self->anm2); + + if (_imgui_item_selectable(self, IMGUI_TIMELINE_ADD_ITEM_NULL)) + anm2_null_add(self->anm2); + + ImGui::EndPopup(); } if (ImGui::BeginPopupModal(IMGUI_TIMELINE_BAKE.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) @@ -1129,22 +1164,6 @@ static void _imgui_timeline(Imgui* self) ImGui::EndPopup(); } } - - ImGui::SameLine(); - - if (_imgui_item_button(self, IMGUI_TIMELINE_FIT_ANIMATION_LENGTH)) - anm2_animation_length_set(animation); - - ImGui::SameLine(); - _imgui_item_inputint(self, IMGUI_TIMELINE_ANIMATION_LENGTH, animation->frameNum); - ImGui::SameLine(); - _imgui_item_inputint(self, IMGUI_TIMELINE_FPS, self->anm2->fps); - ImGui::SameLine(); - _imgui_item_checkbox(self, IMGUI_TIMELINE_LOOP, animation->isLoop); - ImGui::SameLine(); - _imgui_item_text_inputtext(self, IMGUI_TIMELINE_CREATED_BY, self->anm2->createdBy); - - _imgui_item_end_child(); //IMGUI_TIMELINE_FOOTER_OPTIONS_CHILD _imgui_item_end(); // IMGUI_TIMELINE @@ -1159,37 +1178,237 @@ static void _imgui_taskbar(Imgui* self) ImGui::SetNextWindowPos(viewport->Pos); _imgui_item_begin(taskbarItem); + Anm2Animation* animation = anm2_animation_from_reference(self->anm2, self->reference); + _imgui_item_selectable(self, IMGUI_TASKBAR_FILE); + ImGui::SameLine(); + _imgui_item_selectable(self, IMGUI_TASKBAR_WIZARD); + ImGui::SameLine(); + _imgui_item_selectable(self, IMGUI_TASKBAR_PLAYBACK); + _imgui_pending_popup_process(self); if (ImGui::BeginPopup(IMGUI_TASKBAR_FILE.popup.c_str())) { - _imgui_item_selectable(self, IMGUI_FILE_NEW); - _imgui_item_selectable(self, IMGUI_FILE_OPEN); - _imgui_item_selectable(self, IMGUI_FILE_SAVE); - _imgui_item_selectable(self, IMGUI_FILE_SAVE_AS); + _imgui_item_selectable(self, IMGUI_FILE_NEW); // imgui_file_new + _imgui_item_selectable(self, IMGUI_FILE_OPEN); // imgui_file_open + _imgui_item_selectable(self, IMGUI_FILE_SAVE); // imgui_file_save + _imgui_item_selectable(self, IMGUI_FILE_SAVE_AS); // imgui_file_save_as + ImGui::EndPopup(); } - ImGui::SameLine(); - - _imgui_item_selectable(self, IMGUI_TASKBAR_WIZARD); - if (ImGui::BeginPopup(IMGUI_TASKBAR_WIZARD.popup.c_str())) { _imgui_item_selectable(self, IMGUI_TASKBAR_WIZARD_GENERATE_ANIMATION_FROM_GRID); - _imgui_item_selectable(self, IMGUI_TASKBAR_WIZARD_RECORD_GIF_ANIMATION); + ImGui::Separator(); + _imgui_item_selectable(self, IMGUI_TASKBAR_WIZARD_RENDER_ANIMATION); + ImGui::EndPopup(); } - + + _imgui_pending_popup_process(self); + + if (ImGui::BeginPopupModal(IMGUI_TASKBAR_WIZARD_RENDER_ANIMATION.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (animation) + { + RenderType& type = self->settings->renderType; + std::string& path = self->settings->renderPath; + std::string& format = self->settings->renderFormat; + std::string& ffmpegPath = self->settings->ffmpegPath; + + self->isContextualActionsEnabled = false; + + _imgui_item_begin_child(IMGUI_RENDER_ANIMATION_CHILD); + + if (_imgui_item_atlas_image_button(self, IMGUI_RENDER_ANIMATION_BROWSE)) + { + switch (self->settings->renderType) + { + case RENDER_PNG: + dialog_render_directory_set(self->dialog); + break; + default: + dialog_render_path_set(self->dialog); + break; + } + } + + if (self->dialog->isSelected && self->dialog->type == DIALOG_RENDER_PATH_SET) + { + path = self->dialog->path; + dialog_reset(self->dialog); + } + + ImGui::SameLine(); + _imgui_item_inputtext(self, IMGUI_RENDER_ANIMATION_LOCATION, path); + + if (_imgui_item_atlas_image_button(self, IMGUI_RENDER_ANIMATION_FFMPEG_BROWSE)) + dialog_ffmpeg_path_set(self->dialog); + + if (self->dialog->isSelected && self->dialog->type == DIALOG_FFMPEG_PATH_SET) + { + ffmpegPath = self->dialog->path; + dialog_reset(self->dialog); + } + + ImGui::SameLine(); + _imgui_item_inputtext(self, IMGUI_RENDER_ANIMATION_FFMPEG_PATH, ffmpegPath); + + _imgui_item_combo(self, IMGUI_RENDER_ANIMATION_OUTPUT, (s32*)&type); + + _imgui_item_inputtext(self, IMGUI_RENDER_ANIMATION_FORMAT, format); + + ImGui::Separator(); + + if (_imgui_item_button(self, IMGUI_RENDER_ANIMATION_CONFIRM)) + { + bool isRenderStart = true; + + switch (type) + { + case RENDER_PNG: + if (!std::filesystem::is_directory(path)) + { + imgui_message_queue_push(self, IMGUI_MESSAGE_RENDER_ANIMATION_DIRECTORY_ERROR); + isRenderStart = false; + } + break; + case RENDER_GIF: + case RENDER_WEBM: + if (!path_valid(path)) + { + imgui_message_queue_push(self, IMGUI_MESSAGE_RENDER_ANIMATION_PATH_ERROR); + isRenderStart = false; + } + default: + break; + } + + if (isRenderStart) + preview_render_start(self->preview); + else + self->preview->isRenderCancelled = true; + + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (_imgui_item_button(self, IMGUI_RENDER_ANIMATION_CANCEL)) + ImGui::CloseCurrentPopup(); + + _imgui_item_end_child(); // IMGUI_RENDER_ANIMATION_CHILD + ImGui::EndPopup(); + } + else + { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + imgui_message_queue_push(self, IMGUI_MESSAGE_RENDER_ANIMATION_NO_SELECTED_ANIMATION_ERROR); + } + } + + _imgui_pending_popup_process(self); + + if (ImGui::BeginPopupModal(IMGUI_RENDER_ANIMATION_CONFIRM.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + auto rendering_end = [&]() + { + preview_render_end(self->preview); + self->isContextualActionsEnabled = true; + ImGui::CloseCurrentPopup(); + }; + + RenderType& type = self->settings->renderType; + std::string& format = self->settings->renderFormat; + std::vector& frames = self->preview->renderFrames; + std::string path = std::string(self->settings->renderPath.c_str()); + + if (self->preview->isRenderCancelled) + { + rendering_end(); + self->preview->isRenderCancelled = false; + } + + if (!animation) + { + imgui_message_queue_push(self, IMGUI_MESSAGE_RENDER_ANIMATION_NO_ANIMATION_ERROR); + rendering_end(); + } + + _imgui_item_begin_child(IMGUI_RENDERING_ANIMATION_CHILD); + + f32 progress = self->preview->time / (animation->frameNum - 1); + ImGui::ProgressBar(progress); + + if (_imgui_item_button(self, IMGUI_RENDERING_ANIMATION_CANCEL)) + self->preview->isRenderCancelled = true; + + _imgui_item_end_child(); // IMGUI_RENDERING_ANIMATION_CHILD + + if (self->preview->isRenderFinished && frames.empty()) + { + imgui_message_queue_push(self, IMGUI_MESSAGE_RENDER_ANIMATION_NO_FRAMES_ERROR); + rendering_end(); + } + + if (self->preview->isRenderFinished) + { + switch (type) + { + case RENDER_PNG: + { + std::filesystem::path workingPath = std::filesystem::current_path(); + std::filesystem::current_path(path); + + for (auto [i, frame] : std::views::enumerate(frames)) + { + std::string framePath = std::vformat(format, std::make_format_args(i)); + framePath = path_extension_change(framePath, RENDER_EXTENSIONS[type]); + if (!frame.isInvalid) + texture_from_gl_write(&frame, framePath); + } + + std::filesystem::current_path(workingPath); + imgui_message_queue_push(self, std::format(IMGUI_MESSAGE_RENDER_ANIMATION_FRAMES_SAVE_FORMAT, path)); + break; + } + case RENDER_GIF: + case RENDER_WEBM: + { + std::string ffmpegPath = std::string(self->settings->ffmpegPath.c_str()); + path = path_extension_change(path, RENDER_EXTENSIONS[type]); + + if (ffmpeg_render(ffmpegPath, path, frames, self->preview->canvas.size, self->anm2->fps, type)) + { + std::string message = std::format(IMGUI_MESSAGE_RENDER_ANIMATION_SAVE_FORMAT, path); + imgui_message_queue_push(self, message); + log_info(message); + } + else + { + std::string message = std::format(IMGUI_MESSAGE_RENDER_ANIMATION_FFMPEG_ERROR, path); + imgui_message_queue_push(self, message); + log_error(message); + } + break; + } + default: + break; + } + + rendering_end(); + } + + ImGui::EndPopup(); + } + if (ImGui::BeginPopupModal(IMGUI_TASKBAR_WIZARD_GENERATE_ANIMATION_FROM_GRID.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::EndPopup(); } - ImGui::SameLine(); - - _imgui_item_selectable(self, IMGUI_TASKBAR_PLAYBACK); - if (ImGui::BeginPopup(IMGUI_TASKBAR_PLAYBACK.popup.c_str())) { _imgui_item_checkbox(self, IMGUI_PLAYBACK_ALWAYS_LOOP, self->settings->playbackIsLoop); @@ -1197,6 +1416,30 @@ static void _imgui_taskbar(Imgui* self) } _imgui_item_end(); + + if (self->dialog->isSelected) + { + switch (self->dialog->type) + { + case DIALOG_ANM2_OPEN: + *self->reference = Anm2Reference{}; + resources_textures_free(self->resources); + anm2_deserialize(self->anm2, self->resources, self->dialog->path); + window_title_from_path_set(self->window, self->dialog->path); + snapshots_reset(self->snapshots); + imgui_message_queue_push(self, std::format(IMGUI_MESSAGE_FILE_OPEN_FORMAT, self->dialog->path)); + break; + case DIALOG_ANM2_SAVE: + anm2_serialize(self->anm2, self->dialog->path); + window_title_from_path_set(self->window, self->dialog->path); + imgui_message_queue_push(self, std::format(IMGUI_MESSAGE_FILE_SAVE_FORMAT, self->dialog->path)); + break; + default: + break; + } + + dialog_reset(self->dialog); + } } static void _imgui_tools(Imgui* self) @@ -1345,8 +1588,21 @@ static void _imgui_animations(Imgui* self) } ImGui::SameLine(); - _imgui_item_button(self, IMGUI_ANIMATIONS_MERGE); + ImGui::SameLine(); + + if (_imgui_item_button(self, IMGUI_ANIMATION_REMOVE) && animation) + { + anm2_animation_remove(self->anm2, self->reference->animationID); + anm2_reference_clear(self->reference); + } + + ImGui::SameLine(); + + if (_imgui_item_button(self, IMGUI_ANIMATION_DEFAULT) && animation) + self->anm2->defaultAnimationID = self->reference->animationID; + + _imgui_pending_popup_process(self); if (ImGui::BeginPopupModal(IMGUI_ANIMATIONS_MERGE.popup.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { @@ -1462,18 +1718,9 @@ static void _imgui_animations(Imgui* self) ImGui::EndPopup(); } - ImGui::SameLine(); - - if (_imgui_item_button(self, IMGUI_ANIMATION_REMOVE) && animation) - { - anm2_animation_remove(self->anm2, self->reference->animationID); - anm2_reference_clear(self->reference); - } - - ImGui::SameLine(); - - if (_imgui_item_button(self, IMGUI_ANIMATION_DEFAULT) && animation) - self->anm2->defaultAnimationID = self->reference->animationID; + + + _imgui_item_end_child(); // IMGUI_ANIMATIONS_OPTIONS_CHILD) _imgui_item_end(); @@ -1598,7 +1845,7 @@ static void _imgui_spritesheets(Imgui* self) spritesheetPreviewSize.y = IMGUI_SPRITESHEET_PREVIEW_SIZE.x / spritesheetAspect; if (texture->isInvalid) - _imgui_atlas_image(self, TEXTURE_NONE); + _imgui_atlas_image(self, ATLAS_NONE); else ImGui::Image(texture->id, spritesheetPreviewSize); @@ -1614,8 +1861,9 @@ static void _imgui_spritesheets(Imgui* self) _imgui_item_begin_child(IMGUI_SPRITESHEETS_OPTIONS_CHILD); - _imgui_item_button(self, IMGUI_SPRITESHEETS_ADD); - + if (_imgui_item_button(self, IMGUI_SPRITESHEETS_ADD)) + dialog_spritesheet_add(self->dialog); + ImGui::SameLine(); if (_imgui_item_button(self, IMGUI_SPRITESHEETS_RELOAD)) @@ -1634,13 +1882,30 @@ static void _imgui_spritesheets(Imgui* self) ImGui::SameLine(); - if (_imgui_item_button(self, IMGUI_SPRITESHEETS_REPLACE)) + if (_imgui_item_button(self, IMGUI_SPRITESHEETS_REPLACE) && highlightedID != ID_NONE) + dialog_spritesheet_replace(self->dialog, highlightedID); + + if (self->dialog->isSelected) { - if (highlightedID != ID_NONE) + switch (self->dialog->type) { - self->dialog->replaceID = highlightedID; - dialog_png_replace(self->dialog); + case DIALOG_SPRITESHEET_ADD: + { + s32 id = map_next_id_get(self->resources->textures); + self->anm2->spritesheets[id] = Anm2Spritesheet{}; + self->anm2->spritesheets[id].path = self->dialog->path; + resources_texture_init(self->resources, self->dialog->path, id); + break; + } + case DIALOG_SPRITESHEET_REPLACE: + self->anm2->spritesheets[self->dialog->replaceID].path = self->dialog->path; + resources_texture_init(self->resources, self->dialog->path, self->dialog->replaceID); + break; + default: + break; } + + dialog_reset(self->dialog); } ImGui::SameLine(); @@ -1688,7 +1953,7 @@ static void _imgui_spritesheets(Imgui* self) Anm2Spritesheet* spritesheet = &self->anm2->spritesheets[id]; Texture* texture = &self->resources->textures[id]; texture_from_gl_write(texture, spritesheet->path); - imgui_message_queue_push(self, std::format(IMGUI_ACTION_SPRITESHEET_SAVE_FORMAT, id, spritesheet->path)); + imgui_message_queue_push(self, std::format(IMGUI_MESSAGE_SPRITESHEET_SAVE_FORMAT, id, spritesheet->path)); std::filesystem::current_path(workingPath); } } @@ -1736,29 +2001,25 @@ static void _imgui_animation_preview(Imgui* self) _imgui_item_begin_child(IMGUI_ANIMATION_PREVIEW_BACKGROUND_SETTINGS); _imgui_item_coloredit4(self, IMGUI_ANIMATION_PREVIEW_BACKGROUND_COLOR, self->settings->previewBackgroundColor); - std::vector animationStrings; - std::vector animationLabels; std::vector animationIDs; + ImguiItem animationOverlayItem = IMGUI_ANIMATION_PREVIEW_OVERLAY; + s32 animationOverlayCount = self->anm2->animations.size() + 1; - animationStrings.reserve(self->anm2->animations.size() + 1); - animationIDs.reserve(self->anm2->animations.size() + 1); - animationLabels.reserve(self->anm2->animations.size() + 1); - + animationIDs.reserve(animationOverlayCount); + animationOverlayItem.items.reserve(animationOverlayCount); animationIDs.push_back(ID_NONE); - animationStrings.push_back(IMGUI_EVENT_NONE); - animationLabels.push_back(animationStrings.back().c_str()); - + animationOverlayItem.items.push_back(IMGUI_ANIMATION_NONE); + for (auto & [id, animation] : self->anm2->animations) { animationIDs.push_back(id); - animationStrings.push_back(animation.name); - animationLabels.push_back(animationStrings.back().c_str()); + animationOverlayItem.items.push_back(animation.name); } - s32 selectedAnimationID = std::find(animationIDs.begin(), animationIDs.end(), self->preview->animationOverlayID) - animationIDs.begin(); + s32 animationIndex = std::find(animationIDs.begin(), animationIDs.end(), self->preview->animationOverlayID) - animationIDs.begin(); - if (_imgui_item_combo(self, IMGUI_ANIMATION_PREVIEW_OVERLAY, &selectedAnimationID, animationLabels.data(), (s32)animationLabels.size())) - self->preview->animationOverlayID = animationIDs[selectedAnimationID]; + if (_imgui_item_combo(self, animationOverlayItem, &animationIndex)) + self->preview->animationOverlayID = animationIDs[animationIndex]; _imgui_item_dragfloat(self, IMGUI_ANIMATION_PREVIEW_OVERLAY_TRANSPARENCY, self->settings->previewOverlayTransparency); _imgui_item_end_child(); @@ -1770,7 +2031,9 @@ static void _imgui_animation_preview(Imgui* self) ImGui::SameLine(); _imgui_item_coloredit4(self, IMGUI_ANIMATION_PREVIEW_AXIS_COLOR, self->settings->previewAxisColor); _imgui_item_checkbox(self, IMGUI_ANIMATION_PREVIEW_ROOT_TRANSFORM, self->settings->previewIsRootTransform); - _imgui_item_checkbox(self, IMGUI_ANIMATION_PREVIEW_SHOW_PIVOT, self->settings->previewIsShowPivot); + _imgui_item_checkbox(self, IMGUI_ANIMATION_PREVIEW_PIVOTS, self->settings->previewIsPivots); + ImGui::SameLine(); + _imgui_item_checkbox(self, IMGUI_ANIMATION_PREVIEW_TARGETS, self->settings->previewIsTargets); ImGui::SameLine(); _imgui_item_checkbox(self, IMGUI_ANIMATION_PREVIEW_BORDER, self->settings->previewIsBorder); _imgui_item_end_child(); @@ -1995,6 +2258,8 @@ static void _imgui_spritesheet_editor(Imgui* self) } _imgui_item_end(); + + } static void _imgui_frame_properties(Imgui* self) @@ -2045,29 +2310,25 @@ static void _imgui_frame_properties(Imgui* self) } else if (type == ANM2_TRIGGERS) { - std::vector eventStrings; - std::vector eventLabels; std::vector eventIDs; + ImguiItem framePropertiesEventItem = IMGUI_FRAME_PROPERTIES_EVENT; + s32 eventComboCount = self->anm2->events.size() + 1; - eventStrings.reserve(self->anm2->events.size() + 1); - eventIDs.reserve(self->anm2->events.size() + 1); - eventLabels.reserve(self->anm2->events.size() + 1); - + framePropertiesEventItem.items.reserve(eventComboCount); + eventIDs.reserve(eventComboCount); + framePropertiesEventItem.items.push_back(IMGUI_EVENT_NONE); eventIDs.push_back(ID_NONE); - eventStrings.push_back(IMGUI_EVENT_NONE); - eventLabels.push_back(eventStrings.back().c_str()); for (auto & [id, event] : self->anm2->events) { eventIDs.push_back(id); - eventStrings.push_back(event.name); - eventLabels.push_back(eventStrings.back().c_str()); + framePropertiesEventItem.items.push_back(event.name); } - s32 selectedEventIndex = std::find(eventIDs.begin(), eventIDs.end(), frame->eventID) - eventIDs.begin(); - - if (_imgui_item_combo(self, IMGUI_FRAME_PROPERTIES_EVENT, &selectedEventIndex, eventLabels.data(), (s32)eventLabels.size())) - frame->eventID = eventIDs[selectedEventIndex]; + s32 eventIndex = std::find(eventIDs.begin(), eventIDs.end(), frame->eventID) - eventIDs.begin(); + + if (_imgui_item_combo(self, framePropertiesEventItem, &eventIndex)) + frame->eventID = eventIDs[eventIndex]; _imgui_item_inputint(self, IMGUI_FRAME_PROPERTIES_AT_FRAME, frame->atFrame); frame->atFrame = std::clamp(frame->atFrame, 0, animation->frameNum - 1); @@ -2109,7 +2370,7 @@ static void _imgui_messages(Imgui* self) self->messageQueue.erase(self->messageQueue.begin() + i); continue; } - + ImGui::SetNextWindowPos(position, ImGuiCond_Always, {1.0f, 1.0f}); ImGui::PushStyleColor(ImGuiCol_Border, borderColor); ImGui::PushStyleColor(ImGuiCol_Text, textColor); @@ -2124,36 +2385,11 @@ static void _imgui_messages(Imgui* self) position.y -= windowSize.y + IMGUI_MESSAGE_PADDING; } - - if (self->dialog->isJustSelected) - { - switch (self->dialog->lastType) - { - case DIALOG_ANM2_OPEN: - imgui_message_queue_push(self, std::format(IMGUI_ACTION_FILE_OPEN_FORMAT, self->dialog->lastPath)); - break; - case DIALOG_ANM2_SAVE: - imgui_message_queue_push(self, std::format(IMGUI_ACTION_FILE_SAVE_FORMAT, self->dialog->lastPath)); - break; - default: - break; - } - - dialog_reset(self->dialog); - } } static void _imgui_persistent(Imgui* self) { - if (self->preview->isRecording) - { - ImVec2 mousePos = ImGui::GetMousePos(); - - ImGui::SetNextWindowPos(ImVec2(mousePos.x + IMGUI_TOOLTIP_OFFSET.x, mousePos.y + IMGUI_TOOLTIP_OFFSET.y)); - ImGui::BeginTooltip(); - _imgui_item_atlas_image_text(self, IMGUI_RECORDING); - ImGui::EndTooltip(); - } + if (!self->isContextualActionsEnabled) return; if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup(IMGUI_CONTEXT_MENU.popup.c_str()); @@ -2251,9 +2487,7 @@ void imgui_update(Imgui* self) _imgui_messages(self); _imgui_persistent(self); - self->isHotkeysEnabled = !self->isRename && !self->isChangeValue; - - if (self->isHotkeysEnabled) + if (self->isContextualActionsEnabled) { for (const auto& hotkey : imgui_hotkey_registry()) { @@ -2286,7 +2520,7 @@ void imgui_update(Imgui* self) } } - if (_imgui_item_yes_no_popup(self, IMGUI_EXIT_CONFIRMATION)) + if (_imgui_item_option_popup(self, IMGUI_EXIT_CONFIRMATION)) self->isQuit = true; } diff --git a/src/imgui.h b/src/imgui.h index b5d200d..332b073 100644 --- a/src/imgui.h +++ b/src/imgui.h @@ -10,6 +10,8 @@ #include "tool.h" #include "window.h" +#include "ffmpeg.h" + #define IMGUI_IMPL_OPENGL_LOADER_CUSTOM #define IMGUI_ENABLE_DOCKING #define IM_VEC2_CLASS_EXTRA \ @@ -35,56 +37,69 @@ #include #include -#define IMGUI_ANIMATION_DEFAULT_FORMAT "(*) {}" -#define IMGUI_CHORD_NONE (ImGuiMod_None) -#define IMGUI_EVENT_NONE "None" -#define IMGUI_FRAME_BORDER 2.0f -#define IMGUI_FRAME_PROPERTIES_NO_FRAME "Select a frame to show properties..." -#define IMGUI_ITEM_SELECTABLE_EDITABLE_LABEL "## Editing" -#define IMGUI_OPENGL_VERSION "#version 330" -#define IMGUI_PICKER_LINE_COLOR IM_COL32(255, 255, 255, 255) -#define IMGUI_POSITION_FORMAT "Position: {{{:6}, {:6}}}" -#define IMGUI_SPRITESHEET_FORMAT "#{} {}" -#define IMGUI_TIMELINE_CHILD_ID_LABEL "#{} {}" -#define IMGUI_TIMELINE_FOOTER_HEIGHT 40 #define IMGUI_ANIMATIONS_FOOTER_HEIGHT 40 -#define IMGUI_EVENTS_FOOTER_HEIGHT 40 -#define IMGUI_SPRITESHEETS_FOOTER_HEIGHT 65 -#define IMGUI_TIMELINE_FRAME_BORDER 2 -#define IMGUI_TIMELINE_FRAME_LABEL_FORMAT "## {}" -#define IMGUI_TIMELINE_FRAME_MULTIPLE 5 -#define IMGUI_TIMELINE_MERGE -#define IMGUI_TIMELINE_NO_ANIMATION "Select an animation to show timeline..." -#define IMGUI_TIMELINE_PICKER_LINE_WIDTH 2.0f -#define IMGUI_TIMELINE_SPRITESHEET_ID_FORMAT "#{}" - -#define IMGUI_INVISIBLE_LABEL_MARKER "##" #define IMGUI_ANIMATIONS_OPTIONS_ROW_ITEM_COUNT 5 +#define IMGUI_CHORD_NONE (ImGuiMod_None) +#define IMGUI_EVENTS_FOOTER_HEIGHT 40 #define IMGUI_EVENTS_OPTIONS_ROW_ITEM_COUNT 2 -#define IMGUI_SPRITESHEETS_OPTIONS_FIRST_ROW_ITEM_COUNT 4 -#define IMGUI_SPRITESHEETS_OPTIONS_SECOND_ROW_ITEM_COUNT 3 -#define IMGUI_TIMELINE_FOOTER_ITEM_CHILD_ITEM_COUNT 2 -#define IMGUI_TIMELINE_BAKE_OPTIONS_CHILD_ROW_ITEM_COUNT 2 -#define IMGUI_TIMELINE_MERGE_OPTIONS_ROW_ITEM_COUNT 2 +#define IMGUI_FRAME_BORDER 2.0f #define IMGUI_MESSAGE_DURATION 3.0f #define IMGUI_MESSAGE_PADDING 10.0f -#define IMGUI_MESSAGE_FORMAT "## Message {}" -#define IMGUI_MESSAGE_UNDO_FORMAT "Undo: {}" -#define IMGUI_MESSAGE_REDO_FORMAT "Redo: {}" +#define IMGUI_PICKER_LINE_COLOR IM_COL32(255, 255, 255, 255) +#define IMGUI_POPUP_OPTION_CHILD_ROW_ITEM_COUNT 2 +#define IMGUI_RENDER_ANIMATION_OPTIONS_ROW_ITEM_COUNT 2 +#define IMGUI_SPRITESHEETS_FOOTER_HEIGHT 65 +#define IMGUI_SPRITESHEETS_OPTIONS_FIRST_ROW_ITEM_COUNT 4 +#define IMGUI_SPRITESHEETS_OPTIONS_SECOND_ROW_ITEM_COUNT 3 +#define IMGUI_TIMELINE_BAKE_OPTIONS_CHILD_ROW_ITEM_COUNT 2 +#define IMGUI_TIMELINE_FOOTER_HEIGHT 40 +#define IMGUI_TIMELINE_FOOTER_ITEM_CHILD_ITEM_COUNT 2 +#define IMGUI_TIMELINE_FRAME_BORDER 2 +#define IMGUI_TIMELINE_FRAME_MULTIPLE 5 +#define IMGUI_TIMELINE_MERGE +#define IMGUI_TIMELINE_MERGE_OPTIONS_ROW_ITEM_COUNT 2 +#define IMGUI_TIMELINE_PICKER_LINE_WIDTH 2.0f -#define IMGUI_ACTION_FILE_SAVE_FORMAT "Saved anm2 to: {}" -#define IMGUI_ACTION_FILE_OPEN_FORMAT "Opened anm2: {}" -#define IMGUI_ACTION_SPRITESHEET_SAVE_FORMAT "Saved spritesheet #{} to: {}" -#define IMGUI_ACTION_TRIGGER_MOVE "Trigger AtFrame" -#define IMGUI_ACTION_FRAME_SWAP "Frame Swap" -#define IMGUI_ACTION_ANIMATION_SWAP "Animation Swap" -#define IMGUI_ACTION_FRAME_TRANSFORM "Frame Transform" #define IMGUI_ACTION_FRAME_CROP "Frame Crop" +#define IMGUI_ACTION_FRAME_SWAP "Frame Swap" +#define IMGUI_ACTION_FRAME_TRANSFORM "Frame Transform" +#define IMGUI_ACTION_ANIMATION_SWAP "Animation Swap" +#define IMGUI_ACTION_TRIGGER_MOVE "Trigger AtFrame" + +#define IMGUI_MESSAGE_FILE_OPEN_FORMAT "Opened anm2: {}" +#define IMGUI_MESSAGE_FILE_SAVE_FORMAT "Saved anm2 to: {}" +#define IMGUI_MESSAGE_RENDER_ANIMATION_FRAMES_SAVE_FORMAT "Saved rendered frames to: {}" +#define IMGUI_MESSAGE_RENDER_ANIMATION_SAVE_FORMAT "Saved rendered animation to: {}" +#define IMGUI_MESSAGE_RENDER_ANIMATION_NO_SELECTED_ANIMATION_ERROR "Select an animation first to render!" +#define IMGUI_MESSAGE_RENDER_ANIMATION_NO_ANIMATION_ERROR "No animation selected; rendering cancelled." +#define IMGUI_MESSAGE_RENDER_ANIMATION_NO_FRAMES_ERROR "No frames to render; rendering cancelled." +#define IMGUI_MESSAGE_RENDER_ANIMATION_DIRECTORY_ERROR "Invalid directory! Make sure it's valid and you have write permissions." +#define IMGUI_MESSAGE_RENDER_ANIMATION_PATH_ERROR "Invalid path! Make sure it's valid and you have write permissions." +#define IMGUI_MESSAGE_RENDER_ANIMATION_FFMPEG_PATH_ERROR "Invalid FFmpeg path! Make sure you have it installed and the path is correct." +#define IMGUI_MESSAGE_RENDER_ANIMATION_FFMPEG_ERROR "FFmpeg could not render animation! Check paths or your FFmpeg installation." +#define IMGUI_MESSAGE_SPRITESHEET_SAVE_FORMAT "Saved spritesheet #{} to: {}" +#define IMGUI_ANIMATION_DEFAULT_FORMAT "(*) {}" +#define IMGUI_ANIMATION_NONE "None" +#define IMGUI_BUFFER_MAX 255 +#define IMGUI_EVENT_NONE "None" +#define IMGUI_FRAME_PROPERTIES_NO_FRAME "Select a frame to show properties..." +#define IMGUI_INVISIBLE_LABEL_MARKER "##" +#define IMGUI_ITEM_SELECTABLE_EDITABLE_LABEL "## Editing" +#define IMGUI_MESSAGE_FORMAT "## Message {}" +#define IMGUI_MESSAGE_REDO_FORMAT "Redo: {}" +#define IMGUI_MESSAGE_UNDO_FORMAT "Undo: {}" +#define IMGUI_OPENGL_VERSION "#version 330" +#define IMGUI_POSITION_FORMAT "Position: ({:8}, {:8})" +#define IMGUI_SPRITESHEET_FORMAT "#{} {}" +#define IMGUI_TIMELINE_CHILD_ID_LABEL "#{} {}" +#define IMGUI_TIMELINE_FRAME_LABEL_FORMAT "## {}" +#define IMGUI_TIMELINE_NO_ANIMATION "Select an animation to show timeline..." +#define IMGUI_TIMELINE_SPRITESHEET_ID_FORMAT "#{}" #define IMGUI_SPACING 4 const ImVec2 IMGUI_TIMELINE_FRAME_SIZE = {16, 40}; -const ImVec2 IMGUI_TIMELINE_FRAME_CONTENT_OFFSET = {TEXTURE_SIZE_SMALL.x * 0.25f, (IMGUI_TIMELINE_FRAME_SIZE.y * 0.5f) - (TEXTURE_SIZE_SMALL.y * 0.5f)}; +const ImVec2 IMGUI_TIMELINE_FRAME_CONTENT_OFFSET = {ATLAS_SIZE_SMALL.x * 0.25f, (IMGUI_TIMELINE_FRAME_SIZE.y * 0.5f) - (ATLAS_SIZE_SMALL.y * 0.5f)}; const ImVec2 IMGUI_TIMELINE_ITEM_SELECTABLE_SIZE = {150, 0}; const ImVec2 IMGUI_TIMELINE_ITEM_SIZE = {300, 40}; @@ -102,7 +117,7 @@ const ImVec2 IMGUI_FRAME_PROPERTIES_FLIP_BUTTON_SIZE = {100, 0}; const ImVec2 IMGUI_SPRITESHEET_PREVIEW_SIZE = {125.0, 125.0}; const ImVec2 IMGUI_TOOLTIP_OFFSET = {16, 8}; const vec2 IMGUI_SPRITESHEET_EDITOR_CROP_FORGIVENESS = {1, 1}; -const ImVec2 IMGUI_CANVAS_CHILD_SIZE = {200, 85}; +const ImVec2 IMGUI_CANVAS_CHILD_SIZE = {230, 85}; const ImGuiKey IMGUI_INPUT_DELETE = ImGuiKey_Delete; const ImGuiKey IMGUI_INPUT_LEFT = ImGuiKey_LeftArrow; @@ -136,6 +151,13 @@ struct ImguiMessage f32 timeRemaining; }; +enum ImguiPopupType +{ + IMGUI_POPUP_NONE, + IMGUI_POPUP_BY_ITEM, + IMGUI_POPUP_CENTER_SCREEN +}; + struct Imgui { Dialog* dialog = nullptr; @@ -149,8 +171,11 @@ struct Imgui Clipboard* clipboard = nullptr; SDL_Window* window = nullptr; SDL_GLContext* glContext = nullptr; + std::string pendingPopup{}; + ImguiPopupType pendingPopupType = IMGUI_POPUP_NONE; + ImVec2 pendingPopupPosition{}; std::vector messageQueue; - bool isHotkeysEnabled = true; + bool isContextualActionsEnabled = true; bool isRename = false; bool isChangeValue = false; bool isQuit = false; @@ -173,12 +198,6 @@ static std::vector& imgui_hotkey_registry() return registry; } -enum PopupType -{ - IMGUI_POPUP_BY_ITEM, - IMGUI_POPUP_CENTER_SCREEN -}; - struct ImguiColorSet { ImVec4 normal{}; @@ -199,20 +218,21 @@ struct ImguiItemBuilder std::string dragDrop{}; std::string format = "%.1f"; std::string focusWindow{}; + std::vector items{}; ImguiFunction function = nullptr; ImGuiKeyChord chord = IMGUI_CHORD_NONE; - TextureType texture = TEXTURE_NONE; - PopupType popupType = IMGUI_POPUP_BY_ITEM; + AtlasType atlas = ATLAS_NONE; + ImguiPopupType popupType = IMGUI_POPUP_BY_ITEM; bool isUndoable = false; bool isSizeToText = true; bool isSizeToChild = false; ImguiColorSet color{}; ImVec2 size{}; ImVec2 contentOffset{}; - s32 childRowItemCount{}; + s32 childRowItemCount = 1; f64 speed{}; f64 min{}; - f64 max{}; + f64 max = IMGUI_BUFFER_MAX; s32 border{}; s32 step = 1; s32 stepFast = 1; @@ -231,10 +251,11 @@ struct ImguiItem std::string dragDrop{}; std::string format = "%.1f"; std::string focusWindow{}; + std::vector items{}; ImguiFunction function = nullptr; ImGuiKeyChord chord = IMGUI_CHORD_NONE; - TextureType texture = TEXTURE_NONE; - PopupType popupType = IMGUI_POPUP_BY_ITEM; + AtlasType atlas = ATLAS_NONE; + ImguiPopupType popupType = IMGUI_POPUP_BY_ITEM; bool isUndoable = false; bool isInactive = false; bool isSizeToText = true; @@ -242,7 +263,7 @@ struct ImguiItem bool isSelected = false; f64 speed{}; f64 min{}; - f64 max{}; + f64 max = IMGUI_BUFFER_MAX; s32 border{}; s32 childRowItemCount = 1; s32 step = 1; @@ -276,10 +297,11 @@ struct ImguiItem dragDrop = builder.dragDrop; format = builder.format; focusWindow = builder.focusWindow; + items = builder.items; function = builder.function; chord = builder.chord; popupType = builder.popupType; - texture = builder.texture; + atlas = builder.atlas; isUndoable = builder.isUndoable; isSizeToText = builder.isSizeToText; isSizeToChild = builder.isSizeToChild; @@ -363,7 +385,7 @@ static inline void imgui_file_save(Imgui* self) else { anm2_serialize(self->anm2, self->anm2->path); - imgui_message_queue_push(self, std::format(IMGUI_ACTION_FILE_SAVE_FORMAT, self->anm2->path)); + imgui_message_queue_push(self, std::format(IMGUI_MESSAGE_FILE_SAVE_FORMAT, self->anm2->path)); } } @@ -372,20 +394,6 @@ static inline void imgui_file_save_as(Imgui* self) dialog_anm2_save(self->dialog); } -static inline void imgui_png_open(Imgui* self) -{ - dialog_png_open(self->dialog); -} - -static inline void imgui_generate_gif_animation(Imgui* self) -{ - if (anm2_animation_from_reference(self->anm2, self->reference)) - { - self->preview->isRecording = true; - self->preview->time = 0; - } -} - static inline void imgui_undo_stack_push(Imgui* self, const std::string& action = SNAPSHOT_ACTION) { Snapshot snapshot = {*self->anm2, *self->reference, self->preview->time, action}; @@ -549,11 +557,90 @@ const inline ImguiItem IMGUI_TASKBAR_WIZARD_GENERATE_ANIMATION_FROM_GRID = Imgui .popupType = IMGUI_POPUP_CENTER_SCREEN }); -const inline ImguiItem IMGUI_TASKBAR_WIZARD_RECORD_GIF_ANIMATION = ImguiItem +const inline ImguiItem IMGUI_TASKBAR_WIZARD_RENDER_ANIMATION = ImguiItem ({ - .label = "G&enerate GIF Animation", - .tooltip = "Generates a GIF animation from the current animation.", - .function = imgui_generate_gif_animation + .label = "&Render Animation", + .tooltip = "Renders the current animation preview; output options can be customized.", + .popup = "Render Animation", + .popupType = IMGUI_POPUP_CENTER_SCREEN +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_CHILD = ImguiItem +({ + .label = "## Render Animation Child", + .size = {600, 125} +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_LOCATION = ImguiItem +({ + .label = "Location", + .tooltip = "Set the rendered animation's output location.", + .isSizeToChild = true +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_BROWSE = ImguiItem +({ + .label = "## Location Browse", + .atlas = ATLAS_FOLDER +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_OUTPUT = ImguiItem +({ + .label = "Output", + .tooltip = "Select the rendered animation output.\nIt can either be one animated image or a sequence of frames.", + .items = {std::begin(RENDER_TYPE_STRINGS), std::end(RENDER_TYPE_STRINGS)}, + .isSizeToChild = true +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_FORMAT = ImguiItem +({ + .label = "Format", + .tooltip = "(PNG images only).\nSet the format of each output frame; i.e., its filename.\nThe format will only take one argument; that being the frame's index.\nFor example, a format like \"{}.png\" will export a frame of index 0 as \"0.png\".", + .isSizeToChild = true +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_FFMPEG_PATH = ImguiItem +({ + .label = "FFmpeg Path", + .tooltip = "Sets the path FFmpeg currently resides in.\nFFmpeg is required for rendering animations.\nDownload it from https://ffmpeg.org/, your package manager, or wherever else.", + .isSizeToChild = true +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_FFMPEG_BROWSE = ImguiItem +({ + .label = "## FFMpeg Browse", + .atlas = ATLAS_FOLDER +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_CONFIRM = ImguiItem +({ + .label = "Render", + .tooltip = "Render the animation with the chosen options.", + .popup = "Rendering Animation...", + .popupType = IMGUI_POPUP_CENTER_SCREEN, + .isSizeToChild = true, + .childRowItemCount = IMGUI_RENDER_ANIMATION_OPTIONS_ROW_ITEM_COUNT +}); + +const inline ImguiItem IMGUI_RENDER_ANIMATION_CANCEL = ImguiItem +({ + .label = "Cancel", + .tooltip = "Cancel rendering animation.", + .isSizeToChild = true, + .childRowItemCount = IMGUI_RENDER_ANIMATION_OPTIONS_ROW_ITEM_COUNT +}); + +const inline ImguiItem IMGUI_RENDERING_ANIMATION_CHILD = ImguiItem +({ + .label = "## Render Animation Child", + .size = {300, 60}, + .flags = true +}); + +const inline ImguiItem IMGUI_RENDERING_ANIMATION_CANCEL = ImguiItem +({ + .label = "Cancel", + .isSizeToChild = true }); const inline ImguiItem IMGUI_TASKBAR_PLAYBACK = ImguiItem @@ -583,7 +670,7 @@ const inline ImguiItem IMGUI_ANIMATION = ImguiItem .label = "## Animation Item", .action = "Select Animation", .dragDrop = "## Animation Drag Drop", - .texture = TEXTURE_ANIMATION, + .atlas = ATLAS_ANIMATION, .isUndoable = true, .isSizeToText = false }); @@ -731,7 +818,7 @@ const inline ImguiItem IMGUI_EVENTS_CHILD = ImguiItem const inline ImguiItem IMGUI_EVENT = ImguiItem ({ .label = "## Event Item", - .texture = TEXTURE_EVENT, + .atlas = ATLAS_EVENT, .isUndoable = true, .isSizeToText = false }); @@ -781,7 +868,7 @@ const inline ImguiItem IMGUI_SPRITESHEET = ImguiItem ({ .label = "## Spritesheet", .dragDrop = "## Spritesheet Drag Drop", - .texture = TEXTURE_SPRITESHEET, + .atlas = ATLAS_SPRITESHEET, .flags = true }); @@ -795,7 +882,6 @@ const inline ImguiItem IMGUI_SPRITESHEETS_ADD = ImguiItem ({ .label = "Add", .tooltip = "Select an image to add as a spritesheet.", - .function = imgui_png_open, .isSizeToChild = true, .childRowItemCount = IMGUI_SPRITESHEETS_OPTIONS_FIRST_ROW_ITEM_COUNT }); @@ -967,10 +1053,16 @@ const inline ImguiItem IMGUI_ANIMATION_PREVIEW_ROOT_TRANSFORM = ImguiItem .tooltip = "Toggles the root frames's attributes transforming the other items in an animation." }); -const inline ImguiItem IMGUI_ANIMATION_PREVIEW_SHOW_PIVOT = ImguiItem +const inline ImguiItem IMGUI_ANIMATION_PREVIEW_PIVOTS = ImguiItem ({ - .label = "Show Pivot", - .tooltip = "Toggles the appearance of an icon for each animation item's pivot." + .label = "Pivots", + .tooltip = "Toggles drawing each layer's pivot." +}); + +const inline ImguiItem IMGUI_ANIMATION_PREVIEW_TARGETS = ImguiItem +({ + .label = "Targets", + .tooltip = "Toggles drawing the targets (i.e., the colored root/null icons)." }); const inline ImguiItem IMGUI_ANIMATION_PREVIEW_BORDER = ImguiItem @@ -1078,6 +1170,8 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_POSITION = ImguiItem .format = "%.0f", .isUndoable = true, .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_CROP = ImguiItem @@ -1088,6 +1182,8 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_CROP = ImguiItem .format = "%.0f", .isUndoable = true, .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_SIZE = ImguiItem @@ -1097,7 +1193,9 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_SIZE = ImguiItem .action = "Frame Size", .format = "%.0f", .isUndoable = true, - .speed = 0.25f + .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_PIVOT = ImguiItem @@ -1107,7 +1205,9 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_PIVOT = ImguiItem .action = "Frame Pivot", .format = "%.0f", .isUndoable = true, - .speed = 0.25f + .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_SCALE = ImguiItem @@ -1116,7 +1216,9 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_SCALE = ImguiItem .tooltip = "Change the scale of the selected frame.", .action = "Frame Scale", .isUndoable = true, - .speed = 0.25f + .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_ROTATION = ImguiItem @@ -1125,7 +1227,9 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_ROTATION = ImguiItem .tooltip = "Change the rotation of the selected frame.", .action = "Frame Rotation", .isUndoable = true, - .speed = 0.25f + .speed = 0.25f, + .min = 0, + .max = 0 }); const inline ImguiItem IMGUI_FRAME_PROPERTIES_DURATION = ImguiItem @@ -1191,7 +1295,7 @@ const inline ImguiItem IMGUI_FRAME_PROPERTIES_FLIP_Y = ImguiItem const inline ImguiItem IMGUI_FRAME_PROPERTIES_EVENT = ImguiItem ({ .label = "Event", - .tooltip = "Change the event the trigger uses.\nNOTE: This sets the event ID, not the event. If the events change IDs, then this will need to be changed.", + .tooltip = "Change the event the trigger uses.\n", .action = "Trigger Event", .isUndoable = true }); @@ -1212,7 +1316,7 @@ const inline ImguiItem IMGUI_TOOL_PAN = ImguiItem .tooltip = "Use the pan tool.\nWill shift the view as the cursor is dragged.\nYou can also use the middle mouse button to pan at any time.", .function = imgui_tool_pan_set, .chord = ImGuiKey_P, - .texture = TEXTURE_PAN, + .atlas = ATLAS_PAN, }); const inline ImguiItem IMGUI_TOOL_MOVE = ImguiItem @@ -1221,7 +1325,7 @@ const inline ImguiItem IMGUI_TOOL_MOVE = ImguiItem .tooltip = "Use the move tool.\nWill move the selected item as the cursor is dragged, or directional keys are pressed.\n(Animation Preview only.)", .function = imgui_tool_move_set, .chord = ImGuiKey_M, - .texture = TEXTURE_MOVE, + .atlas = ATLAS_MOVE, }); const inline ImguiItem IMGUI_TOOL_ROTATE = ImguiItem @@ -1230,7 +1334,7 @@ const inline ImguiItem IMGUI_TOOL_ROTATE = ImguiItem .tooltip = "Use the rotate tool.\nWill rotate the selected item as the cursor is dragged, or directional keys are pressed.\n(Animation Preview only.)", .function = imgui_tool_rotate_set, .chord = ImGuiKey_R, - .texture = TEXTURE_ROTATE, + .atlas = ATLAS_ROTATE, }); const inline ImguiItem IMGUI_TOOL_SCALE = ImguiItem @@ -1239,7 +1343,7 @@ const inline ImguiItem IMGUI_TOOL_SCALE = ImguiItem .tooltip = "Use the scale tool.\nWill scale the selected item as the cursor is dragged, or directional keys are pressed.\n(Animation Preview only.)", .function = imgui_tool_scale_set, .chord = ImGuiKey_S, - .texture = TEXTURE_SCALE, + .atlas = ATLAS_SCALE, }); const inline ImguiItem IMGUI_TOOL_CROP = ImguiItem @@ -1248,7 +1352,7 @@ const inline ImguiItem IMGUI_TOOL_CROP = ImguiItem .tooltip = "Use the crop tool.\nWill produce a crop rectangle based on how the cursor is dragged.\n(Spritesheet Editor only.)", .function = imgui_tool_crop_set, .chord = ImGuiKey_C, - .texture = TEXTURE_CROP, + .atlas = ATLAS_CROP, }); const inline ImguiItem IMGUI_TOOL_DRAW = ImguiItem @@ -1257,7 +1361,7 @@ const inline ImguiItem IMGUI_TOOL_DRAW = ImguiItem .tooltip = "Draws pixels onto the selected spritesheet, with the current color.\n(Spritesheet Editor only.)", .function = imgui_tool_draw_set, .chord = ImGuiKey_B, - .texture = TEXTURE_DRAW, + .atlas = ATLAS_DRAW, }); const inline ImguiItem IMGUI_TOOL_ERASE = ImguiItem @@ -1266,7 +1370,7 @@ const inline ImguiItem IMGUI_TOOL_ERASE = ImguiItem .tooltip = "Erases pixels from the selected spritesheet.\n(Spritesheet Editor only.)", .function = imgui_tool_erase_set, .chord = ImGuiKey_E, - .texture = TEXTURE_ERASE, + .atlas = ATLAS_ERASE, }); const inline ImguiItem IMGUI_TOOL_COLOR_PICKER = ImguiItem @@ -1275,7 +1379,7 @@ const inline ImguiItem IMGUI_TOOL_COLOR_PICKER = ImguiItem .tooltip = "Selects a color from anywhere on the screen, to be used for drawing.", .function = imgui_tool_color_picker_set, .chord = ImGuiKey_W, - .texture = TEXTURE_COLOR_PICKER, + .atlas = ATLAS_COLOR_PICKER, }); const inline ImguiItem IMGUI_TOOL_UNDO = ImguiItem @@ -1284,7 +1388,7 @@ const inline ImguiItem IMGUI_TOOL_UNDO = ImguiItem .tooltip = "Undoes the last action.", .function = imgui_undo, .chord = ImGuiKey_Z, - .texture = TEXTURE_UNDO + .atlas = ATLAS_UNDO }); const inline ImguiItem IMGUI_TOOL_REDO = ImguiItem @@ -1293,7 +1397,7 @@ const inline ImguiItem IMGUI_TOOL_REDO = ImguiItem .tooltip = "Redoes the last action.", .function = imgui_redo, .chord = ImGuiMod_Shift + ImGuiKey_Z, - .texture = TEXTURE_REDO + .atlas = ATLAS_REDO }); const inline ImguiItem IMGUI_TOOL_COLOR = ImguiItem @@ -1423,7 +1527,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_ROOT_SELECTABLE = ImguiItem .label = "Root", .tooltip = "The root item of an animation.\nChanging its properties will transform the rest of the animation.", .action = "Root Item Select", - .texture = TEXTURE_ROOT, + .atlas = ATLAS_ROOT, .isUndoable = true, .size = IMGUI_TIMELINE_ITEM_SELECTABLE_SIZE }); @@ -1434,7 +1538,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_LAYER_SELECTABLE = ImguiItem .tooltip = "A layer item.\nA graphical item within the animation.", .action = "Layer Item Select", .dragDrop = "## Layer Drag Drop", - .texture = TEXTURE_LAYER, + .atlas = ATLAS_LAYER, .isUndoable = true, .size = IMGUI_TIMELINE_ITEM_SELECTABLE_SIZE }); @@ -1445,7 +1549,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_NULL_SELECTABLE = ImguiItem .tooltip = "A null item.\nAn invisible item within the animation that is accessible via a game engine.", .action = "Null Item Select", .dragDrop = "## Null Drag Drop", - .texture = TEXTURE_NULL, + .atlas = ATLAS_NULL, .isUndoable = true, .size = IMGUI_TIMELINE_ITEM_SELECTABLE_SIZE }); @@ -1455,7 +1559,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_TRIGGERS_SELECTABLE = ImguiItem .label = "Triggers", .tooltip = "The animation's triggers.\nWill fire based on an event.", .action = "Triggers Item Select", - .texture = TEXTURE_TRIGGERS, + .atlas = ATLAS_TRIGGERS, .isUndoable = true, .size = IMGUI_TIMELINE_ITEM_SELECTABLE_SIZE }); @@ -1473,7 +1577,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_VISIBLE = ImguiItem ({ .label = "## Visible", .tooltip = "The item is visible.\nPress to set to invisible.", - .texture = TEXTURE_VISIBLE, + .atlas = ATLAS_VISIBLE, .isUndoable = true }); @@ -1481,7 +1585,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_INVISIBLE = ImguiItem ({ .label = "## Invisible", .tooltip = "The item is invisible.\nPress to set to visible.", - .texture = TEXTURE_INVISIBLE, + .atlas = ATLAS_INVISIBLE, .isUndoable = true }); @@ -1489,7 +1593,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_SHOW_RECT = ImguiItem ({ .label = "## Show Rect", .tooltip = "The rect is shown.\nPress to hide rect.", - .texture = TEXTURE_SHOW_RECT, + .atlas = ATLAS_SHOW_RECT, .isUndoable = true }); @@ -1497,7 +1601,7 @@ const inline ImguiItem IMGUI_TIMELINE_ITEM_HIDE_RECT = ImguiItem ({ .label = "## Hide Rect", .tooltip = "The rect is hidden.\nPress to show rect.", - .texture = TEXTURE_HIDE_RECT, + .atlas = ATLAS_HIDE_RECT, .isUndoable = true }); @@ -1505,7 +1609,7 @@ const inline ImguiItem IMGUI_TIMELINE_SPRITESHEET_ID = ImguiItem ({ .label = "## Spritesheet ID", .tooltip = "Change the spritesheet ID this item uses.", - .texture = TEXTURE_SPRITESHEET, + .atlas = ATLAS_SPRITESHEET, .isUndoable = true, .size = {32, 0}, }); @@ -1631,6 +1735,7 @@ const inline ImguiItem IMGUI_TIMELINE_PLAY = ImguiItem ({ .label = "|> Play", .tooltip = "Play the current animation, if paused.", + .focusWindow = IMGUI_TIMELINE.label, .chord = ImGuiKey_Space }); @@ -1638,6 +1743,7 @@ const inline ImguiItem IMGUI_TIMELINE_PAUSE = ImguiItem ({ .label = "|| Pause", .tooltip = "Pause the current animation, if playing.", + .focusWindow = IMGUI_TIMELINE.label, .chord = ImGuiKey_Space }); @@ -1760,12 +1866,6 @@ const inline ImguiItem IMGUI_TIMELINE_CREATED_BY = ImguiItem const inline ImguiItem IMGUI_TIMELINE_CREATED_ON = ImguiItem({"Created on: "}); const inline ImguiItem IMGUI_TIMELINE_VERSION = ImguiItem({"Version: "}); -const inline ImguiItem IMGUI_RECORDING = ImguiItem -({ - .label = "Recording...", - .texture = TEXTURE_RECORD -}); - const inline ImguiItem IMGUI_CONTEXT_MENU = ImguiItem ({ .label = "## Context Menu", @@ -1794,8 +1894,8 @@ const inline ImguiItem IMGUI_COPY = ImguiItem const inline ImguiItem IMGUI_PASTE = ImguiItem ({ - .label = "Paste", - .tooltip = "Pastes the currently selection contextual element from the clipboard.", + .label = "Paste", + .tooltip = "Pastes the currently selection contextual element from the clipboard.", .action = "Paste", .function = imgui_paste, .chord = ImGuiMod_Ctrl | ImGuiKey_V, @@ -1808,7 +1908,7 @@ const inline ImguiItem IMGUI_RENAMABLE = ImguiItem .tooltip = "Rename the selected item.", .action = "Rename", .isUndoable = true, - .max = 255, + .max = IMGUI_BUFFER_MAX, .flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue }); @@ -1827,16 +1927,24 @@ const inline ImguiItem IMGUI_EXIT_CONFIRMATION = ImguiItem .popup = "Exit Confirmation" }); -const inline ImguiItem IMGUI_POPUP_YES_BUTTON = ImguiItem +const inline ImguiItem IMGUI_POPUP_OK_BUTTON = ImguiItem ({ - .label = "Yes", - .size = {120, 0} + .label = "OK", + .isSizeToChild = true }); -const inline ImguiItem IMGUI_POPUP_NO_BUTTON = ImguiItem +const inline ImguiItem IMGUI_POPUP_CONFIRM_BUTTON = ImguiItem ({ - .label = "No", - .size = {120, 0} + .label = "OK", + .isSizeToChild = true, + .childRowItemCount = IMGUI_POPUP_OPTION_CHILD_ROW_ITEM_COUNT +}); + +const inline ImguiItem IMGUI_POPUP_CANCEL_BUTTON = ImguiItem +({ + .label = "Cancel", + .isSizeToChild = true, + .childRowItemCount = IMGUI_POPUP_OPTION_CHILD_ROW_ITEM_COUNT }); void imgui_init diff --git a/src/preview.cpp b/src/preview.cpp index aae1095..575fcee 100644 --- a/src/preview.cpp +++ b/src/preview.cpp @@ -1,7 +1,15 @@ -// Handles the rendering of the animation preview +// Handles the render of the animation preview #include "preview.h" +static void _preview_render_textures_free(Preview* self) +{ + for (auto& texture : self->renderFrames) + texture_free(&texture); + + self->renderFrames.clear(); +} + void preview_init(Preview* self, Anm2* anm2, Anm2Reference* reference, Resources* resources, Settings* settings) { self->anm2 = anm2; @@ -20,19 +28,45 @@ void preview_tick(Preview* self) { if (self->isPlaying) { + if (self->isRender) + { + vec2& size = self->canvas.size; + u32 framebufferPixelCount = size.x * size.y * TEXTURE_CHANNELS; + std::vector framebufferPixels(framebufferPixelCount); + Texture frameTexture; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, self->canvas.fbo); + glReadBuffer(GL_COLOR_ATTACHMENT0); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, framebufferPixels.data()); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + texture_from_rgba_init(&frameTexture, size, TEXTURE_CHANNELS, framebufferPixels.data()); + self->renderFrames.push_back(frameTexture); + } + self->time += (f32)self->anm2->fps / TICK_RATE; if (self->time >= (f32)animation->frameNum - 1) { - if (self->settings->playbackIsLoop && !self->isRecording) + if (self->isRender) + { + self->isRender = false; + self->isRenderFinished = true; self->time = 0.0f; - else self->isPlaying = false; + } + else + { + if (self->settings->playbackIsLoop) + self->time = 0.0f; + else + self->isPlaying = false; + } } } - - if (!self->isPlaying) - self->time = std::clamp(self->time, 0.0f, (f32)animation->frameNum - 1); + + self->time = std::clamp(self->time, 0.0f, (f32)animation->frameNum - 1); } } @@ -72,11 +106,11 @@ void preview_draw(Preview* self) rootModel = quad_parent_model_get(root.position, vec2(0.0f), root.rotation, PERCENT_TO_UNIT(root.scale)); // Root - if (animation->rootAnimation.isVisible && root.isVisible) + if (self->settings->previewIsTargets && animation->rootAnimation.isVisible && root.isVisible) { mat4 model = quad_model_get(PREVIEW_TARGET_SIZE, root.position, PREVIEW_TARGET_SIZE * 0.5f, root.rotation, PERCENT_TO_UNIT(root.scale)); mat4 rootTransform = transform * model; - f32 vertices[] = ATLAS_UV_VERTICES(TEXTURE_TARGET); + f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_TARGET); canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, rootTransform, vertices, PREVIEW_ROOT_COLOR); } @@ -94,26 +128,26 @@ void preview_draw(Preview* self) if (!frame.isVisible) continue; - Texture texture = self->resources->textures[self->anm2->layers[id].spritesheetID]; + Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID); - if (texture.isInvalid) + if (!texture || texture->isInvalid) continue; - vec2 uvMin = frame.crop / vec2(texture.size); - vec2 uvMax = (frame.crop + frame.size) / vec2(texture.size); + vec2 uvMin = frame.crop / vec2(texture->size); + vec2 uvMax = (frame.crop + frame.size) / vec2(texture->size); f32 vertices[] = UV_VERTICES(uvMin, uvMax); mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, frame.rotation, PERCENT_TO_UNIT(frame.scale)); mat4 layerTransform = transform * (rootModel * model); - canvas_texture_draw(&self->canvas, shaderTexture, texture.id, layerTransform, vertices, frame.tintRGBA, frame.offsetRGB); + canvas_texture_draw(&self->canvas, shaderTexture, texture->id, layerTransform, vertices, frame.tintRGBA, frame.offsetRGB); if (self->settings->previewIsBorder) canvas_rect_draw(&self->canvas, shaderLine, layerTransform, PREVIEW_BORDER_COLOR); - if (self->settings->previewIsShowPivot) + if (self->settings->previewIsPivots) { - f32 vertices[] = ATLAS_UV_VERTICES(TEXTURE_PIVOT); + f32 vertices[] = ATLAS_UV_VERTICES(ATLAS_PIVOT); mat4 pivotModel = quad_model_get(CANVAS_PIVOT_SIZE, frame.position, CANVAS_PIVOT_SIZE * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); mat4 pivotTransform = transform * (rootModel * pivotModel); canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, pivotTransform, vertices, PREVIEW_PIVOT_COLOR); @@ -121,38 +155,41 @@ void preview_draw(Preview* self) } // Nulls - for (auto& [id, nullAnimation] : animation->nullAnimations) + if (self->settings->previewIsTargets) { - if (!nullAnimation.isVisible || nullAnimation.frames.size() <= 0) - continue; - - Anm2Frame frame; - anm2_frame_from_time(self->anm2, &frame, Anm2Reference{animationID, ANM2_NULL, id}, self->time); - - if (!frame.isVisible) - continue; - - Anm2Null null = self->anm2->nulls[id]; - - vec4 color = (self->reference->itemType == ANM2_NULL && self->reference->itemID == id) ? - PREVIEW_NULL_SELECTED_COLOR : - PREVIEW_NULL_COLOR; - - vec2 size = null.isShowRect ? CANVAS_PIVOT_SIZE : PREVIEW_TARGET_SIZE; - TextureType texture = null.isShowRect ? TEXTURE_SQUARE : TEXTURE_TARGET; - - mat4 model = quad_model_get(size, frame.position, size * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); - mat4 nullTransform = transform * (rootModel * model); - - f32 vertices[] = ATLAS_UV_VERTICES(texture); - - canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, nullTransform, vertices, color); - - if (null.isShowRect) + for (auto& [id, nullAnimation] : animation->nullAnimations) { - mat4 rectModel = quad_model_get(PREVIEW_NULL_RECT_SIZE, frame.position, PREVIEW_NULL_RECT_SIZE * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); - mat4 rectTransform = transform * (rootModel * rectModel); - canvas_rect_draw(&self->canvas, shaderLine, rectTransform, color); + if (!nullAnimation.isVisible || nullAnimation.frames.size() <= 0) + continue; + + Anm2Frame frame; + anm2_frame_from_time(self->anm2, &frame, Anm2Reference{animationID, ANM2_NULL, id}, self->time); + + if (!frame.isVisible) + continue; + + Anm2Null null = self->anm2->nulls[id]; + + vec4 color = (self->reference->itemType == ANM2_NULL && self->reference->itemID == id) ? + PREVIEW_NULL_SELECTED_COLOR : + PREVIEW_NULL_COLOR; + + vec2 size = null.isShowRect ? CANVAS_PIVOT_SIZE : PREVIEW_TARGET_SIZE; + AtlasType atlas = null.isShowRect ? ATLAS_SQUARE : ATLAS_TARGET; + + mat4 model = quad_model_get(size, frame.position, size * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); + mat4 nullTransform = transform * (rootModel * model); + + f32 vertices[] = ATLAS_UV_VERTICES(atlas); + + canvas_texture_draw(&self->canvas, shaderTexture, self->resources->atlas.id, nullTransform, vertices, color); + + if (null.isShowRect) + { + mat4 rectModel = quad_model_get(PREVIEW_NULL_RECT_SIZE, frame.position, PREVIEW_NULL_RECT_SIZE * 0.5f, frame.rotation, PERCENT_TO_UNIT(frame.scale)); + mat4 rectTransform = transform * (rootModel * rectModel); + canvas_rect_draw(&self->canvas, shaderLine, rectTransform, color); + } } } } @@ -173,7 +210,7 @@ void preview_draw(Preview* self) for (auto [i, id] : self->anm2->layerMap) { Anm2Frame frame; - Anm2Item& layerAnimation = animation->layerAnimations[id]; + Anm2Item& layerAnimation = animationOverlay->layerAnimations[id]; if (!layerAnimation.isVisible || layerAnimation.frames.size() <= 0) continue; @@ -183,13 +220,13 @@ void preview_draw(Preview* self) if (!frame.isVisible) continue; - Texture texture = self->resources->textures[self->anm2->layers[id].spritesheetID]; - - if (texture.isInvalid) + Texture* texture = map_find(self->resources->textures, self->anm2->layers[id].spritesheetID); + + if (!texture || texture->isInvalid) continue; - vec2 uvMin = frame.crop / vec2(texture.size); - vec2 uvMax = (frame.crop + frame.size) / vec2(texture.size); + vec2 uvMin = frame.crop / vec2(texture->size); + vec2 uvMax = (frame.crop + frame.size) / vec2(texture->size); f32 vertices[] = UV_VERTICES(uvMin, uvMax); mat4 model = quad_model_get(frame.size, frame.position, frame.pivot, frame.rotation, PERCENT_TO_UNIT(frame.scale)); @@ -198,13 +235,29 @@ void preview_draw(Preview* self) vec4 tint = frame.tintRGBA; tint.a *= U8_TO_FLOAT(self->settings->previewOverlayTransparency); - canvas_texture_draw(&self->canvas, shaderTexture, texture.id, layerTransform, vertices, tint, frame.offsetRGB); + canvas_texture_draw(&self->canvas, shaderTexture, texture->id, layerTransform, vertices, tint, frame.offsetRGB); } } canvas_unbind(); } +void preview_render_start(Preview* self) +{ + self->isRender = true; + self->isPlaying = true; + self->time = 0.0f; + _preview_render_textures_free(self); +} + +void preview_render_end(Preview* self) +{ + self->isRender = false; + self->isPlaying = false; + self->isRenderFinished = false; + _preview_render_textures_free(self); +} + void preview_free(Preview* self) { canvas_free(&self->canvas); diff --git a/src/preview.h b/src/preview.h index dfd9ca0..926e694 100644 --- a/src/preview.h +++ b/src/preview.h @@ -35,7 +35,10 @@ struct Preview s32 animationOverlayID = ID_NONE; Canvas canvas; bool isPlaying = false; - bool isRecording = false; + bool isRender = false; + bool isRenderFinished = false; + bool isRenderCancelled = false; + std::vector renderFrames; f32 time{}; }; @@ -43,4 +46,5 @@ void preview_init(Preview* self, Anm2* anm2, Anm2Reference* reference, Resources void preview_draw(Preview* self); void preview_tick(Preview* self); void preview_free(Preview* self); -void preview_record_set(Preview* self); \ No newline at end of file +void preview_render_start(Preview* self); +void preview_render_end(Preview* self); \ No newline at end of file diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..748201c --- /dev/null +++ b/src/render.h @@ -0,0 +1,25 @@ +#pragma once + +#include "COMMON.h" + +enum RenderType +{ + RENDER_PNG, + RENDER_GIF, + RENDER_WEBM, + RENDER_COUNT +}; + +const inline std::string RENDER_TYPE_STRINGS[] = +{ + "PNG Images", + "GIF image", + "WebM video", +}; + +const inline std::string RENDER_EXTENSIONS[RENDER_COUNT] = +{ + ".png", + ".gif", + ".webm" +}; \ No newline at end of file diff --git a/src/resources.cpp b/src/resources.cpp index c9fabbf..4174111 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -1,21 +1,20 @@ #include "resources.h" -void resources_texture_init(Resources* resources, const std::string& path, s32 id) +void resources_texture_init(Resources* self, const std::string& path, s32 id) { Texture texture; - if (resources->textures.find(id) != resources->textures.end() && resources->textures[id].id != resources->textures[TEXTURE_NONE].id) - texture_free(&resources->textures[id]); + if (map_find(self->textures, id)) + texture_free(&self->textures[id]); - if (!texture_from_path_init(&texture, path)) - texture.isInvalid = true; + texture_from_path_init(&texture, path); - resources->textures[id] = texture; + self->textures[id] = texture; } void resources_init(Resources* self) { - texture_from_data_init(&self->atlas, (u8*)TEXTURE_ATLAS, TEXTURE_ATLAS_LENGTH); + texture_from_encoded_data_init(&self->atlas, TEXTURE_ATLAS_SIZE, TEXTURE_CHANNELS, (u8*)TEXTURE_ATLAS, TEXTURE_ATLAS_LENGTH); for (s32 i = 0; i < SHADER_COUNT; i++) shader_init(&self->shaders[i], SHADER_DATA[i].vertex, SHADER_DATA[i].fragment); @@ -25,14 +24,15 @@ void resources_free(Resources* self) { resources_textures_free(self); - for (s32 i = 0; i < SHADER_COUNT; i++) - shader_free(&self->shaders[i]); + for (auto& shader : self->shaders) + shader_free(&shader); + texture_free(&self->atlas); } void resources_textures_free(Resources* self) { - for (auto & [id, texture] : self->textures) + for (auto& [id, texture] : self->textures) texture_free(&self->textures[id]); log_info(RESOURCES_TEXTURES_FREE_INFO); diff --git a/src/resources.h b/src/resources.h index 94e3e3c..68ce17a 100644 --- a/src/resources.h +++ b/src/resources.h @@ -14,6 +14,6 @@ struct Resources }; void resources_init(Resources* self); -void resources_texture_init(Resources* resources, const std::string& path, s32 id); +void resources_texture_init(Resources* self, const std::string& path, s32 id); void resources_free(Resources* self); void resources_textures_free(Resources* self); diff --git a/src/settings.cpp b/src/settings.cpp index c0eac7d..f6250c5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -6,11 +6,17 @@ static void _settings_setting_load(Settings* self, const std::string& line) { const auto& entry = SETTINGS_ENTRIES[i]; const std::string& key = entry.key; + void* target = (u8*)self + entry.offset; auto match_key = [&](const std::string& full) -> const char* { - return (line.starts_with(full) && line[full.size()] == '=') ? line.c_str() + full.size() + 1 : nullptr; + if (!line.starts_with(full)) + return nullptr; + size_t p = full.size(); + while (p < line.size() && std::isspace((u8)line[p])) ++p; + if (p < line.size() && line[p] == '=') return line.c_str() + p + 1; + return nullptr; }; auto* value = match_key(key); @@ -79,9 +85,12 @@ static void _settings_setting_write(Settings* self, std::ostream& out, SettingsE out << entry.key << "=" << value << "\n"; break; case TYPE_STRING: - value = *(std::string*)(selfPointer + entry.offset); - out << entry.key << "=" << value << "\n"; + { + const std::string data = *reinterpret_cast(selfPointer + entry.offset); + if (!data.empty()) + out << entry.key << "=" << data.c_str() << "\n"; break; + } case TYPE_IVEC2: { ivec2* data = (ivec2*)(selfPointer + entry.offset); diff --git a/src/settings.h b/src/settings.h index 46fdff7..d6cd6ec 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,5 +1,6 @@ #pragma once +#include "render.h" #include "tool.h" #define SETTINGS_BUFFER 0xFFFF @@ -24,7 +25,8 @@ struct Settings bool previewIsAxis = true; bool previewIsGrid = true; bool previewIsRootTransform = false; - bool previewIsShowPivot = false; + bool previewIsPivots = false; + bool previewIsTargets = true; bool previewIsBorder = false; f32 previewOverlayTransparency = 255.0f; f32 previewZoom = 200.0; @@ -45,6 +47,10 @@ struct Settings vec4 editorBackgroundColor = {0.113, 0.184, 0.286, 1.0}; ToolType tool = TOOL_PAN; vec4 toolColor = {1.0, 1.0, 1.0, 1.0}; + RenderType renderType = RENDER_PNG; + std::string renderPath = "."; + std::string renderFormat = "{}.png"; + std::string ffmpegPath = "/usr/bin/ffmpeg"; }; const SettingsEntry SETTINGS_ENTRIES[] = @@ -54,7 +60,8 @@ const SettingsEntry SETTINGS_ENTRIES[] = {"previewIsAxis", TYPE_BOOL, offsetof(Settings, previewIsAxis)}, {"previewIsGrid", TYPE_BOOL, offsetof(Settings, previewIsGrid)}, {"previewIsRootTransform", TYPE_BOOL, offsetof(Settings, previewIsRootTransform)}, - {"previewIsShowPivot", TYPE_BOOL, offsetof(Settings, previewIsShowPivot)}, + {"previewIsPivots", TYPE_BOOL, offsetof(Settings, previewIsPivots)}, + {"previewIsTargets", TYPE_BOOL, offsetof(Settings, previewIsTargets)}, {"previewIsBorder", TYPE_BOOL, offsetof(Settings, previewIsBorder)}, {"previewOverlayTransparency", TYPE_FLOAT, offsetof(Settings, previewOverlayTransparency)}, {"previewZoom", TYPE_FLOAT, offsetof(Settings, previewZoom)}, @@ -75,6 +82,10 @@ const SettingsEntry SETTINGS_ENTRIES[] = {"editorBackgroundColor", TYPE_VEC4, offsetof(Settings, editorBackgroundColor)}, {"tool", TYPE_INT, offsetof(Settings, tool)}, {"toolColor", TYPE_VEC4, offsetof(Settings, toolColor)}, + {"renderType", TYPE_INT, offsetof(Settings, renderType)}, + {"renderPath", TYPE_STRING, offsetof(Settings, renderPath)}, + {"renderFormat", TYPE_STRING, offsetof(Settings, renderFormat)}, + {"ffmpegPath", TYPE_STRING, offsetof(Settings, ffmpegPath)} }; constexpr s32 SETTINGS_COUNT = (s32)std::size(SETTINGS_ENTRIES); diff --git a/src/snapshots.cpp b/src/snapshots.cpp index 7453dca..679daaf 100644 --- a/src/snapshots.cpp +++ b/src/snapshots.cpp @@ -34,6 +34,13 @@ void snapshots_init(Snapshots* self, Anm2* anm2, Anm2Reference* reference, Previ self->preview = preview; } +void snapshots_reset(Snapshots* self) +{ + self->undoStack = SnapshotStack{}; + self->redoStack = SnapshotStack{}; + self->action.clear(); +} + void snapshots_undo_stack_push(Snapshots* self, const Snapshot* snapshot) { _snapshot_stack_push(&self->undoStack, snapshot); diff --git a/src/snapshots.h b/src/snapshots.h index a5d93e9..390dda6 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -33,4 +33,5 @@ struct Snapshots void snapshots_undo_stack_push(Snapshots* self, const Snapshot* snapshot); void snapshots_init(Snapshots* self, Anm2* anm2, Anm2Reference* reference, Preview* preview); void snapshots_undo(Snapshots* self); -void snapshots_redo(Snapshots* self); \ No newline at end of file +void snapshots_redo(Snapshots* self); +void snapshots_reset(Snapshots* self); \ No newline at end of file diff --git a/src/state.cpp b/src/state.cpp index 4c5d976..6f17f87 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -10,7 +10,6 @@ static void _update(State* self) SDL_GetWindowSize(self->window, &self->settings.windowSize.x, &self->settings.windowSize.y); imgui_update(&self->imgui); - dialog_update(&self->dialog); if (self->imgui.isQuit) self->isRunning = false; @@ -99,8 +98,8 @@ void init(State* self) glDisable(GL_LINE_SMOOTH); resources_init(&self->resources); + dialog_init(&self->dialog, self->window); clipboard_init(&self->clipboard, &self->anm2); - dialog_init(&self->dialog, &self->anm2, &self->reference, &self->resources, self->window); snapshots_init(&self->snapshots, &self->anm2, &self->reference, &self->preview); preview_init(&self->preview, &self->anm2, &self->reference, &self->resources, &self->settings); editor_init(&self->editor, &self->anm2, &self->reference, &self->resources, &self->settings); @@ -130,7 +129,6 @@ void init(State* self) anm2_new(&self->anm2); } - void loop(State* self) { self->tick = SDL_GetTicks(); diff --git a/src/texture.cpp b/src/texture.cpp index f928229..1c08084 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -8,19 +8,7 @@ #define STB_IMAGE_WRITE_IMPLEMENTATION #include - -static std::vector _texture_download(Texture* self) -{ - std::vector pixels(self->size.x * self->size.y * TEXTURE_CHANNELS); - - glBindTexture(GL_TEXTURE_2D, self->id); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - - return pixels; -} - -void texture_gl_set(Texture* self, void* data) +static void _texture_gl_set(Texture* self, const u8* data) { glGenTextures(1, &self->id); glBindTexture(GL_TEXTURE_2D, self->id); @@ -32,35 +20,67 @@ void texture_gl_set(Texture* self, void* data) glBindTexture(GL_TEXTURE_2D, 0); } +std::vector texture_download(const Texture* self) +{ + std::vector pixels(self->size.x * self->size.y * TEXTURE_CHANNELS); + + glBindTexture(GL_TEXTURE_2D, self->id); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + return pixels; +} + bool texture_from_path_init(Texture* self, const std::string& path) { - void* data = stbi_load(path.c_str(), &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); + *self = Texture{}; + u8* data = stbi_load(path.c_str(), &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); if (!data) { log_error(std::format(TEXTURE_INIT_ERROR, path)); + self->isInvalid = true; return false; } log_info(std::format(TEXTURE_INIT_INFO, path)); - texture_gl_set(self, data); + _texture_gl_set(self, data); return true; } -bool texture_from_data_init(Texture* self, const u8* data, u32 length) +bool texture_from_encoded_data_init(Texture* self, ivec2 size, s32 channels, const u8* data, u32 length) { - void* textureData = stbi_load_from_memory(data, length, &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); + *self = Texture{}; + self->size = size; + self->channels = channels; - if (!textureData) return false; + u8* textureData = stbi_load_from_memory(data, length, &self->size.x, &self->size.y, &self->channels, TEXTURE_CHANNELS); - texture_gl_set(self, textureData); + if (!textureData) + { + self->isInvalid = true; + return false; + } + + _texture_gl_set(self, textureData); return true; } -bool texture_from_data_write(const std::string& path, const u8* data, ivec2 size) +bool texture_from_rgba_init(Texture* self, ivec2 size, s32 channels, const u8* data) +{ + *self = Texture{}; + self->size = size; + self->channels = channels; + + _texture_gl_set(self, data); + + return true; +} + +bool texture_from_rgba_write(const std::string& path, const u8* data, ivec2 size) { log_info(std::format(TEXTURE_SAVE_INFO, path)); return (bool)stbi_write_png(path.c_str(), size.x, size.y, TEXTURE_CHANNELS, data, size.x * TEXTURE_CHANNELS); @@ -68,7 +88,7 @@ bool texture_from_data_write(const std::string& path, const u8* data, ivec2 size bool texture_from_gl_write(Texture* self, const std::string& path) { - return texture_from_data_write(path, _texture_download(self).data(), self->size); + return texture_from_rgba_write(path, texture_download(self).data(), self->size); } void texture_free(Texture* self) diff --git a/src/texture.h b/src/texture.h index a6a46b4..9eeb0fe 100644 --- a/src/texture.h +++ b/src/texture.h @@ -15,11 +15,11 @@ struct Texture bool isInvalid = false; }; -void texture_gl_set(Texture* self, void* data); +bool texture_from_encoded_data_init(Texture* self, ivec2 size, s32 channels, const u8* data, u32 length); +bool texture_from_gl_write(Texture* self, const std::string& path); bool texture_from_path_init(Texture* self, const std::string& path); -bool texture_from_data_init(Texture* self, const u8* data, u32 length); -void texture_free(Texture* self); -std::vector texture_download(Texture* self); -bool texture_from_data_write(const std::string& path, const u8* data, ivec2 size); +bool texture_from_rgba_init(Texture* self, ivec2 size, s32 channels, const u8* data); +bool texture_from_rgba_write(const std::string& path, const u8* data, ivec2 size); bool texture_pixel_set(Texture* self, ivec2 position, vec4 color); -bool texture_from_gl_write(Texture* self, const std::string& path); \ No newline at end of file +void texture_free(Texture* self); +std::vector texture_download(const Texture* self); \ No newline at end of file