~ubuntu-branches/ubuntu/precise/gnome-games/precise-proposed

« back to all changes in this revision

Viewing changes to glchess/src/lib/scene/cairo/__init__.py

  • Committer: Package Import Robot
  • Author(s): Rodrigo Moya
  • Date: 2011-05-30 13:32:04 UTC
  • mfrom: (1.3.4)
  • mto: (163.1.3 precise)
  • mto: This revision was merged to the branch mainline in revision 143.
  • Revision ID: package-import@ubuntu.com-20110530133204-celaq1v1dsxc48q1
Tags: upstream-3.0.2
ImportĀ upstreamĀ versionĀ 3.0.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
import os.path
3
 
 
4
 
import math
5
 
import cairo
6
 
import rsvg
7
 
import gobject
8
 
import gtk.gdk
9
 
 
10
 
import glchess.scene
11
 
 
12
 
from gettext import gettext as _
13
 
 
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.
20
 
    return (r, g, b)
21
 
 
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)
28
 
    return (r, g, b)
29
 
 
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')
43
 
 
44
 
class ChessPiece(glchess.scene.ChessPiece):
45
 
    """
46
 
    """
47
 
 
48
 
    def __init__(self, scene, name, coord, feedback, style='simple'):
49
 
        """
50
 
        """
51
 
        self.scene = scene
52
 
        self.feedback = feedback
53
 
        self.name = name
54
 
        self.coord = coord # Co-ordinate being moved to
55
 
        self.pos = self.__coordToLocation(coord) # Current position
56
 
        self.targetPos   = None
57
 
        self.moving = False
58
 
        self.delete = False # Delete once moved to location
59
 
 
60
 
        self.setStyle(style)
61
 
        
62
 
    def __coordToLocation(self, coord):
63
 
        """
64
 
        """
65
 
        rank = ord(coord[0]) - ord('a')
66
 
        file = ord(coord[1]) - ord('1')
67
 
        
68
 
        return (float(rank), float(file))
69
 
 
70
 
    def setStyle(self, style):
71
 
        self.style = style
72
 
        self.path = os.path.join(glchess.defaults.BASE_DIR, 'pieces', style, self.name + '.svg')
73
 
        try:
74
 
            self.svg = rsvg.Handle(file = self.path)
75
 
        except Exception as e:
76
 
            raise Exception('Error reading %s: %s' % (self.path, e))
77
 
 
78
 
    def move(self, coord, delete, animate = True):
79
 
        """Extends glchess.scene.ChessPiece"""
80
 
        if not coord:
81
 
            self.scene.pieces.remove(self)
82
 
            self.feedback.onDeleted()
83
 
            return
84
 
        
85
 
        self.coord = coord
86
 
        self.delete = delete
87
 
        self.targetPos = self.__coordToLocation(coord)
88
 
 
89
 
        redraw = (self.pos != self.targetPos) or delete
90
 
 
91
 
        # If not animating this move immediately
92
 
        if not animate:
93
 
            self.pos = self.targetPos
94
 
        
95
 
        # If already there then check for deletion
96
 
        if self.pos == self.targetPos:
97
 
            self.targetPos = None
98
 
            if delete:
99
 
                self.scene.pieces.remove(self)
100
 
                self.feedback.onDeleted()
101
 
            if redraw:
102
 
                self.scene.redrawStatic = True
103
 
                self.scene.feedback.onRedraw()
104
 
            return
105
 
 
106
 
        # If not currently moving then start
107
 
        if not self.moving:
108
 
            self.scene._animationQueue.append(self)
109
 
            self.moving = True
110
 
 
111
 
            # Remove piece from static scene
112
 
            self.scene.redrawStatic = True            
113
 
            
114
 
            # Start animation
115
 
            if self.scene.animating is False:
116
 
                self.scene.animating = True
117
 
                self.scene.feedback.startAnimation()
118
 
 
119
 
    def animate(self, timeStep):
120
 
        """
121
 
        
122
 
        Return True if the piece has moved otherwise False.
123
 
        """
