Changed pkg.go.dev link

This commit is contained in:
Sasha Koshka 2023-05-03 20:12:46 -04:00
parent 33c787d70b
commit 54ea1c283f
17 changed files with 1 additions and 1100 deletions

View File

@ -20,5 +20,5 @@ it. It will be placed in `~/.local/lib/nasin/plugins`.
You can find out more about how to use Tomo and Nasin by visiting the examples You can find out more about how to use Tomo and Nasin by visiting the examples
directory, or pull up the documentation by running `godoc` within the directory, or pull up the documentation by running `godoc` within the
repository. You can also view it on the web on repository. You can also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although [pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/tomo/tomo) (although
it may be slightly out of date). it may be slightly out of date).

View File

@ -1,63 +0,0 @@
// Package artutil provides utility functions for working with graphical types
// defined in artist, canvas, and image.
package artutil
import "image"
import "image/color"
import "tomo/artist"
import "tomo/shatter"
// Fill fills the destination canvas with the given pattern.
func Fill (destination artist.Canvas, source artist.Pattern) (updated image.Rectangle) {
source.Draw(destination, destination.Bounds())
return destination.Bounds()
}
// DrawClip lets you draw several subsets of a pattern at once.
func DrawClip (
destination artist.Canvas,
source artist.Pattern,
bounds image.Rectangle,
subsets ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
for _, subset := range subsets {
source.Draw(artist.Cut(destination, subset), bounds)
updatedRegion = updatedRegion.Union(subset)
}
return
}
// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas
// specified by "rocks".
func DrawShatter (
destination artist.Canvas,
source artist.Pattern,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
return DrawClip(destination, source, bounds, tiles...)
}
// AllocateSample returns a new canvas containing the result of a pattern. The
// resulting canvas can be sourced from shape drawing functions. I beg of you
// please do not call this every time you need to draw a shape with a pattern on
// it because that is horrible and cruel to the computer.
func AllocateSample (source artist.Pattern, width, height int) artist.Canvas {
allocated := artist.NewBasicCanvas(width, height)
Fill(allocated, source)
return allocated
}
// Hex creates a color.RGBA value from an RGBA integer value.
func Hex (color uint32) (c color.RGBA) {
c.A = uint8(color)
c.B = uint8(color >> 8)
c.G = uint8(color >> 16)
c.R = uint8(color >> 24)
return
}

View File

@ -1,111 +0,0 @@
package artist
import "image"
import "image/draw"
import "image/color"
// Image represents an immutable canvas.
type Image interface {
image.Image
RGBAAt (x, y int) color.RGBA
}
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
// more efficient drawing. This interface can be easily satisfied using a
// BasicCanvas struct.
type Canvas interface {
draw.Image
Buffer () (data []color.RGBA, stride int)
}
// BasicCanvas is a general purpose implementation of tomo.Canvas.
type BasicCanvas struct {
pix []color.RGBA
stride int
rect image.Rectangle
}
// NewBasicCanvas creates a new basic canvas with the specified width and
// height, allocating a buffer for it.
func NewBasicCanvas (width, height int) (canvas BasicCanvas) {
canvas.pix = make([]color.RGBA, height * width)
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
return
}
// FromImage creates a new BasicCanvas from an image.Image.
func FromImage (img image.Image) (canvas BasicCanvas) {
bounds := img.Bounds()
canvas = NewBasicCanvas(bounds.Dx(), bounds.Dy())
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
canvasPoint := point.Sub(bounds.Min)
canvas.Set (
canvasPoint.X, canvasPoint.Y,
img.At(point.X, point.Y))
}}
return
}
// you know what it do
func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) {
return canvas.rect
}
// you know what it do
func (canvas BasicCanvas) At (x, y int) (color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return nil }
return canvas.pix[x + y * canvas.stride]
}
// you know what it do
func (canvas BasicCanvas) ColorModel () (model color.Model) {
return color.RGBAModel
}
// you know what it do
func (canvas BasicCanvas) Set (x, y int, c color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return }
r, g, b, a := c.RGBA()
canvas.pix[x + y * canvas.stride] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
}
// you know what it do
func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
return canvas.pix, canvas.stride
}
// Reallocate efficiently reallocates the canvas. The data within will be
// garbage. This method will do nothing if this is a cut image.
func (canvas *BasicCanvas) Reallocate (width, height int) {
if canvas.rect.Min != (image.Point { }) { return }
previousLen := len(canvas.pix)
newLen := width * height
bigger := newLen > previousLen
smaller := newLen < previousLen / 2
if bigger || smaller {
canvas.pix = make (
[]color.RGBA,
((height * width) / 4096) * 4096 + 4096)
}
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
}
// Cut returns a sub-canvas of a given canvas.
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
// println(canvas.Bounds().String(), bounds.String())
bounds = bounds.Intersect(canvas.Bounds())
if bounds.Empty() { return }
reduced.rect = bounds
reduced.pix, reduced.stride = canvas.Buffer()
return
}

