You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
8.9 KiB
248 lines
8.9 KiB
import type { LightingBeamAngle, LightingDirection, LightingPosition, LightingPreset, LightingSettings } from "@/types";
|
|
|
|
export const DEFAULT_LIGHTING_SETTINGS: LightingSettings = {
|
|
enabled: false,
|
|
viewMode: "perspective",
|
|
smartMode: false,
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
rimLight: false,
|
|
smartPrompt: "",
|
|
referenceImage: null,
|
|
selectedPresetId: null,
|
|
};
|
|
|
|
export const LIGHTING_PRESETS: LightingPreset[] = [
|
|
{
|
|
id: "overexposed-film",
|
|
label: "过曝胶片",
|
|
thumbnail: "linear-gradient(135deg, #f7ead1 0%, #ffffff 55%, #d8b47f 100%)",
|
|
prompt: "overexposed film lighting, soft bloom, warm highlights",
|
|
settings: {
|
|
brightness: 100,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
smartPrompt: "柯达胶片感的曝光和柔和高光,只调整打光",
|
|
},
|
|
},
|
|
{
|
|
id: "blue-backlight",
|
|
label: "蓝色逆光",
|
|
thumbnail: "linear-gradient(135deg, #080b18 0%, #1646ff 60%, #dbe8ff 100%)",
|
|
prompt: "blue backlight, cool rim glow, dramatic contrast",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#3438ff",
|
|
mainLightDirection: "bottom",
|
|
lightPosition: { x: 0, y: 1, z: 0 },
|
|
beamAngle: 45,
|
|
rimLight: false,
|
|
smartPrompt: "",
|
|
},
|
|
},
|
|
{
|
|
id: "rembrandt",
|
|
label: "伦勃朗光",
|
|
thumbnail: "linear-gradient(135deg, #1b120d 0%, #b9824c 55%, #f3d3a0 100%)",
|
|
prompt: "Rembrandt lighting, painterly side key light, soft shadow triangle",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "left",
|
|
lightPosition: { x: -0.58, y: -0.58, z: 0.58 },
|
|
beamAngle: 45,
|
|
smartPrompt: "",
|
|
},
|
|
},
|
|
{
|
|
id: "cyberpunk",
|
|
label: "赛博朋克",
|
|
thumbnail: "linear-gradient(135deg, #06121c 0%, #00d1ff 45%, #ff2bd6 100%)",
|
|
prompt: "cyberpunk neon lighting, teal and magenta glow, high contrast",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
rimLight: false,
|
|
smartPrompt: "《银翼杀手2045》同款青紫高反差打光,只调整光影",
|
|
},
|
|
},
|
|
{
|
|
id: "sunset-dream",
|
|
label: "落日迷幻",
|
|
thumbnail: "linear-gradient(135deg, #2a0f2d 0%, #ff7b3a 58%, #ffd36a 100%)",
|
|
prompt: "dreamy sunset lighting, warm low-angle glow, hazy atmosphere",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
smartPrompt: "",
|
|
},
|
|
},
|
|
{
|
|
id: "mystic-low-key",
|
|
label: "神秘暗调",
|
|
thumbnail: "linear-gradient(135deg, #030303 0%, #18201d 55%, #5d806f 100%)",
|
|
prompt: "mysterious low-key lighting, deep shadows, subtle green highlights",
|
|
settings: {
|
|
brightness: 10,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
smartPrompt: "百叶窗打光,神秘优雅",
|
|
},
|
|
},
|
|
{
|
|
id: "golden-hour",
|
|
label: "黄金时刻",
|
|
thumbnail: "linear-gradient(135deg, #3b1e09 0%, #ffb33c 50%, #fff1b9 100%)",
|
|
prompt: "golden hour lighting, soft warm sunlight, natural glow",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
smartPrompt: "黄金时刻暖色光影,只调整光照和阴影",
|
|
},
|
|
},
|
|
{
|
|
id: "cool-morning",
|
|
label: "清晨冷灰",
|
|
thumbnail: "linear-gradient(135deg, #20242b 0%, #8796a8 55%, #dbe5ee 100%)",
|
|
prompt: "cool morning light, desaturated grey-blue ambience, gentle shadows",
|
|
settings: {
|
|
brightness: 50,
|
|
color: "#ffffff",
|
|
mainLightDirection: "front",
|
|
lightPosition: { x: 0, y: 0, z: 1 },
|
|
beamAngle: 45,
|
|
smartPrompt: "清晨冷灰色温和柔和阴影,只调整光照和色温",
|
|
},
|
|
},
|
|
];
|
|
|
|
const DIRECTION_LABELS: Record<LightingDirection, string> = {
|
|
left: "left side key light",
|
|
top: "top key light",
|
|
right: "right side key light",
|
|
front: "front key light",
|
|
bottom: "bottom uplight",
|
|
back: "back light",
|
|
};
|
|
|
|
const DIRECTION_INSTRUCTIONS: Record<LightingDirection, string> = {
|
|
left: "Place the main light on the left side of the subject and cast natural shadows to the right.",
|
|
top: "Place the main light above the subject and create soft downward shadows.",
|
|
right: "Place the main light on the right side of the subject and cast natural shadows to the left.",
|
|
front: "Place the main light in front of the subject for clear facial illumination.",
|
|
bottom: "Place the main light below the subject for controlled uplighting.",
|
|
back: "Place the main light behind the subject and create a clean backlit silhouette.",
|
|
};
|
|
|
|
function brightnessInstruction(value: number): string {
|
|
if (value >= 75) return "Use bright high-key illumination without washing out important details.";
|
|
if (value >= 55) return "Use balanced medium-bright illumination with readable details.";
|
|
if (value >= 35) return "Use restrained low-key illumination with visible form and controlled shadows.";
|
|
return "Use dark cinematic illumination while keeping the subject recognizable.";
|
|
}
|
|
|
|
function positionInstruction(position: LightingPosition): string {
|
|
const horizontal = position.x <= -0.25 ? "camera-left" : position.x >= 0.25 ? "camera-right" : "centered";
|
|
const vertical = position.y <= -0.25 ? "above the subject" : position.y >= 0.25 ? "below the subject" : "near eye level";
|
|
const depth = position.z <= -0.25 ? "behind the subject" : position.z >= 0.25 ? "in front of the subject" : "on the subject plane";
|
|
|
|
return [
|
|
`light source vector x=${position.x.toFixed(2)}`,
|
|
`y=${position.y.toFixed(2)}`,
|
|
`z=${position.z.toFixed(2)}`,
|
|
`${horizontal}, ${vertical}, ${depth}`,
|
|
"aim the light directly at the center of the source image",
|
|
].join(", ");
|
|
}
|
|
|
|
function beamAngleInstruction(angle: LightingBeamAngle): string {
|
|
const width = Math.abs(angle) === 45 ? "focused flashlight-like beam" : "wide studio softbox spread";
|
|
const tilt = angle < 0 ? "negative tilt angle" : "positive tilt angle";
|
|
return `${angle} degree beam angle, ${width}, ${tilt}`;
|
|
}
|
|
|
|
export function normalizeLightingSettings(settings?: LightingSettings | null): LightingSettings {
|
|
const current = settings ?? null;
|
|
return {
|
|
...DEFAULT_LIGHTING_SETTINGS,
|
|
...(current ?? {}),
|
|
lightPosition: {
|
|
...DEFAULT_LIGHTING_SETTINGS.lightPosition,
|
|
...(current?.lightPosition ?? {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildLightingPrompt(settings?: LightingSettings | null): string {
|
|
const lighting = normalizeLightingSettings(settings);
|
|
if (!lighting.enabled) return "";
|
|
|
|
const preset = LIGHTING_PRESETS.find((item) => item.id === lighting.selectedPresetId);
|
|
const parts = [
|
|
"Lighting plan:",
|
|
DIRECTION_LABELS[lighting.mainLightDirection],
|
|
DIRECTION_INSTRUCTIONS[lighting.mainLightDirection],
|
|
`${lighting.brightness}% brightness`,
|
|
brightnessInstruction(lighting.brightness),
|
|
`light color ${lighting.color}`,
|
|
positionInstruction(lighting.lightPosition),
|
|
beamAngleInstruction(lighting.beamAngle),
|
|
];
|
|
|
|
if (lighting.rimLight) {
|
|
parts.push("visible rim light around the subject");
|
|
}
|
|
|
|
if (preset?.prompt) {
|
|
parts.push(preset.prompt);
|
|
}
|
|
|
|
if (lighting.smartMode && lighting.smartPrompt.trim()) {
|
|
parts.push(lighting.smartPrompt.trim());
|
|
}
|
|
|
|
return parts.join(", ");
|
|
}
|
|
|
|
export function buildOptimizedLightingPrompt(
|
|
basePrompt: string | null | undefined,
|
|
settings?: LightingSettings | null
|
|
): string {
|
|
const lightingPrompt = buildLightingPrompt(settings);
|
|
const userPrompt = (basePrompt ?? "").trim();
|
|
|
|
if (!lightingPrompt) {
|
|
return userPrompt;
|
|
}
|
|
|
|
const userIntent = userPrompt
|
|
? `User intent: ${userPrompt}`
|
|
: "User intent: relight the uploaded image according to the lighting plan.";
|
|
|
|
return [
|
|
"STRICT image-to-image relighting task: use the uploaded image as the locked source image.",
|
|
userIntent,
|
|
lightingPrompt,
|
|
"The output must show the same exact person from the source image: identical face identity, facial features, age, expression, hair, body shape, hands, pose, clothing, accessories, and camera/phone.",
|
|
"Preserve the original composition, camera angle, crop, background, object layout, readable text overlays, and image style.",
|
|
"Only change illumination: light direction, brightness, color temperature, shadows, highlights, reflections, and ambient lighting mood required by the lighting plan.",
|
|
"Treat any preset or style wording as a lighting reference only, not permission to redesign the person, outfit, background, pose, scene, or photographic composition.",
|
|
"Do not add or remove people or objects, do not beautify or re-age the person, do not replace the face, do not change hairstyle or clothing, do not crop the subject, and do not create a different scene.",
|
|
].join("\n");
|
|
}
|
|
|