/zoeplat

To get this branch, use:
bzr branch /bzr/zoeplat

« back to all changes in this revision

Viewing changes to zoetrope/core/view.lua

  • Committer: Josh C
  • Date: 2013-03-02 20:40:57 UTC
  • Revision ID: josh@9ix.org-20130302204057-yrra0a51zgtpq2v2
zoetrope 1.3.1

Show diffs side-by-side

added added

removed removed

 
1
-- Class: View
 
2
-- A view is a group that packages several useful objects with it.
 
3
-- It's helpful to use, but not required. When a view is created, it
 
4
-- automatically sets the.view for itself. the.view should be considered
 
5
-- a read-only reference. If you want to switch views, you *must* set
 
6
-- the app's view property instead.
 
7
--
 
8
-- Extends:
 
9
--              <Group>
 
10
 
 
11
View = Group:extend{
 
12
        -- Property: timer
 
13
        -- A built-in <Timer> object for use as needed.
 
14
 
 
15
        -- Property: tween
 
16
        -- A built-in <Tween> object for use as needed.
 
17
 
 
18
        -- Property: factory
 
19
        -- A built-in <Factory> object for use as needed.
 
20
 
 
21
        -- Property: focus
 
22
        -- A <Sprite> to keep centered onscreen.
 
23
 
 
24
        -- Property: focusOffset
 
25
        -- This shifts the view of the focus, if one is set. If both
 
26
        -- x and y properties are set to 0, then the view keeps the focus
 
27
        -- centered onscreen.
 
28
        focusOffset = { x = 0, y = 0 },
 
29
 
 
30
        -- Property: minVisible
 
31
        -- The view clamps its scrolling so that nothing above or to the left
 
32
        -- of these x and y coordinates is visible.
 
33
        minVisible = { x = -math.huge, y = -math.huge },
 
34
 
 
35
        -- Property: maxVisible
 
36
        -- This view clamps its scrolling so that nothing below or to the right
 
37
        -- of these x and y coordinates is visible.
 
38
        maxVisible = { x = math.huge, y = math.huge },
 
39
 
 
40
        -- private property: _tint
 
41
        -- used to implement tints.
 
42
 
 
43
        -- private property: _fx
 
44
        -- used to perform fades and flashes.
 
45
 
 
46
        new = function (self, obj)
 
47
                obj = self:extend(obj)
 
48
 
 
49
                obj.timer = Timer:new()
 
50
                obj:add(obj.timer)
 
51
                obj.tween = Tween:new()
 
52
                obj:add(obj.tween)
 
53
                obj.factory = Factory:new()
 
54
 
 
55
                -- set the.view briefly, so that during the onNew() handler
 
56
                -- we appear to be the current view
 
57
        
 
58
                local oldView = the.view
 
59
 
 
60
                the.view = obj
 
61
                if obj.onNew then obj:onNew() end
 
62
 
 
63
                -- then reset it so that nothing breaks for the remainder
 
64
                -- of the frame for the old, outgoing view members.
 
65
                -- our parent app will restore us into the.view at the top of the next frame
 
66
                -- exception: there was no old view.
 
67
 
 
68
                if oldView then the.view = oldView end
 
69
                return obj
 
70
        end,
 
71
 
 
72
        -- Method: loadLayers
 
73
        -- Loads layers from a Lua source file (as generated by Tiled -- http://mapeditor.org).
 
74
        -- Each layer is created as a <Group> and added to preserve its ordering. Tile layers
 
75
        -- are created as <Map> instances; object layers will try to create instances of a class
 
76
        -- named by the object's name property. If no class exists by this name, or the object
 
77
        -- has no name property, a gray fill will be created instead, as a placeholder. If the
 
78
        -- object has a property named _the, then this will set the.[whatever] to it.
 
79
        --
 
80
        -- Arguments:
 
81
        --              file - filename to load
 
82
        --
 
83
        -- Returns:
 
84
        --              nothing
 
85
 
 
86
        loadLayers = function (self, file)
 
87
                local ok, data = pcall(loadstring(Cached:text(file)))
 
88
                local _, _, directory = string.find(file, '^(.*[/\\])')
 
89
                directory = directory or ''
 
90
 
 
91
                if ok then
 
92
                        -- store tile properties by gid
 
93
                        
 
94
                        local tileProtos = {}
 
95
 
 
96
                        for _, tileset in pairs(data.tilesets) do
 
97
                                for _, tile in pairs(tileset.tiles) do
 
98
                                        local id = tileset.firstgid + tile.id
 
99
                                        
 
100
                                        for key, value in pairs(tile.properties) do
 
101
                                                tile.properties[key] = tovalue(value)
 
102
                                        end
 
103
 
 
104
                                        tileProtos[id] = tile
 
105
                                        tileProtos[id].width = tileset.tilewidth
 
106
                                        tileProtos[id].height = tileset.tileheight
 
107
                                end
 
108
                        end
 
109
 
 
110
                        for _, layer in pairs(data.layers) do
 
111
                                if View[layer.name] then
 
112
                                        error('the View class reserves the ' .. layer.name .. ' property for its own use; you cannot load a layer with that name')
 
113
                                end
 
114
 
 
115
                                if STRICT and self[layer.name] then
 
116
                                        local info = debug.getinfo(2, 'Sl')
 
117
                                        print('Warning: a property named ' .. layer.name .. ' already exists in the current view (' ..
 
118
                                                  info.short_src .. ', line ' .. info.currentline .. ')')
 
119
                                end
 
120
 
 
121
                                if layer.type == 'tilelayer' then
 
122
                                        local map = Map:new{ spriteWidth = data.tilewidth, spriteHeight = data.tileheight }
 
123
                                        map:empty(layer.width, layer.height)
 
124
 
 
125
                                        -- load tiles
 
126
 
 
127
                                        for _, tiles in pairs(data.tilesets) do
 
128
                                                map:loadTiles(directory .. tiles.image, Tile, tiles.firstgid)
 
129
 
 
130
                                                -- and mix in properties where applicable
 
131
 
 
132
                                                for id, tile in pairs(tileProtos) do
 
133
                                                        if map.sprites[id] then
 
134
                                                                map.sprites[id]:mixin(tile.properties)
 
135
                                                        end
 
136
                                                end
 
137
                                        end
 
138
 
 
139
                                        -- load tile data
 
140
 
 
141
                                        local x = 1
 
142
                                        local y = 1
 
143
 
 
144
                                        for _, val in ipairs(layer.data) do
 
145
                                                map.map[x][y] = val
 
146
                                                x = x + 1
 
147
 
 
148
                                                if x > layer.width then
 
149
                                                        x = 1
 
150
                                                        y = y + 1
 
151
                                                end
 
152
                                        end
 
153
 
 
154
                                        self[layer.name] = map
 
155
                                        self:add(map)
 
156
                                elseif layer.type == 'objectgroup' then
 
157
                                        local group = Group:new()
 
158
 
 
159
                                        for _, obj in pairs(layer.objects) do
 
160
                                                -- roll in tile properties if based on a tile
 
161
 
 
162
                                                if obj.gid and tileProtos[obj.gid] then
 
163
                                                        local tile = tileProtos[obj.gid]
 
164
 
 
165
                                                        obj.name = tile.properties.name
 
166
                                                        obj.width = tile.width
 
167
                                                        obj.height = tile.height
 
168
 
 
169
                                                        for key, value in pairs(tile.properties) do
 
170
                                                                obj.properties[key] = tovalue(value)
 
171
                                                        end
 
172
 
 
173
                                                        -- Tiled tile-based objects measure their y
 
174
                                                        -- position at their lower-left corner, instead
 
175
                                                        -- of their upper-left corner as usual
 
176
 
 
177
                                                        obj.y = obj.y - obj.height
 
178
                                                end
 
179
 
 
180
                                                -- create a new object if the class does exist
 
181
 
 
182
                                                local spr
 
183
 
 
184
                                                if _G[obj.name] then
 
185
                                                        obj.properties.x = obj.x
 
186
                                                        obj.properties.y = obj.y
 
187
                                                        obj.properties.width = obj.width
 
188
                                                        obj.properties.height = obj.height
 
189
 
 
190
                                                        spr = _G[obj.name]:new(obj.properties)
 
191
                                                else
 
192
                                                        spr = Fill:new{ x = obj.x, y = obj.y, width = obj.width, height = obj.height, fill = { 128, 128, 128 } }
 
193
                                                end
 
194
 
 
195
                                                if obj.properties._the then
 
196
                                                        the[obj.properties._the] = spr
 
197
                                                end
 
198
 
 
199
                                                group:add(spr)
 
200
                                        end
 
201
 
 
202
                                        self[layer.name] = group
 
203
                                        self:add(group)
 
204
                                else
 
205
                                        error("don't know how to create a " .. layer.type .. " layer from file data")
 
206
                                end
 
207
                        end
 
208
                else
 
209
                        error('could not load view data from file: ' .. data)
 
210
                end
 
211
        end,
 
212
 
 
213
        -- Method: clampTo
 
214
        -- Clamps the view so that it never scrolls past a sprite's boundaries.
 
215
        -- This only looks at the sprite's position at this instant in time,
 
216
        -- not afterwards.
 
217
        --
 
218
        -- Arguments:
 
219
        --              sprite - sprite to clamp to
 
220
        --
 
221
        -- Returns:
 
222
        --              nothing
 
223
 
 
224
        clampTo = function (self, sprite)
 
225
                self.minVisible.x = sprite.x
 
226
                
 
227
                if sprite.x + sprite.width > the.app.width then
 
228
                        self.maxVisible.x = sprite.x + sprite.width
 
229
                else
 
230
                        self.maxVisible.x = the.app.width
 
231
                end
 
232
                
 
233
                self.minVisible.y = sprite.y
 
234
                
 
235
                if sprite.y + sprite.height > the.app.height then
 
236
                        self.maxVisible.y = sprite.y + sprite.height
 
237
                else
 
238
                        self.maxVisible.y = the.app.height
 
239
                end
 
240
        end,
 
241
 
 
242
        -- Method: panTo
 
243
        -- Pans the view so that the target sprite or position is centered
 
244
        -- onscreen. This sets the view's focus to nil.
 
245
        --
 
246
        -- Arguments:
 
247
        --              target - sprite or coordinate pair to pan to
 
248
        --              duration - how long the pan will take, in seconds
 
249
        --              ease - what easing to apply, see <Tween> for details, defaults to 'quadInOut'
 
250
        --
 
251
        -- Returns:
 
252
        --              A <Promise> that is fulfilled when the pan completes.
 
253
 
 
254
        panTo = function (self, target, duration, ease)
 
255
                ease = ease or 'quadInOut'
 
256
                local targetX, targetY
 
257
 
 
258
                if STRICT then
 
259
                        assert((target.x and target.y and target.width and target.height) or (#target == 2),
 
260
                                   'pan target does not appear to be a sprite or coordinate pair')
 
261
                        assert(type(duration) == 'number', 'pan duration is not a number')
 
262
                        assert(self.tween.easers[ease], 'pan easing method ' .. ease .. ' is not defined')
 
263
                end
 
264
 
 
265
                if target.x and target.y and target.width and target.height then
 
266
                        targetX = target.x + target.width / 2
 
267
                        targetY = target.y + target.height / 2
 
268
                else
 
269
                        targetX = target[1]
 
270
                        targetY = target[2]
 
271
                end
 
272
 
 
273
                -- calculate translation to center these coordinates
 
274
 
 
275
                local tranX = math.floor(-targetX + the.app.width / 2)
 
276
                local tranY = math.floor(-targetY + the.app.height / 2)
 
277
                
 
278
                -- clamp translation to min and max visible
 
279
                
 
280
                if tranX > - self.minVisible.x then tranX = - self.minVisible.x end
 
281
                if tranY > - self.minVisible.y then tranY = - self.minVisible.y end
 
282
                
 
283
                if tranX < the.app.width - self.maxVisible.x then
 
284
                        tranX = the.app.width - self.maxVisible.x
 
285
                end
 
286
                
 
287
                if tranY < the.app.height - self.maxVisible.y then
 
288
                        tranY = the.app.height - self.maxVisible.y
 
289
                end
 
290
 
 
291
                -- tween the appropriate properties
 
292
                -- some care has to be taken to avoid fulfilling the promise twice
 
293
 
 
294
                self.focus = nil
 
295
                local promise = Promise:new()
 
296
 
 
297
                if tranX ~= self.translate.x then
 
298
                        self.tween:start(self.translate, 'x', tranX, duration, ease)
 
299
                                :andThen(function() promise:fulfill() end)
 
300
 
 
301
                        if tranY ~= self.translate.y then
 
302
                                self.tween:start(self.translate, 'y', tranY, duration, ease)
 
303
                        end
 
304
                elseif tranY ~= self.translate.y then
 
305
                        self.tween:start(self.translate, 'y', tranY, duration, ease)
 
306
                                :andThen(function() promise:fulfill() end)
 
307
                else
 
308
                        promise:fulfill()
 
309
                end
 
310
 
 
311
                return promise
 
312
        end,
 
313
 
 
314
        -- Method: fade
 
315
        -- Fades out to a specified color over a period of time.
 
316
        --
 
317
        -- Arguments:
 
318
        --              color - color table to fade to, e.g. { 0, 0, 0 }
 
319
        --              duration - how long to fade out in seconds, default 1
 
320
        --
 
321
        -- Returns:
 
322
        --              A <Promise> that is fulfilled when the effect completes.
 
323
 
 
324
        fade = function (self, color, duration)
 
325
                assert(type(color) == 'table', 'color to fade to is ' .. type(color) .. ', not a table')
 
326
                local alpha = color[4] or 255
 
327
                self._fx = color
 
328
                self._fx[4] = 0
 
329
                return self.tween:start(self._fx, 4, alpha, duration or 1, 'quadOut')
 
330
        end,
 
331
 
 
332
        -- Method: flash
 
333
        -- Immediately flashes the screen to a specific color, then fades out.
 
334
        --
 
335
        -- Arguments:
 
336
        --              color - color table to flash, e.g. { 0, 0, 0 }
 
337
        --              duration - how long to restore normal view in seconds, default 1
 
338
        --
 
339
        -- Returns:
 
340
        --              A <Promise> that is fulfilled when the effect completes.
 
341
 
 
342
        flash = function (self, color, duration)
 
343
                assert(type(color) == 'table', 'color to flash is ' .. type(color) .. ', not a table')
 
344
                color[4] = color[4] or 255
 
345
                self._fx = color
 
346
                return self.tween:start(self._fx, 4, 0, duration or 1, 'quadOut')
 
347
        end,
 
348
 
 
349
        -- Method: tint
 
350
        -- Immediately tints the screen a color. To restore normal viewing,
 
351
        -- call this method again with no arguments.
 
352
        --
 
353
        -- Arguments:
 
354
        --              red - red component, 0-255
 
355
        --              green - green component, 0-255
 
356
        --              blue - blue component, 0-255
 
357
        --              alpha - alpha, 0-255, default 255
 
358
        --
 
359
        -- Returns:
 
360
        --              nothing
 
361
 
 
362
        tint = function (self, red, green, blue, alpha)
 
363
                alpha = alpha or 255
 
364
 
 
365
                if red and green and blue and alpha > 0 then
 
366
                        self._tint = { red, green, blue, alpha }
 
367
                else
 
368
                        self._tint = nil
 
369
                end
 
370
        end,
 
371
 
 
372
        update = function (self, elapsed)
 
373
                local screenWidth = the.app.width
 
374
                local screenHeight = the.app.height
 
375
 
 
376
                -- follow the focused sprite
 
377
                
 
378
                if self.focus and self.focus.width < screenWidth
 
379
                   and self.focus.height < screenHeight then
 
380
                        self.translate.x = math.floor(- (self.focus.x + self.focusOffset.x) +
 
381
                                                           (screenWidth - self.focus.width) / 2)
 
382
                        self.translate.y = math.floor(- (self.focus.y + self.focusOffset.y) +
 
383
                                                           (screenHeight - self.focus.height) / 2)
 
384
                end
 
385
                
 
386
                -- clamp translation to min and max visible
 
387
                
 
388
                if self.translate.x > - self.minVisible.x then
 
389
                        self.translate.x = - self.minVisible.x
 
390
                end
 
391
 
 
392
                if self.translate.y > - self.minVisible.y then
 
393
                        self.translate.y = - self.minVisible.y
 
394
                end
 
395
                
 
396
                if self.translate.x < screenWidth - self.maxVisible.x then
 
397
                        self.translate.x = screenWidth - self.maxVisible.x
 
398
                end
 
399
                
 
400
                if self.translate.y < screenHeight - self.maxVisible.y then
 
401
                        self.translate.y = screenHeight - self.maxVisible.y
 
402
                end
 
403
 
 
404
                Group.update(self, elapsed)
 
405
        end,
 
406
 
 
407
        draw = function (self, x, y)
 
408
                Group.draw(self, x, y)
 
409
 
 
410
                -- draw our fx and tint on top of everything
 
411
 
 
412
                if self._tint then
 
413
                        love.graphics.setColor(self._tint)
 
414
                        love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height)
 
415
                        love.graphics.setColor(255, 255, 255, 255)
 
416
                end
 
417
 
 
418
                if self._fx then
 
419
                        love.graphics.setColor(self._fx)
 
420
                        love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height)
 
421
                        love.graphics.setColor(255, 255, 255, 255)
 
422
                end
 
423
        end,
 
424
 
 
425
        __tostring = function (self)
 
426
                local result = 'View ('
 
427
 
 
428
                if self.active then
 
429
                        result = result .. 'active'
 
430
                else
 
431
                        result = result .. 'inactive'
 
432
                end
 
433
 
 
434
                if self.visible then
 
435
                        result = result .. ', visible'
 
436
                else
 
437
                        result = result .. ', invisible'
 
438
                end
 
439
 
 
440
                if self.solid then
 
441
                        result = result .. ', solid'
 
442
                else
 
443
                        result = result .. ', not solid'
 
444
                end
 
445
 
 
446
                return result .. ', ' .. self:count(true) .. ' sprites)'
 
447
        end
 
448
}