实用 html 小工具 - 详解

图片加边框

<!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>

在这里插入图片描述

posted @ 2025-09-12 19:58  yfceshi  阅读(17)  评论(0)    收藏  举报