Commit de6231b2 authored by Derek Schaab's avatar Derek Schaab

Draw "on" squares as contiguous regions to avoid hairline gaps between squares when scaling

parent b28723fc
......@@ -80,6 +80,15 @@ func (i Icon) WritePNG(w io.Writer, size int) (err error) {
return
}
type direction uint8
const (
north direction = 1
east direction = 2
south direction = 4
west direction = 8
)
// WriteSVG renders the icon in SVG format at the given size to w.
//
// This is the fastest rendering method, but at small sizes it produces a
......@@ -87,7 +96,7 @@ func (i Icon) WritePNG(w io.Writer, size int) (err error) {
func (i Icon) WriteSVG(w io.Writer, size int) (err error) {
imgSize, rowSize := clampSize(size)
sizeStr := []byte(strconv.Itoa(imgSize))
rowStrs := [8][]byte{
rowStrs := [9][]byte{
[]byte("0"),
[]byte(strconv.Itoa(rowSize)),
[]byte(strconv.Itoa(2 * rowSize)),
......@@ -96,6 +105,7 @@ func (i Icon) WriteSVG(w io.Writer, size int) (err error) {
[]byte(strconv.Itoa(5 * rowSize)),
[]byte(strconv.Itoa(6 * rowSize)),
[]byte(strconv.Itoa(7 * rowSize)),
[]byte(strconv.Itoa(8 * rowSize)),
}
bg, fg := i.Background(), i.Foreground()
wr := &svgWriter{w: w}
......@@ -108,6 +118,12 @@ func (i Icon) WriteSVG(w io.Writer, size int) (err error) {
wr.write(quote)
wr.write(bracket)
wr.write(tagRect)
wr.write(attX)
wr.write(rowStrs[0])
wr.write(quote)
wr.write(attY)
wr.write(rowStrs[0])
wr.write(quote)
wr.write(attWidth)
wr.write(sizeStr)
wr.write(quote)
......@@ -124,7 +140,7 @@ func (i Icon) WriteSVG(w io.Writer, size int) (err error) {
wr.write(quote)
wr.write(slash)
wr.write(bracket)
wr.write(tagGroup)
wr.write(tagPath)
wr.write(attFill)
wr.write([]byte(strconv.Itoa(int(fg.R))))
wr.write(comma)
......@@ -133,29 +149,122 @@ func (i Icon) WriteSVG(w io.Writer, size int) (err error) {
wr.write([]byte(strconv.Itoa(int(fg.B))))
wr.write(paren)
wr.write(quote)
wr.write(bracket)
wr.write(attData)
// Recursively determine the edges of each subpath. edges keeps track of
// which sides of each of the 64 squares have had an edge drawn. The values
// of each element are a bitmask composed of one of the four direction
// values. Thus if edges[0] is 0xf, it means that we've completely surrounded
// the top-left square with edges.
var edges [64]direction
// seg is a helper for writing "M{x},{y}" and "L{x},{y}" sequences, which
// we'll make extensive use of as we build up the SVG path definition. We
// determine the coordinates based on the direction we're "looking at". If
// we're looking to the west, we draw the point at the lower left of the
// current square (identified by x and y). If we're looking to the north, we
// draw the point at the upper left of the current square, and so on
// clockwise around the compass. The point drawn is always to the left of the
// vector starting in the middle of the current square and proceeding in the
// direction d and on the edge intersected by the same vector.
seg := func(c byte, x, y int, d direction) {
wr.write(space)
wr.write([]byte{c})
wr.write(rowStrs[x+int(((d&east)>>1)|((d&south)>>2))])
wr.write(comma)
wr.write(rowStrs[y+int(((d&west)>>3)|((d&south)>>2))])
}
// draw is the function that will actually trace out the subpath. It receives
// the starting square and a look direction, which determines the first side
// of the square it looks at to see whether it's an edge. It returns true for
// closed when it encounters a square that has an edge on the side that is
// one step anticlockwise from the current look direction.
var draw func(int, int, direction) bool
draw = func(x, y int, d direction) (closed bool) {
// j is the index into edges that refers to the current square.
j := (8 * y) + x
// Starting with the look direction, we proceed clockwise around the
// compass, checking each side of the current square.
for k := 0; k < 4; k, d = k+1, d<<1 {
// Reset the direction back to north if we shifted d to an invalid value.
if d > west {
d = north
}
// dx and dy refer to the square next to the current squared in the
// direction d. These coordinates could be beyond the canvas boundaries.
// That's OK, because the Icon.on method will treat all out-of-bounds
// coordinates as "off".
dx := x + int((d&east)>>1) - int((d&west)>>3)
dy := y + int((d&south)>>2) - int((d&north)>>0)
// We know that we should draw an edge if the current square (x, y) is "on"
// and the neighboring square (dx, dy) is "off".
if edge := i.on(x, y) && !i.on(dx, dy); edge {
// If we've already drawn this edge, there's no reason to proceed.
if edges[j]&d != 0 {
continue
}
// Otherwise we add this direction to the appropriate bitmask in edges
// to signal to future invocations that we've already drawn this edge
// and use seg to record the next point in the subpath.
edges[j] |= d
seg('L', x, y, d)
} else if i.on(dx, dy) {
// If the neighboring square is also "on", we recurse into it to
// continue the path. When advancing to a neighboring square, we want
// to start looking in the direction one step anticlockwise from d. For
// example, when stepping into a square to the east, we want to start
// looking at the north edge. When stepping to the south, we want to
// start looking to the east. This pattern ensures that we don't miss
// any edges of the subpath.
e := d >> 1
if e == 0 {
e = west
}
// If we see that the first edge to be examined in the next square has
// already been drawn, we know we've successfully reached the end of
// this subpath. We close the subpath with a "Z" segment and set the
// return value to true so that callers further up the stack can exit
// early.
if edges[(8*dy)+dx]&e != 0 {
wr.write(space)
wr.write([]byte{'Z'})
closed = true
return
}
// Otherwise we recurse. If we find out that the subpath has been
// closed, there's no reason to examine any other edges of the current
// square.
closed = draw(dx, dy, e)
if closed {
return
}
}
}
return
}
// Starting from the top-left corner and proceeding left-to-right,
// top-to-bottom, we examine each square that's "on". For each such square,
// if the square to the west is "off" and we have not yet drawn an edge to
// the west, we start a new subpath. Otherwise, if the square to the east is
// "off" and we have not yet drawn an edge to the east, we have found a
// subpath enclosed by a larger subpath, so we also start a new subpath here.
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
if i.on(x, y) {
wr.write(tagRect)
wr.write(attWidth)
wr.write(rowStrs[1])
wr.write(quote)
wr.write(attHeight)
wr.write(rowStrs[1])
wr.write(quote)
wr.write(attX)
wr.write(rowStrs[x])
wr.write(quote)
wr.write(attY)
wr.write(rowStrs[y])
wr.write(quote)
wr.write(slash)
wr.write(bracket)
if !i.on(x, y) {
continue
}
if !i.on(x-1, y) && edges[(8*y)+x]&west == 0 {
seg('M', x, y, west)
draw(x, y, west)
}
if !i.on(x+1, y) && edges[(8*y)+x]&east == 0 {
seg('M', x, y, east)
draw(x, y, east)
}
}
}
wr.write(tagGroupClose)
wr.write(quote)
wr.write(slash)
wr.write(bracket)
wr.write(tagSVGClose)
return
}
......@@ -275,20 +384,23 @@ var blockPatterns = [8]uint8{
}
// on determines whether the "pixel" at coordinate (x, y) is "turned on" (drawn
// with the foreground color). Only the three least significant bits of x and y
// are considered.
// with the foreground color). Coordinates that are outside the bounds of the
// image are are considered to be "turned off".
func (i Icon) on(x, y int) bool {
x, y = x&0x7, y&0x7
offset := blockOffsets[8*y+x]
nx, ny := x&0x7, y&0x7
if x != nx || y != ny {
return false
}
offset := blockOffsets[8*ny+nx]
block := int(i&(0x7<<offset)) >> offset
block += int((28 - offset) >> 2)
block &= 0x7
if x > 3 {
if nx > 3 {
block ^= 0x7
}
mask := uint8(8)
mask >>= uint(x & 1)
mask >>= uint(y&1) << 1
mask >>= uint(nx & 1)
mask >>= uint(ny&1) << 1
return blockPatterns[block]&mask != 0
}
......@@ -317,21 +429,22 @@ func (i Icon) rasterize(w io.Writer, size int, encode func(io.Writer, image.Imag
}
var (
tagSVG = []byte(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"`)
tagSVGClose = []byte(`</svg>`)
tagGroup = []byte(`<g`)
tagGroupClose = []byte(`</g>`)
tagRect = []byte(`<rect`)
attHeight = []byte(` height="`)
attFill = []byte(` fill="rgb(`)
attWidth = []byte(` width="`)
attX = []byte(` x="`)
attY = []byte(` y="`)
quote = []byte(`"`)
comma = []byte(`,`)
paren = []byte(`)`)
bracket = []byte(`>`)
slash = []byte(`/`)
tagSVG = []byte(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg"`)
tagSVGClose = []byte(`</svg>`)
tagPath = []byte(`<path`)
tagRect = []byte(`<rect`)
attData = []byte(` d="`)
attHeight = []byte(` height="`)
attFill = []byte(` fill="rgb(`)
attWidth = []byte(` width="`)
attX = []byte(` x="`)
attY = []byte(` y="`)
space = []byte(` `)
quote = []byte(`"`)
comma = []byte(`,`)
paren = []byte(`)`)
bracket = []byte(`>`)
slash = []byte(`/`)
)
type svgWriter struct {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment