mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -74,20 +79,12 @@ function TextPreviewOverlay({
|
||||
className="w-full leading-tight break-words whitespace-pre-wrap"
|
||||
style={{
|
||||
fontFamily: cssFontFamily,
|
||||
fontSize: appliedSize,
|
||||
fontSize: `${appliedSize}px`,
|
||||
color: textColor,
|
||||
textAlign: textAlign as "left" | "center" | "right",
|
||||
WebkitTextStroke: `1.5px ${strokeColor}`,
|
||||
textShadow: [
|
||||
`1px 1px 0 ${strokeColor}`,
|
||||
`-1px -1px 0 ${strokeColor}`,
|
||||
`1px -1px 0 ${strokeColor}`,
|
||||
`-1px 1px 0 ${strokeColor}`,
|
||||
`0 1px 0 ${strokeColor}`,
|
||||
`0 -1px 0 ${strokeColor}`,
|
||||
`1px 0 0 ${strokeColor}`,
|
||||
`-1px 0 0 ${strokeColor}`,
|
||||
].join(", "),
|
||||
WebkitTextStroke: `${stroke}px ${strokeColor}`,
|
||||
paintOrder: "stroke fill",
|
||||
textShadow: `1px 1px 2px rgba(0,0,0,0.5)`,
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
@@ -334,6 +331,21 @@ function EditorPreview() {
|
||||
const textAlign = useMemeStore((s) => s.textAlign);
|
||||
const allCaps = useMemeStore((s) => s.allCaps);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(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() {
|
||||
<div className="h-full flex items-center justify-center p-4 overflow-auto">
|
||||
<div className="max-w-2xl w-full">
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-testid="meme-editor-preview"
|
||||
className="relative w-full rounded-lg overflow-hidden border border-border bg-muted/30"
|
||||
>
|
||||
@@ -359,6 +372,7 @@ function EditorPreview() {
|
||||
strokeColor={strokeColor}
|
||||
textAlign={textAlign}
|
||||
allCaps={allCaps}
|
||||
containerWidth={containerWidth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user