UNPKG

@megaads/wm

Version:

To install the library, use npm:

249 lines (228 loc) 7.03 kB
import React, { useEffect, useRef, useState } from 'react'; export default function Preview({ snapshot }) { const { mockup, designLayers, artwork } = snapshot; const wrapperRef = useRef(null); const [scale, setScale] = useState(1); useEffect(() => { if (!mockup || !wrapperRef.current) return; const el = wrapperRef.current; const update = () => { const w = el.clientWidth - 32; const h = el.clientHeight - 32; const s = Math.min(w / mockup.width, h / mockup.height); setScale(s > 0 ? s : 1); }; update(); const ro = new ResizeObserver(update); ro.observe(el); return () => ro.disconnect(); }, [mockup?.width, mockup?.height]); if (!mockup) { return <div className="preview-empty">No mockup data</div>; } return ( <div ref={wrapperRef} className="preview-canvas"> <div style={{ position: 'relative', width: mockup.width, height: mockup.height, transform: `scale(${scale})`, transformOrigin: 'center center', background: '#fff', boxShadow: '0 0 0 1px #334155', }} > {(mockup.layers || []).map((layer, i) => ( <MockupLayer key={layer.id ?? i} layer={layer} designLayers={designLayers} artwork={artwork} printAreas={snapshot.printAreas} /> ))} </div> <div className="position-debug"> mockup {mockup.width}×{mockup.height} @ scale {scale.toFixed(2)} · {artwork && ` artwork ${artwork.width}×${artwork.height} ·`} {' '}design layers: {designLayers.length} </div> </div> ); } function MockupLayer({ layer, designLayers, artwork, printAreas }) { const baseStyle = { position: 'absolute', top: layer.top || 0, left: layer.left || 0, width: layer.width, height: layer.height, opacity: layer.opacity ?? 1, zIndex: layer.position ?? 'auto', transform: layer.rotate ? `rotate(${layer.rotate}deg)` : undefined, transformOrigin: 'center center', }; if (layer.type === 'image') { return ( <img src={layer.src || layer.url} alt="" style={{ ...baseStyle, pointerEvents: 'none', objectFit: 'fill' }} /> ); } if (layer.type === 'printarea') { return ( <PrintAreaLayer layer={layer} designLayers={layer.layers || designLayers} artwork={artwork} printAreas={printAreas} /> ); } return null; } function PrintAreaLayer({ layer, designLayers, artwork, printAreas }) { // Ưu tiên kích thước artwork theo print area cụ thể, fallback về artwork chung const printAreaInfo = (printAreas || []).find((p) => p.id === layer.id); const aw = printAreaInfo?.artwork?.width || artwork?.width || layer.width; const ah = printAreaInfo?.artwork?.height || artwork?.height || layer.height; // Tỉ lệ scale từ artwork-coords về printarea-coords (mockup). const sx = layer.width / aw; const sy = layer.height / ah; const clipPath = layer.masked_enable ? buildClipPath(layer) : undefined; return ( <div style={{ position: 'absolute', top: layer.top || 0, left: layer.left || 0, width: layer.width, height: layer.height, opacity: layer.opacity ?? 1, zIndex: layer.position ?? 'auto', transform: layer.rotate ? `rotate(${layer.rotate}deg)` : undefined, transformOrigin: 'center center', overflow: 'hidden', clipPath, // Outline để dễ thấy print area khi debug outline: '1px dashed rgba(99, 102, 241, 0.3)', }} > <div style={{ position: 'absolute', top: 0, left: 0, width: aw, height: ah, transformOrigin: 'top left', transform: `scale(${sx}, ${sy})`, }} > {(designLayers || []).map((dl, i) => ( <DesignLayer key={dl.id ?? i} layer={dl} /> ))} </div> </div> ); } function buildClipPath(layer) { const clipTop = layer.masked_top || 0; const clipLeft = layer.masked_left || 0; const clipWidth = layer.masked_width || layer.width; const clipHeight = layer.masked_height || layer.height; const insetTop = clipTop; const insetLeft = clipLeft; const insetBottom = layer.height - clipTop - clipHeight; const insetRight = layer.width - clipLeft - clipWidth; return `inset(${insetTop}px ${insetRight}px ${insetBottom}px ${insetLeft}px)`; } function DesignLayer({ layer }) { const baseStyle = { position: 'absolute', top: layer.top || 0, left: layer.left || 0, width: layer.width, height: layer.height, opacity: layer.opacity ?? 1, zIndex: layer.order ?? 'auto', transform: layer.rotate ? `rotate(${layer.rotate}deg)` : undefined, transformOrigin: 'center center', pointerEvents: 'none', }; if (layer.type === 'image') { const maskStyles = layer.masked_enable && layer.masked_image ? { WebkitMaskImage: `url(${layer.masked_image})`, maskImage: `url(${layer.masked_image})`, WebkitMaskSize: 'contain', maskSize: 'contain', WebkitMaskRepeat: 'no-repeat', maskRepeat: 'no-repeat', WebkitMaskPosition: 'center', maskPosition: 'center', } : {}; return ( <img src={layer.src || layer.url} alt="" style={{ ...baseStyle, objectFit: 'fill', ...maskStyles, }} /> ); } if (layer.type === 'text') { const fontFamily = layer.typography_type === 'custom' ? layer.custom_font?.family || 'sans-serif' : layer.typography?.family || 'sans-serif'; const fontSize = layer.typography_type === 'custom' ? layer.custom_font?.size || 16 : layer.typography?.size || 16; const variant = layer.typography?.variant; const fontWeight = variant === 'bold' || (typeof variant === 'string' && variant.includes('700')) ? 700 : variant === 'regular' ? 400 : Number(variant) || 400; const justifyContent = layer.align === 'left' ? 'flex-start' : layer.align === 'right' ? 'flex-end' : 'center'; return ( <div style={{ ...baseStyle, color: layer.color || '#000', fontFamily: `"${fontFamily}", sans-serif`, fontSize, fontWeight, display: 'flex', alignItems: 'center', justifyContent, textAlign: layer.align || 'center', whiteSpace: 'nowrap', overflow: 'hidden', WebkitTextStroke: layer.stroke_enabled ? `${layer.stroke_width}px ${layer.stroke_color}` : undefined, }} > {layer.text} </div> ); } return null; }