显示调整
This commit is contained in:
1 parent
081d06c70e
commit
e2df519d2e
25 files changed
+291
-73
No files matched your search
+6
-2
@@ -113,7 +113,10 @@
|
||||
.thumb-dialog-input.hidden { display: none; }
|
||||
.thumb-dialog-error { color: var(--error); min-height: 18px; }
|
||||
.animated-thumb-card { width: min(480px, 92vw); gap: 10px; padding: 16px; }
|
||||
.thumb-dialog-note.warning { color: #e6b35c; }
|
||||
.animated-thumb-card .thumb-dialog-title { text-align: center; }
|
||||
.animated-thumb-warning { color: #fff; }
|
||||
.animated-thumb-warning-title { display: block; }
|
||||
.animated-thumb-danger { color: var(--error); }
|
||||
.animated-thumb-card .thumb-dialog-note { font-size: 13px; }
|
||||
.animated-thumb-form { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; gap: 10px 14px; padding: 2px 0; }
|
||||
.animated-thumb-label { color: var(--text-dim); }
|
||||
@@ -181,7 +184,8 @@
|
||||
#viewport { position: relative; width: 100%; min-width: 0; flex: 1; overflow: hidden; }
|
||||
canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#status {
|
||||
position: absolute; bottom: calc(22px + var(--timeline-height)); left: 12px;
|
||||
position: absolute; left: 50%; bottom: calc(22px + var(--timeline-height));
|
||||
transform: translateX(-50%);
|
||||
max-width: min(560px, calc(100% - 24px));
|
||||
padding: 6px 12px;
|
||||
color: var(--text); font-size: 12px; font-weight: 500; line-height: 1.45;
|
||||
|
||||
+47
-9
@@ -89,8 +89,8 @@
|
||||
</div>
|
||||
<div id="animatedThumbDialog" class="thumb-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="animatedThumbDialogTitle">
|
||||
<div class="thumb-dialog-card animated-thumb-card">
|
||||
<div id="animatedThumbDialogTitle" class="thumb-dialog-title" data-i18n="viewer.animatedThumbnailTitle">生成动画缩略图</div>
|
||||
<div class="thumb-dialog-note warning" data-i18n="viewer.animatedThumbnailNote">生成后将替换当前素材缩略图,插件无法自动恢复原图。长动画会自动降低帧率以控制文件大小。</div>
|
||||
<div id="animatedThumbDialogTitle" class="thumb-dialog-title" data-i18n="viewer.animatedThumbnailTitle">生成动态缩略图</div>
|
||||
<div class="thumb-dialog-note animated-thumb-warning"><span class="animated-thumb-warning-title" data-i18n="viewer.animatedThumbnailAttention">注意:</span><span data-i18n="viewer.animatedThumbnailNote">生成的缩略图会替换原有缩略图,该操作不可撤销且</span><span class="animated-thumb-danger" data-i18n="viewer.animatedThumbnailDanger">无法恢复原有缩略图</span><span data-i18n="viewer.animatedThumbnailSizeNote">,为避免动态缩略图数据过大,文件尺寸将调整至设定大小之下</span></div>
|
||||
<div class="animated-thumb-form">
|
||||
<label class="animated-thumb-quality"><span data-i18n="viewer.animatedFormat">输出格式</span>
|
||||
<select id="animatedThumbFormat"><option value="webp">AWebP (.webp)</option><option value="apng">APNG (.png)</option></select>
|
||||
@@ -102,8 +102,8 @@
|
||||
<label><span data-i18n="viewer.animatedEnd">结束</span><input id="animatedThumbEnd" type="number" min="0" step="1"></label>
|
||||
<span data-i18n="viewer.framesUnit">帧</span>
|
||||
</div>
|
||||
<label class="animated-thumb-quality"><span data-i18n="viewer.animatedMaxSize">文件大小上限</span>
|
||||
<span class="animated-thumb-size"><input id="animatedThumbMaxSize" type="number" min="50" max="10240" step="50" value="100"><span>KB</span></span>
|
||||
<label class="animated-thumb-quality"><span data-i18n="viewer.animatedMaxSize">缩略图大小</span>
|
||||
<span class="animated-thumb-size"><input id="animatedThumbMaxSize" type="number" min="10" max="1024" step="10" value="30"><span>KB</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="animatedThumbDialogError" class="thumb-dialog-error"></div>
|
||||
@@ -128,6 +128,7 @@
|
||||
const previewSettingsModule = require('../js/spine-preview-settings.js');
|
||||
const apngEncoder = require('../js/apng-encoder.js');
|
||||
const animatedWebpEncoder = require('../js/animated-webp-encoder.js');
|
||||
const visibleBounds = require('../js/spine-visible-bounds.js');
|
||||
let spineBadgeImagePromise = null;
|
||||
|
||||
function loadSpineBadgeImage() {
|
||||
@@ -751,7 +752,8 @@
|
||||
updateWorld(skeleton);
|
||||
|
||||
const size = 400;
|
||||
const bounds = getBounds(skeleton);
|
||||
const geometryBounds = getBounds(skeleton);
|
||||
const bounds = await measureVisibleBounds(skeleton, geometryBounds, 1, () => {}) || geometryBounds;
|
||||
const off = document.createElement('canvas');
|
||||
off.width = size; off.height = size;
|
||||
const octx = off.getContext('2d');
|
||||
@@ -801,7 +803,7 @@
|
||||
endInput.value = String(totalFrames);
|
||||
endInput.max = String(totalFrames);
|
||||
formatInput.value = 'webp';
|
||||
maxSizeInput.value = '100';
|
||||
maxSizeInput.value = '30';
|
||||
errorEl.textContent = '';
|
||||
dialog.classList.remove('hidden');
|
||||
return new Promise((resolve) => {
|
||||
@@ -820,7 +822,7 @@
|
||||
errorEl.textContent = t('viewer.animatedInvalidRange', { duration: totalFrames });
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(maxKilobytes) || maxKilobytes < 50 || maxKilobytes > 10240) {
|
||||
if (!Number.isInteger(maxKilobytes) || maxKilobytes < 10 || maxKilobytes > 1024) {
|
||||
errorEl.textContent = t('viewer.animatedInvalidSize');
|
||||
return;
|
||||
}
|
||||
@@ -836,8 +838,7 @@
|
||||
confirm.addEventListener('click', accept);
|
||||
cancel.addEventListener('click', reject);
|
||||
document.addEventListener('keydown', keydown);
|
||||
startInput.focus();
|
||||
startInput.select();
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -862,6 +863,40 @@
|
||||
return { x: left, y: bottom, width: right - left, height: top - bottom };
|
||||
}
|
||||
|
||||
async function measureVisibleBounds(targetSkeleton, fallbackBounds, poseCount, applyPose) {
|
||||
if (!fallbackBounds || fallbackBounds.width <= 0 || fallbackBounds.height <= 0) return null;
|
||||
const size = 512;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size; canvas.height = size;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
const scale = Math.min(size * 0.9 / fallbackBounds.width, size * 0.9 / fallbackBounds.height);
|
||||
const centerX = fallbackBounds.x + fallbackBounds.width / 2;
|
||||
const centerY = fallbackBounds.y + fallbackBounds.height / 2;
|
||||
let measured = null;
|
||||
let probeRenderer = null;
|
||||
try {
|
||||
probeRenderer = clippingRenderer.createRenderer(spine, context, activeRuntimeDir);
|
||||
probeRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
for (let index = 0; index < poseCount; index++) {
|
||||
applyPose(index);
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.clearRect(0, 0, size, size);
|
||||
context.save();
|
||||
context.translate(size / 2, size / 2);
|
||||
context.scale(scale, -scale);
|
||||
context.translate(-centerX, -centerY);
|
||||
probeRenderer.draw(targetSkeleton);
|
||||
context.restore();
|
||||
const pixels = visibleBounds.findAlphaBounds(context.getImageData(0, 0, size, size).data, size, size);
|
||||
measured = mergeBounds(measured, visibleBounds.toWorldBounds(pixels, size, size, scale, centerX, centerY));
|
||||
if (index % 6 === 5) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
} finally {
|
||||
if (probeRenderer && probeRenderer.dispose) probeRenderer.dispose();
|
||||
}
|
||||
return measured;
|
||||
}
|
||||
|
||||
function quantizeFrame(data, bits) {
|
||||
if (bits >= 8) return new Uint8Array(data);
|
||||
const levels = (1 << bits) - 1;
|
||||
@@ -1032,6 +1067,9 @@
|
||||
}
|
||||
bounds = bounds || stableFitBounds;
|
||||
if (!bounds || bounds.width <= 0 || bounds.height <= 0) throw new Error(t('viewer.emptySkeletonMsg'));
|
||||
bounds = await measureVisibleBounds(previewSkeleton, bounds, sampleTimes.length, (index) => {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index]);
|
||||
}) || bounds;
|
||||
|
||||
const off = document.createElement('canvas');
|
||||
off.width = size; off.height = size;
|
||||
|
||||
Reference in new issue
Block a user