View File

@ -1,2 +0,0 @@
// Package artist provides a simple 2D drawing library for canvas.Canvas.
package artist

View File

@ -1,13 +0,0 @@
package artist
import "image"
import "image/color"
type Icon interface {
// Draw draws the icon to the destination canvas at the specified point,
// using the specified color (if the icon is monochrome).
Draw (destination Canvas, color color.RGBA, at image.Point)
// Bounds returns the bounds of the icon.
Bounds () image.Rectangle
}

View File

@ -1,82 +0,0 @@
package artist
import "image"
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
// Inset represents an inset amount for all four sides of a rectangle. The top
// side is at index zero, the right at index one, the bottom at index two, and
// the left at index three. These values may be negative.
type Inset [4]int
// I allows you to create an inset in a CSS-ish way:
//
// - One argument: all sides are set to this value
// - Two arguments: the top and bottom sides are set to the first value, and
// the left and right sides are set to the second value.
// - Three arguments: the top side is set by the first value, the left and
// right sides are set by the second vaue, and the bottom side is set by the
// third value.
// - Four arguments: each value corresponds to a side.
//
// This function will panic if an argument count that isn't one of these is
// given.
func I (sides ...int) Inset {
switch len(sides) {
case 1: return Inset { sides[0], sides[0], sides[0], sides[0] }
case 2: return Inset { sides[0], sides[1], sides[0], sides[1] }
case 3: return Inset { sides[0], sides[1], sides[2], sides[1] }
case 4: return Inset { sides[0], sides[1], sides[2], sides[3] }
default: panic("I: illegal argument count.")
}
}
// Apply returns the given rectangle, shrunk on all four sides by the given
// inset. If a measurment of the inset is negative, that side will instead be
// expanded outward. If the rectangle's dimensions cannot be reduced any
// further, an empty rectangle near its center will be returned.
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
smaller = bigger
if smaller.Dx() < inset[3] + inset[1] {
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
smaller.Max.X = smaller.Min.X
} else {
smaller.Min.X += inset[3]
smaller.Max.X -= inset[1]
}
if smaller.Dy() < inset[0] + inset[2] {
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
smaller.Max.Y = smaller.Min.Y
} else {
smaller.Min.Y += inset[0]
smaller.Max.Y -= inset[2]
}
return
}
// Inverse returns a negated version of the inset.
func (inset Inset) Inverse () (prime Inset) {
return Inset {
inset[0] * -1,
inset[1] * -1,
inset[2] * -1,
inset[3] * -1,
}
}
// Horizontal returns the sum of SideRight and SideLeft.
func (inset Inset) Horizontal () int {
return inset[SideRight] + inset[SideLeft]
}
// Vertical returns the sum of SideTop and SideBottom.
func (inset Inset) Vertical () int {
return inset[SideTop] + inset[SideBottom]
}

View File

@ -1,13 +0,0 @@
package artist
import "image"
// Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle.
type Pattern interface {
// Draw draws the pattern onto the destination canvas, using the
// specified bounds. The given bounds can be smaller or larger than the
// bounds of the destination canvas. The destination canvas can be cut
// using canvas.Cut() to draw only a specific subset of a pattern.
Draw (destination Canvas, bounds image.Rectangle)
}

View File

