github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/examples/gtxt/mirror/main.go (about)

     1  //go:build gtxt
     2  
     3  package main
     4  
     5  import "os"
     6  import "image"
     7  import "image/color"
     8  import "image/png"
     9  import "path/filepath"
    10  import "log"
    11  import "fmt"
    12  import "math/rand"
    13  
    14  import "golang.org/x/image/math/fixed"
    15  
    16  import "github.com/Kintar/etxt"
    17  
    18  // Must be compiled with '-tags gtxt'
    19  
    20  // NOTE: this is a rather advanced example. The renderer's DefaultDrawFunc
    21  //       is not enough like in gtxt/rainbow, so we will be doing some
    22  //       heavy lifting on our side..! If you aren't familiar with fixed
    23  //       point types, you might also want to take a look at this doc:
    24  //       >> https://github.com/Kintar/etxt/blob/main/docs/fixed-26-6.md
    25  
    26  const fontSize = 48
    27  
    28  func main() {
    29  	// get font path
    30  	if len(os.Args) != 2 {
    31  		msg := "Usage: expects one argument with the path to the font to be used\n"
    32  		fmt.Fprint(os.Stderr, msg)
    33  		os.Exit(1)
    34  	}
    35  
    36  	// parse font
    37  	font, fontName, err := etxt.ParseFontFrom(os.Args[1])
    38  	if err != nil {
    39  		log.Fatal(err)
    40  	}
    41  	fmt.Printf("Font loaded: %s\n", fontName)
    42  
    43  	// create cache
    44  	cache := etxt.NewDefaultCache(1024 * 1024 * 1024) // 1GB cache
    45  
    46  	// create and configure renderer
    47  	renderer := etxt.NewStdRenderer()
    48  	renderer.SetCacheHandler(cache.NewHandler())
    49  	renderer.SetSizePx(fontSize)
    50  	renderer.SetFont(font)
    51  	renderer.SetAlign(etxt.Baseline, etxt.XCenter)
    52  	renderer.SetColor(color.RGBA{255, 255, 255, 255}) // white
    53  
    54  	// create target image and fill it with black
    55  	outImage := image.NewRGBA(image.Rect(0, 0, 256, 128))
    56  	for i := 3; i < 256*128*4; i += 4 {
    57  		outImage.Pix[i] = 255
    58  	}
    59  
    60  	// set target and start drawing
    61  	renderer.SetTarget(outImage)
    62  	renderer.Traverse("Mirror...?", fixed.P(128, 64),
    63  		func(dot fixed.Point26_6, _ rune, glyphIndex etxt.GlyphIndex) {
    64  			// draw the "mirrored" glyph manually *first*, so if there's
    65  			// any overlap with the main glyph (because we are using a rather
    66  			// raw and basic method), the main glyph still gets drawn on top
    67  			mask := renderer.LoadGlyphMask(glyphIndex, dot)
    68  			customMirroredDraw(dot, mask, outImage)
    69  
    70  			// draw the normal letter now
    71  			renderer.DefaultDrawFunc(dot, mask, glyphIndex)
    72  		})
    73  
    74  	// store result as png
    75  	filename, err := filepath.Abs("gtxt_mirror.png")
    76  	if err != nil {
    77  		log.Fatal(err)
    78  	}
    79  	fmt.Printf("Output image: %s\n", filename)
    80  	file, err := os.Create(filename)
    81  	if err != nil {
    82  		log.Fatal(err)
    83  	}
    84  	err = png.Encode(file, outImage)
    85  	if err != nil {
    86  		log.Fatal(err)
    87  	}
    88  	err = file.Close()
    89  	if err != nil {
    90  		log.Fatal(err)
    91  	}
    92  	fmt.Print("Program exited successfully.\n")
    93  }
    94  
    95  // This is the hardcore part of this program. We will use the mask to
    96  // manually draw into the target, applying the given dot drawing position
    97  // and flipping the glyph and stuff.
    98  func customMirroredDraw(dot fixed.Point26_6, mask etxt.GlyphMask, target *image.RGBA) {
    99  	// to draw a mask into a target, we need to displace it by the
   100  	// current dot (drawing position) and be careful with clipping
   101  	srcRect, destRect := getDrawBounds(mask.Rect, target.Bounds(), dot)
   102  	if destRect.Empty() {
   103  		return
   104  	} // nothing to draw
   105  
   106  	// the destRect bounds are not appropriate here, since we want them
   107  	// to be mirrored. we could have done this in a single function, but
   108  	// the getDrawBounds function can be useful for you in other cases too,
   109  	// and this way we don't mix too much stuff in a single place.
   110  	// ...this also makes this code incorrect under some clipping cases,
   111  	//    but don't worry about it, we will just panic :D
   112  	yFlippingPoint := dot.Y.Floor()
   113  	above := yFlippingPoint - destRect.Min.Y
   114  	below := destRect.Max.Y - yFlippingPoint
   115  	if below < 0 {
   116  		below = -below
   117  	} // take the absolute value
   118  	shift := above - below
   119  	destRect = destRect.Add(image.Pt(0, shift))
   120  	clipped := target.Bounds().Intersect(destRect)
   121  	if clipped.Dy() != destRect.Dy() {
   122  		msg := "we panic because our code is weak. Here we would have to "
   123  		msg += "re-adjust the source (mask) rect too, but I'm too lazy and "
   124  		msg += "this doesn't happen if you keep reasonable text and target "
   125  		msg += "sizes"
   126  		panic(msg)
   127  	}
   128  
   129  	// we now have two rects that are the same size but identify
   130  	// different regions of the mask and target images. we can use
   131  	// them to read from one and draw on the other. yay.
   132  
   133  	// we start by creating some helper variables to make iteration
   134  	// through the rects more pleasant
   135  	width := srcRect.Dx()
   136  	height := srcRect.Dy()
   137  	srcOffX := srcRect.Min.X
   138  	srcOffY := srcRect.Min.Y
   139  	destOffX := destRect.Min.X
   140  	destOffY := destRect.Max.Y // (using max for vertical inversion)
   141  
   142  	// iterate the rects and draw!
   143  	for y := 0; y < height; y++ {
   144  		for x := 0; x < width; x++ {
   145  			// get mask alpha level
   146  			level := mask.AlphaAt(srcOffX+x, srcOffY+y).A
   147  			if level == 0 {
   148  				continue
   149  			} // non-filled part of the glyph
   150  
   151  			// actually, I also want to make the mirrored image fade out
   152  			// slightly, so let's apply attenuation based on the current y
   153  			attenuationFactor := float64(y) / float64(height)
   154  			attenuationFactor *= 0.76
   155  
   156  			// and let's add some noise too, why not...
   157  			noise := rand.Float64() * 70
   158  			flevel := float64(level)
   159  			if flevel <= noise {
   160  				noise = 0
   161  			}
   162  			level = uint8((flevel - noise) * attenuationFactor)
   163  
   164  			// now we finally can draw to the target
   165  			color := color.RGBA{level, level, level, 255} // some shade of gray
   166  			target.SetRGBA(destOffX+x, destOffY-y-1, color)
   167  		}
   168  	}
   169  }
   170  
   171  // When you have to draw a mask into a target, you need to displace it
   172  // based on the current drawing position and clip the resulting rect
   173  // if it goes out of the target. It's a bit tricky, so here's this nice
   174  // function that deals with it for you. You can reuse it for your own
   175  // code any time you need it. I even considered putting some of these
   176  // trickier functions in a subpackage, but copying is good enough too.
   177  func getDrawBounds(srcRect, targetRect image.Rectangle, dot fixed.Point26_6) (image.Rectangle, image.Rectangle) {
   178  	shift := image.Pt(dot.X.Floor(), dot.Y.Floor())
   179  	destRect := targetRect.Intersect(srcRect.Add(shift))
   180  	shift.X, shift.Y = -shift.X, -shift.Y
   181  	return destRect.Add(shift), destRect
   182  }