Replaced tomo.Image with tomo.Canvas and tomo.Pattern
This is the first step in transitioning the API over to the new design. The new tomo.Canvas interface gives drawing functions direct access to data buffers and eliminates overhead associated with calling functions for every pixel. The entire artist package will be remade around this.
This commit is contained in:
@@ -1,2 +1,52 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Pattern is capable of generating a pattern pixel by pixel.
|
||||
type Pattern interface {
|
||||
// AtWhen returns the color of the pixel located at (x, y) relative to
|
||||
// the origin point of the pattern (0, 0), when the pattern has the
|
||||
// specified width and height. Patterns may ignore the width and height
|
||||
// parameters, but it may be useful for some patterns such as gradients.
|
||||
AtWhen (x, y, width, height int) (color.RGBA)
|
||||
}
|
||||
|
||||
// Texture is a struct that allows an image to be converted into a tiling
|
||||
// texture pattern.
|
||||
type Texture struct {
|
||||
data []color.RGBA
|
||||
width, height int
|
||||
}
|
||||
|
||||
// NewTexture converts an image into a texture.
|
||||
func NewTexture (source image.Image) (texture Texture) {
|
||||
bounds := source.Bounds()
|
||||
texture.width = bounds.Dx()
|
||||
texture.height = bounds.Dy()
|
||||
texture.data = make([]color.RGBA, texture.width * texture.height)
|
||||
|
||||
index := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
r, g, b, a := source.At(x, y).RGBA()
|
||||
texture.data[index] = color.RGBA {
|
||||
uint8(r >> 8),
|
||||
uint8(g >> 8),
|
||||
uint8(b >> 8),
|
||||
uint8(a >> 8),
|
||||
}
|
||||
index ++
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// AtWhen returns the color at the specified x and y coordinates, wrapped to the
|
||||
// image's width. the width and height are ignored.
|
||||
func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) {
|
||||
x %= texture.width
|
||||
y %= texture.height
|
||||
if x < 0 { x += texture.width }
|
||||
if y < 0 { y += texture.height }
|
||||
return texture.data[x + y * texture.width]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import "git.tebibyte.media/sashakoshka/tomo"
|
||||
// ShadingProfile contains shading information that can be used to draw chiseled
|
||||
// objects.
|
||||
type ShadingProfile struct {
|
||||
Highlight tomo.Image
|
||||
Shadow tomo.Image
|
||||
Stroke tomo.Image
|
||||
Fill tomo.Image
|
||||
Highlight Pattern
|
||||
Shadow Pattern
|
||||
Stroke Pattern
|
||||
Fill Pattern
|
||||
StrokeWeight int
|
||||
ShadingWeight int
|
||||
}
|
||||
@@ -43,6 +43,7 @@ func ChiseledRectangle (
|
||||
strokeWeight := profile.StrokeWeight
|
||||
shadingWeight := profile.ShadingWeight
|
||||
|
||||
data, stride := destination.Buffer()
|
||||
bounds = bounds.Canon()
|
||||
updatedRegion = bounds
|
||||
|
||||
@@ -59,11 +60,6 @@ func ChiseledRectangle (
|
||||
fillBounds.Max = fillBounds.Max.Sub(shadingWeightVector)
|
||||
fillBounds = fillBounds.Canon()
|
||||
|
||||
strokeImageMin := stroke.Bounds().Min
|
||||
highlightImageMin := highlight.Bounds().Min
|
||||
shadowImageMin := shadow.Bounds().Min
|
||||
fillImageMin := fill.Bounds().Min
|
||||
|
||||
width := float64(bounds.Dx())
|
||||
height := float64(bounds.Dy())
|
||||
|
||||
@@ -75,11 +71,10 @@ func ChiseledRectangle (
|
||||
point := image.Point { x, y }
|
||||
switch {
|
||||
case point.In(fillBounds):
|
||||
pixel = fill.RGBAAt (
|
||||
xx - strokeWeight - shadingWeight +
|
||||
fillImageMin.X,
|
||||
yy - strokeWeight - shadingWeight +
|
||||
fillImageMin.Y)
|
||||
pixel = fill.AtWhen (
|
||||
xx - strokeWeight - shadingWeight,
|
||||
yy - strokeWeight - shadingWeight,
|
||||
fillBounds.Dx(), fillBounds.Dy())
|
||||
|
||||
case point.In(shadingBounds):
|
||||
var highlighted bool
|
||||
@@ -97,27 +92,21 @@ func ChiseledRectangle (
|
||||
width - float64(xx) >
|
||||
float64(yy)
|
||||
}
|
||||
|
||||
|
||||
shadingSource := shadow
|
||||
if highlighted {
|
||||
pixel = highlight.RGBAAt (
|
||||
xx - strokeWeight +
|
||||
highlightImageMin.X,
|
||||
yy - strokeWeight +
|
||||
highlightImageMin.Y)
|
||||
} else {
|
||||
pixel = shadow.RGBAAt (
|
||||
xx - strokeWeight +
|
||||
shadowImageMin.X,
|
||||
yy - strokeWeight +
|
||||
shadowImageMin.Y)
|
||||
shadingSource = highlight
|
||||
}
|
||||
|
||||
pixel = shadingSource.AtWhen (
|
||||
xx - strokeWeight,
|
||||
yy - strokeWeight,
|
||||
shadingBounds.Dx(),
|
||||
shadingBounds.Dy())
|
||||
default:
|
||||
pixel = stroke.RGBAAt (
|
||||
xx + strokeImageMin.X,
|
||||
yy + strokeImageMin.Y)
|
||||
pixel = stroke.AtWhen (
|
||||
xx, yy, bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
destination.SetRGBA(x, y, pixel)
|
||||
data[x + y * stride] = pixel
|
||||
xx ++
|
||||
}
|
||||
yy ++
|
||||
|
||||
@@ -5,7 +5,7 @@ import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
func Line (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
@@ -17,6 +17,8 @@ func Line (
|
||||
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
|
||||
updatedRegion.Max.X ++
|
||||
updatedRegion.Max.Y ++
|
||||
width := updatedRegion.Dx()
|
||||
height := updatedRegion.Dy()
|
||||
|
||||
if abs(max.Y - min.Y) <
|
||||
abs(max.X - min.X) {
|
||||
@@ -26,7 +28,7 @@ func Line (
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineLow(destination, source, weight, min, max)
|
||||
lineLow(destination, source, weight, min, max, width, height)
|
||||
} else {
|
||||
|
||||
if max.Y < min.Y {
|
||||
@@ -34,18 +36,21 @@ func Line (
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineHigh(destination, source, weight, min, max)
|
||||
lineHigh(destination, source, weight, min, max, width, height)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lineLow (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
width, height int,
|
||||
) {
|
||||
data, stride := destination.Buffer()
|
||||
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
yi := 1
|
||||
@@ -59,7 +64,7 @@ func lineLow (
|
||||
y := min.Y
|
||||
|
||||
for x := min.X; x < max.X; x ++ {
|
||||
destination.SetRGBA(x, y, source.RGBAAt(x, y))
|
||||
data[x + y * stride] = source.AtWhen(x, y, width, height)
|
||||
if D > 0 {
|
||||
y += yi
|
||||
D += 2 * (deltaY - deltaX)
|
||||
@@ -71,11 +76,14 @@ func lineLow (
|
||||
|
||||
func lineHigh (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
width, height int,
|
||||
) {
|
||||
data, stride := destination.Buffer()
|
||||
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
xi := 1
|
||||
@@ -89,7 +97,7 @@ func lineHigh (
|
||||
x := min.X
|
||||
|
||||
for y := min.Y; y < max.Y; y ++ {
|
||||
destination.SetRGBA(x, y, source.RGBAAt(x, y))
|
||||
data[x + y * stride] = source.AtWhen(x, y, width, height)
|
||||
if D > 0 {
|
||||
x += xi
|
||||
D += 2 * (deltaX - deltaY)
|
||||
|
||||
@@ -1,105 +1,51 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Paste transfers one image onto another, offset by the specified point.
|
||||
// Paste transfers one canvas onto another, offset by the specified point.
|
||||
func Paste (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
source tomo.Canvas,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
sourceBounds := source.Bounds().Canon()
|
||||
dstData, dstStride := destination.Buffer()
|
||||
srcData, srcStride := source.Buffer()
|
||||
|
||||
sourceBounds :=
|
||||
source.Bounds().Canon().
|
||||
Intersect(destination.Bounds().Sub(offset))
|
||||
if sourceBounds.Empty() { return }
|
||||
|
||||
updatedRegion = sourceBounds.Add(offset)
|
||||
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
|
||||
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
|
||||
destination.SetRGBA (
|
||||
x + offset.X, y + offset.Y,
|
||||
source.RGBAAt(x, y))
|
||||
dstData[x + offset.X + (y + offset.Y) * dstStride] =
|
||||
srcData[x + y * srcStride]
|
||||
}}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Rectangle draws a rectangle with an inset border. If the border image is nil,
|
||||
// no border will be drawn. Likewise, if the fill image is nil, the rectangle
|
||||
// will have no fill.
|
||||
func Rectangle (
|
||||
func FillRectangle (
|
||||
destination tomo.Canvas,
|
||||
fill tomo.Image,
|
||||
stroke tomo.Image,
|
||||
weight int,
|
||||
source Pattern,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
bounds = bounds.Canon()
|
||||
data, stride := destination.Buffer()
|
||||
bounds = bounds.Canon().Intersect(destination.Bounds()).Canon()
|
||||
if bounds.Empty() { return }
|
||||
updatedRegion = bounds
|
||||
|
||||
fillBounds := bounds
|
||||
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight })
|
||||
fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight })
|
||||
fillBounds = fillBounds.Canon()
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
var pixel color.RGBA
|
||||
if (image.Point { x, y }).In(fillBounds) {
|
||||
pixel = fill.RGBAAt(x, y)
|
||||
} else {
|
||||
pixel = stroke.RGBAAt(x, y)
|
||||
}
|
||||
destination.SetRGBA(x, y, pixel)
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
for y := 0; y < height; y ++ {
|
||||
for x := 0; x < width; x ++ {
|
||||
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
|
||||
source.AtWhen(x, y, width, height)
|
||||
}}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// OffsetRectangle is the same as Rectangle, but offsets the border image to the
|
||||
// top left corner of the border and the fill image to the top left corner of
|
||||
// the fill.
|
||||
func OffsetRectangle (
|
||||
destination tomo.Canvas,
|
||||
fill tomo.Image,
|
||||
stroke tomo.Image,
|
||||
weight int,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
bounds = bounds.Canon()
|
||||
updatedRegion = bounds
|
||||
|
||||
fillBounds := bounds
|
||||
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight })
|
||||
fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight })
|
||||
fillBounds = fillBounds.Canon()
|
||||
|
||||
strokeImageMin := stroke.Bounds().Min
|
||||
fillImageMin := fill.Bounds().Min
|
||||
|
||||
yy := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
xx := 0
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
var pixel color.RGBA
|
||||
if (image.Point { x, y }).In(fillBounds) {
|
||||
pixel = fill.RGBAAt (
|
||||
xx - weight + fillImageMin.X,
|
||||
yy - weight + fillImageMin.Y)
|
||||
} else {
|
||||
pixel = stroke.RGBAAt (
|
||||
xx + strokeImageMin.X,
|
||||
yy + strokeImageMin.Y)
|
||||
}
|
||||
destination.SetRGBA(x, y, pixel)
|
||||
xx ++
|
||||
}
|
||||
yy ++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package artist
|
||||
// import "fmt"
|
||||
import "image"
|
||||
import "unicode"
|
||||
import "image/draw"
|
||||
// import "image/draw"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
@@ -95,34 +95,35 @@ func (drawer *TextDrawer) SetAlignment (align Align) {
|
||||
// Draw draws the drawer's text onto the specified canvas at the given offset.
|
||||
func (drawer *TextDrawer) Draw (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
source Pattern,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
for _, word := range drawer.layout {
|
||||
for _, character := range word.text {
|
||||
destinationRectangle,
|
||||
mask, maskPoint, _, ok := drawer.face.Glyph (
|
||||
fixed.P (
|
||||
offset.X + word.position.X + character.x,
|
||||
offset.Y + word.position.Y),
|
||||
character.character)
|
||||
if !ok { continue }
|
||||
// TODO: reimplement a version of draw mask that takes in a pattern
|
||||
// for _, word := range drawer.layout {
|
||||
// for _, character := range word.text {
|
||||
// destinationRectangle,
|
||||
// mask, maskPoint, _, ok := drawer.face.Glyph (
|
||||
// fixed.P (
|
||||
// offset.X + word.position.X + character.x,
|
||||
// offset.Y + word.position.Y),
|
||||
// character.character)
|
||||
// if !ok { continue }
|
||||
|
||||
// FIXME: clip destination rectangle if we are on the cusp of
|
||||
// the maximum height.
|
||||
|
||||
draw.DrawMask (
|
||||
destination,
|
||||
destinationRectangle,
|
||||
source, image.Point { },
|
||||
mask, maskPoint,
|
||||
draw.Over)
|
||||
// draw.DrawMask (
|
||||
// destination,
|
||||
// destinationRectangle,
|
||||
// source, image.Point { },
|
||||
// mask, maskPoint,
|
||||
// draw.Over)
|
||||
|
||||
updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
}}
|
||||
// updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
// }}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package artist
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Uniform is an infinite-sized Image of uniform color. It implements the
|
||||
// color.Color, color.Model, and tomo.Image interfaces.
|
||||
// Uniform is an infinite-sized pattern of uniform color. It implements the
|
||||
// color.Color, color.Model, and image.Image interfaces.
|
||||
type Uniform struct {
|
||||
C color.RGBA
|
||||
}
|
||||
@@ -29,13 +29,11 @@ func (uniform *Uniform) RGBA () (r, g, b, a uint32) {
|
||||
}
|
||||
|
||||
func (uniform *Uniform) ColorModel () (model color.Model) {
|
||||
model = uniform
|
||||
return
|
||||
return uniform
|
||||
}
|
||||
|
||||
func (uniform *Uniform) Convert (in color.Color) (out color.Color) {
|
||||
out = uniform.C
|
||||
return
|
||||
func (uniform *Uniform) Convert (in color.Color) (c color.Color) {
|
||||
return uniform.C
|
||||
}
|
||||
|
||||
func (uniform *Uniform) Bounds () (rectangle image.Rectangle) {
|
||||
@@ -45,13 +43,11 @@ func (uniform *Uniform) Bounds () (rectangle image.Rectangle) {
|
||||
}
|
||||
|
||||
func (uniform *Uniform) At (x, y int) (c color.Color) {
|
||||
c = uniform.C
|
||||
return
|
||||
return uniform.C
|
||||
}
|
||||
|
||||
func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) {
|
||||
c = uniform.C
|
||||
return
|
||||
func (uniform *Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
return uniform.C
|
||||
}
|
||||
|
||||
func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) {
|
||||
@@ -59,13 +55,10 @@ func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) {
|
||||
g := uint16(uniform.C.G) << 8 | uint16(uniform.C.G)
|
||||
b := uint16(uniform.C.B) << 8 | uint16(uniform.C.B)
|
||||
a := uint16(uniform.C.A) << 8 | uint16(uniform.C.A)
|
||||
|
||||
c = color.RGBA64 { R: r, G: g, B: b, A: a }
|
||||
return
|
||||
return color.RGBA64 { R: r, G: g, B: b, A: a }
|
||||
}
|
||||
|
||||
// Opaque scans the entire image and reports whether it is fully opaque.
|
||||
func (uniform *Uniform) Opaque () (opaque bool) {
|
||||
opaque = uniform.C.A == 0xFF
|
||||
return
|
||||
return uniform.C.A == 0xFF
|
||||
}
|
||||
|
||||
@@ -1,99 +1 @@
|
||||
package artist
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
import "image"
|
||||
import "image/draw"
|
||||
import "image/color"
|
||||
|
||||
// WrappedImage wraps an image.Image and allows it to satisfy tomo.Image.
|
||||
type WrappedImage struct { Underlying image.Image }
|
||||
|
||||
// WrapImage wraps a generic image.Image and allows it to satisfy tomo.Image.
|
||||
// Do not use this function to wrap images that already satisfy tomo.Image,
|
||||
// because the resulting wrapped image will be rather slow in comparison.
|
||||
func WrapImage (underlying image.Image) (wrapped tomo.Image) {
|
||||
wrapped = WrappedImage { Underlying: underlying }
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedImage) Bounds () (bounds image.Rectangle) {
|
||||
bounds = wrapped.Underlying.Bounds()
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedImage) ColorModel () (model color.Model) {
|
||||
model = wrapped.Underlying.ColorModel()
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedImage) At (x, y int) (pixel color.Color) {
|
||||
pixel = wrapped.Underlying.At(x, y)
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedImage) RGBAAt (x, y int) (pixel color.RGBA) {
|
||||
r, g, b, a := wrapped.Underlying.At(x, y).RGBA()
|
||||
pixel.R = uint8(r >> 8)
|
||||
pixel.G = uint8(g >> 8)
|
||||
pixel.B = uint8(b >> 8)
|
||||
pixel.A = uint8(a >> 8)
|
||||
return
|
||||
}
|
||||
|
||||
// WrappedCanvas wraps a draw.Image and allows it to satisfy tomo.Canvas.
|
||||
type WrappedCanvas struct { Underlying draw.Image }
|
||||
|
||||
// WrapCanvas wraps a generic draw.Image and allows it to satisfy tomo.Canvas.
|
||||
// Do not use this function to wrap images that already satisfy tomo.Canvas,
|
||||
// because the resulting wrapped image will be rather slow in comparison.
|
||||
func WrapCanvas (underlying draw.Image) (wrapped tomo.Canvas) {
|
||||
wrapped = WrappedCanvas { Underlying: underlying }
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) Bounds () (bounds image.Rectangle) {
|
||||
bounds = wrapped.Underlying.Bounds()
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) ColorModel () (model color.Model) {
|
||||
model = wrapped.Underlying.ColorModel()
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) At (x, y int) (pixel color.Color) {
|
||||
pixel = wrapped.Underlying.At(x, y)
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) RGBAAt (x, y int) (pixel color.RGBA) {
|
||||
r, g, b, a := wrapped.Underlying.At(x, y).RGBA()
|
||||
pixel.R = uint8(r >> 8)
|
||||
pixel.G = uint8(g >> 8)
|
||||
pixel.B = uint8(b >> 8)
|
||||
pixel.A = uint8(a >> 8)
|
||||
return
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) Set (x, y int, pixel color.Color) {
|
||||
wrapped.Underlying.Set(x, y, pixel)
|
||||
}
|
||||
|
||||
func (wrapped WrappedCanvas) SetRGBA (x, y int, pixel color.RGBA) {
|
||||
wrapped.Underlying.Set(x, y, pixel)
|
||||
}
|
||||
|
||||
// ToRGBA clones an existing image.Image into an image.RGBA struct, which
|
||||
// directly satisfies tomo.Image. This is useful for things like icons and
|
||||
// textures.
|
||||
func ToRGBA (input image.Image) (output *image.RGBA) {
|
||||
bounds := input.Bounds()
|
||||
output = image.NewRGBA(bounds)
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
output.Set(x, y, input.At(x, y))
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user