@ -1,89 +0,0 @@
package patterns
import "image"
import "tomo/artist"
// Border is a pattern that behaves similarly to border-image in CSS. It divides
// a source canvas into nine sections...
//
// Inset[1]
// ┌──┴──┐
// ┌─┌─────┬─────┬─────┐
// Inset[0]─┤ │ 0 │ 1 │ 2 │
// └─├─────┼─────┼─────┤
// │ 3 │ 4 │ 5 │
// ├─────┼─────┼─────┤─┐
// │ 6 │ 7 │ 8 │ ├─Inset[2]
// └─────┴─────┴─────┘─┘
// └──┬──┘
// Inset[3]
//
// ... Where the bounds of section 4 are defined as the application of the
// pattern's inset to the canvas's bounds. The bounds of the other eight
// sections are automatically sized around it.
//
// When drawn to a destination canvas, the bounds of sections 1, 3, 4, 5, and 7
// are expanded or contracted to fit the given drawing bounds. All sections are
// rendered as if they are Texture patterns, meaning these flexible sections
// will repeat to fill in any empty space.
//
// This pattern can be used to make a static image texture into something that
// responds well to being resized.
type Border struct {
artist.Canvas
artist.Inset
}
// Draw draws the border pattern onto the destination canvas within the given
// bounds.
func (pattern Border) Draw (destination artist.Canvas, bounds image.Rectangle) {
drawBounds := bounds.Canon().Intersect(destination.Bounds())
if drawBounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { }
for index, section := range srcSections {
srcTextures[index].Canvas = artist.Cut(pattern, section)
}
dstSections := nonasect(bounds, pattern.Inset)
for index, section := range dstSections {
srcTextures[index].Draw(destination, section)
}
}
func nonasect (bounds image.Rectangle, inset artist.Inset) [9]image.Rectangle {
center := inset.Apply(bounds)
return [9]image.Rectangle {
// top
image.Rectangle {
bounds.Min,
center.Min },
image.Rect (
center.Min.X, bounds.Min.Y,
center.Max.X, center.Min.Y),
image.Rect (
center.Max.X, bounds.Min.Y,
bounds.Max.X, center.Min.Y),
// center
image.Rect (
bounds.Min.X, center.Min.Y,
center.Min.X, center.Max.Y),
center,
image.Rect (
center.Max.X, center.Min.Y,
bounds.Max.X, center.Max.Y),
// bottom
image.Rect (
bounds.Min.X, center.Max.Y,
center.Min.X, bounds.Max.Y),
image.Rect (
center.Min.X, center.Max.Y,
center.Max.X, bounds.Max.Y),
image.Rect (
center.Max.X, center.Max.Y,
bounds.Max.X, bounds.Max.Y),
}
}

View File

@ -1,3 +0,0 @@
// Package patterns provides a basic set of types that satisfy the
// artist.Pattern interface.
package patterns

View File

@ -1,77 +0,0 @@
package patterns
import "image"
import "tomo/artist"
// Texture is a pattern that tiles the content of a canvas both horizontally and
// vertically.
type Texture struct {
artist.Canvas
}
// Draw tiles the pattern's canvas within the given bounds. The minimum
// point of the pattern's canvas will be lined up with the minimum point of the
// bounding rectangle.
func (pattern Texture) Draw (destination artist.Canvas, bounds image.Rectangle) {
dstBounds := bounds.Canon().Intersect(destination.Bounds())
if dstBounds.Empty() { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := pattern.Buffer()
srcBounds := pattern.Bounds()
// offset is a vector that is added to points in destination space to
// convert them to points in source space
offset := srcBounds.Min.Sub(bounds.Min)
// calculate the starting position in source space
srcPoint := dstBounds.Min.Add(offset)
srcPoint.X = wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X)
srcPoint.Y = wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y)
srcStartPoint := srcPoint
// for each row
dstPoint := image.Point { }
for dstPoint.Y = dstBounds.Min.Y; dstPoint.Y < dstBounds.Max.Y; dstPoint.Y ++ {
srcPoint.X = srcStartPoint.X
dstPoint.X = dstBounds.Min.X
dstYComponent := dstPoint.Y * dstStride
srcYComponent := srcPoint.Y * srcStride
// for each pixel in the row
for {
// draw pixel
dstIndex := dstYComponent + dstPoint.X
srcIndex := srcYComponent + srcPoint.X
dstData[dstIndex] = srcData[srcIndex]
// increment X in source space. wrap to start if out of
// bounds.
srcPoint.X ++
if srcPoint.X >= srcBounds.Max.X {
srcPoint.X = srcBounds.Min.X
}
// increment X in destination space. stop drawing this
// row if out of bounds.
dstPoint.X ++
if dstPoint.X >= dstBounds.Max.X {
break
}
}
// increment row in source space. wrap to start if out of
// bounds.
srcPoint.Y ++
if srcPoint.Y >= srcBounds.Max.Y {
srcPoint.Y = srcBounds.Min.Y
}
}
}
func wrap (value, min, max int) int {
difference := max - min
value = (value - min) % difference
if value < 0 { value += difference }
return value + min
}

