~facundo/enjuewemela/trunk

« back to all changes in this revision

Viewing changes to cocos/layer/scrolling.py

  • Committer: facundo at com
  • Date: 2010-11-20 01:41:24 UTC
  • mto: This revision was merged to the branch mainline in revision 63.
  • Revision ID: facundo@taniquetil.com.ar-20101120014124-zjyxkchmvili5m2u
Project reorder!

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
'''This module defines the ScrollableLayer and ScrollingManager classes.
 
2
 
 
3
Controlling Scrolling
 
4
---------------------
 
5
 
 
6
You have two options for scrolling:
 
7
 
 
8
1. automatically scroll the map but stop at the map edges, and
 
9
2. scroll the map an allow the edge of the map to be displayed.
 
10
 
 
11
The ScrollingManager has a concept of "focus" which is the pixel
 
12
position of the player's view focus (*usually* the center of the
 
13
player sprite itself, but the player may be allowed to
 
14
move the view around, or you may move it around for them to highlight
 
15
something else in the scene). The ScrollingManager is clever enough to
 
16
manage many layers and handle scaling them.
 
17
 
 
18
Two methods are available for setting the map focus:
 
19
 
 
20
**set_focus(x, y)**
 
21
  Attempt to set the focus to the pixel coordinates given. The layer(s)
 
22
  contained in the ScrollingManager are moved accordingly. If a layer
 
23
  would be moved outside of its define px_width, px_height then the
 
24
  scrolling is restricted. The resultant restricted focal point is stored
 
25
  on the ScrollingManager as manager.fx and manager.fy.
 
26
 
 
27
**force_focus(x, y)**
 
28
  Force setting the focus to the pixel coordinates given. The layer(s)
 
29
  contained in the ScrollingManager are moved accordingly regardless of
 
30
  whether any out-of-bounds cells would be displayed. The .fx and .fy
 
31
  attributes are still set, but they'll *always* be set to the supplied
 
32
  x and y values.
 
33
'''
 
34
 
 
35
from cocos.director import director
 
36
from cocos.layer.base_layers import Layer
 
37
import pyglet
 
38
 
 
39
class ScrollableLayer(Layer):
 
40
    '''A Cocos Layer that is scrollable in a Scene.
 
41
 
 
42
    A layer may have a "parallax" value which is used to scale the position
 
43
    (and not the dimensions) of the view of the layer - the layer's view
 
44
    (x, y) coordinates are calculated as::
 
45
 
 
46
       my_view_x = parallax * passed_view_x
 
47
       my_view_y = parallax * passed_view_y
 
48
 
 
49
    Scrollable layers have a view which identifies the section of the layer
 
50
    currently visible.
 
51
 
 
52
    The scrolling is usually managed by a ScrollingManager.
 
53
    '''
 
54
    view_x, view_y = 0, 0
 
55
    view_w, view_h = 0, 0
 
56
    origin_x = origin_y = origin_z = 0
 
57
 
 
58
    def __init__(self, parallax=1):
 
59
        super(ScrollableLayer,self).__init__()
 
60
        self.parallax = parallax
 
61
 
 
62
        # force (cocos) transform anchor to be 0 so we don't OpenGL
 
63
        # glTranslate() and screw up our pixel alignment on screen
 
64
        self.transform_anchor_x = 0
 
65
        self.transform_anchor_y = 0
 
66
 
 
67
        # XXX batch eh?
 
68
        self.batch = pyglet.graphics.Batch()
 
69
 
 
70
    def set_view(self, x, y, w, h):
 
71
        x *= self.parallax
 
72
        y *= self.parallax
 
73
        self.view_x, self.view_y = x, y
 
74
        self.view_w, self.view_h = w, h
 
75
        x -= self.origin_x
 
76
        y -= self.origin_y
 
77
        self.position = (-x, -y)
 
78
 
 
79
    def draw(self):
 
80
        # invoked by Cocos machinery
 
81
        super(ScrollableLayer, self).draw()
 
82
 
 
83
        # XXX overriding draw eh?
 
84
        pyglet.gl.glPushMatrix()
 
85
        self.transform()
 
86
        self.batch.draw()
 
87
        pyglet.gl.glPopMatrix()
 
88
 
 
89
    def set_dirty(self):
 
90
        '''The viewport has changed in some way.
 
91
        '''
 
