diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 526fd50..d7d035f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,2 +1,3 @@ # Contributing + Please create pull requests with descriptive names. diff --git a/code-examples/LICENSE b/LICENSE similarity index 100% rename from code-examples/LICENSE rename to LICENSE diff --git a/README.md b/README.md index 1e5ca3e..9a6cb95 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# oakgrove -Placeholder for [oak](https://github.com/oakmoundstudio/oak) examples that may have dependencies/assets that would bloat the oak package. +# Grove + +Grove is a grab-bag of extended components, examples, and utilities for developing programs with [Oak](https://www.github.com/oakmound/oak). + +## Contributing -# contributing See contributing.md for specifics but pretty much all contributions are welcomed happily. -# licensing +## Licensing + To illustrate what can be done in oak in a positive light this repository includes art for the examples. -To be able to use certain art it was placed under [a different path](assets/) with a corresponding slightly less permissive license. + +Assets in this repository are covered by the license defined within the assets directory. Code in this repository is covered by the top level license file. diff --git a/components/README.md b/components/README.md new file mode 100644 index 0000000..18b768b --- /dev/null +++ b/components/README.md @@ -0,0 +1,4 @@ +# Components + +This directory contains common components that have been found useful in developing Oak games and applications. +Please note that they are not held as closely to the backwards compatibility as packages in Oak \ No newline at end of file diff --git a/components/fonthelper/README.md b/components/fonthelper/README.md new file mode 100644 index 0000000..4c3b657 --- /dev/null +++ b/components/fonthelper/README.md @@ -0,0 +1,3 @@ +# fonthelper + +The fonthelper package contains common font utilities. diff --git a/components/fonthelper/go.mod b/components/fonthelper/go.mod new file mode 100644 index 0000000..bedf920 --- /dev/null +++ b/components/fonthelper/go.mod @@ -0,0 +1,11 @@ +module github.com/oakmound/grove/components/fonthelper + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect +) diff --git a/components/fonthelper/go.sum b/components/fonthelper/go.sum new file mode 100644 index 0000000..d9aba2c --- /dev/null +++ b/components/fonthelper/go.sum @@ -0,0 +1,49 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/fonthelper/regenerators.go b/components/fonthelper/regenerators.go new file mode 100644 index 0000000..d17d225 --- /dev/null +++ b/components/fonthelper/regenerators.go @@ -0,0 +1,33 @@ +package fonthelper + +import ( + "image" + "image/color" + + "github.com/oakmound/oak/v3/render" +) + +// WithSize sets size on a font +func WithSize(size float64) func(render.FontGenerator) render.FontGenerator { + return func(f render.FontGenerator) render.FontGenerator { + f.Size = size + return f + } +} + +// WithColor sets the color on a font +func WithColor(c color.Color) func(render.FontGenerator) render.FontGenerator { + return func(f render.FontGenerator) render.FontGenerator { + f.Color = image.NewUniform(c) + return f + } +} + +// WithSizeAndColor sets the size and color on a font +func WithSizeAndColor(size float64, c color.Color) func(render.FontGenerator) render.FontGenerator { + return func(f render.FontGenerator) render.FontGenerator { + f.Color = image.NewUniform(c) + f.Size = size + return f + } +} diff --git a/components/intswitch/README.md b/components/intswitch/README.md new file mode 100644 index 0000000..b71c6fa --- /dev/null +++ b/components/intswitch/README.md @@ -0,0 +1,3 @@ +# intswitch + +The intswitch package contains a `render.Switch` alternative keyed on `int`s instead of `string`s. diff --git a/components/intswitch/go.mod b/components/intswitch/go.mod new file mode 100644 index 0000000..8a2d717 --- /dev/null +++ b/components/intswitch/go.mod @@ -0,0 +1,11 @@ +module github.com/oakmound/grove/components/intswitch + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect +) diff --git a/components/intswitch/go.sum b/components/intswitch/go.sum new file mode 100644 index 0000000..d9aba2c --- /dev/null +++ b/components/intswitch/go.sum @@ -0,0 +1,49 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/intswitch/switch.go b/components/intswitch/switch.go new file mode 100644 index 0000000..f81e46f --- /dev/null +++ b/components/intswitch/switch.go @@ -0,0 +1,224 @@ +package intswitch + +import ( + "image" + "image/draw" + "sync" + "strconv" + + "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/oakerr" + "github.com/oakmound/oak/v3/physics" + "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v3/render/mod" +) + +var _ render.Modifiable = &Switch{} + +// The Switch type will display one of a set of modifiable sub-components, +// keyed on integers. Consider using it with bitflags. +type Switch struct { + render.LayeredPoint + subRenderables map[int]render.Modifiable + Index int + lock sync.RWMutex +} + +// New creates a new Switch from a map of values to modifiables +func New(start int, m map[int]render.Modifiable) *Switch { + return &Switch{ + LayeredPoint: render.NewLayeredPoint(0, 0, 0), + subRenderables: m, + Index: start, + lock: sync.RWMutex{}, + } +} + +// Add makes a new entry in the Switch's map. If the key already +// existed, it will be overwritten and an error will be returned. +func (c *Switch) Add(k int, v render.Modifiable) (err error) { + if _, ok := c.subRenderables[k]; ok { + err = oakerr.ExistingElement{ + InputName: "k", + InputType: "int", + Overwritten: true, + } + } + c.lock.Lock() + c.subRenderables[k] = v + c.lock.Unlock() + return err +} + +// Set sets the current renderable to the one specified +func (c *Switch) Set(k int) error { + c.lock.RLock() + if _, ok := c.subRenderables[k]; !ok { + return oakerr.NotFound{InputName: "k:" + strconv.Itoa(k)} + } + c.lock.RUnlock() + c.Index = k + return nil +} + +// GetSub returns a keyed Modifiable from this Switch's map +func (c *Switch) GetSub(s int) render.Modifiable { + c.lock.RLock() + m := c.subRenderables[s] + c.lock.RUnlock() + return m +} + +// Get returns the Switch's current key +func (c *Switch) Get() int { + return c.Index +} + +// SetOffsets sets the logical offset for the specified key +func (c *Switch) SetOffsets(k int, offsets physics.Vector) { + c.lock.RLock() + if r, ok := c.subRenderables[k]; ok { + r.SetPos(offsets.X(), offsets.Y()) + } + c.lock.RUnlock() +} + +// Copy creates a copy of the Switch +func (c *Switch) Copy() render.Modifiable { + newC := new(Switch) + newC.LayeredPoint = c.LayeredPoint.Copy() + newSubRenderables := make(map[int]render.Modifiable) + c.lock.RLock() + for k, v := range c.subRenderables { + newSubRenderables[k] = v.Copy() + } + c.lock.RUnlock() + newC.subRenderables = newSubRenderables + newC.Index = c.Index + newC.lock = sync.RWMutex{} + return newC +} + +//GetRGBA returns the current renderables rgba +func (c *Switch) GetRGBA() *image.RGBA { + c.lock.RLock() + rgba := c.subRenderables[c.Index].GetRGBA() + c.lock.RUnlock() + return rgba +} + +// Modify performs the input modifications on all elements of the Switch +func (c *Switch) Modify(ms ...mod.Mod) render.Modifiable { + c.lock.RLock() + for _, r := range c.subRenderables { + r.Modify(ms...) + } + c.lock.RUnlock() + return c +} + +// Filter filters all elements of the Switch with fs +func (c *Switch) Filter(fs ...mod.Filter) { + c.lock.RLock() + for _, r := range c.subRenderables { + r.Filter(fs...) + } + c.lock.RUnlock() +} + +//Draw draws the Switch at an offset from its logical location +func (c *Switch) Draw(buff draw.Image, xOff float64, yOff float64) { + c.lock.RLock() + c.subRenderables[c.Index].Draw(buff, c.X()+xOff, c.Y()+yOff) + c.lock.RUnlock() +} + +// ShiftPos shifts the Switch's logical position +func (c *Switch) ShiftPos(x, y float64) { + c.SetPos(c.X()+x, c.Y()+y) +} + +// GetDims gets the current Renderables dimensions +func (c *Switch) GetDims() (int, int) { + c.lock.RLock() + w, h := c.subRenderables[c.Index].GetDims() + c.lock.RUnlock() + return w, h +} + +// Pause stops the current Renderable if possible +func (c *Switch) Pause() { + c.lock.RLock() + if cp, ok := c.subRenderables[c.Index].(render.CanPause); ok { + cp.Pause() + } + c.lock.RUnlock() +} + +// Unpause tries to unpause the current Renderable if possible +func (c *Switch) Unpause() { + c.lock.RLock() + if cp, ok := c.subRenderables[c.Index].(render.CanPause); ok { + cp.Unpause() + } + c.lock.RUnlock() +} + +// IsInterruptable returns whether the current renderable is interruptable +func (c *Switch) IsInterruptable() bool { + c.lock.RLock() + defer c.lock.RUnlock() + if i, ok := c.subRenderables[c.Index].(render.NonInterruptable); ok { + return i.IsInterruptable() + } + return true +} + +// IsStatic returns whether the current renderable is static +func (c *Switch) IsStatic() bool { + c.lock.RLock() + defer c.lock.RUnlock() + if s, ok := c.subRenderables[c.Index].(render.NonStatic); ok { + return s.IsStatic() + } + return true +} + +// SetTriggerID sets the ID AnimationEnd will trigger on for animating subtypes. +// Todo: standardize this with the other interface Set functions so that it +// also only acts on the current subRenderable, or the other way around, or +// somehow offer both options +func (c *Switch) SetTriggerID(cid event.CID) { + c.lock.RLock() + for _, r := range c.subRenderables { + if t, ok := r.(render.Triggerable); ok { + t.SetTriggerID(cid) + } + } + c.lock.RUnlock() +} + +// Revert will revert all parts of this Switch that can be reverted +func (c *Switch) Revert(mod int) { + c.lock.RLock() + for _, v := range c.subRenderables { + switch t := v.(type) { + case *render.Reverting: + t.Revert(mod) + } + } + c.lock.RUnlock() +} + +// RevertAll will revert all parts of this Switch that can be reverted, back +// to their original state. +func (c *Switch) RevertAll() { + c.lock.RLock() + for _, v := range c.subRenderables { + switch t := v.(type) { + case *render.Reverting: + t.RevertAll() + } + } + c.lock.RUnlock() +} \ No newline at end of file diff --git a/components/intswitch/switch_test.go b/components/intswitch/switch_test.go new file mode 100644 index 0000000..6e74956 --- /dev/null +++ b/components/intswitch/switch_test.go @@ -0,0 +1,18 @@ +package intswitch_test + +import ( + "github.com/oakmound/grove/components/intswitch" + "github.com/oakmound/oak/v3/render" + + "testing" + "image/color" +) + +func TestNew(t *testing.T) { + sw := intswitch.New(0, map[int]render.Modifiable{ + 0: render.NewColorBox(1,1,color.RGBA{255,0,0,255}), + }) + if sw == nil { + t.Fatal("expected non-nil switch") + } +} \ No newline at end of file diff --git a/components/keyhint/README.md b/components/keyhint/README.md new file mode 100644 index 0000000..0bbf162 --- /dev/null +++ b/components/keyhint/README.md @@ -0,0 +1,3 @@ +# keyhint + +The keyhint component contains constructors for bordered, colored text icons (hinting that a given key / button will trigger something) diff --git a/components/keyhint/go.mod b/components/keyhint/go.mod new file mode 100644 index 0000000..ef3c46e --- /dev/null +++ b/components/keyhint/go.mod @@ -0,0 +1,12 @@ +module github.com/oakmound/grove/components/keyhint + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect +) diff --git a/components/keyhint/go.sum b/components/keyhint/go.sum new file mode 100644 index 0000000..461e0e2 --- /dev/null +++ b/components/keyhint/go.sum @@ -0,0 +1,50 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/keyhint/keyhint.go b/components/keyhint/keyhint.go new file mode 100644 index 0000000..b69395a --- /dev/null +++ b/components/keyhint/keyhint.go @@ -0,0 +1,131 @@ +package keyhint + +import ( + "fmt" + "image" + "image/color" + "math" + + "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v3/entities/x/btn" + "github.com/oakmound/oak/v3/entities/x/mods" + "github.com/oakmound/oak/v3/render" +) + +// A KeyHint is a small, colored renderable displaying some text. The text +// is designed to be a key 'A,B,C,D' or controller button 'A,B,X,Y', and intended +// for use as a hint to imply that pressing that key or button will trigger something. +type KeyHint struct { + Options + render.Renderable +} + +type Options struct { + Key string + Color color.RGBA + BorderColor color.RGBA + Font *render.Font + Height int + // Todo: Fit text + Rounded bool +} + +func (o Options) setDefaults() Options { + if o.Key == "" { + o.Key = "A" + } + if o.Color == (color.RGBA{}) { + o.Color = color.RGBA{200, 0, 0, 255} + } + if o.BorderColor == (color.RGBA{}) { + o.BorderColor = color.RGBA{0, 0, 0, 255} + } + if o.Height == 0 { + o.Height = 35 + } + if o.Font == nil { + o.Font = render.DefaultFont() + } + return o +} + +func (o Options) Generate() *KeyHint { + o = o.setDefaults() + rgba := image.NewRGBA(image.Rect(0, 0, o.Height, o.Height)) + // color based on delta from center + center := intgeom.Point2{o.Height / 2, o.Height / 2} + for x := 0; x < o.Height; x++ { + for y := 0; y < o.Height; y++ { + pt := intgeom.Point2{x, y} + distance := 0.0 + if o.Rounded { + distance = center.Distance(pt) + } else { + // distance is greater of x or y delta + xDelta := math.Abs(float64(center.X() - pt.X())) + yDelta := math.Abs(float64(center.Y() - pt.Y())) + distance = xDelta + if yDelta > xDelta { + distance = yDelta + } + } + // color by distance: + distancePercent := distance / float64(o.Height/2) + var c color.Color = o.Color + if distancePercent > 1 { + continue + } else if distancePercent > (float64(o.Height)-0.99)/float64(o.Height) { + rC := o.BorderColor + rC.A /= 4 + rC.R /= 4 + rC.B /= 4 + rC.G /= 4 + c = rC + } else if distancePercent > (float64(o.Height)-1.99)/float64(o.Height) { + rC := o.BorderColor + rC.A /= 2 + rC.R /= 2 + rC.B /= 2 + rC.G /= 2 + c = rC + } else if distancePercent > (float64(o.Height)-2.99)/float64(o.Height) { + c = o.BorderColor + } else if distancePercent > (float64(o.Height)-3.99)/float64(o.Height) { + c = mix(o.BorderColor, mods.Darker(c, .2), .80) + } else if distancePercent > (float64(o.Height)-4.99)/float64(o.Height) { + c = mix(o.BorderColor, mods.Darker(c, .2), .50) + } else if distancePercent > .85 { + c = mods.Darker(c, .2) + } else if distancePercent > .65 { + c = mods.Lighter(c, distancePercent-.65) + } + rgba.Set(x, y, c) + } + } + backing := render.NewSprite(0, 0, rgba) + text := o.Font.NewText(o.Key, 0, 0) + tw, th := text.GetDims() + text.SetPos(float64(center.X()-tw/2), float64(center.Y()-th/2)-3) + comp := render.NewCompositeR(backing, text.ToSprite()) + return &KeyHint{ + Renderable: comp, + Options: o, + } +} + +func mix(c1, c2 color.Color, percent float64) color.Color { + r1, g1, b1, a1 := c1.RGBA() + r2, g2, b2, a2 := c2.RGBA() + return color.RGBA64{ + uint16(float64(r1)*(percent) + float64(r2)*(1-percent)), + uint16(float64(g1)*(percent) + float64(g2)*(1-percent)), + uint16(float64(b1)*(percent) + float64(b2)*(1-percent)), + uint16(float64(a1)*(percent) + float64(a2)*(1-percent)), + } +} + +func AlignHintToButton(kh *KeyHint, b btn.Btn) { + r := b.GetRenderable() + w, _ := r.GetDims() + kh.SetPos((r.X()+float64(w))-float64(kh.Height)*(3.0/4.0), r.Y()-(float64(kh.Height)*(1.0/4.0))) +} diff --git a/components/radar/README.md b/components/radar/README.md new file mode 100644 index 0000000..e8fb95a --- /dev/null +++ b/components/radar/README.md @@ -0,0 +1,3 @@ +# radar + +The radar component displays points of interest at a customizable scaled down ratio, for things like displaying points of interest on a minimap. diff --git a/components/radar/go.mod b/components/radar/go.mod new file mode 100644 index 0000000..a420b96 --- /dev/null +++ b/components/radar/go.mod @@ -0,0 +1,11 @@ +module github.com/oakmound/grove/components/radar + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect +) diff --git a/components/radar/go.sum b/components/radar/go.sum new file mode 100644 index 0000000..d9aba2c --- /dev/null +++ b/components/radar/go.sum @@ -0,0 +1,49 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/radar/radar.go b/components/radar/radar.go new file mode 100644 index 0000000..0a00fab --- /dev/null +++ b/components/radar/radar.go @@ -0,0 +1,127 @@ +package radar + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "sync" + + "github.com/oakmound/oak/v3/render" +) + +// Point is a utility function for location +type Point struct { + X, Y *float64 +} + +// Radar displays points of interest on a radar map +type Radar struct { + render.LayeredPoint + points map[Point]color.Color + pointLookup map[int]*Point + center Point + width, height int + r *image.RGBA + outline *render.Sprite + ratio float64 + sync.Mutex +} + +var ( + centerColor = color.RGBA{255, 255, 0, 255} +) + +// NewRadar creates a radar that will display at 0,0 with the given dimensions. +// The points given will be displayed on the radar relative to the center point, +// With the absolute distance reduced by the given ratio +func NewRadar(w, h int, points map[Point]color.Color, center Point, ratio float64) *Radar { + r := new(Radar) + r.LayeredPoint = render.NewLayeredPoint(0, 0, 0) + r.points = points + r.pointLookup = map[int]*Point{} + r.width = w + r.height = h + r.center = center + r.r = image.NewRGBA(image.Rect(0, 0, w, h)) + r.outline = render.NewColorBox(w, h, color.RGBA{0, 0, 125, 125}) + r.ratio = ratio + return r +} + +// SetPos sets the position of the radar on the screen +func (r *Radar) SetPos(x, y float64) { + r.LayeredPoint.SetPos(x, y) + r.outline.SetPos(x, y) +} + +// SetOutline of the radar to be the provided outline +func (r *Radar) SetOutline(outline *render.Sprite) { + r.outline = outline +} + +// GetRGBA returns this radar's image +func (r *Radar) GetRGBA() *image.RGBA { + return r.r +} + +// Draw draws the radar at a given offset +func (r *Radar) Draw(buff draw.Image, xOff, yOff float64) { + // Draw each point p in r.points + // at r.X() + center.X() - p.X(), r.Y() + center.Y() - p.Y() + // IF that value is < r.width/2, > -r.width/2, < r.height/2, > -r.height/2 + r.Lock() + for p, c := range r.points { + x := int((*p.X-*r.center.X)/r.ratio) + r.width/2 + y := int((*p.Y-*r.center.Y)/r.ratio) + r.height/2 + for x2 := x - 1; x2 < x+1; x2++ { + for y2 := y - 1; y2 < y+1; y2++ { + r.r.Set(x2, y2, c) + } + } + } + r.Unlock() + r.r.Set(r.width/2, r.height/2, centerColor) + r.outline.Draw(buff, xOff, yOff) + render.DrawImage(buff, r.r, int(xOff+r.X()), int(yOff+r.Y())) + + r.r = image.NewRGBA(image.Rect(0, 0, r.width, r.height)) +} + +// AddPoint adds an additional point to the radar to be tracked +func (r *Radar) AddPoint(loc Point, c color.Color) { + r.Lock() + r.points[loc] = c + r.Unlock() +} + +// AddTrackedPoint to the radar. Enables display and later lookup by id (usually the CID of the caller). +func (r *Radar) AddTrackedPoint(loc Point, id int, c color.Color) { + r.Lock() + r.points[loc] = c + r.pointLookup[id] = &loc + r.Unlock() +} + +// LookupPoint by the provided id. This only works if the point was tracked on creation. +func (r *Radar) LookupPoint(id int) (*Point, bool) { + p, ok := r.pointLookup[id] + return p, ok +} + +// RemovePointByLookup removes a point if it is present. If it is not, it does nothing. +func (r *Radar) RemovePointByLookup(id int) error { + p, ok := r.LookupPoint(id) + if !ok { + return nil + } + loc := *p + r.Lock() + defer r.Unlock() + if _, ok := r.points[loc]; ok { + delete(r.points, loc) + } else { + return fmt.Errorf("attempted to remove a radar point that did not exist") + } + return nil +} diff --git a/components/rescaler/README.md b/components/rescaler/README.md new file mode 100644 index 0000000..cb0eb30 --- /dev/null +++ b/components/rescaler/README.md @@ -0,0 +1,3 @@ +# rescaler + +The rescaler component enables scenes to define their elements at a given resolution and have those elements repositioned and rescaled to accomodate different resolutions. diff --git a/components/rescaler/go.mod b/components/rescaler/go.mod new file mode 100644 index 0000000..45046dd --- /dev/null +++ b/components/rescaler/go.mod @@ -0,0 +1,12 @@ +module github.com/oakmound/grove/components/rescaler + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect +) diff --git a/components/rescaler/go.sum b/components/rescaler/go.sum new file mode 100644 index 0000000..461e0e2 --- /dev/null +++ b/components/rescaler/go.sum @@ -0,0 +1,50 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/rescaler/rescaler.go b/components/rescaler/rescaler.go new file mode 100644 index 0000000..c5ab807 --- /dev/null +++ b/components/rescaler/rescaler.go @@ -0,0 +1,86 @@ +package rescaler + +import ( + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v3/entities/x/btn" + "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v3/scene" +) + +// A Rescaler knows the resolution a scene is designed for, and can make simple +// adjustments to scale and positioning of elements in the scene if the window +// is scaled differently from the target resolution at scene start. The rescaler +// does not handle a window's size changing mid-scene. +type Rescaler struct { + ctx *scene.Context + targetResolution intgeom.Point2 + wRatio, hRatio float64 +} + +// New should take in the resolution the scene's elements are all designed for. +func New(ctx *scene.Context, targetResolution intgeom.Point2) *Rescaler { + rsc := &Rescaler{ + ctx: ctx, + targetResolution: targetResolution, + } + w, h := rsc.ctx.Window.Width(), rsc.ctx.Window.Height() + rsc.wRatio = float64(w) / float64(rsc.targetResolution.X()) + rsc.hRatio = float64(h) / float64(rsc.targetResolution.Y()) + return rsc +} + +func (rsc Rescaler) Position(target floatgeom.Point2) (float64, float64) { + return target.X() * rsc.wRatio, target.Y() * rsc.hRatio +} + +func (rsc Rescaler) SetPosition(rnd render.Renderable, target floatgeom.Point2) { + rnd.SetPos(rsc.Position(target)) +} + +// Scale will scale a modifiable +func (rsc Rescaler) Scale(m render.Modifiable) { + m.Modify(mod.Scale(rsc.wRatio, rsc.hRatio)) +} + +func (rsc Rescaler) Height(h float64) float64 { + return h * rsc.hRatio +} + +func (rsc Rescaler) Width(w float64) float64 { + return w * rsc.wRatio +} + +func (rsc Rescaler) Dims(dims floatgeom.Point2) floatgeom.Point2 { + dims[0] *= rsc.hRatio + dims[1] *= rsc.wRatio + return dims +} + +func (rs Rescaler) Draw(r render.Renderable, layers ...int) { + if m, ok := r.(render.Modifiable); ok { + rs.Scale(m) + } + rs.ctx.DrawStack.Draw(r, layers...) +} + +// BtnOption can be used with an x/btn option set to apply most operations a +// rescaler provides where meaningful. +func BtnOption(rsc *Rescaler) btn.Option { + return func(g btn.Generator) btn.Generator { + if g.W != 0 { + g.W = rsc.Width(g.W) + } + if g.H != 0 { + g.H = rsc.Height(g.H) + } + if g.X != 0 || g.Y != 0 { + g.X, g.Y = rsc.Position(floatgeom.Point2{g.X, g.Y}) + } + if g.TxtX != 0 || g.TxtY != 0 { + g.TxtX, g.TxtY = rsc.Position(floatgeom.Point2{g.TxtX, g.TxtY}) + } + return g + } +} diff --git a/components/sound/README.md b/components/sound/README.md new file mode 100644 index 0000000..50706bc --- /dev/null +++ b/components/sound/README.md @@ -0,0 +1,3 @@ +# sound + +The sound package contains utilities for controlling global volume levels, fading audio out or in, and components for manipulating and displaying global volume levels. diff --git a/components/sound/bar.go b/components/sound/bar.go new file mode 100644 index 0000000..05ab849 --- /dev/null +++ b/components/sound/bar.go @@ -0,0 +1,108 @@ +package sound + +import ( + "fmt" + "image/color" + "image/draw" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/alg/intgeom" + "github.com/oakmound/oak/v3/entities" + "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/mouse" + "github.com/oakmound/oak/v3/render" + "golang.org/x/image/colornames" +) + +// BarKind details which type of volume should be manipulated by the bar +type BarKind int + +// Types of bars. +const ( + BarKindMaster BarKind = iota + BarKindMusic BarKind = iota + BarKindSFX BarKind = iota +) + +// NewBar for setting of the sound graphically. +func NewBar(kind BarKind, pos floatgeom.Point2, w, h int) *entities.Solid { + + dl := &DashedLine{ + LayeredPoint: render.NewLayeredPoint(pos.X(), pos.Y(), 0), + Dims: intgeom.Point2{w, h}, + OnColor: colornames.White, + OffColor: colornames.Gray, + DashMod: 4, + Progress: 0, + } + solid := entities.NewSolid(pos.X(), pos.Y(), float64(w), float64(h), dl, mouse.DefaultTree, 0) + var eventName string + switch kind { + case BarKindMaster: + eventName = EventMasterVolumeChanged + dl.Progress = volume + case BarKindMusic: + eventName = EventMusicVolumeChanged + dl.Progress = musicVolume + case BarKindSFX: + eventName = EventSFXVolumeChanged + dl.Progress = sfxVolume + } + solid.CID.Bind(eventName, func(id event.CID, payload interface{}) int { + newVal, ok := payload.(float64) + if !ok { + fmt.Println("expected float progress arg to bar change binding") + return 0 + } + solid, ok := event.GetEntity(id).(*entities.Solid) + if !ok { + fmt.Println("expected doodad entity in bar change binding") + return 0 + } + dl, ok := solid.R.(*DashedLine) + if !ok { + fmt.Println("expected renderable as DashedLine in bar change binding") + return 0 + } + dl.Progress = newVal + return 0 + }) + return solid +} + +// DashedLine to display the current value on the bar. +type DashedLine struct { + render.LayeredPoint + Dims intgeom.Point2 + OnColor color.RGBA + OffColor color.RGBA + DashMod int + Progress float64 +} + +// GetDims for the line. +func (dl *DashedLine) GetDims() (int, int) { + return dl.Dims.X(), dl.Dims.Y() +} + +// Draw the dashed line. +func (dl *DashedLine) Draw(buff draw.Image, xOff, yOff float64) { + shouldDash := false + wf := float64(dl.Dims.X()) + hf := float64(dl.Dims.Y()) + clr := dl.OnColor + for i := 0.0; i < wf; i++ { + if int(i)%dl.DashMod == 0 { + shouldDash = !shouldDash + } + if !shouldDash { + if i/wf > dl.Progress { + clr = dl.OffColor + } + x := i + dl.X() + for y := dl.Y(); y < dl.Y()+hf; y++ { + buff.Set(int(x+xOff), int(y+yOff), clr) + } + } + } +} diff --git a/components/sound/events.go b/components/sound/events.go new file mode 100644 index 0000000..d0bbb36 --- /dev/null +++ b/components/sound/events.go @@ -0,0 +1,8 @@ +package sound + +// Event names to be emitted when audio volumes change. +const ( + EventMasterVolumeChanged = "MasterVolumeChanged" + EventSFXVolumeChanged = "SFXVolumeChanged" + EventMusicVolumeChanged = "MusicVolumeChanged" +) diff --git a/components/sound/fade.go b/components/sound/fade.go new file mode 100644 index 0000000..9aaaeba --- /dev/null +++ b/components/sound/fade.go @@ -0,0 +1,82 @@ +package sound + +import ( + "sync" + "time" +) + +const fadeLoopTime = 20 * time.Millisecond + +// MusicFader exposes a Fader for usage on Music. +var MusicFader = Fader{} + +// Fader is a safe way to fade in our out Music. +type Fader struct { + sync.Mutex + + fadeTo float64 + fadedFrom float64 + running bool + runTil time.Time +} + +// FadeOut the Music over the given time. +func (f *Fader) FadeOut(duration time.Duration) { + f.Lock() + if f.fadeTo == musicVolume && f.fadeTo == 0 { + f.Unlock() + return + } + f.fadedFrom = musicVolume + if f.fadeTo > musicVolume { + f.fadedFrom = f.fadeTo + } + f.fadeTo = 0 + f.Unlock() + f.startFade(duration) +} + +// FadeIn the Music over the given time. +func (f *Fader) FadeIn(duration time.Duration) { + f.Lock() + if f.fadeTo == f.fadedFrom { + f.Unlock() + return + } + f.fadeTo = f.fadedFrom + f.Unlock() + f.startFade(duration) +} + +func (f *Fader) startFade(duration time.Duration) { + f.Lock() + f.runTil = time.Now().Add(duration) + f.Unlock() + if f.running { + return + } + go func() { + f.Lock() + f.running = true + f.Unlock() + + t := time.NewTicker(fadeLoopTime) + defer t.Stop() + + for { + f.Lock() + if time.Now().After(f.runTil) { + SetMusicVolume(f.fadeTo) + break + } + f.Unlock() + + loopsLeft := time.Until(f.runTil) / fadeLoopTime + fadeTo := musicVolume + (f.fadeTo-musicVolume)/float64(loopsLeft) + SetMusicVolume(fadeTo) + <-t.C + } + f.running = false + f.Unlock() + }() +} diff --git a/components/sound/go.mod b/components/sound/go.mod new file mode 100644 index 0000000..1b565f0 --- /dev/null +++ b/components/sound/go.mod @@ -0,0 +1,18 @@ +module github.com/oakmound/grove/components/sound + +go 1.17 + +require ( + github.com/oakmound/oak/v3 v3.2.2 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 +) + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hajimehoshi/go-mp3 v0.3.1 // indirect + github.com/oakmound/alsa v0.0.2 // indirect + github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // indirect + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect +) diff --git a/components/sound/go.sum b/components/sound/go.sum new file mode 100644 index 0000000..ca9bd65 --- /dev/null +++ b/components/sound/go.sum @@ -0,0 +1,57 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= +github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2 h1:JbOUckkJqVvhABth7qy2JgAjqsWuBPggyoYOk1L6eK0= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf h1:od9gEl9UQ/QNHlgYlgsSaC5SZ+CGbvO2/PCIgserJc0= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/sound/music.go b/components/sound/music.go new file mode 100644 index 0000000..5d14635 --- /dev/null +++ b/components/sound/music.go @@ -0,0 +1,89 @@ +package sound + +import ( + "fmt" + "sync" + + "github.com/oakmound/oak/v3/audio" + "github.com/oakmound/oak/v3/audio/font" + "github.com/oakmound/oak/v3/event" +) + +var ( + // Musics is a mapping of music files with an Audio to play. + Musics = map[string]*audio.Audio{} + + musicFileLock sync.RWMutex + musicFiles = map[string]*font.Font{} +) + +// RegisterMusic and make it ready for playback. +// Specify the file to load it from and the music font to use with it. +func RegisterMusic(file string, f *font.Font) { + musicFileLock.Lock() + musicFiles[file] = f + musicFileLock.Unlock() +} + +// ReloadMusicAssets and error if the reload from files fails somehow. +func ReloadMusicAssets() error { + musicFileLock.Lock() + defer musicFileLock.Unlock() + for s, f := range musicFiles { + a, err := audio.Get(s) + if err != nil { + return err + } + Musics[s] = audio.New(f, a) + } + return nil +} + +// PlayMusic stops any playing music and then tries to play the specified music file. +func PlayMusic(s string) error { + if PlayingMusicLabel == s { + return nil + } + StopMusic() + PlayingMusicLabel = s + audOrigin, ok := Musics[s] + if !ok { + return fmt.Errorf("Tried to play unloaded Audio %q", s) + } + audOrigin.Play() + PlayingMusic = audOrigin + return nil +} + +// StopMusic if any music is playing. +func StopMusic() { + if PlayingMusic != nil { + PlayingMusic.Stop() + } +} + +// only one music can be playing + +// PlayingMusic is the currently playing audio. +var PlayingMusic *audio.Audio + +// PlayingMusicLabel is the playing music's string name. +var PlayingMusicLabel string + +// SetMusicVolume to the provided value. +func SetMusicVolume(newVolume float64) { + musicVolume = newVolume + updateMusicVolume(volume, musicVolume) + event.Trigger(EventMusicVolumeChanged, newVolume) +} + +func updateMusicVolume(volume, musicVolume float64) { + newVolume := volume * musicVolume + if newVolume > 1 || newVolume < 0 { + newVolume = 0 + } + scalar := convertVolumeScale(newVolume) + for _, m := range Musics { + m.SetVolume(scalar) + } +} diff --git a/components/sound/sfx.go b/components/sound/sfx.go new file mode 100644 index 0000000..af1293e --- /dev/null +++ b/components/sound/sfx.go @@ -0,0 +1,81 @@ +package sound + +import ( + "fmt" + "sync" + "math/rand" + + "github.com/oakmound/oak/v3/audio" + "github.com/oakmound/oak/v3/audio/font" + "github.com/oakmound/oak/v3/event" +) + +var ( + SFXs = map[string]*audio.Audio{} + + sfxFileLock sync.RWMutex + sfxFiles = map[string]*font.Font{} +) + +func RegisterSFX(file string, f *font.Font) { + sfxFileLock.Lock() + sfxFiles[file] = f + sfxFileLock.Unlock() +} + +func ReloadSFXAssets() error { + sfxFileLock.Lock() + defer sfxFileLock.Unlock() + for s, f := range sfxFiles { + a, err := audio.Get(s) + if err != nil { + return err + } + SFXs[s] = audio.New(f, a) + } + return nil +} + +func PlayOneOfSFX(files ...string) { + i := rand.Intn(len(files)) + PlaySFX(files[i]) +} + +func PlaySFX(s string) error { + audOrigin, ok := SFXs[s] + if !ok { + return fmt.Errorf("Tried to play unloaded Audio %q", s) + } + aud, err := audOrigin.Copy() + if err != nil { + return err + } + a := aud.(*audio.Audio) + + v := volume * sfxVolume + if v > 1 || v < 0 { + v = 0 + } + scalar := convertVolumeScale(v) + a.SetVolume(scalar) + a.Play() + return nil +} + +func SetSFXVolume(newVolume float64) { + sfxVolume = newVolume + updateSFXVolume(volume, sfxVolume) + event.Trigger(EventSFXVolumeChanged, newVolume) +} + +func updateSFXVolume(volume, sfxVolume float64) { + newVolume := volume * sfxVolume + if newVolume > 1 || newVolume < 0 { + newVolume = 0 + } + scalar := convertVolumeScale(newVolume) + + for _, s := range SFXs { + s.SetVolume(scalar) + } +} diff --git a/components/sound/sound.go b/components/sound/sound.go new file mode 100644 index 0000000..3ee6245 --- /dev/null +++ b/components/sound/sound.go @@ -0,0 +1,55 @@ +package sound + +import ( + "sync" + + "github.com/oakmound/oak/v3/event" +) + +var ( + volume = 0.0 + musicVolume = 0.0 + sfxVolume = 0.0 +) + +var initOnce sync.Once + +// Init loads assets and initializes volume levels. It will do nothing +// after it has been called once. +func Init(masterVolume, mVolume, sVolume float64) { + initOnce.Do(func() { + ReloadMusicAssets() + ReloadSFXAssets() + + volume = masterVolume + musicVolume = mVolume + sfxVolume = sVolume + + updateSFXVolume(volume, sfxVolume) + updateMusicVolume(volume, musicVolume) + }) +} + +// convert a volume into the args for the windows api. +// Windows api is from 0 to -10000 but we see that -5000 and down is inaudible. +func convertVolumeScale(volumeScale float64) int32 { + if volumeScale <= .1 { + // map 0 -> .1 to -10000 -> -5000 + volumeScale *= 5 + volumeScale-- + volumeScale *= 10000 + } else { + // map .1 -> 1.0 to -5000 -> 0 + volumeScale-- + volumeScale *= 5555 + } + return int32(volumeScale) +} + +// SetMasterVolume value and update all volume values based of the given value. +func SetMasterVolume(masterVolume float64) { + volume = masterVolume + updateSFXVolume(volume, sfxVolume) + updateMusicVolume(volume, musicVolume) + event.Trigger(EventMasterVolumeChanged, volume) +} diff --git a/components/textfit/README.md b/components/textfit/README.md new file mode 100644 index 0000000..8eb953d --- /dev/null +++ b/components/textfit/README.md @@ -0,0 +1,3 @@ +# textfit + +The textfit component consumes text bodies and plans for how to fit those bodies to a given rectangle, and outputs fitted and resized text appropriately. diff --git a/components/textfit/go.mod b/components/textfit/go.mod new file mode 100644 index 0000000..dd3628c --- /dev/null +++ b/components/textfit/go.mod @@ -0,0 +1,11 @@ +module github.com/oakmound/grove/components/textfit + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect +) diff --git a/components/textfit/go.sum b/components/textfit/go.sum new file mode 100644 index 0000000..d9aba2c --- /dev/null +++ b/components/textfit/go.sum @@ -0,0 +1,49 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/textfit/textfit.go b/components/textfit/textfit.go new file mode 100644 index 0000000..6bc395b --- /dev/null +++ b/components/textfit/textfit.go @@ -0,0 +1,172 @@ +package textfit + +import ( + "fmt" + "sort" + "strings" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/render" +) + +type Generator struct { + Text string + Font *render.Font + Dimensions floatgeom.Point2 + MinSize int + MaxSize int + BreakStyle + RelativePos floatgeom.Point2 + // overflow style - clip / newlines on word / newlines on character / trail off (...) + // padding +} + +type BreakStyle byte + +const ( + BreakStyleCharacter BreakStyle = iota + BreakStyleWord +) + +func (g *Generator) generate() (render.Modifiable, error) { + size := g.MaxSize + dims := g.Dimensions.Sub(g.RelativePos.MulConst(2)) + + var sections []string +ONESECTION: + for { + font, _ := g.Font.RegenerateWith(func(f render.FontGenerator) render.FontGenerator { + f.Size = float64(size) + return f + }) + sections = []string{g.Text} + HEIGHTCHECK: + for { + remainingHeight := dims.Y() - float64(size*len(sections)) + if remainingHeight < 0 { + size-- + if size < g.MinSize { + return nil, fmt.Errorf("size fell below minSize; decrease min size, decrease text ct, or increase bounds") + } + continue ONESECTION + } + for i, sec := range sections { + sec = strings.TrimSpace(sec) + length := font.MeasureString(sec).Round() + if length > int(dims.X()) { + breakPoint := sort.Search(len(sec), func(i int) bool { + // returns the smallest index for which i is true, + // so we want to return true if we are above our goal width, + // and then subtract 1. + length := font.MeasureString(sec[:i]).Round() + return length > int(dims.X()) + }) + breakPoint-- + if g.BreakStyle == BreakStyleWord { + // walk back to the last space + for breakPoint > 1 && sec[breakPoint] != ' ' { + breakPoint-- + } + } + + sections[i] = sec[:breakPoint] + sections = append(sections[:i+1], append( + []string{sec[breakPoint:]}, sections[i+1:]...)...) + + continue HEIGHTCHECK + } + } + break ONESECTION + } + } + + comp := render.NewCompositeR() + + font, _ := g.Font.RegenerateWith(func(f render.FontGenerator) render.FontGenerator { + f.Size = float64(size) + return f + }) + x := g.RelativePos.X() + y := g.RelativePos.Y() + + for _, sec := range sections { + sec = strings.TrimSpace(sec) + t := font.NewText(sec, x, y) + y += float64(size) + comp.Append(t.ToSprite()) + } + + return comp.ToSprite(), nil +} + +func defaultGenerator() *Generator { + return &Generator{ + MinSize: 5, + MaxSize: 30, + Font: render.DefaultFont(), + Text: "placeholder", + Dimensions: floatgeom.Point2{50, 50}, + } +} + +func MustNew(options ...Option) render.Modifiable { + r, err := New(options...) + if err != nil { + panic(err) + } + return r +} + +func New(options ...Option) (render.Modifiable, error) { + gen := defaultGenerator() + for _, opt := range options { + opt(gen) + } + return gen.generate() +} + +type Option (func(*Generator)) + +func String(s string) Option { + return func(g *Generator) { + g.Text = s + } +} + +func Font(f *render.Font) Option { + return func(g *Generator) { + g.Font = f + } +} + +func Dimensions(p floatgeom.Point2) Option { + return func(g *Generator) { + g.Dimensions = p + } +} + +func Inset(w, h float64) Option { + // reduce dimensions by w*2 and h*2 + // set relpos to w,h + return func(g *Generator) { + g.RelativePos = floatgeom.Point2{w, h} + } +} + +func MinSize(min int) Option { + return func(g *Generator) { + g.MinSize = min + } +} + +func MaxSize(max int) Option { + return func(g *Generator) { + g.MaxSize = max + } +} + +func WithBreakStyle(bs BreakStyle) Option { + return func(g *Generator) { + g.BreakStyle = bs + } +} \ No newline at end of file diff --git a/components/textfit/textfit_test.go b/components/textfit/textfit_test.go new file mode 100644 index 0000000..3e299f0 --- /dev/null +++ b/components/textfit/textfit_test.go @@ -0,0 +1,32 @@ +package textfit + +import ( + "testing" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/render" +) + +func TestNew(t *testing.T) { + type testCase struct { + opts []Option + // should not error + } + tcs := []testCase{ + { + opts: []Option{ + String("The final episode of \"The Legend of High School\" show has leaked two hours before its premiere. As a moderator of the official \"The Legend of High School\" forum, you need to keep the fans from learning any details about the ending of the show.You've read the books before they made it a show, so you're already familiar with the plot points that are going to happen: Alexzandre will Confess to Jeremiah, Olivette will miss the Polar Dance and not be sad about it, Tim Runnings will Tie with Ran Jennings for first place in the Quinqometry exams , and finally Sam Sam will show up to Graduation, Walk and end the series with his Goodbye Speech. As always, this is a family friendly forum. Discussion of non-canon romantic pairings of characters on the show is not allowed. Profanity of -any- kind is also not allowed.\nLinking to the leaked episode is not allowed . Asking for links to the leaked episode is also not allowed ."), + MinSize(1), + MaxSize(22), + Font(render.DefaultFont()), + Dimensions(floatgeom.Point2{300, 300}), + }, + }, + } + for _, tc := range tcs { + _, err := New(tc.opts...) + if err != nil { + t.Fatalf("got error: %v", err) + } + } +} diff --git a/components/textinput/README.md b/components/textinput/README.md new file mode 100644 index 0000000..ea84d01 --- /dev/null +++ b/components/textinput/README.md @@ -0,0 +1,3 @@ +# textinput + +The textinput component defines a click-into keyboard input text box. diff --git a/components/textinput/go.mod b/components/textinput/go.mod new file mode 100644 index 0000000..230bcf4 --- /dev/null +++ b/components/textinput/go.mod @@ -0,0 +1,12 @@ +module github.com/oakmound/grove/components/textinput + +go 1.17 + +require github.com/oakmound/oak/v3 v3.2.2 + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect +) diff --git a/components/textinput/go.sum b/components/textinput/go.sum new file mode 100644 index 0000000..461e0e2 --- /dev/null +++ b/components/textinput/go.sum @@ -0,0 +1,50 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/textinput/option.go b/components/textinput/option.go new file mode 100644 index 0000000..5d084fa --- /dev/null +++ b/components/textinput/option.go @@ -0,0 +1,103 @@ +package textinput + +import ( + "image/color" + "time" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/render" +) + +// Option for configuring a TextInput +type Option func(*TextInput) + +func And(opts ...Option) Option { + return func(t *TextInput) { + for _, opt := range opts { + opt(t) + } + } +} + +func WithStr(s string) Option { + return func(t *TextInput) { + t.currentText = &s + } +} + +func WithStrPtr(s *string) Option { + return func(t *TextInput) { + t.currentText = s + } +} + +func WithPlaceholder(s string) Option { + return func(t *TextInput) { + t.currentText = &s + t.onFirstEdit = func(ti *TextInput) { + *t.currentText = "" + } + } +} + +func WithPos(x, y float64) Option { + return func(t *TextInput) { + t.x = x + t.y = y + } +} + +func WithDims(w, h float64) Option { + return func(t *TextInput) { + t.w = w + t.h = h + } +} + +func WithFinalizer(f func(string)) Option { + return func(t *TextInput) { + t.finalizer = f + } +} + +func WithFont(f *render.Font) Option { + return func(t *TextInput) { + t.font = f + } +} + +func WithBlinkerColor(c color.Color) Option { + return func(t *TextInput) { + t.blinkerColor = c + } +} + +func WithSensitive(sensitive bool) Option { + return func(t *TextInput) { + t.sensitive = sensitive + } +} + +func WithBlinkRate(blinkRate time.Duration) Option { + return func(t *TextInput) { + t.blinkRate = blinkRate + } +} + +func WithTextOffset(x, y float64) Option { + return func(t *TextInput) { + t.textOffset = floatgeom.Point2{x, y} + } +} + +func WithOnEdit(onEdit func(*TextInput)) Option { + return func(t *TextInput) { + t.onEdit = onEdit + } +} + +func WithBlinkerLayers(layers ...int) Option { + return func(t *TextInput) { + t.blinkerLayers = layers + } +} diff --git a/components/textinput/textinput.go b/components/textinput/textinput.go new file mode 100644 index 0000000..3861e7c --- /dev/null +++ b/components/textinput/textinput.go @@ -0,0 +1,302 @@ +package textinput + +import ( + "fmt" + "image/color" + "sync" + "time" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v3/entities" + "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/key" + "github.com/oakmound/oak/v3/mouse" + "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v3/scene" + "github.com/oakmound/oak/v3/timing" +) + +type TextInput struct { + *entities.Solid + ctx *scene.Context + + bindingLock sync.Mutex + + textLock sync.Mutex + currentText *string + + editing bool + x, y float64 + w, h float64 + textOffset floatgeom.Point2 + + finalizer func(string) + onFirstEdit func(ti *TextInput) + onEdit func(ti *TextInput) + + font *render.Font + + blinkerLock sync.Mutex + blinker render.Renderable + blinkerColor color.Color + blinkRate time.Duration + blinkerIndex int + blinkerLayers []int + + sensitive bool + sensitiveText string +} + +func New(ctx *scene.Context, opts ...Option) *TextInput { + emptyString := "" + ti := &TextInput{ + ctx: ctx, + w: 100, + h: 20, + font: render.DefaultFont(), + blinkerColor: color.RGBA{255, 255, 255, 255}, + currentText: &emptyString, + blinkerLayers: []int{0, 2}, + } + for _, opt := range opts { + opt(ti) + } + ti.font = ti.font.Copy() + r := ti.font.NewStrPtrText(ti.currentText, 0, 0) + ti.Solid = entities.NewSolid(ti.x, ti.y, ti.w, ti.h, r, mouse.DefaultTree, ti.Init()) + ti.bindStartTyping() + ti.R.SetPos(ti.x+ti.textOffset.X(), ti.y+ti.textOffset.Y()) + return ti +} + +// Select and Deselect simulate mouse click actions to enable or disable +// typing in a text input. + +func (ti *TextInput) Select() { + ti.Trigger(mouse.ClickOn, mouse.Event{}) +} + +func (ti *TextInput) Deselect() { + ti.stopTyping() +} + +func (ti *TextInput) startTyping(me mouse.Event) int { + ti.bindingLock.Lock() + defer ti.bindingLock.Unlock() + + if ti.onFirstEdit != nil { + ti.onFirstEdit(ti) + ti.onFirstEdit = nil + } + if ti.onEdit != nil { + ti.onEdit(ti) + } + ti.editing = true + ti.updateBlinkerToMouse(me) + ti.Bind(key.Down, editBinding) + ti.Bind(key.Held, editBinding) + ti.CheckedBind(mouse.Click, func(ti *TextInput, ev interface{}) int { + return ti.stopTyping() + }) + + return event.UnbindSingle +} + +func (ti *TextInput) bindStartTyping() { + ti.CheckedBind(mouse.ClickOn, func(ti *TextInput, ev interface{}) int { + me, ok := ev.(*mouse.Event) + if !ok { + fmt.Println("text input received non-mouse event argument to ClickOn") + return 0 + } + return ti.startTyping(*me) + }) +} + +func (ti *TextInput) stopTyping() int { + ti.bindingLock.Lock() + defer ti.bindingLock.Unlock() + + if ti.editing { + ti.editing = false + ti.undrawBlinker() + if ti.finalizer != nil { + if ti.sensitive { + ti.finalizer(ti.sensitiveText) + } else { + ti.finalizer(*ti.currentText) + } + } + ti.bindStartTyping() + event.UnbindBindable(event.UnbindOption{ + Event: event.Event{ + Name: key.Down, + CallerID: ti.CID, + }, + Fn: editBinding, + }) + event.UnbindBindable(event.UnbindOption{ + Event: event.Event{ + Name: key.Held, + CallerID: ti.CID, + }, + Fn: editBinding, + }) + return event.UnbindSingle + } + return 0 +} + +func (ti *TextInput) Init() event.CID { + return event.NextID(ti) +} + +func (ti *TextInput) CheckedBind(name string, f func(*TextInput, interface{}) int) { + ti.Bind(name, func(id event.CID, ev interface{}) int { + ti, ok := id.E().(*TextInput) + if !ok { + dlog.Error("Non-TextInput passed to TextInput binding") + return 0 + } + return f(ti, ev) + }) +} + +func (ti *TextInput) updateBlinkerToMouse(me mouse.Event) { + ti.textLock.Lock() + // convert me to index position + // linear scan until its demonstrated we need something with better performance + var textIndex int + for i := 0; i < len(*ti.currentText); i++ { + charX := float64(ti.font.MeasureString((*ti.currentText)[:i]).Round()) + charX += ti.R.X() + if charX > me.X() { + textIndex = i + break + } + } + ti.textLock.Unlock() + + ti.updateBlinker(textIndex) +} + +func (ti *TextInput) updateBlinkerRelative(shift int) { + ti.updateBlinker(ti.blinkerIndex + shift) +} + +func (ti *TextInput) updateBlinker(textIndex int) { + ti.blinkerLock.Lock() + defer ti.blinkerLock.Unlock() + if ti.blinker != nil { + ti.blinker.Undraw() + } + ti.textLock.Lock() + var w float64 + h := ti.font.Height() + if textIndex < 0 { + w = 0 + ti.blinkerIndex = 0 + } else { + if textIndex >= len(*ti.currentText) { + textIndex = len(*ti.currentText) + } + fixedWidth := ti.font.MeasureString((*ti.currentText)[:textIndex]) + w = float64(fixedWidth.Round()) + ti.blinkerIndex = textIndex + } + x, y := ti.R.X(), ti.R.Y() + ti.textLock.Unlock() + if ti.blinkRate != 0 { + ti.blinker = render.NewSequence(timing.FrameDelayToFPS(ti.blinkRate), + render.NewLine(x+w, y, x+w, y+h, ti.blinkerColor), + render.EmptyRenderable(), + ) + } else { + ti.blinker = render.NewLine(x+w, y, x+w, y+h, ti.blinkerColor) + } + ti.ctx.DrawStack.Draw(ti.blinker, ti.blinkerLayers...) +} + +func (ti *TextInput) undrawBlinker() { + ti.blinkerLock.Lock() + defer ti.blinkerLock.Unlock() + if ti.blinker != nil { + ti.blinker.Undraw() + } +} + +// TODO: checked bindings can't be unbound directly + +func editBinding(id event.CID, ev interface{}) int { + ti, ok := id.E().(*TextInput) + if !ok { + return event.UnbindSingle + } + if !ti.editing { + return event.UnbindSingle + } + k, ok := ev.(key.Event) + if !ok { + dlog.Error("Got non key event in text input edit") + return 0 + } + ti.textLock.Lock() + txt := *ti.currentText + ti.textLock.Unlock() + + code := k.Code.String()[4:] + shift := 0 + switch code { + case key.Enter, key.Escape: + ti.bindingLock.Lock() + defer ti.bindingLock.Unlock() + ti.editing = false + ti.undrawBlinker() + if ti.finalizer != nil { + if ti.sensitive { + ti.finalizer(ti.sensitiveText) + } else { + ti.finalizer(txt) + } + } + ti.bindStartTyping() + return event.UnbindSingle + case key.DeleteBackspace: + if len(txt) != 0 && ti.blinkerIndex != 0 { + if ti.blinkerIndex >= len(txt) { + txt = txt[:ti.blinkerIndex-1] + } else { + txt = txt[:ti.blinkerIndex-1] + txt[ti.blinkerIndex:] + } + } + if ti.sensitive && len(ti.sensitiveText) != 0 && ti.blinkerIndex != 0 { + if ti.blinkerIndex >= len(ti.sensitiveText) { + ti.sensitiveText = ti.sensitiveText[:ti.blinkerIndex-1] + } else { + ti.sensitiveText = ti.sensitiveText[:ti.blinkerIndex-1] + ti.sensitiveText[ti.blinkerIndex:] + } + } + shift = -1 + case key.LeftShift, key.RightShift, key.Tab: + case key.LeftArrow: + ti.updateBlinkerRelative(-1) + return 0 + case key.RightArrow: + ti.updateBlinkerRelative(1) + return 0 + default: + if ti.sensitive { + txt += "*" + ti.sensitiveText = ti.sensitiveText[:ti.blinkerIndex] + string(k.Rune) + ti.sensitiveText[ti.blinkerIndex:] + } else { + txt = txt[:ti.blinkerIndex] + string(k.Rune) + txt[ti.blinkerIndex:] + } + shift = len(string(k.Rune)) + } + ti.textLock.Lock() + *ti.currentText = txt + ti.textLock.Unlock() + ti.updateBlinkerRelative(shift) + return 0 +} diff --git a/components/textqueue/README.md b/components/textqueue/README.md new file mode 100644 index 0000000..6535e17 --- /dev/null +++ b/components/textqueue/README.md @@ -0,0 +1,3 @@ +# textqueue + +The textqueue component displays text input via event handlers in a vertical queue, with older texts fading away after a delay. diff --git a/components/textqueue/go.mod b/components/textqueue/go.mod new file mode 100644 index 0000000..c7cbd42 --- /dev/null +++ b/components/textqueue/go.mod @@ -0,0 +1,14 @@ +module github.com/oakmound/grove/components/textqueue + +go 1.17 + +require ( + github.com/oakmound/oak/v3 v3.2.2 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 +) + +require ( + github.com/disintegration/gift v1.2.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect +) diff --git a/components/textqueue/go.sum b/components/textqueue/go.sum new file mode 100644 index 0000000..5297934 --- /dev/null +++ b/components/textqueue/go.sum @@ -0,0 +1,53 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +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/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= +github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/oakmound/alsa v0.0.2/go.mod h1:wx+ehwqFnNL7foTwxxu2bKQlaUmD2oXd4ka1UBSgWAo= +github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= +github.com/oakmound/oak/v3 v3.2.2 h1:8ZQA3Ommh5cgzbq+DKiQ0ozxDtrD0w2M+ssj4snuOII= +github.com/oakmound/oak/v3 v3.2.2/go.mod h1:mXgIg9v/8I7OJzDjxYrpcEHgVgWKiYxCEcYyPwOynLQ= +github.com/oakmound/w32 v2.1.0+incompatible/go.mod h1:lzloWlclSXIU4cDr67WF8qjFFDO8gHHBIk4Qqe90enQ= +github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf/go.mod h1:RBXkZ8n2vvtdJP6PO+TbU/N/DVuCDwUN53CU+C1pJOs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/components/textqueue/textqueue.go b/components/textqueue/textqueue.go new file mode 100644 index 0000000..58426ff --- /dev/null +++ b/components/textqueue/textqueue.go @@ -0,0 +1,122 @@ +package textqueue + +import ( + "fmt" + "image/draw" + "sync" + "time" + + "github.com/oakmound/oak/v3/alg/floatgeom" + "github.com/oakmound/oak/v3/dlog" + "github.com/oakmound/oak/v3/entities/x/mods" + "github.com/oakmound/oak/v3/event" + "github.com/oakmound/oak/v3/render" + "github.com/oakmound/oak/v3/render/mod" + "github.com/oakmound/oak/v3/scene" + "golang.org/x/image/colornames" +) + +type queueItem struct { + mod render.Modifiable + dropAt time.Time +} + +// A TextQueue is a renderable entity that displays text in a column +// for a brief time before fading the text and dropping it. It accepts +// new text elements from the DisplayTextEvent event. +type TextQueue struct { + event.CID + render.LayeredPoint + + queueLock sync.Mutex + queue []queueItem + font *render.Font + sustainTime time.Duration +} + +func (tq *TextQueue) Init() event.CID { + return event.NextID(tq) +} + +// New creates a customized TextQueue. +func New(ctx *scene.Context, initiatingEvent string, pos floatgeom.Point2, layer int, font *render.Font, sustainTime time.Duration) *TextQueue { + tq := &TextQueue{} + + tq.CID = ctx.CallerMap.NextID(tq) + + tq.LayeredPoint = render.NewLayeredPoint(pos.X(), pos.Y(), layer) + tq.font = font + tq.queue = make([]queueItem, 0) + tq.sustainTime = sustainTime + + if initiatingEvent == "" { + initiatingEvent = DisplayTextEvent + } + + tBind := func(id event.CID, payload interface{}) int { + ent := ctx.CallerMap.GetEntity(id) + tq, ok := ent.(*TextQueue) + if !ok { + dlog.Error("expected TextQueue, got " + fmt.Sprintf("%T", ent)) + return 1 + } + str, ok := payload.(string) + if !ok { + dlog.Error("did not get string payload") + return 0 + } + r := tq.font.NewText(str, 0, 0) + m := r.ToSprite().Modify(mods.HighlightOff(colornames.Black, 2, 1, 1)) + tq.queueLock.Lock() + tq.queue = append([]queueItem{{ + mod: m, + dropAt: time.Now().Add(tq.sustainTime), + }}, tq.queue...) + tq.queueLock.Unlock() + return 0 + } + + ctx.EventHandler.Bind(initiatingEvent, tq.CID, tBind) + + return tq +} + +const DisplayTextEvent = "DisplayText" +const RecreationNeeded = "RecreationNeeded" + +const yBuffer = 3 + +func (tq *TextQueue) Draw(buff draw.Image, xOff, yOff float64) { + if len(tq.queue) == 0 { + return + } + now := time.Now() + secondFromNow := now.Add(1 * time.Second) + + if now.After(tq.queue[len(tq.queue)-1].dropAt) { + tq.queueLock.Lock() + tq.queue = tq.queue[:len(tq.queue)-1] + tq.queueLock.Unlock() + } + + xOff += tq.X() + yOff += tq.Y() + for _, item := range tq.queue { + _, y := item.mod.GetDims() + item.mod.Draw(buff, xOff, yOff) + if secondFromNow.After(item.dropAt) { + item.mod.Filter(mod.Fade(5)) + } + yOff += float64(y) + yBuffer + } +} + +func (tq *TextQueue) GetDims() (int, int) { + return 1, 1 +} + +func DisplayError(err error) { + if err != nil { + event.Trigger(DisplayTextEvent, err.Error()) + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..36f33b2 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Examples + +This directory contains extended examples beyond those found within Oak itself. diff --git a/code-examples/tiled-loader/go.mod b/examples/tiled-loader/go.mod similarity index 100% rename from code-examples/tiled-loader/go.mod rename to examples/tiled-loader/go.mod diff --git a/code-examples/tiled-loader/go.sum b/examples/tiled-loader/go.sum similarity index 100% rename from code-examples/tiled-loader/go.sum rename to examples/tiled-loader/go.sum diff --git a/code-examples/tiled-loader/main.go b/examples/tiled-loader/main.go similarity index 100% rename from code-examples/tiled-loader/main.go rename to examples/tiled-loader/main.go diff --git a/code-examples/tiled-loader/maps/basicTile.go b/examples/tiled-loader/maps/basicTile.go similarity index 100% rename from code-examples/tiled-loader/maps/basicTile.go rename to examples/tiled-loader/maps/basicTile.go diff --git a/code-examples/tiled-loader/maps/level.go b/examples/tiled-loader/maps/level.go similarity index 100% rename from code-examples/tiled-loader/maps/level.go rename to examples/tiled-loader/maps/level.go diff --git a/code-examples/tiled-loader/maps/loadMaps.go b/examples/tiled-loader/maps/loadMaps.go similarity index 100% rename from code-examples/tiled-loader/maps/loadMaps.go rename to examples/tiled-loader/maps/loadMaps.go diff --git a/code-examples/tiled-loader/maps/tile.go b/examples/tiled-loader/maps/tile.go similarity index 100% rename from code-examples/tiled-loader/maps/tile.go rename to examples/tiled-loader/maps/tile.go diff --git a/code-examples/tiled-loader/notes.md b/examples/tiled-loader/notes.md similarity index 100% rename from code-examples/tiled-loader/notes.md rename to examples/tiled-loader/notes.md diff --git a/utilities/README.md b/utilities/README.md new file mode 100644 index 0000000..36fffca --- /dev/null +++ b/utilities/README.md @@ -0,0 +1,3 @@ +# Utilities + +This directory contains build, CI, or testing, or other ancillary utilities that have been found to be useful in developing programs with Oak. diff --git a/utilities/jsonx/README.md b/utilities/jsonx/README.md new file mode 100644 index 0000000..e2bc6a1 --- /dev/null +++ b/utilities/jsonx/README.md @@ -0,0 +1,3 @@ +# jsonx + +The jsonx package contains utilities for storing additional types in json format-- e.g. colors and durations. diff --git a/utilities/jsonx/color.go b/utilities/jsonx/color.go new file mode 100644 index 0000000..69cc355 --- /dev/null +++ b/utilities/jsonx/color.go @@ -0,0 +1,81 @@ +package jsonx + +import ( + "encoding/json" + "errors" + "fmt" + "image" + "image/color" + "strings" + + "golang.org/x/image/colornames" +) + +const rgbaHexFormat = "%02X%02X%02X%02X" + +type ColorRGBA color.RGBA + +func (c ColorRGBA) MarshalJSON() ([]byte, error) { + s := fmt.Sprintf(rgbaHexFormat, c.R, c.G, c.B, c.A) + return json.Marshal(s) +} + +func (c *ColorRGBA) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case string: + s := strings.ToLower(value) + if c2, ok := colornames.Map[s]; ok { + *c = ColorRGBA(c2) + } else { + n, err := fmt.Sscanf(value, rgbaHexFormat, &c.R, &c.G, &c.B, &c.A) + if n != 4 { + return fmt.Errorf("not enough hex values (%v)", n) + } + if err != nil { + return fmt.Errorf("hex scan failed: %w", err) + } + } + return nil + default: + return errors.New("invalid color name") + } +} + +type ColorUniform image.Uniform + +func (c ColorUniform) MarshalJSON() ([]byte, error) { + r, g, b, a := c.C.RGBA() + s := fmt.Sprintf(rgbaHexFormat, r/255, g/255, b/255, a/255) + return []byte(s), nil +} + +func (c *ColorUniform) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case string: + rgba := color.RGBA{} + s := strings.ToLower(value) + if c2, ok := colornames.Map[s]; ok { + rgba = c2 + } else { + n, err := fmt.Sscanf(value, rgbaHexFormat, &rgba.R, &rgba.G, &rgba.B, &rgba.A) + if n != 4 { + return fmt.Errorf("not enough hex values (%v)", n) + } + if err != nil { + return fmt.Errorf("hex scan failed: %w", err) + } + } + *c = ColorUniform(*image.NewUniform(rgba)) + return nil + default: + return errors.New("invalid color name") + } +} diff --git a/utilities/jsonx/color_test.go b/utilities/jsonx/color_test.go new file mode 100644 index 0000000..bfc0cd6 --- /dev/null +++ b/utilities/jsonx/color_test.go @@ -0,0 +1,55 @@ +package jsonx_test + +import ( + "encoding/json" + + "bytes" + "image/color" + "testing" + + "github.com/oakmound/grove/utilities/jsonx" +) + +func TestColor(t *testing.T) { + type testCase struct { + name string + input color.RGBA + expectedBytes []byte + shouldErr bool + } + tcs := []testCase{ + { + name: "basic", + input: color.RGBA{255, 0, 0, 255}, + expectedBytes: []byte("\"FF0000FF\""), + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + cr := jsonx.ColorRGBA(tc.input) + outBytes, err := json.Marshal(cr) + if err != nil { + if !tc.shouldErr { + t.Fatalf("got error when no error was expected: %v", err) + } + } else { + if tc.shouldErr { + t.Fatal("got no error when error was expected") + } + if !bytes.Equal(tc.expectedBytes, outBytes) { + t.Fatalf("bytes expected %v vs got %v", string(tc.expectedBytes), string(outBytes)) + } + c2 := &jsonx.ColorRGBA{} + err := json.Unmarshal(outBytes, c2) + if err != nil { + // unmarshal after a successful marshal must not fail + t.Fatalf("unmarshal failed: %v", err) + } + if *c2 != cr { + t.Fatalf("unmarshal mismatched input") + } + } + }) + } +} diff --git a/utilities/jsonx/duration.go b/utilities/jsonx/duration.go new file mode 100644 index 0000000..95b31ab --- /dev/null +++ b/utilities/jsonx/duration.go @@ -0,0 +1,35 @@ +package jsonx + +import ( + "encoding/json" + "errors" + "time" +) + +// A Duration is a wrapper arouind time.Duration that allows for easier json formatting. +type Duration time.Duration + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).String()) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case float64: + *d = Duration(time.Duration(value)) + return nil + case string: + tmp, err := time.ParseDuration(value) + if err != nil { + return err + } + *d = Duration(tmp) + return nil + default: + return errors.New("invalid duration") + } +} diff --git a/utilities/jsonx/go.mod b/utilities/jsonx/go.mod new file mode 100644 index 0000000..75e0f75 --- /dev/null +++ b/utilities/jsonx/go.mod @@ -0,0 +1,5 @@ +module github.com/oakmound/grove/utilities/jsonx + +go 1.17 + +require golang.org/x/image v0.0.0-20211028202545-6944b10bf410 diff --git a/utilities/jsonx/go.sum b/utilities/jsonx/go.sum new file mode 100644 index 0000000..71c2453 --- /dev/null +++ b/utilities/jsonx/go.sum @@ -0,0 +1,4 @@ +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=