Compare commits

...

57 Commits
v1.0.0 ... main

Author SHA1 Message Date
d9a531518d migrated module to git.milar.in 2023-01-22 12:06:29 +01:00
Timon Ringwald
cc30f39e11 fixed drawclipping when drawing text on subimages 2022-04-22 11:57:30 +02:00
Timon Ringwald
69923d9438 fixed draw clipping on subimages 2022-04-22 11:49:17 +02:00
Timon Ringwald
2755d666d6 moduling 2022-04-22 11:48:47 +02:00
Michael Fogleman
8febc0f526 Fix rounding when converting to fixed precision 2021-09-28 10:35:35 -04:00
Michael Fogleman
af4cd58078
Merge pull request #124 from mazznoer/conic-gradient
Add conic gradient
2021-01-31 12:28:31 -05:00
Nor Khasyatillah
c26555283c Add conic gradient 2021-01-29 19:00:22 +07:00
Michael Fogleman
ad4d1eafac
Update README.md 2020-05-14 21:10:29 -04:00
Michael Fogleman
8d127f461d
Merge pull request #100 from ajnirp/patch-1
Update documentation link
2020-05-14 21:09:58 -04:00
Rohan Prinja
b89ba07b94
Update documentation link
When visiting the godoc link, a banner at the top informs me that "pkg.go.dev is a new destination for Go discovery and docs". While it doesn't look like godoc is going away, I figured it would be nice to link to both sites.
2020-05-14 17:15:05 -07:00
Michael Fogleman
4dc34561c6
Merge pull request #83 from Xeoncross/master
Add support for JPEG encoding
2019-08-26 15:13:58 -04:00
xeoncross
068da56c91 Added support for JPEG encoding 2019-08-26 14:04:06 -05:00
Michael Fogleman
f194ddec6f
Merge pull request #77 from leexingliang/master
modify comment
2019-06-12 10:10:55 -04:00
lixingliang
6897f9a1a0 modify comment 2019-06-12 11:43:36 +08:00
Michael Fogleman
72436a171b fix test for 1.12+ 2019-04-16 21:30:52 -04:00
Michael Fogleman
5899172fb0
Merge pull request #70 from bevand10/master
Add SaveJPG method
2019-04-16 21:28:22 -04:00
Dave Bevan
1236b6346f Add SaveJPG method 2019-04-12 18:25:28 +01:00
Michael Fogleman
0403632d5b New test image. 2019-02-20 17:12:49 -05:00
Michael Fogleman
64338842c5 New test image. 2019-02-20 17:12:06 -05:00
Michael Fogleman
3bcf9e0320 Add SetDashOffset. Close #64. 2019-02-20 16:47:39 -05:00
Michael Fogleman
fa28a6e1e3
Merge pull request #65 from kortschak/average
Use fewer ops to calculate mid-point
2019-02-20 10:22:23 -05:00
Dan Kortschak
f23d82b106 Use fewer ops to calculate mid-point 2019-02-20 17:08:29 +10:30
Michael Fogleman
0e8122236d
Merge pull request #61 from JamieCrisman/get-current
Allows the ability to get the current point in the context
2019-02-07 15:47:22 -05:00
Jamie Crisman
ccde3a8923 Allows the ability to get the current point in the context 2019-02-07 14:39:14 -06:00
Michael Fogleman
3795562800
Merge pull request #60 from emaele/jpeg-open&save
Load and save jpeg images
2019-01-19 16:22:23 -05:00
Emanuele Rocco Petrone
74a8429cb8 load and save jpeg images 2019-01-19 18:47:38 +01:00
Michael Fogleman
9db508d34a
Update README.md 2019-01-14 11:18:59 -05:00
Michael Fogleman
77d18b88fe
Merge pull request #52 from rekby/multiline-measure
Add MeasureMultilineString method to Context.
2019-01-14 11:16:02 -05:00
Michael Fogleman
cdabe43353 Add InvertMask and an example 2019-01-14 11:04:49 -05:00
Timofey Kulin
16a00d1152 Add MeasureMultilineString method to Context.
Fix #https://github.com/fogleman/gg/issues/49
2018-10-23 09:27:48 +03:00
Michael Fogleman
0e0ff3ade7 Add gofont.go example, close #45 2018-08-22 21:57:45 -04:00
Michael Fogleman
7cc16ce8b2 Add -save test flag 2018-08-17 11:14:38 -04:00
Michael Fogleman
eb261f0bd1 Perf improvement when nil clip and solid color (usual case) 2018-08-16 22:49:05 -04:00
Michael Fogleman
a4f287e211 Fix #23 via doc on gg.LoadFontFace function. 2018-08-16 22:22:39 -04:00
Michael Fogleman
9a34078211 Fix #42. Performance improvement by reusing Rasterizer 2018-08-16 22:18:45 -04:00
Michael Fogleman
6bee5281ff Add BenchmarkCircles for simple benchmarking 2018-08-16 22:18:08 -04:00
Michael Fogleman
b2d255c6f2 Add some tests! (mostly regression tests) 2018-08-16 22:08:53 -04:00
Michael Fogleman
a9ff18eccd
Merge pull request #38 from tschaub/subimage
Start copying at bounds min instead of zero point
2018-03-16 10:14:17 -04:00
Tim Schaub
ca366ba15b Start copying at bounds min instead of zero point 2018-03-15 21:28:47 -06:00
Michael Fogleman
c97f757e6f better DrawEllipticalArc behavior when dc.hasCurrent is true 2018-03-08 13:42:55 -05:00
Michael Fogleman
da3d0863b9 add crisp lines example 2018-02-23 09:52:38 -05:00
Michael Fogleman
363d282ef3
Merge pull request #28 from derekschaab/issue-25
Return alpha-premultiplied color in gradient interpolation
2018-02-12 10:03:43 -05:00
Michael Fogleman
1b3894b028 Add SetMask and AsMask and an example using them 2018-02-11 23:15:32 -05:00
Michael Fogleman
c828b09e4a
Merge pull request #31 from magneticcoffee/get-dc-fontheight
Add a context fontHeight getter
2018-02-08 12:06:39 -05:00
Alexander Kahl
00ed7b79c0 Add a context fontHeight getter 2017-10-31 16:09:26 +01:00
Derek Schaab
ffbeb6b231 Return alpha-premultiplied color in gradient interpolation
Fixes #25.
2017-09-30 21:34:46 -05:00
Michael Fogleman
e611489b86 concat example 2017-07-28 18:55:21 -04:00
Michael Fogleman
ee8994ff90 Update README.md 2017-05-03 21:14:43 -04:00
Michael Fogleman
30f3b1d4ae Update README.md 2017-05-03 21:09:02 -04:00
Michael Fogleman
7fb6ce3c57 Merge pull request #16 from wsw0108/rotated-image-text
Draw image/text with current transformation appied
2017-05-03 21:06:54 -04:00
Michael Fogleman
0a834b0873 unicode table 2017-04-29 14:19:37 -04:00
Michael Fogleman
5e707d618f Update README.md 2017-04-14 13:57:42 -04:00
wsw
2c35caba58 apply current matrix when draw text 2017-01-08 16:23:33 +08:00
wsw
5e5aa69079 update example 'rotated-image' 2017-01-08 16:18:14 +08:00
wsw
b13517ff6f Merge branch 'master' into draw-image-rotated 2016-12-09 15:52:52 +08:00
wsw
3be68eb22e Merge branch 'master' into draw-image-rotated 2016-12-08 13:24:25 +08:00
wsw
f06d3564a1 apply current matrix when draw image 2016-12-08 11:16:19 +08:00
21 changed files with 923 additions and 44 deletions