View File

@ -1,20 +0,0 @@
package patterns
import "image"
import "image/color"
import "tomo/artist"
import "tomo/artist/shapes"
import "tomo/artist/artutil"
// Uniform is a pattern that draws a solid color.
type Uniform color.RGBA
// Draw fills the bounding rectangle with the pattern's color.
func (pattern Uniform) Draw (destination artist.Canvas, bounds image.Rectangle) {
shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds)
}
// Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) {
return Uniform(artutil.Hex(color))
}

View File

@ -1,11 +0,0 @@
// Package shapes provides some basic shape drawing routines.
//
// A word about using patterns with shape routines:
//
// Most drawing routines have a version that samples from other canvases, and a
// version that samples from a solid color. None of these routines can use
// patterns directly, but it is entirely possible to have a pattern draw to an
// off-screen canvas and then draw a shape based on that canvas. As a little
// bonus, you can save the canvas for later so you don't have to render the
// pattern again when you need to redraw the shape.
package shapes

View File

@ -1,231 +0,0 @@
package shapes
import "math"
import "image"
import "image/color"
import "tomo/artist"
// TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in
// destination and source, using the bounds of destination as the bounds of the
// ellipse and the bounds of source as the "clipping rectangle". Line up the Min
// of both canvases.
func FillEllipse (
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
drawBounds :=
source.Bounds().Sub(offset).
Intersect(destination.Bounds()).
Intersect(bounds)
if bounds.Empty() { return }
updatedRegion = bounds
point := image.Point { }
for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
if inEllipse(point, bounds) {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}
}}
return
}
func StrokeEllipse (
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
weight int,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
drawBounds := destination.Bounds().Inset(weight - 1)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
if drawBounds.Empty() { return }
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
srcData: srcData,
srcStride: srcStride,
weight: weight,
offset: offset,
bounds: bounds,
},
radii: image.Pt(drawBounds.Dx() / 2, drawBounds.Dy() / 2),
}
context.center = drawBounds.Min.Add(context.radii)
context.plotEllipse()
}
type ellipsePlottingContext struct {
plottingContext
radii image.Point
center image.Point
}
func (context ellipsePlottingContext) plotEllipse () {
x := float64(0)
y := float64(context.radii.Y)
// region 1 decision parameter
decision1 :=
float64(context.radii.Y * context.radii.Y) -
float64(context.radii.X * context.radii.X * context.radii.Y) +
(0.25 * float64(context.radii.X) * float64(context.radii.X))
decisionX := float64(2 * context.radii.Y * context.radii.Y * int(x))
decisionY := float64(2 * context.radii.X * context.radii.X * int(y))
// draw region 1
for decisionX < decisionY {
points := []image.Point {
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if (decision1 < 0) {
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decision1 += decisionX + float64(context.radii.Y * context.radii.Y)
} else {
x ++
y --
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision1 +=
decisionX - decisionY +
float64(context.radii.Y * context.radii.Y)
}
}
// region 2 decision parameter
decision2 :=
float64(context.radii.Y * context.radii.Y) * (x + 0.5) * (x + 0.5) +
float64(context.radii.X * context.radii.X) * (y - 1) * (y - 1) -
float64(context.radii.X * context.radii.X * context.radii.Y * context.radii.Y)
// draw region 2
for y >= 0 {
points := []image.Point {
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if decision2 > 0 {
y --
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 += float64(context.radii.X * context.radii.X) - decisionY
} else {
y --
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 +=
decisionX - decisionY +
float64(context.radii.X * context.radii.X)
}
}
}
// FillColorEllipse fills an ellipse within the destination canvas with a solid
// color.
func FillColorEllipse (
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
realBounds := bounds
bounds = bounds.Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
if inEllipse(point, realBounds) {
dstData[point.X + point.Y * dstStride] = color
}
}}
return
}
// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset
// outline of an ellipse instead.
func StrokeColorEllipse (
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) (
updatedRegion image.Rectangle,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
insetBounds := bounds.Inset(weight - 1)
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
color: color,
weight: weight,
bounds: bounds.Intersect(destination.Bounds()),
},
radii: image.Pt(insetBounds.Dx() / 2, insetBounds.Dy() / 2),
}
context.center = insetBounds.Min.Add(context.radii)
context.plotEllipse()
return
}
func inEllipse (point image.Point, bounds image.Rectangle) bool {
point = point.Sub(bounds.Min)
x := (float64(point.X) + 0.5) / float64(bounds.Dx()) - 0.5
y := (float64(point.Y) + 0.5) / float64(bounds.Dy()) - 0.5
return math.Hypot(x, y) <= 0.5
}

