1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
|
-- Class: App
-- An app is where all the magic happens. :) It contains a
-- view, the group where all major action happens, as well as the
-- meta view, which persists across views. Only one app may run at
-- a time.
--
-- An app's job is to get things up and running -- most of its logic
-- lives in its onRun handler, but for a simple app, you can also
-- use the onUpdate handler instead of writing a custom <View>.
--
-- Once an app has begun running, it may be accessed globally via
-- <the>.app.
--
-- Extends:
-- <Class>
--
-- Event: onRun
-- Called once, when the app begins running.
--
-- Event: onEnterFullscreen
-- Called after entering fullscreen successfully.
--
-- Event: onExitFullscreen
-- Called after exiting fullscreen successfully.
App = Class:extend
{
-- Property: name
-- This is shown in the window title bar.
name = 'Zoetrope',
-- Property: icon
-- A path to an image to use as the window icon (a 32x32 PNG is recommended).
-- This doesn't affect the actual executable's icon in the taskbar or dock.
-- Property: fps
-- Maximum frames per second requested. In practice, your
-- FPS may vary from frame to frame. Every event handler (e.g. onUpdate)
-- is passed the exact elapsed time in seconds.
fps = 60,
-- Property: timeScale
-- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
timeScale = 1,
-- Property: active
-- If false, nothing receives update-related events, including the meta view.
-- These events specifically are onStartFrame, onUpdate, and onEndFrame.
active = true,
-- Property: deactivateOnBlur
-- Should the app automatically set its active property to false when its
-- window loses focus?
deactivateOnBlur = true,
-- Property: view
-- The current <View>. When the app is running, this is also accessible
-- globally via <the>.view. In order to switch views, you must set this
-- property, *not* <the>.view.
-- Property: meta
-- A <Group> that persists across all views during the app's lifespan.
-- Property: keys
-- A <Keys> object that listens to the keyboard. When the app is running, this
-- is also accessible globally via <the>.keys.
-- Property: width
-- The width of the app's canvas in pixels. Changing this value has no effect. To
-- set this for real, edit conf.lua. This may *not* correspond to the overall
-- resolution of the window when in fullscreen mode.
-- Property: height
-- The height of the window in pixels. Changing this value has no effect. To
-- set this for real, edit conf.lua. This may *not* correspond to the overall
-- resolution of the window when in fullscreen mode.
-- Property: fullscreen
-- Whether the app is currently running in fullscreen mode. Changing this value
-- has no effect. To change this, use the enterFullscreen(), exitFullscreen(), or
-- toggleFullscreen() methods.
-- Property: inset
-- The screen coordinates where the app's (0, 0) origin should lie. This is only
-- used by Zoetrope to either letterbox or pillar fullscreen apps, but there may
-- be other uses for it.
inset = { x = 0, y = 0},
-- internal property: _sleepTime
-- The amount of time the app intentionally slept for, and should not
-- be counted against elapsed time.
-- internal property: _nextFrameTime
-- Set at the start of a frame to be the next time a frame should be rendered,
-- in timestamp format. This is used to properly maintain FPS -- see the example
-- in https://love2d.org/wiki/love.timer.sleep for how this works.
new = function (self, obj)
obj = self:extend(obj)
-- set icon if possible
if self.icon then
love.graphics.setIcon(Cached:image(self.icon))
end
-- view containers
obj.meta = obj.meta or Group:new()
obj.view = obj.view or View:new()
-- input
if love.keyboard then
obj.keys = obj.keys or Keys:new()
love.keyboard.setKeyRepeat(0.4, 0.04)
obj.meta:add(obj.keys)
end
if love.mouse then
obj.mouse = obj.mouse or Mouse:new()
obj.meta:add(obj.mouse)
end
if love.joystick then
obj.gamepads = {}
the.gamepads = obj.gamepads
end
if obj.numGamepads and obj.numGamepads > 0 then
for i = 1, obj.numGamepads do
obj.gamepads[i] = Gamepad:new{ number = i }
obj.meta:add(obj.gamepads[i])
end
end
-- screen dimensions and state
obj.width, obj.height, obj.fullscreen = love.graphics.getMode()
-- housekeeping
the.app = obj
if obj.onNew then obj:onNew() end
return obj
end,
-- Method: run
-- Starts the app running. Nothing will occur until this call.
--
-- Arguments:
-- none
--
-- Returns:
-- nothing
run = function (self)
math.randomseed(os.time())
-- sync the.view
the.view = self.view
-- attach debug console
if DEBUG then
self.console = DebugConsole:new()
self.meta:add(self.console)
end
-- set up callbacks
love.graphics.setCaption(self.name)
love.update = function (elapsed) self:update(elapsed) end
love.draw = function() self:draw() end
love.focus = function (value) self:onFocus(value) end
if self.onRun then self:onRun() end
self._nextFrameTime = love.timer.getMicroTime()
end,
-- Method: quit
-- Quits the application immediately.
--
-- Arguments:
-- none
--
-- Returns:
-- nothing
quit = function (self)
love.event.quit()
end,
-- Method: useSysCursor
-- Shows or hides the system mouse cursor.
--
-- Arguments:
-- value - show the cursor?
--
-- Returns:
-- nothing
useSysCursor = function (self, value)
if STRICT then
assert(value == true or value == false,
'tried to set system cursor visibility to ' .. type(value))
end
love.mouse.setVisible(value)
end,
-- Method: enterFullscreen
-- Enters fullscreen mode. If the app is already in fullscreen, this has no effect.
-- This tries to use the highest resolution that will not result in distortion, and
-- adjust the app's offset property to accomodate this.
--
-- Arguments:
-- hint - whether to try to letterbox (vertical black bars) or pillar the app
-- (horizontal black bars). You don't need to specify this; the method
-- will try to infer based on the aspect ratio of your app.
--
-- Returns:
-- boolean whether this succeeded
enterFullscreen = function (self, hint)
if STRICT then
assert(not self.fullscreen, 'asked to enter fullscreen when already in fullscreen')
end
local modes = love.graphics.getModes()
if not hint then
if self.width * 9 == self.height * 16 then
hint = 'letterbox'
elseif self.width * 3 == self.height * 4 then
hint = 'pillar'
end
end
-- find the mode with the highest screen area that
-- matches either width or height, according to our hint
local bestMode = { area = 0 }
for _, mode in pairs(modes) do
mode.area = mode.width * mode.height
if (mode.area > bestMode.area) and
((hint == 'letterbox' and mode.width == self.width) or
(hint == 'pillar' and mode.height == self.height)) then
bestMode = mode
end
end
-- if we found a match, switch to it
if bestMode.width then
love.graphics.setMode(bestMode.width, bestMode.height, true)
self.fullscreen = true
-- and adjust inset and scissor
self.inset.x = math.floor((bestMode.width - self.width) / 2)
self.inset.y = math.floor((bestMode.height - self.height) / 2)
love.graphics.setScissor(self.inset.x, self.inset.y, self.width, self.height)
if self.onEnterFullscreen then self:onEnterFullscreen() end
end
return self.fullscreen
end,
-- Method: exitFullscreen
-- Exits fullscreen mode. If the app is already windowed, this has no effect.
--
-- Arguments:
-- none
--
-- Returns:
-- nothing
exitFullscreen = function (self)
if STRICT then
assert(self.fullscreen, 'asked to exit fullscreen when already out of fullscreen')
end
love.graphics.setMode(self.width, self.height, false)
love.graphics.setScissor(0, 0, self.width, self.height)
self.fullscreen = false
self.inset.x = 0
self.inset.y = 0
if self.onExitFullscreen then self:onExitFullscreen() end
end,
-- Method: toggleFullscreen
-- Toggles between windowed and fullscreen mode.
--
-- Arguments:
-- none
--
-- Returns:
-- nothing
toggleFullscreen = function (self)
if self.fullscreen then
self:exitFullscreen()
else
self:enterFullscreen()
end
end,
-- Method: saveScreenshot
-- Saves a snapshot of the current frame to disk.
--
-- Arguments:
-- filename - filename to save as, image format is implied by suffix.
-- This is forced inside the app's data directory,
-- see https://love2d.org/wiki/love.filesystem for details.
--
-- Returns:
-- nothing
saveScreenshot = function (self, filename)
if not filename then
error('asked to save screenshot to a nil filename')
end
local screenshot = love.graphics.newScreenshot()
screenshot:encode(filename)
end,
-- Method: add
-- A shortcut for adding a sprite to the app's view.
--
-- Arguments:
-- sprite - sprite to add
--
-- Returns:
-- nothing
add = function (self, sprite)
self.view:add(sprite)
end,
-- Method: remove
-- A shortcut for removing a sprite from the app's view.
--
-- Arguments:
-- sprite - sprite to remove
--
-- Returns: nothing
remove = function (self, sprite)
self.view:remove(sprite)
end,
update = function (self, elapsed)
-- set the next frame time right at the start
self._nextFrameTime = self._nextFrameTime + 1 / self.fps
elapsed = elapsed - (self._sleepTime or 0)
local realElapsed = elapsed
elapsed = elapsed * self.timeScale
-- sync the.view with our current view
local view = self.view
if the.view ~= view then the.view = view end
-- if we are not active at all, sleep for a half-second
if not self.active then
love.timer.sleep(0.5)
self._sleepTime = 0.5
return
end
self._sleepTime = 0
-- update everyone
-- all update events bubble up from child to parent
-- (we consider the meta view a little
view:startFrame(elapsed)
self.meta:startFrame(elapsed)
if self.onStartFrame then self:onStartFrame(elapsed) end
view:update(elapsed)
self.meta:update(elapsed)
if self.onUpdate then self:onUpdate(elapsed) end
view:endFrame(elapsed)
self.meta:endFrame(elapsed)
if self.onEndFrame then self:onEndFrame(elapsed) end
end,
draw = function (self)
drawStart = love.timer.getMicroTime()
local inset = self.inset.x ~= 0 or self.inset.y ~= 0
if inset then love.graphics.translate(self.inset.x, self.inset.y) end
self.view:draw()
self.meta:draw()
if inset then love.graphics.translate(0, 0) end
-- sleep off any unneeded time to keep up at our FPS
local now = love.timer.getMicroTime()
the.drawTook = now - drawStart
if self._nextFrameTime < now then
self._nextFrameTime = now
else
love.timer.sleep(self._nextFrameTime - now)
end
end,
onFocus = function (self, value)
if self.deactivateOnBlur then
self.active = value
if value then
love.audio.resume()
else
love.audio.pause()
end
end
end,
__tostring = function (self)
local result = 'App ('
if self.active then
result = result .. 'active'
else
result = result .. 'inactive'
end
return result .. ', ' .. self.fps .. ' fps, ' .. self.view:count(true) .. ' sprites)'
end
}
|