diff --git a/OverLua/docs/sample4.lua b/OverLua/docs/sample4.lua new file mode 100644 index 000000000..1f8cb9563 --- /dev/null +++ b/OverLua/docs/sample4.lua @@ -0,0 +1,432 @@ +--[[ + +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 +