You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
4.7 KiB
168 lines
4.7 KiB
// Package image is a go library that creates images from board positions |
|
package image |
|
|
|
import ( |
|
"fmt" |
|
"image/color" |
|
"io" |
|
"strings" |
|
|
|
svg "github.com/ajstarks/svgo" |
|
"git.nightmare.haus/rudi/chessv2" |
|
"git.nightmare.haus/rudi/chessv2/image/internal" |
|
) |
|
|
|
// SVG writes the board SVG representation into the writer. |
|
// An error is returned if there is there is an error writing data. |
|
// SVG also takes options which can customize the image output. |
|
func SVG(w io.Writer, b *chess.Board, opts ...func(*encoder)) error { |
|
e := new(w, opts) |
|
return e.EncodeSVG(b) |
|
} |
|
|
|
// SquareColors is designed to be used as an optional argument |
|
// to the SVG function. It changes the default light and |
|
// dark square colors to the colors given. |
|
func SquareColors(light, dark color.Color) func(*encoder) { |
|
return func(e *encoder) { |
|
e.light = light |
|
e.dark = dark |
|
} |
|
} |
|
|
|
// 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 |
|
// previous move. |
|
func MarkSquares(c color.Color, sqs ...chess.Square) func(*encoder) { |
|
return func(e *encoder) { |
|
for _, sq := range sqs { |
|
e.marks[sq] = 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 |
|
} |
|
|
|
// New returns an encoder that writes to the given writer. |
|
// New also takes options which can customize the image |
|
// 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{}, |
|
} |
|
for _, op := range options { |
|
op(e) |
|
} |
|
return e |
|
} |
|
|
|
const ( |
|
sqWidth = 45 |
|
sqHeight = 45 |
|
boardWidth = 8 * sqWidth |
|
boardHeight = 8 * sqHeight |
|
) |
|
|
|
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} |
|
) |
|
|
|
// EncodeSVG writes the board SVG representation into |
|
// the Encoder's writer. An error is returned if there |
|
// is there is an error writing data. |
|
func (e *encoder) EncodeSVG(b *chess.Board) error { |
|
boardMap := b.SquareMap() |
|
canvas := svg.New(e.w) |
|
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 |
|
} |
|
} |
|
// 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() |
|
return nil |
|
} |
|
|
|
func (e *encoder) colorForSquare(sq chess.Square) color.Color { |
|
sqSum := int(sq.File()) + int(sq.Rank()) |
|
if sqSum%2 == 0 { |
|
return e.dark |
|
} |
|
return e.light |
|
} |
|
|
|
func (e *encoder) colorForText(sq chess.Square) color.Color { |
|
sqSum := int(sq.File()) + int(sq.Rank()) |
|
if sqSum%2 == 0 { |
|
return e.light |
|
} |
|
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)) |
|
} |
|
|
|
func pieceXML(x, y int, p chess.Piece) string { |
|
fileName := fmt.Sprintf("pieces/%s%s.svg", p.Color().String(), pieceTypeMap[p.Type()]) |
|
svgStr := string(internal.MustAsset(fileName)) |
|
old := `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">` |
|
new := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="%d %d 360 360">`, (-1 * x), (-1 * y)) |
|
return strings.Replace(svgStr, old, new, 1) |
|
} |
|
|
|
var ( |
|
pieceTypeMap = map[chess.PieceType]string{ |
|
chess.King: "K", |
|
chess.Queen: "Q", |
|
chess.Rook: "R", |
|
chess.Bishop: "B", |
|
chess.Knight: "N", |
|
chess.Pawn: "P", |
|
} |
|
)
|
|
|