124
 
        if self.targetPos is None:
125
 
            return False
126
 
        
127
 
        if self.pos == self.targetPos:
128
 
            self.targetPos = None
129
 
            if self.delete:
130
 
                self.scene.pieces.remove(self)
131
 
                self.feedback.onDeleted()
132
 
            return False
133
 
        
134
 
        # Get distance to target
135
 
        dx = self.targetPos[0] - self.pos[0]
136
 
        dy = self.targetPos[1] - self.pos[1]
137
 
        
138
 
        # Get movement step in each direction
139
 
        SPEED = 4.0 # FIXME
140
 
        xStep = timeStep * SPEED
141
 
        if xStep > abs(dx):
142
 
            xStep = dx
143
 
        else:
144
 
            xStep *= cmp(dx, 0.0)
145
 
        yStep = timeStep * SPEED
146
 
        if yStep > abs(dy):
147
 
            yStep = dy
148
 
        else:
149
 
            yStep *= cmp(dy, 0.0)
150
 
 
151
 
        # Move the piece
152
 
        self.pos = (self.pos[0] + xStep, self.pos[1] + yStep)
153
 
        return True
154
 
    
155
 
    def render(self, context):
156
 
        """
157
 
        """
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
162
 
 
163
 
        context.translate(x, y)
164
 
        context.translate(self.scene.squareSize / 2, self.scene.squareSize / 2)
165
 
        context.rotate(-self.scene.angle)
166
 
 
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
171
 
        
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)
174
 
 
175
 
        self.svg.render_cairo(context)
176
 
        context.set_matrix(matrix)
177
 
 
178
 
class Scene(glchess.scene.Scene):
179
 
    """
180
 
    """    
181
 
    BORDER = 6.0
182
 
    PIECE_BORDER = 0.0
183
 
 
184
 
    def __init__(self, feedback):
185
 
        """Constructor for a Cairo scene"""
186
 
        self.feedback        = feedback
187
 
        self.highlight       = {}
188
 
        self.pieces          = []
189
 
        self._animationQueue = []
190
 
        self.angle           = 0.0
191
 
        self.targetAngle     = 0.0
192
 
        self.animating       = False
193
 
        self.redrawStatic    = True
194
 
        self.showNumbering   = False
195
 
        self.faceToFace      = False
196
 
        try:
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
201
 
 
202
 
    def addChessPiece(self, chessSet, name, coord, feedback, style = 'simple'):
203
 
        """Add a chess piece model into the scene.
204
 
        
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).
208
 
        
209
 
        Returns a reference to this chess piece or raises an exception.
210
 
        """
211
 
        name = chessSet + name[0].upper() + name[1:]
212
 
        piece = ChessPiece(self, name, coord, feedback, style=style)
213
 
        self.pieces.append(piece)
214
 
        
215
 
        # Redraw the scene
216
 
        self.redrawStatic = True
217
 
        self.feedback.onRedraw()
218
 
        
219
 
        return piece
220
 
 
221
 
    def setBoardHighlight(self, coords):
222
 
        """Highlight a square on the board.
223
 
        
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.
227
 
        """
228
 
        self.redrawStatic = True
229
 
        if coords is None:
230
 
            self.highlight = {}
231
 
        else:
232
 
            self.highlight = coords.copy()
233
 
        self.feedback.onRedraw()
234
 
 
235
 
    def showBoardNumbering(self, showNumbering):
236
 
        """Extends glchess.scene.Scene"""
237
 
        self.showNumbering = showNumbering
238
 
        self.redrawStatic = True
239
 
        self.feedback.onRedraw()
240
 
 
241
 
    def reshape(self, width, height):
242
 
        """Resize the viewport into the scene.
243
 
        
244
 
        'width' is the width of the viewport in pixels.
245
 
        'height' is the width of the viewport in pixels.
246
 
        """
247
 
        self.width = width
248
 
        self.height = height
249
 
        
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
254
 
 
255
 
        self.redrawStatic = True
256
 
        self.feedback.onRedraw()
