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