feat : add language export and some image gen optimaize
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user