From c4d8aaceb8aa1ce286dec5dee1ae19ffc34b67f3 Mon Sep 17 00:00:00 2001 From: luxluth Date: Mon, 10 Jul 2023 16:45:39 +0200 Subject: [PATCH] Drawing Basic Texts --- src/index.ts | 10 +-- src/renderer.ts | 228 +++++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 37 +++++++- src/utils.ts | 6 +- tsconfig.json | 3 +- 5 files changed, 269 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index cfa6563..0ff672f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,15 +19,11 @@ export default class ASS { init() { if (typeof this.video == 'string') { this.videoElement = document.querySelector(this.video) - if (this.videoElement === null) { - throw new Error("Unable to find the video element") - } - } else { - this.videoElement = this.video - } + if (this.videoElement === null) { throw new Error("Unable to find the video element") } + } else { this.videoElement = this.video } this.setCanvasSize() - this.renderer = new Renderer(parse(this.assText), this.canvas as HTMLCanvasElement) + this.renderer = new Renderer(parse(this.assText), this.canvas as HTMLCanvasElement, this.videoElement) this.videoElement?.addEventListener('loadedmetadata', () => { this.setCanvasSize() }) diff --git a/src/renderer.ts b/src/renderer.ts index 19c9ce3..59777e2 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,11 +1,26 @@ -import type { ParsedASS } from "ass-compiler"; +import type { ParsedASS, ParsedASSEventText } from "ass-compiler"; +import { SingleStyle, FontDescriptor } from "./types"; +import { ruleOfThree, convertAegisubToRGBA } from "./utils"; + export class Renderer { parsedAss: ParsedASS canvas: HTMLCanvasElement ctx: CanvasRenderingContext2D - constructor(parsedASS: ParsedASS, canvas: HTMLCanvasElement) { + video: HTMLVideoElement + playerResX: number + playerResY: number + styles: SingleStyle[] + constructor( + parsedASS: ParsedASS, + canvas: HTMLCanvasElement, + video: HTMLVideoElement + ) { this.parsedAss = parsedASS + this.playerResX = parseFloat(this.parsedAss.info.PlayResX) + this.playerResY = parseFloat(this.parsedAss.info.PlayResY) + this.styles = parsedASS.styles.style as SingleStyle[] this.canvas = canvas + this.video = video this.ctx = canvas.getContext("2d") as CanvasRenderingContext2D if (this.ctx === null) { throw new Error("Unable to initilize the Canvas 2D context") } let data = [ @@ -15,6 +30,211 @@ export class Renderer { ] console.debug(data) } - render() {} - redraw() {} + + render() { + this.video.addEventListener('timeupdate', () => { + this.diplay(this.video.currentTime) + }) + } + + redraw() { this.diplay(this.video.currentTime) } + + diplay(time: number) { + const overlappingDialoguesEvents = this.parsedAss.events.dialogue.filter(event => + event.Start <= time && event.End >= time + ); + + // Clear the canvas + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + overlappingDialoguesEvents.forEach(event => { + const { Style, Text } = event; + const style = this.getStyle(Style); + if (style === undefined) { return; } + this.showText(Text, style); + }); + } + + /* + type SingleStyle = { + Name: string; + Fontname: string; + Fontsize: string; + PrimaryColour: string; + SecondaryColour: string; + OutlineColour: string; + BackColour: string; + Bold: string; + Italic: string; + Underline: string; + StrikeOut: string; + ScaleX: string; + ScaleY: string; + Spacing: string; + Angle: string; + BorderStyle: string; + Outline: string; + Shadow: string; + Alignment: string; + MarginL: string; + MarginR: string; + MarginV: string; + Encoding: string; + } + */ + + showText(Text: ParsedASSEventText, style: SingleStyle) { + console.debug(style.Name, Text) + let fontDescriptor = this.getFontDescriptor(style) // FontDescriptor + let c1 = convertAegisubToRGBA(style.PrimaryColour) // primary color + let c2 = convertAegisubToRGBA(style.SecondaryColour) // secondary color + let c3 = convertAegisubToRGBA(style.OutlineColour) // outline color + let c4 = convertAegisubToRGBA(style.BackColour) // shadow color + let marginL = ruleOfThree(this.playerResX, this.canvas.width) * parseFloat(style.MarginL) / 100 + let marginV = ruleOfThree(this.playerResY, this.canvas.height) * parseFloat(style.MarginV) / 100 + let marginR = ruleOfThree(this.playerResX, this.canvas.width) * parseFloat(style.MarginR) / 100 + // console.debug(marginL, marginV, marginR) + let text = Text.parsed[0]?.text as string + + this.ctx.font = `${fontDescriptor.fontsize}px ${fontDescriptor.bold ? "bold" : ""} ${fontDescriptor.italic ? "italic" : ""} ${fontDescriptor.fontname}`; + console.debug(this.ctx.font) + let textAlign = this.getAlignment(parseInt(style.Alignment)) as CanvasTextAlign; + let textBaseline = this.getBaseLine(parseInt(style.Alignment)) as CanvasTextBaseline; + this.ctx.fillStyle = c1; + this.ctx.strokeStyle = c3; + this.ctx.lineWidth = ruleOfThree(this.playerResX, this.canvas.width) * parseFloat(style.Outline) / 100 * 2; + console.debug(this.ctx.lineWidth, style.Outline) + this.ctx.lineJoin = "round"; + this.ctx.lineCap = "round"; + this.ctx.miterLimit = 2; + this.ctx.shadowColor = c4; + this.ctx.shadowBlur = ruleOfThree(this.playerResX, this.canvas.width) * parseFloat(style.Shadow) / 100; + this.ctx.shadowOffsetX = ruleOfThree(this.playerResX, this.canvas.width) * parseFloat(style.Shadow) / 100; + this.ctx.shadowOffsetY = ruleOfThree(this.playerResY, this.canvas.height) * parseFloat(style.Shadow) / 100; + + this.drawText(text, textAlign, textBaseline, marginL, marginV, marginR); + } + + drawText( + text: string, + textAlign: CanvasTextAlign, + textBaseline: CanvasTextBaseline, + marginL: number, + marginV: number, + marginR: number, + ) { + let lines = text.split("\\N"); + let lineHeights = lines.map(line => this.ctx.measureText(line).actualBoundingBoxAscent + this.ctx.measureText(line).actualBoundingBoxDescent); + let lineHeight = Math.max(...lineHeights); + let totalHeight = lineHeight * lines.length; + let y = 0; + switch (textBaseline) { + case "top": + y = marginV + lineHeight; + if (lines.length === 1) { y -= lineHeight; } else { y -= totalHeight / lines.length; } + break; + case "middle": + y = (this.canvas.height - totalHeight) / 2 + lineHeight; + break; + case "bottom": + y = this.canvas.height - marginV; + if (lines.length === 1) { y -= lineHeight; } else { y -= totalHeight / lines.length; } + break; + default: + y = marginV + lineHeight; + break; + } + + lines.forEach(line => { + let lineWidth = this.ctx.measureText(line).width; + let x = 0; + switch (textAlign) { + case "left": + x = marginL; + break; + case "center": + x = (this.canvas.width - lineWidth) / 2; + break; + case "right": + x = this.canvas.width - marginR - lineWidth; + break; + default: + x = marginL; + break; + } + this.ctx.fillText(line, x, y); + this.ctx.strokeText(line, x, y); + y += lineHeight; + }) + } + + getAlignment(alignment: number) { + // 1 = (bottom) left + // 2 = (bottom) center + // 3 = (bottom) right + // 4 = (middle) left + // 5 = (middle) center + // 6 = (middle) right + // 7 = (top) left + // 8 = (top) center + // 9 = (top) right + switch (alignment) { + case 1: + case 4: + case 7: + return "left"; + case 2: + case 5: + case 8: + return "center"; + case 3: + case 6: + case 9: + return "right"; + default: + return "start"; + } + } + + getBaseLine(alignment: number) { + // 1 = (bottom) left + // 2 = (bottom) center + // 3 = (bottom) right + // 4 = (middle) left + // 5 = (middle) center + // 6 = (middle) right + // 7 = (top) left + // 8 = (top) center + // 9 = (top) right + switch (alignment) { + case 1: + case 2: + case 3: + return "bottom"; + case 4: + case 5: + case 6: + return "middle"; + case 7: + case 8: + case 9: + return "top"; + default: + return "alphabetic"; + } + } + + getStyle(styleName: string) {return this.styles.find(style => style.Name === styleName)} + + getFontDescriptor(style: SingleStyle): FontDescriptor { + const fontsize = ruleOfThree(this.playerResY, this.canvas.height) * parseFloat(style.Fontsize) / 100; + return { + fontname: style.Fontname, + fontsize: fontsize, + bold: style.Bold === "-1", + italic: style.Italic === "-1", + underline: style.Underline === "-1", + strikeout: style.StrikeOut === "-1", + }; + } } diff --git a/src/types.ts b/src/types.ts index 892e367..a4ff5bd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,42 @@ export type ASSOptions = { assText: string, /** * The video to display the subtile on. - * Can be either an `HTMLVideoElement` or `string` (html query selector) + * Can be either an `HTMLVideoElement` or `string` (html query selector ) */ video: HTMLVideoElement | string } + +export type SingleStyle = { + Name: string + Fontname: string + Fontsize: string + PrimaryColour: string + SecondaryColour: string + OutlineColour: string + BackColour: string + Bold: string + Italic: string + Underline: string + StrikeOut: string + ScaleX: string + ScaleY: string + Spacing: string + Angle: string + BorderStyle: string + Outline: string + Shadow: string + Alignment: string + MarginL: string + MarginR: string + MarginV: string + Encoding: string +} + +export type FontDescriptor = { + fontname: string + fontsize: number + bold: boolean + italic: boolean + underline: boolean + strikeout: boolean +} diff --git a/src/utils.ts b/src/utils.ts index 8f2cf2c..7c172ac 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,8 +9,9 @@ export function convertAegisubToRGBA(aegisubColor: string) { let red = parseInt(aegisubColor.slice(2, 4), 16) let green = parseInt(aegisubColor.slice(4, 6), 16) let blue = parseInt(aegisubColor.slice(6, 8), 16) - - return 'rgba(' + red + ',' + green + ',' + blue + ',' + alpha + ')' + let clr = `rgba(${red}, ${green}, ${blue}, ${alpha})` + console.debug(clr, aegisubColor) + return `rgba(${red}, ${green}, ${blue}, ${alpha})` } export function ruleOfThree( @@ -34,6 +35,7 @@ export function genRandomString(ln: number) { export function newCanvas(top: number, left: number, width: number, height: number, insertAfter?: HTMLElement, zIndex?: number) { const canvas = document.createElement('canvas'); + canvas.id = "ASSRendererCanvas-" + genRandomString(10); canvas.style.position = 'absolute'; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; diff --git a/tsconfig.json b/tsconfig.json index b5d4f6f..e41aee9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */, "noUncheckedIndexedAccess": true, "noEmit": true, - "lib": ["esnext", "dom"] + "lib": ["esnext", "dom"], + "removeComments": true, } }