优化蒙版
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Upload, X } from 'lucide-react';
|
||||
import type { ElementMaskConfig, TemplateElement } from '../types';
|
||||
@@ -22,14 +22,16 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
onConfirm,
|
||||
}) => {
|
||||
const [draft, setDraft] = useState<ElementMaskConfig>({ enabled: true, type: 'rect' });
|
||||
const draftRef = useRef(draft);
|
||||
draftRef.current = draft;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !element) return;
|
||||
setDraft(
|
||||
element.mask?.enabled
|
||||
? { ...createDefaultElementMask(element), ...element.mask, enabled: true }
|
||||
: createDefaultElementMask(element)
|
||||
);
|
||||
const initial = element.mask?.enabled
|
||||
? { ...createDefaultElementMask(element), ...element.mask, enabled: true }
|
||||
: createDefaultElementMask(element);
|
||||
draftRef.current = initial;
|
||||
setDraft(initial);
|
||||
}, [open, element]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,14 +54,54 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
const canConfirm = maskType !== 'image' || !!draft.image?.trim();
|
||||
|
||||
const patchDraft = (patch: Partial<ElementMaskConfig>) => {
|
||||
setDraft((prev) => ({ ...prev, ...patch }));
|
||||
setDraft((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
draftRef.current = next;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const patchDraftCorner = (key: keyof ElementMaskConfig, val: string) => {
|
||||
if (val.trim() === '') {
|
||||
setDraft((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
draftRef.current = next;
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
patchDraft({ [key]: Math.max(0, round1(Number(val) || 0)) });
|
||||
};
|
||||
|
||||
const normalizeDraft = (source: ElementMaskConfig): ElementMaskConfig => ({
|
||||
...source,
|
||||
enabled: true,
|
||||
x: round1(Number(source.x) || 0),
|
||||
y: round1(Number(source.y) || 0),
|
||||
width: Math.max(0.5, round1(Number(source.width) || element.width)),
|
||||
height: Math.max(0.5, round1(Number(source.height) || element.height)),
|
||||
borderRadius: Math.max(0, round1(Number(source.borderRadius) || 0)),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!canConfirm) return;
|
||||
onConfirm({ ...draft, enabled: true });
|
||||
onClose();
|
||||
const form = e.currentTarget as HTMLFormElement;
|
||||
const active = document.activeElement;
|
||||
|
||||
const submit = () => {
|
||||
if (!canConfirm) return;
|
||||
onConfirm(normalizeDraft(draftRef.current));
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (active instanceof HTMLElement && form.contains(active)) {
|
||||
active.blur();
|
||||
setTimeout(submit, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
submit();
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
@@ -87,9 +129,25 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="ps-modal-body space-y-3">
|
||||
<p className="text-[11px] text-[#888] leading-relaxed">
|
||||
蒙版用于裁剪图层可见区域,相对元素左上角定位;预览、导出与打印均生效。
|
||||
蒙版仅作用于当前图层(背景与内容);先在离屏画布合成,再按蒙版裁剪后绘制到画布。边框不受蒙版影响。
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="ps-field-label">蒙版方向</label>
|
||||
<FieldSelect
|
||||
value={draft.mode ?? 'include'}
|
||||
onChange={(e) =>
|
||||
patchDraft({
|
||||
mode: e.target.value as ElementMaskConfig['mode'],
|
||||
})
|
||||
}
|
||||
className="ps-field text-[11px]"
|
||||
>
|
||||
<option value="include">正向(保留区域内)</option>
|
||||
<option value="exclude">反向(保留区域外)</option>
|
||||
</FieldSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="ps-field-label">蒙版类型</label>
|
||||
<FieldSelect
|
||||
@@ -135,7 +193,9 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
min="0.5"
|
||||
value={regionW}
|
||||
onCommit={(val) =>
|
||||
patchDraft({ width: Math.max(0.5, round1(Number(val) || element.width)) })
|
||||
patchDraft({
|
||||
width: Math.max(0.5, round1(Number(val) || element.width)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono text-[11px]"
|
||||
/>
|
||||
@@ -148,7 +208,9 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
min="0.5"
|
||||
value={regionH}
|
||||
onCommit={(val) =>
|
||||
patchDraft({ height: Math.max(0.5, round1(Number(val) || element.height)) })
|
||||
patchDraft({
|
||||
height: Math.max(0.5, round1(Number(val) || element.height)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono text-[11px]"
|
||||
/>
|
||||
@@ -188,15 +250,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
max="50"
|
||||
step="0.5"
|
||||
value={draft[key] ?? ''}
|
||||
onCommit={(val) => {
|
||||
if (val === '' || val === undefined) {
|
||||
const next = { ...draft };
|
||||
delete next[key];
|
||||
setDraft(next);
|
||||
return;
|
||||
}
|
||||
patchDraft({ [key]: Math.max(0, round1(Number(val) || 0)) });
|
||||
}}
|
||||
onCommit={(val) => patchDraftCorner(key, val)}
|
||||
className="ps-field font-mono text-[10px] flex-1 min-w-0"
|
||||
placeholder="—"
|
||||
/>
|
||||
@@ -226,7 +280,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
/>
|
||||
</label>
|
||||
<p className="text-[9px] text-[#666] leading-relaxed">
|
||||
推荐使用带透明区域的 PNG;不透明区域将保留图层内容,透明区域将被裁剪。
|
||||
按图片亮度控制蒙版:白色区域保留内容,黑色区域裁剪;反向模式下黑白互换。
|
||||
</p>
|
||||
{draft.image && (
|
||||
<div className="border border-[#3a3a3a] bg-[#1e1e1e] p-2">
|
||||
@@ -257,7 +311,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="ps-modal-footer">
|
||||
{element.mask?.enabled && (
|
||||
{element.mask && element.mask.enabled !== false && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
|
||||
@@ -1636,9 +1636,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
>
|
||||
<div
|
||||
data-drag-float-placeholder={el.id}
|
||||
className={`absolute inset-0 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.12)]${
|
||||
dragMultiSelectionBounds ? '' : ' border border-[#31a8ff]/40'
|
||||
}`}
|
||||
className="absolute inset-0 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.12)]"
|
||||
/>
|
||||
<img
|
||||
ref={(node) => {
|
||||
@@ -1672,6 +1670,12 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{selectedElementIds.length === 1 && (
|
||||
<div
|
||||
className="absolute inset-0 ps-resize-outline pointer-events-none z-[3]"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user