Spaces:
Running
Running
Download rembg.html from izuemon/player: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/izuemon/player/resolve/main/rembg.html
- Command line
-
hf download hf://spaces/izuemon/player/rembg.html
-
curl -L -o rembg.html https://huggingface.co/spaces/izuemon/player/resolve/main/rembg.html
13.8 kB
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>画像分割 GIF 生成</title> | |
| <script src="https://cdn.jsdelivr.net/npm/gif.js.optimized/dist/gif.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script> | |
| <style> | |
| body { | |
| font-family: sans-serif; | |
| background: #111; | |
| color: #eee; | |
| margin: 0; | |
| padding: 24px; | |
| } | |
| h1 { | |
| font-size: 24px; | |
| margin-bottom: 20px; | |
| } | |
| .panel { | |
| background: #1e1e1e; | |
| padding: 16px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 14px; | |
| } | |
| input[type="number"] { | |
| width: 120px; | |
| padding: 6px; | |
| font-size: 16px; | |
| } | |
| input[type="range"] { | |
| width: 260px; | |
| vertical-align: middle; | |
| } | |
| button { | |
| padding: 10px 18px; | |
| font-size: 16px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| background: #4f7cff; | |
| color: white; | |
| margin-right: 8px; | |
| margin-top: 8px; | |
| } | |
| button.secondary { | |
| background: #3a9b5d; | |
| } | |
| button.dark { | |
| background: #555; | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| canvas { | |
| image-rendering: pixelated; | |
| border: 1px solid #444; | |
| background: transparent; | |
| margin-top: 12px; | |
| max-width: 100%; | |
| } | |
| .frames { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| margin-top: 16px; | |
| } | |
| .frame-box { | |
| background: #222; | |
| padding: 12px; | |
| border-radius: 10px; | |
| width: fit-content; | |
| } | |
| .frame-box p { | |
| margin: 0 0 10px; | |
| font-size: 14px; | |
| } | |
| .slider-row { | |
| margin-bottom: 14px; | |
| } | |
| .slider-row span { | |
| display: inline-block; | |
| width: 90px; | |
| font-size: 14px; | |
| } | |
| .slider-value { | |
| display: inline-block; | |
| width: 50px; | |
| text-align: right; | |
| margin-left: 8px; | |
| font-size: 14px; | |
| } | |
| .actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 16px; | |
| } | |
| #gifPreview img { | |
| max-width: 100%; | |
| border: 1px solid #444; | |
| margin-top: 16px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>暗部透明化 GIF / ZIP 出力</h1> | |
| <div class="panel"> | |
| <label> | |
| 画像をアップロード | |
| <input type="file" id="imageInput" accept="image/*" /> | |
| </label> | |
| <label> | |
| 横並びフレーム数 | |
| <input type="number" id="sliceCount" min="1" value="4" /> | |
| </label> | |
| <button id="buildBtn" disabled> | |
| フレームを書き出し | |
| </button> | |
| </div> | |
| <div class="panel"> | |
| <h2>一括調整</h2> | |
| <div class="slider-row"> | |
| <span>彩度</span> | |
| <input | |
| type="range" | |
| id="saturationSlider" | |
| min="0" | |
| max="3" | |
| step="0.01" | |
| value="1" | |
| /> | |
| <span class="slider-value" id="saturationValue"> | |
| 1.00 | |
| </span> | |
| </div> | |
| <div class="slider-row"> | |
| <span>明度</span> | |
| <input | |
| type="range" | |
| id="brightnessSlider" | |
| min="0" | |
| max="3" | |
| step="0.01" | |
| value="1" | |
| /> | |
| <span class="slider-value" id="brightnessValue"> | |
| 1.00 | |
| </span> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>フレーム</h2> | |
| <div class="frames" id="frames"></div> | |
| </div> | |
| <div class="panel"> | |
| <h2>出力</h2> | |
| <label> | |
| GIF フレーム間隔(ms) | |
| <br /><br /> | |
| <input | |
| type="number" | |
| id="delay" | |
| min="10" | |
| value="100" | |
| /> | |
| </label> | |
| <div class="actions"> | |
| <button id="gifDownloadBtn" disabled> | |
| Gifで出力(通常ダウンロード) | |
| </button> | |
| <button id="zipDownloadBtn" class="secondary" disabled> | |
| ZIPで出力(通常ダウンロード) | |
| </button> | |
| <button id="gifSaveBtn" class="dark" disabled> | |
| Gifで出力(名前を付けて保存) | |
| </button> | |
| <button id="zipSaveBtn" class="dark" disabled> | |
| ZIPで出力(名前を付けて保存) | |
| </button> | |
| </div> | |
| <div id="gifPreview"></div> | |
| </div> | |
| <script> | |
| const imageInput = document.getElementById('imageInput'); | |
| const sliceCountInput = document.getElementById('sliceCount'); | |
| const delayInput = document.getElementById('delay'); | |
| const buildBtn = document.getElementById('buildBtn'); | |
| const gifDownloadBtn = document.getElementById('gifDownloadBtn'); | |
| const zipDownloadBtn = document.getElementById('zipDownloadBtn'); | |
| const gifSaveBtn = document.getElementById('gifSaveBtn'); | |
| const zipSaveBtn = document.getElementById('zipSaveBtn'); | |
| const framesContainer = document.getElementById('frames'); | |
| const gifPreview = document.getElementById('gifPreview'); | |
| const saturationSlider = document.getElementById('saturationSlider'); | |
| const brightnessSlider = document.getElementById('brightnessSlider'); | |
| const saturationValue = document.getElementById('saturationValue'); | |
| const brightnessValue = document.getElementById('brightnessValue'); | |
| let loadedImage = null; | |
| let frameData = []; | |
| imageInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const img = new Image(); | |
| img.onload = () => { | |
| loadedImage = img; | |
| buildBtn.disabled = false; | |
| }; | |
| img.src = URL.createObjectURL(file); | |
| }); | |
| function rgbToHsl(r, g, b) { | |
| r /= 255; | |
| g /= 255; | |
| b /= 255; | |
| const max = Math.max(r, g, b); | |
| const min = Math.min(r, g, b); | |
| let h, s; | |
| const l = (max + min) / 2; | |
| if (max === min) { | |
| h = s = 0; | |
| } else { | |
| const d = max - min; | |
| s = l > 0.5 | |
| ? d / (2 - max - min) | |
| : d / (max + min); | |
| switch (max) { | |
| case r: | |
| h = (g - b) / d + (g < b ? 6 : 0); | |
| break; | |
| case g: | |
| h = (b - r) / d + 2; | |
| break; | |
| case b: | |
| h = (r - g) / d + 4; | |
| break; | |
| } | |
| h /= 6; | |
| } | |
| return [h, s, l]; | |
| } | |
| function hslToRgb(h, s, l) { | |
| let r, g, b; | |
| if (s === 0) { | |
| r = g = b = l; | |
| } else { | |
| const hue2rgb = (p, q, t) => { | |
| if (t < 0) t += 1; | |
| if (t > 1) t -= 1; | |
| if (t < 1/6) return p + (q - p) * 6 * t; | |
| if (t < 1/2) return q; | |
| if (t < 2/3) { | |
| return p + (q - p) * (2/3 - t) * 6; | |
| } | |
| return p; | |
| }; | |
| const q = l < 0.5 | |
| ? l * (1 + s) | |
| : l + s - l * s; | |
| const p = 2 * l - q; | |
| r = hue2rgb(p, q, h + 1/3); | |
| g = hue2rgb(p, q, h); | |
| b = hue2rgb(p, q, h - 1/3); | |
| } | |
| return [ | |
| Math.round(r * 255), | |
| Math.round(g * 255), | |
| Math.round(b * 255) | |
| ]; | |
| } | |
| function processCanvas(canvas) { | |
| const saturationBoost = Number(saturationSlider.value); | |
| const brightnessBoost = Number(brightnessSlider.value); | |
| const ctx = canvas.getContext('2d'); | |
| const imageData = ctx.getImageData( | |
| 0, | |
| 0, | |
| canvas.width, | |
| canvas.height | |
| ); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| let r = data[i]; | |
| let g = data[i + 1]; | |
| let b = data[i + 2]; | |
| // 元の明るさ | |
| const brightness = (r + g + b) / 3; | |
| // 暗いほど強く補正 | |
| // brightness 0 -> influence 1 | |
| // brightness 255 -> influence 0 | |
| const influence = 1 - (brightness / 255); | |
| let [h, s, l] = rgbToHsl(r, g, b); | |
| // 補正倍率を暗部依存にする | |
| const satAmount = | |
| 1 + ((saturationBoost - 1) * influence); | |
| const briAmount = | |
| 1 + ((brightnessBoost - 1) * influence); | |
| s *= satAmount; | |
| l *= briAmount; | |
| s = Math.max(0, Math.min(1, s)); | |
| l = Math.max(0, Math.min(1, l)); | |
| [r, g, b] = hslToRgb(h, s, l); | |
| data[i] = r; | |
| data[i + 1] = g; | |
| data[i + 2] = b; | |
| // 暗いほど透明 | |
| data[i + 3] = brightness; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| } | |
| function createFrameCanvas(index, frameWidth, frameHeight) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = frameWidth; | |
| canvas.height = frameHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage( | |
| loadedImage, | |
| index * frameWidth, | |
| 0, | |
| frameWidth, | |
| frameHeight, | |
| 0, | |
| 0, | |
| frameWidth, | |
| frameHeight | |
| ); | |
| return canvas; | |
| } | |
| function rebuildAllFrames() { | |
| frameData.forEach(frame => { | |
| const ctx = frame.previewCanvas.getContext('2d'); | |
| ctx.clearRect( | |
| 0, | |
| 0, | |
| frame.previewCanvas.width, | |
| frame.previewCanvas.height | |
| ); | |
| ctx.drawImage(frame.sourceCanvas, 0, 0); | |
| processCanvas(frame.previewCanvas); | |
| }); | |
| saturationValue.textContent = | |
| Number(saturationSlider.value).toFixed(2); | |
| brightnessValue.textContent = | |
| Number(brightnessSlider.value).toFixed(2); | |
| } | |
| buildBtn.addEventListener('click', () => { | |
| if (!loadedImage) return; | |
| frameData = []; | |
| framesContainer.innerHTML = ''; | |
| const sliceCount = Number(sliceCountInput.value); | |
| const frameWidth = Math.floor( | |
| loadedImage.width / sliceCount | |
| ); | |
| const frameHeight = loadedImage.height; | |
| for (let i = 0; i < sliceCount; i++) { | |
| const sourceCanvas = createFrameCanvas( | |
| i, | |
| frameWidth, | |
| frameHeight | |
| ); | |
| const previewCanvas = document.createElement('canvas'); | |
| previewCanvas.width = frameWidth; | |
| previewCanvas.height = frameHeight; | |
| const box = document.createElement('div'); | |
| box.className = 'frame-box'; | |
| const title = document.createElement('p'); | |
| title.textContent = `Frame ${i + 1}`; | |
| box.appendChild(title); | |
| box.appendChild(previewCanvas); | |
| framesContainer.appendChild(box); | |
| frameData.push({ | |
| sourceCanvas, | |
| previewCanvas | |
| }); | |
| } | |
| rebuildAllFrames(); | |
| gifDownloadBtn.disabled = false; | |
| zipDownloadBtn.disabled = false; | |
| gifSaveBtn.disabled = false; | |
| zipSaveBtn.disabled = false; | |
| }); | |
| saturationSlider.addEventListener('input', rebuildAllFrames); | |
| brightnessSlider.addEventListener('input', rebuildAllFrames); | |
| async function createGifBlob() { | |
| return new Promise((resolve) => { | |
| const delay = Number(delayInput.value); | |
| const workerBlob = new Blob([ | |
| ` | |
| self.window = self; | |
| importScripts( | |
| 'https://cdn.jsdelivr.net/npm/gif.js.optimized/dist/gif.worker.js' | |
| ); | |
| ` | |
| ], { | |
| type: 'application/javascript' | |
| }); | |
| const workerUrl = URL.createObjectURL(workerBlob); | |
| const gif = new GIF({ | |
| workers: 2, | |
| quality: 10, | |
| transparent: 0x000000, | |
| workerScript: workerUrl | |
| }); | |
| frameData.forEach(frame => { | |
| gif.addFrame(frame.previewCanvas, { | |
| delay, | |
| copy: true | |
| }); | |
| }); | |
| gif.on('finished', (blob) => { | |
| const url = URL.createObjectURL(blob); | |
| gifPreview.innerHTML = ''; | |
| const img = document.createElement('img'); | |
| img.src = url; | |
| gifPreview.appendChild(img); | |
| resolve(blob); | |
| }); | |
| gif.render(); | |
| }); | |
| } | |
| async function createZipBlob() { | |
| const zip = new JSZip(); | |
| for (let i = 0; i < frameData.length; i++) { | |
| const frame = frameData[i]; | |
| const blob = await new Promise(resolve => { | |
| frame.previewCanvas.toBlob( | |
| resolve, | |
| 'image/png' | |
| ); | |
| }); | |
| zip.file( | |
| `frame_${String(i + 1).padStart(3, '0')}.png`, | |
| blob | |
| ); | |
| } | |
| return await zip.generateAsync({ | |
| type: 'blob' | |
| }); | |
| } | |
| function normalDownload(blob, filename) { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| async function saveWithPicker(blob, filename, mime) { | |
| if (!('showSaveFilePicker' in window)) { | |
| normalDownload(blob, filename); | |
| return; | |
| } | |
| const handle = await window.showSaveFilePicker({ | |
| suggestedName: filename, | |
| types: [ | |
| { | |
| description: mime, | |
| accept: { | |
| [mime]: [ | |
| `.${filename.split('.').pop()}` | |
| ] | |
| } | |
| } | |
| ] | |
| }); | |
| const writable = await handle.createWritable(); | |
| await writable.write(blob); | |
| await writable.close(); | |
| } | |
| gifDownloadBtn.addEventListener('click', async () => { | |
| const blob = await createGifBlob(); | |
| normalDownload(blob, 'output.gif'); | |
| }); | |
| zipDownloadBtn.addEventListener('click', async () => { | |
| const blob = await createZipBlob(); | |
| normalDownload(blob, 'frames.zip'); | |
| }); | |
| gifSaveBtn.addEventListener('click', async () => { | |
| const blob = await createGifBlob(); | |
| await saveWithPicker( | |
| blob, | |
| 'output.gif', | |
| 'image/gif' | |
| ); | |
| }); | |
| zipSaveBtn.addEventListener('click', async () => { | |
| const blob = await createZipBlob(); | |
| await saveWithPicker( | |
| blob, | |
| 'frames.zip', | |
| 'application/zip' | |
| ); | |
| }); | |
| </script> | |
| </body> | |
| </html> |