fix : too high fade
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useDraggable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
@@ -30,7 +31,13 @@ import {
|
||||
ZoomInIcon,
|
||||
ZoomOutIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState, type ChangeEvent, type Ref } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEvent,
|
||||
type CSSProperties,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -113,6 +120,8 @@ type Composition = {
|
||||
fadeColor: string;
|
||||
fadeTint: number;
|
||||
auraUpOffset: number;
|
||||
characterOffset: Position;
|
||||
fadeOffset: Position;
|
||||
};
|
||||
|
||||
type BlendLayer = {
|
||||
@@ -122,6 +131,8 @@ type BlendLayer = {
|
||||
};
|
||||
|
||||
type CropArea = { x: number; y: number; width: number; height: number };
|
||||
type Position = { x: number; y: number };
|
||||
type PosterLayer = "character" | "fade";
|
||||
|
||||
const blendModeItems: Array<{ value: OverviewBlendMode; label: string }> = [
|
||||
...overviewBlendModes,
|
||||
@@ -130,6 +141,45 @@ const blendModeItems: Array<{ value: OverviewBlendMode; label: string }> = [
|
||||
const DISCLAIMER =
|
||||
"ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต";
|
||||
|
||||
function DraggablePosterLayer({
|
||||
id,
|
||||
offset,
|
||||
interactive,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
id: PosterLayer;
|
||||
offset: Position;
|
||||
interactive: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id,
|
||||
disabled: !interactive,
|
||||
});
|
||||
const temporaryTransform = transform
|
||||
? ` translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||
: "";
|
||||
const dragProps = interactive ? { ...attributes, ...listeners } : {};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
transform: `translate(${offset.x}%, ${offset.y}%)${temporaryTransform}`,
|
||||
}}
|
||||
{...dragProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewPoster({
|
||||
artworkUrl,
|
||||
character,
|
||||
@@ -142,12 +192,15 @@ function OverviewPoster({
|
||||
nameCardCrop,
|
||||
nameCardSize,
|
||||
elementIconUrls,
|
||||
captureRef,
|
||||
composition,
|
||||
disclaimerFontSize,
|
||||
interactive = false,
|
||||
activeLayer,
|
||||
onLayerMove,
|
||||
}: {
|
||||
artworkUrl: string | null;
|
||||
character: Character | null;
|
||||
role: OverviewRole;
|
||||
role: string;
|
||||
topColors: [string, string];
|
||||
bottomColors: [string, string];
|
||||
elementColors: [string, string];
|
||||
@@ -156,8 +209,11 @@ function OverviewPoster({
|
||||
nameCardCrop: CropArea | null;
|
||||
nameCardSize: { width: number; height: number } | null;
|
||||
elementIconUrls: Record<string, string | null>;
|
||||
captureRef?: Ref<HTMLDivElement>;
|
||||
composition: Composition;
|
||||
disclaimerFontSize: number | null;
|
||||
interactive?: boolean;
|
||||
activeLayer?: PosterLayer;
|
||||
onLayerMove?: (layer: PosterLayer, delta: Position) => void;
|
||||
}) {
|
||||
const weaponIcon = weaponTypeIconPath(character?.weaponType ?? null);
|
||||
const z = {
|
||||
@@ -169,6 +225,20 @@ function OverviewPoster({
|
||||
} 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 } }),
|
||||
);
|
||||
|
||||
function handlePosterDragEnd({ active, delta }: DragEndEvent) {
|
||||
if (!interactive || !onLayerMove) return;
|
||||
const size = posterRef.current?.getBoundingClientRect().width;
|
||||
if (!size || (active.id !== "character" && active.id !== "fade")) return;
|
||||
onLayerMove(active.id, {
|
||||
x: (delta.x / size) * 100,
|
||||
y: (delta.y / size) * 100,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -190,8 +260,12 @@ function OverviewPoster({
|
||||
(line) => context.measureText(line).width,
|
||||
),
|
||||
);
|
||||
disclaimer.style.width = `${textWidth}px`;
|
||||
disclaimer.style.fontSize = `${Math.min(width * 0.024, (textWidth * 100) / longest)}px`;
|
||||
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)
|
||||
@@ -201,11 +275,12 @@ function OverviewPoster({
|
||||
cancelled = true;
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [character?.name]);
|
||||
}, [character?.name, disclaimerFontSize]);
|
||||
|
||||
return (
|
||||
<DndContext sensors={posterSensors} onDragEnd={handlePosterDragEnd}>
|
||||
<div
|
||||
ref={captureRef}
|
||||
ref={posterRef}
|
||||
className="relative isolate aspect-square size-full overflow-hidden bg-black text-white [container-type:inline-size]"
|
||||
style={{ fontFamily: '"Genshin Overview", sans-serif' }}
|
||||
>
|
||||
@@ -248,7 +323,16 @@ function OverviewPoster({
|
||||
)
|
||||
) : null}
|
||||
{artworkUrl ? (
|
||||
<div className="absolute inset-0" style={{ zIndex: z.character }}>
|
||||
<DraggablePosterLayer
|
||||
id="character"
|
||||
offset={composition.characterOffset}
|
||||
interactive={interactive && activeLayer === "character"}
|
||||
className={cn(
|
||||
"absolute inset-0 cursor-grab active:cursor-grabbing",
|
||||
interactive && "touch-none",
|
||||
)}
|
||||
style={{ zIndex: z.character }}
|
||||
>
|
||||
<Image
|
||||
src={artworkUrl}
|
||||
alt=""
|
||||
@@ -274,15 +358,23 @@ function OverviewPoster({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</DraggablePosterLayer>
|
||||
) : (
|
||||
<div className="absolute inset-0 grid place-items-center bg-muted text-muted-foreground">
|
||||
<ImageIcon aria-hidden="true" className="size-[12cqw]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute inset-x-0 -top-[3cqw] bottom-0"
|
||||
<DraggablePosterLayer
|
||||
id="fade"
|
||||
offset={composition.fadeOffset}
|
||||
interactive={interactive && activeLayer === "fade"}
|
||||
className={cn(
|
||||
"absolute inset-x-0 -top-[3cqw] bottom-0",
|
||||
activeLayer === "fade"
|
||||
? "touch-none cursor-grab active:cursor-grabbing"
|
||||
: "pointer-events-none",
|
||||
)}
|
||||
style={{ zIndex: z.fade }}
|
||||
>
|
||||
<Image
|
||||
@@ -304,7 +396,7 @@ function OverviewPoster({
|
||||
WebkitMask: "url('/img/fade.png') center / 100% 100% no-repeat",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DraggablePosterLayer>
|
||||
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
@@ -458,13 +550,14 @@ function OverviewPoster({
|
||||
</div>
|
||||
<p
|
||||
ref={disclaimerRef}
|
||||
className="mt-[1.5cqw] whitespace-pre text-[2.15cqw] leading-[1.15]"
|
||||
className="mt-[1.5cqw] whitespace-pre text-center text-[2.15cqw] leading-[1.15]"
|
||||
>
|
||||
{DISCLAIMER}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -624,6 +717,7 @@ export function OverviewImageGenerator({
|
||||
const [nameCardCropOpen, setNameCardCropOpen] = useState(false);
|
||||
const [nameCardZoom, setNameCardZoom] = useState(1);
|
||||
const [role, setRole] = useState<OverviewRole>("Main DPS");
|
||||
const [customRoleText, setCustomRoleText] = useState("");
|
||||
const [topColorStart, setTopColorStart] = useState("#E9A51A");
|
||||
const [topColorEnd, setTopColorEnd] = useState("#FFD84A");
|
||||
const [bottomColorStart, setBottomColorStart] = useState("#E9A51A");
|
||||
@@ -646,12 +740,26 @@ export function OverviewImageGenerator({
|
||||
const [fadeColor, setFadeColor] = useState("#0E0200");
|
||||
const [fadeTint, setFadeTint] = useState(100);
|
||||
const [auraUpOffset, setAuraUpOffset] = useState(4);
|
||||
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 composition: Composition = {
|
||||
backgroundUrl,
|
||||
fadeColor,
|
||||
fadeTint,
|
||||
auraUpOffset,
|
||||
characterOffset,
|
||||
fadeOffset,
|
||||
};
|
||||
const displayRole =
|
||||
role === "Custom" ? customRoleText.trim() || "Custom" : role;
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (backgroundUrl) URL.revokeObjectURL(backgroundUrl);
|
||||
@@ -724,6 +832,20 @@ export function OverviewImageGenerator({
|
||||
});
|
||||
}
|
||||
|
||||
function movePosterLayer(layer: PosterLayer, delta: Position) {
|
||||
const update = (current: Position): Position => ({
|
||||
x: Number((current.x + delta.x).toFixed(2)),
|
||||
y: Number((current.y + delta.y).toFixed(2)),
|
||||
});
|
||||
if (layer === "character") setCharacterOffset(update);
|
||||
else setFadeOffset(update);
|
||||
}
|
||||
|
||||
function resetPosterPositions() {
|
||||
setCharacterOffset({ x: 0, y: 0 });
|
||||
setFadeOffset({ x: 0, y: 0 });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const url = character?.nameCardImageUrl;
|
||||
if (!url) return;
|
||||
@@ -906,7 +1028,7 @@ export function OverviewImageGenerator({
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = overviewFileName(character.key, role);
|
||||
link.download = overviewFileName(character.key, displayRole);
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
@@ -1106,6 +1228,50 @@ export function OverviewImageGenerator({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>ตำแหน่งภาพ</FieldLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={resetPosterPositions}
|
||||
>
|
||||
<RotateCcwIcon data-icon="inline-start" />
|
||||
รีเซ็ตตำแหน่งตัวละครและ Fade
|
||||
</Button>
|
||||
<FieldDescription>
|
||||
เลือกเลเยอร์ใต้ตัวอย่าง แล้วลากในตัวอย่างเพื่อเลื่อนตำแหน่ง
|
||||
</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
|
||||
@@ -1124,6 +1290,20 @@ export function OverviewImageGenerator({
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
{role === "Custom" ? (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="overview-custom-role">
|
||||
ข้อความ Custom Role
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="overview-custom-role"
|
||||
value={customRoleText}
|
||||
maxLength={32}
|
||||
placeholder="เช่น Buffer"
|
||||
onChange={(event) => setCustomRoleText(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
<Field>
|
||||
<FieldLabel>Blend mode</FieldLabel>
|
||||
<DndContext
|
||||
@@ -1189,7 +1369,7 @@ export function OverviewImageGenerator({
|
||||
composition={composition}
|
||||
artworkUrl={artworkUrl}
|
||||
character={character}
|
||||
role={role}
|
||||
role={displayRole}
|
||||
topColors={[topColorStart, topColorEnd]}
|
||||
bottomColors={[bottomColorStart, bottomColorEnd]}
|
||||
elementColors={[elementColorStart, elementColorEnd]}
|
||||
@@ -1198,8 +1378,34 @@ export function OverviewImageGenerator({
|
||||
nameCardCrop={nameCardCrop}
|
||||
nameCardSize={nameCardSize}
|
||||
elementIconUrls={elementIconUrls}
|
||||
disclaimerFontSize={disclaimerFontSize}
|
||||
interactive
|
||||
activeLayer={activePosterLayer}
|
||||
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>
|
||||
<FieldDescription>
|
||||
เลือกเลเยอร์แล้วลากตรงบริเวณตัวอย่างเพื่อเลื่อนตำแหน่ง
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1269,6 +1475,7 @@ export function OverviewImageGenerator({
|
||||
</Card>
|
||||
|
||||
<div
|
||||
ref={captureRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed top-0 -left-[10000px] order-5 size-[1000px]"
|
||||
>
|
||||
@@ -1276,7 +1483,7 @@ export function OverviewImageGenerator({
|
||||
composition={composition}
|
||||
artworkUrl={artworkUrl}
|
||||
character={character}
|
||||
role={role}
|
||||
role={displayRole}
|
||||
topColors={[topColorStart, topColorEnd]}
|
||||
bottomColors={[bottomColorStart, bottomColorEnd]}
|
||||
elementColors={[elementColorStart, elementColorEnd]}
|
||||
@@ -1285,7 +1492,7 @@ export function OverviewImageGenerator({
|
||||
nameCardCrop={nameCardCrop}
|
||||
nameCardSize={nameCardSize}
|
||||
elementIconUrls={elementIconUrls}
|
||||
captureRef={captureRef}
|
||||
disclaimerFontSize={disclaimerFontSize}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const overviewRoles = ["Main DPS", "Sub DPS", "Support"] as const;
|
||||
export const overviewRoles = ["Main DPS", "Sub DPS", "Support", "Custom"] as const;
|
||||
|
||||
export type OverviewRole = (typeof overviewRoles)[number];
|
||||
|
||||
@@ -215,7 +215,7 @@ export function weaponTypeIconPath(weaponType: string | null): string | null {
|
||||
return fileName ? `/icon/weapon-types/${fileName}` : null;
|
||||
}
|
||||
|
||||
export function overviewFileName(characterKey: string, role: OverviewRole): string {
|
||||
export function overviewFileName(characterKey: string, role: string): string {
|
||||
const safeKey = characterKey.replace(/[^\p{L}\p{N}._-]+/gu, "-");
|
||||
const safeRole = role.toLowerCase().replaceAll(" ", "-");
|
||||
return `overview-${safeKey}-${safeRole}.png`;
|
||||
|
||||
Reference in New Issue
Block a user