JS+HTML实现在线图片水印添加工具

2025-04-17 04:50

本文主要是介绍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;
  • 实现“离线水印工具”。

运行效果

JS+HTML实现在线图片水印添加工具

JS+HTML实现在线图片水印添加工具

项目源码下载

<!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实现在线图片水印添加工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154256

相关文章

C#实现将Excel表格转换为图片(JPG/ PNG)

《C#实现将Excel表格转换为图片(JPG/PNG)》Excel表格可能会因为不同设备或字体缺失等问题,导致格式错乱或数据显示异常,转换为图片后,能确保数据的排版等保持一致,下面我们看看如何使用C... 目录通过C# 转换Excel工作表到图片通过C# 转换指定单元格区域到图片知识扩展C# 将 Excel

Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案

《Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案》:本文主要介绍Vue3组件中getCurrentInstance()获取App实例,但是返回nu... 目录vue3组件中getCurrentInstajavascriptnce()获取App实例,但是返回n

基于Java实现回调监听工具类

《基于Java实现回调监听工具类》这篇文章主要为大家详细介绍了如何基于Java实现一个回调监听工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录监听接口类 Listenable实际用法打印结果首先,会用到 函数式接口 Consumer, 通过这个可以解耦回调方法,下面先写一个

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

Qt中QGroupBox控件的实现

《Qt中QGroupBox控件的实现》QGroupBox是Qt框架中一个非常有用的控件,它主要用于组织和管理一组相关的控件,本文主要介绍了Qt中QGroupBox控件的实现,具有一定的参考价值,感兴趣... 目录引言一、基本属性二、常用方法2.1 构造函数 2.2 设置标题2.3 设置复选框模式2.4 是否

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法

《springboot整合阿里云百炼DeepSeek实现sse流式打印的操作方法》:本文主要介绍springboot整合阿里云百炼DeepSeek实现sse流式打印,本文给大家介绍的非常详细,对大... 目录1.开通阿里云百炼,获取到key2.新建SpringBoot项目3.工具类4.启动类5.测试类6.测

pytorch自动求梯度autograd的实现

《pytorch自动求梯度autograd的实现》autograd是一个自动微分引擎,它可以自动计算张量的梯度,本文主要介绍了pytorch自动求梯度autograd的实现,具有一定的参考价值,感兴趣... autograd是pytorch构建神经网络的核心。在 PyTorch 中,结合以下代码例子,当你

使用Python构建一个Hexo博客发布工具

《使用Python构建一个Hexo博客发布工具》虽然Hexo的命令行工具非常强大,但对于日常的博客撰写和发布过程,我总觉得缺少一个直观的图形界面来简化操作,下面我们就来看看如何使用Python构建一个... 目录引言Hexo博客系统简介设计需求技术选择代码实现主框架界面设计核心功能实现1. 发布文章2. 加

SpringBoot集成Milvus实现数据增删改查功能

《SpringBoot集成Milvus实现数据增删改查功能》milvus支持的语言比较多,支持python,Java,Go,node等开发语言,本文主要介绍如何使用Java语言,采用springboo... 目录1、Milvus基本概念2、添加maven依赖3、配置yml文件4、创建MilvusClient