View File

@ -6,11 +6,16 @@
## Installation ## Installation
go get github.com/fogleman/gg go get -u github.com/fogleman/gg
## GoDoc Alternatively, you may use gopkg.in to grab a specific major-version:
https://godoc.org/github.com/fogleman/gg go get -u gopkg.in/fogleman/gg.v1
## Documentation
- godoc: https://godoc.org/github.com/fogleman/gg
- pkg.go.dev: https://pkg.go.dev/github.com/fogleman/gg?tab=doc
## Hello, Circle! ## Hello, Circle!
@ -91,6 +96,7 @@ DrawString(s string, x, y float64)
DrawStringAnchored(s string, x, y, ax, ay float64) DrawStringAnchored(s string, x, y, ax, ay float64)
DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
MeasureString(s string) (w, h float64) MeasureString(s string) (w, h float64)
MeasureMultilineString(s string, lineSpacing float64) (w, h float64)
WordWrap(s string, w float64) []string WordWrap(s string, w float64) []string
SetFontFace(fontFace font.Face) SetFontFace(fontFace font.Face)
LoadFontFace(path string, points float64) error LoadFontFace(path string, points float64) error
@ -116,12 +122,13 @@ SetLineWidth(lineWidth float64)
SetLineCap(lineCap LineCap) SetLineCap(lineCap LineCap)
SetLineJoin(lineJoin LineJoin) SetLineJoin(lineJoin LineJoin)
SetDash(dashes ...float64) SetDash(dashes ...float64)
SetDashOffset(offset float64)
SetFillRule(fillRule FillRule) SetFillRule(fillRule FillRule)
``` ```
## Gradients & Patterns ## Gradients & Patterns
`gg` supports linear and radial gradients and surface patterns. You can also implement your own patterns. `gg` supports linear, radial and conic gradients and surface patterns. You can also implement your own patterns.
```go ```go
SetFillStyle(pattern Pattern) SetFillStyle(pattern Pattern)
@ -129,6 +136,7 @@ SetStrokeStyle(pattern Pattern)
NewSolidPattern(color color.Color) NewSolidPattern(color color.Color)
NewLinearGradient(x0, y0, x1, y1 float64) NewLinearGradient(x0, y0, x1, y1 float64)
NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) NewRadialGradient(x0, y0, r0, x1, y1, r1 float64)
NewConicGradient(cx, cy, deg float64)
NewSurfacePattern(im image.Image, op RepeatOp) NewSurfacePattern(im image.Image, op RepeatOp)
``` ```
@ -151,8 +159,6 @@ It is often desired to rotate or scale about a point that is not the origin. The
`InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom. `InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom.
Note: transforms do not currently affect `DrawImage` or `DrawString`.
## Stack Functions ## Stack Functions
Save and restore the state of the context. These can be nested. Save and restore the state of the context. These can be nested.
@ -171,6 +177,9 @@ defined using paths.
Clip() Clip()
ClipPreserve() ClipPreserve()
ResetClip() ResetClip()
AsMask() *image.Alpha
SetMask(mask *image.Alpha)
InvertMask()
``` ```
## Helper Functions ## Helper Functions
@ -187,12 +196,6 @@ SavePNG(path string, im image.Image) error
![Separator](http://i.imgur.com/fsUvnPB.png) ![Separator](http://i.imgur.com/fsUvnPB.png)
## How Do it Do?
`gg` is mostly a wrapper around `github.com/golang/freetype/raster`. The goal
is to provide some more functionality and a nicer API that will suffice for
most use cases.
## Another Example ## Another Example
See the output of this example below. See the output of this example below.

View File

@ -2,16 +2,20 @@
package gg package gg
import ( import (
"errors"
"image" "image"
"image/color" "image/color"
"image/draw" "image/jpeg"
"image/png" "image/png"
"io" "io"
"math" "math"
"strings"
"github.com/golang/freetype/raster" "github.com/golang/freetype/raster"
"golang.org/x/image/draw"
"golang.org/x/image/font" "golang.org/x/image/font"
"golang.org/x/image/font/basicfont" "golang.org/x/image/font/basicfont"
"golang.org/x/image/math/f64"
) )
type LineCap int type LineCap int
@ -52,6 +56,7 @@ var (
type Context struct { type Context struct {
width int width int
height int height int
rasterizer *raster.Rasterizer
im *image.RGBA im *image.RGBA
mask *image.Alpha mask *image.Alpha
color color.Color color color.Color
@ -63,6 +68,7 @@ type Context struct {
current Point current Point
hasCurrent bool hasCurrent bool
dashes []float64 dashes []float64
dashOffset float64
lineWidth float64 lineWidth float64
lineCap LineCap lineCap LineCap
lineJoin LineJoin lineJoin LineJoin
@ -88,9 +94,12 @@ func NewContextForImage(im image.Image) *Context {
// NewContextForRGBA prepares a context for rendering onto the specified image. // NewContextForRGBA prepares a context for rendering onto the specified image.
// No copy is made. // No copy is made.
func NewContextForRGBA(im *image.RGBA) *Context { func NewContextForRGBA(im *image.RGBA) *Context {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
return &Context{ return &Context{
width: im.Bounds().Size().X, width: w,
height: im.Bounds().Size().Y, height: h,
rasterizer: raster.NewRasterizer(w, h),
im: im, im: im,
color: color.Transparent, color: color.Transparent,
fillPattern: defaultFillStyle, fillPattern: defaultFillStyle,
@ -103,6 +112,15 @@ func NewContextForRGBA(im *image.RGBA) *Context {
} }
} }
// GetCurrentPoint will return the current point and if there is a current point.
// The point will have been transformed by the context's transformation matrix.
func (dc *Context) GetCurrentPoint() (Point, bool) {
if dc.hasCurrent {
return dc.current, true
}
return Point{}, false
}
// Image returns the image that has been drawn by this context. // Image returns the image that has been drawn by this context.
func (dc *Context) Image() image.Image { func (dc *Context) Image() image.Image {
return dc.im return dc.im
@ -123,11 +141,23 @@ func (dc *Context) SavePNG(path string) error {
return SavePNG(path, dc.im) return SavePNG(path, dc.im)
} }
// SaveJPG encodes the image as a JPG and writes it to disk.
func (dc *Context) SaveJPG(path string, quality int) error {
return SaveJPG(path, dc.im, quality)
}
// EncodePNG encodes the image as a PNG and writes it to the provided io.Writer. // EncodePNG encodes the image as a PNG and writes it to the provided io.Writer.
func (dc *Context) EncodePNG(w io.Writer) error { func (dc *Context) EncodePNG(w io.Writer) error {
return png.Encode(w, dc.im) return png.Encode(w, dc.im)
} }
// EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer
// in JPEG 4:2:0 baseline format with the given options.
// Default parameters are used if a nil *jpeg.Options is passed.
func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error {
return jpeg.Encode(w, dc.im, o)
}
// SetDash sets the current dash pattern to use. Call with zero arguments to // SetDash sets the current dash pattern to use. Call with zero arguments to
// disable dashes. The values specify the lengths of each dash, with // disable dashes. The values specify the lengths of each dash, with
// alternating on and off lengths. // alternating on and off lengths.
@ -135,6 +165,12 @@ func (dc *Context) SetDash(dashes ...float64) {
dc.dashes = dashes dc.dashes = dashes
} }
// SetDashOffset sets the initial offset into the dash pattern to use when
// stroking dashed paths.
func (dc *Context) SetDashOffset(offset float64) {
dc.dashOffset = offset
}
func (dc *Context) SetLineWidth(lineWidth float64) { func (dc *Context) SetLineWidth(lineWidth float64) {
dc.lineWidth = lineWidth dc.lineWidth = lineWidth
} }
@ -373,14 +409,16 @@ func (dc *Context) joiner() raster.Joiner {
func (dc *Context) stroke(painter raster.Painter) { func (dc *Context) stroke(painter raster.Painter) {
path := dc.strokePath path := dc.strokePath
if len(dc.dashes) > 0 { if len(dc.dashes) > 0 {
path = dashed(path, dc.dashes) path = dashed(path, dc.dashes, dc.dashOffset)
} else { } else {
// TODO: this is a temporary workaround to remove tiny segments // TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues // that result in rendering issues
path = rasterPath(flattenPath(path)) path = rasterPath(flattenPath(path))
} }
r := raster.NewRasterizer(dc.width, dc.height) r := dc.rasterizer
r.UseNonZeroWinding = true r.UseNonZeroWinding = true
r.Clear()
r.Dx, r.Dy = dc.im.Bounds().Min.X, dc.im.Bounds().Min.Y
r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner()) r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
r.Rasterize(painter) r.Rasterize(painter)
} }
@ -392,8 +430,10 @@ func (dc *Context) fill(painter raster.Painter) {
copy(path, dc.fillPath) copy(path, dc.fillPath)
path.Add1(dc.start.Fixed()) path.Add1(dc.start.Fixed())
} }
r := raster.NewRasterizer(dc.width, dc.height) r := dc.rasterizer
r.UseNonZeroWinding = dc.fillRule == FillRuleWinding r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
r.Clear()
r.Dx, r.Dy = dc.im.Bounds().Min.X, dc.im.Bounds().Min.Y
r.AddPath(path) r.AddPath(path)
r.Rasterize(painter) r.Rasterize(painter)
} }
@ -402,7 +442,19 @@ func (dc *Context) fill(painter raster.Painter) {
// line cap, line join and dash settings. The path is preserved after this // line cap, line join and dash settings. The path is preserved after this
// operation. // operation.
func (dc *Context) StrokePreserve() { func (dc *Context) StrokePreserve() {
painter := newPatternPainter(dc.im, dc.mask, dc.strokePattern) var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.strokePattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern)
}
dc.stroke(painter) dc.stroke(painter)
} }
@ -417,7 +469,19 @@ func (dc *Context) Stroke() {
// FillPreserve fills the current path with the current color. Open subpaths // FillPreserve fills the current path with the current color. Open subpaths
// are implicity closed. The path is preserved after this operation. // are implicity closed. The path is preserved after this operation.
func (dc *Context) FillPreserve() { func (dc *Context) FillPreserve() {
painter := newPatternPainter(dc.im, dc.mask, dc.fillPattern) var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.fillPattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern)
}
dc.fill(painter) dc.fill(painter)
} }
@ -444,6 +508,38 @@ func (dc *Context) ClipPreserve() {
} }
} }
// SetMask allows you to directly set the *image.Alpha to be used as a clipping
// mask. It must be the same size as the context, else an error is returned
// and the mask is unchanged.
func (dc *Context) SetMask(mask *image.Alpha) error {
if mask.Bounds().Size() != dc.im.Bounds().Size() {
return errors.New("mask size must match context size")
}
dc.mask = mask
return nil
}
// AsMask returns an *image.Alpha representing the alpha channel of this
// context. This can be useful for advanced clipping operations where you first
// render the mask geometry and then use it as a mask.
func (dc *Context) AsMask() *image.Alpha {
mask := image.NewAlpha(dc.im.Bounds())
draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src)
return mask
}
// InvertMask inverts the alpha values in the current clipping mask such that
// a fully transparent region becomes fully opaque and vice versa.
func (dc *Context) InvertMask() {
if dc.mask == nil {
dc.mask = image.NewAlpha(dc.im.Bounds())
} else {
for i, a := range dc.mask.Pix {
dc.mask.Pix[i] = 255 - a
}
}
}
// Clip updates the clipping region by intersecting the current // Clip updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by dc.Fill(). // clipping region with the current path as it would be filled by dc.Fill().
// The path is cleared after this operation. // The path is cleared after this operation.
@ -520,14 +616,18 @@ func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
a2 := angle1 + (angle2-angle1)*p2 a2 := angle1 + (angle2-angle1)*p2
x0 := x + rx*math.Cos(a1) x0 := x + rx*math.Cos(a1)
y0 := y + ry*math.Sin(a1) y0 := y + ry*math.Sin(a1)
x1 := x + rx*math.Cos(a1+(a2-a1)/2) x1 := x + rx*math.Cos((a1+a2)/2)
y1 := y + ry*math.Sin(a1+(a2-a1)/2) y1 := y + ry*math.Sin((a1+a2)/2)
x2 := x + rx*math.Cos(a2) x2 := x + rx*math.Cos(a2)
y2 := y + ry*math.Sin(a2) y2 := y + ry*math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2 cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2 cy := 2*y1 - y0/2 - y2/2
if i == 0 && !dc.hasCurrent { if i == 0 {
dc.MoveTo(x0, y0) if dc.hasCurrent {
dc.LineTo(x0, y0)
} else {
dc.MoveTo(x0, y0)
}
} }
dc.QuadraticTo(cx, cy, x2, y2) dc.QuadraticTo(cx, cy, x2, y2)
} }
@ -564,7 +664,6 @@ func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) {
} }
// DrawImage draws the specified image at the specified point. // DrawImage draws the specified image at the specified point.
// Currently, rotation and scaling transforms are not supported.
func (dc *Context) DrawImage(im image.Image, x, y int) { func (dc *Context) DrawImage(im image.Image, x, y int) {
dc.DrawImageAnchored(im, x, y, 0, 0) dc.DrawImageAnchored(im, x, y, 0, 0)
} }
@ -576,12 +675,17 @@ func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) {
s := im.Bounds().Size() s := im.Bounds().Size()
x -= int(ax * float64(s.X)) x -= int(ax * float64(s.X))
y -= int(ay * float64(s.Y)) y -= int(ay * float64(s.Y))
p := image.Pt(x, y) transformer := draw.BiLinear
r := image.Rectangle{p, p.Add(s)} fx, fy := float64(x), float64(y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
if dc.mask == nil { if dc.mask == nil {
draw.Draw(dc.im, r, im, image.ZP, draw.Over) transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
} else { } else {
draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over) transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{
DstMask: dc.mask,
DstMaskP: image.ZP,
})
} }
} }
@ -601,6 +705,10 @@ func (dc *Context) LoadFontFace(path string, points float64) error {
return err return err
} }
func (dc *Context) FontHeight() float64 {
return dc.fontHeight
}
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) { func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
d := &font.Drawer{ d := &font.Drawer{
Dst: im, Dst: im,
@ -608,11 +716,34 @@ func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
Face: dc.fontFace, Face: dc.fontFace,
Dot: fixp(x, y), Dot: fixp(x, y),
} }
d.DrawString(s) // based on Drawer.DrawString() in golang.org/x/image/font/font.go
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
d.Dot.X += d.Face.Kern(prevC, c)
}
dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
sr := dr.Sub(dr.Min)
transformer := draw.BiLinear
fx, fy := float64(dr.Min.X), float64(dr.Min.Y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
d.Dot.X += advance
prevC = c
}
} }
// DrawString draws the specified text at the specified point. // DrawString draws the specified text at the specified point.
// Currently, rotation and scaling transforms are not supported.
func (dc *Context) DrawString(s string, x, y float64) { func (dc *Context) DrawString(s string, x, y float64) {
dc.DrawStringAnchored(s, x, y, 0, 0) dc.DrawStringAnchored(s, x, y, 0, 0)
} }
@ -622,11 +753,10 @@ func (dc *Context) DrawString(s string, x, y float64) {
// text. Use ax=0.5, ay=0.5 to center the text at the specified point. // text. Use ax=0.5, ay=0.5 to center the text at the specified point.
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) { func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
w, h := dc.MeasureString(s) w, h := dc.MeasureString(s)
x, y = dc.TransformPoint(x, y)
x -= ax * w x -= ax * w
y += ay * h y += ay * h
if dc.mask == nil { if dc.mask == nil {
dc.drawString(dc.im, s, x, y) dc.drawString(dc.im, s, x+float64(dc.im.Bounds().Min.X), y+float64(dc.im.Bounds().Min.Y))
} else { } else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
dc.drawString(im, s, x, y) dc.drawString(im, s, x, y)
@ -639,8 +769,11 @@ func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
// spacing and text alignment. // spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) { func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
lines := dc.WordWrap(s, width) lines := dc.WordWrap(s, width)
// sync h formula with MeasureMultilineString
h := float64(len(lines)) * dc.fontHeight * lineSpacing h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight h -= (lineSpacing - 1) * dc.fontHeight
x -= ax * width x -= ax * width
y -= ay * h y -= ay * h
switch align { switch align {
@ -660,6 +793,29 @@ func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing
} }
} }
func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
lines := strings.Split(s, "\n")
// sync h formula with DrawStringWrapped
height = float64(len(lines)) * dc.fontHeight * lineSpacing
height -= (lineSpacing - 1) * dc.fontHeight
d := &font.Drawer{
Face: dc.fontFace,
}
// max width from lines
for _, line := range lines {
adv := d.MeasureString(line)
currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
if currentWidth > width {
width = currentWidth
}
}
return width, height
}
// MeasureString returns the rendered width and height of the specified text // MeasureString returns the rendered width and height of the specified text
// given the current font face. // given the current font face.
func (dc *Context) MeasureString(s string) (w, h float64) { func (dc *Context) MeasureString(s string) (w, h float64) {
@ -703,13 +859,13 @@ func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
dc.Translate(-x, -y) dc.Translate(-x, -y)
} }
// Rotate updates the current matrix with a clockwise rotation. // Rotate updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians. // Rotation occurs about the origin. Angle is specified in radians.
func (dc *Context) Rotate(angle float64) { func (dc *Context) Rotate(angle float64) {
dc.matrix = dc.matrix.Rotate(angle) dc.matrix = dc.matrix.Rotate(angle)
} }
// RotateAbout updates the current matrix with a clockwise rotation. // RotateAbout updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians. // Rotation occurs about the specified point. Angle is specified in radians.
func (dc *Context) RotateAbout(angle, x, y float64) { func (dc *Context) RotateAbout(angle, x, y float64) {
dc.Translate(x, y) dc.Translate(x, y)

323
context_test.go Normal file
View File

@ -0,0 +1,323 @@
package gg
import (
"crypto/md5"
"flag"
"fmt"
"image/color"
"math/rand"
"testing"
)
var save bool
func init() {
flag.BoolVar(&save, "save", false, "save PNG output for each test case")
flag.Parse()
}
func hash(dc *Context) string {
return fmt.Sprintf("%x", md5.Sum(dc.im.Pix))
}
func checkHash(t *testing.T, dc *Context, expected string) {
actual := hash(dc)
if actual != expected {
t.Fatalf("expected hash: %s != actual hash: %s", expected, actual)
}
}
func saveImage(dc *Context, name string) error {
if save {
return SavePNG(name+".png", dc.Image())
}
return nil
}
func TestBlank(t *testing.T) {
dc := NewContext(100, 100)
saveImage(dc, "TestBlank")
checkHash(t, dc, "4e0a293a5b638f0aba2c4fe2c3418d0e")
}
func TestGrid(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
for i := 10; i < 100; i += 10 {
x := float64(i) + 0.5
dc.DrawLine(x, 0, x, 100)
dc.DrawLine(0, x, 100, x)
}
dc.SetRGB(0, 0, 0)
dc.Stroke()
saveImage(dc, "TestGrid")
checkHash(t, dc, "78606adda71d8abfbd8bb271087e4d69")
}
func TestLines(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.5, 0.5, 0.5)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
dc.DrawLine(x1, y1, x2, y2)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestLines")
checkHash(t, dc, "036bd220e2529955cc48425dd72bb686")
}
func TestCircles(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 10; i++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
r := rnd.Float64()*10 + 5
dc.DrawCircle(x, y, r)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.FillPreserve()
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.SetLineWidth(rnd.Float64() * 3)
dc.Stroke()
}
saveImage(dc, "TestCircles")
checkHash(t, dc, "c52698000df96fabafe7863701afe922")
}
func TestQuadratic(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.25, 0.25, 0.25)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
x3 := rnd.Float64() * 100
y3 := rnd.Float64() * 100
dc.MoveTo(x1, y1)
dc.QuadraticTo(x2, y2, x3, y3)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestQuadratic")
checkHash(t, dc, "56b842d814aee94b52495addae764a77")
}
func TestCubic(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.75, 0.75, 0.75)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
x3 := rnd.Float64() * 100
y3 := rnd.Float64() * 100
x4 := rnd.Float64() * 100
y4 := rnd.Float64() * 100
dc.MoveTo(x1, y1)
dc.CubicTo(x2, y2, x3, y3, x4, y4)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestCubic")
checkHash(t, dc, "4a7960fc4eaaa33ce74131c5ce0afca8")
}
func TestFill(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 10; i++ {
dc.NewSubPath()
for j := 0; j < 10; j++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
dc.LineTo(x, y)
}
dc.ClosePath()
dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Fill()
}
saveImage(dc, "TestFill")
checkHash(t, dc, "7ccb3a2443906a825e57ab94db785467")
}
func TestClip(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.DrawCircle(50, 50, 40)
dc.Clip()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 1000; i++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
r := rnd.Float64()*10 + 5
dc.DrawCircle(x, y, r)
dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Fill()
}
saveImage(dc, "TestClip")
checkHash(t, dc, "762c32374d529fd45ffa038b05be7865")
}
func TestPushPop(t *testing.T) {
const S = 100
dc := NewContext(S, S)
dc.SetRGBA(0, 0, 0, 0.1)
for i := 0; i < 360; i += 15 {
dc.Push()
dc.RotateAbout(Radians(float64(i)), S/2, S/2)
dc.DrawEllipse(S/2, S/2, S*7/16, S/8)
dc.Fill()
dc.Pop()
}
saveImage(dc, "TestPushPop")
checkHash(t, dc, "31e908ee1c2ea180da98fd5681a89d05")
}
func TestDrawStringWrapped(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.DrawStringWrapped("Hello, world! How are you?", 50, 50, 0.5, 0.5, 90, 1.5, AlignCenter)
saveImage(dc, "TestDrawStringWrapped")
checkHash(t, dc, "8d92f6aae9e8b38563f171abd00893f8")
}
func TestDrawImage(t *testing.T) {
src := NewContext(100, 100)
src.SetRGB(1, 1, 1)
src.Clear()
for i := 10; i < 100; i += 10 {
x := float64(i) + 0.5
src.DrawLine(x, 0, x, 100)
src.DrawLine(0, x, 100, x)
}
src.SetRGB(0, 0, 0)
src.Stroke()
dc := NewContext(200, 200)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.DrawImage(src.Image(), 50, 50)
saveImage(dc, "TestDrawImage")
checkHash(t, dc, "282afbc134676722960b6bec21305b15")
}
func TestSetPixel(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(0, 1, 0)
i := 0
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
if i%31 == 0 {
dc.SetPixel(x, y)
}
i++
}
}
saveImage(dc, "TestSetPixel")
checkHash(t, dc, "27dda6b4b1d94f061018825b11982793")
}
func TestDrawPoint(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(0, 1, 0)
dc.Scale(10, 10)
for y := 0; y <= 10; y++ {
for x := 0; x <= 10; x++ {
dc.DrawPoint(float64(x), float64(y), 3)
dc.Fill()
}
}
saveImage(dc, "TestDrawPoint")
checkHash(t, dc, "55af8874531947ea6eeb62222fb33e0e")
}
func TestLinearGradient(t *testing.T) {
dc := NewContext(100, 100)
g := NewLinearGradient(0, 0, 100, 100)
g.AddColorStop(0, color.RGBA{0, 255, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255})
dc.SetFillStyle(g)
dc.DrawRectangle(0, 0, 100, 100)
dc.Fill()
saveImage(dc, "TestLinearGradient")
checkHash(t, dc, "75eb9385c1219b1d5bb6f4c961802c7a")
}
func TestRadialGradient(t *testing.T) {
dc := NewContext(100, 100)
g := NewRadialGradient(30, 50, 0, 70, 50, 50)
g.AddColorStop(0, color.RGBA{0, 255, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255})
dc.SetFillStyle(g)
dc.DrawRectangle(0, 0, 100, 100)
dc.Fill()
saveImage(dc, "TestRadialGradient")
checkHash(t, dc, "f170f39c3f35c29de11e00428532489d")
}
func TestDashes(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
dc.SetDash(rnd.Float64()*3+1, rnd.Float64()*3+3)
dc.DrawLine(x1, y1, x2, y2)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestDashes")
checkHash(t, dc, "d188069c69dcc3970edfac80f552b53c")
}
func BenchmarkCircles(b *testing.B) {
dc := NewContext(1000, 1000)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < b.N; i++ {
x := rnd.Float64() * 1000
y := rnd.Float64() * 1000
dc.DrawCircle(x, y, 10)
if i%2 == 0 {
dc.SetRGB(0, 0, 0)
} else {
dc.SetRGB(1, 1, 1)
}
dc.Fill()
}
}

