- 初回起動時にA4縦・A4横の3コマテンプレートを自動登録、「ページ新規作成」を「クイック」に改称(線幅の確認を省略) - 出力タブ: 終了ページを総ページ数に自動設定、作業中の作品で自動絞り込み、PDF/EPUB選択時に「背景のみ」「zip保存」を無効化、「フキダシのみ」を削除 - レイアウトタブ「画像」サブタブ: BiRefNet背景除去を実装、実行結果を元画像の位置・大きさで挿入し元画像を非表示に - Nanobanana: 複数枚生成時に全結果をサムネイル表示し、選んだ画像をコマへ挿入 - Imageタブ: トースト・ダイアログの多言語化(ja/en/zh)、テキスト入力をレイアウトタブと同じ操作(Ctrl+Enter確定)に統一 - 初回の表示言語をブラウザの言語から決定、Eagle設定の「ComfyUI 生成画像を自動保存」はWorkflow Studio側設定を案内(グレーアウト) - My曲線の既定画像を同梱、ヘルプ(ja/en/zh)の表記修正、.comfyignoreに!docs/manual/を追加 - 起動ログのポート表示を固定値から実際のポートに修正 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
527 lines
23 KiB
JavaScript
527 lines
23 KiB
JavaScript
// ============================================================
|
||
// main.js 分割ファイル (17/24): Processingタブ+Editタブ
|
||
// 元 main.js の行 13258-13628 に相当
|
||
// type="module" として読み込まれる(ESモジュール化 G7)。
|
||
// 主なトップレベル定義: _getSvgSize,_layerDrawOriginalUnit,_procApplyDenoise,_procApplySharpen,_procRemoveBackground,_procRemoveBackgroundBiRefNet,_procRemoveBackgroundImgly,_procRemoveBackgroundSvg,_procRun,_procState,_procUpscale,_svgBase64ToText,initEditTab,initProcessingTab
|
||
// 未ESM化の外部依存(非moduleのグローバル関数はwindowプロパティとして自動的に見えるため、
|
||
// 呼び出し箇所は書き換えていない): state(01-state.js)
|
||
// ============================================================
|
||
|
||
import { t } from '../i18n.js';
|
||
import { insertImage } from './08-panels-images.js';
|
||
import { _layerDrawState, initLayerDraw, _layerDrawUpdateToggle, _layerDrawDetachOverlay } from './17a-layer-draw-input.js';
|
||
import { _paintToolState, initPaintTool, _paintUpdateToggle, _paintDetachOverlay } from './17d-layer-draw-paint.js';
|
||
import { _layerDrawShapeToPng } from './17b-layer-draw-commit.js';
|
||
import { _svgColorState, _svgColorLoad, _svgColorApply, _svgToPngConvert } from './18-svg-color-png.js';
|
||
import { openImageTabWithSelected, openLayoutI2IModal, moveSelectedObjectToCenter } from './15-pixifx-bridge.js';
|
||
import { state } from './01-state.js';
|
||
import { getPanelLayerSvg, renderLayerPanel } from './04b-layer-panel-render.js';
|
||
import { savePanelSvg } from './07-pages.js';
|
||
|
||
// ============================================================
|
||
// Processing タブ(アップスケール・背景除去)
|
||
// ============================================================
|
||
|
||
const _procState = {
|
||
scale: 2.0,
|
||
};
|
||
|
||
function initProcessingTab() {
|
||
const scaleUp = document.getElementById('proc-scale-up');
|
||
const scaleDown = document.getElementById('proc-scale-down');
|
||
const scaleVal = document.getElementById('proc-scale-value');
|
||
const sharpen = document.getElementById('proc-sharpen');
|
||
const sharpenV = document.getElementById('proc-sharpen-value');
|
||
const runBtn = document.getElementById('proc-run-btn');
|
||
|
||
scaleUp.addEventListener('click', () => {
|
||
_procState.scale = Math.min(4.0, parseFloat((_procState.scale + 0.5).toFixed(1)));
|
||
scaleVal.textContent = _procState.scale.toFixed(1) + '×';
|
||
});
|
||
scaleDown.addEventListener('click', () => {
|
||
_procState.scale = Math.max(1.0, parseFloat((_procState.scale - 0.5).toFixed(1)));
|
||
scaleVal.textContent = _procState.scale.toFixed(1) + '×';
|
||
});
|
||
sharpen.addEventListener('input', () => {
|
||
sharpenV.textContent = sharpen.value + '%';
|
||
});
|
||
const bgRemoveChk = document.getElementById('proc-bg-remove');
|
||
const bgModelSelect = document.getElementById('proc-bg-model');
|
||
const bgQualitySelect = document.getElementById('proc-bg-quality');
|
||
const syncBgUI = () => {
|
||
const on = bgRemoveChk.checked;
|
||
bgModelSelect.style.display = on ? 'inline-block' : 'none';
|
||
bgQualitySelect.style.display = (on && bgModelSelect.value === 'imgly') ? 'inline-block' : 'none';
|
||
};
|
||
bgRemoveChk.addEventListener('change', syncBgUI);
|
||
bgModelSelect.addEventListener('change', syncBgUI);
|
||
_procCheckBiRefNetAvailability(bgModelSelect);
|
||
runBtn.addEventListener('click', _procRun);
|
||
}
|
||
|
||
async function _procRun() {
|
||
const statusEl = document.getElementById('proc-status');
|
||
const bgRemove = document.getElementById('proc-bg-remove').checked;
|
||
const bgModel = document.getElementById('proc-bg-model').value;
|
||
const bgQuality = document.getElementById('proc-bg-quality').value;
|
||
const denoise = document.getElementById('proc-denoise').checked;
|
||
const sharpenPc = parseInt(document.getElementById('proc-sharpen').value, 10) / 100;
|
||
|
||
// 選択画像の base64 取得
|
||
if (!state.selectedImageEl) {
|
||
alert(t('layout.msgSelectImage'));
|
||
return;
|
||
}
|
||
const origHref = state.selectedImageEl.getAttribute('href') || state.selectedImageEl.getAttribute('xlink:href') || '';
|
||
if (!origHref.startsWith('data:')) {
|
||
alert(t('layout.msgImageBase64Unavailable'));
|
||
return;
|
||
}
|
||
|
||
// 元画像の位置・表示サイズ・回転を引き継いで挿入し、成功後に元画像を非表示にする
|
||
const origEl = state.selectedImageEl;
|
||
const origId = origEl.id;
|
||
const num = (n) => parseFloat(origEl.getAttribute(n));
|
||
const placement = [num('x'), num('y'), num('width'), num('height')].every(Number.isFinite)
|
||
? { x: num('x'), y: num('y'), width: num('width'), height: num('height') }
|
||
: null;
|
||
const extraAttrs = {};
|
||
for (const a of ['transform', 'preserveAspectRatio']) {
|
||
const v = origEl.getAttribute(a);
|
||
if (v) extraAttrs[a] = v;
|
||
}
|
||
|
||
statusEl.textContent = t('layout.processing');
|
||
document.getElementById('proc-run-btn').disabled = true;
|
||
|
||
try {
|
||
let dataUrl = origHref;
|
||
const isSvg = dataUrl.startsWith('data:image/svg');
|
||
|
||
// SVGの場合: 背景除去のみ実行してアップスケール等をスキップ
|
||
if (isSvg) {
|
||
if (bgRemove) {
|
||
statusEl.textContent = t('layout.procSvgBgRemoving');
|
||
dataUrl = _procRemoveBackgroundSvg(dataUrl);
|
||
}
|
||
statusEl.textContent = t('layout.procInserting');
|
||
const { w, h } = _getSvgSize(dataUrl);
|
||
await insertImage(dataUrl, w, h, extraAttrs, placement);
|
||
await _procHideOriginal(origId);
|
||
statusEl.textContent = t('common.done');
|
||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||
return;
|
||
}
|
||
|
||
// 背景除去
|
||
if (bgRemove) {
|
||
statusEl.textContent = t('layout.procBgRemoving', bgModel === 'birefnet' ? 'BiRefNet' : t('layout.procLightModel'));
|
||
dataUrl = await _procRemoveBackground(dataUrl, bgModel, bgQuality);
|
||
}
|
||
|
||
// アップスケール
|
||
statusEl.textContent = t('image.upscaleRunning');
|
||
dataUrl = await _procUpscale(dataUrl, _procState.scale, denoise, sharpenPc);
|
||
|
||
// 新規保存(insertImage でコマに挿入)
|
||
statusEl.textContent = t('layout.procInserting');
|
||
const img = new Image();
|
||
await new Promise((resolve, reject) => {
|
||
img.onload = resolve;
|
||
img.onerror = reject;
|
||
img.src = dataUrl;
|
||
});
|
||
await insertImage(dataUrl, img.width, img.height, extraAttrs, placement);
|
||
await _procHideOriginal(origId);
|
||
statusEl.textContent = t('common.done');
|
||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||
|
||
} catch (e) {
|
||
console.error('Processing error:', e);
|
||
statusEl.textContent = t('common.errorPrefix', e.message);
|
||
} finally {
|
||
document.getElementById('proc-run-btn').disabled = false;
|
||
}
|
||
}
|
||
|
||
// 実行後に元画像を非表示にして保存する(レイヤーパネルの👁ボタンと同じ状態。再表示も👁ボタンで可能)
|
||
async function _procHideOriginal(origId) {
|
||
if (!origId) return;
|
||
const svg = getPanelLayerSvg();
|
||
const el = svg && Array.from(svg.querySelectorAll('.inserted-image')).find(e => e.id === origId);
|
||
if (!el) return;
|
||
el.style.display = 'none';
|
||
const panelId = el.closest('g[data-clip-panel]')?.getAttribute('data-clip-panel') ||
|
||
el.getAttribute('data-panel-id') || state.selectedPanelId || 'panel-0';
|
||
await savePanelSvg(panelId, svg);
|
||
if (state.selectedImageId === origId) { state.selectedImageId = null; state.selectedImageEl = null; }
|
||
renderLayerPanel();
|
||
}
|
||
|
||
// base64 SVG dataUrl を UTF-8 テキストにデコードするヘルパー
|
||
function _svgBase64ToText(dataUrl) {
|
||
const raw = dataUrl.split(',')[1] || '';
|
||
const bytes = Uint8Array.from(atob(raw), c => c.charCodeAt(0));
|
||
return new TextDecoder().decode(bytes);
|
||
}
|
||
|
||
// SVGのbase64 dataUrlからviewBox/width/heightを取得して { w, h } を返す
|
||
function _getSvgSize(dataUrl) {
|
||
try {
|
||
const svgText = _svgBase64ToText(dataUrl);
|
||
const parser = new DOMParser();
|
||
const svgEl = parser.parseFromString(svgText, 'image/svg+xml').querySelector('svg');
|
||
if (svgEl) {
|
||
const vb = svgEl.getAttribute('viewBox');
|
||
if (vb) {
|
||
const p = vb.trim().split(/[\s,]+/);
|
||
return { w: parseFloat(p[2]) || 1000, h: parseFloat(p[3]) || 1000 };
|
||
}
|
||
return {
|
||
w: parseFloat(svgEl.getAttribute('width')) || 1000,
|
||
h: parseFloat(svgEl.getAttribute('height')) || 1000,
|
||
};
|
||
}
|
||
} catch (_) {}
|
||
return { w: 1000, h: 1000 };
|
||
}
|
||
|
||
// SVG背景除去: 単色背景のrect/矩形pathを削除し、透過SVGとして返す
|
||
function _procRemoveBackgroundSvg(dataUrl) {
|
||
try {
|
||
const svgText = _svgBase64ToText(dataUrl);
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(svgText, 'image/svg+xml');
|
||
const svgEl = doc.querySelector('svg');
|
||
if (!svgEl) return dataUrl;
|
||
|
||
// 全面を覆うrect(原点付近から全幅・全高、任意色)を削除
|
||
const vb = svgEl.getAttribute('viewBox');
|
||
const vbParts = vb ? vb.trim().split(/[\s,]+/) : null;
|
||
const vbW = vbParts ? parseFloat(vbParts[2]) : null;
|
||
const vbH = vbParts ? parseFloat(vbParts[3]) : null;
|
||
svgEl.querySelectorAll('rect').forEach(el => {
|
||
const x = parseFloat(el.getAttribute('x') || '0');
|
||
const y = parseFloat(el.getAttribute('y') || '0');
|
||
const w = el.getAttribute('width') || '';
|
||
const h = el.getAttribute('height') || '';
|
||
const isOrigin = x <= 0 && y <= 0;
|
||
const isFullW = w === '100%' || (vbW && parseFloat(w) >= vbW * 0.99);
|
||
const isFullH = h === '100%' || (vbH && parseFloat(h) >= vbH * 0.99);
|
||
if (isOrigin && isFullW && isFullH) el.remove();
|
||
});
|
||
|
||
const serializer = new XMLSerializer();
|
||
const newSvg = serializer.serializeToString(svgEl);
|
||
return 'data:image/svg+xml;base64,' + btoa(new TextEncoder().encode(newSvg).reduce((s, b) => s + String.fromCharCode(b), ''));
|
||
} catch (e) {
|
||
console.error('[SVG BgRemove]', e);
|
||
return dataUrl;
|
||
}
|
||
}
|
||
|
||
async function _procRemoveBackground(dataUrl, model = 'imgly', quality = 'medium') {
|
||
if (model === 'birefnet') {
|
||
return await _procRemoveBackgroundBiRefNet(dataUrl);
|
||
}
|
||
return await _procRemoveBackgroundImgly(dataUrl, quality);
|
||
}
|
||
|
||
// quality: 'small'(軽量・低品質、isnet_quint8)/ 'medium'(標準、isnet_fp16、既定)/ 'large'(高品質、isnet)
|
||
async function _procRemoveBackgroundImgly(dataUrl, quality = 'medium') {
|
||
if (!window._imglyRemoveBackground) {
|
||
const mod = await import('https://esm.sh/@imgly/background-removal@1.5.7?bundle&target=es2022');
|
||
window._imglyRemoveBackground = mod.removeBackground;
|
||
}
|
||
const fn = window._imglyRemoveBackground;
|
||
const res = await fetch(dataUrl);
|
||
const blob = await res.blob();
|
||
const resultBlob = await fn(blob, {
|
||
publicPath: 'https://staticimgly.com/@imgly/background-removal-data/1.5.7/dist/',
|
||
model: quality,
|
||
});
|
||
return await new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => resolve(reader.result);
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(resultBlob);
|
||
});
|
||
}
|
||
|
||
// BiRefNet 背景除去(Mask Editor One の /mask_editor/birefnet/* ルートを利用。Imageタブと同じ経路)
|
||
async function _procRemoveBackgroundBiRefNet(dataUrl) {
|
||
const NODE_ID = 'cc_layout_proc_bgremove';
|
||
const storeResp = await fetch('/mask_editor/store_image', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ node_id: NODE_ID, image_b64: dataUrl }),
|
||
});
|
||
if (!storeResp.ok) throw new Error('Failed to cache image for BiRefNet');
|
||
|
||
const resp = await fetch('/mask_editor/birefnet/remove_bg', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ node_id: NODE_ID }),
|
||
});
|
||
const json = await resp.json();
|
||
if (json.error) throw new Error(json.error);
|
||
return await _procApplyMaskToImage(dataUrl, json.mask_b64);
|
||
}
|
||
|
||
// グレースケールマスク(白=前景)を元画像に destination-in で適用してRGBA PNGを返す
|
||
async function _procApplyMaskToImage(imageB64, maskB64) {
|
||
const loadImage = src => new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.onload = () => resolve(img);
|
||
img.onerror = () => reject(new Error('Image load failed'));
|
||
img.src = src;
|
||
});
|
||
const [origImg, maskImg] = await Promise.all([loadImage(imageB64), loadImage(maskB64)]);
|
||
const w = origImg.naturalWidth;
|
||
const h = origImg.naturalHeight;
|
||
|
||
const maskCanvas = document.createElement('canvas');
|
||
maskCanvas.width = w;
|
||
maskCanvas.height = h;
|
||
const mc = maskCanvas.getContext('2d');
|
||
mc.drawImage(maskImg, 0, 0, w, h);
|
||
const maskData = mc.getImageData(0, 0, w, h);
|
||
const md = maskData.data;
|
||
for (let i = 0; i < md.length; i += 4) {
|
||
md[i + 3] = md[i]; // 輝度 → アルファ
|
||
md[i] = md[i + 1] = md[i + 2] = 255;
|
||
}
|
||
mc.putImageData(maskData, 0, 0);
|
||
|
||
const out = document.createElement('canvas');
|
||
out.width = w;
|
||
out.height = h;
|
||
const ctx = out.getContext('2d');
|
||
ctx.drawImage(origImg, 0, 0, w, h);
|
||
ctx.globalCompositeOperation = 'destination-in';
|
||
ctx.drawImage(maskCanvas, 0, 0, w, h);
|
||
return out.toDataURL('image/png');
|
||
}
|
||
|
||
// BiRefNet の利用可否を確認し、利用できない場合は選択肢を無効化して理由をラベルに表示する
|
||
async function _procCheckBiRefNetAvailability(bgModelSelect) {
|
||
const opt = bgModelSelect.querySelector('option[value="birefnet"]');
|
||
if (!opt) return;
|
||
let available = false;
|
||
try {
|
||
const resp = await fetch('/mask_editor/birefnet/status');
|
||
if (resp.ok) {
|
||
const json = await resp.json();
|
||
available = json.loaded === true || json.model_found === true;
|
||
}
|
||
} catch (_) { /* Mask Editor One 未導入 */ }
|
||
opt.disabled = !available;
|
||
const key = available ? 'layout.bgModelBiRefNet' : 'layout.bgModelBiRefNetUnavailable';
|
||
opt.setAttribute('data-i18n', key);
|
||
opt.textContent = t(key);
|
||
if (!available && bgModelSelect.value === 'birefnet') bgModelSelect.value = 'imgly';
|
||
bgModelSelect.dispatchEvent(new Event('change'));
|
||
}
|
||
|
||
async function _procUpscale(dataUrl, scale, denoise, sharpenRatio) {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const w = img.width;
|
||
const h = img.height;
|
||
|
||
// ソースキャンバス
|
||
const srcCanvas = document.createElement('canvas');
|
||
srcCanvas.width = w;
|
||
srcCanvas.height = h;
|
||
const srcCtx = srcCanvas.getContext('2d');
|
||
srcCtx.drawImage(img, 0, 0);
|
||
|
||
let srcData = srcCtx.getImageData(0, 0, w, h);
|
||
|
||
// ノイズ除去
|
||
if (denoise) {
|
||
srcData = _procApplyDenoise(srcData, w, h);
|
||
srcCtx.putImageData(srcData, 0, 0);
|
||
}
|
||
|
||
// アップスケール(バイキュービック相当)
|
||
const dstW = Math.round(w * scale);
|
||
const dstH = Math.round(h * scale);
|
||
const dstCanvas = document.createElement('canvas');
|
||
dstCanvas.width = dstW;
|
||
dstCanvas.height = dstH;
|
||
const dstCtx = dstCanvas.getContext('2d');
|
||
dstCtx.imageSmoothingEnabled = true;
|
||
dstCtx.imageSmoothingQuality = 'high';
|
||
dstCtx.drawImage(srcCanvas, 0, 0, dstW, dstH);
|
||
|
||
// シャープ化
|
||
if (sharpenRatio > 0) {
|
||
const sharpData = _procApplySharpen(dstCtx.getImageData(0, 0, dstW, dstH), dstW, dstH, sharpenRatio);
|
||
dstCtx.putImageData(sharpData, 0, 0);
|
||
}
|
||
|
||
resolve(dstCanvas.toDataURL('image/png'));
|
||
};
|
||
img.onerror = reject;
|
||
img.src = dataUrl;
|
||
});
|
||
}
|
||
|
||
function _procApplyDenoise(imageData, w, h) {
|
||
const src = imageData.data;
|
||
const out = new Uint8ClampedArray(src.length);
|
||
const radius = 2;
|
||
const sigma = 30;
|
||
for (let y = 0; y < h; y++) {
|
||
for (let x = 0; x < w; x++) {
|
||
let rSum = 0, gSum = 0, bSum = 0, wSum = 0;
|
||
const ci = (y * w + x) * 4;
|
||
const cr = src[ci], cg = src[ci+1], cb = src[ci+2];
|
||
for (let dy = -radius; dy <= radius; dy++) {
|
||
for (let dx = -radius; dx <= radius; dx++) {
|
||
const nx = x + dx, ny = y + dy;
|
||
if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue;
|
||
const ni = (ny * w + nx) * 4;
|
||
const nr = src[ni], ng = src[ni+1], nb = src[ni+2];
|
||
const spatialW = Math.exp(-(dx*dx + dy*dy) / (2 * radius * radius));
|
||
const colorD = (nr-cr)**2 + (ng-cg)**2 + (nb-cb)**2;
|
||
const colorW = Math.exp(-colorD / (2 * sigma * sigma));
|
||
const w2 = spatialW * colorW;
|
||
rSum += nr * w2; gSum += ng * w2; bSum += nb * w2; wSum += w2;
|
||
}
|
||
}
|
||
const oi = (y * w + x) * 4;
|
||
out[oi] = rSum / wSum;
|
||
out[oi+1] = gSum / wSum;
|
||
out[oi+2] = bSum / wSum;
|
||
out[oi+3] = src[oi+3];
|
||
}
|
||
}
|
||
return new ImageData(out, w, h);
|
||
}
|
||
|
||
function _procApplySharpen(imageData, w, h, amount) {
|
||
const src = imageData.data;
|
||
const out = new Uint8ClampedArray(src.length);
|
||
const kernel = [-1,-1,-1,-1,9,-1,-1,-1,-1];
|
||
for (let y = 0; y < h; y++) {
|
||
for (let x = 0; x < w; x++) {
|
||
let r = 0, g = 0, b = 0;
|
||
for (let ky = -1; ky <= 1; ky++) {
|
||
for (let kx = -1; kx <= 1; kx++) {
|
||
const nx = Math.max(0, Math.min(w-1, x+kx));
|
||
const ny = Math.max(0, Math.min(h-1, y+ky));
|
||
const ni = (ny * w + nx) * 4;
|
||
const k = kernel[(ky+1)*3 + (kx+1)];
|
||
r += src[ni] * k;
|
||
g += src[ni+1] * k;
|
||
b += src[ni+2] * k;
|
||
}
|
||
}
|
||
const oi = (y * w + x) * 4;
|
||
const sr = src[oi], sg = src[oi+1], sb = src[oi+2];
|
||
out[oi] = Math.max(0, Math.min(255, sr + (r - sr) * amount));
|
||
out[oi+1] = Math.max(0, Math.min(255, sg + (g - sg) * amount));
|
||
out[oi+2] = Math.max(0, Math.min(255, sb + (b - sb) * amount));
|
||
out[oi+3] = src[oi+3];
|
||
}
|
||
}
|
||
return new ImageData(out, w, h);
|
||
}
|
||
|
||
// ============================================================
|
||
// Edit タブ(ボックス描画・テキスト・モザイク・ぼかし)
|
||
// ============================================================
|
||
|
||
function initEditTab() {
|
||
// Editモード切り替えタブ
|
||
document.querySelectorAll('.edit-mode-tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.edit-mode-tab-btn').forEach(b => {
|
||
b.classList.remove('active');
|
||
b.style.background = 'var(--bg-secondary)';
|
||
});
|
||
btn.classList.add('active');
|
||
btn.style.background = 'var(--bg-primary)';
|
||
const mode = btn.dataset.editMode;
|
||
document.getElementById('edit-panel-box').style.display = mode === 'box' ? '' : 'none';
|
||
document.getElementById('edit-panel-paint').style.display = mode === 'paint' ? '' : 'none';
|
||
document.getElementById('edit-panel-svg-color').style.display = mode === 'svg-color' ? '' : 'none';
|
||
document.getElementById('edit-panel-svg-png').style.display = mode === 'svg-png' ? '' : 'none';
|
||
// ドロータブから離れたら描画ONを解除。ペイントタブから離れてもペイントONを解除
|
||
if (mode !== 'box' && _layerDrawState.active) {
|
||
_layerDrawState.active = false;
|
||
_layerDrawUpdateToggle();
|
||
_layerDrawDetachOverlay();
|
||
}
|
||
if (mode !== 'paint' && _paintToolState.active) {
|
||
_paintToolState.active = false;
|
||
_paintUpdateToggle();
|
||
_paintDetachOverlay();
|
||
}
|
||
});
|
||
});
|
||
|
||
// SVG色変更
|
||
document.getElementById('svg-color-load-btn').addEventListener('click', _svgColorLoad);
|
||
document.getElementById('svg-color-apply-btn').addEventListener('click', _svgColorApply);
|
||
|
||
// SVG→PNG変換
|
||
document.getElementById('svg-png-convert-btn').addEventListener('click', _svgToPngConvert);
|
||
|
||
// 縦横比固定: 幅変更で高さを自動更新
|
||
const pngW = document.getElementById('svg-png-width');
|
||
const pngH = document.getElementById('svg-png-height');
|
||
pngW.addEventListener('input', () => {
|
||
if (!document.getElementById('svg-png-keep-aspect').checked) return;
|
||
const ratio = _svgColorState.origHeight / (_svgColorState.origWidth || 1);
|
||
if (pngW.value) pngH.value = Math.round(Number(pngW.value) * ratio) || '';
|
||
});
|
||
pngH.addEventListener('input', () => {
|
||
if (!document.getElementById('svg-png-keep-aspect').checked) return;
|
||
const ratio = _svgColorState.origWidth / (_svgColorState.origHeight || 1);
|
||
if (pngH.value) pngW.value = Math.round(Number(pngH.value) * ratio) || '';
|
||
});
|
||
|
||
// 図形をPNG変換ボタン
|
||
document.getElementById('layer-draw-shape-to-png')?.addEventListener('click', _layerDrawShapeToPng);
|
||
|
||
// レイヤー描画初期化
|
||
initLayerDraw();
|
||
|
||
// ペイントツール初期化(ラスターブラシ。ドローとは別タブ・別状態で動作)
|
||
initPaintTool();
|
||
|
||
// 画像タブで編集ボタン: Imageタブで開く
|
||
document.getElementById('layer-draw-open-imgedit').addEventListener('click', openImageTabWithSelected);
|
||
|
||
// I2Iボタン: 選択画像またはページ全体をWorkflow Studioへ送信し、その場でI2I実行するモーダルを開く
|
||
document.getElementById('layout-i2i-modal-btn')?.addEventListener('click', openLayoutI2IModal);
|
||
|
||
// OCボタン: 選択中オブジェクトをコマ/ページ中央へ移動
|
||
document.getElementById('object-center-btn')?.addEventListener('click', moveSelectedObjectToCenter);
|
||
}
|
||
|
||
// オリジナル画像の一コマをCanvasに描画(レイヤー描画のMy曲線用)
|
||
function _layerDrawOriginalUnit(ctx, x, y, angle, scale, img) {
|
||
if (!img || !img.complete || img.naturalWidth === 0) return;
|
||
const w = img.naturalWidth * scale;
|
||
const h = img.naturalHeight * scale;
|
||
ctx.save();
|
||
ctx.translate(x, y);
|
||
ctx.rotate(angle);
|
||
ctx.drawImage(img, -w / 2, -h / 2, w, h);
|
||
ctx.restore();
|
||
}
|
||
|
||
export { initProcessingTab, initEditTab, _layerDrawOriginalUnit };
|
||
|
||
// まだESM化されていない main/以下の classic <script> から呼べるようにするブリッジ
|
||
// (ESモジュール化移行中の一時措置。全分割ファイルのESM化が完了したら、
|
||
// 各呼び出し元をimport文に置き換えてこのブロックごと削除する)。
|
||
window.initProcessingTab = initProcessingTab;
|
||
window.initEditTab = initEditTab;
|
||
|