本文主要是介绍JS+HTML实现在线图片水印添加工具,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《JS+HTML实现在线图片水印添加工具》在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题,本文将实现一个完全基于HTML+CSS构建的现代化图片水印在线工具...
概述
在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题。给图片添加水印,正是目前主流平台和创作者最常用的解决方案之一。
但问题也随之而来:
- Photoshop 加水印太麻烦?
- 脚本工具不好操作?
- 找不到一个轻便、美观、好用的工具?
别急,今天带大家实现一个完全基于 html + css 构建的现代化 「图片水印在线工具」,无需安装、纯前端交互,支持在线预览和个性化定制,一切只需浏览器即可完成!
功能亮点
该工具界面简洁、响应迅速,适合各类用户在线添加水印。以下是主要功能一览:
1. 拖拽上传图片
- 支持点击上传与拖拽上传;
- 自带上传动画与视觉反馈;
- 支持预览已上传的图片内容。
2. 水印自定义设置
- 支持文本水印和图像水印(可扩展);
- 自定义颜色、字体、大小、透明度、位置;
- 所有设置实时预览,无需刷新。
3. 响应式设计
- 使用 CSS Grid 进行布局;
- 自动适配移动端和桌面端;
- sticky 面板在大屏上保持固定,操作方便。
4. 高颜值 UI
- 全局主题色可配置;
- 光影、圆角、渐变背景一应俱全;
- UI 参考现代 SaaS 工具风格。
使用方法
第一步:上传你的图片
点击或拖拽图片到上传区域,支持 JPG、PNG 等常用格式。
<input type="file" accept="image/*" id="imageInput">
第二步:设置水印参数
通过设置面板可以自定义以下选项:
设置项 | 说明 |
---|---|
文本内容 | 输入水印文字 |
字体大小 | 使用 <input type="range"> 调整 |
字体颜色 | 使用 <input type="color"> 设置 |
透明度 | 调整水印透明度,范围 0 ~ 1 |
位置选择 | 左上、右上、居中、底部等 |
所有设置项变动后会实时更新预览区域的 Canvas。
第三步:查看预览效果
页面左侧 canvas 区域显示当前的最终效果。支持高清缩放。
<canvas id="previewCanvas" width="1080" height="2400"></canvas>
你可以随时修改设置项查看实时预览,真正做到“所见即所得”。
第四步:导出水印图片(可扩展)
可以通过扩展 js 脚本,调用 canvas.toBlob() 或 toDataURL() 将带水印的图像导出为下载链接,甚至上传到云端。
技术解析
HTML:结构清晰,语义为王
采用语义化标签 section、h1、label 等构建主骨架,使结构更清晰易读,利于 seo 和可维护性。
CSS:原子化变量设计 + 现代布局方案
使用 :root 全局变量控制颜色、阴影、圆角等设计语言,一键更换主题毫无压力。
关键点:
CSS Grid + Flexbox 实现自适应布局;
sticky 属性打造固定侧边栏;
图层阴影、过渡动画等增强交互体验。
--primary-color: #4361ee; --shadow: 0 4px 12px rgba(0,0,0,0.08); .settings { position: sticky; top: 20px; }
JavaScript(扩展点)
虽然本文代码未包含完整 JS,但预留了事件钩子,非常适合后续开发:
- 图片上传:监听 input 或拖拽事件;
- 参数设置:绑定 input 和 change 事件;
- Canvas 绘制:结合 drawImage() 和文本渲染 API;
- 导出下载:使用 canvas.toBlob() 实现图片导出。
延伸思考
除了当前展示的功能,我们还可以这样升级它:
1.添加图像型水印
- 支持上传一张 PNG 作为水印图,叠加在主图上;
- 调整透明度、缩放比例和位置。
2.批量处理功能
- 拖入多张图片,逐一添加相同水印;
- 配合 Web Worker 实现多线程处理。
3.用户配置本地保存
- 利用 localStorage 保存用户上一次的设置;
- 实现个性化“水印模板”方案。
4.导出为高分辨率图像
- 允许用户自定义导出分辨率;
- 适配印刷或商用需求。
5.PWA 打包为 App
- 利用 PWA 特性将其打包为桌面应用或手机 App;
- 实现“离线水印工具”。
运行效果
项目源码下载
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> &ljavascriptt;title>图片水印工具</title> <style> :root { --primary-color: #4361ee; --primary-light: #e6e9ff; --secondary-color: #3f37c9; --text-color: #333; --light-text: #666; --border-color: #ddd; --bg-color: #f8f9fa; --card-bg: #fff; --shadow: 0 4px 12px rgba(0,0,0,0.08); } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; line-height: 1.6; color: var(--text-color); background-color: var(--bg-color); padding: 20px; } h1 { color: var(--primary-color); text-align: center; margin-bottom: 24px; font-weight: 600; } .container { display: grid; grid-template-columns: 1fr 350px; gap: 24px; max-width: 1400px; margin: 0 auto; } .preview { background: var(--card-bg); padding: 20px; border-radius: 12px; box-shadow: var(--shadow); display: flex; flex-direction: column; height: fit-content; } .preview-title { font-size: 18px; font-weight: 500; margin-bottom: 16px; color: var(--primary-color); display: flex; align-items: center; gap: 8px; } .preview-title svg { width: 20px; height: 20px; } #previewCanvas { width: 100%; max-width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: repeating-conic-gradient(#f5f5f5 0% 25%, white 0% 50%) 50%/20px 20px; } .settings { background: var(--card-bg); padding: 24px; border-radius: 12px; box-shadow: var(--shadow); position: sticky; top: 20px; } .settings-title { font-size: 18px; font-weight: 500; margin-bottom: 20px; color: var(--primary-color); display: flex; align-items: center; gap: 8px; } .settings-title svg { width: 20px; height: 20px; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; color: var(--light-text); font-size: 14px; } input[type="text"], input[type="number"], select { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 14px; transition: border-color 0.3s; } input[type="text"]:focus, input[type="number"]:focus, select:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px var(--primary-light); } input[type="file"] { display: none; } input[type="color"] { width: 40px; height: 40px; border: 1px solid var(--border-color); border-radius: 6px; padding: 2px; background: white; } .grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } .range-container { display: flex; align-items: center; gap: 10px; } input[type="range"] { flex-grow: 1; height: 6px; border-radius: 3px; background: var(--border-color); -webkit-appearance: none; } input[type="range"javascript]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--primary-color); cursor: pointer; } .range-value { min-width: 50px; text-align: right; font-size: 14px; color: var(--primary-color); } .watermark-type { display: none; } .watermark-type.active { display: block; } .section { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color); } .section-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; color: var(--primary-color); } button { width: 100%; padding: 12px; background-color: var(--primary-color); color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.3s; margin-top: 20px; display: flex; align-items: center; justify-content: center; gap: 8px; } button:hover { background-color: var(--secondary-color); } button svg { width: 18px; height: 18px; } @media (max-width: 768px) { .container { grid-template-columns: 1fr; } .settings { position: static; } } /* 添加一些图标样式 */ .icon { width: 20px; height: 20px; fill: currentColor; } /* 文件上传区域样式 */ .file-upload-area { border: 2px dashed var(--border-color); border-radius: 8px; padding: 20px; text-align: center; cursor: pointer; transition: all 0.3s; margin-bottom: 15px; background-color: var(--bg-color); } .file-upload-area:hover { border-color: var(--primary-color); background-color: var(--primary-light); } .file-upload-area.drag-over { border-color: var(--primary-color); background-color: var(--primary-light); } .file-upload-icon { font-size: 48px; color: var(--primary-color); margin-bottom: 10px; } .file-upload-text { font-size: 14px; color: var(--light-text); } .file-upload-button { display: inline-block; padding: 8px 16px; background-color: var(--primary-color); color: white; border-radius: 4px; font-size: 14px; margin-top: 10px; transition: background-color 0.3s; } .file-upload-button:hover { background-color: var(--secondary-color); } .file-name { font-size: 14px; margin-top: 10px; color: var(--primary-color); word-break: break-all; } </style> </head> <body> <h1>图片水印工具</h1> <div class="container"> <!-- 预览区域 --> <div class="preview"> <h2 class="preview-title"> <svg class="icon" viewBox="0 0 24 24"> <path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z"/> </svg> 预览效果 </h2> <canvas id="previewCanvas" width="1080" height="2400"></canvas> </div> <!-- 设置面板 --> <div class="settings"> <h2 class="settings-title"> <svg class="icon" viewBox="0 0 24 24"> <path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/> </svg> 水印设置 </h2> <div class="form-group"> <label for="imageInput">上传图片</label> <div class="file-upload-area" id="imageUploadArea"> <div class="file-upload-icon"> <svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"> <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/> </svg> </div> <div class="file-upload-text">拖放图片到此处或点击选择文件</div> <div class="file-upload-button">选择文件</div> <div class="file-name" id="imageFileName"></div> </div> <input type="file" id="imageInput" accept="image/*"> </div> <div class="form-group"> <label for="watermarkType">水印类型</label> <select id="watermarkType"> <option value="text">文字水印</option> <option value="image">图片水印</option> </select> </div> <!-- 文字水印设置 --> <div id="textSettings" class="watermark-type active"> <div class="form-group"> <label for="watermarkText">水印文字</label> <input type="text" id="watermarkText" value="机密文件" placeholder="输入水印文字"> </div> <div class="grid-container"> <div class="form-group"> <label for="fontSize">字体大小</label> <input type="number" id="fontSize" value="48" min="10" max="100" placeholder="字号"> </div> <div class="form-group"> <label for="textColor">文字颜色</label> <div class="range-container"> <input type="color" id="textColor" value="#cccccc"> </div> </div> </div> <div class="section"> <h3 class="section-title">高级设置</h3> <div class="form-group"> <label>旋转角度 <span class="range-value" id="angleValue">45</span></label> <div class="range-container"> <input type="range" id="angle" min="-180" max="180" value="45"> </div> </div> <div class="form-group"> <label>水印密度 <span class="range-value" id="densityValue">50%</span></label> <div class="range-container"> <input type="range" id="density" min="10" max="100" value="50"> </div> </div> <div class="form-group"> <label>水平间距 <span class="range-value" id="spacingXValue">150px</span></label> <div class="range-container"> <input type="range" id="spacingX" min="50" max="300" value="150"> </div> </div> <div class="form-group"> <label>垂直间距 <span class="range-value" id="spacingYValue">100px</span></label> <div class="range-container"> <input type="range" id="spacingY" min="50" max="300" value="100"> </div> </div> </div> </div> <!-- 图片水印设置 --> <div id="imageSettings" class="watermark-type"> <div class="form-group"> <label for="watermarkImage">上传水印图片</label> <div class="file-upload-area" id="watermarkUploadArea"> <div class="file-upload-icon"> <svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"> <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/> </svg> </div> <div class="file-upload-text">拖放水印图片到此处或点击选择文件</div> <div class="file-upload-button">选择图片</div> php <div class="file-name" id="watermarkFileName"></div> </div> <input type="file" id="watermarkImage" accept="image/*"> </div> <div class="form-group"> <label>缩放比例 <span class="range-value" id="scaleValue">30%</span></label> <div class="range-container"> <input type="range" id="scale" min="10" max="100" value="30"> </div> </div> </div> <div class="form-group"> <label>透明度 <span class="range-value" id="opacityValue">50%</span></label> <div class="range-container"> <input type="range" id="opacity" min="0" max="1" step="0.1" value="0.5"> </div> </div> <button id="downloadBtn"> <svg class="icon" viewBox="0 0 24 24"> <path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/> </svg> 下载图片 </button> </div> </div> <script> const canvas = document.getElementById('previewCanvas'); const ctx = canvas.getContext('2d'); let originalImage = null; let watermarkImage = null; // 控件元素 const controls = { imageInput: document.getElementById('imageInput'), watermarkType: document.getElementById('watermarkType'), watermarkText: document.getElementById('watermarkText'), fontSize: document.getElementById('fontSize'), textColor: document.getElementById('textColor'), angle: document.getElementById('angle'), density: document.getElementById('density'), spacingX: document.getElementById('spacingX'), spacingY: document.getElementById('spacingY'), opacity: document.getElementById('opacity'), watermarkImageInput: document.getElementById('watermarkImage'), scale: document.getElementById('scale'), downloadBtn: document.getElementById('downloadBtn'), imageUploadArea: document.getElementById('imageUploadArea'), watermarkUploadArea: document.getElementById('watermarkUploadArea'), imageFileName: document.getElementById('imageFileName'), watermarkFileName: document.getElementById('watermarkFileName') }; // 初始化事件监听 function initEventListeners() { // 通用事件 controls.imageInput.addEventListener('change', handleImageUpload); controls.watermarkType.addEventListener('change', toggleWatermarkType); controls.opacity.addEventListener('input', updateRangeValue); controls.opacity.addEventListener('input', drawWatermark); // 文字水印事件 controls.watermarkText.addEventListener('input', drawWatermark); controls.fontSize.addEventListener('input', drawWatermark); controls.textColor.addEventListener('input', drawWatermark); controls.angle.addEventListener('input', updateRangeValue); controls.angle.addEventListener('input', drawWatermark); controls.density.addEventListener('input', updateRangeValue); controls.density.addEventListener('input', drawWatermark); controls.spacingX.addEventListener('inputjavascript', updateRangeValue); controls.spacingX.addEventListener('input', drawWatermark); controls.spacingY.addEventListener('input', updateRangeValue); controls.spacingY.addEventListener('input', drawWatermark); // 图片水印事件 controls.watermarkImageInput.addEventListener('change', handleWatermarkImageUpload); controls.scale.addEventListener('input', updateRangeValue); controls.scale.addEventListener('input', drawWatermark); // 下载按钮 controls.downloadBtn.addEventListener('click', downloadImage); // 文件拖放功能 setupDragAndDrop(controls.imageUploadArea, controls.imageInput, controls.imageFileName); setupDragAndDrop(controls.watermarkUploadArea, controls.watermarkImageInput, controls.watermarkFileName); // 点击上传区域触发文件选择 controls.imageUploadArea.querySelector('.file-upload-button').addEventListener('click', () => controls.imageInput.click()); controls.watermarkUploadArea.querySelector('.file-upload-button').addEventListener('click', () => controls.watermarkImageInput.click()); } function setupDragAndDrop(dropArea, fileInput, fileNameDisplay) { // 阻止默认拖放行为 ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false); }); // 高亮显示拖放区域 ['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false); }); // 处理拖放文件 dropArea.addEventListener('drop', handleDrop, false); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function highlight() { dropArea.classList.add('drag-over'); } function unhighlight() { dropArea.classList.remove('drag-over'); } function handleDrop(e) { const dt = e.dataTransfer; const files = dt.files; if (files.length) { fileInput.files = files; updateFileNameDisplay(files[0].name, fileNameDisplay); if (fileInput === controls.imageInput) { handleImageUpload({ target: fileInput }); } else { handleWatermarkImageUpload({ target: fileInput }); } } } } function updateFileNameDisplay(name, element) { element.textContent = name; } function updateRangeValue(e) { const target = e.target; switch(target.id) { case 'angle': document.getElementById('angleValue').textContent = `${target.value}`; break; case 'density': document.getElementById('densityValue').textContent = `${target.value}%`; break; case 'spacingX': document.getElementById('spacingXValue').textContent = `${target.value}px`; break; case 'spacingY': document.getElementById('spacingYValue').textContent = `${target.value}px`; break; case 'opacity': document.getElementById('opacityValue').textContent = `${Math.round(target.value * 100)}%`; break; case 'scale': document.getElementById('scaleValue').textContent = `${target.value}%`; break; } } function toggleWatermarkType() { document.querySelectorAll('.watermark-type').forEach(el => { el.classList.remove('active'); }); document.getElementById(controls.watermarkType.value === 'text' ? 'textSettings' : 'imageSettings').classList.add('active'); drawWatermark(); } async function handleImageUpload(e) { const file = e.target.files[0]; if (file) { updateFileNameDisplay(file.name, controls.imageFileName); originalImage = await loadImage(file); canvas.width = originalImage.width; canvas.height = originalImage.height; drawWatermark(); } } async function handleWatermarkImageUpload(e) { const file = e.target.files[0]; if (file) { updateFileNameDisplay(file.name, controls.watermarkFileName); watermarkImage = await loadImage(file); drawWatermark(); } } function loadImage(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => resolve(img); img.src = e.target.result; }; reader.readAsDataURL(file); }); } function drawWatermark() { if (!originalImage) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(originalImage, 0, 0); ctx.globalAlpha = controls.opacity.value; if (controls.watermarkType.value === 'text') { drawTextWatermark(); } else if (watermarkImage) { drawImageWatermark(); } ctx.globalAlpha = 1.0; } function drawTextWatermark() { ctx.save(); ctx.font = `${controls.fontSize.value}px Arial`; ctx.fillStyle = controls.textColor.value; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const rotation = (controls.angle.value * Math.PI) / 180; // 根据密度调整间距 const densityFactor = 1 + (100 - controls.density.value) / 50; const stepX = parseInt(controls.spacingX.value) * densityFactor; const stepY = parseInt(controls.spacingY.value) * densityFactor; // 创建平铺效果 for (let x = -canvas.width; x < canvas.width * 2; x += stepX) { for (let y = -canvas.height; y < canvas.height * 2; y += stepY) { ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.fillText(controls.watermarkText.value, 0, 0); ctx.restore(); } } ctx.restore(); } function drawImageWatermark() { const scale = controls.scale.value / 100; const width = watermarkImage.width * scale; const height = watermarkImage.height * scale; ctx.save(); // 右下角位置 const x = canvas.width - width - 20; const y = canvas.height - height - 20; ctx.drawImage(wjavascriptatermarkImage, x, y, width, height); ctx.restore(); } function downloadImage() { if (!originalImage) { alert('请先上传图片'); return; } const link = document.createElement('a'); link.download = `watermarked-${Date.now()}.png`; link.href = canvas.toDataURL('image/png'); link.click(); } // 初始化 initEventListeners(); updateRangeValue({ target: controls.opacity }); updateRangeValue({ target: controls.scale }); updateRangeValue({ target: controls.density }); // 添加默认背景 ctx.fillStyle = '#f5f5f5'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#999'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = '24px Arial'; ctx.fillText('上传图片后预览效果将显示在这里', canvas.width/2, canvas.height/2); </script> </body> </html>
总结
这篇文章,我们不仅展示了一个现代化、UI 优雅、功能强大的前端图片水印工具的开发案例,更结合了 html5 + CSS3 的最佳实践,为大家提供了一个可以即用、也能深度定制的模板基础。
不论你是:
想保护自己原创作品的内容创作者,
还是正在学习前端开发的工程师,
都能从这款工具中找到价值,并快速上手开发属于自己的版本!
到此这篇关于JS+HTML实现在线图片水印添加工具的文章就介绍到这了,更多相关JS图片水印内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!
这篇关于JS+HTML实现在线图片水印添加工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!