diff --git a/rslib/src/image_occlusion/imageocclusion.rs b/rslib/src/image_occlusion/imageocclusion.rs index 507abcfea..8d51fae5b 100644 --- a/rslib/src/image_occlusion/imageocclusion.rs +++ b/rslib/src/image_occlusion/imageocclusion.rs @@ -128,6 +128,11 @@ pub fn get_image_cloze_data(text: &str) -> String { result.push_str(&format!("data-scale=\"{}\" ", property.value)); } } + "fs" => { + if !property.value.is_empty() { + result.push_str(&format!("data-font-size=\"{}\" ", property.value)); + } + } _ => {} } } diff --git a/ts/routes/image-occlusion/review.ts b/ts/routes/image-occlusion/review.ts index e89c4eb9b..52748f895 100644 --- a/ts/routes/image-occlusion/review.ts +++ b/ts/routes/image-occlusion/review.ts @@ -291,23 +291,41 @@ function drawShape({ ctx.restore(); } else if (shape instanceof Text) { ctx.save(); - ctx.font = `40px ${TEXT_FONT_FAMILY}`; + ctx.font = `${shape.fontSize}px ${TEXT_FONT_FAMILY}`; ctx.textBaseline = "top"; ctx.scale(shape.scaleX, shape.scaleY); - const textMetrics = ctx.measureText(shape.text); + const lines = shape.text.split("\n"); + const baseMetrics = ctx.measureText("M"); + const fontHeight = baseMetrics.actualBoundingBoxAscent + baseMetrics.actualBoundingBoxDescent; + const lineHeight = 1.5 * fontHeight; + const linePositions: { text: string; x: number; y: number; width: number; height: number }[] = []; + let maxWidth = 0; + let totalHeight = 0; + for (let i = 0; i < lines.length; i++) { + const textMetrics = ctx.measureText(lines[i]); + linePositions.push({ + text: lines[i], + x: shape.left / shape.scaleX, + y: shape.top / shape.scaleY + i * lineHeight, + width: textMetrics.width, + height: lineHeight, + }); + if (textMetrics.width > maxWidth) { + maxWidth = textMetrics.width; + } + totalHeight += lineHeight; + } ctx.fillStyle = TEXT_BACKGROUND_COLOR; ctx.fillRect( shape.left / shape.scaleX, shape.top / shape.scaleY, - textMetrics.width + TEXT_PADDING, - textMetrics.actualBoundingBoxDescent + TEXT_PADDING, + maxWidth + TEXT_PADDING, + totalHeight + TEXT_PADDING, ); ctx.fillStyle = "#000"; - ctx.fillText( - shape.text, - shape.left / shape.scaleX, - shape.top / shape.scaleY, - ); + for (const line of linePositions) { + ctx.fillText(line.text, line.x, line.y); + } ctx.restore(); } } diff --git a/ts/routes/image-occlusion/shapes/from-cloze.ts b/ts/routes/image-occlusion/shapes/from-cloze.ts index 392445832..71c0e91a8 100644 --- a/ts/routes/image-occlusion/shapes/from-cloze.ts +++ b/ts/routes/image-occlusion/shapes/from-cloze.ts @@ -74,6 +74,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null { points: cloze.dataset.points, text: cloze.dataset.text, scale: cloze.dataset.scale, + fs: cloze.dataset.fontSize, }; return buildShape(type, props); } @@ -118,11 +119,15 @@ function buildShape(type: ShapeType, props: Record): Shape { return new Polygon(props); } case "text": { - return new Text({ + const textProps: Record = { ...props, scaleX: parseFloat(props.scale), scaleY: parseFloat(props.scale), - }); + }; + if (props.fs) { + textProps.fontSize = parseFloat(props.fs); + } + return new Text(textProps); } } } diff --git a/ts/routes/image-occlusion/shapes/text.ts b/ts/routes/image-occlusion/shapes/text.ts index 329dc0825..a8043670a 100644 --- a/ts/routes/image-occlusion/shapes/text.ts +++ b/ts/routes/image-occlusion/shapes/text.ts @@ -3,7 +3,7 @@ import { fabric } from "fabric"; -import { TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "../tools/lib"; +import { TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TEXT_PADDING } from "../tools/lib"; import type { ConstructorParams, Size } from "../types"; import type { ShapeDataForCloze } from "./base"; import { Shape } from "./base"; @@ -13,17 +13,20 @@ export class Text extends Shape { text: string; scaleX: number; scaleY: number; + fontSize: number | undefined; constructor({ text = "", scaleX = 1, scaleY = 1, + fontSize, ...rest }: ConstructorParams = {}) { super(rest); this.text = text; this.scaleX = scaleX; this.scaleY = scaleY; + this.fontSize = fontSize; } toDataForCloze(): TextDataForCloze { @@ -32,6 +35,7 @@ export class Text extends Shape { text: this.text, // scaleX and scaleY are guaranteed to be equal since we lock the aspect ratio scale: floatToDisplay(this.scaleX), + fs: this.fontSize ? floatToDisplay(this.fontSize) : undefined, }; } @@ -42,12 +46,15 @@ export class Text extends Shape { fontFamily: TEXT_FONT_FAMILY, backgroundColor: TEXT_BACKGROUND_COLOR, padding: TEXT_PADDING, + lineHeight: 1, }); } toNormal(size: Size): Text { + const fontSize = this.fontSize ? this.fontSize : TEXT_FONT_SIZE; return new Text({ ...this, + fontSize: fontSize / size.height, ...super.normalPosition(size), }); } @@ -55,6 +62,7 @@ export class Text extends Shape { toAbsolute(size: Size): Text { return new Text({ ...this, + fontSize: this.fontSize ? this.fontSize * size.height : TEXT_FONT_SIZE, ...super.absolutePosition(size), }); } @@ -63,4 +71,5 @@ export class Text extends Shape { interface TextDataForCloze extends ShapeDataForCloze { text: string; scale: string; + fs: string | undefined; } diff --git a/ts/routes/image-occlusion/tools/lib.ts b/ts/routes/image-occlusion/tools/lib.ts index 541c9bb69..3f7483949 100644 --- a/ts/routes/image-occlusion/tools/lib.ts +++ b/ts/routes/image-occlusion/tools/lib.ts @@ -11,6 +11,7 @@ export const BORDER_COLOR = "#212121"; export const TEXT_BACKGROUND_COLOR = "#ffffff"; export const TEXT_FONT_FAMILY = "Arial"; export const TEXT_PADDING = 5; +export const TEXT_FONT_SIZE = 40; let _clipboard; diff --git a/ts/routes/image-occlusion/tools/tool-text.ts b/ts/routes/image-occlusion/tools/tool-text.ts index e69553202..ee3fb1fb6 100644 --- a/ts/routes/image-occlusion/tools/tool-text.ts +++ b/ts/routes/image-occlusion/tools/tool-text.ts @@ -44,6 +44,7 @@ export const drawText = (canvas: fabric.Canvas): void => { backgroundColor: TEXT_BACKGROUND_COLOR, padding: TEXT_PADDING, opacity: get(opacityStateStore) ? 0.4 : 1, + lineHeight: 1, }); text["id"] = "text-" + new Date().getTime();