]> git.sesse.net Git - nageru/blob - theme.lua
Hook up the channel click events.
[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()
94         return {"Cut", "Fade", "Zoom!"}
95 end
96
97 function transition_clicked(num, t)
98         -- local temp = live_signal_num
99         -- live_signal_num = preview_signal_num
100         -- preview_signal_num = temp
101
102         zoom_start = t
103         zoom_end = t + 1.0
104
105         local temp = zoom_src
106         zoom_src = zoom_dst
107         zoom_dst = temp
108 end
109
110 function channel_clicked(num)
111         preview_signal_num = num
112 end
113
114 -- Called every frame. Get the chain for displaying at input <num>,
115 -- where 0 is live, 1 is preview, 2 is the first channel to display
116 -- in the bottom bar, and so on up to num_channels()+1. t is the 
117 -- current time in seconds. width and height are the dimensions of
118 -- the output, although you can ignore them if you don't need them
119 -- (they're useful if you want to e.g. know what to resample by).
120 --
121 -- You should return two objects; the chain itself, and then a
122 -- function (taking no parameters) that is run just before rendering.
123 -- The function needs to call connect_signal on any inputs, so that
124 -- it gets updated video data for the given frame. (You are allowed
125 -- to switch which input your input is getting from between frames,
126 -- but not calling connect_signal results in undefined behavior.)
127 -- If you want to change any parameters in the chain, this is also
128 -- the right place.
129 --
130 -- NOTE: The chain returned must be finalized with the Y'CbCr flag
131 -- if and only if num==0.
132 function get_chain(num, t, width, height)
133         if num == 0 then  -- Live.
134                 if t > zoom_end and zoom_dst == 1.0 then
135                         -- Special case: Show only the single image on screen.
136                         prepare = function()
137                                 simple_chain_hq_input:connect_signal(live_signal_num)
138                         end
139                         return simple_chain_hq, prepare
140                 end
141                 prepare = function()
142                         if t < zoom_start then
143                                 prepare_sbs_chain(main_chain_hq, zoom_src, width, height)
144                         elseif t > zoom_end then
145                                 prepare_sbs_chain(main_chain_hq, zoom_dst, width, height)
146                         else
147                                 local tt = (t - zoom_start) / (zoom_end - zoom_start)
148                                 -- Smooth it a bit.
149                                 tt = math.sin(tt * 3.14159265358 * 0.5)
150                                 prepare_sbs_chain(main_chain_hq, zoom_src + (zoom_dst - zoom_src) * tt, width, height)
151                         end
152                 end
153                 return main_chain_hq.chain, prepare
154         end
155         if num == 1 then  -- Preview.
156                 num = preview_signal_num + 2
157         end
158         if num == 2 then
159                 prepare = function()
160                         simple_chain_lq_input:connect_signal(0)
161                 end
162                 return simple_chain_lq, prepare
163         end
164         if num == 3 then
165                 prepare = function()
166                         simple_chain_lq_input:connect_signal(1)
167                 end
168                 return simple_chain_lq, prepare
169         end
170         if num == 4 then
171                 prepare = function()
172                         prepare_sbs_chain(main_chain_lq, 0.0, width, height)
173                 end
174                 return main_chain_lq.chain, prepare
175         end
176 end
177
178 function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height)
179         local srcx0 = 0.0
180         local srcx1 = 1.0
181         local srcy0 = 0.0
182         local srcy1 = 1.0
183
184         -- Cull.
185         if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
186                 resample_effect:set_int("width", 1)
187                 resample_effect:set_int("height", 1)
188                 resample_effect:set_float("zoom_x", screen_width)
189                 resample_effect:set_float("zoom_y", screen_height)
190                 padding_effect:set_int("left", screen_width + 100)
191                 padding_effect:set_int("top", screen_height + 100)
192                 return
193         end
194
195         -- Clip. (TODO: Clip on upper/left sides, too.)
196         if x1 > screen_width then
197                 srcx1 = (screen_width - x0) / (x1 - x0)
198                 x1 = screen_width
199         end
200         if y1 > screen_height then
201                 srcy1 = (screen_height - y0) / (y1 - y0)
202                 y1 = screen_height
203         end
204
205         if resample_effect ~= nil then
206                 -- High-quality resampling.
207                 local x_subpixel_offset = x0 - math.floor(x0)
208                 local y_subpixel_offset = y0 - math.floor(y0)
209
210                 -- Resampling must be to an integral number of pixels. Round up,
211                 -- and then add an extra pixel so we have some leeway for the border.
212                 local width = math.ceil(x1 - x0) + 1
213                 local height = math.ceil(y1 - y0) + 1
214                 resample_effect:set_int("width", width)
215                 resample_effect:set_int("height", height)
216
217                 -- Correct the discrepancy with zoom. (This will leave a small
218                 -- excess edge of pixels and subpixels, which we'll correct for soon.)
219                 local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
220                 local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
221                 resample_effect:set_float("zoom_x", zoom_x)
222                 resample_effect:set_float("zoom_y", zoom_y)
223                 resample_effect:set_float("zoom_center_x", 0.0)
224                 resample_effect:set_float("zoom_center_y", 0.0)
225
226                 -- Padding must also be to a whole-pixel offset.
227                 padding_effect:set_int("left", math.floor(x0))
228                 padding_effect:set_int("top", math.floor(y0))
229
230                 -- Correct _that_ discrepancy by subpixel offset in the resampling.
231                 resample_effect:set_float("left", -x_subpixel_offset / zoom_x)
232                 resample_effect:set_float("top", -y_subpixel_offset / zoom_y)
233
234                 -- Finally, adjust the border so it is exactly where we want it.
235                 padding_effect:set_float("border_offset_left", x_subpixel_offset)
236                 padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
237                 padding_effect:set_float("border_offset_top", y_subpixel_offset)
238                 padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
239         else
240                 -- Lower-quality simple resizing.
241                 local width = round(x1 - x0)
242                 local height = round(y1 - y0)
243                 resize_effect:set_int("width", width)
244                 resize_effect:set_int("height", height)
245
246                 -- Padding must also be to a whole-pixel offset.
247                 padding_effect:set_int("left", math.floor(x0))
248                 padding_effect:set_int("top", math.floor(y0))
249         end
250 end
251
252 -- This is broken, of course (even for positive numbers), but Lua doesn't give us access to real rounding.
253 function round(x)
254         return math.floor(x + 0.5)
255 end
256
257 function prepare_sbs_chain(chain, t, screen_width, screen_height)
258         chain.input0.input:connect_signal(live_signal_num)
259         chain.input1.input:connect_signal(1)
260
261         -- First input is positioned (16,48) from top-left.
262         local width0 = round(848 * screen_width/1280.0)
263         local height0 = round(width0 * 9.0 / 16.0)
264
265         local top0 = 48 * screen_height/720.0
266         local left0 = 16 * screen_width/1280.0
267         local bottom0 = top0 + height0
268         local right0 = left0 + width0
269
270         -- Second input is positioned (16,48) from the bottom-right.
271         local width1 = 384 * screen_width/1280.0
272         local height1 = 216 * screen_height/720.0
273
274         local bottom1 = screen_height - 48 * screen_height/720.0
275         local right1 = screen_width - 16 * screen_width/1280.0
276         local top1 = bottom1 - height1
277         local left1 = right1 - width1
278
279         -- Interpolate between the fullscreen and side-by-side views.
280         local scale0 = 1.0 + t * (1280.0 / 848.0 - 1.0)
281         local tx0 = 0.0 + t * (-left0 * scale0)
282         local ty0 = 0.0 + t * (-top0 * scale0)
283
284         top0 = top0 * scale0 + ty0
285         bottom0 = bottom0 * scale0 + ty0
286         left0 = left0 * scale0 + tx0
287         right0 = right0 * scale0 + tx0
288
289         top1 = top1 * scale0 + ty0
290         bottom1 = bottom1 * scale0 + ty0
291         left1 = left1 * scale0 + tx0
292         right1 = right1 * scale0 + tx0
293         place_rectangle(chain.input0.resample_effect, chain.input0.resize_effect, chain.input0.padding_effect, left0, top0, right0, bottom0, screen_width, screen_height)
294         place_rectangle(chain.input1.resample_effect, chain.input1.resize_effect, chain.input1.padding_effect, left1, top1, right1, bottom1, screen_width, screen_height)
295 end