实用 html 小工具

发布于:2025-09-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

图片加边框

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>批量图片加灰色渐变相框</title>
  <style>
    :root{
      --bg:#0f1115;--panel:#151821;--muted:#8087a2;--accent:#4f8cff;--text:#e8ecf1;--card:#0d0f14;--border:#242837;
    }
    *{box-sizing:border-box}
    html,body{height:100%}
    body{margin:0;background:linear-gradient(180deg,#0c0f14 0%,#0b0d12 100%);color:var(--text);font:15px/1.4 system-ui,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"}
    header{position:sticky;top:0;z-index:5;background:rgba(13,15,20,.7);backdrop-filter:saturate(140%) blur(8px);border-bottom:1px solid var(--border)}
    .wrap{max-width:1100px;margin:0 auto;padding:18px}
    h1{font-size:20px;margin:0}
    .controls{display:grid;grid-template-columns:repeat(12,1fr);gap:12px;margin-top:12px}
    .card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:14px}
    .ctrl{display:flex;flex-direction:column;gap:8px}
    .ctrl label{font-size:12px;color:var(--muted)}
    input[type="number"],select,input[type="color"],button{width:100%;padding:10px;border-radius:10px;border:1px solid var(--border);background:var(--card);color:var(--text)}
    button{cursor:pointer;border:1px solid #2c3347}
    button.primary{background:linear-gradient(180deg,#3a79ff,#2f67da);border:none}
    button.ghost{background:transparent;border:1px dashed #2c3347}
    .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px;margin:18px 0 80px}
    .item{background:var(--panel);border:1px solid var(--border);border-radius:14px;overflow:hidden}
    .thumb{display:flex;align-items:center;justify-content:center;background:#0b0d12}
    .thumb canvas{max-width:100%;height:auto;display:block}
    .meta{padding:10px;display:flex;gap:8px}
    .meta button{flex:1}
    .drop{display:flex;align-items:center;justify-content:center;border:2px dashed #39508a;border-radius:14px;padding:28px;color:#aab2cc;background:#0c1220}
    .drop.drag{border-color:#6aa2ff;background:#0e1730}
    footer{position:fixed;left:0;right:0;bottom:0;background:rgba(13,15,20,.85);backdrop-filter:blur(8px);border-top:1px solid var(--border)}
    .bar{display:flex;gap:10px;align-items:center;justify-content:space-between}
    .left, .right{display:flex;gap:10px;align-items:center}
    .hint{font-size:12px;color:#93a0c3}
    .hidden{display:none !important}
  </style>
</head>
<body>
  <header>
    <div class="wrap">
      <h1>批量图片加灰色渐变相框</h1>
      <div class="controls">
        <div class="card ctrl" style="grid-column:span 5">
          <label>选择图片(可多选)</label>
          <div class="drop" id="drop">
            将图片拖拽到此处,或
            <label style="margin-left:8px"><input id="file" type="file" accept="image/*" multiple class="hidden"> <button class="ghost" id="pickBtn" type="button">浏览文件</button></label>
          </div>
          <span class="hint">支持 JPG、PNG、WebP、BMP、GIF(取首帧)</span>
        </div>
        <div class="card ctrl" style="grid-column:span 7">
          <label>相框样式</label>
          <div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px">
            <div class="ctrl"><label>相框宽度(px)</label><input id="frameWidth" type="number" min="1" max="300" value="28"></div>
            <div class="ctrl"><label>圆角半径(px)</label><input id="radius" type="number" min="0" max="200" value="14"></div>
            <div class="ctrl"><label>渐变类型</label>
              <select id="gradType">
                <option value="radial">径向渐变</option>
                <option value="linear">线性渐变</option>
              </select>
            </div>
            <div class="ctrl"><label>外侧颜色</label><input id="outerColor" type="color" value="#3a3a3a"/></div>
            <div class="ctrl"><label>内侧颜色</label><input id="innerColor" type="color" value="#bdbdbd"/></div>
            <div class="ctrl"><label>阴影强度</label>
              <select id="shadowLevel">
                <option value="0"></option>
                <option value="1" selected></option>
                <option value="2"></option>
                <option value="3"></option>
              </select>
            </div>
          </div>
          <div style="margin-top:10px;display:flex;gap:10px">
            <button class="primary" id="applyAll" type="button">应用相框</button>
            <button id="clearAll" type="button">清空列表</button>
          </div>
        </div>
      </div>
    </div>
  </header>

  <main class="wrap">
    <div id="list" class="grid"></div>
  </main>

  <footer>
    <div class="wrap bar">
      <div class="left">
        <button id="downloadAll" class="primary" type="button">全部打包下载</button>
        <span class="hint" id="countHint">尚未添加图片</span>
      </div>
      <div class="right">
        <span class="hint">相框建议:外深内浅灰,保持 CS——哦不,是保持整体风格统一 🙂</span>
      </div>
    </div>
  </footer>

  <!-- 可选:打包下载依赖(在线) -->
  <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>

  <script>
    const el = {
      file: document.getElementById('file'),
      pickBtn: document.getElementById('pickBtn'),
      drop: document.getElementById('drop'),
      list: document.getElementById('list'),
      applyAll: document.getElementById('applyAll'),
      clearAll: document.getElementById('clearAll'),
      downloadAll: document.getElementById('downloadAll'),
      countHint: document.getElementById('countHint'),
      frameWidth: document.getElementById('frameWidth'),
      radius: document.getElementById('radius'),
      gradType: document.getElementById('gradType'),
      outerColor: document.getElementById('outerColor'),
      innerColor: document.getElementById('innerColor'),
      shadowLevel: document.getElementById('shadowLevel'),
    };

    const items = []; // { file, name, img, canvas }

    // ---------- UI helpers ----------
    function updateCount(){
      el.countHint.textContent = items.length ? `共 ${items.length} 张图片` : '尚未添加图片';
    }

    function addFiles(files){
      const arr = Array.from(files || []).filter(f => /^image\//.test(f.type));
      if(!arr.length) return;
      arr.forEach(file => addItem(file));
      updateCount();
    }

    function addItem(file){
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.crossOrigin = 'anonymous';
      img.onload = () => {
        renderItem({file, name:file.name.replace(/\.(\w+)$/, ''), img});
        URL.revokeObjectURL(url);
      };
      img.src = url;
    }

    function renderItem(obj){
      items.push(obj);
      const wrap = document.createElement('div');
      wrap.className = 'item';
      wrap.innerHTML = `
        <div class="thumb"><canvas></canvas></div>
        <div class="meta">
          <button class="ghost one">下载</button>
          <button class="ghost rerender">重绘</button>
          <span class="hint" style="margin-left:auto">${escapeHtml(obj.name)}</span>
        </div>`;
      obj.canvas = wrap.querySelector('canvas');
      el.list.prepend(wrap);
      drawWithFrame(obj); // 初次渲染

      wrap.querySelector('.one').addEventListener('click', async()=>{
        const blob = await canvasToBlob(obj.canvas);
        saveAs(blob, `${obj.name}_framed.png`);
      });
      wrap.querySelector('.rerender').addEventListener('click',()=> drawWithFrame(obj));
    }

    // ---------- Core drawing ----------
    function drawWithFrame(obj){
      const fw = clamp(parseInt(el.frameWidth.value||0,10), 0, 300);
      const r  = clamp(parseInt(el.radius.value||0,10), 0, 400);
      const type = el.gradType.value;
      const outer = el.outerColor.value;
      const inner = el.innerColor.value;
      const shadow = parseInt(el.shadowLevel.value,10);

      const img = obj.img;
      const w = img.naturalWidth + fw*2;
      const h = img.naturalHeight + fw*2;
      const c = obj.canvas;
      c.width = w; c.height = h;
      const ctx = c.getContext('2d');
      ctx.clearRect(0,0,w,h);

      // 背景 + 渐变相框(外深内浅)
      const grad = (type === 'radial')
        ? radialGrad(ctx, w, h, fw, outer, inner)
        : linearGrad(ctx, w, h, fw, outer, inner);

      // 画圆角外框
      roundRectPath(ctx, 0.5, 0.5, w-1, h-1, r);
      ctx.fillStyle = grad;
      ctx.fill();

      // 可选阴影(内缘轻微暗角)
      if(shadow>0){
        const alpha = [0, 0.10, 0.17, 0.24][shadow];
        const g2 = ctx.createRadialGradient(w/2,h/2,Math.max(w,h)/3, w/2,h/2, Math.max(w,h)/1.2);
        g2.addColorStop(0, `rgba(0,0,0,0)`);
        g2.addColorStop(1, `rgba(0,0,0,${alpha})`);
        roundRectPath(ctx, 0.5, 0.5, w-1, h-1, r);
        ctx.fillStyle = g2;
        ctx.fill();
      }

      // 镂空内窗
      ctx.save();
      ctx.globalCompositeOperation = 'destination-out';
      roundRectPath(ctx, fw + 0.5, fw + 0.5, img.naturalWidth -1, img.naturalHeight -1, Math.max(0, r - Math.min(r, fw)));
      ctx.fill();
      ctx.restore();

      // 绘制图片(裁切到内窗)
      ctx.save();
      roundRectPath(ctx, fw, fw, img.naturalWidth, img.naturalHeight, Math.max(0, r - Math.min(r, fw)));
      ctx.clip();
      ctx.drawImage(img, fw, fw);
      ctx.restore();
    }

    function roundRectPath(ctx, x,y,w,h,r){
      const rr = Math.min(r, w/2, h/2);
      ctx.beginPath();
      ctx.moveTo(x+rr, y);
      ctx.arcTo(x+w, y, x+w, y+h, rr);
      ctx.arcTo(x+w, y+h, x, y+h, rr);
      ctx.arcTo(x, y+h, x, y, rr);
      ctx.arcTo(x, y, x+w, y, rr);
      ctx.closePath();
    }

    function radialGrad(ctx, w,h, fw, outer, inner){
      const g = ctx.createRadialGradient(w/2,h/2, Math.max(8, Math.min(w,h)/8), w/2,h/2, Math.max(w,h)/2);
      g.addColorStop(0, inner);
      g.addColorStop(1, outer);
      return g;
    }
    function linearGrad(ctx, w,h, fw, outer, inner){
      const g = ctx.createLinearGradient(0,0,w,h);
      g.addColorStop(0, outer);
      g.addColorStop(0.5, inner);
      g.addColorStop(1, outer);
      return g;
    }

    function clamp(n,min,max){return Math.max(min, Math.min(max,n))}
    function escapeHtml(s){return s.replace(/[&<>"']/g, m=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[m]))}
    function canvasToBlob(canvas){
      return new Promise(res=>canvas.toBlob(b=>res(b),'image/png'))
    }

    // ---------- Events ----------
    el.pickBtn.addEventListener('click',()=> el.file.click());
    el.file.addEventListener('change', e=> addFiles(e.target.files));

    ;['dragenter','dragover'].forEach(t=> el.drop.addEventListener(t, e=>{e.preventDefault(); e.dataTransfer.dropEffect='copy'; el.drop.classList.add('drag')}));
    ;['dragleave','drop'].forEach(t=> el.drop.addEventListener(t, e=>{e.preventDefault(); el.drop.classList.remove('drag')}));
    el.drop.addEventListener('drop', e=> addFiles(e.dataTransfer.files));

    el.applyAll.addEventListener('click', ()=> items.forEach(drawWithFrame));
    el.clearAll.addEventListener('click', ()=>{ items.length=0; el.list.innerHTML=''; updateCount(); });

    el.downloadAll.addEventListener('click', async()=>{
      if(!items.length) return;
      if(!(window.JSZip && window.saveAs)){ alert('缺少打包依赖,已自动改为逐张下载。'); for(const it of items){ const b=await canvasToBlob(it.canvas); saveAs(b, `${it.name}_framed.png`);} return; }
      const zip = new JSZip();
      const folder = zip.folder('framed');
      for(const it of items){
        await new Promise(r => setTimeout(r,0));
        const blob = await canvasToBlob(it.canvas);
        folder.file(`${it.name}_framed.png`, blob);
      }
      const content = await zip.generateAsync({type:'blob'});
      saveAs(content, `framed_${new Date().toISOString().slice(0,10)}.zip`);
    });
  </script>
</body>
</html>

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到