diff --git a/image/README.md b/image/README.md index 85c2ff3..9356521 100644 --- a/image/README.md +++ b/image/README.md @@ -40,6 +40,18 @@ mark := image.MarkSquares(yellow, chess.D2, chess.D4) image.SVG(file, pos.Board(), mark) ``` +### Perspective + +Perspective is designed to be used as an optional argument +to the SVG function. It draws the board from the perspective +of the given color. White is the default. The following +generates a board from Black's perspective: + +```go +fromBlack := image.Perspective(chess.Black) +image.SVG(file, pos.Board(), fromBlack) +``` + ### Example Program ```go diff --git a/image/black_example.svg b/image/black_example.svg new file mode 100644 index 0000000..c6d710f --- /dev/null +++ b/image/black_example.svg @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +3 + + + + + + + + +4 + + + + + + + + + + + + +5 + + + + + + + + +6 + + + + + + + + + + + +7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +8 +h + + + + + + + + + + +g + + + + + + + + + + + +f + + + + + + + + + + + +e + + + + + + + + + + + + + + + + + + + +d + + + + + + + + + + + +c + + + + + + + + + + +b + + + + + + + + + + + + + + + + +a + \ No newline at end of file diff --git a/image/example.svg b/image/example.svg index 1674249..3314905 100644 --- a/image/example.svg +++ b/image/example.svg @@ -4,55 +4,70 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - - - + + + + + + d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z " + style="stroke-linecap:butt;" /> + d="M 12,35.5 L 33,35.5 L 33,35.5" + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> + d="M 13,31.5 L 32,31.5" + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> + + + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> -1 -a - - +8 + + + style="fill:#000000; stroke:#000000;" /> + style="fill:#000000; stroke:#000000;" /> + style="fill:#ffffff; stroke:#ffffff;" /> + style="fill:#ffffff; stroke:#ffffff;" /> + -b - - + + - + + d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" + style="fill:none; stroke:#ffffff; stroke-linejoin:miter;" /> -c - - - - - + + + + + + + + + + + d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z" + style="stroke-linecap:butt; stroke:#000000;" /> + d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 10.5,36 10.5,36 C 9,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z" + style="stroke-linecap:butt;" /> + d="M 11,38.5 A 35,35 1 0 0 34,38.5" + style="fill:none; stroke:#000000; stroke-linecap:butt;" /> + d="M 11,29 A 35,35 1 0 1 34,29" + style="fill:none; stroke:#ffffff;" /> + d="M 12.5,31.5 L 32.5,31.5" + style="fill:none; stroke:#ffffff;" /> + d="M 11.5,34.5 A 35,35 1 0 0 33.5,34.5" + style="fill:none; stroke:#ffffff;" /> + d="M 10.5,37.5 A 35,35 1 0 0 34.5,37.5" + style="fill:none; stroke:#ffffff;" /> -d - - + + - + d="M 22.5,11.63 L 22.5,6" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" + id="path6570" /> + d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" + style="fill:#000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;" /> + d="M 11.5,37 C 17,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 19,16 9.5,13 6.5,19.5 C 3.5,25.5 11.5,29.5 11.5,29.5 L 11.5,37 z " + style="fill:#000000; stroke:#000000;" /> + d="M 20,8 L 25,8" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> + d="M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.51,26.6 L 22.5,24.5 C 20,18 9.906,14 6.997,19.85 C 4.5,25.5 11.85,28.85 11.85,28.85" + style="fill:none; stroke:#ffffff;" /> + d="M 11.5,30 C 17,27 27,27 32.5,30 M 11.5,33.5 C 17,30.5 27,30.5 32.5,33.5 M 11.5,37 C 17,34 27,34 32.5,37" + style="fill:none; stroke:#ffffff;" /> -e - - + + - + + d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" + style="fill:none; stroke:#ffffff; stroke-linejoin:miter;" /> -f - - + + + style="fill:#000000; stroke:#000000;" /> + style="fill:#000000; stroke:#000000;" /> + style="fill:#ffffff; stroke:#ffffff;" /> + style="fill:#ffffff; stroke:#ffffff;" /> + -g - - - + + + + + + d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z " + style="stroke-linecap:butt;" /> + d="M 12,35.5 L 33,35.5 L 33,35.5" + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> + d="M 13,31.5 L 32,31.5" + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> + + + style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" /> -h - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> -2 - - +7 + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - -3 - - - - - - - - -4 - - - - - + + + style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - - - - -5 - - - - - - - 6 @@ -274,119 +270,133 @@ - - + +5 + + + + + + + + +4 + + + + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> -7 - - + + + + + +3 + + + + + + + + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - +2 + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - + + + style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" /> - - - + + + - - - - + d="M 34,14 L 31,17 L 14,17 L 11,14" /> + d="M 31,17 L 31,29.5 L 14,29.5 L 14,17" + style="stroke-linecap:butt; stroke-linejoin:miter;" /> + d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" /> + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> -8 - - +1 +a + + + style="fill:#ffffff; stroke:#000000;" /> + style="fill:#ffffff; stroke:#000000;" /> + style="fill:#000000; stroke:#000000;" /> - + style="fill:#000000; stroke:#000000;" /> - - +b + + - + + d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> - - - - - - - - - - +c + + + + d="M 9 13 A 2 2 0 1 1 5,13 A 2 2 0 1 1 9 13 z" + transform="translate(-1,-1)" /> + d="M 9 13 A 2 2 0 1 1 5,13 A 2 2 0 1 1 9 13 z" + transform="translate(15.5,-5.5)" /> + d="M 9 13 A 2 2 0 1 1 5,13 A 2 2 0 1 1 9 13 z" + transform="translate(32,-1)" /> + d="M 9 13 A 2 2 0 1 1 5,13 A 2 2 0 1 1 9 13 z" + transform="translate(7,-4.5)" /> + d="M 9 13 A 2 2 0 1 1 5,13 A 2 2 0 1 1 9 13 z" + transform="translate(24,-4)" /> + d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38,14 L 31,25 L 31,11 L 25.5,24.5 L 22.5,9.5 L 19.5,24.5 L 14,10.5 L 14,25 L 7,14 L 9,26 z " + style="stroke-linecap:butt;" /> + d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 10.5,36 10.5,36 C 9,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z " + style="stroke-linecap:butt;" /> + + - - +d + + + d="M 22.5,11.63 L 22.5,6" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> + d="M 20,8 L 25,8" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> + d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" + style="fill:#ffffff; stroke:#000000; stroke-linecap:butt; stroke-linejoin:miter;" /> + d="M 11.5,37 C 17,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 19,16 9.5,13 6.5,19.5 C 3.5,25.5 11.5,29.5 11.5,29.5 L 11.5,37 z " + style="fill:#ffffff; stroke:#000000;" /> + d="M 11.5,30 C 17,27 27,27 32.5,30" + style="fill:none; stroke:#000000;" /> + d="M 11.5,33.5 C 17,30.5 27,30.5 32.5,33.5" + style="fill:none; stroke:#000000;" /> + - - +e + + - + + d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> - - +f + + + style="fill:#ffffff; stroke:#000000;" /> + style="fill:#ffffff; stroke:#000000;" /> + style="fill:#000000; stroke:#000000;" /> - + style="fill:#000000; stroke:#000000;" /> - - - +g + + + - - - - + d="M 34,14 L 31,17 L 14,17 L 11,14" /> + d="M 31,17 L 31,29.5 L 14,29.5 L 14,17" + style="stroke-linecap:butt; stroke-linejoin:miter;" /> + d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" /> + style="fill:none; stroke:#000000; stroke-linejoin:miter;" /> +h \ No newline at end of file diff --git a/image/image.go b/image/image.go index f23217b..9599390 100755 --- a/image/image.go +++ b/image/image.go @@ -42,12 +42,22 @@ func MarkSquares(c color.Color, sqs ...chess.Square) func(*encoder) { } } +// Perspective is designed to be used as an optional argument +// to the SVG function. It draws the board from the perspective +// of the given color. White is the default. +func Perspective(c chess.Color) func(*encoder) { + return func(e *encoder) { + e.perspective = c + } +} + // A Encoder encodes chess boards into images. type encoder struct { - w io.Writer - light color.Color - dark color.Color - marks map[chess.Square]color.Color + w io.Writer + light color.Color + dark color.Color + perspective chess.Color + marks map[chess.Square]color.Color } // New returns an encoder that writes to the given writer. @@ -55,10 +65,11 @@ type encoder struct { // output. func new(w io.Writer, options []func(*encoder)) *encoder { e := &encoder{ - w: w, - light: color.RGBA{235, 209, 166, 1}, - dark: color.RGBA{165, 117, 81, 1}, - marks: map[chess.Square]color.Color{}, + w: w, + light: color.RGBA{235, 209, 166, 1}, + dark: color.RGBA{165, 117, 81, 1}, + perspective: chess.White, + marks: map[chess.Square]color.Color{}, } for _, op := range options { op(e) @@ -74,8 +85,10 @@ const ( ) var ( - orderOfRanks = []chess.Rank{chess.Rank8, chess.Rank7, chess.Rank6, chess.Rank5, chess.Rank4, chess.Rank3, chess.Rank2, chess.Rank1} - orderOfFiles = []chess.File{chess.FileA, chess.FileB, chess.FileC, chess.FileD, chess.FileE, chess.FileF, chess.FileG, chess.FileH} + orderOfRanks = []chess.Rank{chess.Rank8, chess.Rank7, chess.Rank6, chess.Rank5, chess.Rank4, chess.Rank3, chess.Rank2, chess.Rank1} + orderOfRanksBlack = []chess.Rank{chess.Rank1, chess.Rank2, chess.Rank3, chess.Rank4, chess.Rank5, chess.Rank6, chess.Rank7, chess.Rank8} + orderOfFiles = []chess.File{chess.FileA, chess.FileB, chess.FileC, chess.FileD, chess.FileE, chess.FileF, chess.FileG, chess.FileH} + orderOfFilesBlack = []chess.File{chess.FileH, chess.FileG, chess.FileF, chess.FileE, chess.FileD, chess.FileC, chess.FileB, chess.FileA} ) // EncodeSVG writes the board SVG representation into @@ -87,34 +100,42 @@ func (e *encoder) EncodeSVG(b *chess.Board) error { canvas.Start(boardWidth, boardHeight) canvas.Rect(0, 0, boardWidth, boardHeight) - for i := 0; i < 64; i++ { - sq := chess.Square(i) - x, y := xyForSquare(sq) - // draw square - c := e.colorForSquare(sq) - canvas.Rect(x, y, sqWidth, sqHeight, "fill: "+colorToHex(c)) - markColor, ok := e.marks[sq] - if ok { - canvas.Rect(x, y, sqWidth, sqHeight, "fill-opacity:0.2;fill: "+colorToHex(markColor)) - } - // draw piece - p := boardMap[sq] - if p != chess.NoPiece { - xml := pieceXML(x, y, p) - if _, err := io.WriteString(canvas.Writer, xml); err != nil { - return err + ranks := orderOfRanks + files := orderOfFiles + if e.perspective == chess.Black { + ranks = orderOfRanksBlack + files = orderOfFilesBlack + } + for i, rank := range ranks { + for j, file := range files { + x := j * sqHeight + y := i * sqHeight + sq := chess.NewSquare(file, rank) + c := e.colorForSquare(sq) + canvas.Rect(x, y, sqWidth, sqHeight, "fill: "+colorToHex(c)) + markColor, ok := e.marks[sq] + if ok { + canvas.Rect(x, y, sqWidth, sqHeight, "fill-opacity:0.2;fill: "+colorToHex(markColor)) + } + // draw piece + p := boardMap[sq] + if p != chess.NoPiece { + xml := pieceXML(x, y, p) + if _, err := io.WriteString(canvas.Writer, xml); err != nil { + return err + } + } + // draw rank text on file A + txtColor := e.colorForText(sq) + if j == 0 { + style := "font-size:11px;fill: " + colorToHex(txtColor) + canvas.Text(x+(sqWidth*1/20), y+(sqHeight*5/20), sq.Rank().String(), style) + } + // draw file text on rank 1 + if i == 7 { + style := "text-anchor:end;font-size:11px;fill: " + colorToHex(txtColor) + canvas.Text(x+(sqWidth*19/20), y+sqHeight-(sqHeight*1/15), sq.File().String(), style) } - } - // draw rank text on file A - txtColor := e.colorForText(sq) - if sq.File() == chess.FileA { - style := "font-size:11px;fill: " + colorToHex(txtColor) - canvas.Text(x+(sqWidth*1/20), y+(sqHeight*5/20), sq.Rank().String(), style) - } - // draw file text on rank 1 - if sq.Rank() == chess.Rank1 { - style := "text-anchor:end;font-size:11px;fill: " + colorToHex(txtColor) - canvas.Text(x+(sqWidth*19/20), y+sqHeight-(sqHeight*1/15), sq.File().String(), style) } } canvas.End() @@ -137,12 +158,6 @@ func (e *encoder) colorForText(sq chess.Square) color.Color { return e.dark } -func xyForSquare(sq chess.Square) (x, y int) { - fileIndex := int(sq.File()) - rankIndex := 7 - int(sq.Rank()) - return fileIndex * sqWidth, rankIndex * sqHeight -} - func colorToHex(c color.Color) string { r, g, b, _ := c.RGBA() return fmt.Sprintf("#%02x%02x%02x", uint8(float64(r)+0.5), uint8(float64(g)*1.0+0.5), uint8(float64(b)*1.0+0.5)) diff --git a/image/image_test.go b/image/image_test.go index f0bb21f..b442c52 100755 --- a/image/image_test.go +++ b/image/image_test.go @@ -14,7 +14,8 @@ import ( "github.com/notnil/chess/image" ) -const expectedMD5 = "da140af8b83ce7903915ee39973e36dd" +const expectedMD5 = "08aaa6fcfde3bb900fc54bdfef3d5c81" +const expectedMD5Black = "badac5ca5cfbdea9b98a1f9988ba54bc" func TestSVG(t *testing.T) { // create buffer of actual svg @@ -46,3 +47,35 @@ func TestSVG(t *testing.T) { t.Error(err) } } + +func TestSVGFromBlack(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) + } + mark := image.MarkSquares(color.RGBA{255, 255, 0, 1}, chess.D2, chess.D4) + per := image.Perspective(chess.Black) + if err := image.SVG(buf, pos.Board(), mark, 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 != expectedMD5Black { + t.Errorf("expected actual md5 hash to be %s but got %s", expectedMD5Black, actualMD5) + } + + // create actual svg file for visualization + f, err := os.Create("black_example.svg") + defer f.Close() + if err != nil { + t.Error(err) + } + if _, err := io.Copy(f, bytes.NewBufferString(actualSVG)); err != nil { + t.Error(err) + } +}