Last active
August 23, 2022 16:25
-
-
Save axetroy/6763af7e4b188e5594272e9af1af736a to your computer and use it in GitHub Desktop.
前端水印方案
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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