#include "BoardCanvas.hpp" #define TRANS(PT) { \ wxPoint2DDouble tmp((PT).x,(PT).y); \ tmp=rot_m.TransformPoint(tmp); \ (PT).x=tmp.m_x+xsrc;(PT).y=tmp.m_y+ysrc;} #define TRANS2(PT) { \ (PT).x+=xsrc;(PT).y+=ysrc;} wxDEFINE_EVENT(PLAY_MOVE_EVENT, wxCommandEvent); BoardCanvas::BoardCanvas(wxFrame *parent) : wxPanel(parent), black_side(false), frozen(false), lock_square_size(false), t(new Theme()), t_captures(new Theme()) { is_dragging = false; valid_drag = false; valid_arrow = false; // Init animation data adata.duration=200; adata.duration_fast=80; adata.fps=30; adata.buffer=new wxBitmap(500,500,32); adata.animate=false; // Init game state gs.is_black_turn=false; gs.board = "rnbqkbnrpppppppp PPPPPPPPRNBQKBNR"; gs.mat_black=false; gs.mat_white=false; // Init clocks SetClockTime(-1, -1, -1, false); SetClockTime(-1, -1, -1, true); // Init capture pieces t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR); // Load preferences ApplyPreferences(); // The following should be called when using an EVT_PAINT handler SetBackgroundStyle(wxBG_STYLE_PAINT); // Let GameTableLeftPanel process keyboard events: Bind(wxEVT_KEY_DOWN, [p=this](wxKeyEvent &e){e.ResumePropagation(1);e.Skip();}); Bind(wxEVT_KEY_UP, [p=this](wxKeyEvent &e){e.ResumePropagation(1);e.Skip();}); Bind(wxEVT_PAINT, &BoardCanvas::OnPaint, this); // Mouse events: Bind(wxEVT_MOTION, &BoardCanvas::MouseEvent, this); Bind(wxEVT_LEFT_DOWN, &BoardCanvas::MouseEvent, this); Bind(wxEVT_LEFT_UP, &BoardCanvas::MouseEvent, this); Bind(wxEVT_RIGHT_DOWN, &BoardCanvas::MouseEvent, this); Bind(wxEVT_RIGHT_UP, &BoardCanvas::MouseEvent, this); Bind(wxEVT_RIGHT_DCLICK, &BoardCanvas::MouseEvent, this); } BoardCanvas::~BoardCanvas() { delete t; delete t_captures; if(adata.buffer!= nullptr) free(adata.buffer); } BoardCanvas::BoardCanvas(wxFrame *parent, std::uint32_t square_width, bool frozen) : BoardCanvas(parent) { t->ResizeSquaresAndPieces(square_width); t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR); this->frozen = true; lock_square_size = true; } void BoardCanvas::OnPaint(wxPaintEvent &event) { wxBufferedPaintDC dc(this); dc.SetBackground(*wxWHITE_BRUSH); dc.Clear(); if(!adata.animate){ // Setting up required attributes REFRESH_MOUSE_LOCATION(); square_width = t->GetSquaresSizes(); piece_width = t->GetPiecesSizes(); canvas_size = this->GetSize(); boardX = (canvas_size.x - (8 * square_width)) / 2; boardY = (canvas_size.y - (8 * square_width)) / 2; if (boardX > canvas_size.x) boardX = 0; if (boardY > canvas_size.y) boardY = 0; DrawBoard(dc); } else { // Reuse buffer and animate dc.DrawBitmap(*adata.buffer, 0, 0, true); double percent=adata.frame/adata.frames; // Draw moving piece dc.DrawBitmap(*t->Get(adata.piece_moved), adata.src.x + adata.frame*(adata.transVect.x/adata.frames), adata.src.y + adata.frame*(adata.transVect.y/adata.frames), false); // End drawing adata.frame++; } } void BoardCanvas::ApplyPreferences() { if (t != nullptr) delete t; if (t_captures != nullptr) delete t_captures; t = new Theme(); t_captures = new Theme(); CONFIG_OPEN(config); black_side = config->Read("board/black_by_default", false); if (lock_square_size) { t->ResizeSquaresAndPieces(square_width); } else { t->ResizeSquaresAndPieces(config->Read("board/square_size", 80)); } t->SetSquareRadius(config->Read("board/corner_radius", 10)); t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR); CONFIG_CLOSE(config); Refresh(); } void BoardCanvas::SetupBoard(const GameState &new_gs) { gs.board = new_gs.board; gs.is_black_turn = new_gs.is_black_turn; gs.captures = new_gs.captures; gs.white=new_gs.white; gs.black=new_gs.black; gs.mat_white=new_gs.mat_white; gs.mat_black=new_gs.mat_black; gs.black_time=new_gs.black_time; gs.white_time=new_gs.white_time; gs.squares_hl=new_gs.squares_hl; gs.arrows=new_gs.arrows; Refresh(); } void BoardCanvas::Animate(const GameState &new_gs, const std::string &src, const std::string &dst, bool faster){ std::uint8_t pfile = src[0]-'a'; std::uint8_t prank = src[1]-'1'; adata.piece_moved = gs.board[pfile + 8 * (7-prank)]; // Piece to move // Now init adata.buffer and remove the piece that will be moved gs.board[pfile + 8 * (7-prank)]=' '; wxSize buffer_size=adata.buffer->GetSize(); if(canvas_size!=buffer_size){ wxLogDebug("Allocate a new animation buffer"); free(adata.buffer); adata.buffer=new wxBitmap(canvas_size.x,canvas_size.y,32); } wxMemoryDC memDC(*adata.buffer); memDC.SetBackground(*wxWHITE_BRUSH); memDC.Clear(); DrawBoard(memDC); // Now compute piece start position and translation vector std::uint32_t centrer_offset = (square_width - piece_width) / 2; if (!black_side) { prank = 7 - prank; pfile = 7 - pfile; } std::uint32_t x = boardX + (7 - pfile) * square_width; std::uint32_t y = boardY + prank * square_width; adata.src.x = x + centrer_offset; adata.src.y = y + centrer_offset; // Now dst: pfile = dst[0]-'a'; prank = dst[1]-'1'; if (!black_side) { prank = 7 - prank; pfile = 7 - pfile; } x = boardX + (7 - pfile) * square_width; y = boardY + prank * square_width; adata.transVect.x=x-adata.src.x+centrer_offset; adata.transVect.y=y-adata.src.y+centrer_offset; // Start animation: adata.animate=true; int duration_backup=adata.duration; adata.duration=faster ? adata.duration_fast : adata.duration; int frame_duration=(1000/adata.fps); adata.frame=0; adata.frames=adata.duration/frame_duration; // total number of frames int time_per_frame=adata.duration/adata.frames; wxStopWatch sw; for(int i=adata.frames;i>0;i--){ Refresh(); Update(); int delay=sw.Time()-frame_duration; if(delay>5){ // 5ms tolerance wxMilliSleep(delay); } sw.Start(0); } adata.duration=faster ? duration_backup : duration_backup; adata.animate=false; SetupBoard(new_gs); } void BoardCanvas::DrawBoard(wxDC &dc) { std::uint32_t centrer_offset = (square_width - piece_width) / 2; wxSize numbers_size=dc.GetTextExtent("0"); bool DrawDraggingPiece = false; char dp = 'p'; std::uint32_t dpx = 0, dpy = 0; for (std::int8_t file = 7; file >= 0; file--) { for (std::uint8_t rank = 0; rank <= 7; rank++) { std::uint32_t x = boardX + (7 - file) * square_width; std::uint32_t y = boardY + rank * square_width; if ((file + rank) % 2 == 0) { if (file == 0 && rank == 0) { dc.DrawBitmap(*t->Get('1'), x, y, true); } else if (file == 7 && rank == 7) { dc.DrawBitmap(*t->Get('2'), x, y, true); } else { dc.DrawBitmap(*t->Get('s'), x, y, true); } } else { if (file == 7 && rank == 0) { dc.DrawBitmap(*t->Get('0'), x, y, true); } else if (file == 0 && rank == 7) { dc.DrawBitmap(*t->Get('3'), x, y, true); } else { dc.DrawBitmap(*t->Get('S'), x, y, true); } } // Draw highlighted squares for(const std::string &s:gs.squares_hl){ std::uint8_t sfile = s[0]-'a'; std::uint8_t srank = s[1]-'1'; if (!black_side) { srank = 7 - srank; sfile = 7 - sfile; } if(srank == rank && sfile==file){ char type='a'; if(s.size()>2) type=s[2]; // Default highlight (type='a' or something else not supported) dc.SetPen(wxNullPen); dc.SetBrush(wxColour(255,0,0,110)); if(type=='b') dc.SetBrush(wxColour(0,255,0,110)); else if(type=='c') dc.SetBrush(wxColour(0,0,255,110)); else if(type=='d') dc.SetBrush(wxColour(255,0,0,50)); else if(type=='e') dc.SetBrush(wxColour(0,255,0,50)); else if(type=='f') dc.SetBrush(wxColour(0,0,255,50)); dc.DrawRectangle(wxRect(x,y,square_width,square_width)); } } // Draw numbers dc.SetFont(wxFont(*wxNORMAL_FONT).MakeBold()); if(file==7){ // Right numbers dc.DrawText(wxString((char)('1'+7-rank)), x-numbers_size.x*1.5,y+square_width/2-numbers_size.y/2); } if(rank==7){ // Bottom numbers dc.DrawText(wxString((char)('a'+7-file)), x+square_width/2-numbers_size.x/2,y+square_width); } // Draw pieces std::uint8_t prank = rank; std::uint8_t pfile = file; if (black_side) { prank = 7 - rank; pfile = 7 - file; } std::uint32_t px = x + centrer_offset; std::uint32_t py = y + centrer_offset; char piece = gs.board[(7 - pfile) + 8 * prank]; if (is_dragging && (7 - pfile) == active_square.x && (7 - prank) == active_square.y) { dp = piece; dpx = px - (lastClickX - mouseX); dpy = py - (lastClickY - mouseY); DrawDraggingPiece = true; continue; } if (piece != ' ') { dc.DrawBitmap(*t->Get(piece), px, py, false); if((piece == 'k' && gs.mat_black) || (piece == 'K' && gs.mat_white)) dc.DrawBitmap(*t->Get('#'), x+square_width/2+centrer_offset, y+centrer_offset, false); } } } // Draw badge dc.SetPen(wxPen(*wxBLACK, 3)); std::uint32_t badgeY = boardY; std::uint32_t badgeWidth = square_width / 2; if (gs.is_black_turn) { dc.SetBrush(*wxBLACK_BRUSH); if (black_side) { badgeY = boardY + (8 * square_width) - badgeWidth; } } else { dc.SetBrush(*wxWHITE_BRUSH); if (!black_side) { badgeY = boardY + (8 * square_width) - badgeWidth; } } wxRect badge(boardX + (8 * square_width) + badgeWidth/2, badgeY, badgeWidth, badgeWidth); dc.DrawRectangle(badge); // Draw captures (+player names) first for white then for black std::uint32_t captures_size = t_captures->GetPiecesSizes(); std::uint8_t padding = 10; std::uint32_t offsetX = 0; std::uint32_t offsetY = -(captures_size + padding); std::uint32_t offsetYPlayerName=offsetY-captures_size-padding; // For top player name we // add -padding at the end (ortherwise bottom of the letters // are to close to captured pieces) // White (black's captures): if (black_side) { offsetY = 8 * square_width + padding + numbers_size.y; offsetYPlayerName = offsetY+captures_size; } for (char p : {'P', 'N', 'B', 'R', 'Q'}) { if (gs.captures.find(p) != gs.captures.end()) { for (std::uint8_t i = 0; i < gs.captures[p]; i++) { dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY); offsetX += captures_size / 2; } offsetX += captures_size / 2; } } dc.DrawText(wxString(gs.black),boardX,boardY + offsetYPlayerName); // Black (white's captures): offsetX = 0; if (black_side) { offsetY = -(captures_size + padding); offsetYPlayerName = offsetY-captures_size-padding; // Same for -padding (cf. for black's captures) } else { offsetY = 8 * square_width + padding + numbers_size.y; offsetYPlayerName = offsetY+captures_size; } for (char p : {'p', 'n', 'b', 'r', 'q'}) { if (gs.captures.find(p) != gs.captures.end()) { for (std::uint8_t i = 0; i < gs.captures[p]; i++) { dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY); offsetX += captures_size / 2; } offsetX += captures_size / 2; } } dc.DrawText(wxString(gs.white),boardX,boardY + offsetYPlayerName); // Draw dragging piece if (DrawDraggingPiece) { dc.DrawBitmap(*t->Get(dp), dpx, dpy, false); } // Draw numbers for (char l = 'a'; l < 'a' + 8; l++) { dc.DrawText(wxString(l), wxPoint(boardX + l - 'a' * square_width, boardY + 8 * square_width + 10)); } // Draw Clocks if (std::get<0>(gs.black_time) >= 0) { wxFont font = dc.GetFont(); ClockTime clock = black_side ? gs.white_time : gs.black_time; wxString time = wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock)); if (std::get<0>(clock) > 0) { time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock)); } else if (std::get<1>(clock) > 0) { time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock)); } dc.SetFont(font.Scale(1.5).MakeBold()); wxCoord width, height; dc.GetTextExtent(time, &width, &height); dc.DrawText(time, wxPoint(boardX + square_width * 8 - width, boardY - height - numbers_size.y*2)); clock = black_side ? gs.black_time : gs.white_time; time = wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock)); if (std::get<0>(clock) > 0) { time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock)); } else if (std::get<1>(clock) > 0) { time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock)); } dc.GetTextExtent(time, &width, &height); dc.DrawText(time, wxPoint(boardX + square_width * 8 - width, boardY + square_width * 8 + numbers_size.y*2)); } // Draw arrows for(const std::string arrow: gs.arrows){ std::uint8_t sfile = arrow[0]-'a'; std::uint8_t srank = arrow[1]-'1'; std::uint8_t dfile = arrow[2]-'a'; std::uint8_t drank = arrow[3]-'1'; if (!black_side) { srank = 7 - srank; sfile = 7 - sfile; drank = 7 - drank; dfile = 7 - dfile; } std::uint32_t sx = boardX + (7 - sfile) * square_width + square_width/2; std::uint32_t sy = boardY + srank * square_width + square_width/2; std::uint32_t dx = boardX + (7 - dfile) * square_width + square_width/2; std::uint32_t dy = boardY + drank * square_width + square_width/2; if(((abs(drank-srank) == 2) && (abs(dfile-sfile) == 1))|| ((abs(drank-srank) == 1) && (abs(dfile-sfile) == 2))){ if(abs(drank-srank) == 1){ DrawLArrow(dc,sx,sy,dx,dy); } else { DrawLArrow(dc,sx,sy,dx,dy,true); } } else { DrawArrow(dc,sx,sy,dx,dy); } } } void BoardCanvas::MouseEvent(wxMouseEvent &event) { if (!frozen) { if (event.Dragging()) { if (valid_drag) { is_dragging = true; Refresh(); } } else if ((valid_arrow && event.RightUp()) || event.RightDClick()) { valid_arrow=false; // Handle drop REFRESH_MOUSE_LOCATION(); INIT_CURRENT_SQUARE(); if (IsCurrentSquareValid) { std::string src=((char)('a' + active_square.x)) + std::to_string(+active_square.y + 1); std::string dst=((char)('a' + file)) + std::to_string(rank + 1); if(src!=dst){ gs.arrows.push_back(src+dst); wxLogDebug("Draw arrow %s",src+dst); } else { if(std::count(gs.squares_hl.begin(), gs.squares_hl.end(), src)){ gs.squares_hl.erase(std::remove(gs.squares_hl.begin(), gs.squares_hl.end(), src), gs.squares_hl.end()); }else{ gs.squares_hl.push_back(src); wxLogDebug("Highlight square %s",src); } } Refresh(); } } else { if (is_dragging) { is_dragging = false; valid_drag = false; // Handle drop REFRESH_MOUSE_LOCATION(); INIT_CURRENT_SQUARE(); if (IsCurrentSquareValid) { wxLogDebug("Drag end on square (%d,%d)", file, rank); /// Play the move wxCommandEvent event(PLAY_MOVE_EVENT, GetId()); event.SetEventObject(this); std::string move = ((char)('a' + active_square.x)) + std::to_string(+active_square.y + 1) + ((char)('a' + file)) + std::to_string(rank + 1); event.SetString(move); ProcessEvent(event); } else { // If square not valid just redraw (place piece back to its square) Refresh(); } } if (event.LeftDown() || event.RightDown()) { SetFocus(); REFRESH_MOUSE_LOCATION(); lastClickX = mouseX; lastClickY = mouseY; INIT_CURRENT_SQUARE(); if (IsCurrentSquareValid) { active_square.x = file; active_square.y = rank; if(event.LeftDown()){ if (gs.board[(7 - rank) * 8 + file] != ' ') { wxLogDebug("Drag start on square (%d,%d)", file, rank); valid_drag = true; } } else { wxLogDebug("Arrow start on square (%d,%d)", file, rank); valid_arrow = true; } } } else if(event.LeftUp()){ gs.arrows.clear(); gs.squares_hl.clear(); Refresh(); } } } // Let GameTableLeftPanel process mouse wheel events: if (event.GetWheelRotation() != 0) { event.ResumePropagation(1);event.Skip(); } } void BoardCanvas::Zoom(std::int32_t zoom) { if(!t->Zoom(zoom)) return; t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR); Refresh(); } void BoardCanvas::Swap() { black_side = !black_side; Refresh(); } void BoardCanvas::SetClockTime(short hours, short min, short sec, bool IsBlack) { if (IsBlack) { gs.black_time = std::make_tuple(hours, min, sec); } else { gs.white_time = std::make_tuple(hours, min, sec); } } void BoardCanvas::DrawArrow(wxDC &dc, int xsrc, int ysrc, int xdst, int ydst){ // Compute arrow length and angle wxPoint vect(xdst-xsrc,ydst-ysrc); double length=ceil(sqrt(pow(vect.x,2)+pow(vect.y,2))); double angle=acos(vect.x/length); angle= (vect.y>0) ? angle=angle : -angle; // Compute metrics std::uint8_t thickness=50; std::uint8_t tip_height=sqrt(3)/2*thickness; std::uint32_t tail_length=length-tip_height; // Setup transform matrix wxAffineMatrix2D rot_m; rot_m.Rotate(angle); wxLogDebug("Angle is %d,%d",(int)vect.x,(int)vect.y); // Init polygons wxPoint tip[]={wxPoint(tail_length,-thickness/2),wxPoint(tail_length,thickness/2),wxPoint(tail_length+tip_height,0)}; wxPoint tail[]={wxPoint(0,-thickness/4),wxPoint(tail_length,-thickness/4),wxPoint(tail_length,thickness/4),wxPoint(0,thickness/4)}; // Transform points TRANS(tip[0]);TRANS(tip[1]);TRANS(tip[2]); TRANS(tail[0]);TRANS(tail[1]);TRANS(tail[2]);TRANS(tail[3]); // Draw dc.SetPen(wxNullPen); dc.SetBrush(wxColour(255,0,0)); dc.DrawPolygon(3,tip); dc.DrawPolygon(4,tail); } void BoardCanvas::DrawLArrow(wxDC &dc, int xsrc, int ysrc, int xdst, int ydst, bool flip){ double X=xdst-xsrc; double Y=ydst-ysrc; // Check if L arrows are required if(X==0 || Y==0){ DrawArrow(dc, xsrc, ysrc, xdst, ydst); return; } if(flip){ // Compute metrics std::uint8_t thickness=50; double tip_height=sqrt(3)/2*thickness; double yoffset=Y<0 ? -thickness/4 : thickness/4; // Y tail should be longer to form a right angle (corner of tail joins) if(X<0){tip_height=-tip_height;} wxPoint tip[]={wxPoint(X-tip_height,-thickness/2+Y),wxPoint(X,Y),wxPoint(X-tip_height,thickness/2+Y)}; wxPoint tail1[]={wxPoint(-thickness/4,0),wxPoint(thickness/4,0),wxPoint(thickness/4,Y+yoffset),wxPoint(-thickness/4,Y+yoffset)}; wxPoint tail2[]={wxPoint(0,Y-thickness/4),wxPoint(X-tip_height,Y-thickness/4),wxPoint(X-tip_height,Y+thickness/4),wxPoint(0,Y+thickness/4)}; // Apply transforms TRANS2(tip[0]);TRANS2(tip[1]);TRANS2(tip[2]); TRANS2(tail1[0]);TRANS2(tail1[1]);TRANS2(tail1[2]);TRANS2(tail1[3]); TRANS2(tail2[0]);TRANS2(tail2[1]);TRANS2(tail2[2]);TRANS2(tail2[3]); // Draw dc.SetPen(wxNullPen); dc.SetBrush(wxColour(255,0,0)); dc.DrawPolygon(4,tail1); dc.DrawPolygon(4,tail2); dc.DrawPolygon(3,tip); } else { // Compute metrics std::uint8_t thickness=50; double tip_height=sqrt(3)/2*thickness; double xoffset=X<0 ? -thickness/4 : thickness/4; // Y tail should be longer to form a right angle (corner of tail joins) if(Y<0){tip_height=-tip_height;} wxPoint tip[]={wxPoint(-thickness/2 + X,Y-tip_height),wxPoint(thickness/2 + X ,Y-tip_height),wxPoint(X,Y)}; wxPoint tail1[]={wxPoint(0,-thickness/4),wxPoint(X+xoffset,-thickness/4),wxPoint(X+xoffset,thickness/4),wxPoint(0,thickness/4)}; wxPoint tail2[]={wxPoint(X-thickness/4,0),wxPoint(X+thickness/4,0),wxPoint(X+thickness/4,Y-tip_height),wxPoint(X-thickness/4,Y-tip_height)}; // Apply transforms TRANS2(tip[0]);TRANS2(tip[1]);TRANS2(tip[2]); TRANS2(tail1[0]);TRANS2(tail1[1]);TRANS2(tail1[2]);TRANS2(tail1[3]); TRANS2(tail2[0]);TRANS2(tail2[1]);TRANS2(tail2[2]);TRANS2(tail2[3]); // Draw dc.SetPen(wxNullPen); dc.SetBrush(wxColour(255,0,0)); dc.DrawPolygon(4,tail1); dc.DrawPolygon(4,tail2); dc.DrawPolygon(3,tip); } }