github.com/kintar/etxt@v0.0.9/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  }