fix(POSTV1-TEMPLATE-LAYOUT-19): 按原始文本框修复模板偏移
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type TextTemplateNinePatch,
|
||||
type TextTemplateParticleLayer,
|
||||
type TextTemplateTextLayer,
|
||||
type TextVerticalAlign,
|
||||
} from "./text-assets.js";
|
||||
import { COLOR_CARD_HALF_SIZES, drawColorCard } from "./palette-provider.js";
|
||||
|
||||
@@ -201,6 +202,32 @@ function drawTemplateParticles(
|
||||
context.restore();
|
||||
}
|
||||
|
||||
interface TextTemplateBoxLayoutInput {
|
||||
align: "center" | "left" | "right";
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
boxHeight: number;
|
||||
boxWidth: number;
|
||||
fontSize: number;
|
||||
lineCount: number;
|
||||
lineHeight: number;
|
||||
verticalAlign: TextVerticalAlign;
|
||||
}
|
||||
|
||||
export function textTemplateBoxLayout(input: TextTemplateBoxLayoutInput) {
|
||||
const boxLeft = -input.anchorX * input.boxWidth;
|
||||
const boxTop = -input.anchorY * input.boxHeight;
|
||||
const lineAdvance = input.fontSize * input.lineHeight;
|
||||
const blockHeight = input.fontSize + Math.max(0, input.lineCount - 1) * lineAdvance;
|
||||
const x = input.align === "left" ? boxLeft
|
||||
: input.align === "right" ? boxLeft + input.boxWidth
|
||||
: boxLeft + input.boxWidth / 2;
|
||||
const firstY = input.verticalAlign === "top" ? boxTop + input.fontSize / 2
|
||||
: input.verticalAlign === "bottom" ? boxTop + input.boxHeight - blockHeight + input.fontSize / 2
|
||||
: boxTop + (input.boxHeight - blockHeight) / 2 + input.fontSize / 2;
|
||||
return { blockHeight, firstY, lineAdvance, x };
|
||||
}
|
||||
|
||||
function drawTemplateTextLayer(
|
||||
context: CanvasRenderingContext2D,
|
||||
element: CanvasState["elements"][number],
|
||||
@@ -212,7 +239,7 @@ function drawTemplateTextLayer(
|
||||
const fontSize = editable ? element.font_size ?? layer.fontSize : layer.fontSize;
|
||||
const lineHeight = editable ? styleValue(element, "line_height", layer.lineHeight) : layer.lineHeight;
|
||||
const letterSpacing = editable ? styleValue(element, "letter_spacing", layer.letterSpacing) : layer.letterSpacing;
|
||||
const align = (editable ? styleValue(element, "text_align", layer.align) : layer.align) as CanvasTextAlign;
|
||||
const align = (editable ? styleValue(element, "text_align", layer.align) : layer.align) as TextTemplateTextLayer["align"];
|
||||
const lines = (editable ? element.content ?? layer.text : layer.text).split("\n");
|
||||
context.save();
|
||||
context.globalAlpha *= layer.alpha;
|
||||
@@ -225,12 +252,23 @@ function drawTemplateTextLayer(
|
||||
context.textBaseline = "middle";
|
||||
const widths = lines.map((line) => context.measureText(line).width + Math.max(0, Array.from(line).length - 1) * letterSpacing);
|
||||
const textWidth = Math.max(1, ...widths);
|
||||
const textHeight = Math.max(fontSize * lineHeight, lines.length * fontSize * lineHeight);
|
||||
const layout = textTemplateBoxLayout({
|
||||
align,
|
||||
anchorX: layer.anchorX,
|
||||
anchorY: layer.anchorY,
|
||||
boxHeight: layer.height,
|
||||
boxWidth: layer.width,
|
||||
fontSize,
|
||||
lineCount: lines.length,
|
||||
lineHeight,
|
||||
verticalAlign: layer.verticalAlign,
|
||||
});
|
||||
if (editable && styleValue(element, "background_enabled", false)) {
|
||||
const alpha = context.globalAlpha;
|
||||
context.globalAlpha = alpha * styleValue(element, "background_opacity", 1);
|
||||
context.fillStyle = styleValue(element, "background_color", "#FFE62C");
|
||||
context.fillRect(-textWidth / 2 - 16, -textHeight / 2 - 16, textWidth + 32, textHeight + 32);
|
||||
const backgroundLeft = align === "left" ? layout.x : align === "right" ? layout.x - textWidth : layout.x - textWidth / 2;
|
||||
context.fillRect(backgroundLeft - 16, layout.firstY - fontSize / 2 - 16, textWidth + 32, layout.blockHeight + 32);
|
||||
context.globalAlpha = alpha;
|
||||
}
|
||||
context.shadowColor = layer.shadowColor;
|
||||
@@ -243,17 +281,12 @@ function drawTemplateTextLayer(
|
||||
: editable ? styleValue(element, "fill_color", layer.fillColor) : layer.fillColor;
|
||||
context.strokeStyle = editable ? styleValue(element, "stroke_color", layer.strokeColor) : layer.strokeColor;
|
||||
context.lineWidth = editable ? styleValue(element, "stroke_width", layer.strokeWidth) : layer.strokeWidth;
|
||||
const centerY = (0.5 - layer.anchorY) * textHeight;
|
||||
const firstY = centerY - ((lines.length - 1) * fontSize * lineHeight) / 2;
|
||||
const anchorX = align === "left" ? -layer.anchorX * textWidth
|
||||
: align === "right" ? (1 - layer.anchorX) * textWidth
|
||||
: (0.5 - layer.anchorX) * textWidth;
|
||||
lines.forEach((line, index) => {
|
||||
const y = firstY + index * fontSize * lineHeight;
|
||||
const y = layout.firstY + index * layout.lineAdvance;
|
||||
if ((editable ? styleValue(element, "stroke_enabled", layer.strokeWidth > 0) : layer.strokeWidth > 0) && context.lineWidth > 0) {
|
||||
context.strokeText(line, anchorX, y);
|
||||
context.strokeText(line, layout.x, y);
|
||||
}
|
||||
context.fillText(line, anchorX, y);
|
||||
context.fillText(line, layout.x, y);
|
||||
});
|
||||
context.restore();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ type CanvasElement = CanvasState["elements"][number];
|
||||
|
||||
export type TextTemplateCategory = "flower" | "simple" | "tag" | "title";
|
||||
export type TextAlign = "center" | "left" | "right";
|
||||
export type TextVerticalAlign = "bottom" | "middle" | "top";
|
||||
|
||||
export interface TextTemplateImageLayer {
|
||||
alpha: number;
|
||||
@@ -70,6 +71,7 @@ export interface TextTemplateTextLayer {
|
||||
strokeColor: string;
|
||||
strokeWidth: number;
|
||||
text: string;
|
||||
verticalAlign: TextVerticalAlign;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -157,7 +159,7 @@ interface RawTextLayer {
|
||||
alpha?: number; align: string; anchor_x?: number; anchor_y?: number; editable: boolean; fill_color: string; fill_pattern_asset_id?: string; font_id: string; font_size: number;
|
||||
height: number; letter_spacing: number; line_height: number; order: number; rotation: number; scale_x: number; scale_y: number;
|
||||
shadow_blur: number; shadow_color: string; shadow_offset_x: number; shadow_offset_y: number; stroke_color: string;
|
||||
stroke_width: number; text: string; width: number; x: number; y: number;
|
||||
stroke_width: number; text: string; vertical_align: TextVerticalAlign; width: number; x: number; y: number;
|
||||
}
|
||||
|
||||
function renderModel(item: (typeof complexAssetCatalog.text_templates)[number]): TextTemplateRenderModel {
|
||||
@@ -180,7 +182,7 @@ function renderModel(item: (typeof complexAssetCatalog.text_templates)[number]):
|
||||
scaleY: layer.scale_y, shadowBlur: layer.shadow_blur, shadowColor: layer.shadow_color,
|
||||
shadowOffsetX: layer.shadow_offset_x, shadowOffsetY: layer.shadow_offset_y,
|
||||
strokeColor: layer.stroke_color, strokeWidth: layer.stroke_width, text: layer.text,
|
||||
width: layer.width, x: layer.x, y: layer.y,
|
||||
verticalAlign: layer.vertical_align, width: layer.width, x: layer.x, y: layer.y,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -518,6 +518,9 @@ function prefabLayers(prefab, input) {
|
||||
stroke_width: Math.max(0, Number(outline?.outlineSize ?? 0)),
|
||||
text: String(textMesh.m_text ?? ""),
|
||||
type: "text",
|
||||
vertical_align: Number(style.vAlignment ?? 1) === 0 ? "top"
|
||||
: Number(style.vAlignment ?? 1) === 2 ? "bottom"
|
||||
: "middle",
|
||||
width: Math.max(1, Number(object.m_contentSize?.width ?? 1) * Math.abs(scaleX)),
|
||||
x: transform.x,
|
||||
y: -transform.y,
|
||||
@@ -762,6 +765,7 @@ export function compileTextTemplateAssets({ templateDirectory, templateId }) {
|
||||
stroke_width: 0,
|
||||
text: value,
|
||||
type: "text",
|
||||
vertical_align: "middle",
|
||||
width: Math.max(96, Array.from(value).length * 52),
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { P0A_DYNAMIC_STICKERS } from "../../apps/web/src/dynamic-provider.js";
|
||||
import { DYNAMIC_RENDER_MODELS } from "../../apps/web/src/dynamic-render-models.js";
|
||||
import { P0A_COLOR_CARDS } from "../../apps/web/src/palette-provider.js";
|
||||
import { P0A_FONT_OPTIONS, P0A_TEXT_TEMPLATES } from "../../apps/web/src/text-assets.js";
|
||||
import { ninePatchSlices } from "../../apps/web/src/editor-stage.js";
|
||||
import { ninePatchSlices, textTemplateBoxLayout } from "../../apps/web/src/editor-stage.js";
|
||||
import {
|
||||
P0A_COLOR_CARD_IDS,
|
||||
P0A_DYNAMIC_STICKER_IDS,
|
||||
@@ -96,6 +96,12 @@ describe("POSTV1-ASSET-ALL-16 complete asset catalog", () => {
|
||||
]));
|
||||
expect(byId.get("H079")!.render_model.text_layers.slice(0, 3).every((layer) => Math.abs(layer.rotation + 10) < 0.001)).toBe(true);
|
||||
expect(byId.get("H005")!.render_model.text_layers[1]).toMatchObject({ anchor_y: 0 });
|
||||
expect(byId.get("H005")!.render_model.text_layers[0]).toMatchObject({ vertical_align: "middle" });
|
||||
expect(byId.get("H005")!.render_model.text_layers[1]).toMatchObject({ vertical_align: "top" });
|
||||
|
||||
const heading016Text = byId.get("H016")!.render_model.text_layers;
|
||||
expect(heading016Text).toHaveLength(3);
|
||||
expect(heading016Text.every((layer) => layer.vertical_align === "bottom")).toBe(true);
|
||||
|
||||
const heading041 = byId.get("H041")!;
|
||||
const heading041Text = heading041.render_model.text_layers[0]!;
|
||||
@@ -105,6 +111,32 @@ describe("POSTV1-ASSET-ALL-16 complete asset catalog", () => {
|
||||
expect(byId.get("FLOWER024")!.render_model.image_layers[0]).toMatchObject({ alpha: 0.4000000059604645 });
|
||||
});
|
||||
|
||||
it("positions template text inside the captured fixed box instead of its measured glyph width", () => {
|
||||
const shortText = textTemplateBoxLayout({
|
||||
align: "center", anchorX: 0, anchorY: 1, boxHeight: 70, boxWidth: 517,
|
||||
fontSize: 70, lineCount: 1, lineHeight: 1, verticalAlign: "bottom",
|
||||
});
|
||||
const longText = textTemplateBoxLayout({
|
||||
align: "center", anchorX: 0, anchorY: 1, boxHeight: 70, boxWidth: 517,
|
||||
fontSize: 70, lineCount: 1, lineHeight: 1, verticalAlign: "bottom",
|
||||
});
|
||||
|
||||
expect(shortText.x).toBe(258.5);
|
||||
expect(longText.x).toBe(shortText.x);
|
||||
expect(shortText.firstY).toBe(-35);
|
||||
});
|
||||
|
||||
it("honors source top, middle, and bottom vertical alignment inside fixed text boxes", () => {
|
||||
const layout = (verticalAlign: "top" | "middle" | "bottom") => textTemplateBoxLayout({
|
||||
align: "left", anchorX: 0, anchorY: 0, boxHeight: 120, boxWidth: 200,
|
||||
fontSize: 40, lineCount: 1, lineHeight: 1, verticalAlign,
|
||||
});
|
||||
|
||||
expect(layout("top").firstY).toBe(20);
|
||||
expect(layout("middle").firstY).toBe(60);
|
||||
expect(layout("bottom").firstY).toBe(100);
|
||||
});
|
||||
|
||||
it("preserves nine-patch edges while stretching only the decoration center", () => {
|
||||
const slices = ninePatchSlices(231, 63, {
|
||||
bottom: 16, left: 53, right: 25, sourceHeight: 63, sourceWidth: 159, top: 12,
|
||||
|
||||
Reference in New Issue
Block a user