BIN
examples/baboon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

30
examples/concat.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"math"
"github.com/fogleman/gg"
)
func main() {
im1, err := gg.LoadPNG("examples/baboon.png")
if err != nil {
panic(err)
}
im2, err := gg.LoadPNG("examples/gopher.png")
if err != nil {
panic(err)
}
s1 := im1.Bounds().Size()
s2 := im2.Bounds().Size()
width := int(math.Max(float64(s1.X), float64(s2.X)))
height := s1.Y + s2.Y
dc := gg.NewContext(width, height)
dc.DrawImage(im1, 0, 0)
dc.DrawImage(im2, 0, s1.Y)
dc.SavePNG("out.png")
}

44
examples/crisp.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"github.com/fogleman/gg"
)
func main() {
const W = 1000
const H = 1000
const Minor = 10
const Major = 100
dc := gg.NewContext(W, H)
dc.SetRGB(1, 1, 1)
dc.Clear()
// minor grid
for x := Minor; x < W; x += Minor {
fx := float64(x) + 0.5
dc.DrawLine(fx, 0, fx, H)
}
for y := Minor; y < H; y += Minor {
fy := float64(y) + 0.5
dc.DrawLine(0, fy, W, fy)
}
dc.SetLineWidth(1)
dc.SetRGBA(0, 0, 0, 0.25)
dc.Stroke()
// major grid
for x := Major; x < W; x += Major {
fx := float64(x) + 0.5
dc.DrawLine(fx, 0, fx, H)
}
for y := Major; y < H; y += Major {
fy := float64(y) + 0.5
dc.DrawLine(0, fy, W, fy)
}
dc.SetLineWidth(1)
dc.SetRGBA(0, 0, 0, 0.5)
dc.Stroke()
dc.SavePNG("out.png")
}

