diff --git a/example.png b/example.png
index bea9a16..94dd073 100644
Binary files a/example.png and b/example.png differ
diff --git a/image/black_example.svg b/image/black_example.svg
index c6d710f..4360faf 100644
--- a/image/black_example.svg
+++ b/image/black_example.svg
@@ -531,4 +531,22 @@
a
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/image/example.png b/image/example.png
index bea9a16..94dd073 100644
Binary files a/image/example.png and b/image/example.png differ
diff --git a/image/example.svg b/image/example.svg
index 3314905..c626dda 100644
--- a/image/example.svg
+++ b/image/example.svg
@@ -531,4 +531,22 @@
h
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/image/image.go b/image/image.go
index a42a7e3..72e948a 100755
--- a/image/image.go
+++ b/image/image.go
@@ -30,6 +30,30 @@ func SquareColors(light, dark color.Color) func(*Encoder) {
}
}
+// Arrow allocates and returns a new arrow with the default color
+func Arrow(from, to chess.Square) arrow {
+ return arrow{
+ from: from,
+ to: to,
+ color: color.RGBA{247, 181, 75, 1}, // orange
+ }
+}
+
+// WithColor alters the color of the arrow (the default is a light orange)
+func (arrow arrow) WithColor(col color.Color) arrow {
+ arrow.color = col
+ return arrow
+}
+
+// MarkArrows is designed to be used as an optional argument
+// to the SVG function. It marks an arrow between the given
+// squares with the an Arrow of the given color.
+func MarkArrows(arrows ...arrow) func(*encoder) {
+ return func(e *encoder) {
+ e.arrows = append(e.arrows, arrows...)
+ }
+}
+
// MarkSquares is designed to be used as an optional argument
// to the SVG function. It marks the given squares with the
// color. A possible usage includes marking squares of the
@@ -58,6 +82,14 @@ type Encoder struct {
dark color.Color
perspective chess.Color
marks map[chess.Square]color.Color
+ arrows []arrow
+}
+
+// An arrow represents the visualization of a move
+type arrow struct {
+ from chess.Square
+ to chess.Square
+ color color.Color
}
// New returns an encoder that writes to the given writer.
@@ -70,6 +102,7 @@ func new(w io.Writer, options []func(*Encoder)) *Encoder {
dark: color.RGBA{165, 117, 81, 1},
perspective: chess.White,
marks: map[chess.Square]color.Color{},
+ arrows: []arrow{},
}
for _, op := range options {
op(e)
@@ -80,8 +113,12 @@ func new(w io.Writer, options []func(*Encoder)) *Encoder {
const (
sqWidth = 45
sqHeight = 45
+ sqCenter = 22.5
boardWidth = 8 * sqWidth
boardHeight = 8 * sqHeight
+
+ arrowWidth = 8
+ arrowOpacity = "75%"
)
var (
@@ -138,6 +175,17 @@ func (e *Encoder) EncodeSVG(b *chess.Board) error {
}
}
}
+ for i, arrow := range e.arrows {
+ var xml string
+ if isKnightMove(arrow.from, arrow.to) {
+ xml = knightArrowXML(i, arrow, e.perspective)
+ } else {
+ xml = straightArrowXML(i, arrow, e.perspective)
+ }
+ if _, err := io.WriteString(canvas.Writer, xml); err != nil {
+ return err
+ }
+ }
canvas.End()
return nil
}
@@ -171,6 +219,249 @@ func pieceXML(x, y int, p chess.Piece) string {
return strings.Replace(svgStr, old, new, 1)
}
+func straightArrowXML(id int, a arrow, perspective chess.Color) string {
+ c := colorToHex(a.color)
+ x1, y1 := squareCenter(a.from, perspective)
+ x2, y2 := squareCenter(a.to, perspective)
+
+ // move the start of the arrow away from the center of the square
+ // as well as head of the arrow to the center of its square
+ offset := float32(sqCenter / 1.5)
+ if x1 == x2 {
+ // horizontal line
+ if y1 < y2 {
+ y1 += offset
+ y2 -= offset
+ } else {
+ y1 -= offset
+ y2 += offset
+ }
+ } else if y1 == y2 {
+ // vertical line
+ if x1 < x2 {
+ x1 += offset
+ x2 -= offset
+ } else {
+ x1 -= offset
+ x2 += offset
+ }
+ } else {
+ // diagonal line
+ offset /= 2
+ if x1 < x2 {
+ x1 += offset
+ x2 -= offset
+ } else {
+ x1 -= offset
+ x2 += offset
+ }
+
+ if y1 < y2 {
+ y1 += offset
+ y2 -= offset
+ } else {
+ y1 -= offset
+ y2 += offset
+ }
+ }
+
+ return fmt.Sprintf(``,
+ // arrow head
+ id,
+ c,
+ arrowOpacity,
+
+ // line
+ id,
+ arrowWidth,
+ x1,
+ y1,
+ x2,
+ y2,
+ c,
+ arrowOpacity,
+ )
+}
+
+func knightArrowXML(id int, a arrow, perspective chess.Color) string {
+ c := colorToHex(a.color)
+
+ horizontal := horizontalMoves(a.from, a.to)
+ vertical := verticalMoves(a.from, a.to)
+
+ pivot := a.from
+ if abs(vertical) == 1 {
+ pivot = squareOffset(pivot, horizontal)
+ } else {
+ if vertical > 0 {
+ pivot = squareOffset(pivot, 16)
+ } else {
+ pivot = squareOffset(pivot, -16)
+ }
+ }
+
+ x11, y11 := squareCenter(a.from, perspective)
+ x12, y12 := squareCenter(pivot, perspective)
+
+ x21, y21 := squareCenter(pivot, perspective)
+ x22, y22 := squareCenter(a.to, perspective)
+
+ // move the start and end of the first line further away from the center of the square
+ offset := float32(sqCenter / 1.5)
+ halfWidth := float32(arrowWidth / 2)
+ if abs(horizontal) > abs(vertical) {
+ if x11 < x12 {
+ x11 += offset
+ x12 += halfWidth
+ } else {
+ x11 -= offset
+ x12 -= halfWidth
+ }
+ } else {
+ if y11 < y12 {
+ y11 += offset
+ y12 += halfWidth
+ } else {
+ y11 -= offset
+ y12 -= halfWidth
+ }
+ }
+
+ // move the arrow head to the center of the square
+ if perspective == chess.Black {
+ offset *= -1
+ }
+ if abs(horizontal) > abs(vertical) {
+ if vertical > 0 {
+ y21 += halfWidth
+ y22 += offset
+ } else {
+ y21 -= halfWidth
+ y22 -= offset
+ }
+ } else {
+ if horizontal > 0 {
+ x21 -= halfWidth
+ x22 -= offset
+ } else {
+ x21 += halfWidth
+ x22 += offset
+ }
+ }
+
+ template := ``
+
+ return fmt.Sprintf(
+ template,
+
+ // arrow head
+ id,
+ c,
+ arrowOpacity,
+
+ // first line
+ arrowWidth,
+ x11,
+ y11,
+ x12,
+ y12,
+ c,
+ arrowOpacity,
+
+ // second line
+ id,
+ arrowWidth,
+ x21,
+ y21,
+ x22,
+ y22,
+ c,
+ arrowOpacity,
+ )
+}
+
+func isKnightMove(from, to chess.Square) bool {
+ vertical := abs(verticalMoves(from, to))
+ horizontal := abs(horizontalMoves(from, to))
+ return (vertical == 1 && horizontal == 2) || (vertical == 2 && horizontal == 1)
+}
+
+func verticalMoves(from, to chess.Square) int {
+ fromRank := int(from) / 8
+ toRank := int(to) / 8
+ return toRank - fromRank
+}
+
+func horizontalMoves(from, to chess.Square) int {
+ fromMod := int(from) % 8
+ toMod := int(to) % 8
+ return toMod - fromMod
+}
+
+func abs(a int) int {
+ if a < 0 {
+ return -a
+ }
+ return a
+}
+
+func squareCenter(sq chess.Square, perspective chess.Color) (float32, float32) {
+ if perspective == chess.White {
+ x := float32(sq%8)*sqWidth + sqCenter
+ y := boardHeight - float32(sq/8)*sqWidth - sqCenter
+ return x, y
+ } else {
+ x := boardWidth - float32(sq%8)*sqWidth - sqCenter
+ y := float32(sq/8)*sqWidth + sqCenter
+ return x, y
+ }
+}
+
+func squareOffset(sq chess.Square, offset int) chess.Square {
+ return chess.Square(int(sq) + offset)
+}
+
var (
pieceTypeMap = map[chess.PieceType]string{
chess.King: "K",
diff --git a/image/image_test.go b/image/image_test.go
index b442c52..0894180 100755
--- a/image/image_test.go
+++ b/image/image_test.go
@@ -14,8 +14,9 @@ import (
"github.com/notnil/chess/image"
)
-const expectedMD5 = "08aaa6fcfde3bb900fc54bdfef3d5c81"
-const expectedMD5Black = "badac5ca5cfbdea9b98a1f9988ba54bc"
+const expectedMD5 = "a2ee66ca19e4c347aec41371c1ca07f8"
+const expectedMD5Black = "ce4d4e033a50678898c62928b8e0a15c"
+const expectedMD5KnightsAndDiagonalArrows = "9c95aa56cec67be2ceee141f259753f7"
func TestSVG(t *testing.T) {
// create buffer of actual svg
@@ -26,7 +27,8 @@ func TestSVG(t *testing.T) {
t.Error(err)
}
mark := image.MarkSquares(color.RGBA{255, 255, 0, 1}, chess.D2, chess.D4)
- if err := image.SVG(buf, pos.Board(), mark); err != nil {
+ arrows := image.MarkArrows(image.Arrow(chess.D2, chess.D4))
+ if err := image.SVG(buf, pos.Board(), mark, arrows); err != nil {
t.Error(err)
}
@@ -57,8 +59,9 @@ func TestSVGFromBlack(t *testing.T) {
t.Error(err)
}
mark := image.MarkSquares(color.RGBA{255, 255, 0, 1}, chess.D2, chess.D4)
+ arrows := image.MarkArrows(image.Arrow(chess.D2, chess.D4).WithColor(color.Black))
per := image.Perspective(chess.Black)
- if err := image.SVG(buf, pos.Board(), mark, per); err != nil {
+ if err := image.SVG(buf, pos.Board(), mark, arrows, per); err != nil {
t.Error(err)
}
@@ -79,3 +82,57 @@ func TestSVGFromBlack(t *testing.T) {
t.Error(err)
}
}
+
+func TestSVGKnightsAndDiagonals(t *testing.T) {
+ // create buffer of actual svg
+ buf := bytes.NewBuffer([]byte{})
+ fenStr := "rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1"
+ pos := &chess.Position{}
+ if err := pos.UnmarshalText([]byte(fenStr)); err != nil {
+ t.Error(err)
+ }
+ arrows := image.MarkArrows(
+ // all possible knight directions
+ image.Arrow(chess.F6, chess.E4),
+ image.Arrow(chess.F6, chess.D5),
+ image.Arrow(chess.F6, chess.D7),
+ image.Arrow(chess.F6, chess.E8),
+ image.Arrow(chess.F6, chess.G4),
+ image.Arrow(chess.F6, chess.H5),
+ image.Arrow(chess.F6, chess.H7),
+ image.Arrow(chess.F6, chess.G8),
+
+ // a couple knight moves with no overlapping arrows
+ image.Arrow(chess.B1, chess.D2),
+ image.Arrow(chess.B8, chess.C6),
+
+ // diagonal arrows
+ image.Arrow(chess.C4, chess.A6),
+ image.Arrow(chess.C4, chess.D5),
+
+ // anti-diagonal arrows
+ image.Arrow(chess.C4, chess.A2),
+ image.Arrow(chess.C4, chess.D3),
+ )
+ per := image.Perspective(chess.Black)
+ if err := image.SVG(buf, pos.Board(), arrows, per); err != nil {
+ t.Error(err)
+ }
+
+ // compare to expected svg
+ actualSVG := strings.TrimSpace(buf.String())
+ actualMD5 := fmt.Sprintf("%x", md5.Sum([]byte(actualSVG)))
+ if actualMD5 != expectedMD5KnightsAndDiagonalArrows {
+ t.Errorf("expected actual md5 hash to be %s but got %s", expectedMD5KnightsAndDiagonalArrows, actualMD5)
+ }
+
+ // create actual svg file for visualization
+ f, err := os.Create("knight_arrows_example.svg")
+ defer f.Close()
+ if err != nil {
+ t.Error(err)
+ }
+ if _, err := io.Copy(f, bytes.NewBufferString(actualSVG)); err != nil {
+ t.Error(err)
+ }
+}
diff --git a/image/knight_arrows_example.svg b/image/knight_arrows_example.svg
new file mode 100644
index 0000000..b8d567f
--- /dev/null
+++ b/image/knight_arrows_example.svg
@@ -0,0 +1,834 @@
+
+
+
\ No newline at end of file