feat : add language export and some image gen optimaize

This commit is contained in:
2026-09-15 17:50:20 +07:00 Unverified
parent 5f1d2bccc9
commit 072310091d
9 changed files with 307 additions and 162 deletions
+83 -104
View File
@@ -80,6 +80,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Slider } from "@/components/ui/slider";
import { Spinner } from "@/components/ui/spinner";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { elementIconFilter } from "@/lib/catalog/element";
@@ -121,6 +122,7 @@ type Composition = {
fadeColor: string;
fadeTint: number;
auraUpOffset: number;
characterScale: number;
characterOffset: Position;
fadeOffset: Position;
};
@@ -140,10 +142,12 @@ const blendModeItems: Array<{ value: OverviewBlendMode; label: string }> = [
];
const DISCLAIMER = "ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต";
const DEFAULT_FADE_OFFSET: Position = { x: 0, y: 2 };
function DraggablePosterLayer({
id,
offset,
scale = 1,
interactive,
className,
style,
@@ -151,6 +155,7 @@ function DraggablePosterLayer({
}: {
id: PosterLayer;
offset: Position;
scale?: number;
interactive: boolean;
className?: string;
style?: CSSProperties;
@@ -171,7 +176,7 @@ function DraggablePosterLayer({
className={className}
style={{
...style,
transform: `translate(${offset.x}%, ${offset.y}%)${temporaryTransform}`,
transform: `translate(${offset.x}%, ${offset.y}%)${temporaryTransform} scale(${scale})`,
}}
{...dragProps}
>
@@ -193,7 +198,6 @@ function OverviewPoster({
nameCardSize,
elementIconUrls,
composition,
disclaimerFontSize,
interactive = false,
activeLayer,
onLayerMove,
@@ -210,7 +214,6 @@ function OverviewPoster({
nameCardSize: { width: number; height: number } | null;
elementIconUrls: Record<string, string | null>;
composition: Composition;
disclaimerFontSize: number | null;
interactive?: boolean;
activeLayer?: PosterLayer;
onLayerMove?: (layer: PosterLayer, delta: Position) => void;
@@ -224,7 +227,6 @@ function OverviewPoster({
text: 5,
} as const;
const titleRef = useRef<HTMLParagraphElement>(null);
const disclaimerRef = useRef<HTMLParagraphElement>(null);
const posterRef = useRef<HTMLDivElement>(null);
const posterSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
@@ -243,29 +245,13 @@ function OverviewPoster({
useEffect(() => {
let cancelled = false;
const fit = () => {
if (cancelled || !titleRef.current || !disclaimerRef.current) return;
if (cancelled || !titleRef.current) return;
const title = titleRef.current;
const disclaimer = disclaimerRef.current;
const width = title.parentElement!.getBoundingClientRect().width;
title.style.fontSize = `${width / 10}px`;
if (title.scrollWidth > width) {
title.style.fontSize = `${((width / 10) * width) / title.scrollWidth}px`;
}
const textWidth = title.getBoundingClientRect().width;
const context = document.createElement("canvas").getContext("2d");
if (!context) return;
context.font = '100px "Genshin Overview"';
const longest = Math.max(
...DISCLAIMER.split("\n").map(
(line) => context.measureText(line).width,
),
);
disclaimer.style.width = "max-content";
disclaimer.style.textAlign = "center";
disclaimer.style.fontSize =
disclaimerFontSize === null
? `${Math.min(width * 0.024, (textWidth * 100) / longest)}px`
: `${disclaimerFontSize}cqw`;
};
const observer = new ResizeObserver(fit);
if (titleRef.current?.parentElement)
@@ -275,7 +261,7 @@ function OverviewPoster({
cancelled = true;
observer.disconnect();
};
}, [character?.name, disclaimerFontSize]);
}, [character?.name]);
return (
<DndContext sensors={posterSensors} onDragEnd={handlePosterDragEnd}>
@@ -326,6 +312,7 @@ function OverviewPoster({
<DraggablePosterLayer
id="character"
offset={composition.characterOffset}
scale={composition.characterScale}
interactive={interactive && activeLayer === "character"}
className={cn(
"absolute inset-0 cursor-grab active:cursor-grabbing",
@@ -340,7 +327,7 @@ function OverviewPoster({
unoptimized
loading="eager"
sizes="1024px"
className="object-cover"
className="object-contain"
/>
{[...blendLayers].reverse().map((layer) => (
<Image
@@ -351,7 +338,7 @@ function OverviewPoster({
unoptimized
loading="eager"
sizes="1024px"
className="object-cover"
className="object-contain"
style={{
mixBlendMode: layer.mode,
opacity: layer.amount / 100,
@@ -549,8 +536,7 @@ function OverviewPoster({
})}
</div>
<p
ref={disclaimerRef}
className="mt-[1.5cqw] whitespace-pre text-center text-[2.15cqw] leading-[1.15]"
className="mt-[1.5cqw] w-max whitespace-pre text-center text-[2.03167cqw] leading-[1.15]"
>
{DISCLAIMER}
</p>
@@ -668,14 +654,17 @@ function SortableBlendLayerRow({
<FieldLabel htmlFor={`overview-blend-amount-${layer.id}`}>
ความเข้ม {layer.amount}%
</FieldLabel>
<Input
<Slider
id={`overview-blend-amount-${layer.id}`}
type="range"
min={0}
max={100}
step={5}
value={layer.amount}
onChange={(event) => onUpdate({ amount: Number(event.target.value) })}
value={[layer.amount]}
onValueChange={(value) =>
onUpdate({
amount: typeof value === "number" ? value : value[0] ?? 0,
})
}
/>
</Field>
</div>
@@ -738,23 +727,20 @@ export function OverviewImageGenerator({
const [exporting, setExporting] = useState(false);
const [backgroundUrl, setBackgroundUrl] = useState<string | null>(null);
const [fadeColor, setFadeColor] = useState("#0E0200");
const [fadeTint, setFadeTint] = useState(100);
const [fadeTint, setFadeTint] = useState(0);
const [auraUpOffset, setAuraUpOffset] = useState(4);
const [characterScale, setCharacterScale] = useState(100);
const [characterOffset, setCharacterOffset] = useState<Position>({
x: 0,
y: 0,
});
const [fadeOffset, setFadeOffset] = useState<Position>({ x: 0, y: 0 });
const [activePosterLayer, setActivePosterLayer] =
useState<PosterLayer>("character");
const [disclaimerFontSize, setDisclaimerFontSize] = useState<number | null>(
null,
);
const [fadeOffset, setFadeOffset] = useState<Position>(DEFAULT_FADE_OFFSET);
const composition: Composition = {
backgroundUrl,
fadeColor,
fadeTint,
auraUpOffset,
characterScale: characterScale / 100,
characterOffset,
fadeOffset,
};
@@ -843,7 +829,7 @@ export function OverviewImageGenerator({
function resetPosterPositions() {
setCharacterOffset({ x: 0, y: 0 });
setFadeOffset({ x: 0, y: 0 });
setFadeOffset(DEFAULT_FADE_OFFSET);
}
useEffect(() => {
@@ -900,6 +886,10 @@ export function OverviewImageGenerator({
});
if (target === "artwork") {
detectArtworkTheme(image, image.naturalWidth, image.naturalHeight);
setCharacterScale(100);
setArtworkUrl(url);
setArtworkError(null);
return;
}
setArtworkError(null);
setCropArea(null);
@@ -1146,11 +1136,30 @@ export function OverviewImageGenerator({
onChange={(event) => selectArtwork(event)}
/>
<FieldDescription>
ทุกภาพจะเปิดหน้าต่างให้ครอป ซูม และจัดตำแหน่งก่อนใช้งาน
ใช้ภาพเต็มโดยไม่ครอป และสามารถลากเพื่อจัดตำแหน่งในตัวอย่างได้
</FieldDescription>
{artworkError ? <FieldError>{artworkError}</FieldError> : null}
</Field>
<Field data-disabled={!artworkUrl}>
<FieldLabel htmlFor="overview-artwork-zoom">
ซูมภาพตัวละคร {characterScale}%
</FieldLabel>
<Slider
id="overview-artwork-zoom"
min={25}
max={300}
step={1}
value={[characterScale]}
disabled={!artworkUrl}
onValueChange={(value) =>
setCharacterScale(
typeof value === "number" ? value : value[0] ?? 100,
)
}
/>
</Field>
<Field>
<FieldLabel htmlFor="overview-background">
อัปโหลดพื้นหลัง
@@ -1197,29 +1206,31 @@ export function OverviewImageGenerator({
<FieldLabel htmlFor="overview-fade-tint">
ความเข้มสี Fade {fadeTint}%
</FieldLabel>
<Input
<Slider
id="overview-fade-tint"
type="range"
min={0}
max={100}
step={1}
value={fadeTint}
onChange={(event) => setFadeTint(Number(event.target.value))}
value={[fadeTint]}
onValueChange={(value) =>
setFadeTint(typeof value === "number" ? value : value[0] ?? 0)
}
/>
</Field>
<Field>
<FieldLabel htmlFor="overview-aura-up">
เลื่อน Aura ด้านบนขึ้น {auraUpOffset}%
</FieldLabel>
<Input
<Slider
id="overview-aura-up"
type="range"
min={0}
max={7}
step={0.1}
value={auraUpOffset}
onChange={(event) =>
setAuraUpOffset(Number(event.target.value))
value={[auraUpOffset]}
onValueChange={(value) =>
setAuraUpOffset(
typeof value === "number" ? value : value[0] ?? 0,
)
}
/>
</Field>
@@ -1237,36 +1248,6 @@ export function OverviewImageGenerator({
เลือกเลเยอร์ใต้ตัวอย่าง แล้วลากในตัวอย่างเพื่อเลื่อนตำแหน่ง
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="overview-disclaimer-size">
ขนาด Disclaimer {disclaimerFontSize === null ? "ตามชื่ออัตโนมัติ" : `${disclaimerFontSize}cqw`}
</FieldLabel>
<div className="flex items-center gap-3">
<Input
id="overview-disclaimer-size"
type="range"
min={0.5}
max={5}
step={0.05}
value={disclaimerFontSize ?? 2.15}
onChange={(event) =>
setDisclaimerFontSize(Number(event.target.value))
}
/>
<Button
type="button"
variant="outline"
size="sm"
disabled={disclaimerFontSize === null}
onClick={() => setDisclaimerFontSize(null)}
>
อัตโนมัติ
</Button>
</div>
<FieldDescription>
ค่าเริ่มต้นจะปรับให้พอดีกับความกว้างชื่ออัตโนมัติ
</FieldDescription>
</Field>
<Field>
<FieldLabel>Role</FieldLabel>
<ToggleGroup
@@ -1373,32 +1354,30 @@ export function OverviewImageGenerator({
nameCardCrop={nameCardCrop}
nameCardSize={nameCardSize}
elementIconUrls={elementIconUrls}
disclaimerFontSize={disclaimerFontSize}
interactive
activeLayer={activePosterLayer}
activeLayer="character"
onLayerMove={movePosterLayer}
/>
</div>
<Field className="mt-4">
<FieldLabel>เลือกเลเยอร์ที่จะเลื่อน</FieldLabel>
<ToggleGroup
value={[activePosterLayer]}
onValueChange={(value) => {
const selected = value[0] as PosterLayer | undefined;
if (selected) setActivePosterLayer(selected);
}}
variant="outline"
className="w-full"
>
<ToggleGroupItem value="character" className="flex-1">
Char
</ToggleGroupItem>
<ToggleGroupItem value="fade" className="flex-1">
Fade
</ToggleGroupItem>
</ToggleGroup>
<FieldLabel htmlFor="overview-fade-y">
ตำแหน่ง Fade แนวตั้ง {fadeOffset.y}%
</FieldLabel>
<Slider
id="overview-fade-y"
min={-20}
max={20}
step={0.1}
value={[fadeOffset.y]}
onValueChange={(value) =>
setFadeOffset((current) => ({
...current,
y: typeof value === "number" ? value : value[0] ?? 2,
}))
}
/>
<FieldDescription>
เลือกเลเยอร์แล้วลากตรงบริเวณตัวอย่างเพื่อเลื่อนตำแหน่ง
ลากในตัวอย่างเพื่อเลื่อนตัวละคร และใช้แถบนี้เพื่อเลื่อน Fade
</FieldDescription>
</Field>
</CardContent>
@@ -1453,15 +1432,16 @@ export function OverviewImageGenerator({
<FieldLabel htmlFor="overview-element-gradient-angle">
มุมสีไล่ระดับธาตุ {elementGradientAngle}°
</FieldLabel>
<Input
<Slider
id="overview-element-gradient-angle"
type="range"
min={0}
max={360}
step={1}
value={elementGradientAngle}
onChange={(event) =>
setElementGradientAngle(Number(event.target.value))
value={[elementGradientAngle]}
onValueChange={(value) =>
setElementGradientAngle(
typeof value === "number" ? value : value[0] ?? 0,
)
}
/>
</Field>
@@ -1487,7 +1467,6 @@ export function OverviewImageGenerator({
nameCardCrop={nameCardCrop}
nameCardSize={nameCardSize}
elementIconUrls={elementIconUrls}
disclaimerFontSize={disclaimerFontSize}
/>
</div>