player / rembg.html
izuemon's picture
Update rembg.html
f913ea5 verified
Raw History Blame Contribute Delete
13.8 kB
<!DOCTYPE html>
<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>