26
examples/gofont.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"log"
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
func main() {
font, err := truetype.Parse(goregular.TTF)
if err != nil {
log.Fatal(err)
}
face := truetype.NewFace(font, &truetype.Options{Size: 48})
dc := gg.NewContext(1024, 1024)
dc.SetFontFace(face)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.DrawStringAnchored("Hello, world!", 512, 512, 0.5, 0.5)
dc.SavePNG("out.png")
}

View File

@ -0,0 +1,38 @@
// +build ignore
package main
import (
"image/color"
"github.com/fogleman/gg"
)
func main() {
dc := gg.NewContext(400, 400)
grad1 := gg.NewConicGradient(200, 200, 0)
grad1.AddColorStop(0.0, color.Black)
grad1.AddColorStop(0.5, color.RGBA{255, 215, 0, 255})
grad1.AddColorStop(1.0, color.RGBA{255, 0, 0, 255})
grad2 := gg.NewConicGradient(200, 200, 90)
grad2.AddColorStop(0.00, color.RGBA{255, 0, 0, 255})
grad2.AddColorStop(0.16, color.RGBA{255, 255, 0, 255})
grad2.AddColorStop(0.33, color.RGBA{0, 255, 0, 255})
grad2.AddColorStop(0.50, color.RGBA{0, 255, 255, 255})
grad2.AddColorStop(0.66, color.RGBA{0, 0, 255, 255})
grad2.AddColorStop(0.83, color.RGBA{255, 0, 255, 255})
grad2.AddColorStop(1.00, color.RGBA{255, 0, 0, 255})
dc.SetStrokeStyle(grad1)
dc.SetLineWidth(20)
dc.DrawCircle(200, 200, 180)
dc.Stroke()
dc.SetFillStyle(grad2)
dc.DrawCircle(200, 200, 150)
dc.Fill()
dc.SavePNG("gradient-conic.png")
}

