diff --git a/go.mod b/go.mod index c5a9f6c..ad7135c 100644 --- a/go.mod +++ b/go.mod @@ -5,40 +5,19 @@ go 1.25.0 require ( github.com/go-telegram/bot v1.20.0 github.com/robfig/cron/v3 v3.0.1 - github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e go.mongodb.org/mongo-driver/v2 v2.7.0 + golang.org/x/image v0.41.0 + golang.org/x/text v0.37.0 ) require ( - codeberg.org/go-pdf/fpdf v0.11.1 // indirect - github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect - github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect - github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect - github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect - github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect - github.com/andybalholm/brotli v1.2.1 // indirect - github.com/benoitkugler/textlayout v0.3.2 // indirect - github.com/benoitkugler/textprocessing v0.0.6 // indirect - github.com/go-fonts/latin-modern v0.3.3 // indirect - github.com/go-text/typesetting v0.3.4 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/klauspost/compress v1.17.6 // indirect - github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect - github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect - github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 // indirect - github.com/tdewolff/minify/v2 v2.24.13 // indirect - github.com/tdewolff/parse/v2 v2.8.12 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - github.com/yuin/goldmark v1.8.2 // indirect golang.org/x/crypto v0.51.0 // indirect - golang.org/x/image v0.41.0 // indirect - golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.37.0 // indirect - modernc.org/knuth v0.5.5 // indirect - modernc.org/token v1.1.0 // indirect - star-tex.org/x/tex v0.7.1 // indirect ) diff --git a/go.sum b/go.sum index 254c45e..fad5cd6 100644 --- a/go.sum +++ b/go.sum @@ -1,108 +1,40 @@ -codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= -codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= -codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI= -codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y= -codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs= -codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= -git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= -git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= -git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM= -git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM= -github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= -github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= -github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= -github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw= -github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= -github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= -github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE= -github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= -github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo= -github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g= -github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0= -github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY= github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc= github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= -github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU= -github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= -github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= -github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= -github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= -github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= -github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= -github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f h1:uLR2GaV0kWYZ3Ns3l3sjtiN+mOWAQadvrL8HXcyKjl0= -github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f/go.mod h1:LZwgIPG9X6nH6j5Ef+xMFspl6Hru4b5EJxzMfeqHYJY= -github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+/3uerT2ezy9J7KQWH18JCfhDmI= -github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os= -github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e h1:Z9HqI3WUPBDwJEDH9BCLpQGvjmGs0XNRDBLry4shZfs= -github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e/go.mod h1:Lz12ClttuKvo2jb1UxBfJJWmKV0LmEc8IRo/30R7z+k= -github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 h1:wP/+8oeXiF55DUy9U0BP4iDD2mNu+o7evt5QLbLHv9E= -github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189/go.mod h1:KjNTiF0TXOXJyWGS/0DPXRknEC20c12kc0JcCYNqtz4= -github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58= -github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0= -github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg= -github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= -github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= -github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mongodb.org/mongo-driver/v2 v2.7.0 h1:RO+zqavD2/GCL3cxOMyZhx6R9Irzr8/6gsoqx5tcY/c= go.mongodb.org/mongo-driver/v2 v2.7.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -110,7 +42,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= @@ -119,13 +50,3 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4= -gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y= -modernc.org/knuth v0.5.5 h1:6lap2U/ISm8aC/4NU58ALFCRllNPaK0EZcIGY/oDgUg= -modernc.org/knuth v0.5.5/go.mod h1:e5SBb35HQBj2aFwbBO3ClPcViLY3Wi0LzaOd7c/3qMk= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -star-tex.org/x/tex v0.7.1 h1:4qGAByRyY0WQsOjtcHlxz+FgrYxz8fzxIds2Gjepp5U= -star-tex.org/x/tex v0.7.1/go.mod h1:Y3y0U7sZTltTh/CDZIx0oAtMjG7eMaTuTtvDZGdyhJo= diff --git a/internal/modules/misc/fonts/dejavu-sans-license.txt b/internal/modules/misc/fonts/dejavu-sans-license.txt deleted file mode 100644 index ecf22a0..0000000 --- a/internal/modules/misc/fonts/dejavu-sans-license.txt +++ /dev/null @@ -1,47 +0,0 @@ -DejaVu Sans font license - -Source: https://dejavu-fonts.github.io/ - -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. -Bitstream Vera is a trademark of Bitstream, Inc. -DejaVu changes are in public domain. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of the fonts accompanying this license ("Fonts") and associated -documentation files (the "Font Software"), to reproduce and distribute the -Font Software, including without limitation the rights to use, copy, merge, -publish, distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to the -following conditions: - -The above copyright and trademark notices and this permission notice shall -be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular -the designs of glyphs or characters in the Fonts may be modified and -additional glyphs or characters may be added to the Fonts, only if the fonts -are renamed to names not containing either the words "Bitstream" or the word -"Vera". - -This License becomes null and void to the extent applicable to Fonts or Font -Software that has been modified and distributed under the "Bitstream Vera" -names. - -The Font Software may be sold as part of a larger software package but no -copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, -TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME -FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING -ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE -FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome -Foundation, and Bitstream Inc., shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this Font Software -without prior written authorization from the Gnome Foundation or Bitstream -Inc., respectively. diff --git a/internal/modules/misc/fonts/dejavu-sans.ttf b/internal/modules/misc/fonts/dejavu-sans.ttf deleted file mode 100644 index fb0bd94..0000000 Binary files a/internal/modules/misc/fonts/dejavu-sans.ttf and /dev/null differ diff --git a/internal/modules/misc/handlers_test.go b/internal/modules/misc/handlers_test.go index 31ab3e6..ccd1854 100644 --- a/internal/modules/misc/handlers_test.go +++ b/internal/modules/misc/handlers_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/go-telegram/bot/models" - "github.com/tdewolff/canvas" "github.com/tiennm99/miti99bot/internal/modules" "github.com/tiennm99/miti99bot/internal/storage" @@ -426,39 +425,22 @@ func TestWheelOfNamesBeta_RotatesOptionLabelsWithSlices(t *testing.T) { } } -func TestWheelOfNamesBeta_DisplayTextPreservesVietnamese(t *testing.T) { - input := "Tiếng Việt Đặng" - got := wheelBetaDisplayText(input, 32) - if got != input { - t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, input) +func TestWheelOfNamesBeta_DisplayTextNormalizesVietnamese(t *testing.T) { + input := "không dấu Tiếng Việt Đặng Ơ Ư ấ ệ" + want := "khong dau Tieng Viet Dang O U a e" + got := wheelBetaDisplayText(input, 64) + if got != want { + t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, want) } if strings.Contains(got, "?") { t.Fatalf("wheelBetaDisplayText() replaced Vietnamese with ?: %q", got) } } -func TestWheelOfNamesBeta_FontUsesNormalWeight(t *testing.T) { - if got := wheelBetaTextFont.Style().Weight(); got != canvas.FontRegular { - t.Fatalf("font weight = %v, want regular non-bold face", got) - } - if wheelBetaTextFont.Style().Italic() { - t.Fatalf("font style = %v, want non-italic regular face", wheelBetaTextFont.Style()) - } -} - -func TestWheelOfNamesBeta_FontSupportsVietnameseGlyphs(t *testing.T) { - face := newWheelBetaTextFace(wheelBetaInkColorIndex) - for _, r := range "Tiếng Việt Đặng Ơ Ư ấ ệ" { - if r == ' ' { - continue - } - glyphs := face.Glyphs(string(r)) - if len(glyphs) == 0 { - t.Fatalf("Glyphs(%q) is empty, want Vietnamese glyph support", r) - } - if glyphs[0].ID == 0 { - t.Fatalf("Glyphs(%q)[0].ID = 0, want Vietnamese glyph support", r) - } +func TestWheelOfNamesBeta_DisplayTextNormalizesDecomposedVietnamese(t *testing.T) { + got := wheelBetaDisplayText("tie\u0302\u0301ng Vie\u0323t", 32) + if got != "tieng Viet" { + t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, "tieng Viet") } } diff --git a/internal/modules/misc/wheelofnames_beta.go b/internal/modules/misc/wheelofnames_beta.go index 99e7a86..fd0bc8a 100644 --- a/internal/modules/misc/wheelofnames_beta.go +++ b/internal/modules/misc/wheelofnames_beta.go @@ -5,6 +5,7 @@ import ( "fmt" "image" "image/color" + "image/draw" "image/gif" "math" ) @@ -103,8 +104,38 @@ func renderWheelBetaFrameWithStatus(options []string, winner int, rotation float } func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation float64, reveal bool, status string, celebrateStep int) *image.Paletted { + rect := image.Rect(0, 0, wheelBetaSize, wheelBetaSize) + img := image.NewPaletted(rect, wheelBetaPalette) + draw.Draw(img, rect, image.NewUniform(wheelBetaPalette[wheelBetaBackgroundColorIndex]), image.Point{}, draw.Src) + + cx, cy := wheelBetaSize/2, wheelBetaSize/2 + drawWheelDropShadow(img, cx, cy) + + segment := 2 * math.Pi / float64(len(options)) + r2 := wheelBetaRadius * wheelBetaRadius + for y := cy - wheelBetaRadius; y <= cy+wheelBetaRadius; y++ { + for x := cx - wheelBetaRadius; x <= cx+wheelBetaRadius; x++ { + dx, dy := x-cx, y-cy + if dx*dx+dy*dy > r2 { + continue + } + theta := normalizeAngle(math.Atan2(float64(dy), float64(dx)) - rotation) + idx := int(theta / segment) + colorIndex := wheelBetaSliceColorIndexes[idx%len(wheelBetaSliceColorIndexes)] + img.SetColorIndex(x, y, colorIndex) + } + } + currentIndex := currentWheelBetaIndex(len(options), rotation) pointerColor := wheelBetaSliceColorIndexes[currentIndex%len(wheelBetaSliceColorIndexes)] + drawWheelLighting(img, cx, cy) + drawWheelRim(img, cx, cy) + drawWinnerCelebration(img, celebrateStep) + drawWheelSliceLabels(img, options, rotation) + drawCircle(img, cx, cy, 24, wheelBetaPaperColorIndex) + drawCircle(img, cx, cy, 18, wheelBetaInkColorIndex) + drawPointer(img, cx+wheelBetaRadius, cy, pointerColor) + drawCenteredText(img, "WHEELOFNAMES BETA", cy+wheelBetaRadius+34, wheelBetaInkColorIndex) label := status if label == "" { label = "CURRENT" @@ -114,7 +145,8 @@ func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation label = "RESULT" value = wheelBetaDisplayText(options[winner], 28) } - return palettizeWheelBetaFrame(renderWheelBetaCanvasImage(options, rotation, pointerColor, label, value, celebrateStep)) + drawStatusBand(img, label, value) + return img } func finalWheelRotation(optionCount, winner int) float64 { diff --git a/internal/modules/misc/wheelofnames_beta_drawing.go b/internal/modules/misc/wheelofnames_beta_drawing.go index b5edf86..02c28d1 100644 --- a/internal/modules/misc/wheelofnames_beta_drawing.go +++ b/internal/modules/misc/wheelofnames_beta_drawing.go @@ -1,121 +1,36 @@ package misc import ( - _ "embed" "image" - "image/draw" + "image/color" "math" "strings" "unicode" - "github.com/tdewolff/canvas" - "github.com/tdewolff/canvas/renderers/rasterizer" + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" + "golang.org/x/text/unicode/norm" ) const ( wheelBetaSliceLabelRadius = 64 - wheelBetaFontSize = 10 ) -//go:embed fonts/dejavu-sans.ttf -var wheelBetaTextFontBytes []byte - -var ( - wheelBetaTextFont = mustWheelBetaTextFont() - wheelBetaTextFontFamily = mustWheelBetaTextFontFamily() -) - -func mustWheelBetaTextFont() *canvas.Font { - parsed, err := canvas.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular) - if err != nil { - panic(err) - } - return parsed -} - -func mustWheelBetaTextFontFamily() *canvas.FontFamily { - family := canvas.NewFontFamily("wheel-beta") - if err := family.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular); err != nil { - panic(err) - } - return family -} - -func newWheelBetaTextFace(colorIndex byte) *canvas.FontFace { - return wheelBetaTextFontFamily.Face(wheelBetaFontSize, wheelBetaPalette[colorIndex], canvas.FontRegular) -} - -func renderWheelBetaCanvasImage(options []string, rotation float64, pointerColorIndex byte, label, value string, celebrateStep int) image.Image { - c := canvas.New(float64(wheelBetaSize), float64(wheelBetaSize)) - ctx := canvas.NewContext(c) - ctx.SetCoordSystem(canvas.CartesianIV) - ctx.SetStrokeCapper(canvas.RoundCap) - ctx.SetStrokeJoiner(canvas.RoundJoin) - - drawWheelBetaRect(ctx, 0, 0, float64(wheelBetaSize), float64(wheelBetaSize), wheelBetaBackgroundColorIndex) - - cx := float64(wheelBetaSize) / 2 - cy := float64(wheelBetaSize) / 2 - drawWheelDropShadow(ctx, cx, cy) - drawWheelSlices(ctx, options, rotation) - drawWheelLighting(ctx, cx, cy) - drawWheelRim(ctx, cx, cy) - drawWinnerCelebration(ctx, celebrateStep) - drawWheelSliceLabels(ctx, options, rotation) - drawCircle(ctx, cx, cy, 24, wheelBetaPaperColorIndex) - drawCircle(ctx, cx, cy, 18, wheelBetaInkColorIndex) - drawPointer(ctx, cx+float64(wheelBetaRadius), cy, pointerColorIndex) - drawCenteredText(ctx, "WHEELOFNAMES BETA", cy+float64(wheelBetaRadius)+24, wheelBetaInkColorIndex) - drawStatusBand(ctx, label, value) - - return rasterizer.Draw(c, canvas.DPMM(1), canvas.DefaultColorSpace) -} - -func palettizeWheelBetaFrame(src image.Image) *image.Paletted { - rect := image.Rect(0, 0, wheelBetaSize, wheelBetaSize) - img := image.NewPaletted(rect, wheelBetaPalette) - draw.FloydSteinberg.Draw(img, rect, src, image.Point{}) - return img -} - -func drawWheelSlices(ctx *canvas.Context, options []string, rotation float64) { +func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float64) { if len(options) == 0 { return } - cx := float64(wheelBetaSize) / 2 - cy := float64(wheelBetaSize) / 2 - segment := 2 * math.Pi / float64(len(options)) - steps := int(math.Ceil(float64(wheelBetaRadius) * segment / 4)) - if steps < 8 { - steps = 8 - } - for idx := range options { - start := rotation + float64(idx)*segment - path := &canvas.Path{} - path.MoveTo(cx, cy) - for step := 0; step <= steps; step++ { - angle := start + segment*float64(step)/float64(steps) - path.LineTo(cx+math.Cos(angle)*float64(wheelBetaRadius), cy+math.Sin(angle)*float64(wheelBetaRadius)) - } - path.Close() - drawWheelBetaPath(ctx, path, wheelBetaSliceColorIndexes[idx%len(wheelBetaSliceColorIndexes)]) - } -} - -func drawWheelSliceLabels(ctx *canvas.Context, options []string, rotation float64) { - if len(options) == 0 { - return - } - cx := float64(wheelBetaSize) / 2 - cy := float64(wheelBetaSize) / 2 + cx, cy := wheelBetaSize/2, wheelBetaSize/2 segment := 2 * math.Pi / float64(len(options)) limit := wheelBetaSliceLabelLimit(len(options)) for idx, option := range options { angle := rotation + (float64(idx)+0.5)*segment - centerX := cx + math.Cos(angle)*float64(wheelBetaSliceLabelRadius) - centerY := cy + math.Sin(angle)*float64(wheelBetaSliceLabelRadius) + centerX := cx + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius))) + centerY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius))) text := wheelBetaDisplayText(option, limit) - drawRotatedCenteredTextAt(ctx, text, centerX, centerY, angle, wheelBetaInkColorIndex) + drawRotatedCenteredTextAt(img, text, centerX+1, centerY+1, angle, wheelBetaPaperColorIndex) + drawRotatedCenteredTextAt(img, text, centerX, centerY, angle, wheelBetaInkColorIndex) } } @@ -132,130 +47,229 @@ func wheelBetaSliceLabelLimit(optionCount int) int { } } -func drawCircle(ctx *canvas.Context, cx, cy, radius float64, colorIndex byte) { - ctx.SetFillColor(wheelBetaPalette[colorIndex]) - ctx.DrawPath(cx, cy, canvas.Circle(radius)) - ctx.Fill() +func drawCircle(img *image.Paletted, cx, cy, radius int, colorIndex byte) { + r2 := radius * radius + for y := cy - radius; y <= cy+radius; y++ { + for x := cx - radius; x <= cx+radius; x++ { + dx, dy := x-cx, y-cy + if dx*dx+dy*dy <= r2 { + img.SetColorIndex(x, y, colorIndex) + } + } + } } -func drawWheelDropShadow(ctx *canvas.Context, cx, cy float64) { - ctx.SetFillColor(wheelBetaPalette[wheelBetaShadowColorIndex]) - ctx.DrawPath(cx, cy+8, canvas.Ellipse(float64(wheelBetaRadius+9), float64(wheelBetaRadius+5))) - ctx.Fill() +func drawWheelDropShadow(img *image.Paletted, cx, cy int) { + rx := wheelBetaRadius + 9 + ry := wheelBetaRadius + 5 + shadowCY := cy + 8 + limit := rx * rx * ry * ry + for y := shadowCY - ry; y <= shadowCY+ry; y++ { + for x := cx - rx; x <= cx+rx; x++ { + dx := x - cx + dy := y - shadowCY + if dx*dx*ry*ry+dy*dy*rx*rx <= limit { + setWheelBetaPixel(img, x, y, wheelBetaShadowColorIndex) + } + } + } } -func drawWheelLighting(ctx *canvas.Context, cx, cy float64) { - drawHighlightOval(ctx, cx-30, cy-50, 44, 18) - drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-5), 2, wheelBetaBevelColorIndex) - drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-8), 1, wheelBetaHighlightColorIndex) +func drawWheelLighting(img *image.Paletted, cx, cy int) { + outer := wheelBetaRadius * wheelBetaRadius + edgeStart := wheelBetaRadius - 13 + edge := edgeStart * edgeStart + for y := cy - wheelBetaRadius; y <= cy+wheelBetaRadius; y++ { + for x := cx - wheelBetaRadius; x <= cx+wheelBetaRadius; x++ { + dx, dy := x-cx, y-cy + d2 := dx*dx + dy*dy + if d2 > outer || d2 < edge { + continue + } + if dx+dy > wheelBetaRadius/3 { + img.SetColorIndex(x, y, wheelBetaBevelColorIndex) + } + if dx+dy < -wheelBetaRadius { + img.SetColorIndex(x, y, wheelBetaHighlightColorIndex) + } + } + } + + drawHighlightOval(img, cx-30, cy-50, 44, 18) } -func drawHighlightOval(ctx *canvas.Context, cx, cy, rx, ry float64) { - ctx.SetFillColor(wheelBetaPalette[wheelBetaHighlightColorIndex]) - ctx.DrawPath(cx, cy, canvas.Ellipse(rx, ry)) - ctx.Fill() +func drawHighlightOval(img *image.Paletted, cx, cy, rx, ry int) { + limit := rx * rx * ry * ry + for y := cy - ry; y <= cy+ry; y++ { + for x := cx - rx; x <= cx+rx; x++ { + dx := x - cx + dy := y - cy + if dx*dx*ry*ry+dy*dy*rx*rx <= limit { + setWheelBetaPixel(img, x, y, wheelBetaHighlightColorIndex) + } + } + } } -func drawWheelRim(ctx *canvas.Context, cx, cy float64) { - drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius)-1.5, 3, wheelBetaInkColorIndex) - drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-5), 1, wheelBetaBevelColorIndex) - drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-8), 1, wheelBetaHighlightColorIndex) +func drawWheelRim(img *image.Paletted, cx, cy int) { + drawCircleOutline(img, cx, cy, wheelBetaRadius, 3, wheelBetaInkColorIndex) + drawCircleOutline(img, cx, cy, wheelBetaRadius-5, 1, wheelBetaBevelColorIndex) + drawCircleOutline(img, cx, cy, wheelBetaRadius-8, 1, wheelBetaHighlightColorIndex) } -func drawCircleOutline(ctx *canvas.Context, cx, cy, radius, thickness float64, colorIndex byte) { - ctx.SetFillColor(canvas.Transparent) - ctx.SetStrokeColor(wheelBetaPalette[colorIndex]) - ctx.SetStrokeWidth(thickness) - ctx.DrawPath(cx, cy, canvas.Circle(radius)) - ctx.Stroke() +func drawCircleOutline(img *image.Paletted, cx, cy, radius, thickness int, colorIndex byte) { + outer := radius * radius + innerRadius := radius - thickness + inner := innerRadius * innerRadius + for y := cy - radius; y <= cy+radius; y++ { + for x := cx - radius; x <= cx+radius; x++ { + dx, dy := x-cx, y-cy + d2 := dx*dx + dy*dy + if d2 <= outer && d2 >= inner { + img.SetColorIndex(x, y, colorIndex) + } + } + } } -func drawPointer(ctx *canvas.Context, tipX, cy float64, fillColorIndex byte) { - outer := &canvas.Path{} - outer.MoveTo(tipX+2, cy) - outer.LineTo(tipX+34, cy+24) - outer.LineTo(tipX+34, cy-24) - outer.Close() - drawWheelBetaPath(ctx, outer, wheelBetaInkColorIndex) - drawWheelBetaRect(ctx, tipX-1, cy-2, tipX+5, cy+2, wheelBetaInkColorIndex) - - inner := &canvas.Path{} - inner.MoveTo(tipX+4, cy) - inner.LineTo(tipX+30, cy+10) - inner.LineTo(tipX+30, cy-10) - inner.Close() - drawWheelBetaPath(ctx, inner, fillColorIndex) +func drawPointer(img *image.Paletted, tipX, cy int, fillColorIndex byte) { + for xOffset := 0; xOffset < 34; xOffset++ { + half := xOffset / 2 + x := tipX + xOffset + for y := cy - half; y <= cy+half; y++ { + setWheelBetaPixel(img, x, y, wheelBetaInkColorIndex) + } + } + for xOffset := 3; xOffset < 30; xOffset++ { + half := xOffset/2 - 2 + if half < 0 { + continue + } + x := tipX + xOffset + for y := cy - half; y <= cy+half; y++ { + setWheelBetaPixel(img, x, y, fillColorIndex) + } + } } -func drawWinnerCelebration(ctx *canvas.Context, step int) { +func drawWinnerCelebration(img *image.Paletted, step int) { if step < 0 { return } - cx := float64(wheelBetaSize) / 2 - cy := float64(wheelBetaSize) / 2 + cx, cy := wheelBetaSize/2, wheelBetaSize/2 phase := step % wheelBetaCelebrateFrames ringRadius := 34 + phase*5 if ringRadius < wheelBetaRadius-8 { - drawCircleOutline(ctx, cx, cy, float64(ringRadius), 1, wheelBetaSparkColorIndex) + drawCircleOutline(img, cx, cy, ringRadius, 1, wheelBetaSparkColorIndex) } for i := 0; i < 14; i++ { angle := (float64(i)/14)*2*math.Pi + float64(phase)*0.31 inner := float64(wheelBetaRadius + 9 + phase%3) outer := inner + 7 + float64(phase%4) - x1 := cx + math.Cos(angle)*inner - y1 := cy + math.Sin(angle)*inner - x2 := cx + math.Cos(angle)*outer - y2 := cy + math.Sin(angle)*outer + x1 := cx + int(math.Round(math.Cos(angle)*inner)) + y1 := cy + int(math.Round(math.Sin(angle)*inner)) + x2 := cx + int(math.Round(math.Cos(angle)*outer)) + y2 := cy + int(math.Round(math.Sin(angle)*outer)) colorIndex := wheelBetaSliceColorIndexes[(i+phase)%len(wheelBetaSliceColorIndexes)] if i%5 == 0 { colorIndex = wheelBetaSparkColorIndex } - drawWheelBetaLine(ctx, x1, y1, x2, y2, 2, colorIndex) - drawSpark(ctx, x2, y2, colorIndex) + drawPalettedLine(img, x1, y1, x2, y2, colorIndex) + drawSpark(img, x2, y2, colorIndex) } } -func drawStatusBand(ctx *canvas.Context, label, value string) { - drawWheelBetaRect(ctx, 31, 235, float64(wheelBetaSize-25), 286, wheelBetaShadowColorIndex) - drawWheelBetaRect(ctx, 28, 230, float64(wheelBetaSize-28), 282, wheelBetaPaperColorIndex) - drawWheelBetaLine(ctx, 28, 230, float64(wheelBetaSize-28), 230, 1, wheelBetaHighlightColorIndex) - drawWheelBetaLine(ctx, 28, 281, float64(wheelBetaSize-28), 281, 1, wheelBetaBevelColorIndex) - drawCenteredText(ctx, label, 240, wheelBetaInkColorIndex) - drawCenteredText(ctx, value, 260, wheelBetaInkColorIndex) +func drawStatusBand(img *image.Paletted, label, value string) { + for y := 235; y < 286; y++ { + for x := 31; x < wheelBetaSize-25; x++ { + img.SetColorIndex(x, y, wheelBetaShadowColorIndex) + } + } + for y := 230; y < 282; y++ { + for x := 28; x < wheelBetaSize-28; x++ { + img.SetColorIndex(x, y, wheelBetaPaperColorIndex) + } + } + for x := 28; x < wheelBetaSize-28; x++ { + img.SetColorIndex(x, 230, wheelBetaHighlightColorIndex) + img.SetColorIndex(x, 281, wheelBetaBevelColorIndex) + } + drawCenteredText(img, label, 250, wheelBetaInkColorIndex) + drawCenteredText(img, value, 270, wheelBetaInkColorIndex) } -func drawCenteredText(ctx *canvas.Context, text string, topY float64, colorIndex byte) { - drawCenteredTextAt(ctx, text, float64(wheelBetaSize)/2, topY, colorIndex) +func drawCenteredText(img *image.Paletted, text string, baselineY int, colorIndex byte) { + drawCenteredTextAt(img, text, wheelBetaSize/2, baselineY, colorIndex) } -func drawCenteredTextAt(ctx *canvas.Context, text string, centerX, topY float64, colorIndex byte) { - face := newWheelBetaTextFace(colorIndex) - ctx.DrawText(centerX, topY, canvas.NewTextLine(face, text, canvas.Center)) +func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int, colorIndex byte) { + face := basicfont.Face7x13 + width := font.MeasureString(face, text).Ceil() + x := centerX - width/2 + drawer := font.Drawer{ + Dst: img, + Src: image.NewUniform(wheelBetaPalette[colorIndex]), + Face: face, + Dot: fixed.P(x, baselineY), + } + drawer.DrawString(text) } -func drawRotatedCenteredTextAt(ctx *canvas.Context, text string, centerX, centerY, angle float64, colorIndex byte) { - face := newWheelBetaTextFace(colorIndex) - topY := centerY - face.Metrics().LineHeight/2 - ctx.Push() - ctx.RotateAbout(angle*180/math.Pi, centerX, centerY) - ctx.DrawText(centerX, topY, canvas.NewTextLine(face, text, canvas.Center)) - ctx.Pop() +func drawRotatedCenteredTextAt(img *image.Paletted, text string, centerX, centerY int, angle float64, colorIndex byte) { + face := basicfont.Face7x13 + padding := 2 + metrics := face.Metrics() + textWidth := font.MeasureString(face, text).Ceil() + textHeight := metrics.Height.Ceil() + mask := image.NewAlpha(image.Rect(0, 0, textWidth+padding*2, textHeight+padding*2)) + drawer := font.Drawer{ + Dst: mask, + Src: image.NewUniform(color.Alpha{A: 255}), + Face: face, + Dot: fixed.P(padding, padding+metrics.Ascent.Ceil()), + } + drawer.DrawString(text) + + sourceCenterX := float64(mask.Bounds().Dx()-1) / 2 + sourceCenterY := float64(mask.Bounds().Dy()-1) / 2 + sin, cos := math.Sin(angle), math.Cos(angle) + for y := mask.Bounds().Min.Y; y < mask.Bounds().Max.Y; y++ { + for x := mask.Bounds().Min.X; x < mask.Bounds().Max.X; x++ { + if mask.AlphaAt(x, y).A == 0 { + continue + } + localX := float64(x) - sourceCenterX + localY := float64(y) - sourceCenterY + targetX := centerX + int(math.Round(localX*cos-localY*sin)) + targetY := centerY + int(math.Round(localX*sin+localY*cos)) + setWheelBetaPixel(img, targetX, targetY, colorIndex) + } + } } func wheelBetaDisplayText(s string, limit int) string { var b strings.Builder count := 0 - for _, r := range s { + for _, r := range norm.NFD.String(s) { + if unicode.Is(unicode.Mn, r) { + continue + } if count >= limit { break } + r = wheelBetaASCIIRune(r) if unicode.IsControl(r) { b.WriteByte('?') count++ continue } + if r < 32 || r > 126 { + b.WriteByte('?') + count++ + continue + } b.WriteRune(r) count++ } @@ -266,35 +280,65 @@ func wheelBetaDisplayText(s string, limit int) string { return out } -func drawSpark(ctx *canvas.Context, cx, cy float64, colorIndex byte) { - drawWheelBetaLine(ctx, cx-2, cy, cx+2, cy, 1, colorIndex) - drawWheelBetaLine(ctx, cx, cy-2, cx, cy+2, 1, colorIndex) +func wheelBetaASCIIRune(r rune) rune { + switch r { + case 'đ': + return 'd' + case 'Đ': + return 'D' + default: + return r + } } -func drawWheelBetaLine(ctx *canvas.Context, x0, y0, x1, y1, width float64, colorIndex byte) { - ctx.SetFillColor(canvas.Transparent) - ctx.SetStrokeColor(wheelBetaPalette[colorIndex]) - ctx.SetStrokeWidth(width) - path := &canvas.Path{} - path.MoveTo(x0, y0) - path.LineTo(x1, y1) - ctx.DrawPath(0, 0, path) - ctx.Stroke() +func drawSpark(img *image.Paletted, cx, cy int, colorIndex byte) { + for y := cy - 1; y <= cy+1; y++ { + for x := cx - 1; x <= cx+1; x++ { + if x == cx || y == cy { + setWheelBetaPixel(img, x, y, colorIndex) + } + } + } } -func drawWheelBetaRect(ctx *canvas.Context, x0, y0, x1, y1 float64, colorIndex byte) { - path := &canvas.Path{} - path.MoveTo(x0, y0) - path.LineTo(x1, y0) - path.LineTo(x1, y1) - path.LineTo(x0, y1) - path.Close() - drawWheelBetaPath(ctx, path, colorIndex) +func drawPalettedLine(img *image.Paletted, x0, y0, x1, y1 int, colorIndex byte) { + dx := absInt(x1 - x0) + sx := -1 + if x0 < x1 { + sx = 1 + } + dy := -absInt(y1 - y0) + sy := -1 + if y0 < y1 { + sy = 1 + } + err := dx + dy + for { + setWheelBetaPixel(img, x0, y0, colorIndex) + if x0 == x1 && y0 == y1 { + return + } + e2 := 2 * err + if e2 >= dy { + err += dy + x0 += sx + } + if e2 <= dx { + err += dx + y0 += sy + } + } } -func drawWheelBetaPath(ctx *canvas.Context, path *canvas.Path, colorIndex byte) { - ctx.SetStrokeColor(canvas.Transparent) - ctx.SetFillColor(wheelBetaPalette[colorIndex]) - ctx.DrawPath(0, 0, path) - ctx.Fill() +func setWheelBetaPixel(img *image.Paletted, x, y int, colorIndex byte) { + if image.Pt(x, y).In(img.Rect) { + img.SetColorIndex(x, y, colorIndex) + } +} + +func absInt(v int) int { + if v < 0 { + return -v + } + return v } diff --git a/plans/260706-1448-wheel-beta-canvas-renderer/phase-01-add-canvas-dependency.md b/plans/260706-1448-wheel-beta-canvas-renderer/phase-01-add-canvas-dependency.md deleted file mode 100644 index 72423ff..0000000 --- a/plans/260706-1448-wheel-beta-canvas-renderer/phase-01-add-canvas-dependency.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -phase: 1 -title: Add Canvas Dependency -status: completed -effort: '' -priority: P2 -dependencies: [] ---- - -# Phase 1: Add Canvas Dependency - -## Overview - -Add `github.com/tdewolff/canvas` as the renderer dependency and inspect its local API surface enough to choose the smallest integration path. - -## Requirements - -- Functional: project builds with the new dependency available to `internal/modules/misc`. -- Non-functional: avoid GUI/game/native-runtime dependencies; keep dependency addition Go-only. - -## Architecture - -Dependency is used only by the wheel beta renderer. GIF encoding remains in `image/gif`; canvas is only responsible for drawing frame imagery into a Go image. - -## Related Code Files - -- Modify: `go.mod` -- Modify: `go.sum` -- Read: module docs/source under Go module cache as needed. - -## Implementation Steps - -1. Run `go get github.com/tdewolff/canvas@latest`. -2. Inspect the installed package APIs for raster image rendering and font/text support. -3. Record any API constraints directly in implementation notes or comments only if useful. - -## Success Criteria - -- [x] `go.mod` includes `github.com/tdewolff/canvas`. -- [x] No native runtime package such as raylib/cairo/skia is introduced. -- [x] `go test ./internal/modules/misc -run TestWheelOfNamesBeta_RenderGIFTiming -count=1` still compiles or fails only on not-yet-migrated code. - -## Risk Assessment - -Risk: canvas latest is pseudo-version and API may differ from docs. Mitigation: inspect local package docs/source before editing renderer. diff --git a/plans/260706-1448-wheel-beta-canvas-renderer/phase-02-migrate-renderer.md b/plans/260706-1448-wheel-beta-canvas-renderer/phase-02-migrate-renderer.md deleted file mode 100644 index 680a5a9..0000000 --- a/plans/260706-1448-wheel-beta-canvas-renderer/phase-02-migrate-renderer.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -phase: 2 -title: Migrate Renderer -status: completed -effort: '' -priority: P1 -dependencies: - - 1 ---- - -# Phase 2: Migrate Renderer - -## Overview - -Move wheel beta frame drawing onto `tdewolff/canvas` while retaining the current frame-generation functions and observable command behavior. - -## Requirements - -- Functional: `renderWheelBetaFrame` returns frames with same size, palette expectations, pointer alignment, status band, labels, and celebration states. -- Non-functional: keep renderer headless, deterministic, and local to misc module. - -## Architecture - -The spin profile and GIF assembly stay unchanged. The per-frame renderer creates a canvas-backed raster image, draws the wheel using paths/text/transforms, converts the result to the existing GIF palette, then returns `*image.Paletted` for `gif.EncodeAll`. - -```text -spin profile -> canvas frame renderer -> paletted frame -> image/gif.EncodeAll -``` - -## Related Code Files - -- Modify: `internal/modules/misc/wheelofnames_beta.go` -- Modify: `internal/modules/misc/wheelofnames_beta_drawing.go` -- Modify: `internal/modules/misc/handlers_test.go` only if internal rendering tests need adaptation. - -## Implementation Steps - -1. Add canvas frame creation and raster-to-paletted conversion helpers. -2. Port slices, rim, pointer, highlight, status band, title/status text, labels, and celebration drawing to canvas-backed helpers. -3. Preserve current constants where possible so tests and behavior remain stable. -4. Keep `renderWheelBetaFrame`, `renderWheelBetaFrameWithStatus`, and `renderWheelBetaFrameWithCelebration` call sites intact. -5. Remove obsolete manual text-mask helpers only if no tests depend on them; otherwise update tests to target observable behavior. - -## Success Criteria - -- [x] Existing GIF timing/frame transition tests pass. -- [x] Existing pointer and winner slice tests pass. -- [x] Existing label placement/rotation and Vietnamese glyph tests pass. -- [x] No command registration, caption, filename, dimensions, or duration changes. - -## Risk Assessment - -Risk: palette conversion can shift exact color indexes and break tests. Mitigation: keep final paletted frame using `wheelBetaPalette` and adjust only tests that were over-coupled to manual-pixel internals. - -Risk: canvas text APIs may require font face plumbing that is heavier than expected. Mitigation: keep embedded font bytes and use canvas text/font APIs only within misc renderer. diff --git a/plans/260706-1448-wheel-beta-canvas-renderer/phase-03-validate-behavior.md b/plans/260706-1448-wheel-beta-canvas-renderer/phase-03-validate-behavior.md deleted file mode 100644 index 1718143..0000000 --- a/plans/260706-1448-wheel-beta-canvas-renderer/phase-03-validate-behavior.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -phase: 3 -title: Validate Behavior -status: completed -effort: '' -priority: P1 -dependencies: - - 2 ---- - -# Phase 3: Validate Behavior - -## Overview - -Verify the canvas-backed renderer preserves command behavior and does not introduce lint, vet, or repo-wide test failures. - -## Requirements - -- Functional: all acceptance criteria from `plan.md` are verified by tests or code inspection. -- Non-functional: lint and vet pass before any commit. - -## Architecture - -Validation centers on existing misc tests plus repo-wide Go gates because command behavior and renderer internals share misc package files. - -## Related Code Files - -- Read/verify: `internal/modules/misc/handlers_test.go` -- Read/verify: `internal/modules/misc/wheelofnames_beta*.go` -- Read/verify: `go.mod` - -## Implementation Steps - -1. Run focused wheel beta tests first. -2. Run full misc package tests. -3. Run `go test ./...`. -4. Run `go vet ./...`. -5. Run `golangci-lint run`. -6. Review diff for public contract changes and unintended docs/command surface changes. - -## Success Criteria - -- [x] Focused wheel beta tests pass. -- [x] `go test ./internal/modules/misc -count=1` passes. -- [x] `go test ./...` passes. -- [x] `go vet ./...` passes. -- [x] `golangci-lint run` passes. -- [x] Diff contains no command rename, menu, storage, or Telegram payload contract changes. - -## Risk Assessment - -Risk: transitive dependencies increase lint/test time. Mitigation: measure via standard gates and keep changes scoped. diff --git a/plans/260706-1448-wheel-beta-canvas-renderer/plan.md b/plans/260706-1448-wheel-beta-canvas-renderer/plan.md deleted file mode 100644 index f71b845..0000000 --- a/plans/260706-1448-wheel-beta-canvas-renderer/plan.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Wheel Beta Canvas Renderer -description: >- - Migrate /wheelofnamesbeta frame rendering from manual pixel drawing to - github.com/tdewolff/canvas while preserving Telegram behavior. -status: completed -priority: P2 -branch: main -tags: - - feature - - backend - - graphics -blockedBy: [] -blocks: [] -created: '2026-07-06T07:48:34.819Z' -createdBy: 'ck:plan' -source: skill ---- - -# Wheel Beta Canvas Renderer - -## Overview - -Migrate `/wheelofnamesbeta` frame rendering to `github.com/tdewolff/canvas` so wheel slices, text, highlights, rim, pointer, and celebration effects are drawn through a higher-level vector renderer. Keep the existing Telegram command contract and stdlib GIF encoding. - -Expected output: `/wheelofnamesbeta ` still sends a 320x320 animated GIF named `wheelofnamesbeta.gif`, but frames are generated through canvas-backed rendering instead of manual low-level pixel/text stamping. - -Acceptance criteria: -- GIF frame count, loop count, delays, duration, width, and height stay unchanged. -- Winner/current option tracking remains aligned with the 3h pointer. -- Vietnamese option text still renders without `?` glyph replacement. -- Slice labels still rotate with their slices and appear inside the wheel. -- `/wheelofnamesbeta` send behavior and caption stay unchanged. -- Focused misc tests, `go test ./...`, `go vet ./...`, and `golangci-lint run` pass. - -Scope boundary: -- Do not add `gifski`, ffmpeg, shell commands, cgo graphics stacks, or external runtime binaries. -- Do not change command names, command menu metadata, Telegram API payload shape, or spin-profile math. -- Do not refactor unrelated misc commands. - -Constraints: -- Go-only/headless deployment for Coolify. -- Keep embedded DejaVu Sans font use; no runtime font discovery. -- Keep changes localized to `internal/modules/misc` and module dependency files unless tests require otherwise. -- Preserve public/internal function signatures where tests and command code depend on them. - -Scout context: -- Project is a Go 1.25 Telegram bot with pluggable modules under `internal/modules`. -- Current wheel beta code lives in `internal/modules/misc/wheelofnames_beta.go`, `wheelofnames_beta_drawing.go`, `wheelofnames_beta_spin_profile.go`, and `wheelofnames_beta_command.go`. -- Existing tests in `internal/modules/misc/handlers_test.go` cover GIF timing, frame transitions, pointer alignment, label placement/rotation, Vietnamese glyph support, and Telegram send payloads. -- Prior completed plan `plans/260703-0404-wheelofnames-misc-command` is complete and non-blocking. -- Research report `plans/reports/260706-1441-wheel-beta-gif-rendering-library-research.md` recommends `tdewolff/canvas` as renderer and keeping `image/gif` first. - -## Phases - -| Phase | Name | Status | -|-------|------|--------| -| 1 | [Add Canvas Dependency](./phase-01-add-canvas-dependency.md) | Completed | -| 2 | [Migrate Renderer](./phase-02-migrate-renderer.md) | Completed | -| 3 | [Validate Behavior](./phase-03-validate-behavior.md) | Completed | - -## Dependencies - -Cross-plan dependencies: none. - -Implementation dependencies: -- Add `github.com/tdewolff/canvas` via `go get`. -- Continue using stdlib `image/gif` for animation encoding. -- Continue using current embedded `internal/modules/misc/fonts/dejavu-sans.ttf`. - -## Touchpoints - -| File | Action | -|---|---| -| `go.mod` / `go.sum` | Add canvas module and transitive dependency metadata. | -| `internal/modules/misc/wheelofnames_beta.go` | Route frame generation through canvas-backed renderer while preserving GIF contract. | -| `internal/modules/misc/wheelofnames_beta_drawing.go` | Replace or reduce manual draw helpers with canvas equivalents where practical. | -| `internal/modules/misc/handlers_test.go` | Adjust/add tests only where rendering internals legitimately change. | - -## Verification - -- `go test ./internal/modules/misc -count=1` -- `go test ./...` -- `go vet ./...` -- `golangci-lint run` diff --git a/plans/reports/260706-1441-wheel-beta-gif-rendering-library-research.md b/plans/reports/260706-1441-wheel-beta-gif-rendering-library-research.md deleted file mode 100644 index 38ce8d6..0000000 --- a/plans/reports/260706-1441-wheel-beta-gif-rendering-library-research.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -type: research-report -created: 2026-07-06 14:41 UTC -topic: wheel beta gif rendering library options -status: complete ---- - -# Research Report: Wheel Beta GIF Rendering Library - -## Summary - -Best fit: use `github.com/tdewolff/canvas` for frame rendering, keep the current stdlib `image/gif` encoder first, and evaluate `gifski` only if GIF color quality remains poor. - -Reason: this bot runs server-side as a Go Telegram bot. The rendering stack should stay deterministic, headless, and low-ops. `canvas` gives higher-level vector drawing, affine transforms, text handling, gradients, and raster output without bringing a GUI/game runtime. `gifski` is excellent for final GIF encoding quality, but it adds CLI/C library operational complexity. - -Important correction: current code does not use `x/image` to encode GIFs. It uses standard `image/gif` for animation encoding and `golang.org/x/image/font/opentype` for font rasterization. Replacing direct `x/image` usage mostly means replacing our low-level text/glyph-mask drawing path. - -## Method - -- Scope: Go libraries suitable for rendering animated wheel GIF frames for `/wheelofnamesbeta`. -- Criteria: server deploy friction, text quality, transforms, gradients, animation frame pipeline, dependency size, license, maintenance, integration cost. -- Sources: project code, pkg.go.dev, GitHub READMEs, project homepages. -- Search limit: within `ck:research` 5-call max. - -## Current State - -Current wheel beta stack: - -```text -spin math -> render each frame into image.Paletted -> image/gif.EncodeAll - | - +-> direct pixel loops for slices, rim, pointer - +-> x/image/opentype for title/status/label text -``` - -Pain points: - -- Manual pixel loops make anti-aliasing and gradients awkward. -- Rotated text is custom mask stamping. -- Fixed 14-color palette restricts smooth highlights and anti-aliased text. -- Better animation feel is already mostly math; better visual output needs a higher-level renderer and possibly better quantization. - -## Recommendation - -### 1. Primary Renderer: `tdewolff/canvas` - -Use `github.com/tdewolff/canvas` for drawing each wheel frame into a raster image. - -Why: - -- It is a Go vector graphics library that outputs raster formats, SVG, PDF, EPS, and more. Its README describes it as a Cairo/node-canvas-style target with path manipulation and text formatting support. -- It supports text drawing through `Context.DrawText`, view transforms through `Context.Rotate`, and raster writers including PNG/GIF via `renderers`. -- The package is current enough for this repo: `go list -m -json github.com/tdewolff/canvas@latest` returned `v0.0.0-20260617131110-529326a1955e` with Go 1.25. -- It avoids GUI/display server concerns. - -Trade-offs: - -- More complex API than `gg`. -- Not v1/stable-tagged on pkg.go.dev. -- Full package has many imports; use only the subpackages needed. -- It may still use Go image-related packages internally. The value is removing our direct low-level glyph/pixel renderer, not purging every image dependency. - -Fit for wheel: - -```text -for each frame: - c := canvas.New(widthMM, heightMM) - ctx := canvas.NewContext(c) - draw slices as paths/arcs - draw labels with text object + rotated view - draw pointer, rim, status band - rasterize to RGBA/paletted image -append frame to GIF -``` - -### 2. Keep `image/gif` Initially - -Keep `image/gif.EncodeAll` for first migration. It is already working, tested, and simple. - -Change only the frame source first: - -```text -old: renderWheelBetaFrame(...) *image.Paletted -new: renderWheelBetaFrameRGBA(...) *image.RGBA - palettize frame - gif.EncodeAll(...) -``` - -If anti-aliasing is added, the fixed 14-color palette will limit quality. Move to a larger 128/256-color palette before introducing external encoders. - -### 3. Optional Encoder Upgrade: `gifski` - -Use `gifski` only if we specifically need better GIF color quality after renderer migration. - -Why: - -- `gifski` is designed for high-quality GIF conversion. Its docs highlight pngquant palettes, temporal dithering, lossy LZW, temporal smoothing, and denoising. -- It accepts PNG frames through CLI, and can also be compiled as a C library. - -Why not first: - -- Adds external binary or C/Rust/FFI complexity. -- More moving parts in Coolify/self-host deploy. -- Current animation is only 320x320 and simple; the standard GIF pipeline may be good enough with a better renderer and palette. - -## Comparison - -| Option | Role | Pros | Cons | Verdict | -|---|---|---|---|---| -| `tdewolff/canvas` | Vector renderer | Good transforms, paths, text, gradients, raster targets, current Go metadata | Larger API/import surface, no v1 tag | Best primary renderer | -| `fogleman/gg` | Simple 2D RGBA renderer | Very easy API, anchored text, transforms, gradients | Last tagged release 2019; still uses font/image stack; less advanced text/layout | Good fallback for quick prototype | -| `llgcode/draw2d` | Canvas-style vector renderer | Pure Go, affine transforms, text, image/PDF/SVG backends | Older API style, less compelling than `canvas` for text/layout | Acceptable but not first choice | -| `gifski` | GIF encoder | Best output quality, temporal dithering, PNG-frame input | External binary/C library operational cost | Optional phase 2 encoder | -| `raylib-go` | Game/graphics runtime | Mature 2D/game APIs and animation examples | OpenGL/window/runtime requirements, first build can be heavy, poor fit for headless bot | Avoid | -| Cairo/Skia bindings | Native renderer | Excellent rendering quality | C/C++ deps, cgo/build complexity | Avoid unless Go-native options fail | - -## Implementation Path - -1. Add a renderer seam: - -```go -type wheelBetaFrameRenderer interface { - Render(options []string, winner int, rotation float64, reveal bool, status string, celebrateStep int) image.Image -} -``` - -2. Keep current renderer as baseline. - -3. Add `canvas` renderer behind a small internal function, not public command behavior. - -4. Preserve current animation contract: - -- same frame count -- same delay array -- same final winner alignment at 3h pointer -- same Telegram `sendAnimation` behavior -- same Vietnamese glyph coverage - -5. Add visual-focused tests: - -- GIF decodes and has expected frame count/delay. -- Result frame differs from last spin frame. -- Text appears near expected slice label centers. -- Current/winner index still tracks pointer. -- Optional golden-ish image metrics: count non-background pixels, pointer bounds, label bounds. - -6. Only after renderer looks better, decide whether encoder upgrade is needed: - -- If color banding/text edges remain bad: evaluate 256-color palette and `gifski`. -- If file size too large: tune frame count, palette, or consider MP4 upload if acceptable for Telegram. - -## Code Sketch - -```go -func renderWheelOfNamesBetaGIF(options []string, winner int) ([]byte, error) { - frames := make([]*image.Paletted, 0, wheelBetaSpinFrames+wheelBetaHoldFrames) - delays := make([]int, 0, cap(frames)) - - renderer := newWheelBetaCanvasRenderer() - profile := newWheelBetaSpinProfile(len(options), winner, nil) - - for i := 0; i < wheelBetaSpinFrames; i++ { - t := float64(i) / float64(wheelBetaSpinFrames-1) - rgba := renderer.Render(options, winner, profile.rotationAt(t), false, profile.statusAt(t), -1) - frames = append(frames, palettizeWheelFrame(rgba)) - delays = append(delays, wheelBetaSpinDelay) - } - - // hold frames unchanged - return encodeWheelGIF(frames, delays) -} -``` - -Keep this as an internal refactor; no Telegram command contract change. - -## Security And Operations - -- Prefer pure Go. Avoid spawning shell commands from message handlers unless strongly justified. -- If using `gifski` CLI later, never pass user-controlled paths or shell strings; write temp frames in a controlled temp dir and execute with `exec.Command` args. -- Watch memory: 50 frames at 320x320 RGBA is about 20 MB before GIF/palette overhead. Fine, but avoid retaining extra PNG buffers if using `gifski`. -- Avoid runtime font discovery. Continue embedding font bytes in the Go package. - -## Source Notes - -- `tdewolff/canvas` README: vector graphics target with raster output and text formatting; can output PNG/JPG/GIF and other formats. Source: https://github.com/tdewolff/canvas -- `tdewolff/canvas/renderers`: GIF writer accepts `canvas.Resolution`, `canvas.Colorspace`, and `image/gif.*Options`; `Write` supports `.gif`, `.png`, `.webp`, `.svg`, `.pdf`, and more. Source: https://pkg.go.dev/github.com/tdewolff/canvas/renderers -- `tdewolff/canvas` pkg metadata: latest pseudo-version published 2026-06-17, MIT, Go 1.25. Source: https://pkg.go.dev/github.com/tdewolff/canvas -- `fogleman/gg`: creates `image.RGBA` contexts, supports text drawing, anchored text, gradients, transforms, and rotated drawing helpers. Source: https://pkg.go.dev/github.com/fogleman/gg and https://github.com/fogleman/gg -- `draw2d`: supports image/PDF/OpenGL/SVG outputs, text rendering with TrueType fonts, and affine transforms. Source: https://pkg.go.dev/github.com/llgcode/draw2d and https://github.com/llgcode/draw2d -- `gifski`: uses pngquant palettes, temporal dithering, lossy LZW compression, smoothing/denoising; supports CLI and C library integration. Source: https://gif.ski/ and https://github.com/ImageOptim/gifski -- `raylib-go`: game-oriented Go bindings; includes raylib C source and has Linux graphics dependencies or embedded native libraries depending mode. Source: https://github.com/gen2brain/raylib-go -- Cairo: strong renderer but C library/bindings; supports affine transforms and antialiased text, but increases native dependency surface. Source: https://www.cairographics.org/ - -## Next Steps - -1. Prototype `tdewolff/canvas` in a small internal spike rendering one static wheel frame to PNG/GIF. -2. Compare output against current renderer at 320x320 and maybe 640x640 downsampled. -3. If quality is clearly better and build/test cost acceptable, migrate `/wheelofnamesbeta` frame rendering. -4. Only then evaluate `gifski` or broader GIF quantization. - -## Unresolved Questions - -None.