| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- // 九宫格图生成器 - 下载处理模块
- // 实现类似 Texture Packer 的功能:将多张图片拼接成一张图,并生成 JSON 文件
- (function () {
- // 获取当前登录用户名
- function getCurrentUsername() {
- try {
- const loginDataStr = localStorage.getItem('loginData');
- if (!loginDataStr) {
- return null;
- }
-
- const loginData = JSON.parse(loginDataStr);
- const now = Date.now();
-
- // 检查是否过期
- if (now >= loginData.expireTime) {
- localStorage.removeItem('loginData');
- return null;
- }
-
- return loginData.user ? loginData.user.username : null;
- } catch (error) {
- console.error('[SpriteSheet] 获取用户名失败:', error);
- return null;
- }
- }
- // 显示提示信息(使用全局 Alert 组件,直接调用)
- function showAlert(message, duration = 2000) {
- // 直接调用父窗口的 GlobalAlert(不通过 postMessage)
- try {
- // 优先使用父窗口的 GlobalAlert
- if (window.parent && window.parent !== window && window.parent.GlobalAlert) {
- window.parent.GlobalAlert.show(message, duration);
- return;
- }
-
- // 如果不在 iframe 中,直接使用当前窗口的 GlobalAlert
- if (window.GlobalAlert) {
- window.GlobalAlert.show(message, duration);
- return;
- }
-
- // 降级处理
- console.log('[Alert]', message);
- } catch (error) {
- console.error('[SpriteSheet] 显示 alert 失败:', error);
- alert(message);
- }
- }
- // 统一的错误处理函数:解析服务端错误响应并显示
- async function handleServerError(response, defaultMessage = '操作失败') {
- let errorMessage = defaultMessage;
-
- try {
- // 尝试解析 JSON 错误响应
- const errorData = await response.json().catch(() => null);
- if (errorData) {
- // 优先使用服务端返回的 message
- if (errorData.message) {
- errorMessage = errorData.message;
- } else if (errorData.error) {
- errorMessage = errorData.error;
- } else if (typeof errorData === 'string') {
- errorMessage = errorData;
- }
- }
- } catch (e) {
- // 如果解析失败,使用默认消息或状态码
- if (response.status) {
- errorMessage = `${defaultMessage} (状态码: ${response.status})`;
- }
- }
-
- showAlert(errorMessage);
- return errorMessage;
- }
- // Cocos Creator 配置选项
- const COCOS_CONFIG = {
- // 是否使用 2 的幂次方尺寸(Cocos Creator 3.8 不需要,但可以启用以获得更好的性能)
- usePowerOfTwo: false,
- // 图片拼接时是否对齐到像素边界(推荐开启)
- pixelPerfect: true
- };
- // 计算最小2的幂次方(可选,用于优化纹理内存使用)
- function nextPowerOfTwo(n) {
- if (n <= 0) return 1;
- if ((n & (n - 1)) === 0) return n; // 已经是2的幂
- let power = 1;
- while (power < n) {
- power <<= 1;
- }
- return power;
- }
- // 计算最终尺寸(根据配置决定是否使用 2 的幂次方)
- function calculateFinalSize(width, height) {
- if (COCOS_CONFIG.usePowerOfTwo) {
- return {
- width: nextPowerOfTwo(width),
- height: nextPowerOfTwo(height)
- };
- } else {
- // 直接使用实际尺寸,Cocos Creator 3.8 完全支持
- return { width, height };
- }
- }
- // 加载图片
- function loadImage(src) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.crossOrigin = 'anonymous';
- img.onload = () => resolve(img);
- img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
- img.src = src;
- });
- }
- // 显示加载动画
- function showLoadingModal(folderName) {
- // 创建模态框
- const modal = document.createElement('div');
- modal.id = 'spriteSheetLoadingModal';
- modal.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 99999;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- `;
- const content = document.createElement('div');
- content.style.cssText = `
- background: #ffffff;
- border-radius: 12px;
- padding: 30px 40px;
- text-align: center;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- min-width: 300px;
- `;
- const title = document.createElement('div');
- title.textContent = '正在生成 Sprite Sheet';
- title.style.cssText = `
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 10px;
- `;
- const folder = document.createElement('div');
- folder.textContent = folderName;
- folder.style.cssText = `
- font-size: 14px;
- color: #6b7280;
- margin-bottom: 20px;
- `;
- // 加载动画
- const spinner = document.createElement('div');
- spinner.style.cssText = `
- width: 40px;
- height: 40px;
- border: 4px solid #e5e7eb;
- border-top-color: #3b82f6;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin: 0 auto 20px;
- `;
- // 添加动画样式
- if (!document.getElementById('spriteSheetLoadingStyle')) {
- const style = document.createElement('style');
- style.id = 'spriteSheetLoadingStyle';
- style.textContent = `
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- `;
- document.head.appendChild(style);
- }
- const status = document.createElement('div');
- status.id = 'spriteSheetStatus';
- status.textContent = '加载图片中...';
- status.style.cssText = `
- font-size: 14px;
- color: #374151;
- `;
- content.appendChild(title);
- content.appendChild(folder);
- content.appendChild(spinner);
- content.appendChild(status);
- modal.appendChild(content);
- document.body.appendChild(modal);
- return {
- modal,
- updateStatus: (text) => {
- status.textContent = text;
- }
- };
- }
- // 隐藏加载动画
- function hideLoadingModal() {
- const modal = document.getElementById('spriteSheetLoadingModal');
- if (modal) {
- modal.remove();
- }
- }
- // 下载文件
- function downloadFile(data, filename, mimeType) {
- const blob = new Blob([data], { type: mimeType });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
- // 生成 Cocos Creator 3.8 兼容的 JSON 格式的 sprite sheet 数据
- // Cocos Creator 使用标准的 TexturePacker JSON 格式,坐标系统从上到下(左上角为原点)
- function generateJSON(folderName, images, sheetWidth, sheetHeight) {
- const frames = {};
-
- images.forEach((img, index) => {
- const frameNum = (index + 1).toString().padStart(2, '0');
- const frameName = `${frameNum}.png`;
- const x = img.x;
- const y = img.y; // Cocos Creator 和 Canvas 都使用从上到下的坐标系统
- const width = img.width;
- const height = img.height;
-
- // 标准的 TexturePacker JSON 格式,Cocos Creator 3.8 完全兼容
- frames[frameName] = {
- frame: {
- x: x,
- y: y,
- w: width,
- h: height
- },
- rotated: false,
- trimmed: false,
- spriteSourceSize: { x: 0, y: 0, w: width, h: height },
- sourceSize: { w: width, h: height }
- };
- });
- const json = {
- frames: frames,
- meta: {
- app: "SpriteSheetMaker for Cocos Creator 3.8",
- version: "1.0",
- image: `${folderName}.png`,
- format: "RGBA8888",
- size: { w: sheetWidth, h: sheetHeight },
- scale: "1"
- }
- };
- return JSON.stringify(json, null, 2);
- }
- // 将 Blob 转换为 Base64
- function blobToBase64(blob) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onloadend = () => {
- // 移除 data:image/png;base64, 前缀
- const base64 = reader.result.split(',')[1];
- resolve(base64);
- };
- reader.onerror = reject;
- reader.readAsDataURL(blob);
- });
- }
- // 发送数据到服务器打包并下载
- async function packAndDownload(folderName, imageBlob, jsonData, loading) {
- try {
- loading.updateStatus('转换图片数据...');
- // 将图片转换为 Base64
- const imageBase64 = await blobToBase64(imageBlob);
-
- loading.updateStatus('发送到服务器打包...');
- // 发送到服务器打包
- const response = await fetch('http://localhost:3000/api/pack', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- folderName: folderName,
- imageData: imageBase64,
- jsonData: jsonData
- })
- });
- if (!response.ok) {
- const errorMessage = await handleServerError(response, '打包失败');
- throw new Error(errorMessage);
- }
- loading.updateStatus('下载 ZIP 文件...');
- // 获取 ZIP 文件的 Blob
- const zipBlob = await response.blob();
-
- // 下载 ZIP 文件
- downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
-
- hideLoadingModal();
- } catch (error) {
- hideLoadingModal();
- console.error('打包失败:', error);
- showAlert(`打包失败: ${error.message}`);
- }
- }
- // 拼接图片
- async function packImages(folderName, frameNumbers) {
- const loading = showLoadingModal(folderName);
- try {
- // 1. 加载所有图片
- loading.updateStatus(`加载图片中... (0/${frameNumbers.length})`);
- const images = [];
-
- // 获取用户名
- const username = getCurrentUsername();
- if (!username) {
- throw new Error('请先登录');
- }
-
- for (let i = 0; i < frameNumbers.length; i++) {
- const frameNum = frameNumbers[i];
- const frameName = frameNum.toString().padStart(2, '0');
- // 使用API路径,从用户目录加载
- const imagePath = `${folderName}/${frameName}.png`;
- const imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
-
- loading.updateStatus(`加载图片中... (${i + 1}/${frameNumbers.length})`);
- const img = await loadImage(imgSrc);
- images.push({
- img: img,
- width: img.width,
- height: img.height,
- frameNum: frameNum
- });
- }
- // 2. 计算最优布局(尽可能接近正方形)
- loading.updateStatus('计算布局中...');
-
- // 计算所有图片的平均尺寸和总面积
- let totalArea = 0;
- let maxImageWidth = 0;
- let maxImageHeight = 0;
- images.forEach((item) => {
- totalArea += item.width * item.height;
- maxImageWidth = Math.max(maxImageWidth, item.width);
- maxImageHeight = Math.max(maxImageHeight, item.height);
- });
-
- // 估算目标尺寸(接近正方形)
- const estimatedSide = Math.ceil(Math.sqrt(totalArea));
- const estimatedCols = Math.ceil(estimatedSide / maxImageWidth);
-
- // 尝试不同的列数,找到长宽差最小的方案
- let bestLayout = null;
- let bestDiff = Infinity;
-
- // 缩小搜索范围以提高效率,但确保覆盖合理的范围
- let maxCols = Math.min(images.length, Math.ceil(estimatedCols * 1.5));
- let minCols = Math.max(1, Math.floor(estimatedCols * 0.7));
-
- // 确保minCols不超过maxCols,并且至少尝试几种列数
- if (minCols > maxCols) {
- minCols = Math.max(1, Math.floor(maxCols * 0.5));
- }
-
- // 对于图片数量很少的情况,确保尝试所有可能的列数
- if (images.length <= 10) {
- minCols = 1;
- maxCols = images.length;
- }
-
- for (let cols = minCols; cols <= maxCols; cols++) {
- const rows = Math.ceil(images.length / cols);
- const layout = [];
-
- // 计算每行的宽度和高度
- const rowWidths = new Array(rows).fill(0);
- const rowHeights = new Array(rows).fill(0);
-
- images.forEach((item, index) => {
- const row = Math.floor(index / cols);
- const col = index % cols;
-
- rowWidths[row] += item.width;
- rowHeights[row] = Math.max(rowHeights[row], item.height);
- });
-
- // 计算总尺寸(实际尺寸)
- const totalWidth = Math.max(...rowWidths);
- const totalHeight = rowHeights.reduce((sum, h) => sum + h, 0);
-
- // 计算最终尺寸(根据配置决定是否使用 2 的幂次方)
- const finalSize = calculateFinalSize(totalWidth, totalHeight);
- const sheetWidth = finalSize.width;
- const sheetHeight = finalSize.height;
-
- // 计算长宽差的绝对值(使用实际尺寸比较,以便找到最接近正方形的布局)
- const diff = Math.abs(totalWidth - totalHeight);
-
- // 如果这个方案更好,保存它
- if (diff < bestDiff) {
- bestDiff = diff;
-
- // 生成完整的布局位置信息
- const currentLayout = [];
- let currentY = 0;
-
- for (let row = 0; row < rows; row++) {
- let currentX = 0;
- const rowHeight = rowHeights[row];
-
- for (let col = 0; col < cols; col++) {
- const index = row * cols + col;
- if (index >= images.length) break;
-
- const item = images[index];
- currentLayout.push({
- x: currentX,
- y: currentY,
- width: item.width,
- height: item.height,
- img: item.img,
- frameNum: item.frameNum
- });
-
- currentX += item.width;
- }
-
- currentY += rowHeight;
- }
-
- bestLayout = {
- layout: currentLayout,
- width: sheetWidth,
- height: sheetHeight
- };
- }
- }
-
- // 如果没找到最佳布局(理论上不应该发生),使用默认的水平布局作为后备
- if (!bestLayout) {
- let currentX = 0;
- let maxHeight = 0;
- const defaultLayout = [];
-
- images.forEach((item) => {
- defaultLayout.push({
- x: currentX,
- y: 0,
- width: item.width,
- height: item.height,
- img: item.img,
- frameNum: item.frameNum
- });
- currentX += item.width;
- maxHeight = Math.max(maxHeight, item.height);
- });
-
- const totalWidth = currentX;
- const totalHeight = maxHeight;
- const finalSize = calculateFinalSize(totalWidth, totalHeight);
- bestLayout = {
- layout: defaultLayout,
- width: finalSize.width,
- height: finalSize.height
- };
- }
-
- // 使用最佳布局
- const layout = bestLayout.layout;
- const sheetWidth = bestLayout.width;
- const sheetHeight = bestLayout.height;
- // 4. 创建 Canvas 并绘制
- loading.updateStatus('拼接图片中...');
- const canvas = document.createElement('canvas');
- canvas.width = sheetWidth;
- canvas.height = sheetHeight;
- const ctx = canvas.getContext('2d');
- // 填充透明背景
- ctx.clearRect(0, 0, sheetWidth, sheetHeight);
- // 绘制所有图片
- layout.forEach((item) => {
- ctx.drawImage(item.img, item.x, item.y);
- });
- // 5. 生成 JSON 文件
- loading.updateStatus('生成 JSON 文件...');
- const jsonData = generateJSON(folderName, layout, sheetWidth, sheetHeight);
- // 6. 打包并下载(使用服务器端打包)
- loading.updateStatus('准备打包...');
-
- // 将图片转换为 blob,然后发送到服务器打包
- canvas.toBlob(async (imageBlob) => {
- await packAndDownload(folderName, imageBlob, jsonData, loading);
- }, 'image/png');
- } catch (error) {
- hideLoadingModal();
- showAlert(`生成失败: ${error.message}`);
- }
- }
- // 处理卡片下载按钮点击事件
- async function handleDownloadClick(folderName, index) {
- if (!folderName) {
- return;
- }
- try {
- // 获取帧列表
- const encodedFolderName = encodeURIComponent(folderName);
- const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
- if (!response.ok) {
- const errorMessage = await handleServerError(response, '无法获取帧列表');
- throw new Error(errorMessage);
- }
- const data = await response.json();
- const frameNumbers = data.frames || [];
- if (frameNumbers.length === 0) {
- showAlert('该文件夹中没有图片');
- return;
- }
- // 开始拼接
- await packImages(folderName, frameNumbers);
- } catch (error) {
- showAlert(`下载失败: ${error.message}`);
- }
- }
- // 导出到全局
- window.SpriteSheetMaker = {
- handleDownloadClick: handleDownloadClick
- };
- })();
|