图片加边框
<!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:}*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:linear-gradient(180deg,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 button.primary{background:linear-gradient(180deg,button.ghost{background:transparent;border:1px dashed .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:.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 .drop.drag{border-color: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:.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=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[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>
