]> git.sesse.net Git - nageru/blob - theme.lua
Wire the transition names through to the UI.
[nageru] / theme.lua
1 -- The theme is what decides what's actually shown on screen, what kind of
2 -- transitions are available (if any), and what kind of inputs there are,
3 -- if any. In general, it drives the entire display logic by creating Movit
4 -- chains, setting their parameters and then deciding which to show when.
5 --
6 -- Themes are written in Lua, which reflects a simplified form of the Movit API
7 -- where all the low-level details (such as texture formats) are handled by the
8 -- C++ side and you generally just build chains.
9 io.write("hello from lua\n")
10
11 local zoom_start = -2.0
12 local zoom_end = -1.0
13 local zoom_src = 0.0
14 local zoom_dst = 1.0
15
16 local live_signal_num = 0
17 local preview_signal_num = 1
18
19 -- The main live chain.
20 function make_sbs_chain(hq)
21         local chain = EffectChain.new(16, 9)
22         local input0 = chain:add_live_input()
23         input0:connect_signal(0)
24         local input1 = chain:add_live_input()
25         input1:connect_signal(1)
26
27         local resample_effect = nil
28         local resize_effect = nil
29         if (hq) then
30                 resample_effect = chain:add_effect(ResampleEffect.new(), input0)
31         else
32                 resize_effect = chain:add_effect(ResizeEffect.new(), input0)
33         end
34
35         local padding_effect = chain:add_effect(IntegralPaddingEffect.new())
36         padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 1.0)
37
38         local resample2_effect = nil
39         local resize2_effect = nil
40         if (hq) then
41                 resample2_effect = chain:add_effect(ResampleEffect.new(), input1)
42         else
43                 resize2_effect = chain:add_effect(ResizeEffect.new(), input1)
44         end
45         -- Effect *saturation_effect = chain->add_effect(new SaturationEffect())
46         -- CHECK(saturation_effect->set_float("saturation", 0.3f))
47         local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
48         wb_effect:set_float("output_color_temperature", 3500.0)
49         local padding2_effect = chain:add_effect(IntegralPaddingEffect.new())
50
51         chain:add_effect(OverlayEffect.new(), padding_effect, padding2_effect)
52         chain:finalize(hq)
53
54         return {
55                 chain = chain,
56                 input0 = {
57                         input = input0,
58                         resample_effect = resample_effect,
59                         resize_effect = resize_effect,
60                         padding_effect = padding_effect
61                 },
62                 input1 = {
63                         input = input1,
64                         resample_effect = resample2_effect,
65                         resize_effect = resize2_effect,
66                         padding_effect = padding2_effect
67                 }
68         }
69 end
70
71 local main_chain_hq = make_sbs_chain(true)
72 local main_chain_lq = make_sbs_chain(false)
73
74 -- A chain to show a single input on screen (HQ version).
75 local simple_chain_hq = EffectChain.new(16, 9)
76 local simple_chain_hq_input = simple_chain_hq:add_live_input()
77 simple_chain_hq_input:connect_signal(0);  -- First input card. Can be changed whenever you want.
78 simple_chain_hq:finalize(true)
79
80 -- A chain to show a single input on screen (LQ version).
81 local simple_chain_lq = EffectChain.new(16, 9)
82 local simple_chain_lq_input = simple_chain_lq:add_live_input()
83 simple_chain_lq_input:connect_signal(0);  -- First input card. Can be changed whenever you want.
84 simple_chain_lq:finalize(false)
85
86 -- Returns the number of outputs in addition to the live (0) and preview (1).
87 -- Called only once, at the start of the program.
88 function num_channels()
89         return 3
90 end
91
92 -- Called every frame.
93 function get_transitions(t)
94         -- If live is 2 (SBS) but de-facto single, make it so.
95         if live_signal_num == 2 and t >= zoom_end and zoom_dst == 1.0 then
96                 live_signal_num = 0
97         end
98
99         if live_signal_num == preview_signal_num then
100                 return {}
101         end
102
103         if live_signal_num == 2 and t >= zoom_start and t <= zoom_end then
104                 -- Zoom in progress.
105                 return {"Cut"}
106         end
107
108         if (live_signal_num == 0 and preview_signal_num == 1) or
109            (live_signal_num == 1 and preview_signal_num == 0) then
110                 return {"Cut"}
111         end
112
113         if live_signal_num == 2 and preview_signal_num == 1 then
114                 -- Zoom-out not supported here yet.
115                 return {"Cut"}
116         end
117
118         if live_signal_num == 2 and preview_signal_num == 0 then
119                 return {"Cut", "Zoom in"}
120         elseif live_signal_num == 0 and preview_signal_num == 2 then
121                 return {"Cut", "Zoom out"}
122         end
123
124         return {"Cut"}
125 end
126
127 function transition_clicked(num, t)
128         if num == 0 then
129                 -- Cut.
130                 local temp = live_signal_num
131                 live_signal_num = preview_signal_num
132                 preview_signal_num = temp
133
134                 if live_signal_num == 2 then
135                         -- Just cut to SBS, we need to reset any zooms.
136                         zoom_src = 1.0
137                         zoom_dst = 0.0
138                         zoom_start = -2.0
139                         zoom_end = -1.0
140                 end
141         elseif num == 1 then
142                 -- Zoom.
143
144                 -- If live is 2 (SBS) but de-facto single, make it so.
145                 if live_signal_num == 2 and t >= zoom_end and zoom_dst == 1.0 then
146                         live_signal_num = 0
147                 end
148
149                 if live_signal_num == preview_signal_num then
150                         -- Nothing to do.
151                         return
152                 end
153
154                 if (live_signal_num == 0 and preview_signal_num == 1) or
155                    (live_signal_num == 1 and preview_signal_num == 0) then
156                         -- We can't zoom between these. Just make a cut.
157                         io.write("Cutting from " .. live_signal_num .. " to " .. live_signal_num .. "\n")
158                         local temp = live_signal_num
159                         live_signal_num = preview_signal_num
160                         preview_signal_num = temp
161                         return
162                 end
163
164                 if live_signal_num == 2 and preview_signal_num == 1 then
165                         io.write("NOT SUPPORTED YET\n")
166                         return
167                 end
168
169                 if live_signal_num == 2 and preview_signal_num == 0 then
170                         -- Zoom in from SBS to single.
171                         zoom_start = t
172                         zoom_end = t + 1.0
173                         zoom_src = 0.0
174                         zoom_dst = 1.0
175                         preview_signal_num = 2
176                 elseif live_signal_num == 0 and preview_signal_num == 2 then
177                         -- Zoom out from single to SBS.
178                         zoom_start = t
179                         zoom_end = t + 1.0
180                         zoom_src = 1.0
181                         zoom_dst = 0.0
182                         preview_signal_num = 0
183                         live_signal_num = 2
184                 end
185         end
186 end
187
188 function channel_clicked(num)
189         preview_signal_num = num
190 end
191
192 -- Called every frame. Get the chain for displaying at input <num>,
193 -- where 0 is live, 1 is preview, 2 is the first channel to display
194 -- in the bottom bar, and so on up to num_channels()+1. t is the
195 -- current time in seconds. width and height are the dimensions of
196 -- the output, although you can ignore them if you don't need them
197 -- (they're useful if you want to e.g. know what to resample by).
198 --
199 -- You should return two objects; the chain itself, and then a
200 -- function (taking no parameters) that is run just before rendering.
201 -- The function needs to call connect_signal on any inputs, so that
202 -- it gets updated video data for the given frame. (You are allowed
203 -- to switch which input your input is getting from between frames,
204 -- but not calling connect_signal results in undefined behavior.)
205 -- If you want to change any parameters in the chain, this is also
206 -- the right place.
207 --
208 -- NOTE: The chain returned must be finalized with the Y'CbCr flag
209 -- if and only if num==0.
210 function get_chain(num, t, width, height)
211         if num == 0 then  -- Live.
212                 if live_signal_num == 0 or live_signal_num == 1 then  -- Plain inputs.
213                         prepare = function()
214                                 simple_chain_hq_input:connect_signal(live_signal_num)
215                         end
216                         return simple_chain_hq, prepare
217                 end
218
219                 -- SBS code (live_signal_num == 2).
220                 if t > zoom_end and zoom_dst == 1.0 then
221                         -- Special case: Show only the single image on screen.
222                         prepare = function()
223                                 simple_chain_hq_input:connect_signal(0)
224                         end
225                         return simple_chain_hq, prepare
226                 end
227                 prepare = function()
228                         if t < zoom_start then
229                                 prepare_sbs_chain(main_chain_hq, zoom_src, width, height)
230                         elseif t > zoom_end then
231                                 prepare_sbs_chain(main_chain_hq, zoom_dst, width, height)
232                         else
233                                 local tt = (t - zoom_start) / (zoom_end - zoom_start)
234                                 -- Smooth it a bit.
235                                 tt = math.sin(tt * 3.14159265358 * 0.5)
236                                 prepare_sbs_chain(main_chain_hq, zoom_src + (zoom_dst - zoom_src) * tt, width, height)
237                         end
238                 end
239                 return main_chain_hq.chain, prepare
240         end
241         if num == 1 then  -- Preview.
242                 num = preview_signal_num + 2
243         end
244         if num == 2 then
245                 prepare = function()
246                         simple_chain_lq_input:connect_signal(0)
247                 end
248                 return simple_chain_lq, prepare
249         end
250         if num == 3 then
251                 prepare = function()
252                         simple_chain_lq_input:connect_signal(1)
253                 end
254                 return simple_chain_lq, prepare
255         end
256         if num == 4 then
257                 prepare = function()
258                         prepare_sbs_chain(main_chain_lq, 0.0, width, height)
259                 end
260                 return main_chain_lq.chain, prepare
261         end
262 end
263
264 function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height)
265         local srcx0 = 0.0
266         local srcx1 = 1.0
267         local srcy0 = 0.0
268         local srcy1 = 1.0
269
270         -- Cull.
271         if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
272                 resample_effect:set_int("width", 1)
273                 resample_effect:set_int("height", 1)
274                 resample_effect:set_float("zoom_x", screen_width)
275                 resample_effect:set_float("zoom_y", screen_height)
276                 padding_effect:set_int("left", screen_width + 100)
277                 padding_effect:set_int("top", screen_height + 100)
278                 return
279         end
280
281         -- Clip. (TODO: Clip on upper/left sides, too.)
282         if x1 > screen_width then
283                 srcx1 = (screen_width - x0) / (x1 - x0)
284                 x1 = screen_width
285         end
286         if y1 > screen_height then
287                 srcy1 = (screen_height - y0) / (y1 - y0)
288                 y1 = screen_height
289         end
290
291         if resample_effect ~= nil then
292                 -- High-quality resampling.
293                 local x_subpixel_offset = x0 - math.floor(x0)
294                 local y_subpixel_offset = y0 - math.floor(y0)
295
296                 -- Resampling must be to an integral number of pixels. Round up,
297                 -- and then add an extra pixel so we have some leeway for the border.
298                 local width = math.ceil(x1 - x0) + 1
299                 local height = math.ceil(y1 - y0) + 1
300                 resample_effect:set_int("width", width)
301                 resample_effect:set_int("height", height)
302
303                 -- Correct the discrepancy with zoom. (This will leave a small
304                 -- excess edge of pixels and subpixels, which we'll correct for soon.)
305                 local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
306                 local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
307                 resample_effect:set_float("zoom_x", zoom_x)
308                 resample_effect:set_float("zoom_y", zoom_y)
309                 resample_effect:set_float("zoom_center_x", 0.0)
310                 resample_effect:set_float("zoom_center_y", 0.0)
311
312                 -- Padding must also be to a whole-pixel offset.
313                 padding_effect:set_int("left", math.floor(x0))
314                 padding_effect:set_int("top", math.floor(y0))
315
316                 -- Correct _that_ discrepancy by subpixel offset in the resampling.
317                 resample_effect:set_float("left", -x_subpixel_offset / zoom_x)
318                 resample_effect:set_float("top", -y_subpixel_offset / zoom_y)
319
320                 -- Finally, adjust the border so it is exactly where we want it.
321                 padding_effect:set_float("border_offset_left", x_subpixel_offset)
322                 padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
323                 padding_effect:set_float("border_offset_top", y_subpixel_offset)
324                 padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
325         else
326                 -- Lower-quality simple resizing.
327                 local width = round(x1 - x0)
328                 local height = round(y1 - y0)
329                 resize_effect:set_int("width", width)
330                 resize_effect:set_int("height", height)
331
332                 -- Padding must also be to a whole-pixel offset.
333                 padding_effect:set_int("left", math.floor(x0))
334                 padding_effect:set_int("top", math.floor(y0))
335         end
336 end
337
338 -- This is broken, of course (even for positive numbers), but Lua doesn't give us access to real rounding.
339 function round(x)
340         return math.floor(x + 0.5)
341 end
342
343 function prepare_sbs_chain(chain, t, screen_width, screen_height)
344         chain.input0.input:connect_signal(0)
345         chain.input1.input:connect_signal(1)
346
347         -- First input is positioned (16,48) from top-left.
348         local width0 = round(848 * screen_width/1280.0)
349         local height0 = round(width0 * 9.0 / 16.0)
350
351         local top0 = 48 * screen_height/720.0
352         local left0 = 16 * screen_width/1280.0
353         local bottom0 = top0 + height0
354         local right0 = left0 + width0
355
356         -- Second input is positioned (16,48) from the bottom-right.
357         local width1 = 384 * screen_width/1280.0
358         local height1 = 216 * screen_height/720.0
359
360         local bottom1 = screen_height - 48 * screen_height/720.0
361         local right1 = screen_width - 16 * screen_width/1280.0
362         local top1 = bottom1 - height1
363         local left1 = right1 - width1
364
365         -- Interpolate between the fullscreen and side-by-side views.
366         local scale0 = 1.0 + t * (1280.0 / 848.0 - 1.0)
367         local tx0 = 0.0 + t * (-left0 * scale0)
368         local ty0 = 0.0 + t * (-top0 * scale0)
369
370         top0 = top0 * scale0 + ty0
371         bottom0 = bottom0 * scale0 + ty0
372         left0 = left0 * scale0 + tx0
373         right0 = right0 * scale0 + tx0
374
375         top1 = top1 * scale0 + ty0
376         bottom1 = bottom1 * scale0 + ty0
377         left1 = left1 * scale0 + tx0
378         right1 = right1 * scale0 + tx0
379         place_rectangle(chain.input0.resample_effect, chain.input0.resize_effect, chain.input0.padding_effect, left0, top0, right0, bottom0, screen_width, screen_height)
380         place_rectangle(chain.input1.resample_effect, chain.input1.resize_effect, chain.input1.padding_effect, left1, top1, right1, bottom1, screen_width, screen_height)
381 end