最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Vue3和Canvas實現(xiàn)一個水印工具

 更新時間:2025年10月21日 08:23:35   作者:劉大華  
在公眾號運營和內(nèi)容創(chuàng)作中,圖片版權(quán)保護是一個非常重要的問題,今天我分享一個基于Vue3圖片水印添加的源碼和實現(xiàn)步驟,即使你是前端新手也能輕松掌握,需要的朋友可以參考下

在公眾號運營和內(nèi)容創(chuàng)作中,圖片版權(quán)保護是一個非常重要的問題。今天我分享一個基于Vue3圖片水印添加的源碼和實現(xiàn)步驟,即使你是前端新手也能輕松掌握!

一、項目效果預(yù)覽

我們最終將實現(xiàn)一個這樣的工具:

  • 支持上傳本地圖片
  • 兩種水印模式:單水?。?個位置可選)和平鋪水印
  • 完全自定義水印樣式:文本、字體、大小、顏色、透明度、旋轉(zhuǎn)角度
  • 實時預(yù)覽效果
  • 一鍵下載帶水印的圖片

效果圖

完整源碼在文末可直接復(fù)制。

二、環(huán)境準備

這個項目只需要一個HTML文件,不需要復(fù)雜的開發(fā)環(huán)境:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3 圖片水印添加器</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <!-- 我們的代碼將寫在這里 -->
</body>
</html>

三、項目結(jié)構(gòu)設(shè)計

讓我們先規(guī)劃一下頁面的整體結(jié)構(gòu):

容器 (Container)
├── 頭部 (Header)
├── 主要內(nèi)容區(qū)域 (App Content)
    ├── 控制面板 (Controls)
    │   ├── 圖片上傳區(qū)域
    │   ├── 水印模式選擇
    │   ├── 水印設(shè)置
    │   ├── 位置控制(單水印模式)
    │   ├── 平鋪設(shè)置(平鋪模式)
    │   └── 操作按鈕
    └── 預(yù)覽區(qū)域 (Preview)
        ├── 圖片預(yù)覽容器
        └── 預(yù)覽信息

四、核心代碼詳解

1. Vue3應(yīng)用初始化

const { createApp, ref, watch, onMounted } = Vue;

createApp({
  setup() {
    // 這里寫我們的邏輯代碼
  }
}).mount('#app');

簡單解釋:

  • createApp:創(chuàng)建Vue應(yīng)用
  • ref:用于創(chuàng)建響應(yīng)式數(shù)據(jù)
  • watch:監(jiān)聽數(shù)據(jù)變化
  • onMounted:頁面加載完成后執(zhí)行

2. 數(shù)據(jù)定義

const imageSrc = ref('');  // 存儲上傳的圖片
const watermarkCanvas = ref(null);  // 畫布元素引用

// 水印配置數(shù)據(jù)
const watermark = ref({
  mode: 'single',        // 模式:single(單水印) 或 tile(平鋪)
  text: '示例水印',       // 水印文字
  fontFamily: 'Microsoft YaHei',  // 字體
  fontSize: 24,          // 字體大小
  color: '#ffffff',      // 顏色
  opacity: 0.7,          // 透明度
  rotation: -15,         // 旋轉(zhuǎn)角度
  position: 'bottom-right',  // 位置(單水印模式)
  tileSpacingX: 100,     // 水平間距(平鋪模式)
  tileSpacingY: 100      // 垂直間距(平鋪模式)
});

響應(yīng)式數(shù)據(jù)說明: 當(dāng)這些數(shù)據(jù)發(fā)生變化時,頁面會自動更新,這就是Vue3的響應(yīng)式特性。

3. 圖片上傳功能

// 觸發(fā)文件選擇
const triggerFileInput = () => {
  document.getElementById('fileInput').click();
};

// 處理圖片上傳
const handleImageUpload = (event) => {
  const file = event.target.files[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = (e) => {
      imageSrc.value = e.target.result;  // 將圖片轉(zhuǎn)為base64格式
    };
    reader.readAsDataURL(file);
  }
};

