--[[ Sample script for OverLua - advanced karaoke effect, Prism Ark OP kara effect for Anime-Share Fansubs 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. Not an extremely advanced effect, but showcases improved parsing of ASS files and using information from the Style lines for the styling information. Pretty fast to render at SD resolutions. As for other effects, please consider that it's not much fun to just re-use an effect someone else wrote, especially not verbatim. If you elect to use this sample for something, I ask you to do something original with it. I can't force you, but please :) I'm leaving several sections of this script mostly unexplained, because I've for a large part copied those from the Gundam 00 OP 1 effect (sample3) I did a few days before this one. Please see sample3.lua for explanations of those, if you need them. ]] ---- START CONFIGURATION ---- -- Duration of line fade-in/outs, in seconds local line_fade_duration = 0.5 -- Minimum duration of highlights, also seconds local syl_highlight_duration = 0.5 ---- END CONFIGURATION ---- -- Trim spaces from beginning and end of string function string.trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end -- Script and video resolutions local sres_x, sres_y local vres_x, vres_y -- Stuff read from style definitions local font_name = {} local font_size = {} local font_bold = {} local font_italic = {} local pos_v = {} local vertical = {} local color1, color2, color3, color4 = {}, {}, {}, {} -- Input lines local lines = {} -- Read input file function read_field(ass_line, num) local val, rest = ass_line:match("(.-),(.*)") if not rest then return ass_line, "" elseif num > 1 then return val, read_field(rest, num-1) else return val, rest end end 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_style_color(color) local res = {r = 0, g = 0, b = 0, a = 0} local a, b, g, r = color:match("&H(%x%x)(%x%x)(%x%x)(%x%x)") res.r = tonumber(r, 16) / 255 res.g = tonumber(g, 16) / 255 res.b = tonumber(b, 16) / 255 res.a = 1 - tonumber(a, 16) / 255 -- Alpha has inverse meaning in ASS and cairo return res end function parse_k_timing(text, start_time) local syls = {} local cleantext = "" local i = 1 local curtime = start_time for timing, syltext in text:gmatch("{\\k(%d+)}([^{]*)") do local syl = {} syl.dur = parsenum(timing)/100 syl.text = syltext syl.i = i syl.start_time = curtime syl.end_time = curtime + syl.dur table.insert(syls, syl) cleantext = cleantext .. syl.text i = i + 1 curtime = curtime + syl.dur end if cleantext == "" then cleantext = text end return syls, cleantext end function read_input_file(name) for line in io.lines(name) do -- Try PlayResX/PlayResY local playresx = line:match("^PlayResX: (.*)") if playresx then sres_x = parsenum(playresx) end local playresy = line:match("^PlayResY: (.*)") if playresy then sres_y = parsenum(playresy) end -- Try dialogue line -- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text local dialogue_line = line:match("^Dialogue:(.*)") if dialogue_line then local layer, start_time, end_time, style, actor, margin_l, margin_r, margin_v, effect, text = read_field(dialogue_line, 9) local ls = {} ls.layer = parsenum(layer) ls.start_time = parse_ass_time(start_time) ls.end_time = parse_ass_time(end_time) ls.style = style:trim() ls.actor = actor:trim() ls.effect = effect:trim() ls.rawtext = text ls.kara, ls.cleantext = parse_k_timing(text, ls.start_time) table.insert(lines, ls) end -- Try style line -- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding local style_line = line:match("^Style:(.*)") if style_line then local name, font, size, c1, c2, c3, c4, bold, italic, underline, overstrike, scalex, scaley, spacing, angle, borderstyle, outline, shadow, alignment, margin_l, margin_r, margin_v, encoding = read_field(style_line, 22) -- Direct data name = name:trim() font_name[name] = font:trim() font_size[name] = parsenum(size) color1[name] = parse_style_color(c1) color2[name] = parse_style_color(c2) color3[name] = parse_style_color(c3) color4[name] = parse_style_color(c4) font_bold[name] = (parsenum(bold) ~= 0) and "bold" or "" font_italic[name] = (parsenum(italic) ~= 0) and "italic" or "" -- Derived data if font:match("@") then vertical[name] = true end alignment = parsenum(alignment) if alignment <= 3 then if vertical[name] then pos_v[name] = sres_x - parsenum(margin_v) else pos_v[name] = sres_y - parsenum(margin_v) end elseif alignment <= 6 then if vertical[name] then pos_v[name] = sres_x / 2 else pos_v[name] = sres_y / 2 end else pos_v[name] = parsenum(margin_v) end end end end function init(f) if inited then return end inited = true vres_x = f.width vres_y = f.height read_input_file(overlua_datastring) end -- Mask for noise over background local noisemask, noisemaskctx, noisemaskfilled -- Additional images to overlay the frame local frame_overlays = {} -- Calculate size and position of a line and its syllables function calc_line_metrics(ctx, line) if line.pos_x then return end assert(font_name[line.style], "No font name for style " .. line.style) ctx.select_font_face(font_name[line.style], font_italic[line.style], font_bold[line.style]) ctx.set_font_size(font_size[line.style]) line.te = ctx.text_extents(line.cleantext) line.fe = ctx.font_extents() if vertical[line.style] then line.pos_x = pos_v[line.style] line.pos_y = (sres_y - line.te.width) / 2 - line.te.x_bearing else line.pos_x = (sres_x - line.te.width) / 2 - line.te.x_bearing line.pos_y = pos_v[line.style] end if #line.kara < 2 then return end local curx = line.pos_x local cury = line.pos_y for i, syl in pairs(line.kara) do syl.te = ctx.text_extents(syl.text) if vertical[line.style] then syl.pos_x = line.pos_x syl.pos_y = cury syl.center_x = syl.pos_x + syl.te.x_bearing + syl.te.width/2 syl.center_y = cury - line.fe.ascent/2 + line.fe.descent/2 else syl.pos_x = curx syl.pos_y = line.pos_y syl.center_x = curx + syl.te.x_bearing + syl.te.width/2 syl.center_y = syl.pos_y - line.fe.ascent/2 + line.fe.descent/2 end curx = curx + syl.te.x_advance cury = cury + syl.te.x_advance end end -- Style handling functions local stylefunc = {} function stylefunc.generic(t, line) if not line.textsurf then line.textsurf = cairo.image_surface_create(sres_x, sres_y, "argb32") local c = line.textsurf.create_context() c.select_font_face(font_name[line.style], font_italic[line.style], font_bold[line.style]) c.set_font_size(font_size[line.style]) if vertical[line.style] then c.translate(line.pos_x, line.pos_y) c.rotate(math.pi/2) c.move_to(0,0) else c.move_to(line.pos_x, line.pos_y) end c.text_path(line.cleantext) local c1, c3 = color1[line.style], color3[line.style] c.set_source_rgba(c1.r, c1.g, c1.b, c1.a) c.set_line_join("round") c.set_line_width(4) c.stroke_preserve() c.set_source_rgba(c3.r, c3.g, c3.b, c3.a) c.fill() end -- Fade-factor (alpha for line) local fade = 0 if t < line.start_time and t >= line.start_time - line_fade_duration then fade = 1 - (line.start_time - t) / line_fade_duration elseif t >= line.end_time and t < line.end_time + line_fade_duration then fade = 1 - (t - line.end_time) / line_fade_duration elseif t >= line.start_time and t < line.end_time then fade = 1 else fade = 0 end if fade > 0 then local lo = {} -- line overlay lo.surf = line.textsurf lo.x, lo.y = 0, 0 lo.alpha = 0.85 * fade lo.operator = "over" lo.name = "line" table.insert(frame_overlays, lo) noisemaskctx.set_source_surface(line.textsurf, 0, 0) noisemaskctx.paint_with_alpha(fade) noisemaskfilled = true end for i, syl in pairs(line.kara) do if syl.end_time < syl.start_time + syl_highlight_duration then syl.end_time = syl.start_time + syl_highlight_duration end if t >= syl.start_time and t < syl.end_time then local sw, sh = syl.te.width*3, line.fe.height*3 if vertical[line.style] then sw, sh = sh, sw end local fade = (syl.end_time - t) / (syl.end_time - syl.start_time) local surf = cairo.image_surface_create(sw, sh, "argb32") local ctx = surf.create_context() ctx.select_font_face(font_name[line.style], font_italic[line.style], font_bold[line.style]) ctx.set_font_size(font_size[line.style]*2) local te, fe = ctx.text_extents(syl.text), ctx.font_extents() local rx, ry = (sw - te.width) / 2 + te.x_bearing, (sh - fe.height) / 2 + fe.ascent assert(not vertical[line.style], "Can't handle vertical kara in syllable highlight code - poke jfs if you need this") ctx.move_to(rx, ry) ctx.text_path(syl.text) local path = ctx.copy_path() local function modpath(x, y) local cx = math.sin(y/sh*math.pi) local cy = math.sin(x/sw*math.pi) cx = cx * x + (1-cx)/2*sw cy = cy * y + (1-cy)/2*sh return fade*x+(1-fade)*cx, fade*y+(1-fade)*cy end path.map_coords(modpath) ctx.new_path() ctx.append_path(path) local c2, c3 = color2[line.style], color3[line.style] for b = 8, 1, -3 do local bs = cairo.image_surface_create(sw, sh, "argb32") local bc = bs.create_context() bc.set_source_rgba(c2.r, c2.g, c2.b, c2.a) bc.append_path(path) bc.fill() raster.gaussian_blur(bs, b) local bo = {} bo.surf = bs bo.x = syl.center_x - sw/2 bo.y = syl.center_y - sh/2 bo.alpha = fade bo.operator = "add" bo.name = "blur " .. b table.insert(frame_overlays, bo) end ctx.set_source_rgba(c3.r, c3.g, c3.b, c3.a) ctx.set_line_join("round") ctx.set_operator("over") ctx.set_line_width(3*fade) ctx.stroke_preserve() ctx.set_operator("dest_out") ctx.fill() raster.box_blur(surf, 3, 2) local so = {} so.surf = surf so.x = syl.center_x - sw/2 so.y = syl.center_y - sh/2 so.alpha = 1 so.operator = "over" so.name = string.format("bord %s %.1f %.1f (%.1f,%.1f)", syl.text, sw, sh, rx, ry) table.insert(frame_overlays, so) end end end stylefunc.Romaji = stylefunc.generic stylefunc.Kanji = stylefunc.generic stylefunc.English = stylefunc.generic -- Main rendering function function render_frame(f, t) init(f) local surf = f.create_cairo_surface() local ctx = surf.create_context() ctx.scale(vres_x/sres_x, vres_y/sres_y) -- The line rendering functions add the mask of the line they rendered to -- this image. It will be used to draw the glow around all lines. -- It has to be done in this way to avoid the glows from nearby lines to -- interfere and produce double effect. noisemask = cairo.image_surface_create(sres_x, sres_y, "argb32") noisemaskctx = noisemask.create_context() -- Set to true as soon as anything is put into the noise mask -- This is merely an optimisation to avoid doing anything when there aren't -- any lines on screen. noisemaskfilled = false -- List of images to overlay on the video frame, after the noise mask. frame_overlays = {} for i, line in pairs(lines) do if stylefunc[line.style] then calc_line_metrics(ctx, line) stylefunc[line.style](t, line) end end if noisemaskfilled then -- Greenish and jittery version of the frame local noiseimg = f.create_cairo_surface() raster.box_blur(noiseimg, 5, 2) raster.pixel_value_map(noiseimg, "G rand 0.4 * + =G G 1 - 1 G ifgtz =G") -- Blurred version of the noisemask raster.gaussian_blur(noisemask, 8) -- Mask additive paint the noise mask: only show the area near the text -- and have it do interesting things with the video. ctx.set_source_surface(noiseimg, 0, 0) ctx.set_operator("add") ctx.mask_surface(noisemask, 0, 0) end -- Paint generated overlays onto the video. for i, o in pairs(frame_overlays) do ctx.set_source_surface(o.surf, o.x, o.y) ctx.set_operator(o.operator) ctx.paint_with_alpha(o.alpha) end f.overlay_cairo_surface(surf, 0, 0) end