257
 
 
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
262
 
        
263
 
        redraw = False
264
 
 
265
 
        if self.faceToFace != faceToFace:
266
 
            redraw = True
267
 
            self.faceToFace = faceToFace
268
 
        
269
 
        if self.angle == a:
270
 
            animate = False
271
 
        else:
272
 
            self.targetAngle = a
273
 
            if not animate:
274
 
                self.angle = a
275
 
                redraw = True
276
 
 
277
 
        # Start animation or redraw now
278
 
        if animate and self.animating is False:
279
 
            self.animating = True
280
 
            self.feedback.startAnimation()
281
 
        elif redraw:
282
 
            self.redrawStatic = True
283
 
            self.feedback.onRedraw()
284
 
 
285
 
    def setPiecesStyle(self, piecesStyle):
286
 
        for piece in self.pieces:
287
 
            piece.setStyle(piecesStyle)
288
 
        self.redrawStatic = True
289
 
        self.feedback.onRedraw()
290
 
    
291
 
    def animate(self, timeStep):
292
 
        """Extends glchess.scene.Scene"""
293
 
        if self.angle == self.targetAngle and len(self._animationQueue) == 0:
294
 
            return False
295
 
 
296
 
        redraw = False
297
 
        
298
 
        if self.angle != self.targetAngle:
299
 
            offset = self.targetAngle - self.angle
300
 
            if offset < 0:
301
 
                offset += 2 * math.pi
302
 
            step = timeStep * math.pi * 0.7
303
 
            if step > offset:
304
 
                self.angle = self.targetAngle
305
 
            else:
306
 
                self.angle += step
307
 
                if self.angle > 2 * math.pi:
308
 
                    self.angle -= 2 * math.pi
309
 
            self.redrawStatic = True
310
 
            redraw = True
311
 
        
312
 
        animationQueue = []
313
 
        for piece in self._animationQueue:
314
 
            if piece.animate(timeStep):
315
 
                piece.moving = True
316
 
                redraw = True
317
 
                assert(animationQueue.count(piece) == 0)
318
 
                animationQueue.append(piece)
319
 
            else:
320
 
                # Redraw static scene once pieces stop
321
 
                if piece.moving:
322
 
                    redraw = True
323
 
                    self.redrawStatic = True
324
 
                piece.moving = False
325
 
 
326
 
                # Notify higher classes
327
 
                piece.feedback.onMoved()
328
 
 
329
 
        self._animationQueue = animationQueue
330
 
        self.animating = len(self._animationQueue) > 0 or self.angle != self.targetAngle
331
 
 
332
 
        if redraw:
333
 
            self.feedback.onRedraw()
334
 
 
335
 
        return self.animating
336
 
    
337
 
    def __rotate(self, context):
338
 
        """
339
 
        """
340
 
        context.translate(self.width / 2, self.height / 2)
341
 
        
342
 
        # Shrink board so it is always visible:
343
 
        #
344
 
        # a  b
345
 
        # +-----------------+
346
 
        # |  |    ```----__ |
347
 
        # |  |             ||
348
 
        # | ,`            ,`|
349
 
        # | |      o      | |
350
 
        # |,`      c      | |
351
 
        # ||___          ,` |
352
 
        # |    ----...   |  |
353
 
        # +-----------------+
354
 
        #
355
 
        # To calculate the scaling factor compare lengths a-c and b-c
356
 
        #
357
 
        # Rotation angle (r) is angle between a-c and b-c.
358
 
        #
359
 
        # Make two triangles:
360
 
        #
361
 
        #   +------+-------+
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)
366
 
        #            `.r\r'|
367
 
        #              `.\ |  s = y/x = 1 / (sqrt(2) * cos(45 - r)
368
 
        #                `\|
369
 
        #                              QED
370
 
        #
371
 
        # Finally clip the angles so the board does not expand
372
 
        # in the middle of the rotation.
373
 
        a = self.angle
374
 
        if a > math.pi:
375
 
            a -= math.pi
376
 
        if a > math.pi / 4:
