invisible-watermark-vue
Version:
A TypeScript library for creating and detecting invisible watermarks using Canvas API, with Vue 3 support
1 lines • 15.1 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","names":["cleanupResize: (() => void) | null","vWatermark: Directive<HTMLElement, WatermarkOptions | string>"],"sources":["../src/WatermarkCreator.ts","../src/composables.ts","../src/directive.ts"],"sourcesContent":["import type { WatermarkOptions } from './types';\n\n/**\n * 水印创建器类\n * 负责在Canvas上创建和绘制不可见水印\n */\nexport class WatermarkCreator {\n private canvas: HTMLCanvasElement | null = null;\n private ctx: CanvasRenderingContext2D | null = null;\n private container: HTMLElement | null = null;\n private currentText = '';\n private currentOptions: Required<WatermarkOptions>;\n\n private readonly defaultOptions: Required<WatermarkOptions> = {\n text: '',\n opacity: 0.005,\n fontSize: 20,\n fontFamily: 'Arial',\n rotate: -15,\n color: '0, 0, 0',\n spacingX: 1,\n spacingY: 1.5,\n };\n\n constructor() {\n this.currentOptions = { ...this.defaultOptions };\n }\n\n /**\n * 初始化Canvas元素\n */\n private initCanvas(container: HTMLElement): void {\n // 检查是否已存在canvas\n let canvas = container.querySelector('.invisible-watermark-canvas') as HTMLCanvasElement;\n\n if (!canvas) {\n canvas = document.createElement('canvas');\n canvas.className = 'invisible-watermark-canvas';\n canvas.style.position = 'absolute';\n canvas.style.top = '0';\n canvas.style.left = '0';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.pointerEvents = 'none';\n canvas.style.zIndex = '9999';\n\n // 确保容器是定位元素\n const position = window.getComputedStyle(container).position;\n if (position === 'static') {\n container.style.position = 'relative';\n }\n\n container.appendChild(canvas);\n }\n\n this.canvas = canvas;\n this.ctx = canvas.getContext('2d');\n this.container = container;\n }\n\n /**\n * 绘制水印到Canvas\n */\n private drawWatermark(): void {\n if (!this.canvas || !this.ctx || !this.container) {\n throw new Error('Canvas not initialized. Call apply() first.');\n }\n\n const { opacity, fontSize, fontFamily, rotate, color, spacingX, spacingY } = this.currentOptions;\n\n // 设置canvas尺寸为容器尺寸\n const dpr = window.devicePixelRatio || 1;\n this.canvas.width = this.container.offsetWidth * dpr;\n this.canvas.height = this.container.offsetHeight * dpr;\n this.canvas.style.width = `${this.container.offsetWidth}px`;\n this.canvas.style.height = `${this.container.offsetHeight}px`;\n\n // 清空canvas\n this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n\n // 缩放以适应高DPI屏幕\n this.ctx.scale(dpr, dpr);\n\n // 设置文本样式\n this.ctx.font = `${fontSize}px ${fontFamily}`;\n this.ctx.fillStyle = `rgba(${color}, ${opacity})`;\n this.ctx.textAlign = 'center';\n this.ctx.textBaseline = 'middle';\n\n // 计算文本尺寸\n const textWidth = this.ctx.measureText(this.currentText).width * spacingX;\n const textHeight = fontSize * spacingY;\n\n // 计算需要多少行和列\n const cols = Math.ceil(this.container.offsetWidth / textWidth) + 2;\n const rows = Math.ceil(this.container.offsetHeight / textHeight) + 2;\n\n // 保存当前状态\n this.ctx.save();\n\n // 旋转画布\n this.ctx.translate(this.container.offsetWidth / 2, this.container.offsetHeight / 2);\n this.ctx.rotate((rotate * Math.PI) / 180);\n this.ctx.translate(-this.container.offsetWidth / 2, -this.container.offsetHeight / 2);\n\n // 绘制水印网格\n for (let i = 0; i < rows; i++) {\n for (let j = 0; j < cols; j++) {\n const x = j * textWidth - textWidth;\n const y = i * textHeight - textHeight;\n this.ctx.fillText(this.currentText, x, y);\n }\n }\n\n // 恢复状态\n this.ctx.restore();\n }\n\n /**\n * 应用水印到指定容器\n * @param container - 目标DOM容器\n * @param options - 水印配置选项\n */\n apply(container: HTMLElement, options: WatermarkOptions): void {\n if (!options.text || !options.text.trim()) {\n throw new Error('Watermark text cannot be empty');\n }\n\n this.currentOptions = { ...this.defaultOptions, ...options };\n this.currentText = options.text;\n\n this.initCanvas(container);\n this.drawWatermark();\n }\n\n /**\n * 更新水印\n * @param options - 新的水印配置选项\n */\n update(options: Partial<WatermarkOptions>): void {\n if (!this.container) {\n throw new Error('Watermark not initialized. Call apply() first.');\n }\n\n if (options.text !== undefined) {\n this.currentText = options.text;\n }\n\n this.currentOptions = { ...this.currentOptions, ...options };\n this.drawWatermark();\n }\n\n /**\n * 清除水印\n */\n clear(): void {\n if (this.canvas && this.ctx) {\n this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n this.currentText = '';\n }\n\n /**\n * 移除水印Canvas元素\n */\n destroy(): void {\n if (this.canvas && this.canvas.parentNode) {\n this.canvas.parentNode.removeChild(this.canvas);\n }\n\n this.canvas = null;\n this.ctx = null;\n this.container = null;\n this.currentText = '';\n }\n\n /**\n * 获取当前水印文本\n */\n getCurrentText(): string {\n return this.currentText;\n }\n\n /**\n * 获取当前配置\n */\n getCurrentOptions(): Required<WatermarkOptions> {\n return { ...this.currentOptions };\n }\n\n /**\n * 监听窗口大小变化并重绘水印\n */\n enableAutoResize(): () => void {\n const handleResize = () => {\n if (this.currentText && this.container) {\n this.drawWatermark();\n }\n };\n\n window.addEventListener('resize', handleResize);\n\n // 返回清理函数\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }\n}\n","import { ref, onMounted, onUnmounted, type Ref } from 'vue';\nimport { WatermarkCreator } from './WatermarkCreator';\nimport type { WatermarkOptions } from './types';\n\n/**\n * Vue3 组合式函数 - 水印创建\n * @param containerRef - 容器元素的ref\n * @param options - 水印配置选项\n * @returns 水印控制方法\n */\nexport function useWatermark(\n containerRef: Ref<HTMLElement | null>,\n options?: WatermarkOptions\n) {\n const creator = new WatermarkCreator();\n const isApplied = ref(false);\n const currentText = ref('');\n let cleanupResize: (() => void) | null = null;\n\n /**\n * 应用水印\n */\n const apply = (newOptions?: WatermarkOptions) => {\n if (!containerRef.value) {\n console.warn('Container element not found');\n return;\n }\n\n const finalOptions = newOptions || options;\n if (!finalOptions) {\n throw new Error('Watermark options are required');\n }\n\n creator.apply(containerRef.value, finalOptions);\n currentText.value = finalOptions.text;\n isApplied.value = true;\n\n // 启用自动调整大小\n if (!cleanupResize) {\n cleanupResize = creator.enableAutoResize();\n }\n };\n\n /**\n * 更新水印\n */\n const update = (newOptions: Partial<WatermarkOptions>) => {\n creator.update(newOptions);\n if (newOptions.text !== undefined) {\n currentText.value = newOptions.text;\n }\n };\n\n /**\n * 清除水印\n */\n const clear = () => {\n creator.clear();\n isApplied.value = false;\n currentText.value = '';\n };\n\n /**\n * 销毁水印\n */\n const destroy = () => {\n creator.destroy();\n if (cleanupResize) {\n cleanupResize();\n cleanupResize = null;\n }\n isApplied.value = false;\n currentText.value = '';\n };\n\n // 如果提供了初始选项,在挂载时自动应用\n onMounted(() => {\n if (options && containerRef.value) {\n apply(options);\n }\n });\n\n // 组件卸载时清理\n onUnmounted(() => {\n destroy();\n });\n\n return {\n isApplied,\n currentText,\n apply,\n update,\n clear,\n destroy,\n };\n}\n","import type { Directive, DirectiveBinding } from 'vue';\nimport { WatermarkCreator } from './WatermarkCreator';\nimport type { WatermarkOptions } from './types';\n\n// 存储每个元素对应的水印实例\nconst watermarkInstances = new WeakMap<HTMLElement, WatermarkCreator>();\n\n/**\n * Vue 3 水印指令\n *\n * @example\n * // 基础用法\n * <div v-watermark=\"{ text: 'user_12345', opacity: 0.005 }\">内容</div>\n *\n * // 使用变量\n * <div v-watermark=\"watermarkConfig\">内容</div>\n *\n * // 简写(仅文本)\n * <div v-watermark=\"'user_12345'\">内容</div>\n */\nexport const vWatermark: Directive<HTMLElement, WatermarkOptions | string> = {\n mounted(el: HTMLElement, binding: DirectiveBinding<WatermarkOptions | string>) {\n const options = typeof binding.value === 'string'\n ? { text: binding.value }\n : binding.value;\n\n if (!options || !options.text) {\n console.warn('[v-watermark] Watermark text is required');\n return;\n }\n\n try {\n const creator = new WatermarkCreator();\n creator.apply(el, options);\n\n // 启用自动调整大小\n const cleanup = creator.enableAutoResize();\n\n // 保存实例和清理函数\n watermarkInstances.set(el, creator);\n\n // 保存清理函数到元素上(用于unmounted时调用)\n (el as any)._watermarkCleanup = cleanup;\n } catch (error) {\n console.error('[v-watermark] Failed to apply watermark:', error);\n }\n },\n\n updated(el: HTMLElement, binding: DirectiveBinding<WatermarkOptions | string>) {\n const creator = watermarkInstances.get(el);\n if (!creator) {\n console.warn('[v-watermark] Watermark instance not found');\n return;\n }\n\n const options = typeof binding.value === 'string'\n ? { text: binding.value }\n : binding.value;\n\n if (!options || !options.text) {\n console.warn('[v-watermark] Watermark text is required');\n return;\n }\n\n try {\n creator.update(options);\n } catch (error) {\n console.error('[v-watermark] Failed to update watermark:', error);\n }\n },\n\n unmounted(el: HTMLElement) {\n const creator = watermarkInstances.get(el);\n if (creator) {\n creator.destroy();\n watermarkInstances.delete(el);\n }\n\n // 清理resize监听器\n const cleanup = (el as any)._watermarkCleanup;\n if (cleanup) {\n cleanup();\n delete (el as any)._watermarkCleanup;\n }\n },\n};\n\n/**\n * 水印插件\n *\n * @example\n * import { createApp } from 'vue';\n * import { WatermarkPlugin } from 'invisible-watermark-vue';\n *\n * const app = createApp(App);\n * app.use(WatermarkPlugin);\n */\nexport const WatermarkPlugin = {\n install(app: any) {\n app.directive('watermark', vWatermark);\n },\n};\n"],"mappings":"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"}