FileReader小知識: FileReader是瀏覽器提供的API,可以讀取文件內(nèi)容。readAsDataURL方法將文件讀取為Data URL格式,可以直接用在img標簽的src屬性中。

4. 水印繪制核心邏輯

這是整個項目最核心的部分,我們分步驟來看:

準備工作

const drawWatermark = () => {
  if (!imageSrc.value || !watermarkCanvas.value) return;
  
  const canvas = watermarkCanvas.value;
  const ctx = canvas.getContext('2d');
  const img = new Image();
  
  img.onload = () => {
    // 設(shè)置canvas尺寸與圖片一致
    canvas.width = img.width;
    canvas.height = img.height;
    
    // 清除之前的繪制內(nèi)容
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 設(shè)置水印樣式
    ctx.font = `${watermark.value.fontSize}px ${watermark.value.fontFamily}`;
    ctx.fillStyle = watermark.value.color;
    ctx.globalAlpha = watermark.value.opacity;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    
    // 根據(jù)模式繪制水印
    if (watermark.value.mode === 'single') {
      drawSingleWatermark(ctx, canvas);
    } else {
      drawTiledWatermark(ctx, canvas);
    }
  };
  
  img.src = imageSrc.value;
};

單水印模式繪制

const drawSingleWatermark = (ctx, canvas) => {
  let x, y;
  const padding = 20;  // 距離邊緣的間距
  
  // 根據(jù)選擇的位置計算坐標
  switch(watermark.value.position) {
    case 'top-left':
      x = padding;
      y = padding;
      ctx.textAlign = 'left';
      ctx.textBaseline = 'top';
      break;
    case 'top-center':
      x = canvas.width / 2;
      y = padding;
      ctx.textBaseline = 'top';
      break;
    // ... 其他7個位置的類似計算
  }
  
  // 應(yīng)用旋轉(zhuǎn)效果
  ctx.save();                    // 保存當(dāng)前畫布狀態(tài)
  ctx.translate(x, y);           // 移動坐標原點到水印位置
  ctx.rotate(watermark.value.rotation * Math.PI / 180);  // 旋轉(zhuǎn)
  ctx.fillText(watermark.value.text, 0, 0);  // 繪制文字
  ctx.restore();                 // 恢復(fù)畫布狀態(tài)
};

Canvas繪圖要點:

  • save()restore():保存和恢復(fù)畫布狀態(tài),避免旋轉(zhuǎn)影響后續(xù)繪制
  • translate():移動坐標原點
  • rotate():旋轉(zhuǎn)畫布,參數(shù)是弧度(角度×π/180)

平鋪水印模式繪制

const drawTiledWatermark = (ctx, canvas) => {
  // 計算文字占據(jù)的寬度和高度
  const textWidth = ctx.measureText(watermark.value.text).width;
  const textHeight = watermark.value.fontSize;
  
  // 計算每個水印單元的大?。ㄎ淖?間距)
  const unitWidth = textWidth + watermark.value.tileSpacingX;
  const unitHeight = textHeight + watermark.value.tileSpacingY;
  
  // 計算需要繪制多少行和列(+1確保覆蓋整個圖片)
  const cols = Math.ceil(canvas.width / unitWidth) + 1;
  const rows = Math.ceil(canvas.height / unitHeight) + 1;
  
  // 循環(huán)繪制每個水印
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      const x = j * unitWidth;
      const y = i * unitHeight;
      
      // 交錯排列,讓平鋪效果更自然
      const offsetX = (i % 2) * (unitWidth / 2);
      
      ctx.save();
      ctx.translate(x + offsetX, y);
      ctx.rotate(watermark.value.rotation * Math.PI / 180);
      ctx.fillText(watermark.value.text, 0, 0);
      ctx.restore();
    }
  }
};

5. 圖片下載功能