377
 
            if a > math.pi * 3 / 4:
378
 
                a = math.pi / 4 - (a - (math.pi * 3 / 4))
379
 
            else:
380
 
                a = math.pi / 4
381
 
        s = 1.0 / (math.sqrt(2) * math.cos(math.pi / 4 - a))
382
 
        
383
 
        context.scale(s, s)
384
 
        
385
 
        context.rotate(self.angle);
386
 
 
387
 
    def renderStatic(self, context, background_color):
388
 
        """Render the static elements in a scene.
389
 
        """
390
 
        if self.redrawStatic is False:
391
 
            return False
392
 
        self.redrawStatic = False
393
 
 
394
 
        context.set_source_rgb(*background_color)
395
 
        context.paint()
396
 
        
397
 
        # Rotate board
398
 
        self.__rotate(context)
399
 
        
400
 
        # Draw border
401
 
        context.set_source_rgb(*BORDER_COLOUR)
402
 
        borderSize = math.ceil(self.squareSize * 4.5)
403
 
        context.rectangle(-borderSize, -borderSize, borderSize * 2, borderSize * 2)
404
 
        context.fill()
405
 
 
406
 
        # Draw numbering
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)
411
 
            
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)
420
 
            offset = 0
421
 
            for i in xrange(8):
422
 
                f = 'abcdefgh'[i]
423
 
                r = '87654321'[i]
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
429
 
        
430
 
        # Draw squares
431
 
        for i in xrange(8):
432
 
            for j in xrange(8):
433
 
                x = (i - 4) * self.squareSize
434
 
                y = (3 - j) * self.squareSize
435
 
                
436
 
                coord = chr(ord('a') + i) + chr(ord('1') + j)
437
 
                try:
438
 
                    highlight = self.highlight[coord]
439
 
                except KeyError:
440
 
                    highlight = None
441
 
                
442
 
                context.rectangle(x, y, self.squareSize, self.squareSize)
443
 
                if (i + j) % 2 == 0:
444
 
                    colour = BLACK_SQUARE_COLOURS[highlight]
445
 
                else:
446
 
                    colour = WHITE_SQUARE_COLOURS[highlight]
447
 
                context.set_source_rgb(*colour)
448
 
                context.fill()
449
 
                
450
 
        context.set_source_rgb(*PIECE_COLOUR)
451
 
        for piece in self.pieces:
452
 
            if piece.moving:
453
 
                continue
454
 
            piece.render(context)
455
 
        
456
 
        return True
457
 
 
458
 
    def renderDynamic(self, context):
459
 
        """Render the dynamic elements in a scene.
460
 
        
461
 
        This requires a Cairo context.
462
 
        """
463
 
        # Rotate board
464
 
        self.__rotate(context)
465
 
 
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:
470
 
                continue
471
 
            piece.render(context)
472
 
 
473
 
    def getSquare(self, x, y):
474
 
        """Find the chess square at a given 2D location.
475
 
        
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.
478
 
        
479
 
        Return the co-ordinate in LAN format (string) or None if no square at this point.
480
 
        """
481
 
        # FIXME: Should use cairo rotation matrix but we don't have a context here
482
 
        if self.angle != self.targetAngle:
483
 
            return None
484
 
        
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)
487
 
        
488
 
        rank = (x - offset[0]) / self.squareSize
489
 
        if rank < 0 or rank >= 8.0:
490
 
            return None
491
 
        rank = int(rank)
492
 
 
493
 
        file = (y - offset[1]) / self.squareSize
494
 
        if file < 0 or file >= 8.0:
495
 
            return None
496
 
        file = 7 - int(file)
497
 
        
498
 
        # FIXME: See above
499
 
        if self.angle == math.pi:
500
 
            rank = 7 - rank
501
 
            file = 7 - file
502
 
 
503
 
        # Convert from co-ordinates to LAN format
504
 
        rank = chr(ord('a') + rank)
505
 
        file = chr(ord('1') + file)
506
 
 
507
 
        return rank + file