@zzjiaxiang/vue-watermark
Version:
A watermark Component for Vue3
290 lines (271 loc) • 7.1 kB
JavaScript
import { ref, watch, onMounted, openBlock, createElementBlock, renderSlot, nextTick } from 'vue';
function toLowercaseSeparator(key) {
return key
.replace(/([A-Z])/g, (replacement) => `-${replacement}`)
.toLowerCase()
}
function getStyleStr(style) {
return Object.keys(style)
.map((key) => `${toLowercaseSeparator(key)}: ${style[key]};`)
.join(' ')
}
/** Returns the ratio of the device's physical pixel resolution to the css pixel resolution */
function getPixelRatio() {
return window.devicePixelRatio || 1
}
/** Rotate with the watermark as the center point */
function rotateWatermark(ctx, rotateX, rotateY, rotate) {
ctx.translate(rotateX, rotateY);
ctx.rotate((Math.PI / 180) * Number(rotate));
ctx.translate(-rotateX, -rotateY);
}
/** Whether to re-render the watermark */
const reRendering = (mutation, watermarkElement) => {
let flag = false;
// Whether to delete the watermark node
if (mutation.removedNodes.length) {
flag = Array.from(mutation.removedNodes).includes(watermarkElement);
}
// Whether the watermark dom property value has been modified
if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
flag = true;
}
return flag
};
const MutateObserver = (targetNode, callback) => {
const config = {
subtree: true,
childList: true,
attributeFilter: ['style', 'class'],
};
if (targetNode && 'MutationObserver' in window) {
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
};
const FontGap = 3;
var script = /*#__PURE__*/Object.assign({
name: 'WaterMark',
}, {
__name: 'water-mark',
props: {
show: {
type: Boolean,
default: true,
},
gapX: {
type: Number,
default: 200,
},
gapY: {
type: Number,
default: 200,
},
zIndex: {
type: Number,
default: 1,
},
width: {
type: Number,
default: 120,
},
height: {
type: Number,
default: 64,
},
rotate: {
type: Number,
default: -22,
},
image: {
type: String,
default: '',
},
imageWidth: {
type: Number,
default: 120,
},
imageHeight: {
type: Number,
default: 64,
},
content: {
type: [String, Array],
default: '',
},
fontColor: {
type: String,
default: 'rgba(0,0,0,.15)',
},
fontStyle: {
type: String,
default: 'normal',
},
fontFamily: {
type: String,
default: 'sans-serif',
},
fontWeight: {
type: String,
default: 'normal',
},
fontSize: {
type: [String, Number],
default: 16,
},
},
setup(__props) {
const props = __props;
const markWrap = ref(null);
const watermark = ref(null);
const stopObservation = ref(false);
const watermarkStyle = () => ({
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: props.zIndex,
});
const getMarkSize = (ctx) => {
let defaultWidth = 120;
let defaultHeight = 64;
if (ctx.measureText) {
ctx.font = `${Number(props.fontSize)}px ${props.fontFamily}`;
const contents = Array.isArray(props.content)
? props.content
: [props.content];
const widths = contents.map((item) => ctx.measureText(item).width);
defaultWidth = Math.ceil(Math.max(...widths));
defaultHeight = Number(props.fontSize) * contents.length;
}
return [defaultWidth, defaultHeight]
};
const appendWatermark = (base64Url, markWidth) => {
if (markWrap.value && watermark.value) {
stopObservation.value = true;
watermark.value.setAttribute(
'style',
getStyleStr({
...watermarkStyle(),
backgroundImage: `url(${base64Url})`,
backgroundSize: `${props.gapX + markWidth}px`,
}),
);
markWrap.value?.append(watermark.value);
nextTick(() => {
stopObservation.value = false;
});
}
};
const fillTexts = (
ctx,
drawX,
drawY,
drawWidth,
drawHeight,
canvas,
markWidth,
) => {
const ratio = getPixelRatio();
const mergedFontSize = Number(props.fontSize) * ratio;
ctx.font = `${props.fontStyle} normal ${props.fontWeight} ${mergedFontSize}px/${drawHeight}px ${props.fontFamily}`;
ctx.fillStyle = props.fontColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.translate(drawWidth / 2, 0);
const contents = Array.isArray(props.content)
? props.content
: [props.content];
contents?.forEach((item, index) => {
ctx.fillText(
item ?? '',
drawX,
drawY + index * (mergedFontSize + FontGap * ratio),
);
});
appendWatermark(canvas.toDataURL(), markWidth);
};
const renderWatermark = () => {
if (!props.show) {
return
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
if (!watermark.value) {
watermark.value = document.createElement('div');
}
const ratio = getPixelRatio();
const [markWidth, markHeight] = getMarkSize(ctx);
const canvasWidth = `${(props.gapX + markWidth) * ratio}px`;
const canvasHeight = `${(props.gapY + markHeight) * ratio}px`;
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
const drawX = (props.gapX * ratio) / 2;
const drawY = (props.gapY * ratio) / 2;
const drawWidth = markWidth * ratio;
const drawHeight = markHeight * ratio;
const rotateX = (drawWidth + props.gapX * ratio) / 2;
const rotateY = (drawHeight + props.gapY * ratio) / 2;
rotateWatermark(ctx, rotateX, rotateY, props.rotate);
if (props.image) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.referrerPolicy = 'no-referrer';
img.src = props.image;
img.onload = () => {
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
appendWatermark(canvas.toDataURL(), markWidth);
};
img.onerror = () => {
fillTexts(ctx, drawX, drawY, drawWidth, drawHeight, canvas, markWidth);
};
} else {
fillTexts(ctx, drawX, drawY, drawWidth, drawHeight, canvas, markWidth);
}
ctx.restore();
}
};
const destroyedWatermark = () => {
if (watermark.value) {
watermark.value.remove();
// release vue reactive
watermark.value = null;
}
};
const onMutate = (targetNode) => {
const callback = (mutationsList) => {
if (stopObservation.value) {
return
}
mutationsList.forEach((mutationsL) => {
if (reRendering(mutationsL, watermark.value)) {
destroyedWatermark();
renderWatermark();
}
});
};
MutateObserver(targetNode, callback);
};
watch(props, () => renderWatermark(), { deep: true });
onMounted(() => {
renderWatermark();
onMutate(markWrap.value);
});
return (_ctx, _cache) => {
return (openBlock(), createElementBlock("div", {
ref_key: "markWrap",
ref: markWrap,
style: { position: 'relative' }
}, [
renderSlot(_ctx.$slots, "default")
], 512 /* NEED_PATCH */))
}
}
});
script.__file = "src/package/components/water-mark/src/water-mark.vue";
script.install = (app) => {
app.component(script.name, script);
};
export { script as default };