41
examples/gradient-text.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"image/color"
"github.com/fogleman/gg"
)
const (
W = 1024
H = 512
)
func main() {
dc := gg.NewContext(W, H)
// draw text
dc.SetRGB(0, 0, 0)
dc.LoadFontFace("/Library/Fonts/Impact.ttf", 128)
dc.DrawStringAnchored("Gradient Text", W/2, H/2, 0.5, 0.5)
// get the context as an alpha mask
mask := dc.AsMask()
// clear the context
dc.SetRGB(1, 1, 1)
dc.Clear()
// set a gradient
g := gg.NewLinearGradient(0, 0, W, H)
g.AddColorStop(0, color.RGBA{255, 0, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
dc.SetFillStyle(g)
// using the mask, fill the context with the gradient
dc.SetMask(mask)
dc.DrawRectangle(0, 0, W, H)
dc.Fill()
dc.SavePNG("out.png")
}

14
examples/invert-mask.go Normal file
View File

@ -0,0 +1,14 @@
package main
import "github.com/fogleman/gg"
func main() {
dc := gg.NewContext(1024, 1024)
dc.DrawCircle(512, 512, 384)
dc.Clip()
dc.InvertMask()
dc.DrawRectangle(0, 0, 1024, 1024)
dc.SetRGB(0, 0, 0)
dc.Fill()
dc.SavePNG("out.png")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

View File

@ -7,7 +7,7 @@ import (
) )
func main() { func main() {
im, err := gg.LoadImage("examples/lenna.png") im, err := gg.LoadImage("examples/baboon.png")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -3,7 +3,7 @@ package main
import "github.com/fogleman/gg" import "github.com/fogleman/gg"
func main() { func main() {
im, err := gg.LoadPNG("examples/lenna.png") im, err := gg.LoadPNG("examples/baboon.png")
if err != nil { if err != nil {
panic(err) panic(err)
} }

34
examples/rotated-image.go Normal file
View File

@ -0,0 +1,34 @@
package main
import "github.com/fogleman/gg"
func main() {
const W = 400
const H = 500
im, err := gg.LoadPNG("examples/gopher.png")
if err != nil {
panic(err)
}
iw, ih := im.Bounds().Dx(), im.Bounds().Dy()
dc := gg.NewContext(W, H)
// draw outline
dc.SetHexColor("#ff0000")
dc.SetLineWidth(1)
dc.DrawRectangle(0, 0, float64(W), float64(H))
dc.Stroke()
// draw full image
dc.SetHexColor("#0000ff")
dc.SetLineWidth(2)
dc.DrawRectangle(100, 210, float64(iw), float64(ih))
dc.Stroke()
dc.DrawImage(im, 100, 210)
// draw image with current matrix applied
dc.SetHexColor("#0000ff")
dc.SetLineWidth(2)
dc.Rotate(gg.Radians(10))
dc.DrawRectangle(100, 0, float64(iw), float64(ih)/2+20.0)
dc.StrokePreserve()
dc.Clip()
dc.DrawImageAnchored(im, 100, 0, 0.0, 0.0)
dc.SavePNG("out.png")
}

30
examples/rotated-text.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
func main() {
const S = 400
dc := gg.NewContext(S, S)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
font, err := truetype.Parse(goregular.TTF)
if err != nil {
panic("")
}
face := truetype.NewFace(font, &truetype.Options{
Size: 40,
})
dc.SetFontFace(face)
text := "Hello, world!"
w, h := dc.MeasureString(text)
dc.Rotate(gg.Radians(10))
dc.DrawRectangle(100, 180, w, h)
dc.Stroke()
dc.DrawStringAnchored(text, 100, 180, 0.0, 0.0)
dc.SavePNG("out.png")
}

25
examples/unicode.go Normal file
View File

@ -0,0 +1,25 @@
package main
import "github.com/fogleman/gg"
func main() {
const S = 4096 * 2
const T = 16 * 2
const F = 28
dc := gg.NewContext(S, S)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
if err := dc.LoadFontFace("Xolonium-Regular.ttf", F); err != nil {
panic(err)
}
for r := 0; r < 256; r++ {
for c := 0; c < 256; c++ {
i := r*256 + c
x := float64(c*T) + T/2
y := float64(r*T) + T/2
dc.DrawStringAnchored(string(rune(i)), x, y, 0.5, 0.5)
}
}
dc.SavePNG("out.png")
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module git.milar.in/milarin/gg
go 1.18
require (
github.com/fogleman/gg v1.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -164,6 +164,52 @@ func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
return g return g
} }
// Conic Gradient
type conicGradient struct {
cx, cy float64
rotation float64
stops stops
}
func (g *conicGradient) ColorAt(x, y int) color.Color {
if len(g.stops) == 0 {
return color.Transparent
}
a := math.Atan2(float64(y)-g.cy, float64(x)-g.cx)
t := norm(a, -math.Pi, math.Pi) - g.rotation
if t < 0 {
t += 1
}
return getColor(t, g.stops)
}
func (g *conicGradient) AddColorStop(offset float64, color color.Color) {
g.stops = append(g.stops, stop{pos: offset, color: color})
sort.Sort(g.stops)
}
func NewConicGradient(cx, cy, deg float64) Gradient {
g := &conicGradient{
cx: cx,
cy: cy,
rotation: normalizeAngle(deg) / 360,
}
return g
}
func normalizeAngle(t float64) float64 {
t = math.Mod(t, 360)
if t < 0 {
t += 360
}
return t
}
// Map value which is in range [a..b] to range [0..1]
func norm(value, a, b float64) float64 {
return (value - a) * (1.0 / (b - a))
}
func getColor(pos float64, stops stops) color.Color { func getColor(pos float64, stops stops) color.Color {
if pos <= 0.0 || len(stops) == 1 { if pos <= 0.0 || len(stops) == 1 {
return stops[0].color return stops[0].color
@ -189,7 +235,7 @@ func colorLerp(c0, c1 color.Color, t float64) color.Color {
r0, g0, b0, a0 := c0.RGBA() r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA() r1, g1, b1, a1 := c1.RGBA()
return color.NRGBA{ return color.RGBA{
lerp(r0, r1, t), lerp(r0, r1, t),
lerp(g0, g1, t), lerp(g0, g1, t),
lerp(b0, b1, t), lerp(b0, b1, t),

29
path.go
View File

@ -1,6 +1,8 @@
package gg package gg
import ( import (
"math"
"github.com/golang/freetype/raster" "github.com/golang/freetype/raster"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
@ -57,7 +59,7 @@ func flattenPath(p raster.Path) [][]Point {
return result return result
} }
func dashPath(paths [][]Point, dashes []float64) [][]Point { func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point {
var result [][]Point var result [][]Point
if len(dashes) == 0 { if len(dashes) == 0 {
return paths return paths
@ -73,6 +75,27 @@ func dashPath(paths [][]Point, dashes []float64) [][]Point {
pathIndex := 1 pathIndex := 1
dashIndex := 0 dashIndex := 0
segmentLength := 0.0 segmentLength := 0.0
// offset
if offset != 0 {
var totalLength float64
for _, dashLength := range dashes {
totalLength += dashLength
}
offset = math.Mod(offset, totalLength)
if offset < 0 {
offset += totalLength
}
for i, dashLength := range dashes {
offset -= dashLength
if offset < 0 {
dashIndex = i
segmentLength = dashLength + offset
break
}
}
}
var segment []Point var segment []Point
segment = append(segment, previous) segment = append(segment, previous)
for pathIndex < len(path) { for pathIndex < len(path) {
@ -135,6 +158,6 @@ func rasterPath(paths [][]Point) raster.Path {
return result return result
} }
func dashed(path raster.Path, dashes []float64) raster.Path { func dashed(path raster.Path, dashes []float64, offset float64) raster.Path {
return rasterPath(dashPath(flattenPath(path), dashes)) return rasterPath(dashPath(flattenPath(path), dashes, offset))
} }

35
util.go
View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"image" "image"
"image/draw" "image/draw"
"image/jpeg"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"io/ioutil" "io/ioutil"
@ -53,9 +54,32 @@ func SavePNG(path string, im image.Image) error {
return png.Encode(file, im) return png.Encode(file, im)
} }
func LoadJPG(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return jpeg.Decode(file)
}
func SaveJPG(path string, im image.Image, quality int) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
var opt jpeg.Options
opt.Quality = quality
return jpeg.Encode(file, im, &opt)
}
func imageToRGBA(src image.Image) *image.RGBA { func imageToRGBA(src image.Image) *image.RGBA {
dst := image.NewRGBA(src.Bounds()) bounds := src.Bounds()
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src) dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
return dst return dst
} }
@ -85,7 +109,7 @@ func fixp(x, y float64) fixed.Point26_6 {
} }
func fix(x float64) fixed.Int26_6 { func fix(x float64) fixed.Int26_6 {
return fixed.Int26_6(x * 64) return fixed.Int26_6(math.Round(x * 64))
} }
func unfix(x fixed.Int26_6) float64 { func unfix(x fixed.Int26_6) float64 {
@ -100,6 +124,11 @@ func unfix(x fixed.Int26_6) float64 {
return 0 return 0
} }
// LoadFontFace is a helper function to load the specified font file with
// the specified point size. Note that the returned `font.Face` objects
// are not thread safe and cannot be used in parallel across goroutines.
// You can usually just use the Context.LoadFontFace function instead of
// this package-level function.
func LoadFontFace(path string, points float64) (font.Face, error) { func LoadFontFace(path string, points float64) (font.Face, error) {
fontBytes, err := ioutil.ReadFile(path) fontBytes, err := ioutil.ReadFile(path)
if err != nil { if err != nil {