1
# -*- coding: utf-8 -*-
12
from gettext import gettext as _
14
def parse_colour(colour):
15
assert colour.startswith('#')
16
assert len(colour) == 7
17
r = int(colour[1:3], 16) / 255.
18
g = int(colour[3:5], 16) / 255.
19
b = int(colour[5:7], 16) / 255.
22
def blend_colour(colour_a, colour_b, alpha):
23
a = parse_colour(colour_a)
24
b = parse_colour(colour_b)
25
r = a[0] * alpha + b[0] * (1 - alpha)
26
g = a[1] * alpha + b[1] * (1 - alpha)
27
b = a[2] * alpha + b[2] * (1 - alpha)
30
BORDER_COLOUR = parse_colour('#2e3436')
31
NUMBERING_COLOUR = parse_colour('#888a85')
32
BLACK_SQUARE_COLOURS = {None: parse_colour('#babdb6'),
33
glchess.scene.HIGHLIGHT_SELECTED: parse_colour('#73d216'),
34
glchess.scene.HIGHLIGHT_CAN_MOVE: parse_colour('#3465a4'),
35
glchess.scene.HIGHLIGHT_THREATENED: blend_colour('#af0000', '#babdb6', 0.2),
36
glchess.scene.HIGHLIGHT_CAN_TAKE: blend_colour('#af0000', '#babdb6', 0.8)}
37
WHITE_SQUARE_COLOURS = {None: parse_colour('#eeeeec'),
38
glchess.scene.HIGHLIGHT_SELECTED: parse_colour('#8ae234'),
39
glchess.scene.HIGHLIGHT_CAN_MOVE: parse_colour('#204a87'),
40
glchess.scene.HIGHLIGHT_THREATENED: blend_colour('#cc0000', '#eeeeec', 0.2),
41
glchess.scene.HIGHLIGHT_CAN_TAKE: blend_colour('#cc0000', '#eeeeec', 0.8)}
42
PIECE_COLOUR = parse_colour('#000000')
44
class ChessPiece(glchess.scene.ChessPiece):
48
def __init__(self, scene, name, coord, feedback, style='simple'):
52
self.feedback = feedback
54
self.coord = coord # Co-ordinate being moved to
55
self.pos = self.__coordToLocation(coord) # Current position
58
self.delete = False # Delete once moved to location
62
def __coordToLocation(self, coord):
65
rank = ord(coord[0]) - ord('a')
66
file = ord(coord[1]) - ord('1')
68
return (float(rank), float(file))
70
def setStyle(self, style):
72
self.path = os.path.join(glchess.defaults.BASE_DIR, 'pieces', style, self.name + '.svg')
74
self.svg = rsvg.Handle(file = self.path)
75
except Exception as e:
76
raise Exception('Error reading %s: %s' % (self.path, e))
78
def move(self, coord, delete, animate = True):
79
"""Extends glchess.scene.ChessPiece"""
81
self.scene.pieces.remove(self)
82
self.feedback.onDeleted()
87
self.targetPos = self.__coordToLocation(coord)
89
redraw = (self.pos != self.targetPos) or delete
91
# If not animating this move immediately
93
self.pos = self.targetPos
95
# If already there then check for deletion
96
if self.pos == self.targetPos:
99
self.scene.pieces.remove(self)
100
self.feedback.onDeleted()
102
self.scene.redrawStatic = True
103
self.scene.feedback.onRedraw()
106
# If not currently moving then start
108
self.scene._animationQueue.append(self)
111
# Remove piece from static scene
112
self.scene.redrawStatic = True
115
if self.scene.animating is False:
116
self.scene.animating = True
117
self.scene.feedback.startAnimation()
119
def animate(self, timeStep):
122
Return True if the piece has moved otherwise False.
124
if self.targetPos is None:
127
if self.pos == self.targetPos:
128
self.targetPos = None
130
self.scene.pieces.remove(self)
131
self.feedback.onDeleted()
134
# Get distance to target
135
dx = self.targetPos[0] - self.pos[0]
136
dy = self.targetPos[1] - self.pos[1]
138
# Get movement step in each direction
140
xStep = timeStep * SPEED
144
xStep *= cmp(dx, 0.0)
145
yStep = timeStep * SPEED
149
yStep *= cmp(dy, 0.0)
152
self.pos = (self.pos[0] + xStep, self.pos[1] + yStep)
155
def render(self, context):
158
offset = self.scene.PIECE_BORDER
159
matrix = context.get_matrix()
160
x = (self.pos[0] - 4) * self.scene.squareSize
161
y = (3 - self.pos[1]) * self.scene.squareSize
163
context.translate(x, y)
164
context.translate(self.scene.squareSize / 2, self.scene.squareSize / 2)
165
context.rotate(-self.scene.angle)
167
# If Face to Face mode is enabled, we rotate the black player's pieces by 180 degrees
168
if self.scene.faceToFace and self.name.find('black') != -1:
169
context.rotate(math.pi)
170
offset = - offset - self.scene.pieceSize + self.scene.squareSize
172
context.translate(-self.scene.squareSize / 2 + offset, -self.scene.squareSize / 2 + offset)
173
context.scale(self.scene.pieceSize/self.svg.props.width, self.scene.pieceSize/self.svg.props.height)
175
self.svg.render_cairo(context)
176
context.set_matrix(matrix)
178
class Scene(glchess.scene.Scene):
184
def __init__(self, feedback):
185
"""Constructor for a Cairo scene"""
186
self.feedback = feedback
189
self._animationQueue = []
191
self.targetAngle = 0.0
192
self.animating = False
193
self.redrawStatic = True
194
self.showNumbering = False
195
self.faceToFace = False
197
pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(glchess.defaults.SHARED_IMAGE_DIR, 'baize.png'))
198
(self.background_pixmap, _) = pixbuf.render_pixmap_and_mask()
199
except gobject.GError:
200
self.background_pixmap = None
202
def addChessPiece(self, chessSet, name, coord, feedback, style = 'simple'):
203
"""Add a chess piece model into the scene.
205
'chessSet' is the name of the chess set (string).
206
'name' is the name of the piece (string).
207
'coord' is the the chess board location of the piece in LAN format (string).
209
Returns a reference to this chess piece or raises an exception.
211
name = chessSet + name[0].upper() + name[1:]
212
piece = ChessPiece(self, name, coord, feedback, style=style)
213
self.pieces.append(piece)
216
self.redrawStatic = True
217
self.feedback.onRedraw()
221
def setBoardHighlight(self, coords):
222
"""Highlight a square on the board.
224
'coords' is a dictionary of highlight types keyed by square co-ordinates.
225
The co-ordinates are a tuple in the form (file,rank).
226
If None the highlight will be cleared.
228
self.redrawStatic = True
232
self.highlight = coords.copy()
233
self.feedback.onRedraw()
235
def showBoardNumbering(self, showNumbering):
236
"""Extends glchess.scene.Scene"""
237
self.showNumbering = showNumbering
238
self.redrawStatic = True
239
self.feedback.onRedraw()
241
def reshape(self, width, height):
242
"""Resize the viewport into the scene.
244
'width' is the width of the viewport in pixels.
245
'height' is the width of the viewport in pixels.
250
# Make the squares as large as possible while still pixel aligned
251
shortEdge = min(self.width, self.height)
252
self.squareSize = math.floor((shortEdge - 2.0*self.BORDER) / 9.0)
253
self.pieceSize = self.squareSize - 2.0*self.PIECE_BORDER
255
self.redrawStatic = True
256
self.feedback.onRedraw()
258
def setBoardRotation(self, angle, faceToFace = False, animate = True):
259
"""Extends glchess.scene.Scene"""
260
# Convert from degrees to radians
261
a = angle * math.pi / 180.0
265
if self.faceToFace != faceToFace:
267
self.faceToFace = faceToFace
277
# Start animation or redraw now
278
if animate and self.animating is False:
279
self.animating = True
280
self.feedback.startAnimation()
282
self.redrawStatic = True
283
self.feedback.onRedraw()
285
def setPiecesStyle(self, piecesStyle):
286
for piece in self.pieces:
287
piece.setStyle(piecesStyle)
288
self.redrawStatic = True
289
self.feedback.onRedraw()
291
def animate(self, timeStep):
292
"""Extends glchess.scene.Scene"""
293
if self.angle == self.targetAngle and len(self._animationQueue) == 0:
298
if self.angle != self.targetAngle:
299
offset = self.targetAngle - self.angle
301
offset += 2 * math.pi
302
step = timeStep * math.pi * 0.7
304
self.angle = self.targetAngle
307
if self.angle > 2 * math.pi:
308
self.angle -= 2 * math.pi
309
self.redrawStatic = True
313
for piece in self._animationQueue:
314
if piece.animate(timeStep):
317
assert(animationQueue.count(piece) == 0)
318
animationQueue.append(piece)
320
# Redraw static scene once pieces stop
323
self.redrawStatic = True
326
# Notify higher classes
327
piece.feedback.onMoved()
329
self._animationQueue = animationQueue
330
self.animating = len(self._animationQueue) > 0 or self.angle != self.targetAngle
333
self.feedback.onRedraw()
335
return self.animating
337
def __rotate(self, context):
340
context.translate(self.width / 2, self.height / 2)
342
# Shrink board so it is always visible:
345
# +-----------------+
353
# +-----------------+
355
# To calculate the scaling factor compare lengths a-c and b-c
357
# Rotation angle (r) is angle between a-c and b-c.
359
# Make two triangles:
362
# `. \ | r = angle rotated
363
# `. \ | r' = 45deg - r
364
# `. y\ | z z/x = cos(45) = 1/sqrt(2)
365
# x `. \ | z/y = cos(r') = cos(45 - r)
367
# `.\ | s = y/x = 1 / (sqrt(2) * cos(45 - r)
371
# Finally clip the angles so the board does not expand
372
# in the middle of the rotation.
377
if a > math.pi * 3 / 4:
378
a = math.pi / 4 - (a - (math.pi * 3 / 4))
381
s = 1.0 / (math.sqrt(2) * math.cos(math.pi / 4 - a))
385
context.rotate(self.angle);
387
def renderStatic(self, context, background_color):
388
"""Render the static elements in a scene.
390
if self.redrawStatic is False:
392
self.redrawStatic = False
394
context.set_source_rgb(*background_color)
398
self.__rotate(context)
401
context.set_source_rgb(*BORDER_COLOUR)
402
borderSize = math.ceil(self.squareSize * 4.5)
403
context.rectangle(-borderSize, -borderSize, borderSize * 2, borderSize * 2)
407
if self.showNumbering:
408
context.set_source_rgb(*NUMBERING_COLOUR)
409
context.set_font_size(self.squareSize * 0.4)
410
context.select_font_face("sans-serif", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
412
def drawCenteredText(x, y, text):
413
(_, _, w, h, _, _) = context.text_extents('b')
414
matrix = context.get_matrix()
415
context.translate(x, y)
416
context.rotate(-self.angle)
417
context.move_to(-w/2, h/2)
418
context.show_text(text)
419
context.set_matrix(matrix)
424
drawCenteredText(offset - self.squareSize * 3.5, -self.squareSize * 4.25, glchess.chess.translate_file(f))
425
drawCenteredText(offset - self.squareSize * 3.5, self.squareSize * 4.25, glchess.chess.translate_file(f))
426
drawCenteredText(-self.squareSize * 4.25, offset - self.squareSize * 3.5, glchess.chess.translate_rank(r))
427
drawCenteredText(self.squareSize * 4.25, offset - self.squareSize * 3.5, glchess.chess.translate_rank(r))
428
offset += self.squareSize
433
x = (i - 4) * self.squareSize
434
y = (3 - j) * self.squareSize
436
coord = chr(ord('a') + i) + chr(ord('1') + j)
438
highlight = self.highlight[coord]
442
context.rectangle(x, y, self.squareSize, self.squareSize)
444
colour = BLACK_SQUARE_COLOURS[highlight]
446
colour = WHITE_SQUARE_COLOURS[highlight]
447
context.set_source_rgb(*colour)
450
context.set_source_rgb(*PIECE_COLOUR)
451
for piece in self.pieces:
454
piece.render(context)
458
def renderDynamic(self, context):
459
"""Render the dynamic elements in a scene.
461
This requires a Cairo context.
464
self.__rotate(context)
466
context.set_source_rgb(*PIECE_COLOUR)
467
for piece in self.pieces:
468
# If not rotating and piece not moving then was rendered in the static phase
469
if self.angle == self.targetAngle and not piece.moving:
471
piece.render(context)
473
def getSquare(self, x, y):
474
"""Find the chess square at a given 2D location.
476
'x' is the number of pixels from the left of the scene to select.
477
'y' is the number of pixels from the bottom of the scene to select.
479
Return the co-ordinate in LAN format (string) or None if no square at this point.
481
# FIXME: Should use cairo rotation matrix but we don't have a context here
482
if self.angle != self.targetAngle:
485
boardWidth = self.squareSize * 9.0
486
offset = ((self.width - boardWidth) / 2.0 + self.squareSize * 0.5, (self.height - boardWidth) / 2.0 + self.squareSize * 0.5)
488
rank = (x - offset[0]) / self.squareSize
489
if rank < 0 or rank >= 8.0:
493
file = (y - offset[1]) / self.squareSize
494
if file < 0 or file >= 8.0:
499
if self.angle == math.pi:
503
# Convert from co-ordinates to LAN format
504
rank = chr(ord('a') + rank)
505
file = chr(ord('1') + file)