Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
d9a531518d | |||
![]() |
cc30f39e11 | ||
![]() |
69923d9438 | ||
![]() |
2755d666d6 | ||
![]() |
8febc0f526 | ||
![]() |
af4cd58078 | ||
![]() |
c26555283c | ||
![]() |
ad4d1eafac | ||
![]() |
8d127f461d | ||
![]() |
b89ba07b94 | ||
![]() |
4dc34561c6 | ||
![]() |
068da56c91 | ||
![]() |
f194ddec6f | ||
![]() |
6897f9a1a0 | ||
![]() |
72436a171b | ||
![]() |
5899172fb0 | ||
![]() |
1236b6346f | ||
![]() |
0403632d5b | ||
![]() |
64338842c5 | ||
![]() |
3bcf9e0320 | ||
![]() |
fa28a6e1e3 | ||
![]() |
f23d82b106 | ||
![]() |
0e8122236d | ||
![]() |
ccde3a8923 | ||
![]() |
3795562800 | ||
![]() |
74a8429cb8 | ||
![]() |
9db508d34a | ||
![]() |
77d18b88fe | ||
![]() |
cdabe43353 | ||
![]() |
16a00d1152 | ||
![]() |
0e0ff3ade7 | ||
![]() |
7cc16ce8b2 | ||
![]() |
eb261f0bd1 | ||
![]() |
a4f287e211 | ||
![]() |
9a34078211 | ||
![]() |
6bee5281ff | ||
![]() |
b2d255c6f2 | ||
![]() |
a9ff18eccd | ||
![]() |
ca366ba15b | ||
![]() |
c97f757e6f | ||
![]() |
da3d0863b9 | ||
![]() |
363d282ef3 | ||
![]() |
1b3894b028 | ||
![]() |
c828b09e4a | ||
![]() |
00ed7b79c0 | ||
![]() |
ffbeb6b231 | ||
![]() |
e611489b86 | ||
![]() |
ee8994ff90 | ||
![]() |
30f3b1d4ae | ||
![]() |
7fb6ce3c57 | ||
![]() |
0a834b0873 | ||
![]() |
5e707d618f | ||
![]() |
2c35caba58 | ||
![]() |
5e5aa69079 | ||
![]() |
b13517ff6f | ||
![]() |
3be68eb22e | ||
![]() |
f06d3564a1 |
27
README.md
27
README.md
@ -6,11 +6,16 @@
|
||||
|
||||
## 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!
|
||||
|
||||
@ -91,6 +96,7 @@ DrawString(s string, x, y float64)
|
||||
DrawStringAnchored(s string, x, y, ax, ay float64)
|
||||
DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
|
||||
MeasureString(s string) (w, h float64)
|
||||
MeasureMultilineString(s string, lineSpacing float64) (w, h float64)
|
||||
WordWrap(s string, w float64) []string
|
||||
SetFontFace(fontFace font.Face)
|
||||
LoadFontFace(path string, points float64) error
|
||||
@ -116,12 +122,13 @@ SetLineWidth(lineWidth float64)
|
||||
SetLineCap(lineCap LineCap)
|
||||
SetLineJoin(lineJoin LineJoin)
|
||||
SetDash(dashes ...float64)
|
||||
SetDashOffset(offset float64)
|
||||
SetFillRule(fillRule FillRule)
|
||||
```
|
||||
|
||||
## 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
|
||||
SetFillStyle(pattern Pattern)
|
||||
@ -129,6 +136,7 @@ SetStrokeStyle(pattern Pattern)
|
||||
NewSolidPattern(color color.Color)
|
||||
NewLinearGradient(x0, y0, x1, y1 float64)
|
||||
NewRadialGradient(x0, y0, r0, x1, y1, r1 float64)
|
||||
NewConicGradient(cx, cy, deg float64)
|
||||
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.
|
||||
|
||||
Note: transforms do not currently affect `DrawImage` or `DrawString`.
|
||||
|
||||
## Stack Functions
|
||||
|
||||
Save and restore the state of the context. These can be nested.
|
||||
@ -171,6 +177,9 @@ defined using paths.
|
||||
Clip()
|
||||
ClipPreserve()
|
||||
ResetClip()
|
||||
AsMask() *image.Alpha
|
||||
SetMask(mask *image.Alpha)
|
||||
InvertMask()
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
@ -187,12 +196,6 @@ SavePNG(path string, im image.Image) error
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
See the output of this example below.
|
||||
|
200
context.go
200
context.go
@ -2,16 +2,20 @@
|
||||
package gg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype/raster"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/f64"
|
||||
)
|
||||
|
||||
type LineCap int
|
||||
@ -52,6 +56,7 @@ var (
|
||||
type Context struct {
|
||||
width int
|
||||
height int
|
||||
rasterizer *raster.Rasterizer
|
||||
im *image.RGBA
|
||||
mask *image.Alpha
|
||||
color color.Color
|
||||
@ -63,6 +68,7 @@ type Context struct {
|
||||
current Point
|
||||
hasCurrent bool
|
||||
dashes []float64
|
||||
dashOffset float64
|
||||
lineWidth float64
|
||||
lineCap LineCap
|
||||
lineJoin LineJoin
|
||||
@ -88,9 +94,12 @@ func NewContextForImage(im image.Image) *Context {
|
||||
// NewContextForRGBA prepares a context for rendering onto the specified image.
|
||||
// No copy is made.
|
||||
func NewContextForRGBA(im *image.RGBA) *Context {
|
||||
w := im.Bounds().Size().X
|
||||
h := im.Bounds().Size().Y
|
||||
return &Context{
|
||||
width: im.Bounds().Size().X,
|
||||
height: im.Bounds().Size().Y,
|
||||
width: w,
|
||||
height: h,
|
||||
rasterizer: raster.NewRasterizer(w, h),
|
||||
im: im,
|
||||
color: color.Transparent,
|
||||
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.
|
||||
func (dc *Context) Image() image.Image {
|
||||
return dc.im
|
||||
@ -123,11 +141,23 @@ func (dc *Context) SavePNG(path string) error {
|
||||
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.
|
||||
func (dc *Context) EncodePNG(w io.Writer) error {
|
||||
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
|
||||
// disable dashes. The values specify the lengths of each dash, with
|
||||
// alternating on and off lengths.
|
||||
@ -135,6 +165,12 @@ func (dc *Context) SetDash(dashes ...float64) {
|
||||
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) {
|
||||
dc.lineWidth = lineWidth
|
||||
}
|
||||
@ -373,14 +409,16 @@ func (dc *Context) joiner() raster.Joiner {
|
||||
func (dc *Context) stroke(painter raster.Painter) {
|
||||
path := dc.strokePath
|
||||
if len(dc.dashes) > 0 {
|
||||
path = dashed(path, dc.dashes)
|
||||
path = dashed(path, dc.dashes, dc.dashOffset)
|
||||
} else {
|
||||
// TODO: this is a temporary workaround to remove tiny segments
|
||||
// that result in rendering issues
|
||||
path = rasterPath(flattenPath(path))
|
||||
}
|
||||
r := raster.NewRasterizer(dc.width, dc.height)
|
||||
r := dc.rasterizer
|
||||
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.Rasterize(painter)
|
||||
}
|
||||
@ -392,8 +430,10 @@ func (dc *Context) fill(painter raster.Painter) {
|
||||
copy(path, dc.fillPath)
|
||||
path.Add1(dc.start.Fixed())
|
||||
}
|
||||
r := raster.NewRasterizer(dc.width, dc.height)
|
||||
r := dc.rasterizer
|
||||
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.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
|
||||
// operation.
|
||||
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)
|
||||
}
|
||||
|
||||
@ -417,7 +469,19 @@ func (dc *Context) Stroke() {
|
||||
// FillPreserve fills the current path with the current color. Open subpaths
|
||||
// are implicity closed. The path is preserved after this operation.
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
// clipping region with the current path as it would be filled by dc.Fill().
|
||||
// The path is cleared after this operation.
|
||||
@ -520,15 +616,19 @@ func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
|
||||
a2 := angle1 + (angle2-angle1)*p2
|
||||
x0 := x + rx*math.Cos(a1)
|
||||
y0 := y + ry*math.Sin(a1)
|
||||
x1 := x + rx*math.Cos(a1+(a2-a1)/2)
|
||||
y1 := y + ry*math.Sin(a1+(a2-a1)/2)
|
||||
x1 := x + rx*math.Cos((a1+a2)/2)
|
||||
y1 := y + ry*math.Sin((a1+a2)/2)
|
||||
x2 := x + rx*math.Cos(a2)
|
||||
y2 := y + ry*math.Sin(a2)
|
||||
cx := 2*x1 - x0/2 - x2/2
|
||||
cy := 2*y1 - y0/2 - y2/2
|
||||
if i == 0 && !dc.hasCurrent {
|
||||
if i == 0 {
|
||||
if dc.hasCurrent {
|
||||
dc.LineTo(x0, y0)
|
||||
} else {
|
||||
dc.MoveTo(x0, y0)
|
||||
}
|
||||
}
|
||||
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.
|
||||
// Currently, rotation and scaling transforms are not supported.
|
||||
func (dc *Context) DrawImage(im image.Image, x, y int) {
|
||||
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()
|
||||
x -= int(ax * float64(s.X))
|
||||
y -= int(ay * float64(s.Y))
|
||||
p := image.Pt(x, y)
|
||||
r := image.Rectangle{p, p.Add(s)}
|
||||
transformer := draw.BiLinear
|
||||
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 {
|
||||
draw.Draw(dc.im, r, im, image.ZP, draw.Over)
|
||||
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
|
||||
} 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
|
||||
}
|
||||
|
||||
func (dc *Context) FontHeight() float64 {
|
||||
return dc.fontHeight
|
||||
}
|
||||
|
||||
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
|
||||
d := &font.Drawer{
|
||||
Dst: im,
|
||||
@ -608,11 +716,34 @@ func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
|
||||
Face: dc.fontFace,
|
||||
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.
|
||||
// Currently, rotation and scaling transforms are not supported.
|
||||
func (dc *Context) DrawString(s string, x, y float64) {
|
||||
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.
|
||||
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
|
||||
w, h := dc.MeasureString(s)
|
||||
x, y = dc.TransformPoint(x, y)
|
||||
x -= ax * w
|
||||
y += ay * h
|
||||
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 {
|
||||
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
|
||||
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.
|
||||
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
|
||||
lines := dc.WordWrap(s, width)
|
||||
|
||||
// sync h formula with MeasureMultilineString
|
||||
h := float64(len(lines)) * dc.fontHeight * lineSpacing
|
||||
h -= (lineSpacing - 1) * dc.fontHeight
|
||||
|
||||
x -= ax * width
|
||||
y -= ay * h
|
||||
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
|
||||
// given the current font face.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (dc *Context) Rotate(angle float64) {
|
||||
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.
|
||||
func (dc *Context) RotateAbout(angle, x, y float64) {
|
||||
dc.Translate(x, y)
|
||||
|
323
context_test.go
Normal file
323
context_test.go
Normal 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
BIN
examples/baboon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 622 KiB |
30
examples/concat.go
Normal file
30
examples/concat.go
Normal 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
44
examples/crisp.go
Normal 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
26
examples/gofont.go
Normal 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")
|
||||
}
|
38
examples/gradient-conic.go
Normal file
38
examples/gradient-conic.go
Normal 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
41
examples/gradient-text.go
Normal 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
14
examples/invert-mask.go
Normal 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 |
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
im, err := gg.LoadImage("examples/lenna.png")
|
||||
im, err := gg.LoadImage("examples/baboon.png")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package main
|
||||
import "github.com/fogleman/gg"
|
||||
|
||||
func main() {
|
||||
im, err := gg.LoadPNG("examples/lenna.png")
|
||||
im, err := gg.LoadPNG("examples/baboon.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
34
examples/rotated-image.go
Normal file
34
examples/rotated-image.go
Normal 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
30
examples/rotated-text.go
Normal 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
25
examples/unicode.go
Normal 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
9
go.mod
Normal 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
8
go.sum
Normal 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=
|
48
gradient.go
48
gradient.go
@ -164,6 +164,52 @@ func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
|
||||
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 {
|
||||
if pos <= 0.0 || len(stops) == 1 {
|
||||
return stops[0].color
|
||||
@ -189,7 +235,7 @@ func colorLerp(c0, c1 color.Color, t float64) color.Color {
|
||||
r0, g0, b0, a0 := c0.RGBA()
|
||||
r1, g1, b1, a1 := c1.RGBA()
|
||||
|
||||
return color.NRGBA{
|
||||
return color.RGBA{
|
||||
lerp(r0, r1, t),
|
||||
lerp(g0, g1, t),
|
||||
lerp(b0, b1, t),
|
||||
|
29
path.go
29
path.go
@ -1,6 +1,8 @@
|
||||
package gg
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/golang/freetype/raster"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
@ -57,7 +59,7 @@ func flattenPath(p raster.Path) [][]Point {
|
||||
return result
|
||||
}
|
||||
|
||||
func dashPath(paths [][]Point, dashes []float64) [][]Point {
|
||||
func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point {
|
||||
var result [][]Point
|
||||
if len(dashes) == 0 {
|
||||
return paths
|
||||
@ -73,6 +75,27 @@ func dashPath(paths [][]Point, dashes []float64) [][]Point {
|
||||
pathIndex := 1
|
||||
dashIndex := 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
|
||||
segment = append(segment, previous)
|
||||
for pathIndex < len(path) {
|
||||
@ -135,6 +158,6 @@ func rasterPath(paths [][]Point) raster.Path {
|
||||
return result
|
||||
}
|
||||
|
||||
func dashed(path raster.Path, dashes []float64) raster.Path {
|
||||
return rasterPath(dashPath(flattenPath(path), dashes))
|
||||
func dashed(path raster.Path, dashes []float64, offset float64) raster.Path {
|
||||
return rasterPath(dashPath(flattenPath(path), dashes, offset))
|
||||
}
|
||||
|
35
util.go
35
util.go
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
@ -53,9 +54,32 @@ func SavePNG(path string, im image.Image) error {
|
||||
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 {
|
||||
dst := image.NewRGBA(src.Bounds())
|
||||
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
|
||||
bounds := src.Bounds()
|
||||
dst := image.NewRGBA(bounds)
|
||||
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
|
||||
return dst
|
||||
}
|
||||
|
||||
@ -85,7 +109,7 @@ func fixp(x, y float64) fixed.Point26_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 {
|
||||
@ -100,6 +124,11 @@ func unfix(x fixed.Int26_6) float64 {
|
||||
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) {
|
||||
fontBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user