92
        pass
 
93
 
 
94
    is_event_handler = True
 
95
    def on_resize(self, width, height):
 
96
        self.view_w, self.view_h = width, height
 
97
        self.set_dirty()
 
98
 
 
99
class ScrollingManager(Layer):
 
100
    '''Manages scrolling of Layers in a Cocos Scene.
 
101
 
 
102
    Each ScrollableLayer that is added to this manager (via standard list
 
103
    methods) may have pixel dimensions .px_width and .px_height. Tile
 
104
    module MapLayers have these attribtues. The manager will limit scrolling
 
105
    to stay within the pixel boundary of the most limiting layer.
 
106
 
 
107
    If a layer has no dimensions it will scroll freely and without bound.
 
108
 
 
109
    The manager is initialised with the viewport (usually a Window) which has
 
110
    the pixel dimensions .width and .height which are used during focusing.
 
111
 
 
112
    A ScrollingManager knows how to convert pixel coordinates from its own
 
113
    pixel space to the screen space.
 
114
    '''
 
115
    def __init__(self, viewport=None):
 
116
        # initialise the viewport stuff
 
117
        if viewport is None:
 
118
            from cocos import director
 
119
            self.view_w, self.view_h = director.director.get_window_size()
 
120
        else:
 
121
            self.view_w, self.view_h = viewport.width, viewport.height
 
122
 
 
123
        # These variables define the Layer-space pixel view which is mapping
 
124
        # to the viewport. If the Layer is not scrolled or scaled then this
 
125
        # will be a one to one mapping.
 
126
        self.view_x, self.view_y = 0, 0
 
127
 
 
128
        # Focal point on the Layer
 
129
        self.fx = self.fy = 0
 
130
 
 
131
        super(ScrollingManager, self).__init__()
 
132
 
 
133
        # always transform about 0,0
 
134
        self.transform_anchor_x = 0
 
135
        self.transform_anchor_y = 0
 
136
 
 
137
    is_event_handler = True
 
138
    def on_resize(self, width, height):
 
139
        self.view_w, self.view_h = width, height
 
140
        if self.children:
 
141
            self.set_focus(self.fx, self.fy)
 
142
 
 
143
    _scale = 0
 
144
    def set_scale(self, scale):
 
145
        self._scale = scale
 
146
        self._old_focus = None      # disable NOP check
 
147
        if self.children:
 
148
            self.set_focus(self.fx, self.fy)
 
149
    scale = property(lambda s: s._scale, set_scale)
 
150
 
 
151
    def add(self, child, z=0, name=None):
 
152
        '''Add the child and then update the manager's focus / viewport.
 
153
        '''
 
154
        super(ScrollingManager, self).add(child, z=z, name=name)
 
155
        # set the focus again and force it so we don't just skip because the
 
156
        # focal point hasn't changed
 
157
        self.set_focus(self.fx, self.fy, force=True)
 
158
 
 
159
    def pixel_from_screen(self, x, y):
 
160
        '''Look up the Layer-space pixel matching the screen-space pixel.
 
161
 
 
162
        Account for viewport, layer and screen transformations.
 
163
        '''
 
164
        # director display scaling
 
165
        x, y = director.get_virtual_coordinates(x, y)
 
166
 
 
167
        # normalise x,y coord
 
168
        ww, wh = director.get_window_size()
 
169
        sx = x / ww
 
170
        sy = y / wh
 
171
 
 
172
        # get the map-space dimensions
 
173
        vx, vy, w, h = self.view_x, self.view_y, self.view_w, self.view_h
 
174
 
 
175
        #print (int(x), int(y)), (vx, vy, w, h), int(vx + sx * w), int(vy + sy * h)
 
176
 
 
177
        # convert screen pixel to map pixel
 
178
        return int(vx + sx * w), int(vy + sy * h)
 
179
 
 
180
    def pixel_to_screen(self, x, y):
 
181
        '''Look up the screen-space pixel matching the Layer-space pixel.
 
182
 
 
183
        Account for viewport, layer and screen transformations.
 
184
        '''
 
185
        raise NotImplementedError('do this some day')
 
186
        # scaling of layer
 
187
        x *= self.scale
 
188
        y *= self.scale
 
189
 
 
190
        # XXX rotation of layer
 
191
 
 
192
        # shift for viewport
 
