diff --git a/src/components/studio/canvas/ScenePreview.tsx b/src/components/studio/canvas/ScenePreview.tsx index fc99738..08a6b2b 100644 --- a/src/components/studio/canvas/ScenePreview.tsx +++ b/src/components/studio/canvas/ScenePreview.tsx @@ -1,39 +1,71 @@ "use client"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import type { Layer, Scene } from "@/lib/studio-types"; /** * Read-only scene preview for scene-engine (FlexStory/Remotion) templates. The - * Konva editor can't reproduce the real 3D render, so instead we show the scene's - * rendered still (scene.image) and overlay clickable, labelled HOTSPOTS over the - * editable fields (logo / text). Clicking a hotspot selects that field so the - * sidebar form scrolls to + highlights it — "click the logo to edit it". + * Konva editor can't reproduce the real 3D render, so we show the scene's rendered + * still (scene.image) and overlay clickable, labelled HOTSPOTS over the editable + * fields. Clicking a hotspot selects that field so the sidebar form scrolls to + + * highlights it — "click the logo to edit it". * - * Positions are heuristic (the studio doesn't know the exact Remotion coordinates), - * tuned for our block layouts: the visual/logo sits centred, text fields stack in - * the lower third — which matches LogoMotion3D and the FlexStory blocks closely. + * The still is FIT (contain) inside the available area — so portrait 9:16 and wide + * 16:9 scenes both fit without overflow — and the hotspot overlay is anchored to the + * fitted image rectangle, so the placeholders always line up with and scale to the + * image (responsive). Positions are aspect-aware heuristics tuned to the block + * layouts (visual/logo upper-centre, text fields stacked below), clamped to stay on + * the image. */ function fieldLabel(layer: Layer): string { return layer.name?.trim() || (layer.type === "image" ? "تصویر" : "متن"); } -function hotspot(layer: Layer, fields: Layer[]): React.CSSProperties { +const clamp = (v: number, lo = 3, hi = 97) => Math.max(lo, Math.min(hi, v)); + +/** Normalised (%) hotspot rect for a field, aware of the scene aspect so it stays + * on the image whether it's tall, square or wide. */ +function hotspot(layer: Layer, fields: Layer[], tall: boolean): React.CSSProperties { const images = fields.filter((l) => l.type === "image"); const texts = fields.filter((l) => l.type === "text"); + if (layer.type === "image") { const i = images.indexOf(layer); const n = Math.max(1, images.length); - if (n === 1) return { position: "absolute", left: "30%", top: "24%", width: "40%", height: "34%" }; - const w = 76 / n; - return { position: "absolute", left: `${12 + i * w}%`, top: "26%", width: `${w - 4}%`, height: "32%" }; + const top = tall ? 30 : 24; + if (n === 1) { + const w = tall ? 56 : 40; + return { position: "absolute", left: `${clamp(50 - w / 2)}%`, top: `${top}%`, width: `${w}%`, height: tall ? "26%" : "34%" }; + } + // multiple images → a row of even cells + const cols = Math.min(n, 3); + const cw = 88 / cols; + const col = i % cols; + const row = Math.floor(i / cols); + return { + position: "absolute", + left: `${clamp(6 + col * cw, 3, 97 - cw)}%`, + top: `${top + row * 22}%`, + width: `${cw - 4}%`, + height: "18%", + }; } + const i = texts.indexOf(layer); const n = Math.max(1, texts.length); - const slot = Math.min(30 / n, 12); - return { position: "absolute", left: "12%", top: `${64 + i * (30 / n)}%`, width: "76%", height: `${slot}%` }; + const band = tall ? 40 : 30; // vertical space the text block occupies + const startTop = tall ? 56 : 62; + const step = band / n; + const h = Math.min(step - 1.5, tall ? 9 : 11); + return { + position: "absolute", + left: "10%", + top: `${clamp(startTop + i * step, 3, 96)}%`, + width: "80%", + height: `${Math.max(5, h)}%`, + }; } interface ScenePreviewProps { @@ -49,46 +81,88 @@ export function ScenePreview({ scene, selectedLayerId, onSelect }: ScenePreviewP ); const bg = scene.image ?? scene.thumbnailUrl ?? null; - return ( -