github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/examples/ebiten/rng_shape/main.go (about) 1 package main 2 3 import "os" 4 import "log" 5 import "fmt" 6 import "strconv" 7 import "strings" 8 import "image" 9 import "image/color" 10 import "image/png" 11 import "math/rand" 12 import "time" 13 14 import "github.com/hajimehoshi/ebiten/v2" 15 import "github.com/hajimehoshi/ebiten/v2/ebitenutil" 16 import "github.com/Kintar/etxt/emask" 17 import "golang.org/x/image/math/fixed" 18 19 // One day I was checking the results of emask.EdgeMarkerRasterizer 20 // and I decided to compare them with vector.Rasterizer. I made tests 21 // be randomized so I could be more confident that everything was 22 // good... but after having some trouble matching the results of the 23 // two, I exported the images for visual comparison and found out 24 // they were actually really cool! 25 // 26 // Imagine, writing tests leading to fancier results than when I 27 // intentionally try to make something look good >.< 28 // 29 // And that's the story of how this example was born. Does it have 30 // anything to do with etxt? Not really, but it's cool. I even added 31 // symmetries for extra fun! 32 33 func init() { 34 rand.Seed(time.Now().UnixNano()) 35 } 36 37 var keys = []ebiten.Key{ 38 ebiten.KeySpace, ebiten.KeyArrowUp, ebiten.KeyArrowDown, 39 ebiten.KeyE, ebiten.KeyM, ebiten.KeyH, ebiten.KeyF, 40 } 41 42 type Game struct { 43 keyPressed map[ebiten.Key]bool 44 rasterizer *emask.EdgeMarkerRasterizer 45 shape emask.Shape 46 size int 47 hideShortcuts bool 48 segments int 49 symmetryMode int // 0 = none, 1 = mirror, 2 = x2, 3 = diag.x2 50 originalImg *image.Alpha 51 symmetryImg *image.Alpha 52 ebiImg *ebiten.Image 53 } 54 55 func (self *Game) Layout(w, h int) (int, int) { return w, h } 56 func (self *Game) Update() error { 57 for _, key := range keys { 58 wasPressed := self.keyPressed[key] 59 isPressed := ebiten.IsKeyPressed(key) 60 self.keyPressed[key] = isPressed 61 if !wasPressed && isPressed { 62 switch key { 63 case ebiten.KeyH: 64 self.hideShortcuts = !self.hideShortcuts 65 case ebiten.KeyF: 66 ebiten.SetFullscreen(!ebiten.IsFullscreen()) 67 case ebiten.KeySpace: 68 err := self.newImage() 69 if err != nil { 70 return err 71 } 72 case ebiten.KeyArrowUp: 73 slow := ebiten.IsKeyPressed(ebiten.KeyShiftLeft) 74 if ebiten.IsKeyPressed(ebiten.KeyS) { 75 self.segments += 1 76 } else { 77 change := 20 78 if slow { 79 change = 1 80 } 81 self.size += change 82 } 83 case ebiten.KeyArrowDown: 84 slow := ebiten.IsKeyPressed(ebiten.KeyShiftLeft) 85 if ebiten.IsKeyPressed(ebiten.KeyS) { 86 self.segments -= 1 87 if self.segments < 3 { 88 self.segments = 3 89 } 90 } else { 91 change := 20 92 if slow { 93 change = 1 94 } 95 self.size -= change 96 if self.size < 50 { 97 self.size = 50 98 } 99 } 100 case ebiten.KeyM: // increase symmetry num 101 self.symmetryMode += 1 102 if self.symmetryMode == 4 { 103 self.symmetryMode = 0 104 } 105 self.refreshSymmetry() 106 case ebiten.KeyE: 107 // export image 108 file, err := os.Create("rng_shape.png") 109 if err != nil { 110 return err 111 } 112 err = png.Encode(file, self.symmetryImg) 113 if err != nil { 114 return err 115 } 116 err = file.Close() 117 if err != nil { 118 return err 119 } 120 121 // export raw data 122 file, err = os.Create("rng_shape.txt") 123 if err != nil { 124 return err 125 } 126 var strBuilder strings.Builder 127 for _, segment := range self.shape.Segments() { 128 strBuilder.WriteString(strconv.Itoa(int(segment.Op))) 129 for i := 0; i < 3; i++ { 130 strBuilder.WriteRune(' ') 131 point := segment.Args[i] 132 strBuilder.WriteString(strconv.Itoa(int(point.X))) 133 strBuilder.WriteRune(' ') 134 strBuilder.WriteString(strconv.Itoa(int(point.Y))) 135 } 136 strBuilder.WriteRune('\n') 137 } 138 _, err = file.WriteString(strBuilder.String()) 139 if err != nil { 140 return err 141 } 142 err = file.Close() 143 if err != nil { 144 return err 145 } 146 fmt.Print("Exported shape data successfully!\n") 147 default: 148 panic(key) 149 } 150 } 151 } 152 153 return nil 154 } 155 156 func (self *Game) newImage() error { 157 fsw, fsh := float64(self.size)*64, float64(self.size)*64 158 var makeXY = func() (fixed.Int26_6, fixed.Int26_6) { 159 return fixed.Int26_6(rand.Float64() * fsw), fixed.Int26_6(rand.Float64() * fsh) 160 } 161 startX, startY := makeXY() 162 self.shape = emask.NewShape(self.segments + 3) 163 self.shape.InvertY(true) 164 165 // trick to expand bounds 166 self.shape.MoveTo(0, 0) 167 self.shape.MoveTo(self.size, self.size) 168 169 // actual shape generation 170 self.shape.MoveToFract(startX, startY) 171 for i := 0; i < self.segments; i++ { 172 x, y := makeXY() 173 switch rand.Intn(3) { 174 case 0: // LineTo 175 self.shape.LineToFract(x, y) 176 case 1: // QuadTo 177 cx, cy := makeXY() 178 self.shape.QuadToFract(cx, cy, x, y) 179 case 2: // CubeTo 180 cx1, cy1 := makeXY() 181 cx2, cy2 := makeXY() 182 self.shape.CubeToFract(cx1, cy1, cx2, cy2, x, y) 183 } 184 } 185 self.shape.LineToFract(startX, startY) 186 var err error 187 self.originalImg, err = emask.Rasterize(self.shape.Segments(), self.rasterizer, fixed.Point26_6{}) 188 if err != nil { 189 return err 190 } 191 self.refreshSymmetry() 192 return nil 193 } 194 195 func (self *Game) refreshSymmetry() { 196 bounds := self.originalImg.Bounds() 197 w, h := bounds.Dx(), bounds.Dy() 198 if w != h { 199 panic("what?") 200 } 201 202 self.symmetryImg = image.NewAlpha(bounds) 203 copy(self.symmetryImg.Pix, self.originalImg.Pix) 204 205 switch self.symmetryMode { 206 case 0: 207 // nothing to do here 208 case 1: // mirror 209 xStart, yStart, xEnd, yEnd := 0, 0, w/2, h 210 pix := getImgRect(self.originalImg, xStart, yStart, xEnd, yEnd) 211 xStart, xEnd = w-1, w-w/2-1 212 setImgRect(self.symmetryImg, xStart, yStart, xEnd, yEnd, pix) 213 case 2: // x2 214 odd := (w % 2) 215 xStart, yStart, xEnd, yEnd := 0, 0, w/2+odd, h/2+odd 216 pix := getImgRect(self.originalImg, xStart, yStart, xEnd, yEnd) 217 xStart, xEnd = w-1, w-w/2-1-odd 218 setImgRect(self.symmetryImg, xStart, yStart, xEnd, yEnd, pix) 219 yStart, yEnd = h-1, h-h/2-1-odd 220 setImgRect(self.symmetryImg, xStart, yStart, xEnd, yEnd, pix) 221 xStart, xEnd = 0, w/2+odd 222 setImgRect(self.symmetryImg, xStart, yStart, xEnd, yEnd, pix) 223 case 3: // diag. x2 224 pix := getImgTopToCenterTriangle(self.originalImg) 225 setImgBottomToCenterTriangle(self.symmetryImg, pix) 226 setImgLeftToCenterTriangle(self.symmetryImg, pix) 227 setImgRightToCenterTriangle(self.symmetryImg, pix) 228 } 229 230 self.ebiImg = ebiten.NewImage(w, h) 231 self.ebiImg.Fill(color.Black) 232 img := ebiten.NewImageFromImage(self.symmetryImg) 233 self.ebiImg.DrawImage(img, nil) 234 } 235 236 func (self *Game) Draw(screen *ebiten.Image) { 237 sw, sh := screen.Size() 238 iw, ih := self.ebiImg.Size() 239 tx := (sw - iw) / 2 240 ty := (sh - ih) / 2 241 242 opts := &ebiten.DrawImageOptions{} 243 opts.GeoM.Translate(float64(tx), float64(ty)) 244 screen.DrawImage(self.ebiImg, opts) 245 246 if !self.hideShortcuts { 247 content := "export [E]\ngenerate [SPACE]\nsize " 248 content += strconv.Itoa(self.size) + " [UP/DOWN](+shift)\n" 249 switch self.symmetryMode { 250 case 0: 251 content += "no" 252 case 1: 253 content += "mirror" 254 case 2: 255 content += "x2" 256 case 3: 257 content += "diag.x2" 258 } 259 content += " symmetry [M]\nsegments " 260 content += strconv.Itoa(self.segments) + " [S + UP/DOWN]" 261 ebitenutil.DebugPrint(screen, content) 262 } 263 } 264 265 func main() { 266 fmt.Print("Instructions can be hidden with [H]\n") 267 fmt.Print("Fullscreen can be switched with [F]\n") 268 ebiten.SetWindowTitle("rng shapes") 269 ebiten.SetWindowResizable(true) 270 ebiten.SetWindowSize(640, 480) 271 game := &Game{ 272 rasterizer: emask.NewStdEdgeMarkerRasterizer(), 273 keyPressed: make(map[ebiten.Key]bool), 274 size: 476, 275 segments: 16, 276 symmetryMode: 1, 277 } 278 err := game.newImage() 279 if err != nil { 280 log.Fatal(err) 281 } 282 err = ebiten.RunGame(game) 283 if err != nil { 284 log.Fatal(err) 285 } 286 } 287 288 // --- lots of helper functions for symmetries --- 289 290 // Precondition: xStart <= xEnd, yStart <= yEnd 291 // Returns the colors of the given rect for the given image (xEnd and yEnd 292 // not included) as a single slice, from the top left to the bottom right. 293 func getImgRect(img *image.Alpha, xStart, yStart, xEnd, yEnd int) []color.Alpha { 294 result := make([]color.Alpha, 0, (xEnd-xStart)*(yEnd-yStart)) 295 for y := yStart; y < yEnd; y += 1 { 296 for x := xStart; x < xEnd; x += 1 { 297 result = append(result, img.AlphaAt(x, y)) 298 } 299 } 300 return result 301 } 302 303 // Sets the colors of the given rect on the given image with the contents 304 // of pix (xEnd and yEnd not included). xStart and yStart may be smaller 305 // than xEnd and yEnd, respectively, and the iteration direction will be 306 // changed. 307 func setImgRect(img *image.Alpha, xStart, yStart, xEnd, yEnd int, pix []color.Alpha) { 308 xChange, yChange := 1, 1 309 if xEnd < xStart { 310 xChange = -1 311 } 312 if yEnd < yStart { 313 yChange = -1 314 } 315 index := 0 316 for y := yStart; y != yEnd; y += yChange { 317 for x := xStart; x != xEnd; x += xChange { 318 img.SetAlpha(x, y, pix[index]) 319 index += 1 320 } 321 } 322 if index != len(pix) { 323 panic("incorrect pix len") 324 } 325 } 326 327 // Returns the colors of the triangle that goes from the top corners of 328 // the given image to its center, as a single slice, from left to right, 329 // top to bottom. 330 func getImgTopToCenterTriangle(img *image.Alpha) []color.Alpha { 331 bounds := img.Bounds() 332 height := bounds.Dy() 333 xStart, xEnd := bounds.Min.X, bounds.Max.X 334 yStart, yEnd := bounds.Min.Y, bounds.Min.Y+height/2 335 336 result := make([]color.Alpha, 0, (height*height)/4) 337 offset := 0 // increased for each triangle row 338 for y := yStart; y < yEnd; y += 1 { 339 for x := xStart + offset; x < xEnd-offset; x += 1 { 340 result = append(result, img.AlphaAt(x, y)) 341 } 342 offset += 1 343 } 344 return result 345 } 346 347 // Sets the colors of the triangle that goes from the bottom corners of 348 // the given image to its center, from right to left and bottom to top. 349 func setImgBottomToCenterTriangle(img *image.Alpha, pix []color.Alpha) { 350 bounds := img.Bounds() 351 xStart, xEnd := bounds.Min.X, bounds.Max.X-1 352 yStart, yEnd := bounds.Max.Y-1, bounds.Max.Y-bounds.Dy()/2 353 354 index := 0 // pix index 355 offset := 0 // increased for each triangle row 356 for y := yStart; y >= yEnd; y -= 1 { 357 for x := xEnd - offset; x >= xStart+offset; x -= 1 { 358 img.SetAlpha(x, y, pix[index]) 359 index += 1 360 } 361 offset += 1 362 } 363 if index != len(pix) { 364 panic("incorrect pix len") 365 } 366 } 367 368 // Sets the colors of the triangle that goes from the left corners of 369 // the given image to its center, from top to bottom and left to right. 370 func setImgLeftToCenterTriangle(img *image.Alpha, pix []color.Alpha) { 371 bounds := img.Bounds() 372 xStart, xEnd := bounds.Min.X, bounds.Min.X+bounds.Dx()/2 373 yStart, yEnd := bounds.Min.Y, bounds.Max.Y 374 375 index := 0 // pix index 376 offset := 0 // increased for each triangle column 377 for x := xStart; x < xEnd; x += 1 { 378 for y := yStart + offset; y < yEnd-offset; y += 1 { 379 img.SetAlpha(x, y, pix[index]) 380 index += 1 381 } 382 offset += 1 383 } 384 if index != len(pix) { 385 panic("incorrect pix len") 386 } 387 } 388 389 // Sets the colors of the triangle that goes from the right corners of 390 // the given image to its center, from bottom to top and right to left. 391 func setImgRightToCenterTriangle(img *image.Alpha, pix []color.Alpha) { 392 bounds := img.Bounds() 393 xStart, xEnd := bounds.Max.X-1, bounds.Max.X-bounds.Dx()/2 394 yStart, yEnd := bounds.Max.Y-1, bounds.Min.Y 395 396 index := 0 // pix index 397 offset := 0 // increased for each triangle column 398 for x := xStart; x >= xEnd; x -= 1 { 399 for y := yStart - offset; y >= yEnd+offset; y -= 1 { 400 img.SetAlpha(x, y, pix[index]) 401 index += 1 402 } 403 offset += 1 404 } 405 if index != len(pix) { 406 panic("incorrect pix len") 407 } 408 }