193
        x += self.view_x
 
194
        y += self.view_y
 
195
 
 
196
        # XXX director display scaling
 
197
 
 
198
        return int(x), int(y)
 
199
 
 
200
    _old_focus = None
 
201
    def set_focus(self, fx, fy, force=False):
 
202
        '''Determine the viewport based on a desired focus pixel in the
 
203
        Layer space (fx, fy) and honoring any bounding restrictions of
 
204
        child layers.
 
205
 
 
206
        The focus will always be shifted to ensure no child layers display
 
207
        out-of-bounds data, as defined by their dimensions px_width and px_height.
 
208
        '''
 
209
        # if no child specifies dimensions then just force the focus
 
210
        if not [l for z,l in self.children if hasattr(l, 'px_width')]:
 
211
            return self.force_focus(fx, fy)
 
212
 
 
213
        # This calculation takes into account the scaling of this Layer (and
 
214
        # therefore also its children).
 
215
        # The result is that all chilren will have their viewport set, defining
 
216
        # which of their pixels should be visible.
 
217
 
 
218
        fx, fy = int(fx), int(fy)
 
219
 
 
220
        a = (fx, fy, self.scale)
 
221
 
 
222
        # check for NOOP (same arg passed in)
 
223
        if not force and self._old_focus == a:
 
224
            return
 
225
        self._old_focus = a
 
226
 
 
227
        # collate children dimensions
 
228
        x1 = []; y1 = []; x2 = []; y2 = []
 
229
        for z, layer in self.children:
 
230
            if not hasattr(layer, 'px_width'): continue
 
231
            x1.append(layer.origin_x)
 
232
            y1.append(layer.origin_y)
 
233
            x2.append(layer.origin_x + layer.px_width)
 
234
            y2.append(layer.origin_y + layer.px_height)
 
235
 
 
236
        # figure the child layer min/max bounds
 
237
        b_min_x = min(x1)
 
238
        b_min_y = min(y1)
 
239
        b_max_x = min(x2)
 
240
        b_max_y = min(y2)
 
241
 
 
242
        # get our viewport information, scaled as appropriate
 
243
        w = int(self.view_w / self.scale)
 
244
        h = int(self.view_h / self.scale)
 
245
        w2, h2 = w//2, h//2
 
246
 
 
247
        # check for the minimum, and then maximum bound
 
248
        if (fx - w2) < b_min_x:
 
249
            fx = b_min_x + w2       # hit minimum X extent
 
250
        elif (fx + w2) > b_max_x:
 
251
            fx = b_max_x - w2       # hit maximum X extent
 
252
        if (fy - h2) < b_min_y:
 
253
            fy = b_min_y + h2       # hit minimum Y extent
 
254
        elif (fy + h2) > b_max_y:
 
255
            fy = b_max_y - h2       # hit maximum Y extent
 
256
 
 
257
        # ... and this is our focus point, center of screen
 
258
        self.fx, self.fy = map(int, (fx, fy))
 
259
 
 
260
        # determine child view bounds to match that focus point
 
261
        x, y = int(fx - w2), int(fy - h2)
 
262
        self.view_x, self.view_y = x, y
 
263
        for z, layer in self.children:
 
264
            layer.set_view(x, y, w, h)
 
265
 
 
266
    def force_focus(self, fx, fy):
 
267
        '''Force the manager to focus on a point, regardless of any managed layer
 
268
        visible boundaries.
 
269
 
 
270
        '''
 
271
        # This calculation takes into account the scaling of this Layer (and
 
272
        # therefore also its children).
 
273
        # The result is that all chilren will have their viewport set, defining
 
274
        # which of their pixels should be visible.
 
275
 
 
276
        self.fx, self.fy = map(int, (fx, fy))
 
277
 
 
278
        # get our scaled view size
 
279
        w = int(self.view_w / self.scale)
 
280
        h = int(self.view_h / self.scale)
 
281
        cx, cy = w//2, h//2
 
282
 
 
283
        # bottom-left corner of the
 
284
        x, y = fx - cx * self.scale, fy - cy * self.scale
 
285
 
 
286
        self.view_x, self.view_y = x, y
 
287
 
 
288
        # translate the layers to match focus
 
289
        for z, layer in self.children:
 
290
            layer.set_view(x, y, w, h)
 
291