From dac30f14f0e182a9f685dc3216ed539d97741068 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 18:17:17 +0800 Subject: [PATCH] fix(meme-generator): match preview and server render text sizing - Preview font size now calculated from actual container width, not viewport - Preview stroke scales proportionally with font size (4% ratio) - Server stroke reduced from 6% to 4% to match preview - Added ResizeObserver to track container width for accurate sizing - Added paint-order: stroke fill to CSS preview for consistent rendering --- apps/api/src/lib/meme-text-renderer.ts | 2 +- .../tools/meme-generator-preview.tsx | 44 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/api/src/lib/meme-text-renderer.ts b/apps/api/src/lib/meme-text-renderer.ts index cdaab0df..c212d429 100644 --- a/apps/api/src/lib/meme-text-renderer.ts +++ b/apps/api/src/lib/meme-text-renderer.ts @@ -213,7 +213,7 @@ export function renderMemeTextSvg(opts: MemeTextOptions): Buffer { const fontSize = fixedFontSize ?? autoSizeFontToFit(text, fontFamily, bw, bh); const lineHeight = fontSize * LINE_HEIGHT_FACTOR; const lines = wrapText(text, fontFamily, fontSize, bw); - const strokeWidth = Math.max(1, Math.round(fontSize * 0.06)); + const strokeWidth = Math.max(1, Math.round(fontSize * 0.04)); const fillAttr = escapeXmlAttr(textColor); const strokeAttr = escapeXmlAttr(strokeColor); diff --git a/apps/web/src/components/tools/meme-generator-preview.tsx b/apps/web/src/components/tools/meme-generator-preview.tsx index 1703564c..5902e509 100644 --- a/apps/web/src/components/tools/meme-generator-preview.tsx +++ b/apps/web/src/components/tools/meme-generator-preview.tsx @@ -8,7 +8,7 @@ import { Search, Sparkles, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { CATEGORIES, @@ -35,6 +35,7 @@ function TextPreviewOverlay({ strokeColor, textAlign, allCaps, + containerWidth, }: { boxes: TemplateTextBox[]; textValues: TextBoxValue[]; @@ -44,6 +45,7 @@ function TextPreviewOverlay({ strokeColor: string; textAlign: string; allCaps: boolean; + containerWidth: number; }) { const cssFontFamily = FONT_FAMILY_MAP[fontFamily] ?? FONT_FAMILY_MAP.anton; @@ -53,8 +55,11 @@ function TextPreviewOverlay({ const value = textValues.find((v) => v.id === box.id); const text = value?.text || box.defaultText || ""; const displayText = allCaps ? text.toUpperCase() : text; - const autoSize = `clamp(10px, ${box.height * 0.35}vw, 60px)`; - const appliedSize = fontSize > 0 ? `${fontSize}px` : autoSize; + const boxPxW = (box.width / 100) * containerWidth; + const boxPxH = (box.height / 100) * containerWidth; + const autoSize = Math.max(10, Math.min(Math.floor(boxPxW / 8), Math.floor(boxPxH / 2), 48)); + const appliedSize = fontSize > 0 ? fontSize : autoSize; + const stroke = Math.max(1, Math.round(appliedSize * 0.04)); return (
{displayText} @@ -334,6 +331,21 @@ function EditorPreview() { const textAlign = useMemeStore((s) => s.textAlign); const allCaps = useMemeStore((s) => s.allCaps); + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(600); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + const imageSrc = selectedTemplate ? `/api/v1/meme-templates/full/${selectedTemplate.filename}` : (customImageUrl ?? ""); @@ -346,6 +358,7 @@ function EditorPreview() {
@@ -359,6 +372,7 @@ function EditorPreview() { strokeColor={strokeColor} textAlign={textAlign} allCaps={allCaps} + containerWidth={containerWidth} />