@megaads/wm
Version:
To install the library, use npm:
249 lines (228 loc) • 7.03 kB
JSX
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;
}