├── .gitignore ├── LICENSE.md ├── README.md ├── bezier.go ├── context.go ├── context_test.go ├── examples ├── baboon.png ├── beziers.go ├── circle.go ├── clip.go ├── concat.go ├── crisp.go ├── cubic.go ├── ellipse.go ├── gofont.go ├── gopher.png ├── gradient-conic.go ├── gradient-linear.go ├── gradient-radial.go ├── gradient-text.go ├── invert-mask.go ├── lines.go ├── linewidth.go ├── lorem.go ├── mask.go ├── meme.go ├── openfill.go ├── pattern-fill.go ├── quadratic.go ├── rotated-image.go ├── rotated-text.go ├── scatter.go ├── sine.go ├── spiral.go ├── star.go ├── stars.go ├── text.go ├── tiling.go ├── unicode.go └── wrap.go ├── gradient.go ├── matrix.go ├── path.go ├── pattern.go ├── point.go ├── util.go └── wrap.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Graphics 2 | 3 | `gg` is a library for rendering 2D graphics in pure Go. 4 | 5 | ![Stars](http://i.imgur.com/CylQIJt.png) 6 | 7 | ## Installation 8 | 9 | go get -u github.com/fogleman/gg 10 | 11 | Alternatively, you may use gopkg.in to grab a specific major-version: 12 | 13 | go get -u gopkg.in/fogleman/gg.v1 14 | 15 | ## Documentation 16 | 17 | - godoc: https://godoc.org/github.com/fogleman/gg 18 | - pkg.go.dev: https://pkg.go.dev/github.com/fogleman/gg?tab=doc 19 | 20 | ## Hello, Circle! 21 | 22 | Look how easy! 23 | 24 | ```go 25 | package main 26 | 27 | import "github.com/fogleman/gg" 28 | 29 | func main() { 30 | dc := gg.NewContext(1000, 1000) 31 | dc.DrawCircle(500, 500, 400) 32 | dc.SetRGB(0, 0, 0) 33 | dc.Fill() 34 | dc.SavePNG("out.png") 35 | } 36 | ``` 37 | 38 | ## Examples 39 | 40 | There are [lots of examples](https://github.com/fogleman/gg/tree/master/examples) included. They're mostly for testing the code, but they're good for learning, too. 41 | 42 | ![Examples](http://i.imgur.com/tMFoyzu.png) 43 | 44 | ## Creating Contexts 45 | 46 | There are a few ways of creating a context. 47 | 48 | ```go 49 | NewContext(width, height int) *Context 50 | NewContextForImage(im image.Image) *Context 51 | NewContextForRGBA(im *image.RGBA) *Context 52 | ``` 53 | 54 | ## Drawing Functions 55 | 56 | Ever used a graphics library that didn't have functions for drawing rectangles 57 | or circles? What a pain! 58 | 59 | ```go 60 | DrawPoint(x, y, r float64) 61 | DrawLine(x1, y1, x2, y2 float64) 62 | DrawRectangle(x, y, w, h float64) 63 | DrawRoundedRectangle(x, y, w, h, r float64) 64 | DrawCircle(x, y, r float64) 65 | DrawArc(x, y, r, angle1, angle2 float64) 66 | DrawEllipse(x, y, rx, ry float64) 67 | DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) 68 | DrawRegularPolygon(n int, x, y, r, rotation float64) 69 | DrawImage(im image.Image, x, y int) 70 | DrawImageAnchored(im image.Image, x, y int, ax, ay float64) 71 | SetPixel(x, y int) 72 | 73 | MoveTo(x, y float64) 74 | LineTo(x, y float64) 75 | QuadraticTo(x1, y1, x2, y2 float64) 76 | CubicTo(x1, y1, x2, y2, x3, y3 float64) 77 | ClosePath() 78 | ClearPath() 79 | NewSubPath() 80 | 81 | Clear() 82 | Stroke() 83 | Fill() 84 | StrokePreserve() 85 | FillPreserve() 86 | ``` 87 | 88 | It is often desired to center an image at a point. Use `DrawImageAnchored` with `ax` and `ay` set to 0.5 to do this. Use 0 to left or top align. Use 1 to right or bottom align. `DrawStringAnchored` does the same for text, so you don't need to call `MeasureString` yourself. 89 | 90 | ## Text Functions 91 | 92 | It will even do word wrap for you! 93 | 94 | ```go 95 | DrawString(s string, x, y float64) 96 | DrawStringAnchored(s string, x, y, ax, ay float64) 97 | DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) 98 | MeasureString(s string) (w, h float64) 99 | MeasureMultilineString(s string, lineSpacing float64) (w, h float64) 100 | WordWrap(s string, w float64) []string 101 | SetFontFace(fontFace font.Face) 102 | LoadFontFace(path string, points float64) error 103 | ``` 104 | 105 | ## Color Functions 106 | 107 | Colors can be set in several different ways for your convenience. 108 | 109 | ```go 110 | SetRGB(r, g, b float64) 111 | SetRGBA(r, g, b, a float64) 112 | SetRGB255(r, g, b int) 113 | SetRGBA255(r, g, b, a int) 114 | SetColor(c color.Color) 115 | SetHexColor(x string) 116 | ``` 117 | 118 | ## Stroke & Fill Options 119 | 120 | ```go 121 | SetLineWidth(lineWidth float64) 122 | SetLineCap(lineCap LineCap) 123 | SetLineJoin(lineJoin LineJoin) 124 | SetDash(dashes ...float64) 125 | SetDashOffset(offset float64) 126 | SetFillRule(fillRule FillRule) 127 | ``` 128 | 129 | ## Gradients & Patterns 130 | 131 | `gg` supports linear, radial and conic gradients and surface patterns. You can also implement your own patterns. 132 | 133 | ```go 134 | SetFillStyle(pattern Pattern) 135 | SetStrokeStyle(pattern Pattern) 136 | NewSolidPattern(color color.Color) 137 | NewLinearGradient(x0, y0, x1, y1 float64) 138 | NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) 139 | NewConicGradient(cx, cy, deg float64) 140 | NewSurfacePattern(im image.Image, op RepeatOp) 141 | ``` 142 | 143 | ## Transformation Functions 144 | 145 | ```go 146 | Identity() 147 | Translate(x, y float64) 148 | Scale(x, y float64) 149 | Rotate(angle float64) 150 | Shear(x, y float64) 151 | ScaleAbout(sx, sy, x, y float64) 152 | RotateAbout(angle, x, y float64) 153 | ShearAbout(sx, sy, x, y float64) 154 | TransformPoint(x, y float64) (tx, ty float64) 155 | InvertY() 156 | ``` 157 | 158 | It is often desired to rotate or scale about a point that is not the origin. The functions `RotateAbout`, `ScaleAbout`, `ShearAbout` are provided as a convenience. 159 | 160 | `InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom. 161 | 162 | ## Stack Functions 163 | 164 | Save and restore the state of the context. These can be nested. 165 | 166 | ```go 167 | Push() 168 | Pop() 169 | ``` 170 | 171 | ## Clipping Functions 172 | 173 | Use clipping regions to restrict drawing operations to an area that you 174 | defined using paths. 175 | 176 | ```go 177 | Clip() 178 | ClipPreserve() 179 | ResetClip() 180 | AsMask() *image.Alpha 181 | SetMask(mask *image.Alpha) 182 | InvertMask() 183 | ``` 184 | 185 | ## Helper Functions 186 | 187 | Sometimes you just don't want to write these yourself. 188 | 189 | ```go 190 | Radians(degrees float64) float64 191 | Degrees(radians float64) float64 192 | LoadImage(path string) (image.Image, error) 193 | LoadPNG(path string) (image.Image, error) 194 | SavePNG(path string, im image.Image) error 195 | ``` 196 | 197 | ![Separator](http://i.imgur.com/fsUvnPB.png) 198 | 199 | ## Another Example 200 | 201 | See the output of this example below. 202 | 203 | ```go 204 | package main 205 | 206 | import "github.com/fogleman/gg" 207 | 208 | func main() { 209 | const S = 1024 210 | dc := gg.NewContext(S, S) 211 | dc.SetRGBA(0, 0, 0, 0.1) 212 | for i := 0; i < 360; i += 15 { 213 | dc.Push() 214 | dc.RotateAbout(gg.Radians(float64(i)), S/2, S/2) 215 | dc.DrawEllipse(S/2, S/2, S*7/16, S/8) 216 | dc.Fill() 217 | dc.Pop() 218 | } 219 | dc.SavePNG("out.png") 220 | } 221 | ``` 222 | 223 | ![Ellipses](http://i.imgur.com/J9CBZef.png) 224 | -------------------------------------------------------------------------------- /bezier.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "math" 4 | 5 | func quadratic(x0, y0, x1, y1, x2, y2, t float64) (x, y float64) { 6 | u := 1 - t 7 | a := u * u 8 | b := 2 * u * t 9 | c := t * t 10 | x = a*x0 + b*x1 + c*x2 11 | y = a*y0 + b*y1 + c*y2 12 | return 13 | } 14 | 15 | func QuadraticBezier(x0, y0, x1, y1, x2, y2 float64) []Point { 16 | l := (math.Hypot(x1-x0, y1-y0) + 17 | math.Hypot(x2-x1, y2-y1)) 18 | n := int(l + 0.5) 19 | if n < 4 { 20 | n = 4 21 | } 22 | d := float64(n) - 1 23 | result := make([]Point, n) 24 | for i := 0; i < n; i++ { 25 | t := float64(i) / d 26 | x, y := quadratic(x0, y0, x1, y1, x2, y2, t) 27 | result[i] = Point{x, y} 28 | } 29 | return result 30 | } 31 | 32 | func cubic(x0, y0, x1, y1, x2, y2, x3, y3, t float64) (x, y float64) { 33 | u := 1 - t 34 | a := u * u * u 35 | b := 3 * u * u * t 36 | c := 3 * u * t * t 37 | d := t * t * t 38 | x = a*x0 + b*x1 + c*x2 + d*x3 39 | y = a*y0 + b*y1 + c*y2 + d*y3 40 | return 41 | } 42 | 43 | func CubicBezier(x0, y0, x1, y1, x2, y2, x3, y3 float64) []Point { 44 | l := (math.Hypot(x1-x0, y1-y0) + 45 | math.Hypot(x2-x1, y2-y1) + 46 | math.Hypot(x3-x2, y3-y2)) 47 | n := int(l + 0.5) 48 | if n < 4 { 49 | n = 4 50 | } 51 | d := float64(n) - 1 52 | result := make([]Point, n) 53 | for i := 0; i < n; i++ { 54 | t := float64(i) / d 55 | x, y := cubic(x0, y0, x1, y1, x2, y2, x3, y3, t) 56 | result[i] = Point{x, y} 57 | } 58 | return result 59 | } 60 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Package gg provides a simple API for rendering 2D graphics in pure Go. 2 | package gg 3 | 4 | import ( 5 | "errors" 6 | "image" 7 | "image/color" 8 | "image/jpeg" 9 | "image/png" 10 | "io" 11 | "math" 12 | "strings" 13 | 14 | "github.com/golang/freetype/raster" 15 | "golang.org/x/image/draw" 16 | "golang.org/x/image/font" 17 | "golang.org/x/image/font/basicfont" 18 | "golang.org/x/image/math/f64" 19 | ) 20 | 21 | type LineCap int 22 | 23 | const ( 24 | LineCapRound LineCap = iota 25 | LineCapButt 26 | LineCapSquare 27 | ) 28 | 29 | type LineJoin int 30 | 31 | const ( 32 | LineJoinRound LineJoin = iota 33 | LineJoinBevel 34 | ) 35 | 36 | type FillRule int 37 | 38 | const ( 39 | FillRuleWinding FillRule = iota 40 | FillRuleEvenOdd 41 | ) 42 | 43 | type Align int 44 | 45 | const ( 46 | AlignLeft Align = iota 47 | AlignCenter 48 | AlignRight 49 | ) 50 | 51 | var ( 52 | defaultFillStyle = NewSolidPattern(color.White) 53 | defaultStrokeStyle = NewSolidPattern(color.Black) 54 | ) 55 | 56 | type Context struct { 57 | width int 58 | height int 59 | rasterizer *raster.Rasterizer 60 | im *image.RGBA 61 | mask *image.Alpha 62 | color color.Color 63 | fillPattern Pattern 64 | strokePattern Pattern 65 | strokePath raster.Path 66 | fillPath raster.Path 67 | start Point 68 | current Point 69 | hasCurrent bool 70 | dashes []float64 71 | dashOffset float64 72 | lineWidth float64 73 | lineCap LineCap 74 | lineJoin LineJoin 75 | fillRule FillRule 76 | fontFace font.Face 77 | fontHeight float64 78 | matrix Matrix 79 | stack []*Context 80 | } 81 | 82 | // NewContext creates a new image.RGBA with the specified width and height 83 | // and prepares a context for rendering onto that image. 84 | func NewContext(width, height int) *Context { 85 | return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height))) 86 | } 87 | 88 | // NewContextForImage copies the specified image into a new image.RGBA 89 | // and prepares a context for rendering onto that image. 90 | func NewContextForImage(im image.Image) *Context { 91 | return NewContextForRGBA(imageToRGBA(im)) 92 | } 93 | 94 | // NewContextForRGBA prepares a context for rendering onto the specified image. 95 | // No copy is made. 96 | func NewContextForRGBA(im *image.RGBA) *Context { 97 | w := im.Bounds().Size().X 98 | h := im.Bounds().Size().Y 99 | return &Context{ 100 | width: w, 101 | height: h, 102 | rasterizer: raster.NewRasterizer(w, h), 103 | im: im, 104 | color: color.Transparent, 105 | fillPattern: defaultFillStyle, 106 | strokePattern: defaultStrokeStyle, 107 | lineWidth: 1, 108 | fillRule: FillRuleWinding, 109 | fontFace: basicfont.Face7x13, 110 | fontHeight: 13, 111 | matrix: Identity(), 112 | } 113 | } 114 | 115 | // GetCurrentPoint will return the current point and if there is a current point. 116 | // The point will have been transformed by the context's transformation matrix. 117 | func (dc *Context) GetCurrentPoint() (Point, bool) { 118 | if dc.hasCurrent { 119 | return dc.current, true 120 | } 121 | return Point{}, false 122 | } 123 | 124 | // Image returns the image that has been drawn by this context. 125 | func (dc *Context) Image() image.Image { 126 | return dc.im 127 | } 128 | 129 | // Width returns the width of the image in pixels. 130 | func (dc *Context) Width() int { 131 | return dc.width 132 | } 133 | 134 | // Height returns the height of the image in pixels. 135 | func (dc *Context) Height() int { 136 | return dc.height 137 | } 138 | 139 | // SavePNG encodes the image as a PNG and writes it to disk. 140 | func (dc *Context) SavePNG(path string) error { 141 | return SavePNG(path, dc.im) 142 | } 143 | 144 | // SaveJPG encodes the image as a JPG and writes it to disk. 145 | func (dc *Context) SaveJPG(path string, quality int) error { 146 | return SaveJPG(path, dc.im, quality) 147 | } 148 | 149 | // EncodePNG encodes the image as a PNG and writes it to the provided io.Writer. 150 | func (dc *Context) EncodePNG(w io.Writer) error { 151 | return png.Encode(w, dc.im) 152 | } 153 | 154 | // EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer 155 | // in JPEG 4:2:0 baseline format with the given options. 156 | // Default parameters are used if a nil *jpeg.Options is passed. 157 | func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error { 158 | return jpeg.Encode(w, dc.im, o) 159 | } 160 | 161 | // SetDash sets the current dash pattern to use. Call with zero arguments to 162 | // disable dashes. The values specify the lengths of each dash, with 163 | // alternating on and off lengths. 164 | func (dc *Context) SetDash(dashes ...float64) { 165 | dc.dashes = dashes 166 | } 167 | 168 | // SetDashOffset sets the initial offset into the dash pattern to use when 169 | // stroking dashed paths. 170 | func (dc *Context) SetDashOffset(offset float64) { 171 | dc.dashOffset = offset 172 | } 173 | 174 | func (dc *Context) SetLineWidth(lineWidth float64) { 175 | dc.lineWidth = lineWidth 176 | } 177 | 178 | func (dc *Context) SetLineCap(lineCap LineCap) { 179 | dc.lineCap = lineCap 180 | } 181 | 182 | func (dc *Context) SetLineCapRound() { 183 | dc.lineCap = LineCapRound 184 | } 185 | 186 | func (dc *Context) SetLineCapButt() { 187 | dc.lineCap = LineCapButt 188 | } 189 | 190 | func (dc *Context) SetLineCapSquare() { 191 | dc.lineCap = LineCapSquare 192 | } 193 | 194 | func (dc *Context) SetLineJoin(lineJoin LineJoin) { 195 | dc.lineJoin = lineJoin 196 | } 197 | 198 | func (dc *Context) SetLineJoinRound() { 199 | dc.lineJoin = LineJoinRound 200 | } 201 | 202 | func (dc *Context) SetLineJoinBevel() { 203 | dc.lineJoin = LineJoinBevel 204 | } 205 | 206 | func (dc *Context) SetFillRule(fillRule FillRule) { 207 | dc.fillRule = fillRule 208 | } 209 | 210 | func (dc *Context) SetFillRuleWinding() { 211 | dc.fillRule = FillRuleWinding 212 | } 213 | 214 | func (dc *Context) SetFillRuleEvenOdd() { 215 | dc.fillRule = FillRuleEvenOdd 216 | } 217 | 218 | // Color Setters 219 | 220 | func (dc *Context) setFillAndStrokeColor(c color.Color) { 221 | dc.color = c 222 | dc.fillPattern = NewSolidPattern(c) 223 | dc.strokePattern = NewSolidPattern(c) 224 | } 225 | 226 | // SetFillStyle sets current fill style 227 | func (dc *Context) SetFillStyle(pattern Pattern) { 228 | // if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString) 229 | if fillStyle, ok := pattern.(*solidPattern); ok { 230 | dc.color = fillStyle.color 231 | } 232 | dc.fillPattern = pattern 233 | } 234 | 235 | // SetStrokeStyle sets current stroke style 236 | func (dc *Context) SetStrokeStyle(pattern Pattern) { 237 | dc.strokePattern = pattern 238 | } 239 | 240 | // SetColor sets the current color(for both fill and stroke). 241 | func (dc *Context) SetColor(c color.Color) { 242 | dc.setFillAndStrokeColor(c) 243 | } 244 | 245 | // SetHexColor sets the current color using a hex string. The leading pound 246 | // sign (#) is optional. Both 3- and 6-digit variations are supported. 8 digits 247 | // may be provided to set the alpha value as well. 248 | func (dc *Context) SetHexColor(x string) { 249 | r, g, b, a := parseHexColor(x) 250 | dc.SetRGBA255(r, g, b, a) 251 | } 252 | 253 | // SetRGBA255 sets the current color. r, g, b, a values should be between 0 and 254 | // 255, inclusive. 255 | func (dc *Context) SetRGBA255(r, g, b, a int) { 256 | dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} 257 | dc.setFillAndStrokeColor(dc.color) 258 | } 259 | 260 | // SetRGB255 sets the current color. r, g, b values should be between 0 and 255, 261 | // inclusive. Alpha will be set to 255 (fully opaque). 262 | func (dc *Context) SetRGB255(r, g, b int) { 263 | dc.SetRGBA255(r, g, b, 255) 264 | } 265 | 266 | // SetRGBA sets the current color. r, g, b, a values should be between 0 and 1, 267 | // inclusive. 268 | func (dc *Context) SetRGBA(r, g, b, a float64) { 269 | dc.color = color.NRGBA{ 270 | uint8(r * 255), 271 | uint8(g * 255), 272 | uint8(b * 255), 273 | uint8(a * 255), 274 | } 275 | dc.setFillAndStrokeColor(dc.color) 276 | } 277 | 278 | // SetRGB sets the current color. r, g, b values should be between 0 and 1, 279 | // inclusive. Alpha will be set to 1 (fully opaque). 280 | func (dc *Context) SetRGB(r, g, b float64) { 281 | dc.SetRGBA(r, g, b, 1) 282 | } 283 | 284 | // Path Manipulation 285 | 286 | // MoveTo starts a new subpath within the current path starting at the 287 | // specified point. 288 | func (dc *Context) MoveTo(x, y float64) { 289 | if dc.hasCurrent { 290 | dc.fillPath.Add1(dc.start.Fixed()) 291 | } 292 | x, y = dc.TransformPoint(x, y) 293 | p := Point{x, y} 294 | dc.strokePath.Start(p.Fixed()) 295 | dc.fillPath.Start(p.Fixed()) 296 | dc.start = p 297 | dc.current = p 298 | dc.hasCurrent = true 299 | } 300 | 301 | // LineTo adds a line segment to the current path starting at the current 302 | // point. If there is no current point, it is equivalent to MoveTo(x, y) 303 | func (dc *Context) LineTo(x, y float64) { 304 | if !dc.hasCurrent { 305 | dc.MoveTo(x, y) 306 | } else { 307 | x, y = dc.TransformPoint(x, y) 308 | p := Point{x, y} 309 | dc.strokePath.Add1(p.Fixed()) 310 | dc.fillPath.Add1(p.Fixed()) 311 | dc.current = p 312 | } 313 | } 314 | 315 | // QuadraticTo adds a quadratic bezier curve to the current path starting at 316 | // the current point. If there is no current point, it first performs 317 | // MoveTo(x1, y1) 318 | func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) { 319 | if !dc.hasCurrent { 320 | dc.MoveTo(x1, y1) 321 | } 322 | x1, y1 = dc.TransformPoint(x1, y1) 323 | x2, y2 = dc.TransformPoint(x2, y2) 324 | p1 := Point{x1, y1} 325 | p2 := Point{x2, y2} 326 | dc.strokePath.Add2(p1.Fixed(), p2.Fixed()) 327 | dc.fillPath.Add2(p1.Fixed(), p2.Fixed()) 328 | dc.current = p2 329 | } 330 | 331 | // CubicTo adds a cubic bezier curve to the current path starting at the 332 | // current point. If there is no current point, it first performs 333 | // MoveTo(x1, y1). Because freetype/raster does not support cubic beziers, 334 | // this is emulated with many small line segments. 335 | func (dc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float64) { 336 | if !dc.hasCurrent { 337 | dc.MoveTo(x1, y1) 338 | } 339 | x0, y0 := dc.current.X, dc.current.Y 340 | x1, y1 = dc.TransformPoint(x1, y1) 341 | x2, y2 = dc.TransformPoint(x2, y2) 342 | x3, y3 = dc.TransformPoint(x3, y3) 343 | points := CubicBezier(x0, y0, x1, y1, x2, y2, x3, y3) 344 | previous := dc.current.Fixed() 345 | for _, p := range points[1:] { 346 | f := p.Fixed() 347 | if f == previous { 348 | // TODO: this fixes some rendering issues but not all 349 | continue 350 | } 351 | previous = f 352 | dc.strokePath.Add1(f) 353 | dc.fillPath.Add1(f) 354 | dc.current = p 355 | } 356 | } 357 | 358 | // ClosePath adds a line segment from the current point to the beginning 359 | // of the current subpath. If there is no current point, this is a no-op. 360 | func (dc *Context) ClosePath() { 361 | if dc.hasCurrent { 362 | dc.strokePath.Add1(dc.start.Fixed()) 363 | dc.fillPath.Add1(dc.start.Fixed()) 364 | dc.current = dc.start 365 | } 366 | } 367 | 368 | // ClearPath clears the current path. There is no current point after this 369 | // operation. 370 | func (dc *Context) ClearPath() { 371 | dc.strokePath.Clear() 372 | dc.fillPath.Clear() 373 | dc.hasCurrent = false 374 | } 375 | 376 | // NewSubPath starts a new subpath within the current path. There is no current 377 | // point after this operation. 378 | func (dc *Context) NewSubPath() { 379 | if dc.hasCurrent { 380 | dc.fillPath.Add1(dc.start.Fixed()) 381 | } 382 | dc.hasCurrent = false 383 | } 384 | 385 | // Path Drawing 386 | 387 | func (dc *Context) capper() raster.Capper { 388 | switch dc.lineCap { 389 | case LineCapButt: 390 | return raster.ButtCapper 391 | case LineCapRound: 392 | return raster.RoundCapper 393 | case LineCapSquare: 394 | return raster.SquareCapper 395 | } 396 | return nil 397 | } 398 | 399 | func (dc *Context) joiner() raster.Joiner { 400 | switch dc.lineJoin { 401 | case LineJoinBevel: 402 | return raster.BevelJoiner 403 | case LineJoinRound: 404 | return raster.RoundJoiner 405 | } 406 | return nil 407 | } 408 | 409 | func (dc *Context) stroke(painter raster.Painter) { 410 | path := dc.strokePath 411 | if len(dc.dashes) > 0 { 412 | path = dashed(path, dc.dashes, dc.dashOffset) 413 | } else { 414 | // TODO: this is a temporary workaround to remove tiny segments 415 | // that result in rendering issues 416 | path = rasterPath(flattenPath(path)) 417 | } 418 | r := dc.rasterizer 419 | r.UseNonZeroWinding = true 420 | r.Clear() 421 | r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner()) 422 | r.Rasterize(painter) 423 | } 424 | 425 | func (dc *Context) fill(painter raster.Painter) { 426 | path := dc.fillPath 427 | if dc.hasCurrent { 428 | path = make(raster.Path, len(dc.fillPath)) 429 | copy(path, dc.fillPath) 430 | path.Add1(dc.start.Fixed()) 431 | } 432 | r := dc.rasterizer 433 | r.UseNonZeroWinding = dc.fillRule == FillRuleWinding 434 | r.Clear() 435 | r.AddPath(path) 436 | r.Rasterize(painter) 437 | } 438 | 439 | // StrokePreserve strokes the current path with the current color, line width, 440 | // line cap, line join and dash settings. The path is preserved after this 441 | // operation. 442 | func (dc *Context) StrokePreserve() { 443 | var painter raster.Painter 444 | if dc.mask == nil { 445 | if pattern, ok := dc.strokePattern.(*solidPattern); ok { 446 | // with a nil mask and a solid color pattern, we can be more efficient 447 | // TODO: refactor so we don't have to do this type assertion stuff? 448 | p := raster.NewRGBAPainter(dc.im) 449 | p.SetColor(pattern.color) 450 | painter = p 451 | } 452 | } 453 | if painter == nil { 454 | painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern) 455 | } 456 | dc.stroke(painter) 457 | } 458 | 459 | // Stroke strokes the current path with the current color, line width, 460 | // line cap, line join and dash settings. The path is cleared after this 461 | // operation. 462 | func (dc *Context) Stroke() { 463 | dc.StrokePreserve() 464 | dc.ClearPath() 465 | } 466 | 467 | // FillPreserve fills the current path with the current color. Open subpaths 468 | // are implicity closed. The path is preserved after this operation. 469 | func (dc *Context) FillPreserve() { 470 | var painter raster.Painter 471 | if dc.mask == nil { 472 | if pattern, ok := dc.fillPattern.(*solidPattern); ok { 473 | // with a nil mask and a solid color pattern, we can be more efficient 474 | // TODO: refactor so we don't have to do this type assertion stuff? 475 | p := raster.NewRGBAPainter(dc.im) 476 | p.SetColor(pattern.color) 477 | painter = p 478 | } 479 | } 480 | if painter == nil { 481 | painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern) 482 | } 483 | dc.fill(painter) 484 | } 485 | 486 | // Fill fills the current path with the current color. Open subpaths 487 | // are implicity closed. The path is cleared after this operation. 488 | func (dc *Context) Fill() { 489 | dc.FillPreserve() 490 | dc.ClearPath() 491 | } 492 | 493 | // ClipPreserve updates the clipping region by intersecting the current 494 | // clipping region with the current path as it would be filled by dc.Fill(). 495 | // The path is preserved after this operation. 496 | func (dc *Context) ClipPreserve() { 497 | clip := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) 498 | painter := raster.NewAlphaOverPainter(clip) 499 | dc.fill(painter) 500 | if dc.mask == nil { 501 | dc.mask = clip 502 | } else { 503 | mask := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) 504 | draw.DrawMask(mask, mask.Bounds(), clip, image.ZP, dc.mask, image.ZP, draw.Over) 505 | dc.mask = mask 506 | } 507 | } 508 | 509 | // SetMask allows you to directly set the *image.Alpha to be used as a clipping 510 | // mask. It must be the same size as the context, else an error is returned 511 | // and the mask is unchanged. 512 | func (dc *Context) SetMask(mask *image.Alpha) error { 513 | if mask.Bounds().Size() != dc.im.Bounds().Size() { 514 | return errors.New("mask size must match context size") 515 | } 516 | dc.mask = mask 517 | return nil 518 | } 519 | 520 | // AsMask returns an *image.Alpha representing the alpha channel of this 521 | // context. This can be useful for advanced clipping operations where you first 522 | // render the mask geometry and then use it as a mask. 523 | func (dc *Context) AsMask() *image.Alpha { 524 | mask := image.NewAlpha(dc.im.Bounds()) 525 | draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src) 526 | return mask 527 | } 528 | 529 | // InvertMask inverts the alpha values in the current clipping mask such that 530 | // a fully transparent region becomes fully opaque and vice versa. 531 | func (dc *Context) InvertMask() { 532 | if dc.mask == nil { 533 | dc.mask = image.NewAlpha(dc.im.Bounds()) 534 | } else { 535 | for i, a := range dc.mask.Pix { 536 | dc.mask.Pix[i] = 255 - a 537 | } 538 | } 539 | } 540 | 541 | // Clip updates the clipping region by intersecting the current 542 | // clipping region with the current path as it would be filled by dc.Fill(). 543 | // The path is cleared after this operation. 544 | func (dc *Context) Clip() { 545 | dc.ClipPreserve() 546 | dc.ClearPath() 547 | } 548 | 549 | // ResetClip clears the clipping region. 550 | func (dc *Context) ResetClip() { 551 | dc.mask = nil 552 | } 553 | 554 | // Convenient Drawing Functions 555 | 556 | // Clear fills the entire image with the current color. 557 | func (dc *Context) Clear() { 558 | src := image.NewUniform(dc.color) 559 | draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src) 560 | } 561 | 562 | // SetPixel sets the color of the specified pixel using the current color. 563 | func (dc *Context) SetPixel(x, y int) { 564 | dc.im.Set(x, y, dc.color) 565 | } 566 | 567 | // DrawPoint is like DrawCircle but ensures that a circle of the specified 568 | // size is drawn regardless of the current transformation matrix. The position 569 | // is still transformed, but not the shape of the point. 570 | func (dc *Context) DrawPoint(x, y, r float64) { 571 | dc.Push() 572 | tx, ty := dc.TransformPoint(x, y) 573 | dc.Identity() 574 | dc.DrawCircle(tx, ty, r) 575 | dc.Pop() 576 | } 577 | 578 | func (dc *Context) DrawLine(x1, y1, x2, y2 float64) { 579 | dc.MoveTo(x1, y1) 580 | dc.LineTo(x2, y2) 581 | } 582 | 583 | func (dc *Context) DrawRectangle(x, y, w, h float64) { 584 | dc.NewSubPath() 585 | dc.MoveTo(x, y) 586 | dc.LineTo(x+w, y) 587 | dc.LineTo(x+w, y+h) 588 | dc.LineTo(x, y+h) 589 | dc.ClosePath() 590 | } 591 | 592 | func (dc *Context) DrawRoundedRectangle(x, y, w, h, r float64) { 593 | x0, x1, x2, x3 := x, x+r, x+w-r, x+w 594 | y0, y1, y2, y3 := y, y+r, y+h-r, y+h 595 | dc.NewSubPath() 596 | dc.MoveTo(x1, y0) 597 | dc.LineTo(x2, y0) 598 | dc.DrawArc(x2, y1, r, Radians(270), Radians(360)) 599 | dc.LineTo(x3, y2) 600 | dc.DrawArc(x2, y2, r, Radians(0), Radians(90)) 601 | dc.LineTo(x1, y3) 602 | dc.DrawArc(x1, y2, r, Radians(90), Radians(180)) 603 | dc.LineTo(x0, y1) 604 | dc.DrawArc(x1, y1, r, Radians(180), Radians(270)) 605 | dc.ClosePath() 606 | } 607 | 608 | func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) { 609 | const n = 16 610 | for i := 0; i < n; i++ { 611 | p1 := float64(i+0) / n 612 | p2 := float64(i+1) / n 613 | a1 := angle1 + (angle2-angle1)*p1 614 | a2 := angle1 + (angle2-angle1)*p2 615 | x0 := x + rx*math.Cos(a1) 616 | y0 := y + ry*math.Sin(a1) 617 | x1 := x + rx*math.Cos((a1+a2)/2) 618 | y1 := y + ry*math.Sin((a1+a2)/2) 619 | x2 := x + rx*math.Cos(a2) 620 | y2 := y + ry*math.Sin(a2) 621 | cx := 2*x1 - x0/2 - x2/2 622 | cy := 2*y1 - y0/2 - y2/2 623 | if i == 0 { 624 | if dc.hasCurrent { 625 | dc.LineTo(x0, y0) 626 | } else { 627 | dc.MoveTo(x0, y0) 628 | } 629 | } 630 | dc.QuadraticTo(cx, cy, x2, y2) 631 | } 632 | } 633 | 634 | func (dc *Context) DrawEllipse(x, y, rx, ry float64) { 635 | dc.NewSubPath() 636 | dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi) 637 | dc.ClosePath() 638 | } 639 | 640 | func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) { 641 | dc.DrawEllipticalArc(x, y, r, r, angle1, angle2) 642 | } 643 | 644 | func (dc *Context) DrawCircle(x, y, r float64) { 645 | dc.NewSubPath() 646 | dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi) 647 | dc.ClosePath() 648 | } 649 | 650 | func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) { 651 | angle := 2 * math.Pi / float64(n) 652 | rotation -= math.Pi / 2 653 | if n%2 == 0 { 654 | rotation += angle / 2 655 | } 656 | dc.NewSubPath() 657 | for i := 0; i < n; i++ { 658 | a := rotation + angle*float64(i) 659 | dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a)) 660 | } 661 | dc.ClosePath() 662 | } 663 | 664 | // DrawImage draws the specified image at the specified point. 665 | func (dc *Context) DrawImage(im image.Image, x, y int) { 666 | dc.DrawImageAnchored(im, x, y, 0, 0) 667 | } 668 | 669 | // DrawImageAnchored draws the specified image at the specified anchor point. 670 | // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the 671 | // image. Use ax=0.5, ay=0.5 to center the image at the specified point. 672 | func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) { 673 | s := im.Bounds().Size() 674 | x -= int(ax * float64(s.X)) 675 | y -= int(ay * float64(s.Y)) 676 | transformer := draw.BiLinear 677 | fx, fy := float64(x), float64(y) 678 | m := dc.matrix.Translate(fx, fy) 679 | s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0} 680 | if dc.mask == nil { 681 | transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil) 682 | } else { 683 | transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{ 684 | DstMask: dc.mask, 685 | DstMaskP: image.ZP, 686 | }) 687 | } 688 | } 689 | 690 | // Text Functions 691 | 692 | func (dc *Context) SetFontFace(fontFace font.Face) { 693 | dc.fontFace = fontFace 694 | dc.fontHeight = float64(fontFace.Metrics().Height) / 64 695 | } 696 | 697 | func (dc *Context) LoadFontFace(path string, points float64) error { 698 | face, err := LoadFontFace(path, points) 699 | if err == nil { 700 | dc.fontFace = face 701 | dc.fontHeight = points * 72 / 96 702 | } 703 | return err 704 | } 705 | 706 | func (dc *Context) FontHeight() float64 { 707 | return dc.fontHeight 708 | } 709 | 710 | func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) { 711 | d := &font.Drawer{ 712 | Dst: im, 713 | Src: image.NewUniform(dc.color), 714 | Face: dc.fontFace, 715 | Dot: fixp(x, y), 716 | } 717 | // based on Drawer.DrawString() in golang.org/x/image/font/font.go 718 | prevC := rune(-1) 719 | for _, c := range s { 720 | if prevC >= 0 { 721 | d.Dot.X += d.Face.Kern(prevC, c) 722 | } 723 | dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c) 724 | if !ok { 725 | // TODO: is falling back on the U+FFFD glyph the responsibility of 726 | // the Drawer or the Face? 727 | // TODO: set prevC = '\ufffd'? 728 | continue 729 | } 730 | sr := dr.Sub(dr.Min) 731 | transformer := draw.BiLinear 732 | fx, fy := float64(dr.Min.X), float64(dr.Min.Y) 733 | m := dc.matrix.Translate(fx, fy) 734 | s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0} 735 | transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{ 736 | SrcMask: mask, 737 | SrcMaskP: maskp, 738 | }) 739 | d.Dot.X += advance 740 | prevC = c 741 | } 742 | } 743 | 744 | // DrawString draws the specified text at the specified point. 745 | func (dc *Context) DrawString(s string, x, y float64) { 746 | dc.DrawStringAnchored(s, x, y, 0, 0) 747 | } 748 | 749 | // DrawStringAnchored draws the specified text at the specified anchor point. 750 | // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the 751 | // text. Use ax=0.5, ay=0.5 to center the text at the specified point. 752 | func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) { 753 | w, h := dc.MeasureString(s) 754 | x -= ax * w 755 | y += ay * h 756 | if dc.mask == nil { 757 | dc.drawString(dc.im, s, x, y) 758 | } else { 759 | im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) 760 | dc.drawString(im, s, x, y) 761 | draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) 762 | } 763 | } 764 | 765 | // DrawStringWrapped word-wraps the specified string to the given max width 766 | // and then draws it at the specified anchor point using the given line 767 | // spacing and text alignment. 768 | func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) { 769 | lines := dc.WordWrap(s, width) 770 | 771 | // sync h formula with MeasureMultilineString 772 | h := float64(len(lines)) * dc.fontHeight * lineSpacing 773 | h -= (lineSpacing - 1) * dc.fontHeight 774 | 775 | x -= ax * width 776 | y -= ay * h 777 | switch align { 778 | case AlignLeft: 779 | ax = 0 780 | case AlignCenter: 781 | ax = 0.5 782 | x += width / 2 783 | case AlignRight: 784 | ax = 1 785 | x += width 786 | } 787 | ay = 1 788 | for _, line := range lines { 789 | dc.DrawStringAnchored(line, x, y, ax, ay) 790 | y += dc.fontHeight * lineSpacing 791 | } 792 | } 793 | 794 | func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) { 795 | lines := strings.Split(s, "\n") 796 | 797 | // sync h formula with DrawStringWrapped 798 | height = float64(len(lines)) * dc.fontHeight * lineSpacing 799 | height -= (lineSpacing - 1) * dc.fontHeight 800 | 801 | d := &font.Drawer{ 802 | Face: dc.fontFace, 803 | } 804 | 805 | // max width from lines 806 | for _, line := range lines { 807 | adv := d.MeasureString(line) 808 | currentWidth := float64(adv >> 6) // from gg.Context.MeasureString 809 | if currentWidth > width { 810 | width = currentWidth 811 | } 812 | } 813 | 814 | return width, height 815 | } 816 | 817 | // MeasureString returns the rendered width and height of the specified text 818 | // given the current font face. 819 | func (dc *Context) MeasureString(s string) (w, h float64) { 820 | d := &font.Drawer{ 821 | Face: dc.fontFace, 822 | } 823 | a := d.MeasureString(s) 824 | return float64(a >> 6), dc.fontHeight 825 | } 826 | 827 | // WordWrap wraps the specified string to the given max width and current 828 | // font face. 829 | func (dc *Context) WordWrap(s string, w float64) []string { 830 | return wordWrap(dc, s, w) 831 | } 832 | 833 | // Transformation Matrix Operations 834 | 835 | // Identity resets the current transformation matrix to the identity matrix. 836 | // This results in no translating, scaling, rotating, or shearing. 837 | func (dc *Context) Identity() { 838 | dc.matrix = Identity() 839 | } 840 | 841 | // Translate updates the current matrix with a translation. 842 | func (dc *Context) Translate(x, y float64) { 843 | dc.matrix = dc.matrix.Translate(x, y) 844 | } 845 | 846 | // Scale updates the current matrix with a scaling factor. 847 | // Scaling occurs about the origin. 848 | func (dc *Context) Scale(x, y float64) { 849 | dc.matrix = dc.matrix.Scale(x, y) 850 | } 851 | 852 | // ScaleAbout updates the current matrix with a scaling factor. 853 | // Scaling occurs about the specified point. 854 | func (dc *Context) ScaleAbout(sx, sy, x, y float64) { 855 | dc.Translate(x, y) 856 | dc.Scale(sx, sy) 857 | dc.Translate(-x, -y) 858 | } 859 | 860 | // Rotate updates the current matrix with a anticlockwise rotation. 861 | // Rotation occurs about the origin. Angle is specified in radians. 862 | func (dc *Context) Rotate(angle float64) { 863 | dc.matrix = dc.matrix.Rotate(angle) 864 | } 865 | 866 | // RotateAbout updates the current matrix with a anticlockwise rotation. 867 | // Rotation occurs about the specified point. Angle is specified in radians. 868 | func (dc *Context) RotateAbout(angle, x, y float64) { 869 | dc.Translate(x, y) 870 | dc.Rotate(angle) 871 | dc.Translate(-x, -y) 872 | } 873 | 874 | // Shear updates the current matrix with a shearing angle. 875 | // Shearing occurs about the origin. 876 | func (dc *Context) Shear(x, y float64) { 877 | dc.matrix = dc.matrix.Shear(x, y) 878 | } 879 | 880 | // ShearAbout updates the current matrix with a shearing angle. 881 | // Shearing occurs about the specified point. 882 | func (dc *Context) ShearAbout(sx, sy, x, y float64) { 883 | dc.Translate(x, y) 884 | dc.Shear(sx, sy) 885 | dc.Translate(-x, -y) 886 | } 887 | 888 | // TransformPoint multiplies the specified point by the current matrix, 889 | // returning a transformed position. 890 | func (dc *Context) TransformPoint(x, y float64) (tx, ty float64) { 891 | return dc.matrix.TransformPoint(x, y) 892 | } 893 | 894 | // InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at 895 | // the bottom of the image. 896 | func (dc *Context) InvertY() { 897 | dc.Translate(0, float64(dc.height)) 898 | dc.Scale(1, -1) 899 | } 900 | 901 | // Stack 902 | 903 | // Push saves the current state of the context for later retrieval. These 904 | // can be nested. 905 | func (dc *Context) Push() { 906 | x := *dc 907 | dc.stack = append(dc.stack, &x) 908 | } 909 | 910 | // Pop restores the last saved context state from the stack. 911 | func (dc *Context) Pop() { 912 | before := *dc 913 | s := dc.stack 914 | x, s := s[len(s)-1], s[:len(s)-1] 915 | *dc = *x 916 | dc.mask = before.mask 917 | dc.strokePath = before.strokePath 918 | dc.fillPath = before.fillPath 919 | dc.start = before.start 920 | dc.current = before.current 921 | dc.hasCurrent = before.hasCurrent 922 | } 923 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "crypto/md5" 5 | "flag" 6 | "fmt" 7 | "image/color" 8 | "math/rand" 9 | "testing" 10 | ) 11 | 12 | var save bool 13 | 14 | func init() { 15 | flag.BoolVar(&save, "save", false, "save PNG output for each test case") 16 | flag.Parse() 17 | } 18 | 19 | func hash(dc *Context) string { 20 | return fmt.Sprintf("%x", md5.Sum(dc.im.Pix)) 21 | } 22 | 23 | func checkHash(t *testing.T, dc *Context, expected string) { 24 | actual := hash(dc) 25 | if actual != expected { 26 | t.Fatalf("expected hash: %s != actual hash: %s", expected, actual) 27 | } 28 | } 29 | 30 | func saveImage(dc *Context, name string) error { 31 | if save { 32 | return SavePNG(name+".png", dc.Image()) 33 | } 34 | return nil 35 | } 36 | 37 | func TestBlank(t *testing.T) { 38 | dc := NewContext(100, 100) 39 | saveImage(dc, "TestBlank") 40 | checkHash(t, dc, "4e0a293a5b638f0aba2c4fe2c3418d0e") 41 | } 42 | 43 | func TestGrid(t *testing.T) { 44 | dc := NewContext(100, 100) 45 | dc.SetRGB(1, 1, 1) 46 | dc.Clear() 47 | for i := 10; i < 100; i += 10 { 48 | x := float64(i) + 0.5 49 | dc.DrawLine(x, 0, x, 100) 50 | dc.DrawLine(0, x, 100, x) 51 | } 52 | dc.SetRGB(0, 0, 0) 53 | dc.Stroke() 54 | saveImage(dc, "TestGrid") 55 | checkHash(t, dc, "78606adda71d8abfbd8bb271087e4d69") 56 | } 57 | 58 | func TestLines(t *testing.T) { 59 | dc := NewContext(100, 100) 60 | dc.SetRGB(0.5, 0.5, 0.5) 61 | dc.Clear() 62 | rnd := rand.New(rand.NewSource(99)) 63 | for i := 0; i < 100; i++ { 64 | x1 := rnd.Float64() * 100 65 | y1 := rnd.Float64() * 100 66 | x2 := rnd.Float64() * 100 67 | y2 := rnd.Float64() * 100 68 | dc.DrawLine(x1, y1, x2, y2) 69 | dc.SetLineWidth(rnd.Float64() * 3) 70 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 71 | dc.Stroke() 72 | } 73 | saveImage(dc, "TestLines") 74 | checkHash(t, dc, "036bd220e2529955cc48425dd72bb686") 75 | } 76 | 77 | func TestCircles(t *testing.T) { 78 | dc := NewContext(100, 100) 79 | dc.SetRGB(1, 1, 1) 80 | dc.Clear() 81 | rnd := rand.New(rand.NewSource(99)) 82 | for i := 0; i < 10; i++ { 83 | x := rnd.Float64() * 100 84 | y := rnd.Float64() * 100 85 | r := rnd.Float64()*10 + 5 86 | dc.DrawCircle(x, y, r) 87 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 88 | dc.FillPreserve() 89 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 90 | dc.SetLineWidth(rnd.Float64() * 3) 91 | dc.Stroke() 92 | } 93 | saveImage(dc, "TestCircles") 94 | checkHash(t, dc, "c52698000df96fabafe7863701afe922") 95 | } 96 | 97 | func TestQuadratic(t *testing.T) { 98 | dc := NewContext(100, 100) 99 | dc.SetRGB(0.25, 0.25, 0.25) 100 | dc.Clear() 101 | rnd := rand.New(rand.NewSource(99)) 102 | for i := 0; i < 100; i++ { 103 | x1 := rnd.Float64() * 100 104 | y1 := rnd.Float64() * 100 105 | x2 := rnd.Float64() * 100 106 | y2 := rnd.Float64() * 100 107 | x3 := rnd.Float64() * 100 108 | y3 := rnd.Float64() * 100 109 | dc.MoveTo(x1, y1) 110 | dc.QuadraticTo(x2, y2, x3, y3) 111 | dc.SetLineWidth(rnd.Float64() * 3) 112 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 113 | dc.Stroke() 114 | } 115 | saveImage(dc, "TestQuadratic") 116 | checkHash(t, dc, "56b842d814aee94b52495addae764a77") 117 | } 118 | 119 | func TestCubic(t *testing.T) { 120 | dc := NewContext(100, 100) 121 | dc.SetRGB(0.75, 0.75, 0.75) 122 | dc.Clear() 123 | rnd := rand.New(rand.NewSource(99)) 124 | for i := 0; i < 100; i++ { 125 | x1 := rnd.Float64() * 100 126 | y1 := rnd.Float64() * 100 127 | x2 := rnd.Float64() * 100 128 | y2 := rnd.Float64() * 100 129 | x3 := rnd.Float64() * 100 130 | y3 := rnd.Float64() * 100 131 | x4 := rnd.Float64() * 100 132 | y4 := rnd.Float64() * 100 133 | dc.MoveTo(x1, y1) 134 | dc.CubicTo(x2, y2, x3, y3, x4, y4) 135 | dc.SetLineWidth(rnd.Float64() * 3) 136 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 137 | dc.Stroke() 138 | } 139 | saveImage(dc, "TestCubic") 140 | checkHash(t, dc, "4a7960fc4eaaa33ce74131c5ce0afca8") 141 | } 142 | 143 | func TestFill(t *testing.T) { 144 | dc := NewContext(100, 100) 145 | dc.SetRGB(1, 1, 1) 146 | dc.Clear() 147 | rnd := rand.New(rand.NewSource(99)) 148 | for i := 0; i < 10; i++ { 149 | dc.NewSubPath() 150 | for j := 0; j < 10; j++ { 151 | x := rnd.Float64() * 100 152 | y := rnd.Float64() * 100 153 | dc.LineTo(x, y) 154 | } 155 | dc.ClosePath() 156 | dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64()) 157 | dc.Fill() 158 | } 159 | saveImage(dc, "TestFill") 160 | checkHash(t, dc, "7ccb3a2443906a825e57ab94db785467") 161 | } 162 | 163 | func TestClip(t *testing.T) { 164 | dc := NewContext(100, 100) 165 | dc.SetRGB(1, 1, 1) 166 | dc.Clear() 167 | dc.DrawCircle(50, 50, 40) 168 | dc.Clip() 169 | rnd := rand.New(rand.NewSource(99)) 170 | for i := 0; i < 1000; i++ { 171 | x := rnd.Float64() * 100 172 | y := rnd.Float64() * 100 173 | r := rnd.Float64()*10 + 5 174 | dc.DrawCircle(x, y, r) 175 | dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64()) 176 | dc.Fill() 177 | } 178 | saveImage(dc, "TestClip") 179 | checkHash(t, dc, "762c32374d529fd45ffa038b05be7865") 180 | } 181 | 182 | func TestPushPop(t *testing.T) { 183 | const S = 100 184 | dc := NewContext(S, S) 185 | dc.SetRGBA(0, 0, 0, 0.1) 186 | for i := 0; i < 360; i += 15 { 187 | dc.Push() 188 | dc.RotateAbout(Radians(float64(i)), S/2, S/2) 189 | dc.DrawEllipse(S/2, S/2, S*7/16, S/8) 190 | dc.Fill() 191 | dc.Pop() 192 | } 193 | saveImage(dc, "TestPushPop") 194 | checkHash(t, dc, "31e908ee1c2ea180da98fd5681a89d05") 195 | } 196 | 197 | func TestDrawStringWrapped(t *testing.T) { 198 | dc := NewContext(100, 100) 199 | dc.SetRGB(1, 1, 1) 200 | dc.Clear() 201 | dc.SetRGB(0, 0, 0) 202 | dc.DrawStringWrapped("Hello, world! How are you?", 50, 50, 0.5, 0.5, 90, 1.5, AlignCenter) 203 | saveImage(dc, "TestDrawStringWrapped") 204 | checkHash(t, dc, "8d92f6aae9e8b38563f171abd00893f8") 205 | } 206 | 207 | func TestDrawImage(t *testing.T) { 208 | src := NewContext(100, 100) 209 | src.SetRGB(1, 1, 1) 210 | src.Clear() 211 | for i := 10; i < 100; i += 10 { 212 | x := float64(i) + 0.5 213 | src.DrawLine(x, 0, x, 100) 214 | src.DrawLine(0, x, 100, x) 215 | } 216 | src.SetRGB(0, 0, 0) 217 | src.Stroke() 218 | 219 | dc := NewContext(200, 200) 220 | dc.SetRGB(0, 0, 0) 221 | dc.Clear() 222 | dc.DrawImage(src.Image(), 50, 50) 223 | saveImage(dc, "TestDrawImage") 224 | checkHash(t, dc, "282afbc134676722960b6bec21305b15") 225 | } 226 | 227 | func TestSetPixel(t *testing.T) { 228 | dc := NewContext(100, 100) 229 | dc.SetRGB(0, 0, 0) 230 | dc.Clear() 231 | dc.SetRGB(0, 1, 0) 232 | i := 0 233 | for y := 0; y < 100; y++ { 234 | for x := 0; x < 100; x++ { 235 | if i%31 == 0 { 236 | dc.SetPixel(x, y) 237 | } 238 | i++ 239 | } 240 | } 241 | saveImage(dc, "TestSetPixel") 242 | checkHash(t, dc, "27dda6b4b1d94f061018825b11982793") 243 | } 244 | 245 | func TestDrawPoint(t *testing.T) { 246 | dc := NewContext(100, 100) 247 | dc.SetRGB(0, 0, 0) 248 | dc.Clear() 249 | dc.SetRGB(0, 1, 0) 250 | dc.Scale(10, 10) 251 | for y := 0; y <= 10; y++ { 252 | for x := 0; x <= 10; x++ { 253 | dc.DrawPoint(float64(x), float64(y), 3) 254 | dc.Fill() 255 | } 256 | } 257 | saveImage(dc, "TestDrawPoint") 258 | checkHash(t, dc, "55af8874531947ea6eeb62222fb33e0e") 259 | } 260 | 261 | func TestLinearGradient(t *testing.T) { 262 | dc := NewContext(100, 100) 263 | g := NewLinearGradient(0, 0, 100, 100) 264 | g.AddColorStop(0, color.RGBA{0, 255, 0, 255}) 265 | g.AddColorStop(1, color.RGBA{0, 0, 255, 255}) 266 | g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255}) 267 | dc.SetFillStyle(g) 268 | dc.DrawRectangle(0, 0, 100, 100) 269 | dc.Fill() 270 | saveImage(dc, "TestLinearGradient") 271 | checkHash(t, dc, "75eb9385c1219b1d5bb6f4c961802c7a") 272 | } 273 | 274 | func TestRadialGradient(t *testing.T) { 275 | dc := NewContext(100, 100) 276 | g := NewRadialGradient(30, 50, 0, 70, 50, 50) 277 | g.AddColorStop(0, color.RGBA{0, 255, 0, 255}) 278 | g.AddColorStop(1, color.RGBA{0, 0, 255, 255}) 279 | g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255}) 280 | dc.SetFillStyle(g) 281 | dc.DrawRectangle(0, 0, 100, 100) 282 | dc.Fill() 283 | saveImage(dc, "TestRadialGradient") 284 | checkHash(t, dc, "f170f39c3f35c29de11e00428532489d") 285 | } 286 | 287 | func TestDashes(t *testing.T) { 288 | dc := NewContext(100, 100) 289 | dc.SetRGB(1, 1, 1) 290 | dc.Clear() 291 | rnd := rand.New(rand.NewSource(99)) 292 | for i := 0; i < 100; i++ { 293 | x1 := rnd.Float64() * 100 294 | y1 := rnd.Float64() * 100 295 | x2 := rnd.Float64() * 100 296 | y2 := rnd.Float64() * 100 297 | dc.SetDash(rnd.Float64()*3+1, rnd.Float64()*3+3) 298 | dc.DrawLine(x1, y1, x2, y2) 299 | dc.SetLineWidth(rnd.Float64() * 3) 300 | dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64()) 301 | dc.Stroke() 302 | } 303 | saveImage(dc, "TestDashes") 304 | checkHash(t, dc, "d188069c69dcc3970edfac80f552b53c") 305 | } 306 | 307 | func BenchmarkCircles(b *testing.B) { 308 | dc := NewContext(1000, 1000) 309 | dc.SetRGB(1, 1, 1) 310 | dc.Clear() 311 | rnd := rand.New(rand.NewSource(99)) 312 | for i := 0; i < b.N; i++ { 313 | x := rnd.Float64() * 1000 314 | y := rnd.Float64() * 1000 315 | dc.DrawCircle(x, y, 10) 316 | if i%2 == 0 { 317 | dc.SetRGB(0, 0, 0) 318 | } else { 319 | dc.SetRGB(1, 1, 1) 320 | } 321 | dc.Fill() 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /examples/baboon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/gg/8febc0f526adecda6f8ae80f3869b7cd77e52984/examples/baboon.png -------------------------------------------------------------------------------- /examples/beziers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func random() float64 { 10 | return rand.Float64()*2 - 1 11 | } 12 | 13 | func point() (x, y float64) { 14 | return random(), random() 15 | } 16 | 17 | func drawCurve(dc *gg.Context) { 18 | dc.SetRGBA(0, 0, 0, 0.1) 19 | dc.FillPreserve() 20 | dc.SetRGB(0, 0, 0) 21 | dc.SetLineWidth(12) 22 | dc.Stroke() 23 | } 24 | 25 | func drawPoints(dc *gg.Context) { 26 | dc.SetRGBA(1, 0, 0, 0.5) 27 | dc.SetLineWidth(2) 28 | dc.Stroke() 29 | } 30 | 31 | func randomQuadratic(dc *gg.Context) { 32 | x0, y0 := point() 33 | x1, y1 := point() 34 | x2, y2 := point() 35 | dc.MoveTo(x0, y0) 36 | dc.QuadraticTo(x1, y1, x2, y2) 37 | drawCurve(dc) 38 | dc.MoveTo(x0, y0) 39 | dc.LineTo(x1, y1) 40 | dc.LineTo(x2, y2) 41 | drawPoints(dc) 42 | } 43 | 44 | func randomCubic(dc *gg.Context) { 45 | x0, y0 := point() 46 | x1, y1 := point() 47 | x2, y2 := point() 48 | x3, y3 := point() 49 | dc.MoveTo(x0, y0) 50 | dc.CubicTo(x1, y1, x2, y2, x3, y3) 51 | drawCurve(dc) 52 | dc.MoveTo(x0, y0) 53 | dc.LineTo(x1, y1) 54 | dc.LineTo(x2, y2) 55 | dc.LineTo(x3, y3) 56 | drawPoints(dc) 57 | } 58 | 59 | func main() { 60 | const ( 61 | S = 256 62 | W = 8 63 | H = 8 64 | ) 65 | dc := gg.NewContext(S*W, S*H) 66 | dc.SetRGB(1, 1, 1) 67 | dc.Clear() 68 | for j := 0; j < H; j++ { 69 | for i := 0; i < W; i++ { 70 | x := float64(i)*S + S/2 71 | y := float64(j)*S + S/2 72 | dc.Push() 73 | dc.Translate(x, y) 74 | dc.Scale(S/2, S/2) 75 | if j%2 == 0 { 76 | randomCubic(dc) 77 | } else { 78 | randomQuadratic(dc) 79 | } 80 | dc.Pop() 81 | } 82 | } 83 | dc.SavePNG("out.png") 84 | } 85 | -------------------------------------------------------------------------------- /examples/circle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | dc := gg.NewContext(1000, 1000) 7 | dc.DrawCircle(500, 500, 400) 8 | dc.SetRGB(0, 0, 0) 9 | dc.Fill() 10 | dc.SavePNG("out.png") 11 | } 12 | -------------------------------------------------------------------------------- /examples/clip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | dc := gg.NewContext(1000, 1000) 7 | dc.DrawCircle(350, 500, 300) 8 | dc.Clip() 9 | dc.DrawCircle(650, 500, 300) 10 | dc.Clip() 11 | dc.DrawRectangle(0, 0, 1000, 1000) 12 | dc.SetRGB(0, 0, 0) 13 | dc.Fill() 14 | dc.SavePNG("out.png") 15 | } 16 | -------------------------------------------------------------------------------- /examples/concat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | im1, err := gg.LoadPNG("examples/baboon.png") 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | im2, err := gg.LoadPNG("examples/gopher.png") 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | s1 := im1.Bounds().Size() 21 | s2 := im2.Bounds().Size() 22 | 23 | width := int(math.Max(float64(s1.X), float64(s2.X))) 24 | height := s1.Y + s2.Y 25 | 26 | dc := gg.NewContext(width, height) 27 | dc.DrawImage(im1, 0, 0) 28 | dc.DrawImage(im2, 0, s1.Y) 29 | dc.SavePNG("out.png") 30 | } 31 | -------------------------------------------------------------------------------- /examples/crisp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fogleman/gg" 5 | ) 6 | 7 | func main() { 8 | const W = 1000 9 | const H = 1000 10 | const Minor = 10 11 | const Major = 100 12 | 13 | dc := gg.NewContext(W, H) 14 | dc.SetRGB(1, 1, 1) 15 | dc.Clear() 16 | 17 | // minor grid 18 | for x := Minor; x < W; x += Minor { 19 | fx := float64(x) + 0.5 20 | dc.DrawLine(fx, 0, fx, H) 21 | } 22 | for y := Minor; y < H; y += Minor { 23 | fy := float64(y) + 0.5 24 | dc.DrawLine(0, fy, W, fy) 25 | } 26 | dc.SetLineWidth(1) 27 | dc.SetRGBA(0, 0, 0, 0.25) 28 | dc.Stroke() 29 | 30 | // major grid 31 | for x := Major; x < W; x += Major { 32 | fx := float64(x) + 0.5 33 | dc.DrawLine(fx, 0, fx, H) 34 | } 35 | for y := Major; y < H; y += Major { 36 | fy := float64(y) + 0.5 37 | dc.DrawLine(0, fy, W, fy) 38 | } 39 | dc.SetLineWidth(1) 40 | dc.SetRGBA(0, 0, 0, 0.5) 41 | dc.Stroke() 42 | 43 | dc.SavePNG("out.png") 44 | } 45 | -------------------------------------------------------------------------------- /examples/cubic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 1000 7 | dc := gg.NewContext(S, S) 8 | dc.SetRGB(1, 1, 1) 9 | dc.Clear() 10 | dc.Translate(S/2, S/2) 11 | dc.Scale(40, 40) 12 | 13 | var x0, y0, x1, y1, x2, y2, x3, y3 float64 14 | x0, y0 = -10, 0 15 | x1, y1 = -8, -8 16 | x2, y2 = 8, 8 17 | x3, y3 = 10, 0 18 | 19 | dc.MoveTo(x0, y0) 20 | dc.CubicTo(x1, y1, x2, y2, x3, y3) 21 | dc.SetRGBA(0, 0, 0, 0.2) 22 | dc.SetLineWidth(8) 23 | dc.FillPreserve() 24 | dc.SetRGB(0, 0, 0) 25 | dc.SetDash(16, 24) 26 | dc.Stroke() 27 | 28 | dc.MoveTo(x0, y0) 29 | dc.LineTo(x1, y1) 30 | dc.LineTo(x2, y2) 31 | dc.LineTo(x3, y3) 32 | dc.SetRGBA(1, 0, 0, 0.4) 33 | dc.SetLineWidth(2) 34 | dc.SetDash(4, 8, 1, 8) 35 | dc.Stroke() 36 | 37 | dc.SavePNG("out.png") 38 | } 39 | -------------------------------------------------------------------------------- /examples/ellipse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 1024 7 | dc := gg.NewContext(S, S) 8 | dc.SetRGBA(0, 0, 0, 0.1) 9 | for i := 0; i < 360; i += 15 { 10 | dc.Push() 11 | dc.RotateAbout(gg.Radians(float64(i)), S/2, S/2) 12 | dc.DrawEllipse(S/2, S/2, S*7/16, S/8) 13 | dc.Fill() 14 | dc.Pop() 15 | } 16 | if im, err := gg.LoadImage("examples/gopher.png"); err == nil { 17 | dc.DrawImageAnchored(im, S/2, S/2, 0.5, 0.5) 18 | } 19 | dc.SavePNG("out.png") 20 | } 21 | -------------------------------------------------------------------------------- /examples/gofont.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/fogleman/gg" 7 | "github.com/golang/freetype/truetype" 8 | "golang.org/x/image/font/gofont/goregular" 9 | ) 10 | 11 | func main() { 12 | font, err := truetype.Parse(goregular.TTF) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | face := truetype.NewFace(font, &truetype.Options{Size: 48}) 18 | 19 | dc := gg.NewContext(1024, 1024) 20 | dc.SetFontFace(face) 21 | dc.SetRGB(1, 1, 1) 22 | dc.Clear() 23 | dc.SetRGB(0, 0, 0) 24 | dc.DrawStringAnchored("Hello, world!", 512, 512, 0.5, 0.5) 25 | dc.SavePNG("out.png") 26 | } 27 | -------------------------------------------------------------------------------- /examples/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/gg/8febc0f526adecda6f8ae80f3869b7cd77e52984/examples/gopher.png -------------------------------------------------------------------------------- /examples/gradient-conic.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "image/color" 7 | 8 | "github.com/fogleman/gg" 9 | ) 10 | 11 | func main() { 12 | dc := gg.NewContext(400, 400) 13 | 14 | grad1 := gg.NewConicGradient(200, 200, 0) 15 | grad1.AddColorStop(0.0, color.Black) 16 | grad1.AddColorStop(0.5, color.RGBA{255, 215, 0, 255}) 17 | grad1.AddColorStop(1.0, color.RGBA{255, 0, 0, 255}) 18 | 19 | grad2 := gg.NewConicGradient(200, 200, 90) 20 | grad2.AddColorStop(0.00, color.RGBA{255, 0, 0, 255}) 21 | grad2.AddColorStop(0.16, color.RGBA{255, 255, 0, 255}) 22 | grad2.AddColorStop(0.33, color.RGBA{0, 255, 0, 255}) 23 | grad2.AddColorStop(0.50, color.RGBA{0, 255, 255, 255}) 24 | grad2.AddColorStop(0.66, color.RGBA{0, 0, 255, 255}) 25 | grad2.AddColorStop(0.83, color.RGBA{255, 0, 255, 255}) 26 | grad2.AddColorStop(1.00, color.RGBA{255, 0, 0, 255}) 27 | 28 | dc.SetStrokeStyle(grad1) 29 | dc.SetLineWidth(20) 30 | dc.DrawCircle(200, 200, 180) 31 | dc.Stroke() 32 | 33 | dc.SetFillStyle(grad2) 34 | dc.DrawCircle(200, 200, 150) 35 | dc.Fill() 36 | 37 | dc.SavePNG("gradient-conic.png") 38 | } 39 | -------------------------------------------------------------------------------- /examples/gradient-linear.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | dc := gg.NewContext(500, 400) 11 | 12 | grad := gg.NewLinearGradient(20, 320, 400, 20) 13 | grad.AddColorStop(0, color.RGBA{0, 255, 0, 255}) 14 | grad.AddColorStop(1, color.RGBA{0, 0, 255, 255}) 15 | grad.AddColorStop(0.5, color.RGBA{255, 0, 0, 255}) 16 | 17 | dc.SetColor(color.White) 18 | dc.DrawRectangle(20, 20, 400-20, 300) 19 | dc.Stroke() 20 | 21 | dc.SetStrokeStyle(grad) 22 | dc.SetLineWidth(4) 23 | dc.MoveTo(10, 10) 24 | dc.LineTo(410, 10) 25 | dc.LineTo(410, 100) 26 | dc.LineTo(10, 100) 27 | dc.ClosePath() 28 | dc.Stroke() 29 | 30 | dc.SetFillStyle(grad) 31 | dc.MoveTo(10, 120) 32 | dc.LineTo(410, 120) 33 | dc.LineTo(410, 300) 34 | dc.LineTo(10, 300) 35 | dc.ClosePath() 36 | dc.Fill() 37 | 38 | dc.SavePNG("out.png") 39 | } 40 | -------------------------------------------------------------------------------- /examples/gradient-radial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | dc := gg.NewContext(400, 200) 11 | 12 | grad := gg.NewRadialGradient(100, 100, 10, 100, 120, 80) 13 | grad.AddColorStop(0, color.RGBA{0, 255, 0, 255}) 14 | grad.AddColorStop(1, color.RGBA{0, 0, 255, 255}) 15 | 16 | dc.SetFillStyle(grad) 17 | dc.DrawRectangle(0, 0, 200, 200) 18 | dc.Fill() 19 | 20 | dc.SetColor(color.White) 21 | dc.DrawCircle(100, 100, 10) 22 | dc.Stroke() 23 | dc.DrawCircle(100, 120, 80) 24 | dc.Stroke() 25 | 26 | dc.SavePNG("out.png") 27 | } 28 | -------------------------------------------------------------------------------- /examples/gradient-text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | const ( 10 | W = 1024 11 | H = 512 12 | ) 13 | 14 | func main() { 15 | dc := gg.NewContext(W, H) 16 | 17 | // draw text 18 | dc.SetRGB(0, 0, 0) 19 | dc.LoadFontFace("/Library/Fonts/Impact.ttf", 128) 20 | dc.DrawStringAnchored("Gradient Text", W/2, H/2, 0.5, 0.5) 21 | 22 | // get the context as an alpha mask 23 | mask := dc.AsMask() 24 | 25 | // clear the context 26 | dc.SetRGB(1, 1, 1) 27 | dc.Clear() 28 | 29 | // set a gradient 30 | g := gg.NewLinearGradient(0, 0, W, H) 31 | g.AddColorStop(0, color.RGBA{255, 0, 0, 255}) 32 | g.AddColorStop(1, color.RGBA{0, 0, 255, 255}) 33 | dc.SetFillStyle(g) 34 | 35 | // using the mask, fill the context with the gradient 36 | dc.SetMask(mask) 37 | dc.DrawRectangle(0, 0, W, H) 38 | dc.Fill() 39 | 40 | dc.SavePNG("out.png") 41 | } 42 | -------------------------------------------------------------------------------- /examples/invert-mask.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | dc := gg.NewContext(1024, 1024) 7 | dc.DrawCircle(512, 512, 384) 8 | dc.Clip() 9 | dc.InvertMask() 10 | dc.DrawRectangle(0, 0, 1024, 1024) 11 | dc.SetRGB(0, 0, 0) 12 | dc.Fill() 13 | dc.SavePNG("out.png") 14 | } 15 | -------------------------------------------------------------------------------- /examples/lines.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | const W = 1024 11 | const H = 1024 12 | dc := gg.NewContext(W, H) 13 | dc.SetRGB(0, 0, 0) 14 | dc.Clear() 15 | for i := 0; i < 1000; i++ { 16 | x1 := rand.Float64() * W 17 | y1 := rand.Float64() * H 18 | x2 := rand.Float64() * W 19 | y2 := rand.Float64() * H 20 | r := rand.Float64() 21 | g := rand.Float64() 22 | b := rand.Float64() 23 | a := rand.Float64()*0.5 + 0.5 24 | w := rand.Float64()*4 + 1 25 | dc.SetRGBA(r, g, b, a) 26 | dc.SetLineWidth(w) 27 | dc.DrawLine(x1, y1, x2, y2) 28 | dc.Stroke() 29 | } 30 | dc.SavePNG("out.png") 31 | } 32 | -------------------------------------------------------------------------------- /examples/linewidth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | dc := gg.NewContext(1000, 1000) 7 | dc.SetRGB(1, 1, 1) 8 | dc.Clear() 9 | dc.SetRGB(0, 0, 0) 10 | w := 0.1 11 | for i := 100; i <= 900; i += 20 { 12 | x := float64(i) 13 | dc.DrawLine(x+50, 0, x-50, 1000) 14 | dc.SetLineWidth(w) 15 | dc.Stroke() 16 | w += 0.1 17 | } 18 | dc.SavePNG("out.png") 19 | } 20 | -------------------------------------------------------------------------------- /examples/lorem.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | var lines = []string{ 6 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod", 7 | "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,", 8 | "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo", 9 | "consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse", 10 | "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat", 11 | "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 12 | } 13 | 14 | func main() { 15 | const W = 800 16 | const H = 400 17 | dc := gg.NewContext(W, H) 18 | dc.SetRGB(1, 1, 1) 19 | dc.Clear() 20 | dc.SetRGB(0, 0, 0) 21 | // dc.LoadFontFace("/Library/Fonts/Arial.ttf", 18) 22 | const h = 24 23 | for i, line := range lines { 24 | y := H/2 - h*len(lines)/2 + i*h 25 | dc.DrawStringAnchored(line, 400, float64(y), 0.5, 0.5) 26 | } 27 | dc.SavePNG("out.png") 28 | } 29 | -------------------------------------------------------------------------------- /examples/mask.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | im, err := gg.LoadImage("examples/baboon.png") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | dc := gg.NewContext(512, 512) 16 | dc.DrawRoundedRectangle(0, 0, 512, 512, 64) 17 | dc.Clip() 18 | dc.DrawImage(im, 0, 0) 19 | dc.SavePNG("out.png") 20 | } 21 | -------------------------------------------------------------------------------- /examples/meme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 1024 7 | dc := gg.NewContext(S, S) 8 | dc.SetRGB(1, 1, 1) 9 | dc.Clear() 10 | if err := dc.LoadFontFace("/Library/Fonts/Impact.ttf", 96); err != nil { 11 | panic(err) 12 | } 13 | dc.SetRGB(0, 0, 0) 14 | s := "ONE DOES NOT SIMPLY" 15 | n := 6 // "stroke" size 16 | for dy := -n; dy <= n; dy++ { 17 | for dx := -n; dx <= n; dx++ { 18 | if dx*dx+dy*dy >= n*n { 19 | // give it rounded corners 20 | continue 21 | } 22 | x := S/2 + float64(dx) 23 | y := S/2 + float64(dy) 24 | dc.DrawStringAnchored(s, x, y, 0.5, 0.5) 25 | } 26 | } 27 | dc.SetRGB(1, 1, 1) 28 | dc.DrawStringAnchored(s, S/2, S/2, 0.5, 0.5) 29 | dc.SavePNG("out.png") 30 | } 31 | -------------------------------------------------------------------------------- /examples/openfill.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "github.com/fogleman/gg" 8 | ) 9 | 10 | func main() { 11 | dc := gg.NewContext(1000, 1000) 12 | for j := 0; j < 10; j++ { 13 | for i := 0; i < 10; i++ { 14 | x := float64(i)*100 + 50 15 | y := float64(j)*100 + 50 16 | a1 := rand.Float64() * 2 * math.Pi 17 | a2 := a1 + rand.Float64()*math.Pi + math.Pi/2 18 | dc.DrawArc(x, y, 40, a1, a2) 19 | // dc.ClosePath() 20 | } 21 | } 22 | dc.SetRGB(0, 0, 0) 23 | dc.FillPreserve() 24 | dc.SetRGB(1, 1, 1) 25 | dc.SetLineWidth(8) 26 | dc.StrokePreserve() 27 | dc.SetRGB(1, 0, 0) 28 | dc.SetLineWidth(4) 29 | dc.StrokePreserve() 30 | dc.SavePNG("out.png") 31 | } 32 | -------------------------------------------------------------------------------- /examples/pattern-fill.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | im, err := gg.LoadPNG("examples/baboon.png") 7 | if err != nil { 8 | panic(err) 9 | } 10 | pattern := gg.NewSurfacePattern(im, gg.RepeatBoth) 11 | dc := gg.NewContext(600, 600) 12 | dc.MoveTo(20, 20) 13 | dc.LineTo(590, 20) 14 | dc.LineTo(590, 590) 15 | dc.LineTo(20, 590) 16 | dc.ClosePath() 17 | dc.SetFillStyle(pattern) 18 | dc.Fill() 19 | dc.SavePNG("out.png") 20 | } 21 | -------------------------------------------------------------------------------- /examples/quadratic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 1000 7 | dc := gg.NewContext(S, S) 8 | dc.SetRGB(1, 1, 1) 9 | dc.Clear() 10 | dc.Translate(S/2, S/2) 11 | dc.Scale(40, 40) 12 | 13 | var x0, y0, x1, y1, x2, y2, x3, y3, x4, y4 float64 14 | x0, y0 = -10, 0 15 | x1, y1 = -5, -10 16 | x2, y2 = 0, 0 17 | x3, y3 = 5, 10 18 | x4, y4 = 10, 0 19 | 20 | dc.MoveTo(x0, y0) 21 | dc.LineTo(x1, y1) 22 | dc.LineTo(x2, y2) 23 | dc.LineTo(x3, y3) 24 | dc.LineTo(x4, y4) 25 | dc.SetHexColor("FF2D00") 26 | dc.SetLineWidth(8) 27 | dc.Stroke() 28 | 29 | dc.MoveTo(x0, y0) 30 | dc.QuadraticTo(x1, y1, x2, y2) 31 | dc.QuadraticTo(x3, y3, x4, y4) 32 | dc.SetHexColor("3E606F") 33 | dc.SetLineWidth(16) 34 | dc.FillPreserve() 35 | dc.SetRGB(0, 0, 0) 36 | dc.Stroke() 37 | 38 | dc.DrawCircle(x0, y0, 0.5) 39 | dc.DrawCircle(x1, y1, 0.5) 40 | dc.DrawCircle(x2, y2, 0.5) 41 | dc.DrawCircle(x3, y3, 0.5) 42 | dc.DrawCircle(x4, y4, 0.5) 43 | dc.SetRGB(1, 1, 1) 44 | dc.FillPreserve() 45 | dc.SetRGB(0, 0, 0) 46 | dc.SetLineWidth(4) 47 | dc.Stroke() 48 | 49 | dc.LoadFontFace("/Library/Fonts/Arial.ttf", 200) 50 | dc.DrawStringAnchored("g", -5, 5, 0.5, 0.5) 51 | dc.DrawStringAnchored("G", 5, -5, 0.5, 0.5) 52 | 53 | dc.SavePNG("out.png") 54 | } 55 | -------------------------------------------------------------------------------- /examples/rotated-image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const W = 400 7 | const H = 500 8 | im, err := gg.LoadPNG("examples/gopher.png") 9 | if err != nil { 10 | panic(err) 11 | } 12 | iw, ih := im.Bounds().Dx(), im.Bounds().Dy() 13 | dc := gg.NewContext(W, H) 14 | // draw outline 15 | dc.SetHexColor("#ff0000") 16 | dc.SetLineWidth(1) 17 | dc.DrawRectangle(0, 0, float64(W), float64(H)) 18 | dc.Stroke() 19 | // draw full image 20 | dc.SetHexColor("#0000ff") 21 | dc.SetLineWidth(2) 22 | dc.DrawRectangle(100, 210, float64(iw), float64(ih)) 23 | dc.Stroke() 24 | dc.DrawImage(im, 100, 210) 25 | // draw image with current matrix applied 26 | dc.SetHexColor("#0000ff") 27 | dc.SetLineWidth(2) 28 | dc.Rotate(gg.Radians(10)) 29 | dc.DrawRectangle(100, 0, float64(iw), float64(ih)/2+20.0) 30 | dc.StrokePreserve() 31 | dc.Clip() 32 | dc.DrawImageAnchored(im, 100, 0, 0.0, 0.0) 33 | dc.SavePNG("out.png") 34 | } 35 | -------------------------------------------------------------------------------- /examples/rotated-text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fogleman/gg" 5 | "github.com/golang/freetype/truetype" 6 | "golang.org/x/image/font/gofont/goregular" 7 | ) 8 | 9 | func main() { 10 | const S = 400 11 | dc := gg.NewContext(S, S) 12 | dc.SetRGB(1, 1, 1) 13 | dc.Clear() 14 | dc.SetRGB(0, 0, 0) 15 | font, err := truetype.Parse(goregular.TTF) 16 | if err != nil { 17 | panic("") 18 | } 19 | face := truetype.NewFace(font, &truetype.Options{ 20 | Size: 40, 21 | }) 22 | dc.SetFontFace(face) 23 | text := "Hello, world!" 24 | w, h := dc.MeasureString(text) 25 | dc.Rotate(gg.Radians(10)) 26 | dc.DrawRectangle(100, 180, w, h) 27 | dc.Stroke() 28 | dc.DrawStringAnchored(text, 100, 180, 0.0, 0.0) 29 | dc.SavePNG("out.png") 30 | } 31 | -------------------------------------------------------------------------------- /examples/scatter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func CreatePoints(n int) []gg.Point { 10 | points := make([]gg.Point, n) 11 | for i := 0; i < n; i++ { 12 | x := 0.5 + rand.NormFloat64()*0.1 13 | y := x + rand.NormFloat64()*0.1 14 | points[i] = gg.Point{x, y} 15 | } 16 | return points 17 | } 18 | 19 | func main() { 20 | const S = 1024 21 | const P = 64 22 | dc := gg.NewContext(S, S) 23 | dc.InvertY() 24 | dc.SetRGB(1, 1, 1) 25 | dc.Clear() 26 | points := CreatePoints(1000) 27 | dc.Translate(P, P) 28 | dc.Scale(S-P*2, S-P*2) 29 | // draw minor grid 30 | for i := 1; i <= 10; i++ { 31 | x := float64(i) / 10 32 | dc.MoveTo(x, 0) 33 | dc.LineTo(x, 1) 34 | dc.MoveTo(0, x) 35 | dc.LineTo(1, x) 36 | } 37 | dc.SetRGBA(0, 0, 0, 0.25) 38 | dc.SetLineWidth(1) 39 | dc.Stroke() 40 | // draw axes 41 | dc.MoveTo(0, 0) 42 | dc.LineTo(1, 0) 43 | dc.MoveTo(0, 0) 44 | dc.LineTo(0, 1) 45 | dc.SetRGB(0, 0, 0) 46 | dc.SetLineWidth(4) 47 | dc.Stroke() 48 | // draw points 49 | dc.SetRGBA(0, 0, 1, 0.5) 50 | for _, p := range points { 51 | dc.DrawCircle(p.X, p.Y, 3.0/S) 52 | dc.Fill() 53 | } 54 | // draw text 55 | dc.Identity() 56 | dc.SetRGB(0, 0, 0) 57 | if err := dc.LoadFontFace("/Library/Fonts/Arial Bold.ttf", 24); err != nil { 58 | panic(err) 59 | } 60 | dc.DrawStringAnchored("Chart Title", S/2, P/2, 0.5, 0.5) 61 | if err := dc.LoadFontFace("/Library/Fonts/Arial.ttf", 18); err != nil { 62 | panic(err) 63 | } 64 | dc.DrawStringAnchored("X Axis Title", S/2, S-P/2, 0.5, 0.5) 65 | dc.SavePNG("out.png") 66 | } 67 | -------------------------------------------------------------------------------- /examples/sine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | const W = 1200 11 | const H = 60 12 | dc := gg.NewContext(W, H) 13 | // dc.SetHexColor("#FFFFFF") 14 | // dc.Clear() 15 | dc.ScaleAbout(0.95, 0.75, W/2, H/2) 16 | for i := 0; i < W; i++ { 17 | a := float64(i) * 2 * math.Pi / W * 8 18 | x := float64(i) 19 | y := (math.Sin(a) + 1) / 2 * H 20 | dc.LineTo(x, y) 21 | } 22 | dc.ClosePath() 23 | dc.SetHexColor("#3E606F") 24 | dc.FillPreserve() 25 | dc.SetHexColor("#19344180") 26 | dc.SetLineWidth(8) 27 | dc.Stroke() 28 | dc.SavePNG("out.png") 29 | } 30 | -------------------------------------------------------------------------------- /examples/spiral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | func main() { 10 | const S = 1024 11 | const N = 2048 12 | dc := gg.NewContext(S, S) 13 | dc.SetRGB(1, 1, 1) 14 | dc.Clear() 15 | dc.SetRGB(0, 0, 0) 16 | for i := 0; i <= N; i++ { 17 | t := float64(i) / N 18 | d := t*S*0.4 + 10 19 | a := t * math.Pi * 2 * 20 20 | x := S/2 + math.Cos(a)*d 21 | y := S/2 + math.Sin(a)*d 22 | r := t * 8 23 | dc.DrawCircle(x, y, r) 24 | } 25 | dc.Fill() 26 | dc.SavePNG("out.png") 27 | } 28 | -------------------------------------------------------------------------------- /examples/star.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/fogleman/gg" 7 | ) 8 | 9 | type Point struct { 10 | X, Y float64 11 | } 12 | 13 | func Polygon(n int, x, y, r float64) []Point { 14 | result := make([]Point, n) 15 | for i := 0; i < n; i++ { 16 | a := float64(i)*2*math.Pi/float64(n) - math.Pi/2 17 | result[i] = Point{x + r*math.Cos(a), y + r*math.Sin(a)} 18 | } 19 | return result 20 | } 21 | 22 | func main() { 23 | n := 5 24 | points := Polygon(n, 512, 512, 400) 25 | dc := gg.NewContext(1024, 1024) 26 | dc.SetHexColor("fff") 27 | dc.Clear() 28 | for i := 0; i < n+1; i++ { 29 | index := (i * 2) % n 30 | p := points[index] 31 | dc.LineTo(p.X, p.Y) 32 | } 33 | dc.SetRGBA(0, 0.5, 0, 1) 34 | dc.SetFillRule(gg.FillRuleEvenOdd) 35 | dc.FillPreserve() 36 | dc.SetRGBA(0, 1, 0, 0.5) 37 | dc.SetLineWidth(16) 38 | dc.Stroke() 39 | dc.SavePNG("out.png") 40 | } 41 | -------------------------------------------------------------------------------- /examples/stars.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "github.com/fogleman/gg" 8 | ) 9 | 10 | type Point struct { 11 | X, Y float64 12 | } 13 | 14 | func Polygon(n int) []Point { 15 | result := make([]Point, n) 16 | for i := 0; i < n; i++ { 17 | a := float64(i)*2*math.Pi/float64(n) - math.Pi/2 18 | result[i] = Point{math.Cos(a), math.Sin(a)} 19 | } 20 | return result 21 | } 22 | 23 | func main() { 24 | const W = 1200 25 | const H = 120 26 | const S = 100 27 | dc := gg.NewContext(W, H) 28 | dc.SetHexColor("#FFFFFF") 29 | dc.Clear() 30 | n := 5 31 | points := Polygon(n) 32 | for x := S / 2; x < W; x += S { 33 | dc.Push() 34 | s := rand.Float64()*S/4 + S/4 35 | dc.Translate(float64(x), H/2) 36 | dc.Rotate(rand.Float64() * 2 * math.Pi) 37 | dc.Scale(s, s) 38 | for i := 0; i < n+1; i++ { 39 | index := (i * 2) % n 40 | p := points[index] 41 | dc.LineTo(p.X, p.Y) 42 | } 43 | dc.SetLineWidth(10) 44 | dc.SetHexColor("#FFCC00") 45 | dc.StrokePreserve() 46 | dc.SetHexColor("#FFE43A") 47 | dc.Fill() 48 | dc.Pop() 49 | } 50 | dc.SavePNG("out.png") 51 | } 52 | -------------------------------------------------------------------------------- /examples/text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 1024 7 | dc := gg.NewContext(S, S) 8 | dc.SetRGB(1, 1, 1) 9 | dc.Clear() 10 | dc.SetRGB(0, 0, 0) 11 | if err := dc.LoadFontFace("/Library/Fonts/Arial.ttf", 96); err != nil { 12 | panic(err) 13 | } 14 | dc.DrawStringAnchored("Hello, world!", S/2, S/2, 0.5, 0.5) 15 | dc.SavePNG("out.png") 16 | } 17 | -------------------------------------------------------------------------------- /examples/tiling.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const NX = 4 7 | const NY = 3 8 | im, err := gg.LoadPNG("examples/gopher.png") 9 | if err != nil { 10 | panic(err) 11 | } 12 | w := im.Bounds().Size().X 13 | h := im.Bounds().Size().Y 14 | dc := gg.NewContext(w*NX, h*NY) 15 | for y := 0; y < NY; y++ { 16 | for x := 0; x < NX; x++ { 17 | dc.DrawImage(im, x*w, y*h) 18 | } 19 | } 20 | dc.SavePNG("out.png") 21 | } 22 | -------------------------------------------------------------------------------- /examples/unicode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | func main() { 6 | const S = 4096 * 2 7 | const T = 16 * 2 8 | const F = 28 9 | dc := gg.NewContext(S, S) 10 | dc.SetRGB(1, 1, 1) 11 | dc.Clear() 12 | dc.SetRGB(0, 0, 0) 13 | if err := dc.LoadFontFace("Xolonium-Regular.ttf", F); err != nil { 14 | panic(err) 15 | } 16 | for r := 0; r < 256; r++ { 17 | for c := 0; c < 256; c++ { 18 | i := r*256 + c 19 | x := float64(c*T) + T/2 20 | y := float64(r*T) + T/2 21 | dc.DrawStringAnchored(string(rune(i)), x, y, 0.5, 0.5) 22 | } 23 | } 24 | dc.SavePNG("out.png") 25 | } 26 | -------------------------------------------------------------------------------- /examples/wrap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fogleman/gg" 4 | 5 | const TEXT = "Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people's hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me." 6 | 7 | func main() { 8 | const W = 1024 9 | const H = 1024 10 | const P = 16 11 | dc := gg.NewContext(W, H) 12 | dc.SetRGB(1, 1, 1) 13 | dc.Clear() 14 | dc.DrawLine(W/2, 0, W/2, H) 15 | dc.DrawLine(0, H/2, W, H/2) 16 | dc.DrawRectangle(P, P, W-P-P, H-P-P) 17 | dc.SetRGBA(0, 0, 1, 0.25) 18 | dc.SetLineWidth(3) 19 | dc.Stroke() 20 | dc.SetRGB(0, 0, 0) 21 | if err := dc.LoadFontFace("/Library/Fonts/Arial Bold.ttf", 18); err != nil { 22 | panic(err) 23 | } 24 | dc.DrawStringWrapped("UPPER LEFT", P, P, 0, 0, 0, 1.5, gg.AlignLeft) 25 | dc.DrawStringWrapped("UPPER RIGHT", W-P, P, 1, 0, 0, 1.5, gg.AlignRight) 26 | dc.DrawStringWrapped("BOTTOM LEFT", P, H-P, 0, 1, 0, 1.5, gg.AlignLeft) 27 | dc.DrawStringWrapped("BOTTOM RIGHT", W-P, H-P, 1, 1, 0, 1.5, gg.AlignRight) 28 | dc.DrawStringWrapped("UPPER MIDDLE", W/2, P, 0.5, 0, 0, 1.5, gg.AlignCenter) 29 | dc.DrawStringWrapped("LOWER MIDDLE", W/2, H-P, 0.5, 1, 0, 1.5, gg.AlignCenter) 30 | dc.DrawStringWrapped("LEFT MIDDLE", P, H/2, 0, 0.5, 0, 1.5, gg.AlignLeft) 31 | dc.DrawStringWrapped("RIGHT MIDDLE", W-P, H/2, 1, 0.5, 0, 1.5, gg.AlignRight) 32 | if err := dc.LoadFontFace("/Library/Fonts/Arial.ttf", 12); err != nil { 33 | panic(err) 34 | } 35 | dc.DrawStringWrapped(TEXT, W/2-P, H/2-P, 1, 1, W/3, 1.75, gg.AlignLeft) 36 | dc.DrawStringWrapped(TEXT, W/2+P, H/2-P, 0, 1, W/3, 2, gg.AlignLeft) 37 | dc.DrawStringWrapped(TEXT, W/2-P, H/2+P, 1, 0, W/3, 2.25, gg.AlignLeft) 38 | dc.DrawStringWrapped(TEXT, W/2+P, H/2+P, 0, 0, W/3, 2.5, gg.AlignLeft) 39 | dc.SavePNG("out.png") 40 | } 41 | -------------------------------------------------------------------------------- /gradient.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "sort" 7 | ) 8 | 9 | type stop struct { 10 | pos float64 11 | color color.Color 12 | } 13 | 14 | type stops []stop 15 | 16 | // Len satisfies the Sort interface. 17 | func (s stops) Len() int { 18 | return len(s) 19 | } 20 | 21 | // Less satisfies the Sort interface. 22 | func (s stops) Less(i, j int) bool { 23 | return s[i].pos < s[j].pos 24 | } 25 | 26 | // Swap satisfies the Sort interface. 27 | func (s stops) Swap(i, j int) { 28 | s[i], s[j] = s[j], s[i] 29 | } 30 | 31 | type Gradient interface { 32 | Pattern 33 | AddColorStop(offset float64, color color.Color) 34 | } 35 | 36 | // Linear Gradient 37 | type linearGradient struct { 38 | x0, y0, x1, y1 float64 39 | stops stops 40 | } 41 | 42 | func (g *linearGradient) ColorAt(x, y int) color.Color { 43 | if len(g.stops) == 0 { 44 | return color.Transparent 45 | } 46 | 47 | fx, fy := float64(x), float64(y) 48 | x0, y0, x1, y1 := g.x0, g.y0, g.x1, g.y1 49 | dx, dy := x1-x0, y1-y0 50 | 51 | // Horizontal 52 | if dy == 0 && dx != 0 { 53 | return getColor((fx-x0)/dx, g.stops) 54 | } 55 | 56 | // Vertical 57 | if dx == 0 && dy != 0 { 58 | return getColor((fy-y0)/dy, g.stops) 59 | } 60 | 61 | // Dot product 62 | s0 := dx*(fx-x0) + dy*(fy-y0) 63 | if s0 < 0 { 64 | return g.stops[0].color 65 | } 66 | // Calculate distance to (x0,y0) alone (x0,y0)->(x1,y1) 67 | mag := math.Hypot(dx, dy) 68 | u := ((fx-x0)*-dy + (fy-y0)*dx) / (mag * mag) 69 | x2, y2 := x0+u*-dy, y0+u*dx 70 | d := math.Hypot(fx-x2, fy-y2) / mag 71 | return getColor(d, g.stops) 72 | } 73 | 74 | func (g *linearGradient) AddColorStop(offset float64, color color.Color) { 75 | g.stops = append(g.stops, stop{pos: offset, color: color}) 76 | sort.Sort(g.stops) 77 | } 78 | 79 | func NewLinearGradient(x0, y0, x1, y1 float64) Gradient { 80 | g := &linearGradient{ 81 | x0: x0, y0: y0, 82 | x1: x1, y1: y1, 83 | } 84 | return g 85 | } 86 | 87 | // Radial Gradient 88 | type circle struct { 89 | x, y, r float64 90 | } 91 | 92 | type radialGradient struct { 93 | c0, c1, cd circle 94 | a, inva float64 95 | mindr float64 96 | stops stops 97 | } 98 | 99 | func dot3(x0, y0, z0, x1, y1, z1 float64) float64 { 100 | return x0*x1 + y0*y1 + z0*z1 101 | } 102 | 103 | func (g *radialGradient) ColorAt(x, y int) color.Color { 104 | if len(g.stops) == 0 { 105 | return color.Transparent 106 | } 107 | 108 | // copy from pixman's pixman-radial-gradient.c 109 | 110 | dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y 111 | b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r) 112 | c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r) 113 | 114 | if g.a == 0 { 115 | if b == 0 { 116 | return color.Transparent 117 | } 118 | t := 0.5 * c / b 119 | if t*g.cd.r >= g.mindr { 120 | return getColor(t, g.stops) 121 | } 122 | return color.Transparent 123 | } 124 | 125 | discr := dot3(b, g.a, 0, b, -c, 0) 126 | if discr >= 0 { 127 | sqrtdiscr := math.Sqrt(discr) 128 | t0 := (b + sqrtdiscr) * g.inva 129 | t1 := (b - sqrtdiscr) * g.inva 130 | 131 | if t0*g.cd.r >= g.mindr { 132 | return getColor(t0, g.stops) 133 | } else if t1*g.cd.r >= g.mindr { 134 | return getColor(t1, g.stops) 135 | } 136 | } 137 | 138 | return color.Transparent 139 | } 140 | 141 | func (g *radialGradient) AddColorStop(offset float64, color color.Color) { 142 | g.stops = append(g.stops, stop{pos: offset, color: color}) 143 | sort.Sort(g.stops) 144 | } 145 | 146 | func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient { 147 | c0 := circle{x0, y0, r0} 148 | c1 := circle{x1, y1, r1} 149 | cd := circle{x1 - x0, y1 - y0, r1 - r0} 150 | a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r) 151 | var inva float64 152 | if a != 0 { 153 | inva = 1.0 / a 154 | } 155 | mindr := -c0.r 156 | g := &radialGradient{ 157 | c0: c0, 158 | c1: c1, 159 | cd: cd, 160 | a: a, 161 | inva: inva, 162 | mindr: mindr, 163 | } 164 | return g 165 | } 166 | 167 | // Conic Gradient 168 | type conicGradient struct { 169 | cx, cy float64 170 | rotation float64 171 | stops stops 172 | } 173 | 174 | func (g *conicGradient) ColorAt(x, y int) color.Color { 175 | if len(g.stops) == 0 { 176 | return color.Transparent 177 | } 178 | a := math.Atan2(float64(y)-g.cy, float64(x)-g.cx) 179 | t := norm(a, -math.Pi, math.Pi) - g.rotation 180 | if t < 0 { 181 | t += 1 182 | } 183 | return getColor(t, g.stops) 184 | } 185 | 186 | func (g *conicGradient) AddColorStop(offset float64, color color.Color) { 187 | g.stops = append(g.stops, stop{pos: offset, color: color}) 188 | sort.Sort(g.stops) 189 | } 190 | 191 | func NewConicGradient(cx, cy, deg float64) Gradient { 192 | g := &conicGradient{ 193 | cx: cx, 194 | cy: cy, 195 | rotation: normalizeAngle(deg) / 360, 196 | } 197 | return g 198 | } 199 | 200 | func normalizeAngle(t float64) float64 { 201 | t = math.Mod(t, 360) 202 | if t < 0 { 203 | t += 360 204 | } 205 | return t 206 | } 207 | 208 | // Map value which is in range [a..b] to range [0..1] 209 | func norm(value, a, b float64) float64 { 210 | return (value - a) * (1.0 / (b - a)) 211 | } 212 | 213 | func getColor(pos float64, stops stops) color.Color { 214 | if pos <= 0.0 || len(stops) == 1 { 215 | return stops[0].color 216 | } 217 | 218 | last := stops[len(stops)-1] 219 | 220 | if pos >= last.pos { 221 | return last.color 222 | } 223 | 224 | for i, stop := range stops[1:] { 225 | if pos < stop.pos { 226 | pos = (pos - stops[i].pos) / (stop.pos - stops[i].pos) 227 | return colorLerp(stops[i].color, stop.color, pos) 228 | } 229 | } 230 | 231 | return last.color 232 | } 233 | 234 | func colorLerp(c0, c1 color.Color, t float64) color.Color { 235 | r0, g0, b0, a0 := c0.RGBA() 236 | r1, g1, b1, a1 := c1.RGBA() 237 | 238 | return color.RGBA{ 239 | lerp(r0, r1, t), 240 | lerp(g0, g1, t), 241 | lerp(b0, b1, t), 242 | lerp(a0, a1, t), 243 | } 244 | } 245 | 246 | func lerp(a, b uint32, t float64) uint8 { 247 | return uint8(int32(float64(a)*(1.0-t)+float64(b)*t) >> 8) 248 | } 249 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import "math" 4 | 5 | type Matrix struct { 6 | XX, YX, XY, YY, X0, Y0 float64 7 | } 8 | 9 | func Identity() Matrix { 10 | return Matrix{ 11 | 1, 0, 12 | 0, 1, 13 | 0, 0, 14 | } 15 | } 16 | 17 | func Translate(x, y float64) Matrix { 18 | return Matrix{ 19 | 1, 0, 20 | 0, 1, 21 | x, y, 22 | } 23 | } 24 | 25 | func Scale(x, y float64) Matrix { 26 | return Matrix{ 27 | x, 0, 28 | 0, y, 29 | 0, 0, 30 | } 31 | } 32 | 33 | func Rotate(angle float64) Matrix { 34 | c := math.Cos(angle) 35 | s := math.Sin(angle) 36 | return Matrix{ 37 | c, s, 38 | -s, c, 39 | 0, 0, 40 | } 41 | } 42 | 43 | func Shear(x, y float64) Matrix { 44 | return Matrix{ 45 | 1, y, 46 | x, 1, 47 | 0, 0, 48 | } 49 | } 50 | 51 | func (a Matrix) Multiply(b Matrix) Matrix { 52 | return Matrix{ 53 | a.XX*b.XX + a.YX*b.XY, 54 | a.XX*b.YX + a.YX*b.YY, 55 | a.XY*b.XX + a.YY*b.XY, 56 | a.XY*b.YX + a.YY*b.YY, 57 | a.X0*b.XX + a.Y0*b.XY + b.X0, 58 | a.X0*b.YX + a.Y0*b.YY + b.Y0, 59 | } 60 | } 61 | 62 | func (a Matrix) TransformVector(x, y float64) (tx, ty float64) { 63 | tx = a.XX*x + a.XY*y 64 | ty = a.YX*x + a.YY*y 65 | return 66 | } 67 | 68 | func (a Matrix) TransformPoint(x, y float64) (tx, ty float64) { 69 | tx = a.XX*x + a.XY*y + a.X0 70 | ty = a.YX*x + a.YY*y + a.Y0 71 | return 72 | } 73 | 74 | func (a Matrix) Translate(x, y float64) Matrix { 75 | return Translate(x, y).Multiply(a) 76 | } 77 | 78 | func (a Matrix) Scale(x, y float64) Matrix { 79 | return Scale(x, y).Multiply(a) 80 | } 81 | 82 | func (a Matrix) Rotate(angle float64) Matrix { 83 | return Rotate(angle).Multiply(a) 84 | } 85 | 86 | func (a Matrix) Shear(x, y float64) Matrix { 87 | return Shear(x, y).Multiply(a) 88 | } 89 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/golang/freetype/raster" 7 | "golang.org/x/image/math/fixed" 8 | ) 9 | 10 | func flattenPath(p raster.Path) [][]Point { 11 | var result [][]Point 12 | var path []Point 13 | var cx, cy float64 14 | for i := 0; i < len(p); { 15 | switch p[i] { 16 | case 0: 17 | if len(path) > 0 { 18 | result = append(result, path) 19 | path = nil 20 | } 21 | x := unfix(p[i+1]) 22 | y := unfix(p[i+2]) 23 | path = append(path, Point{x, y}) 24 | cx, cy = x, y 25 | i += 4 26 | case 1: 27 | x := unfix(p[i+1]) 28 | y := unfix(p[i+2]) 29 | path = append(path, Point{x, y}) 30 | cx, cy = x, y 31 | i += 4 32 | case 2: 33 | x1 := unfix(p[i+1]) 34 | y1 := unfix(p[i+2]) 35 | x2 := unfix(p[i+3]) 36 | y2 := unfix(p[i+4]) 37 | points := QuadraticBezier(cx, cy, x1, y1, x2, y2) 38 | path = append(path, points...) 39 | cx, cy = x2, y2 40 | i += 6 41 | case 3: 42 | x1 := unfix(p[i+1]) 43 | y1 := unfix(p[i+2]) 44 | x2 := unfix(p[i+3]) 45 | y2 := unfix(p[i+4]) 46 | x3 := unfix(p[i+5]) 47 | y3 := unfix(p[i+6]) 48 | points := CubicBezier(cx, cy, x1, y1, x2, y2, x3, y3) 49 | path = append(path, points...) 50 | cx, cy = x3, y3 51 | i += 8 52 | default: 53 | panic("bad path") 54 | } 55 | } 56 | if len(path) > 0 { 57 | result = append(result, path) 58 | } 59 | return result 60 | } 61 | 62 | func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point { 63 | var result [][]Point 64 | if len(dashes) == 0 { 65 | return paths 66 | } 67 | if len(dashes) == 1 { 68 | dashes = append(dashes, dashes[0]) 69 | } 70 | for _, path := range paths { 71 | if len(path) < 2 { 72 | continue 73 | } 74 | previous := path[0] 75 | pathIndex := 1 76 | dashIndex := 0 77 | segmentLength := 0.0 78 | 79 | // offset 80 | if offset != 0 { 81 | var totalLength float64 82 | for _, dashLength := range dashes { 83 | totalLength += dashLength 84 | } 85 | offset = math.Mod(offset, totalLength) 86 | if offset < 0 { 87 | offset += totalLength 88 | } 89 | for i, dashLength := range dashes { 90 | offset -= dashLength 91 | if offset < 0 { 92 | dashIndex = i 93 | segmentLength = dashLength + offset 94 | break 95 | } 96 | } 97 | } 98 | 99 | var segment []Point 100 | segment = append(segment, previous) 101 | for pathIndex < len(path) { 102 | dashLength := dashes[dashIndex] 103 | point := path[pathIndex] 104 | d := previous.Distance(point) 105 | maxd := dashLength - segmentLength 106 | if d > maxd { 107 | t := maxd / d 108 | p := previous.Interpolate(point, t) 109 | segment = append(segment, p) 110 | if dashIndex%2 == 0 && len(segment) > 1 { 111 | result = append(result, segment) 112 | } 113 | segment = nil 114 | segment = append(segment, p) 115 | segmentLength = 0 116 | previous = p 117 | dashIndex = (dashIndex + 1) % len(dashes) 118 | } else { 119 | segment = append(segment, point) 120 | previous = point 121 | segmentLength += d 122 | pathIndex++ 123 | } 124 | } 125 | if dashIndex%2 == 0 && len(segment) > 1 { 126 | result = append(result, segment) 127 | } 128 | } 129 | return result 130 | } 131 | 132 | func rasterPath(paths [][]Point) raster.Path { 133 | var result raster.Path 134 | for _, path := range paths { 135 | var previous fixed.Point26_6 136 | for i, point := range path { 137 | f := point.Fixed() 138 | if i == 0 { 139 | result.Start(f) 140 | } else { 141 | dx := f.X - previous.X 142 | dy := f.Y - previous.Y 143 | if dx < 0 { 144 | dx = -dx 145 | } 146 | if dy < 0 { 147 | dy = -dy 148 | } 149 | if dx+dy > 8 { 150 | // TODO: this is a hack for cases where two points are 151 | // too close - causes rendering issues with joins / caps 152 | result.Add1(f) 153 | } 154 | } 155 | previous = f 156 | } 157 | } 158 | return result 159 | } 160 | 161 | func dashed(path raster.Path, dashes []float64, offset float64) raster.Path { 162 | return rasterPath(dashPath(flattenPath(path), dashes, offset)) 163 | } 164 | -------------------------------------------------------------------------------- /pattern.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/golang/freetype/raster" 8 | ) 9 | 10 | type RepeatOp int 11 | 12 | const ( 13 | RepeatBoth RepeatOp = iota 14 | RepeatX 15 | RepeatY 16 | RepeatNone 17 | ) 18 | 19 | type Pattern interface { 20 | ColorAt(x, y int) color.Color 21 | } 22 | 23 | // Solid Pattern 24 | type solidPattern struct { 25 | color color.Color 26 | } 27 | 28 | func (p *solidPattern) ColorAt(x, y int) color.Color { 29 | return p.color 30 | } 31 | 32 | func NewSolidPattern(color color.Color) Pattern { 33 | return &solidPattern{color: color} 34 | } 35 | 36 | // Surface Pattern 37 | type surfacePattern struct { 38 | im image.Image 39 | op RepeatOp 40 | } 41 | 42 | func (p *surfacePattern) ColorAt(x, y int) color.Color { 43 | b := p.im.Bounds() 44 | switch p.op { 45 | case RepeatX: 46 | if y >= b.Dy() { 47 | return color.Transparent 48 | } 49 | case RepeatY: 50 | if x >= b.Dx() { 51 | return color.Transparent 52 | } 53 | case RepeatNone: 54 | if x >= b.Dx() || y >= b.Dy() { 55 | return color.Transparent 56 | } 57 | } 58 | x = x%b.Dx() + b.Min.X 59 | y = y%b.Dy() + b.Min.Y 60 | return p.im.At(x, y) 61 | } 62 | 63 | func NewSurfacePattern(im image.Image, op RepeatOp) Pattern { 64 | return &surfacePattern{im: im, op: op} 65 | } 66 | 67 | type patternPainter struct { 68 | im *image.RGBA 69 | mask *image.Alpha 70 | p Pattern 71 | } 72 | 73 | // Paint satisfies the Painter interface. 74 | func (r *patternPainter) Paint(ss []raster.Span, done bool) { 75 | b := r.im.Bounds() 76 | for _, s := range ss { 77 | if s.Y < b.Min.Y { 78 | continue 79 | } 80 | if s.Y >= b.Max.Y { 81 | return 82 | } 83 | if s.X0 < b.Min.X { 84 | s.X0 = b.Min.X 85 | } 86 | if s.X1 > b.Max.X { 87 | s.X1 = b.Max.X 88 | } 89 | if s.X0 >= s.X1 { 90 | continue 91 | } 92 | const m = 1<<16 - 1 93 | y := s.Y - r.im.Rect.Min.Y 94 | x0 := s.X0 - r.im.Rect.Min.X 95 | // RGBAPainter.Paint() in $GOPATH/src/github.com/golang/freetype/raster/paint.go 96 | i0 := (s.Y-r.im.Rect.Min.Y)*r.im.Stride + (s.X0-r.im.Rect.Min.X)*4 97 | i1 := i0 + (s.X1-s.X0)*4 98 | for i, x := i0, x0; i < i1; i, x = i+4, x+1 { 99 | ma := s.Alpha 100 | if r.mask != nil { 101 | ma = ma * uint32(r.mask.AlphaAt(x, y).A) / 255 102 | if ma == 0 { 103 | continue 104 | } 105 | } 106 | c := r.p.ColorAt(x, y) 107 | cr, cg, cb, ca := c.RGBA() 108 | dr := uint32(r.im.Pix[i+0]) 109 | dg := uint32(r.im.Pix[i+1]) 110 | db := uint32(r.im.Pix[i+2]) 111 | da := uint32(r.im.Pix[i+3]) 112 | a := (m - (ca * ma / m)) * 0x101 113 | r.im.Pix[i+0] = uint8((dr*a + cr*ma) / m >> 8) 114 | r.im.Pix[i+1] = uint8((dg*a + cg*ma) / m >> 8) 115 | r.im.Pix[i+2] = uint8((db*a + cb*ma) / m >> 8) 116 | r.im.Pix[i+3] = uint8((da*a + ca*ma) / m >> 8) 117 | } 118 | } 119 | } 120 | 121 | func newPatternPainter(im *image.RGBA, mask *image.Alpha, p Pattern) *patternPainter { 122 | return &patternPainter{im, mask, p} 123 | } 124 | -------------------------------------------------------------------------------- /point.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "math" 5 | 6 | "golang.org/x/image/math/fixed" 7 | ) 8 | 9 | type Point struct { 10 | X, Y float64 11 | } 12 | 13 | func (a Point) Fixed() fixed.Point26_6 { 14 | return fixp(a.X, a.Y) 15 | } 16 | 17 | func (a Point) Distance(b Point) float64 { 18 | return math.Hypot(a.X-b.X, a.Y-b.Y) 19 | } 20 | 21 | func (a Point) Interpolate(b Point, t float64) Point { 22 | x := a.X + (b.X-a.X)*t 23 | y := a.Y + (b.Y-a.Y)*t 24 | return Point{x, y} 25 | } 26 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | "image/jpeg" 8 | _ "image/jpeg" 9 | "image/png" 10 | "io/ioutil" 11 | "math" 12 | "os" 13 | "strings" 14 | 15 | "github.com/golang/freetype/truetype" 16 | 17 | "golang.org/x/image/font" 18 | "golang.org/x/image/math/fixed" 19 | ) 20 | 21 | func Radians(degrees float64) float64 { 22 | return degrees * math.Pi / 180 23 | } 24 | 25 | func Degrees(radians float64) float64 { 26 | return radians * 180 / math.Pi 27 | } 28 | 29 | func LoadImage(path string) (image.Image, error) { 30 | file, err := os.Open(path) 31 | if err != nil { 32 | return nil, err 33 | } 34 | defer file.Close() 35 | im, _, err := image.Decode(file) 36 | return im, err 37 | } 38 | 39 | func LoadPNG(path string) (image.Image, error) { 40 | file, err := os.Open(path) 41 | if err != nil { 42 | return nil, err 43 | } 44 | defer file.Close() 45 | return png.Decode(file) 46 | } 47 | 48 | func SavePNG(path string, im image.Image) error { 49 | file, err := os.Create(path) 50 | if err != nil { 51 | return err 52 | } 53 | defer file.Close() 54 | return png.Encode(file, im) 55 | } 56 | 57 | func LoadJPG(path string) (image.Image, error) { 58 | file, err := os.Open(path) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer file.Close() 63 | return jpeg.Decode(file) 64 | } 65 | 66 | func SaveJPG(path string, im image.Image, quality int) error { 67 | file, err := os.Create(path) 68 | if err != nil { 69 | return err 70 | } 71 | defer file.Close() 72 | 73 | var opt jpeg.Options 74 | opt.Quality = quality 75 | 76 | return jpeg.Encode(file, im, &opt) 77 | } 78 | 79 | func imageToRGBA(src image.Image) *image.RGBA { 80 | bounds := src.Bounds() 81 | dst := image.NewRGBA(bounds) 82 | draw.Draw(dst, bounds, src, bounds.Min, draw.Src) 83 | return dst 84 | } 85 | 86 | func parseHexColor(x string) (r, g, b, a int) { 87 | x = strings.TrimPrefix(x, "#") 88 | a = 255 89 | if len(x) == 3 { 90 | format := "%1x%1x%1x" 91 | fmt.Sscanf(x, format, &r, &g, &b) 92 | r |= r << 4 93 | g |= g << 4 94 | b |= b << 4 95 | } 96 | if len(x) == 6 { 97 | format := "%02x%02x%02x" 98 | fmt.Sscanf(x, format, &r, &g, &b) 99 | } 100 | if len(x) == 8 { 101 | format := "%02x%02x%02x%02x" 102 | fmt.Sscanf(x, format, &r, &g, &b, &a) 103 | } 104 | return 105 | } 106 | 107 | func fixp(x, y float64) fixed.Point26_6 { 108 | return fixed.Point26_6{fix(x), fix(y)} 109 | } 110 | 111 | func fix(x float64) fixed.Int26_6 { 112 | return fixed.Int26_6(math.Round(x * 64)) 113 | } 114 | 115 | func unfix(x fixed.Int26_6) float64 { 116 | const shift, mask = 6, 1<<6 - 1 117 | if x >= 0 { 118 | return float64(x>>shift) + float64(x&mask)/64 119 | } 120 | x = -x 121 | if x >= 0 { 122 | return -(float64(x>>shift) + float64(x&mask)/64) 123 | } 124 | return 0 125 | } 126 | 127 | // LoadFontFace is a helper function to load the specified font file with 128 | // the specified point size. Note that the returned `font.Face` objects 129 | // are not thread safe and cannot be used in parallel across goroutines. 130 | // You can usually just use the Context.LoadFontFace function instead of 131 | // this package-level function. 132 | func LoadFontFace(path string, points float64) (font.Face, error) { 133 | fontBytes, err := ioutil.ReadFile(path) 134 | if err != nil { 135 | return nil, err 136 | } 137 | f, err := truetype.Parse(fontBytes) 138 | if err != nil { 139 | return nil, err 140 | } 141 | face := truetype.NewFace(f, &truetype.Options{ 142 | Size: points, 143 | // Hinting: font.HintingFull, 144 | }) 145 | return face, nil 146 | } 147 | -------------------------------------------------------------------------------- /wrap.go: -------------------------------------------------------------------------------- 1 | package gg 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | type measureStringer interface { 9 | MeasureString(s string) (w, h float64) 10 | } 11 | 12 | func splitOnSpace(x string) []string { 13 | var result []string 14 | pi := 0 15 | ps := false 16 | for i, c := range x { 17 | s := unicode.IsSpace(c) 18 | if s != ps && i > 0 { 19 | result = append(result, x[pi:i]) 20 | pi = i 21 | } 22 | ps = s 23 | } 24 | result = append(result, x[pi:]) 25 | return result 26 | } 27 | 28 | func wordWrap(m measureStringer, s string, width float64) []string { 29 | var result []string 30 | for _, line := range strings.Split(s, "\n") { 31 | fields := splitOnSpace(line) 32 | if len(fields)%2 == 1 { 33 | fields = append(fields, "") 34 | } 35 | x := "" 36 | for i := 0; i < len(fields); i += 2 { 37 | w, _ := m.MeasureString(x + fields[i]) 38 | if w > width { 39 | if x == "" { 40 | result = append(result, fields[i]) 41 | x = "" 42 | continue 43 | } else { 44 | result = append(result, x) 45 | x = "" 46 | } 47 | } 48 | x += fields[i] + fields[i+1] 49 | } 50 | if x != "" { 51 | result = append(result, x) 52 | } 53 | } 54 | for i, line := range result { 55 | result[i] = strings.TrimSpace(line) 56 | } 57 | return result 58 | } 59 | --------------------------------------------------------------------------------