const downloadImage = () => {
  if (!imageSrc.value) return;
  
  // 創(chuàng)建臨時canvas
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();
  
  img.onload = () => {
    // 設(shè)置canvas尺寸
    canvas.width = img.width;
    canvas.height = img.height;
    
    // 先繪制原始圖片
    ctx.drawImage(img, 0, 0);
    
    // 再繪制水?。ㄟ壿嬇c預(yù)覽時相同)
    // ... 水印繪制代碼
    
    // 創(chuàng)建下載鏈接
    const link = document.createElement('a');
    link.download = 'watermarked-image.png';
    link.href = canvas.toDataURL('image/png');
    link.click();  // 觸發(fā)下載
  };
  
  img.src = imageSrc.value;
};

下載原理:

  1. 創(chuàng)建隱藏的canvas
  2. 先畫原始圖片,再畫水印
  3. 將canvas轉(zhuǎn)為Data URL
  4. 創(chuàng)建a標簽觸發(fā)下載

6. 響應(yīng)式更新

// 監(jiān)聽水印設(shè)置變化,實時更新預(yù)覽
watch(watermark, () => {
  drawWatermark();
}, { deep: true });

// 監(jiān)聽圖片變化
watch(imageSrc, () => {
  if (imageSrc.value) {
    setTimeout(drawWatermark, 100);
  }
});

watch函數(shù)說明:

  • 第一個參數(shù):要監(jiān)聽的數(shù)據(jù)
  • 第二個參數(shù):數(shù)據(jù)變化時執(zhí)行的回調(diào)函數(shù)
  • deep: true:深度監(jiān)聽,對象內(nèi)部屬性變化也會觸發(fā)

五、界面設(shè)計與CSS技巧

1. 整體布局

使用Flexbox實現(xiàn)響應(yīng)式布局:

.app-content {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;  /* 元素間距 */
}

.controls {
  flex: 1;
  min-width: 320px;  /* 最小寬度,確保在小屏幕上也能正常顯示 */
}

.preview {
  flex: 2;
  min-width: 500px;
}

2. 網(wǎng)格布局實現(xiàn)9宮格位置選擇

.position-controls {
  display: grid;
  grid-template-columns: repeat(3, 1fr);  /* 3列等寬 */
  gap: 5px;  /* 格子間距 */
}

3. 視覺反饋

使用CSS過渡效果增強用戶體驗:

button {
  transition: all 0.3s;  /* 所有屬性變化都有0.3秒過渡 */
}

button:hover {
  transform: translateY(-2px);  /* 懸停時輕微上移 */
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);  /* 添加陰影 */
}

六、使用指南

1. 基本使用步驟

  1. 上傳圖片:點擊"選擇圖片"按鈕選擇本地圖片
  2. 選擇模式:根據(jù)需求選擇"單水印"或"平鋪水印"
  3. 設(shè)置水印
    • 單水印模式:調(diào)整位置、文字、樣式
    • 平鋪模式:調(diào)整間距、文字、樣式
  4. 實時預(yù)覽:右側(cè)查看效果
  5. 下載圖片:滿意后點擊下載

2. 水印設(shè)置建議

  • 文字顏色:建議使用白色或淺灰色,通過透明度調(diào)節(jié)效果
  • 字體大小:根據(jù)圖片尺寸調(diào)整,一般20-40px比較合適
  • 旋轉(zhuǎn)角度:-15°到-45°的斜向水印更難被去除
  • 透明度:0.5-0.8之間既能看清又不會太突兀

七、技術(shù)亮點總結(jié)

  1. 純前端實現(xiàn):所有操作在瀏覽器完成,圖片不上傳服務(wù)器
  2. 實時預(yù)覽:所有修改立即顯示效果
  3. Canvas繪圖:使用HTML5 Canvas實現(xiàn)精準的水印繪制
  4. Vue3響應(yīng)式:數(shù)據(jù)驅(qū)動視圖,代碼簡潔易維護

八、擴展思路

這個基礎(chǔ)版本還可以繼續(xù)增強:

  1. 圖片水印:支持上傳Logo圖片作為水印
  2. 批量處理:一次為多張圖片添加水印
  3. 模板保存:保存常用的水印設(shè)置
  4. 更多樣式:文字陰影、描邊等特效
  5. 壓縮優(yōu)化:下載時自動壓縮圖片大小

