概述
在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题。给图片添加水印,正是目前主流平台和创作者最常用的解决方案之一。
但问题也随之而来:
- 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">
<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"]::-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>
<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('input', 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(watermarkimage, 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图片水印内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论