View File

@ -1,110 +0,0 @@
package shapes
import "image"
import "image/color"
import "tomo/artist"
// ColorLine draws a line from one point to another with the specified weight
// and color.
func ColorLine (
destination artist.Canvas,
color color.RGBA,
weight int,
min image.Point,
max image.Point,
) (
updatedRegion image.Rectangle,
) {
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++
updatedRegion.Max.Y ++
data, stride := destination.Buffer()
bounds := destination.Bounds()
context := linePlottingContext {
plottingContext: plottingContext {
dstData: data,
dstStride: stride,
color: color,
weight: weight,
bounds: bounds,
},
min: min,
max: max,
}
if abs(max.Y - min.Y) < abs(max.X - min.X) {
if max.X < min.X { context.swap() }
context.lineLow()
} else {
if max.Y < min.Y { context.swap() }
context.lineHigh()
}
return
}
type linePlottingContext struct {
plottingContext
min image.Point
max image.Point
}
func (context *linePlottingContext) swap () {
temp := context.max
context.max = context.min
context.min = temp
}
func (context linePlottingContext) lineLow () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
point := context.min
for ; point.X < context.max.X; point.X ++ {
context.plotColor(point)
if D > 0 {
D += 2 * (deltaY - deltaX)
point.Y += yi
} else {
D += 2 * deltaY
}
}
}
func (context linePlottingContext) lineHigh () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
point := context.min
for ; point.Y < context.max.Y; point.Y ++ {
context.plotColor(point)
if D > 0 {
point.X += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (n int) int {
if n < 0 { n *= -1}
return n
}

View File

@ -1,47 +0,0 @@
package shapes
import "image"
import "image/color"
// FIXME? drawing a ton of overlapping squares might be a bit wasteful.
type plottingContext struct {
dstData []color.RGBA
dstStride int
srcData []color.RGBA
srcStride int
color color.RGBA
weight int
offset image.Point
bounds image.Rectangle
}
func (context plottingContext) square (center image.Point) (square image.Rectangle) {
return image.Rect(0, 0, context.weight, context.weight).
Sub(image.Pt(context.weight / 2, context.weight / 2)).
Add(center).
Intersect(context.bounds)
}
func (context plottingContext) plotColor (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
context.dstData[x + y * context.dstStride] = context.color
}}
}
func (context plottingContext) plotSource (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
// we offset srcIndex here because we have already applied the
// offset to the square, and we need to reverse that to get the
// proper source coordinates.
srcIndex :=
x + context.offset.X +
(y + context.offset.Y) * context.dstStride
dstIndex := x + y * context.dstStride
context.dstData[dstIndex] = context.srcData [srcIndex]
}}
}

View File

