This commit is contained in:
nap.liu 2024-06-06 10:09:46 +08:00
parent 4b75a06c22
commit 163115c8d5
2 changed files with 79 additions and 30 deletions

View File

@ -36,3 +36,7 @@
transform: rotate(360deg);
}
}
input {
width: 100%;
}

View File

@ -8,6 +8,7 @@ type ImageWatermarkProps = {
fontColor?: string;
opacity?: number;
spacing?: number;
isOrigin?: boolean;
};
const ImageWatermark = ({
file,
@ -16,8 +17,8 @@ const ImageWatermark = ({
fontColor = "#000",
opacity = 0.2,
spacing = 50,
isOrigin = false,
}: ImageWatermarkProps) => {
// const [src, setSrc] = useState("");
const canvasRef = useRef<HTMLCanvasElement>(null);
const imgRef = useRef(new Image());
@ -31,21 +32,26 @@ const ImageWatermark = ({
img.src = src;
img.onload = () => {
// 获取设备像素比
// const devicePixelRatio = window.devicePixelRatio || 1;
const devicePixelRatio = 1;
const width = isOrigin ? img.width : 1200;
const height = isOrigin ? img.height : (img.height / img.width) * width;
// 设置canvas的物理像素尺寸
canvas.width = img.width * devicePixelRatio;
canvas.height = img.height * devicePixelRatio;
canvas.width = width;
canvas.height = height;
// 缩放绘图上下文以适应设备像素比
context.scale(devicePixelRatio, devicePixelRatio);
// 绘制图片
context.drawImage(img, 0, 0, img.width, img.height);
context.drawImage(img, 0, 0, width, height);
// 根据图片尺寸动态计算字体大小
const finallyFontSize = Math.min(width, height) / fontSize;
// 设置水印样式
context.font = `${fontSize}px Arial`;
context.font = `${finallyFontSize}px Arial`;
context.fillStyle = fontColor;
context.globalAlpha = opacity;
context.textAlign = "center";
@ -54,24 +60,24 @@ const ImageWatermark = ({
// 计算水印文本的宽度和高度
const textMetrics = context.measureText(text);
const textWidth = textMetrics.width;
const textHeight = fontSize;
const textHeight = finallyFontSize;
// 计算水印的间距和偏移
const stepX = textWidth + spacing;
const stepY = textHeight + spacing;
const stepX = textWidth + finallyFontSize + spacing;
const stepY = textHeight + finallyFontSize + spacing;
const angle = Math.PI / 4; // 45度角
// 保存当前绘图状态
context.save();
// 旋转canvas以绘制斜角水印
context.translate(img.width / 2, img.height / 2);
context.translate(width / 2, height / 2);
context.rotate(-angle);
context.translate(-img.width / 2, -img.height / 2);
context.translate(-width / 2, -height / 2);
// 绘制斜角、重复的水印文本
for (let x = -img.width; x < img.width * 2; x += stepX) {
for (let y = -img.height; y < img.height * 2; y += stepY) {
for (let x = -width; x < width * 2; x += stepX) {
for (let y = -height; y < height * 2; y += stepY) {
context.fillText(text, x, y);
}
}
@ -86,11 +92,13 @@ const ImageWatermark = ({
}, [file, text, fontSize, fontColor, opacity, spacing]);
const onDownload = () => {
const canvas = canvasRef.current!;
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = file.name;
a.click();
if (confirm("确定下载图片?")) {
const canvas = canvasRef.current!;
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = file.name;
a.click();
}
};
return (
@ -99,6 +107,7 @@ const ImageWatermark = ({
style={{
width: "100%", // 设置CSS尺寸
height: "auto", // 设置CSS尺寸
marginBottom: "20px",
}}
onClick={onDownload}
/>
@ -108,11 +117,32 @@ const ImageWatermark = ({
function App() {
const [files, setFiles] = useState<File[]>([]);
const [keyword, setKeyword] = useState<string>("水印");
const [fontSize, setFontSize] = useState(16);
const [fontColor, setFontColor] = useState("#000");
const [opacity, setOpacity] = useState(0.2);
const [spacing, setSpacing] = useState(50);
const [keyword, setKeyword] = useState<string>(
localStorage.getItem("keyword") || "水印"
);
const [fontSize, setFontSize] = useState(
Number(localStorage.getItem("fontSize")) || 16
);
const [fontColor, setFontColor] = useState(
localStorage.getItem("fontColor") || "#000000"
);
const [opacity, setOpacity] = useState(
Number(localStorage.getItem("opacity")) || 0.2
);
const [spacing, setSpacing] = useState(
Number(localStorage.getItem("spacing")) || 50
);
const [isOrigin, setIsOrigin] = useState(
localStorage.getItem("isOrigin") === "true"
);
useEffect(() => {
localStorage.setItem("keyword", keyword);
localStorage.setItem("fontSize", String(fontSize));
localStorage.setItem("fontColor", fontColor);
localStorage.setItem("opacity", String(opacity));
localStorage.setItem("spacing", String(spacing));
}, [keyword, fontSize, fontColor, opacity, spacing]);
const onSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
setFiles(Array.from(e.target.files || []));
@ -121,7 +151,14 @@ function App() {
return (
<div className="App">
<div
style={{ gap: 10, display: "grid", gridTemplate: "'a b' / auto 1fr" }}
style={{
padding: 16,
gap: 10,
display: "grid",
gridTemplate: "'a b' / auto 1fr",
alignItems: "flex-start",
justifyItems: "flex-start",
}}
>
<label htmlFor="file"></label>
<input
@ -139,14 +176,14 @@ function App() {
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<label htmlFor="fontSize"></label>
<label htmlFor="fontSize"></label>
<input
type="range"
id="fontSize"
value={fontSize}
min={12}
max={48}
onChange={(e) => setFontSize(Number(e.target.value))}
value={50 - fontSize}
min={3}
max={50}
onChange={(e) => setFontSize(50 - Number(e.target.value))}
/>
<label htmlFor="fontColor"></label>
<input
@ -170,11 +207,18 @@ function App() {
type="range"
id="spacing"
value={spacing}
min={10}
min={0}
max={100}
step={10}
onChange={(e) => setSpacing(Number(e.target.value))}
/>
<label htmlFor="isOrigin">使</label>
<input
id="isOrigin"
type="checkbox"
checked={isOrigin}
onChange={(e) => setIsOrigin(e.target.checked)}
/>
</div>
{files.map((file) => (
@ -186,6 +230,7 @@ function App() {
opacity={opacity}
key={file.name}
text={keyword}
isOrigin={isOrigin}
/>
))}
</div>