package goat
import (
"bytes"
"fmt"
"io"
)
type SVG struct {
Body string
Width int
Height int
}
func (s SVG) String() string {
return fmt.Sprintf("\n",
"diagram",
"http://www.w3.org/2000/svg",
"1.1", s.Height, s.Width, s.Body)
}
// BuildSVG reads in a newline-delimited ASCII diagram from src and returns a SVG.
func BuildSVG(src io.Reader) SVG {
var buff bytes.Buffer
canvas := NewCanvas(src)
canvas.WriteSVGBody(&buff)
return SVG{
Body: buff.String(),
Width: canvas.widthScreen(),
Height: canvas.heightScreen(),
}
}
// BuildAndWriteSVG reads in a newline-delimited ASCII diagram from src and writes a
// corresponding SVG diagram to dst.
func BuildAndWriteSVG(src io.Reader, dst io.Writer) {
canvas := NewCanvas(src)
// Preamble
writeBytes(dst,
"\n")
}
func writeBytes(out io.Writer, format string, args ...interface{}) {
bytesOut := fmt.Sprintf(format, args...)
_, err := out.Write([]byte(bytesOut))
if err != nil {
panic(nil)
}
}
// Draw a straight line as an SVG path.
func (l Line) Draw(out io.Writer) {
start := l.start.asPixel()
stop := l.stop.asPixel()
// For cases when a vertical line hits a perpendicular like this:
//
// | |
// | or v
// --- ---
//
// We need to nudge the vertical line half a vertical cell in the
// appropriate direction in order to meet up cleanly with the midline of
// the cell next to it.
// A diagonal segment all by itself needs to be shifted slightly to line
// up with _ baselines:
// _
// \_
//
// TODO make this a method on Line to return accurate pixel
if l.lonely {
switch l.orientation {
case NE:
start.x -= 4
stop.x -= 4
start.y += 8
stop.y += 8
case SE:
start.x -= 4
stop.x -= 4
start.y -= 8
stop.y -= 8
case S:
start.y -= 8
stop.y -= 8
}
// Half steps
switch l.chop {
case N:
stop.y -= 8
case S:
start.y += 8
}
}
if l.needsNudgingDown {
stop.y += 8
if l.horizontal() {
start.y += 8
}
}
if l.needsNudgingLeft {
start.x -= 8
}
if l.needsNudgingRight {
stop.x += 8
}
if l.needsTinyNudgingLeft {
start.x -= 4
if l.orientation == NE {
start.y += 8
} else if l.orientation == SE {
start.y -= 8
}
}
if l.needsTinyNudgingRight {
stop.x += 4
if l.orientation == NE {
stop.y -= 8
} else if l.orientation == SE {
stop.y += 8
}
}
writeBytes(out,
"\n",
start.x, start.y,
stop.x, stop.y,
)
}
// Draw a solid triable as an SVG polygon element.
func (t Triangle) Draw(out io.Writer) {
// https://www.w3.org/TR/SVG/shapes.html#PolygonElement
/*
+-----+-----+
| /|\ |
| / | \ |
x +- / -+- \ -+
| / | \ |
|/ | \|
+-----+-----+
y
*/
x, y := float32(t.start.asPixel().x), float32(t.start.asPixel().y)
r := 0.0
x0 := x + 8
y0 := y
x1 := x - 4
y1 := y - 0.35*16
x2 := x - 4
y2 := y + 0.35*16
switch t.orientation {
case N:
r = 270
if t.needsNudging {
x0 += 8
x1 += 8
x2 += 8
}
case NE:
r = 300
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case NW:
r = 240
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case W:
r = 180
if t.needsNudging {
x0 -= 8
x1 -= 8
x2 -= 8
}
case E:
r = 0
if t.needsNudging {
x0 -= 8
x1 -= 8
x2 -= 8
}
case S:
r = 90
if t.needsNudging {
x0 += 8
x1 += 8
x2 += 8
}
case SW:
r = 120
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case SE:
r = 60
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
}
writeBytes(out,
"\n",
x0, y0,
x1, y1,
x2, y2,
r,
x, y,
)
}
// Draw a solid circle as an SVG circle element.
func (c *Circle) Draw(out io.Writer) {
fill := "#fff"
if c.bold {
fill = "currentColor"
}
pixel := c.start.asPixel()
writeBytes(out,
"\n",
pixel.x,
pixel.y,
fill,
)
}
// Draw a single text character as an SVG text element.
func (t Text) Draw(out io.Writer) {
p := t.start.asPixel()
c := t.contents
opacity := 0
// Markdeep special-cases these character and treats them like a
// checkerboard.
switch c {
case "▉":
opacity = -64
case "▓":
opacity = 64
case "▒":
opacity = 128
case "░":
opacity = 191
}
fill := "currentColor"
if opacity > 0 {
fill = fmt.Sprintf("rgb(%d,%d,%d)", opacity, opacity, opacity)
}
if opacity != 0 {
writeBytes(out,
"",
p.x-4, p.y-8,
fill,
)
return
}
// Escape for XML
switch c {
case "&":
c = "&"
case ">":
c = ">"
case "<":
c = "<"
}
writeBytes(out,
"%s\n",
p.x, p.y+4, c,
)
}
// Draw a rounded corner as an SVG elliptical arc element.
func (c *RoundedCorner) Draw(out io.Writer) {
// https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
x, y := c.start.asPixelXY()
startX, startY, endX, endY, sweepFlag := 0, 0, 0, 0, 0
switch c.orientation {
case NW:
startX = x + 8
startY = y
endX = x - 8
endY = y + 16
case NE:
sweepFlag = 1
startX = x - 8
startY = y
endX = x + 8
endY = y + 16
case SE:
sweepFlag = 1
startX = x + 8
startY = y - 16
endX = x - 8
endY = y
case SW:
startX = x - 8
startY = y - 16
endX = x + 8
endY = y
}
writeBytes(out,
"\n",
startX,
startY,
sweepFlag,
endX,
endY,
)
}
// Draw a bridge as an SVG elliptical arc element.
func (b Bridge) Draw(out io.Writer) {
x, y := b.start.asPixelXY()
sweepFlag := 1
if b.orientation == W {
sweepFlag = 0
}
writeBytes(out,
"\n",
x, y-8,
sweepFlag,
x, y+8,
)
}