UNPKG

@zzjiaxiang/vue-watermark

Version:
290 lines (271 loc) 7.1 kB
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 };