@ -1,130 +0,0 @@
package shapes
import "image"
import "image/color"
import "tomo/artist"
import "tomo/shatter"
// TODO: return updatedRegion for all routines in this package
func FillRectangle (
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
drawBounds :=
source.Bounds().Sub(offset).
Intersect(destination.Bounds()).
Intersect(bounds)
if drawBounds.Empty() { return }
updatedRegion = drawBounds
point := image.Point { }
for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}}
return
}
func StrokeRectangle (
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
weight int,
) (
updatedRegion image.Rectangle,
) {
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
return FillRectangle(destination, source, bounds)
}
return FillRectangleShatter(destination, source, bounds, insetBounds)
}
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
// specified in "rocks".
func FillRectangleShatter (
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
FillRectangle (
artist.Cut(destination, tile),
source, tile)
updatedRegion = updatedRegion.Union(tile)
}
return
}
// FillColorRectangle fills a rectangle within the destination canvas with a
// solid color.
func FillColorRectangle (
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
bounds = bounds.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
updatedRegion = bounds
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
dstData[x + y * dstStride] = color
}}
return
}
// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in
// areas specified in "rocks".
func FillColorRectangleShatter (
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
FillColorRectangle(destination, color, tile)
updatedRegion = updatedRegion.Union(tile)
}
return
}
// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset
// outline of the given rectangle instead.
func StrokeColorRectangle (
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) (
updatedRegion image.Rectangle,
) {
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
return FillColorRectangle(destination, color, bounds)
}
return FillColorRectangleShatter(destination, color, bounds, insetBounds)
}

View File

@ -1,97 +0,0 @@
// Package shatter provides boolean operations for image.Rectangle.
package shatter
import "image"
// Shatter takes in a bounding rectangle, and several rectangles to be
// subtracted from it. It returns a slice of rectangles that tile together to
// make up the difference between them. This is intended to be used for figuring
// out which areas of a container element's background are covered by other
// elements so it doesn't waste CPU cycles drawing to those areas.
func Shatter (
glass image.Rectangle,
rocks ...image.Rectangle,
) (
tiles []image.Rectangle,
) {
// in this function, the metaphor of throwing several rocks at a sheet
// of glass is used to illustrate the concept.
tiles = []image.Rectangle { glass }
for _, rock := range rocks {
// check each tile to see if the rock has collided with it
tileLen := len(tiles)
for tileIndex := 0; tileIndex < tileLen; tileIndex ++ {
tile := tiles[tileIndex]
if !rock.Overlaps(tile) { continue }
newTiles, n := shatterOnce(tile, rock)
if n > 0 {
// the tile was shattered into one or more sub
// tiles
tiles[tileIndex] = newTiles[0]
tiles = append(tiles, newTiles[1:n]...)
} else {
// the tile was entirely obscured by the rock
// and must be wholly removed
tiles = remove(tiles, tileIndex)
tileIndex --
tileLen --
}
}
}
return
}
func shatterOnce (glass, rock image.Rectangle) (tiles [4]image.Rectangle, n int) {
rock = rock.Intersect(glass)
// |'''''''''''|
// | |
// |###|'''| |
// |###|___| |
// | |
// |___________|
if rock.Min.X > glass.Min.X { tiles[n] = image.Rect (
glass.Min.X, rock.Min.Y,
rock.Min.X, rock.Max.Y,
); n ++ }
// |'''''''''''|
// | |
// | |'''|###|
// | |___|###|
// | |
// |___________|
if rock.Max.X < glass.Max.X { tiles[n] = image.Rect (
rock.Max.X, rock.Min.Y,
glass.Max.X, rock.Max.Y,
); n ++ }
// |###########|
// |###########|
// | |'''| |
// | |___| |
// | |
// |___________|
if rock.Min.Y > glass.Min.Y { tiles[n] = image.Rect (
glass.Min.X, glass.Min.Y,
glass.Max.X, rock.Min.Y,
); n ++ }
// |'''''''''''|
// | |
// | |'''| |
// | |___| |
// |###########|
// |###########|
if rock.Max.Y < glass.Max.Y { tiles[n] = image.Rect (
glass.Min.X, rock.Max.Y,
glass.Max.X, glass.Max.Y,
); n ++ }
return
}
func remove[ELEMENT any] (slice []ELEMENT, s int) []ELEMENT {
return append(slice[:s], slice[s + 1:]...)
}