mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-26 20:20:15 +00:00
fix(misc): restore wheel beta renderer
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 <options>` 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`
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user