github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/ebiten_no.go (about) 1 //go:build gtxt 2 3 package etxt 4 5 import "image" 6 import "image/draw" 7 import "image/color" 8 import "golang.org/x/image/math/fixed" 9 10 type TargetImage = draw.Image 11 type GlyphMask = *image.Alpha 12 13 type MixMode uint8 14 15 const ( 16 MixOver MixMode = 0 // glyphs drawn over target (default mode) 17 MixReplace MixMode = 1 // glyph mask only (transparent pixels included!) 18 MixAdd MixMode = 2 // add colors (black adds nothing, white stays white) 19 MixSub MixMode = 3 // subtract colors (black removes nothing) (alpha = target) 20 MixMultiply MixMode = 4 // multiply % of glyph and target colors and MixOver 21 MixCut MixMode = 5 // cut glyph shape hole based on alpha (cutout text) 22 MixFiftyFifty MixMode = 6 // mix glyph and target hues 50%-50% and MixOver 23 24 // TODO: many of the modes above will have some trouble with 25 // semi-transparency, I should look more into it. 26 ) 27 const defaultMixMode = MixOver 28 29 // this doesn't do anything in gtxt, only ebiten needs it 30 func convertAlphaImageToGlyphMask(i *image.Alpha) GlyphMask { return i } 31 32 // TODO: just remove glyph index from default draw func if unused everywhere. 33 // Maybe replace with ColorM? like... that would seem much more 34 // reasonable for traverse. we are doing a lot of color conversion each 35 // time. hmmmm... at least for ebitengine. 36 37 // The default glyph drawing function used in renderers. Do not confuse with 38 // the main [Renderer.Draw]() function. DefaultDrawFunc is a low level function, 39 // rarely necessary except when paired with [Renderer.Traverse]*() operations. 40 func (self *Renderer) DefaultDrawFunc(dot fixed.Point26_6, mask GlyphMask, _ GlyphIndex) { 41 if mask == nil { 42 return 43 } // spaces and empty glyphs will be nil 44 45 // compute src and target rects within bounds 46 targetBounds := self.target.Bounds() 47 srcRect := mask.Rect 48 shift := image.Pt(dot.X.Floor(), dot.Y.Floor()) 49 targetRect := targetBounds.Intersect(srcRect.Add(shift)) 50 if targetRect.Empty() { 51 return 52 } 53 shift.X, shift.Y = -shift.X, -shift.Y 54 srcRect = targetRect.Add(shift) 55 56 switch self.mixMode { 57 case MixReplace: // ---- source only ---- 58 self.mixImageInto(mask, self.target, srcRect, targetRect, 59 func(new, _ color.Color) color.Color { return new }) 60 case MixOver: // ---- default mixing ---- 61 self.mixImageInto(mask, self.target, srcRect, targetRect, mixOverFunc) 62 case MixCut: // ---- remove alpha mode ---- 63 self.mixImageInto(mask, self.target, srcRect, targetRect, 64 func(new, curr color.Color) color.Color { 65 _, _, _, na := new.RGBA() 66 if na == 0 { 67 return curr 68 } 69 cr, cg, cb, ca := curr.RGBA() 70 71 alpha := ca - na 72 if alpha < 0 { 73 alpha = 0 74 } 75 return color.RGBA64{ 76 R: min32As16(cr, alpha), 77 G: min32As16(cg, alpha), 78 B: min32As16(cb, alpha), 79 A: uint16(alpha), 80 } 81 }) 82 case MixMultiply: // ---- multiplicative mixing ---- 83 self.mixImageInto(mask, self.target, srcRect, targetRect, 84 func(new, curr color.Color) color.Color { 85 nr, ng, nb, na := new.RGBA() 86 cr, cg, cb, ca := curr.RGBA() 87 pureMult := color.RGBA64{ 88 R: uint16(nr * cr / 0xFFFF), 89 G: uint16(ng * cg / 0xFFFF), 90 B: uint16(nb * cb / 0xFFFF), 91 A: uint16(na * ca / 0xFFFF), 92 } 93 return mixOverFunc(pureMult, curr) 94 }) 95 case MixAdd: // --- additive mixing ---- 96 self.mixImageInto(mask, self.target, srcRect, targetRect, 97 func(new, curr color.Color) color.Color { 98 nr, ng, nb, na := new.RGBA() 99 if na == 0 { 100 return curr 101 } 102 cr, cg, cb, ca := curr.RGBA() 103 return color.RGBA64{ 104 R: uint16N(nr + cr), 105 G: uint16N(ng + cg), 106 B: uint16N(nb + cb), 107 A: uint16N(na + ca), 108 } 109 }) 110 case MixSub: // --- subtractive mixing (only color) ---- 111 self.mixImageInto(mask, self.target, srcRect, targetRect, 112 func(new, curr color.Color) color.Color { 113 nr, ng, nb, na := new.RGBA() 114 if na == 0 { 115 return curr 116 } 117 cr, cg, cb, ca := curr.RGBA() 118 return color.RGBA64{ 119 R: uint32subFloor16(cr, nr), 120 G: uint32subFloor16(cg, ng), 121 B: uint32subFloor16(cb, nb), 122 A: uint16(ca), 123 } 124 }) 125 case MixFiftyFifty: // ---- 50%-50% hue mixing ---- 126 self.mixImageInto(mask, self.target, srcRect, targetRect, 127 func(new, curr color.Color) color.Color { 128 var nr, ng, nb, na uint32 129 nrgba, isNrgba := new.(color.NRGBA64) 130 if isNrgba { 131 nr, ng, nb, na = uint32(nrgba.R), uint32(nrgba.G), uint32(nrgba.B), uint32(nrgba.A) 132 } else { 133 nr, ng, nb, na = new.RGBA() 134 } 135 if na == 0 { 136 return curr 137 } 138 if !isNrgba && na != 65535 { 139 panic("broken assumptions") 140 } 141 cr, cg, cb, ca := curr.RGBA() 142 alphaSum := na + ca 143 if alphaSum > 65535 { 144 alphaSum = 65535 145 } 146 partial := color.NRGBA64{ 147 R: uint16((nr + cr) / 2), 148 G: uint16((ng + cg) / 2), 149 B: uint16((nb + cb) / 2), 150 A: uint16(na), 151 } 152 return mixOverFunc(partial, curr) 153 }) 154 //case MixCMYK: 155 // pigment color mixing? mixbox? 156 // black = 1 - max(R, G, B)/255 157 // cyan = (1 - (R/255 - K))/(1 - K) 158 // magenta ... like cyan, but with G 159 // yellow ... like cyan, but with B 160 // TODO: what about shifting gradients with hue? 161 default: 162 panic("unexpected mix mode") 163 } 164 } 165 166 // All this code is extremely slow due to using a very straightforward 167 // implementation. Making this faster, though, is not so trivial. 168 func (self *Renderer) mixImageInto(src GlyphMask, target draw.Image, srcRect, tarRect image.Rectangle, mixFunc func(color.Color, color.Color) color.Color) { 169 width := srcRect.Dx() 170 height := srcRect.Dy() 171 srcOffX := srcRect.Min.X 172 srcOffY := srcRect.Min.Y 173 tarOffX := tarRect.Min.X 174 tarOffY := tarRect.Min.Y 175 176 r, g, b, a := self.mainColor.RGBA() 177 transColor := color.RGBA64{0, 0, 0, 0} 178 directColor := color.RGBA64{ 179 R: uint16(r), 180 G: uint16(g), 181 B: uint16(b), 182 A: uint16(a), 183 } 184 nrgba := color.NRGBA64{ 185 R: uint16(r), 186 G: uint16(g), 187 B: uint16(b), 188 A: 0, 189 } 190 191 for y := 0; y < height; y++ { 192 for x := 0; x < width; x++ { 193 // get mask alpha applied to our main drawing color 194 level := src.AlphaAt(srcOffX+x, srcOffY+y).A 195 var newColor color.Color 196 if level == 0 { 197 newColor = transColor 198 } else if level == 255 { 199 newColor = directColor 200 } else { 201 nrgba.A = uint16((a * uint32(level)) / 255) 202 newColor = nrgba 203 } 204 205 // get target current color and mix 206 currColor := target.At(tarOffX+x, tarOffY+y) 207 mixColor := mixFunc(newColor, currColor) 208 target.Set(tarOffX+x, tarOffY+y, mixColor) 209 } 210 } 211 } 212 213 func uint16N(value uint32) uint16 { 214 if value > 65535 { 215 return 65535 216 } 217 return uint16(value) 218 } 219 220 func uint32subFloor16(a, b uint32) uint16 { 221 if b >= a { 222 return 0 223 } 224 return uint16(a - b) 225 } 226 227 func min32As16(a, b uint32) uint16 { 228 if a <= b { 229 return uint16(a) 230 } 231 return uint16(b) 232 } 233 234 // ---- color mixing functions ---- 235 func mixOverFunc(new, curr color.Color) color.Color { 236 nr, ng, nb, na := new.RGBA() 237 if na == 0xFFFF { 238 return new 239 } 240 if na == 0 { 241 return curr 242 } 243 cr, cg, cb, ca := curr.RGBA() 244 if ca == 0 { 245 return new 246 } 247 248 return color.RGBA64{ 249 R: uint16N((nr*0xFFFF + cr*(0xFFFF-na)) / 0xFFFF), 250 G: uint16N((ng*0xFFFF + cg*(0xFFFF-na)) / 0xFFFF), 251 B: uint16N((nb*0xFFFF + cb*(0xFFFF-na)) / 0xFFFF), 252 A: uint16N((na*0xFFFF + ca*(0xFFFF-na)) / 0xFFFF), 253 } 254 }