完整代碼

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3 圖片水印添加器</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }
    
    body {
      background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
      min-height: 100vh;
      padding: 20px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      max-width: 1400px;
      width: 100%;
      background: white;
      border-radius: 15px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    
    header {
      background: linear-gradient(90deg, #4b6cb7 0%, #182848 100%);
      color: white;
      padding: 20px;
      text-align: center;
    }
    
    h1 {
      font-size: 28px;
      margin-bottom: 10px;
    }
    
    .subtitle {
      font-size: 16px;
      opacity: 0.8;
    }
    
    .app-content {
      display: flex;
      flex-wrap: wrap;
      padding: 20px;
      gap: 20px;
    }
    
    .controls {
      flex: 1;
      min-width: 320px;
      padding: 20px;
      background: #f8f9fa;
      border-radius: 10px;
    }
    
    .preview {
      flex: 2;
      min-width: 500px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px;
    }
    
    .control-group {
      margin-bottom: 20px;
    }
    
    h2 {
      font-size: 18px;
      margin-bottom: 15px;
      color: #2c3e50;
      border-bottom: 1px solid #eaeaea;
      padding-bottom: 8px;
    }
    
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 500;
      color: #34495e;
    }
    
    input, select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
      font-size: 14px;
    }
    
    input[type="color"] {
      height: 40px;
      padding: 3px;
    }
    
    input[type="range"] {
      padding: 0;
    }
    
    .range-value {
      display: inline-block;
      width: 40px;
      text-align: center;
      margin-left: 10px;
    }
    
    .button-group {
      display: flex;
      gap: 10px;
      margin-top: 20px;
    }
    
    button {
      flex: 1;
      padding: 12px;
      border: none;
      border-radius: 5px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
    }
    
    .upload-btn {
      background: #3498db;
      color: white;
    }
    
    .download-btn {
      background: #2ecc71;
      color: white;
    }
    
    .reset-btn {
      background: #e74c3c;
      color: white;
    }
    
    button:hover {
      transform: translateY(-2px);
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
    }
    
    button:disabled {
      background: #bdc3c7;
      cursor: not-allowed;
      transform: none;
      box-shadow: none;
    }
    
    .preview-container {
      width: 100%;
      max-width: 700px;
      border: 1px solid #eaeaea;
      border-radius: 10px;
      overflow: hidden;
      position: relative;
      background: #f8f9fa;
      min-height: 400px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    #previewImage {
      max-width: 100%;
      max-height: 500px;
      display: block;
    }
    
    .watermark-canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
    }
    
    .upload-placeholder {
      text-align: center;
      color: #7f8c8d;
      padding: 40px;
    }
    
    .upload-placeholder i {
      font-size: 48px;
      margin-bottom: 15px;
      display: block;
      color: #bdc3c7;
    }
    
    .position-controls {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 5px;
      margin-top: 10px;
    }
    
    .position-btn {
      padding: 10px;
      background: #ecf0f1;
      border: 1px solid #bdc3c7;
      border-radius: 5px;
      text-align: center;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    .position-btn.active {
      background: #3498db;
      color: white;
      border-color: #2980b9;
    }
    
    .position-btn:hover {
      background: #d5dbdb;
    }
    
    .position-btn.active:hover {
      background: #2980b9;
    }
    
    .mode-controls {
      display: flex;
      gap: 10px;
      margin-top: 10px;
    }
    
    .mode-btn {
      flex: 1;
      padding: 10px;
      background: #ecf0f1;
      border: 1px solid #bdc3c7;
      border-radius: 5px;
      text-align: center;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    .mode-btn.active {
      background: #9b59b6;
      color: white;
      border-color: #8e44ad;
    }
    
    .mode-btn:hover {
      background: #d5dbdb;
    }
    
    .mode-btn.active:hover {
      background: #8e44ad;
    }
    
    .tile-controls {
      margin-top: 15px;
      padding: 15px;
      background: #e8f4fc;
      border-radius: 8px;
      border-left: 4px solid #3498db;
    }
    
    @media (max-width: 768px) {
      .app-content {
        flex-direction: column;
      }
      
      .controls, .preview {
        min-width: 100%;
      }
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="container">
      <header>
        <h1>Vue3 圖片水印添加器</h1>
        <p class="subtitle">上傳圖片,添加自定義水印,支持單水印和平鋪模式</p>
      </header>
      
      <div class="app-content">
        <div class="controls">
          <div class="control-group">
            <h2>圖片上傳</h2>
            <input type="file" id="fileInput" @change="handleImageUpload" accept="image/*" style="display: none;">
            <button class="upload-btn" @click="triggerFileInput">選擇圖片</button>
          </div>
          
          <div class="control-group">
            <h2>水印模式</h2>
            <div class="mode-controls">
              <div class="mode-btn" 
                   :class="{active: watermark.mode === 'single'}" 
                   @click="watermark.mode = 'single'">單水印</div>
              <div class="mode-btn" 
                   :class="{active: watermark.mode === 'tile'}" 
                   @click="watermark.mode = 'tile'">平鋪水印</div>
            </div>
          </div>
          
          <div class="control-group">
            <h2>水印設(shè)置</h2>
            <label for="watermarkText">水印文本</label>
            <input type="text" id="watermarkText" v-model="watermark.text" placeholder="輸入水印文本">
            
            <label for="fontFamily">字體</label>
            <select id="fontFamily" v-model="watermark.fontFamily">
              <option value="Arial">Arial</option>
              <option value="Verdana">Verdana</option>
              <option value="Helvetica">Helvetica</option>
              <option value="Tahoma">Tahoma</option>
              <option value="Times New Roman">Times New Roman</option>
              <option value="Courier New">Courier New</option>
              <option value="Georgia">Georgia</option>
              <option value="Microsoft YaHei">微軟雅黑</option>
              <option value="SimHei">黑體</option>
              <option value="SimSun">宋體</option>
            </select>
            
            <label for="fontSize">字體大小: <span class="range-value">{{ watermark.fontSize }}px</span></label>
            <input type="range" id="fontSize" v-model.number="watermark.fontSize" min="10" max="60" step="1">
            
            <label for="color">顏色</label>
            <input type="color" id="color" v-model="watermark.color">
            
            <label for="opacity">透明度: <span class="range-value">{{ watermark.opacity }}</span></label>
            <input type="range" id="opacity" v-model.number="watermark.opacity" min="0.1" max="1" step="0.1">
            
            <label for="rotation">旋轉(zhuǎn)角度: <span class="range-value">{{ watermark.rotation }}°</span></label>
            <input type="range" id="rotation" v-model.number="watermark.rotation" min="0" max="360" step="1">
          </div>
          
          <div class="control-group" v-if="watermark.mode === 'single'">
            <h2>水印位置</h2>
            <div class="position-controls">
              <div class="position-btn" 
                   :class="{active: watermark.position === 'top-left'}" 
                   @click="watermark.position = 'top-left'">左上</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'top-center'}" 
                   @click="watermark.position = 'top-center'">中上</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'top-right'}" 
                   @click="watermark.position = 'top-right'">右上</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'center-left'}" 
                   @click="watermark.position = 'center-left'">左中</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'center'}" 
                   @click="watermark.position = 'center'">中心</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'center-right'}" 
                   @click="watermark.position = 'center-right'">右中</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'bottom-left'}" 
                   @click="watermark.position = 'bottom-left'">左下</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'bottom-center'}" 
                   @click="watermark.position = 'bottom-center'">中下</div>
              <div class="position-btn" 
                   :class="{active: watermark.position === 'bottom-right'}" 
                   @click="watermark.position = 'bottom-right'">右下</div>
            </div>
          </div>
          
          <div class="control-group" v-if="watermark.mode === 'tile'">
            <h2>平鋪設(shè)置</h2>
            <div class="tile-controls">
              <label for="tileSpacingX">水平間距: <span class="range-value">{{ watermark.tileSpacingX }}px</span></label>
              <input type="range" id="tileSpacingX" v-model.number="watermark.tileSpacingX" min="20" max="200" step="5">
              
              <label for="tileSpacingY">垂直間距: <span class="range-value">{{ watermark.tileSpacingY }}px</span></label>
              <input type="range" id="tileSpacingY" v-model.number="watermark.tileSpacingY" min="20" max="200" step="5">
            </div>
          </div>
          
          <div class="button-group">
            <button class="download-btn" @click="downloadImage" :disabled="!imageSrc">下載圖片</button>
            <button class="reset-btn" @click="resetWatermark">重置設(shè)置</button>
          </div>
        </div>
        
        <div class="preview">
          <h2>預(yù)覽效果</h2>
          <div class="preview-container">
            <div v-if="!imageSrc" class="upload-placeholder">
              <i>??</i>
              <p>請上傳圖片以添加水印</p>
            </div>
            <img v-else :src="imageSrc" id="previewImage" alt="預(yù)覽圖片">
            <canvas class="watermark-canvas" ref="watermarkCanvas"></canvas>
          </div>
          <div v-if="imageSrc" class="preview-info">
            <p>當(dāng)前模式: <strong>{{ watermark.mode === 'single' ? '單水印' : '平鋪水印' }}</strong></p>
            <p v-if="watermark.mode === 'single'">位置: <strong>{{ getPositionText(watermark.position) }}</strong></p>
            <p v-else>平鋪間距: <strong>{{ watermark.tileSpacingX }}px × {{ watermark.tileSpacingY }}px</strong></p>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    const { createApp, ref, watch, onMounted } = Vue;
    
    createApp({
      setup() {
        const imageSrc = ref('');
        const watermarkCanvas = ref(null);
        
        const watermark = ref({
          mode: 'single', // 'single' 或 'tile'
          text: '示例水印',
          fontFamily: 'Microsoft YaHei',
          fontSize: 24,
          color: '#ffffff',
          opacity: 0.7,
          rotation: -15,
          position: 'bottom-right',
          tileSpacingX: 100,
          tileSpacingY: 100
        });
        
        const getPositionText = (position) => {
          const positionMap = {
            'top-left': '左上',
            'top-center': '中上',
            'top-right': '右上',
            'center-left': '左中',
            'center': '中心',
            'center-right': '右中',
            'bottom-left': '左下',
            'bottom-center': '中下',
            'bottom-right': '右下'
          };
          return positionMap[position] || position;
        };
        
        const triggerFileInput = () => {
          document.getElementById('fileInput').click();
        };
        
        const handleImageUpload = (event) => {
          const file = event.target.files[0];
          if (file) {
            const reader = new FileReader();
            reader.onload = (e) => {
              imageSrc.value = e.target.result;
            };
            reader.readAsDataURL(file);
          }
        };
        
        const drawWatermark = () => {
          if (!imageSrc.value || !watermarkCanvas.value) return;
          
          const canvas = watermarkCanvas.value;
          const ctx = canvas.getContext('2d');
          const img = new Image();
          
          img.onload = () => {
            // 設(shè)置canvas尺寸與圖片一致
            canvas.width = img.width;
            canvas.height = img.height;
            
            // 清除畫布
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // 設(shè)置水印樣式
            ctx.font = `${watermark.value.fontSize}px ${watermark.value.fontFamily}`;
            ctx.fillStyle = watermark.value.color;
            ctx.globalAlpha = watermark.value.opacity;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            
            if (watermark.value.mode === 'single') {
              // 單水印模式
              let x, y;
              const padding = 20;
              
              switch(watermark.value.position) {
                case 'top-left':
                  x = padding;
                  y = padding;
                  ctx.textAlign = 'left';
                  ctx.textBaseline = 'top';
                  break;
                case 'top-center':
                  x = canvas.width / 2;
                  y = padding;
                  ctx.textBaseline = 'top';
                  break;
                case 'top-right':
                  x = canvas.width - padding;
                  y = padding;
                  ctx.textAlign = 'right';
                  ctx.textBaseline = 'top';
                  break;
                case 'center-left':
                  x = padding;
                  y = canvas.height / 2;
                  ctx.textAlign = 'left';
                  break;
                case 'center':
                  x = canvas.width / 2;
                  y = canvas.height / 2;
                  break;
                case 'center-right':
                  x = canvas.width - padding;
                  y = canvas.height / 2;
                  ctx.textAlign = 'right';
                  break;
                case 'bottom-left':
                  x = padding;
                  y = canvas.height - padding;
                  ctx.textAlign = 'left';
                  ctx.textBaseline = 'bottom';
                  break;
                case 'bottom-center':
                  x = canvas.width / 2;
                  y = canvas.height - padding;
                  ctx.textBaseline = 'bottom';
                  break;
                case 'bottom-right':
                  x = canvas.width - padding;
                  y = canvas.height - padding;
                  ctx.textAlign = 'right';
                  ctx.textBaseline = 'bottom';
                  break;
              }
              
              // 應(yīng)用旋轉(zhuǎn)
              ctx.save();
              ctx.translate(x, y);
              ctx.rotate(watermark.value.rotation * Math.PI / 180);
              ctx.fillText(watermark.value.text, 0, 0);
              ctx.restore();
            } else {
              // 平鋪水印模式
              const textWidth = ctx.measureText(watermark.value.text).width;
              const textHeight = watermark.value.fontSize;
              
              // 計算每個水印單元的大小(包括間距)
              const unitWidth = textWidth + watermark.value.tileSpacingX;
              const unitHeight = textHeight + watermark.value.tileSpacingY;
              
              // 計算需要繪制的水印數(shù)量
              const cols = Math.ceil(canvas.width / unitWidth) + 1;
              const rows = Math.ceil(canvas.height / unitHeight) + 1;
              
              // 平鋪繪制水印
              for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                  const x = j * unitWidth;
                  const y = i * unitHeight;
                  
                  // 交錯排列,使平鋪更自然
                  const offsetX = (i % 2) * (unitWidth / 2);
                  
                  ctx.save();
                  ctx.translate(x + offsetX, y);
                  ctx.rotate(watermark.value.rotation * Math.PI / 180);
                  ctx.fillText(watermark.value.text, 0, 0);
                  ctx.restore();
                }
              }
            }
          };
          
          img.src = imageSrc.value;
        };
        
        const downloadImage = () => {
          if (!imageSrc.value) return;
          
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          const img = new Image();
          
          img.onload = () => {
            canvas.width = img.width;
            canvas.height = img.height;
            
            // 繪制原始圖片
            ctx.drawImage(img, 0, 0);
            
            // 設(shè)置水印樣式
            ctx.font = `${watermark.value.fontSize}px ${watermark.value.fontFamily}`;
            ctx.fillStyle = watermark.value.color;
            ctx.globalAlpha = watermark.value.opacity;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            
            if (watermark.value.mode === 'single') {
              // 單水印模式
              let x, y;
              const padding = 20;
              
              switch(watermark.value.position) {
                case 'top-left':
                  x = padding;
                  y = padding;
                  ctx.textAlign = 'left';
                  ctx.textBaseline = 'top';
                  break;
                case 'top-center':
                  x = canvas.width / 2;
                  y = padding;
                  ctx.textBaseline = 'top';
                  break;
                case 'top-right':
                  x = canvas.width - padding;
                  y = padding;
                  ctx.textAlign = 'right';
                  ctx.textBaseline = 'top';
                  break;
                case 'center-left':
                  x = padding;
                  y = canvas.height / 2;
                  ctx.textAlign = 'left';
                  break;
                case 'center':
                  x = canvas.width / 2;
                  y = canvas.height / 2;
                  break;
                case 'center-right':
                  x = canvas.width - padding;
                  y = canvas.height / 2;
                  ctx.textAlign = 'right';
                  break;
                case 'bottom-left':
                  x = padding;
                  y = canvas.height - padding;
                  ctx.textAlign = 'left';
                  ctx.textBaseline = 'bottom';
                  break;
                case 'bottom-center':
                  x = canvas.width / 2;
                  y = canvas.height - padding;
                  ctx.textBaseline = 'bottom';
                  break;
                case 'bottom-right':
                  x = canvas.width - padding;
                  y = canvas.height - padding;
                  ctx.textAlign = 'right';
                  ctx.textBaseline = 'bottom';
                  break;
              }
              
              // 應(yīng)用旋轉(zhuǎn)
              ctx.save();
              ctx.translate(x, y);
              ctx.rotate(watermark.value.rotation * Math.PI / 180);
              ctx.fillText(watermark.value.text, 0, 0);
              ctx.restore();
            } else {
              // 平鋪水印模式
              const textWidth = ctx.measureText(watermark.value.text).width;
              const textHeight = watermark.value.fontSize;
              
              // 計算每個水印單元的大小(包括間距)
              const unitWidth = textWidth + watermark.value.tileSpacingX;
              const unitHeight = textHeight + watermark.value.tileSpacingY;
              
              // 計算需要繪制的水印數(shù)量
              const cols = Math.ceil(canvas.width / unitWidth) + 1;
              const rows = Math.ceil(canvas.height / unitHeight) + 1;
              
              // 平鋪繪制水印
              for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                  const x = j * unitWidth;
                  const y = i * unitHeight;
                  
                  // 交錯排列,使平鋪更自然
                  const offsetX = (i % 2) * (unitWidth / 2);
                  
                  ctx.save();
                  ctx.translate(x + offsetX, y);
                  ctx.rotate(watermark.value.rotation * Math.PI / 180);
                  ctx.fillText(watermark.value.text, 0, 0);
                  ctx.restore();
                }
              }
            }
            
            // 創(chuàng)建下載鏈接
            const link = document.createElement('a');
            link.download = 'watermarked-image.png';
            link.href = canvas.toDataURL('image/png');
            link.click();
          };
          
          img.src = imageSrc.value;
        };
        
        const resetWatermark = () => {
          watermark.value = {
            mode: 'single',
            text: '示例水印',
            fontFamily: 'Microsoft YaHei',
            fontSize: 24,
            color: '#ffffff',
            opacity: 0.7,
            rotation: -15,
            position: 'bottom-right',
            tileSpacingX: 100,
            tileSpacingY: 100
          };
        };
        
        // 監(jiān)聽水印設(shè)置變化,實時更新預(yù)覽
        watch(watermark, () => {
          drawWatermark();
        }, { deep: true });
        
        // 監(jiān)聽圖片變化,更新水印
        watch(imageSrc, () => {
          if (imageSrc.value) {
            // 等待DOM更新后繪制水印
            setTimeout(drawWatermark, 100);
          }
        });
        
        onMounted(() => {
          // 初始化水印
          drawWatermark();
        });
        
        return {
          imageSrc,
          watermarkCanvas,
          watermark,
          getPositionText,
          triggerFileInput,
          handleImageUpload,
          downloadImage,
          resetWatermark
        };
      }
    }).mount('#app');
  </script>
</body>
</html>

完整代碼可以直接復(fù)制到HTML文件中運行,無需任何額外配置。

希望這篇教程能幫助你理解Vue3和Canvas的配合使用,并為你的圖片版權(quán)保護提供有力工具。

以上就是基于Vue3和Canvas實現(xiàn)一個水印工具的詳細內(nèi)容,更多關(guān)于Vue3 Canvas水印工具的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論

镇原县| 铅山县| 长寿区| 黄冈市| 高要市| 赫章县| 西华县| 和政县| 左权县| 敦化市| 四会市| 太康县| 乐至县| 临洮县| 澄江县| 高清| 海伦市| 滨州市| 灌云县| 五台县| 姜堰市| 敖汉旗| 徐水县| 景谷| 板桥市| 陕西省| 广汉市| 繁昌县| 神池县| 大冶市| 五大连池市| 宜兴市| 旬阳县| 紫金县| 斗六市| 蓝山县| 叶城县| 海阳市| 酉阳| 信阳市| 孟村|