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

// 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",
}
)