Skip to content

Instantly share code, notes, and snippets.

@axetroy
Last active August 23, 2022 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save axetroy/6763af7e4b188e5594272e9af1af736a to your computer and use it in GitHub Desktop.
Save axetroy/6763af7e4b188e5594272e9af1af736a to your computer and use it in GitHub Desktop.
前端水印方案
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>水印生成器</title>
</head>
<body>
<div id="root">
<div>
<div>
<label for="text">水印内容:</label>
</div>
<textarea name="text" id="text" rows="4" cols="50"> </textarea>
</div>
<div>
<button onclick="addWatermark()">生成水印</button>
<button onclick="cancelWatermark()">取消水印</button>
</div>
<div>
refs:
<ul>
<li>
<a
href="https://newspaint.wordpress.com/2014/05/22/writing-rotated-text-on-a-javascript-canvas/"
>https://newspaint.wordpress.com/2014/05/22/writing-rotated-text-on-a-javascript-canvas/</a
>
</li>
<li>
<a
href="https://stackoverflow.com/questions/25172501/html5-rotate-text-around-its-centre-point"
>https://stackoverflow.com/questions/25172501/html5-rotate-text-around-its-centre-point</a
>
</li>
</ul>
</div>
</div>
<script type="module">
import { Watermark } from "./watermark.js";
const waterMark = new Watermark(document.body);
const $text = document.querySelector("#text");
$text.value = "法外狂徒张三\n13377801021";
window.addWatermark = function addWatermark() {
waterMark.show({
words: [
{
value: "法外狂徒张三",
color: "rgba(255,0,0,0.5)",
fontSize: 22,
},
{
value: "13377801021",
color: "rgba(0,255,0,0.5)",
fontSize: 16,
},
{
value: "2022-08-20",
color: "rgba(0,0,255,0.5)",
fontSize: 10,
},
],
position: "fixed",
rotate: 45,
padding: 20,
debug: true,
textAlign: "center",
});
};
window.cancelWatermark = function cancelWatermark() {
waterMark.dispose();
};
addWatermark();
</script>
</body>
</html>
const MutationObserver =
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver;
/**
* 水印实例
*/
export class Watermark {
/**
* @type {HTMLElement} container
* */
constructor(container = document.body) {
/**
* @type {HTMLElement}
*/
this.container = container;
/**
* @type { MutationObserver }
*/
this.ob = null;
/**
* @type { MutationObserver }
*/
this.obContainer = null;
}
/**
* 显示水印
* @returns {number}
*/
show({
fillStyle = "rgba(184, 184, 184, 0.6)",
opacity = 0.5,
padding = 60,
textAlign = "center",
image = null, // 内嵌的图片
words = [], // [{"value": "xxx", "color": "red"}]
rotate = 45, // 0 - 90
zIndex = 10000,
debug = false,
position = "absolute",
} = {}) {
this.dispose();
const fontFamily = "verdana";
const container = this.container;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const maxWidth =
Math.round(
Math.max(
...words.map((v) => {
ctx.font = `${v.fontSize}px ${fontFamily}`;
const metrics = ctx.measureText(v.value);
return metrics.width;
})
)
) +
padding * 2;
const lineSpace = 4;
/**
* 每行文字高度
*/
const lineHeights = words.map((v, index) => {
ctx.textBaseline = "top";
ctx.font = `${v.fontSize}px ${fontFamily}`;
const metrics = ctx.measureText(v.value);
const actualHeight =
Math.abs(metrics.actualBoundingBoxAscent) + // 边界可能超出基线,会是负数
metrics.actualBoundingBoxDescent;
const lineHeight = Math.round(actualHeight);
return lineHeight;
});
const linesText = lineHeights.map((h, index) => {
const word = words[index];
// 跟上一行字的间距
const space = index === 0 ? 0 : index * lineSpace;
return {
...word,
x: 0,
y:
padding + // 容器的 padding
lineHeights.slice(0, index).reduce((a, b) => a + b, 0) +
space,
lineHeight: h,
};
});
const maxHeight =
Math.round(lineHeights.reduce((a, b) => a + b, 0)) + // 每行字的高度
(linesText.length - 1) * lineSpace + // 每行字的间隙
padding * 2; // 容器的 padding
canvas.width = maxWidth;
canvas.height = maxHeight;
ctx.save();
const actualWidth =
Math.cos((rotate * Math.PI) / 180) * maxWidth +
Math.cos(((90 - rotate) * Math.PI) / 180) * maxHeight;
const actualHeight =
Math.sin((rotate * Math.PI) / 180) * maxWidth +
Math.sin(((90 - rotate) * Math.PI) / 180) * maxHeight;
canvas.width = actualWidth;
canvas.height = actualHeight;
ctx.fillStyle = "black";
ctx.font = `20px ${fontFamily}`;
if (debug) {
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, canvas.height / 2);
ctx.lineTo(canvas.width, canvas.height / 2);
ctx.stroke();
}
ctx.textAlign = textAlign;
ctx.textBaseline = "top";
/**
* 图中的中心点做吧
*/
const center = {
x:
canvas.width / 2 +
Math.cos(((90 - rotate) * Math.PI) / 180) * (maxHeight / 2),
y:
canvas.height / 2 -
Math.cos((rotate * Math.PI) / 180) * (maxHeight / 2),
};
if (ctx.textAlign === "center") {
ctx.translate(center.x, center.y);
} else if (ctx.textAlign === "left") {
ctx.translate(
center.x - (Math.cos((rotate * Math.PI) / 180) * maxWidth) / 4,
center.y - (Math.sin((rotate * Math.PI) / 180) * maxWidth) / 4
);
} else if (ctx.textAlign === "right") {
ctx.translate(
center.x + (Math.cos((rotate * Math.PI) / 180) * maxWidth) / 4,
center.y + (Math.sin((rotate * Math.PI) / 180) * maxWidth) / 4
);
}
ctx.rotate((rotate * Math.PI) / 180);
ctx.fillStyle = fillStyle;
ctx.globalAlpha = opacity;
linesText.forEach((word, index) => {
ctx.fillStyle = word.color;
ctx.font = `${word.fontSize}px ${fontFamily}`;
ctx.fillText(word.value, word.x, word.y);
});
ctx.restore();
const base64Url = canvas.toDataURL();
const $watermarkBox = container.querySelector(".watermark-box");
const id = "id-" + parseInt(Math.random() * 100000000);
let watermarkDiv = $watermarkBox || document.createElement("div");
const styleStr = `
position:${position};
display: block;
opacity: 1;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: ${zIndex};
pointer-events: none;
background-repeat: repeat;
background-image: url('${base64Url}')`;
watermarkDiv.className = "watermark-box";
watermarkDiv.setAttribute("style", styleStr);
watermarkDiv.id = watermarkDiv.id || id;
Watermark.Map[id] = this;
let clonedWatermarkDiv = watermarkDiv.cloneNode(true);
const containerPosition = getComputedStyle(container).position;
if (!containerPosition || containerPosition === "static") {
container.style.position = "relative";
}
if (!$watermarkBox) {
container.insertBefore(watermarkDiv, container.firstChild);
}
if (MutationObserver) {
// 防止属性被修改而隐藏了水印
const startObserve = () => {
this.ob && this.ob.disconnect();
this.ob = null;
this.ob = new MutationObserver((nodes) => {
if (!watermarkDiv) {
container.insertBefore(watermarkDiv, container.firstChild);
} else {
watermarkDiv.remove();
this.ob.disconnect();
this.ob = null;
watermarkDiv = clonedWatermarkDiv;
clonedWatermarkDiv = clonedWatermarkDiv.cloneNode(true);
container.insertBefore(watermarkDiv, container.firstChild);
startObserve();
requestAnimationFrame(() => startObserveContainer());
}
});
this.ob.observe(watermarkDiv, { attributes: true });
};
startObserve();
// 防止节点本删除而隐藏水印
const startObserveContainer = () => {
this.obContainer && this.obContainer.disconnect();
this.obContainer = null;
this.obContainer = new MutationObserver((nodes) => {
watermarkDiv = document.getElementById(watermarkDiv.id);
if (!watermarkDiv) {
this.obContainer.disconnect();
this.obContainer = null;
watermarkDiv = clonedWatermarkDiv;
clonedWatermarkDiv = clonedWatermarkDiv.cloneNode(true);
container.insertBefore(watermarkDiv, container.firstChild);
startObserveContainer();
requestAnimationFrame(() => startObserve());
}
});
this.obContainer.observe(this.container, { childList: true });
};
startObserveContainer();
}
return watermarkDiv.id;
}
dispose() {
if (this.ob) {
this.ob.disconnect();
}
if (this.obContainer) {
this.obContainer.disconnect();
}
const $watermarkBox = this.container.querySelector(".watermark-box");
if ($watermarkBox) {
$watermarkBox.remove();
delete Watermark.Map[$watermarkBox.id];
}
}
}
/**
* 存储所有的水印实例
* @private
* @type { { [key:string]: Watermark } }
*/
Watermark.Map = {};
/**
* 指定隐藏某个水印
* @public
* @param {string} id
* */
Watermark.dispose = (id) => {
const instance = Watermark.Map[id];
if (instance) {
instance.dispose();
}
};
/**
* 销毁所有的水印
* @public
* */
Watermark.destroy = () => {
for (const id in Watermark.Map) {
Watermark.dispose(id);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment