646 lines
20 KiB
Lua
646 lines
20 KiB
Lua
|
--[[
|
||
|
|
||
|
Sample script for OverLua
|
||
|
- advanced karaoke effect, first version of Mendoi-Conclave Gundam 00 OP 1
|
||
|
|
||
|
Given into the public domain.
|
||
|
(You can do anything you want with this file, with no restrictions whatsoever.
|
||
|
You don't get any warranties of any kind either, though.)
|
||
|
|
||
|
Originally authored by Niels Martin Hansen.
|
||
|
|
||
|
While I can't prevent you from it, please don't use this effect script
|
||
|
verbatim or almost-verbatim for own productions. It's mainly intended for
|
||
|
showing techniques, just using it without modifications or with only light
|
||
|
modifications is what I'd consider "cheap".
|
||
|
|
||
|
Be aware that this effect is very slow at rendering, at full 720p resolution
|
||
|
it takes around 3 hours to render on my dual 2.2 GHz Opteron.
|
||
|
|
||
|
This effect is called "OH NOES" by the way. No special meaning to that.
|
||
|
|
||
|
It's best read from bottom to top.
|
||
|
|
||
|
]]
|
||
|
|
||
|
|
||
|
-- Virtual resolution, 720p
|
||
|
local virtual_res_x = 1280
|
||
|
local virtual_res_y = 720
|
||
|
-- Font names
|
||
|
--local latin_font = "Eras Bold ITC"
|
||
|
local latin_font = "Briem Akademi Std Semibold"
|
||
|
local latin_weight = ""
|
||
|
local kanji_font = "DFGSoGei-W9"
|
||
|
-- Font sizes
|
||
|
local romaji_size = 34
|
||
|
local engrish_size = 36
|
||
|
local kanji_size = 30
|
||
|
local tl_size = 36
|
||
|
-- Text positions (vertical only, assumed centered)
|
||
|
local romaji_pos_y = 55
|
||
|
local tl_pos_y = virtual_res_y - 38
|
||
|
local kanji_pos_y = virtual_res_y - 27
|
||
|
local kanji_pos_x = virtual_res_x - 55
|
||
|
local engrish_pos_y = virtual_res_y - 38
|
||
|
|
||
|
|
||
|
timing_input_file = overlua_datastring
|
||
|
assert(timing_input_file, "OH NOES! Missing timing input file.")
|
||
|
|
||
|
|
||
|
-- Here's some mostly standard input file parsing functions
|
||
|
|
||
|
function parsenum(str)
|
||
|
return tonumber(str) or 0
|
||
|
end
|
||
|
function parse_ass_time(ass)
|
||
|
local h, m, s, cs = ass:match("(%d+):(%d+):(%d+)%.(%d+)")
|
||
|
return parsenum(cs)/100 + parsenum(s) + parsenum(m)*60 + parsenum(h)*3600
|
||
|
end
|
||
|
|
||
|
function parse_k_timing(text)
|
||
|
local syls = {}
|
||
|
local cleantext = ""
|
||
|
local i = 1
|
||
|
for timing, syltext in text:gmatch("{\\k(%d+)}([^{]*)") do
|
||
|
local syl = {dur = parsenum(timing)/100, text = syltext, i = i}
|
||
|
local maintext, furitext = syltext:match("(.-)|(.+)")
|
||
|
-- Note that there is a light support for Auto4 style furigana
|
||
|
-- in this script, but I haven't maintained it since it ended up being
|
||
|
-- unused.
|
||
|
if maintext and furitext and furitext ~= "" then
|
||
|
syl.text = maintext
|
||
|
syl.furi = furitext
|
||
|
end
|
||
|
table.insert(syls, syl)
|
||
|
cleantext = cleantext .. syl.text
|
||
|
i = i + 1
|
||
|
end
|
||
|
return syls, cleantext
|
||
|
end
|
||
|
|
||
|
function read_input_file(name)
|
||
|
for line in io.lines(name) do
|
||
|
local start_time, end_time, style, fx, text = line:match("Dialogue: 0,(.-),(.-),(.-),,0000,0000,0000,(.-),(.*)")
|
||
|
if text then
|
||
|
local ls = {}
|
||
|
ls.start_time = parse_ass_time(start_time)
|
||
|
ls.end_time = parse_ass_time(end_time)
|
||
|
ls.style = style
|
||
|
ls.fx = fx
|
||
|
ls.rawtext = text
|
||
|
ls.kara, ls.cleantext = parse_k_timing(text)
|
||
|
table.insert(lines, ls)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function init()
|
||
|
if inited then return end
|
||
|
inited = true
|
||
|
|
||
|
lines = {}
|
||
|
read_input_file(timing_input_file)
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Calculate size and position of a line and its syllables
|
||
|
-- Only for horizontal lines, not vertical
|
||
|
function calc_line_metrics(ctx, line, font_name, font_size, pos_y)
|
||
|
if line.pos_x then return end
|
||
|
|
||
|
ctx.select_font_face(font_name, "", latin_weight)
|
||
|
ctx.set_font_size(font_size)
|
||
|
|
||
|
line.te = ctx.text_extents(line.cleantext)
|
||
|
line.fe = ctx.font_extents()
|
||
|
|
||
|
line.pos_x = (virtual_res_x - line.te.width) / 2 - line.te.x_bearing
|
||
|
line.pos_y = pos_y
|
||
|
|
||
|
if #line.kara < 2 then return end
|
||
|
|
||
|
local curx = line.pos_x
|
||
|
for i, syl in pairs(line.kara) do
|
||
|
syl.te = ctx.text_extents(syl.text)
|
||
|
syl.pos_x = curx
|
||
|
syl.center_x = curx + syl.te.x_bearing + syl.te.width/2
|
||
|
syl.center_y = pos_y - line.fe.ascent/2 + line.fe.descent/2
|
||
|
curx = curx + syl.te.x_advance
|
||
|
|
||
|
if syl.furi then
|
||
|
ctx.set_font_size(font_size/2)
|
||
|
syl.furite = ctx.text_extents(syl.furi)
|
||
|
syl.furife = ctx.font_extents()
|
||
|
ctx.set_font_size(font_size)
|
||
|
syl.furi_x = syl.center_x - syl.furite.width/2 - syl.furite.x_bearing
|
||
|
syl.furi_y = pos_y - line.fe.height
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Paint the image of a line of text to a cairo context
|
||
|
-- Assumes the current path in the context is of the text to be painted
|
||
|
function paint_text(surf, ctx)
|
||
|
ctx.set_line_join("round")
|
||
|
ctx.set_source_rgba(0, 0.2, 0.3, 0.8)
|
||
|
ctx.set_line_width(3)
|
||
|
ctx.stroke_preserve()
|
||
|
raster.gaussian_blur(surf, 1.7)
|
||
|
ctx.set_source_rgba(1, 1, 1, 0.95)
|
||
|
ctx.fill()
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Render one of the zoomed circles with some parameters
|
||
|
-- width and height are of the source area to be visible in the zoomed image
|
||
|
-- Some of this is a bit hacked, I just changed stuff around until it worked,
|
||
|
-- honestly. Analyse it if you want, it still doesn't fully make sense to me ;)
|
||
|
function make_zoomed_ellipsis(srcsurf, center_x, center_y, width, height)
|
||
|
local factor = 0.7
|
||
|
|
||
|
local target_width, target_height = math.ceil(width/factor), math.ceil(height/factor)
|
||
|
|
||
|
local target = cairo.image_surface_create(target_width, target_height, "argb32")
|
||
|
local targetctx = target.create_context()
|
||
|
|
||
|
local src_x, src_y = center_x - width/2, center_y - height/2
|
||
|
|
||
|
-- The basic premise is just taking the source surface, making an upscaling
|
||
|
-- pattern of it and fill a circle with the correct portion of it.
|
||
|
-- Actually pretty simple, it's just getting the numbers right.
|
||
|
local srcpat = cairo.pattern_create_for_surface(srcsurf)
|
||
|
srcpat.set_extend("none")
|
||
|
local srcpatmatrix = cairo.matrix_create()
|
||
|
srcpatmatrix.init_translate(src_x, src_y)
|
||
|
srcpatmatrix.scale(factor, factor)
|
||
|
srcpat.set_matrix(srcpatmatrix)
|
||
|
|
||
|
targetctx.scale(target_width, target_height)
|
||
|
targetctx.arc(0.5, 0.5, 0.5, 0, math.pi*2)
|
||
|
targetctx.scale(1/target_width, 1/target_height)
|
||
|
targetctx.set_source(srcpat)
|
||
|
targetctx.fill()
|
||
|
|
||
|
return target, target_width, target_height
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Duration in seconds for the fade-in/-outs
|
||
|
local fadeinoutdur = 1.2
|
||
|
|
||
|
|
||
|
-- Paint a complete line of karaoke text with all effects, except the
|
||
|
-- zoom circles, to a context. It depends on l.textsurf containing the line
|
||
|
-- image.
|
||
|
-- The main attraction here is the fade-over effect.
|
||
|
function paint_kara_text(f, ctx, t, l)
|
||
|
local fade, fademask, fadetype
|
||
|
-- Check if we're fading in?
|
||
|
if t < l.start_time + fadeinoutdur and l.fx ~= "nofadein" then
|
||
|
-- Calculate the position of the fade
|
||
|
fade = 1 - (l.start_time - t + fadeinoutdur/2) / fadeinoutdur
|
||
|
-- Create a gradient pattern that shows only the relevant part of
|
||
|
-- the line for the fade.
|
||
|
fademask = cairo.pattern_create_linear(virtual_res_x*fade, virtual_res_y/2, virtual_res_x*fade - 100, virtual_res_y/2-30)
|
||
|
fademask.add_color_stop_rgba(0, 1, 1, 1, 0)
|
||
|
fademask.add_color_stop_rgba(0.05, 1, 1, 1, 1)
|
||
|
fademask.add_color_stop_rgba(0.3, 1, 1, 1, 0.2)
|
||
|
fademask.add_color_stop_rgba(1, 1, 1, 1, 1)
|
||
|
fadetype = "in"
|
||
|
end
|
||
|
-- Or fading out?
|
||
|
if l.end_time - fadeinoutdur <= t and l.fx ~= "last" and l.fx~= "nofadeout" then
|
||
|
-- Pretty much the same as for fade in, except that a different part of
|
||
|
-- the line is shown by the produced pattern
|
||
|
fade = (t - l.end_time + fadeinoutdur/2) / fadeinoutdur
|
||
|
fademask = cairo.pattern_create_linear(virtual_res_x*fade, virtual_res_y/2, virtual_res_x*fade + 100, virtual_res_y/2+30)
|
||
|
fademask.add_color_stop_rgba(0, 1, 1, 1, 0)
|
||
|
fademask.add_color_stop_rgba(0.05, 1, 1, 1, 1)
|
||
|
fademask.add_color_stop_rgba(0.3, 1, 1, 1, 0.2)
|
||
|
fademask.add_color_stop_rgba(1, 1, 1, 1, 1)
|
||
|
fadetype = "out"
|
||
|
end
|
||
|
-- Is the line even visible?!
|
||
|
if not fade and (t < l.start_time or l.end_time <= t) then return end
|
||
|
|
||
|
-- A function that calculates the distance between a point and the fade
|
||
|
-- The distance is calculated only along the X axis, so it's not the
|
||
|
-- shortest distance from the point to the "fade line".
|
||
|
-- Used to determine which side of the fade a point is on.
|
||
|
local function fadedist(x, y) -- on X axis
|
||
|
local fade_x_at_y = virtual_res_x*fade - (y - virtual_res_y/2) * 3/10
|
||
|
if fadetype == "in" then
|
||
|
return fade_x_at_y - x
|
||
|
else
|
||
|
return x - fade_x_at_y
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- We'll be painting the surface with the image of the text
|
||
|
ctx.set_source_surface(l.textsurf, 0, 0)
|
||
|
if fade then
|
||
|
-- So first paint the text with the fading-mask
|
||
|
ctx.mask(fademask)
|
||
|
|
||
|
-- Now generate a slightly different mask for the bloom effect
|
||
|
-- This one goes "both ways", it's not restricted to just one direction;
|
||
|
-- it gets limited later
|
||
|
local bloommask = cairo.pattern_create_linear(virtual_res_x*fade - 200, virtual_res_y/2-60, virtual_res_x*fade + 200, virtual_res_y/2+60)
|
||
|
bloommask.add_color_stop_rgba(0, 1, 1, 1, 0)
|
||
|
bloommask.add_color_stop_rgba(0.5, 1, 1, 1, 1)
|
||
|
bloommask.add_color_stop_rgba(1, 1, 1, 1, 0)
|
||
|
local bloom = cairo.image_surface_create(virtual_res_x, virtual_res_y, "argb32")
|
||
|
local bc = bloom.create_context()
|
||
|
bc.set_source_surface(l.textsurf, 0, 0)
|
||
|
bc.mask(fademask)
|
||
|
-- Ok, this could be done in a faster way I bet... modify the colour of
|
||
|
-- the bloom effect depending on whether it's a fade in or out,
|
||
|
-- by running a pixel value mapping program over them.
|
||
|
if fadetype == "out" then
|
||
|
raster.pixel_value_map(bloom, "R 0.9 * =R G 0.1 * =G B 0.4 * =B")
|
||
|
else
|
||
|
raster.pixel_value_map(bloom, "R 0.22 * =R G 0.45 * =G B 0.44 * =B")
|
||
|
end
|
||
|
-- Now, three times, do an additive blending of a successively more
|
||
|
-- blurred version of the masked text.
|
||
|
-- Exploit that the text border is very dark, so it won't contribute
|
||
|
-- much at all to the overall result.
|
||
|
-- If the border was brighter a different image of the text would need
|
||
|
-- to be used instead.
|
||
|
-- This is what *really* kills the rendering speed!
|
||
|
ctx.set_operator("add")
|
||
|
raster.gaussian_blur(bloom, 3)
|
||
|
ctx.set_source_surface(bloom, 0, 0)
|
||
|
ctx.mask(bloommask)
|
||
|
raster.gaussian_blur(bloom, 3)
|
||
|
ctx.set_source_surface(bloom, 0, 0)
|
||
|
ctx.mask(bloommask)
|
||
|
raster.gaussian_blur(bloom, 3)
|
||
|
ctx.set_source_surface(bloom, 0, 0)
|
||
|
ctx.mask(bloommask)
|
||
|
ctx.set_operator("over")
|
||
|
else
|
||
|
-- We aren't fading, just do a plain paint of the text image
|
||
|
ctx.paint()
|
||
|
end
|
||
|
|
||
|
return fade, fademask, fadetype, fadedist
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Line style processing functions
|
||
|
-- The entries in this table are matched with the line Style fields to pick
|
||
|
-- an appropriate handling function for the line.
|
||
|
stylefunc = {}
|
||
|
|
||
|
-- This is a generic handling function called by other functions
|
||
|
function stylefunc.generic(f, ctx, t, l, font_name, font_size, pos_y)
|
||
|
-- Fast return for irrelevant lines
|
||
|
if t < l.start_time - fadeinoutdur/2 then return end
|
||
|
if l.end_time + fadeinoutdur/2 <= t then return end
|
||
|
|
||
|
-- Make sure we have the positioning information for the line
|
||
|
calc_line_metrics(ctx, l, font_name, font_size, pos_y)
|
||
|
|
||
|
-- If it's the first time this line is processed, generate the image of it
|
||
|
if not l.textsurf then
|
||
|
-- Create surface for the text image
|
||
|
local textsurf = cairo.image_surface_create(virtual_res_x, virtual_res_y, "argb32")
|
||
|
local c = textsurf.create_context()
|
||
|
|
||
|
-- Fill it with a path of the text
|
||
|
c.select_font_face(font_name, "", latin_weight)
|
||
|
c.set_font_size(font_size)
|
||
|
|
||
|
c.move_to(l.pos_x, l.pos_y)
|
||
|
c.text_path(l.cleantext)
|
||
|
|
||
|
for i, syl in pairs(l.kara) do
|
||
|
if syl.furi then
|
||
|
c.set_font_size(kanji_size/2)
|
||
|
c.move_to(syl.furi_x, syl.furi_y)
|
||
|
c.text_path(syl.furi)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
paint_text(textsurf, c)
|
||
|
|
||
|
l.textsurf = textsurf
|
||
|
end
|
||
|
|
||
|
-- Check if we're on the last line which needs the "fade all out" effect
|
||
|
if l.fx == "last" and t > l.end_time - 1.5 then
|
||
|
fade_all_out = (l.end_time - t) / 1.5
|
||
|
else
|
||
|
fade_all_out = nil
|
||
|
end
|
||
|
|
||
|
-- Put the actual text onto the video image
|
||
|
local fade, fademask, fadetype, fadedist = paint_kara_text(f, ctx, t, l)
|
||
|
|
||
|
-- Search for a currently highlighted syllable in the text
|
||
|
local sumdur = l.start_time
|
||
|
local cursyl = -1
|
||
|
for i, syl in pairs(l.kara) do
|
||
|
syl.start_time = sumdur
|
||
|
if t >= sumdur and t < sumdur+syl.dur then
|
||
|
cursyl = i
|
||
|
end
|
||
|
sumdur = sumdur + syl.dur
|
||
|
end
|
||
|
|
||
|
if cursyl >= 1 then
|
||
|
-- There is a current syllable
|
||
|
-- Figure out where to put the zoom circle
|
||
|
local syl = l.kara[cursyl]
|
||
|
-- Assume it's at the center of the syllable for now
|
||
|
local zoompoint = {
|
||
|
cx = syl.center_x,
|
||
|
cy = syl.center_y,
|
||
|
size = math.max(syl.te.width, syl.te.height)
|
||
|
}
|
||
|
-- But check if we're time-wise close enough to the previous syllable
|
||
|
-- (if there is one) to do a transition from it
|
||
|
local prevsyl
|
||
|
if cursyl >= 2 then
|
||
|
local prevsyli = cursyl - 1
|
||
|
repeat
|
||
|
prevsyl = l.kara[prevsyli]
|
||
|
prevsyli = prevsyli - 1
|
||
|
until (prevsyl.dur > 0)
|
||
|
if syl.dur > 0.100 and t - syl.start_time < 0.100 then
|
||
|
local pcx, pcy = prevsyl.center_x, prevsyl.center_y
|
||
|
local psize = math.max(prevsyl.te.width, prevsyl.te.height)
|
||
|
local v = (t - syl.start_time) / 0.100
|
||
|
local iv = 1 - v
|
||
|
zoompoint.cx = iv * pcx + v * zoompoint.cx
|
||
|
zoompoint.cy = iv * pcy + v * zoompoint.cy
|
||
|
zoompoint.size = iv * psize + v * zoompoint.size
|
||
|
end
|
||
|
elseif cursyl == 1 and syl.dur > 0.100 and t - syl.start_time < 0.100 then
|
||
|
zoompoint.size = zoompoint.size * (t - syl.start_time) / 0.100
|
||
|
end
|
||
|
zoompoint.size = zoompoint.size * 1.1
|
||
|
-- Check that we aren't fading over and that the center of the zoom is
|
||
|
-- not outside the visible part of the line.
|
||
|
if not fade or fadedist(zoompoint.cx, zoompoint.cy) > 0 then
|
||
|
-- Insert (enable) the zoom point then
|
||
|
table.insert(zoompoints, zoompoint)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- The Romaji and Engrish styles are both the same generic thing
|
||
|
function stylefunc.Romaji(f, ctx, t, l)
|
||
|
stylefunc.generic(f, ctx, t, l, latin_font, romaji_size, romaji_pos_y)
|
||
|
end
|
||
|
|
||
|
-- Engrish was used for the somewhat-English lines in the original lyrics
|
||
|
-- (I.e. not for the translation.)
|
||
|
function stylefunc.Engrish(f, ctx, t, l)
|
||
|
stylefunc.generic(f, ctx, t, l, latin_font, engrish_size, engrish_pos_y)
|
||
|
end
|
||
|
|
||
|
-- The vertical kanji need a rather different handling
|
||
|
function stylefunc.Kanji(f, ctx, t, l)
|
||
|
-- Again, check for fast skip
|
||
|
if t < l.start_time - fadeinoutdur/2 then return end
|
||
|
if l.end_time + fadeinoutdur/2 <= t then return end
|
||
|
|
||
|
-- Mostly the same as for the generic handling, except that we also
|
||
|
-- calculate the metrics here.
|
||
|
if not l.textsurf then
|
||
|
local textsurf = cairo.image_surface_create(virtual_res_x, virtual_res_y, "argb32")
|
||
|
local c = textsurf.create_context()
|
||
|
|
||
|
c.select_font_face("@"..kanji_font)
|
||
|
c.set_font_size(kanji_size)
|
||
|
|
||
|
l.te = c.text_extents(l.cleantext)
|
||
|
l.fe = c.font_extents()
|
||
|
|
||
|
l.pos_x = kanji_pos_x
|
||
|
l.pos_y = (virtual_res_y - l.te.width) / 2 - l.te.x_bearing
|
||
|
|
||
|
local cury = l.pos_y
|
||
|
for i, syl in pairs(l.kara) do
|
||
|
syl.te = c.text_extents(syl.text)
|
||
|
syl.pos_y = cury
|
||
|
syl.center_y = cury + syl.te.x_bearing + syl.te.width/2
|
||
|
syl.center_x = kanji_pos_x + l.fe.ascent/2 - l.fe.descent/2
|
||
|
cury = cury + syl.te.x_advance
|
||
|
end
|
||
|
|
||
|
c.translate(l.pos_x, l.pos_y)
|
||
|
c.rotate(math.pi/2)
|
||
|
c.move_to(0,0)
|
||
|
c.text_path(l.cleantext)
|
||
|
|
||
|
paint_text(textsurf, c)
|
||
|
|
||
|
l.textsurf = textsurf
|
||
|
end
|
||
|
|
||
|
local fade, fademask, fadetype, fadedist = paint_kara_text(f, ctx, t, l)
|
||
|
|
||
|
-- Lots of copy-paste (code re-use!) here, slightly adapted for vertical
|
||
|
-- text rather than horizontal stuff.
|
||
|
local sumdur = l.start_time
|
||
|
local cursyl = -1
|
||
|
for i, syl in pairs(l.kara) do
|
||
|
syl.start_time = sumdur
|
||
|
if t >= sumdur and t < sumdur+syl.dur then
|
||
|
cursyl = i
|
||
|
end
|
||
|
sumdur = sumdur + syl.dur
|
||
|
end
|
||
|
|
||
|
if cursyl >= 1 then
|
||
|
local syl = l.kara[cursyl]
|
||
|
local zoompoint = {
|
||
|
cx = syl.center_x,
|
||
|
cy = syl.center_y,
|
||
|
size = math.max(syl.te.width, syl.te.height)
|
||
|
}
|
||
|
local prevsyl
|
||
|
if cursyl >= 2 then
|
||
|
local prevsyli = cursyl - 1
|
||
|
repeat
|
||
|
prevsyl = l.kara[prevsyli]
|
||
|
prevsyli = prevsyli - 1
|
||
|
until (prevsyl.dur > 0)
|
||
|
if syl.dur > 0.100 and t - syl.start_time < 0.100 then
|
||
|
local pcx, pcy = prevsyl.center_x, prevsyl.center_y
|
||
|
local psize = math.max(prevsyl.te.width, prevsyl.te.height)
|
||
|
local v = (t - syl.start_time) / 0.100
|
||
|
local iv = 1 - v
|
||
|
zoompoint.cx = iv * pcx + v * zoompoint.cx
|
||
|
zoompoint.cy = iv * pcy + v * zoompoint.cy
|
||
|
zoompoint.size = iv * psize + v * zoompoint.size
|
||
|
end
|
||
|
elseif cursyl == 1 and syl.dur > 0.100 and t - syl.start_time < 0.100 then
|
||
|
zoompoint.size = zoompoint.size * (t - syl.start_time) / 0.100
|
||
|
end
|
||
|
zoompoint.size = zoompoint.size * 1.1
|
||
|
if not fade or fadedist(zoompoint.cx, zoompoint.cy) > 0 then
|
||
|
table.insert(zoompoints, zoompoint)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- The translation lines get a somewhat simplified handling again.
|
||
|
-- Originally separated out because some translated lines were split into two
|
||
|
-- stacked lines, but that was dropped again.
|
||
|
function stylefunc.TL(f, ctx, t, l)
|
||
|
if t < l.start_time - fadeinoutdur/2 then return end
|
||
|
if l.end_time + fadeinoutdur/2 <= t then return end
|
||
|
|
||
|
local line1, line2 = l.rawtext, l.rawtext:find("\\n", 1, true)
|
||
|
if line2 then
|
||
|
line1 = l.rawtext:sub(line2+2)
|
||
|
line2 = l.rawtext:sub(1, line2-1)
|
||
|
else
|
||
|
line2 = ""
|
||
|
end
|
||
|
|
||
|
if not l.textsurf then
|
||
|
local textsurf = cairo.image_surface_create(virtual_res_x, virtual_res_y, "argb32")
|
||
|
local c = textsurf.create_context()
|
||
|
|
||
|
c.select_font_face(latin_font, "", latin_weight)
|
||
|
c.set_font_size(tl_size)
|
||
|
|
||
|
l.te1 = c.text_extents(line1)
|
||
|
l.te2 = c.text_extents(line2)
|
||
|
l.fe = c.font_extents()
|
||
|
|
||
|
l.pos1_x = (virtual_res_x - l.te1.width) / 2 - l.te1.x_bearing
|
||
|
l.pos2_x = (virtual_res_x - l.te2.width) / 2 - l.te2.x_bearing
|
||
|
l.pos1_y = tl_pos_y
|
||
|
l.pos2_y = tl_pos_y - l.fe.height
|
||
|
|
||
|
c.move_to(l.pos1_x, l.pos1_y)
|
||
|
c.text_path(line1)
|
||
|
c.move_to(l.pos2_x, l.pos2_y)
|
||
|
c.text_path(line2)
|
||
|
|
||
|
paint_text(textsurf, c)
|
||
|
|
||
|
l.textsurf = textsurf
|
||
|
end
|
||
|
|
||
|
paint_kara_text(f, ctx, t, l)
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Paint a zoom circle onto the video
|
||
|
-- zp is one of the zoompoint structures generated in the style functions
|
||
|
function draw_zoompoint(surf, ctx, t, zp)
|
||
|
if zp.size < 5 then return end
|
||
|
|
||
|
local zoom, zoom_width, zoom_height = make_zoomed_ellipsis(surf, zp.cx, zp.cy, zp.size*1.2, zp.size*1.2)
|
||
|
|
||
|
local glow = cairo.image_surface_create(zoom_width+50, zoom_height+50, "argb32")
|
||
|
local gc = glow.create_context()
|
||
|
|
||
|
-- Hue-rotation
|
||
|
-- Based on HSL-to-RGB code from Aegisub
|
||
|
local r, g, b
|
||
|
local cspeed = 1/5
|
||
|
local sat = 69
|
||
|
local q = math.floor((cspeed*t) % 6)
|
||
|
local qf = ((cspeed*t) % 6 - q) * (255-sat)
|
||
|
if q == 0 then
|
||
|
r = 255
|
||
|
g = sat + qf
|
||
|
b = sat
|
||
|
elseif q == 1 then
|
||
|
r = sat + 255 - qf
|
||
|
g = 255
|
||
|
b = sat
|
||
|
elseif q == 2 then
|
||
|
r = sat
|
||
|
g = 255
|
||
|
b = sat + qf
|
||
|
elseif q == 3 then
|
||
|
r = sat
|
||
|
g = sat + 255 - qf
|
||
|
b = 255
|
||
|
elseif q == 4 then
|
||
|
r = sat + qf
|
||
|
g = qf
|
||
|
b = 255
|
||
|
elseif q == 5 then
|
||
|
r = 255
|
||
|
g = sat
|
||
|
b = qf + 255 - qf
|
||
|
end
|
||
|
-- Circle-tail-chaser thing
|
||
|
-- Just a bunch of increasingly opaque lines drawn from a center
|
||
|
-- and overlapping enough to create a sense of continuity.
|
||
|
gc.set_line_width(6)
|
||
|
for a = 0, 1, 1/zoom_height do
|
||
|
gc.set_source_rgba(r/255, g/255, b/255, a)
|
||
|
gc.move_to(zoom_width/2+25, zoom_height/2+25)
|
||
|
gc.rel_line_to((zoom_width/2+5) * math.sin(-t*8-a*math.pi*2), (zoom_height/2+5) * math.cos(-t*8-a*math.pi*2))
|
||
|
gc.stroke()
|
||
|
end
|
||
|
-- Love gaussian blur!
|
||
|
raster.gaussian_blur(glow, 2)
|
||
|
|
||
|
-- Use additive blend to put the tail-chaser onto the video
|
||
|
ctx.set_source_surface(glow, zp.cx-zoom_width/2-25, zp.cy-zoom_height/2-25)
|
||
|
local oldop = ctx.get_operator()
|
||
|
ctx.set_operator("add")
|
||
|
ctx.paint()
|
||
|
ctx.set_operator(oldop)
|
||
|
|
||
|
-- And regular blend for the zoom circle
|
||
|
ctx.set_source_surface(zoom, zp.cx-zoom_width/2, zp.cy-zoom_height/2)
|
||
|
ctx.paint()
|
||
|
end
|
||
|
|
||
|
|
||
|
function render_frame(f, t)
|
||
|
-- Make sure we're initialised
|
||
|
init()
|
||
|
|
||
|
-- Clear the list of zoom points
|
||
|
zoompoints = {}
|
||
|
|
||
|
-- Create a surface and context from the video
|
||
|
local worksurf = f.create_cairo_surface()
|
||
|
local workctx = worksurf.create_context()
|
||
|
-- This should make it possible to render on different resolution videos,
|
||
|
-- but I don't think it works
|
||
|
workctx.scale(f.width / virtual_res_x, f.height / virtual_res_y)
|
||
|
|
||
|
-- Run over each input line, processing it
|
||
|
-- This will draw the main text and transition effects
|
||
|
for i, line in pairs(lines) do
|
||
|
if stylefunc[line.style] then
|
||
|
stylefunc[line.style](worksurf, workctx, t, line)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Then go over the zoom points and draw those on top
|
||
|
-- If this isn't done after all lines have been drawn, lines that are close
|
||
|
-- to each other could end up overlapping each others' zoom circles.
|
||
|
for i, zp in pairs(zoompoints) do
|
||
|
draw_zoompoint(worksurf, workctx, t, zp)
|
||
|
end
|
||
|
|
||
|
-- If we're fading it all out, make the karaoke less visible by doing
|
||
|
-- an alpha paint over with the original video frame.
|
||
|
if fade_all_out then
|
||
|
local vidsurf = f.create_cairo_surface()
|
||
|
workctx.set_source_surface(vidsurf, 0, 0)
|
||
|
workctx.paint_with_alpha(1-fade_all_out)
|
||
|
end
|
||
|
|
||
|
-- Finally put the video frame back
|
||
|
f.overlay_cairo_surface(worksurf, 0, 0)
|
||
|
end
|