Browse Source

Merge branch 'master' of http://git.fmode.cn:3000/0235656/cloth-design

0235625 2 days ago
parent
commit
7dedafc959

BIN
cloth-design/public/cloth/body.png


BIN
cloth-design/public/cloth/cloth.png


BIN
cloth-design/public/cloth/head.png


BIN
cloth-design/public/cloth/left.png


BIN
cloth-design/public/cloth/right.png


+ 743 - 0
cloth-design/public/diy.html

@@ -0,0 +1,743 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>羽绒布颜色更换系统</title>
+    <style>
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            max-width: 1000px;
+            margin: 0 auto;
+            padding: 20px;
+            background-color: #f8f9fa;
+            color: #333;
+        }
+
+        h1 {
+            text-align: center;
+            margin-bottom: 30px;
+            color: #2c3e50;
+        }
+
+      .part-selector {
+            display: flex;
+            justify-content: center;
+            gap: 10px;
+            margin-bottom: 20px;
+            flex-wrap: wrap;
+        }
+
+      .part-btn {
+            padding: 6px 15px;
+            background: white;
+            border: 1px solid #ddd;
+            border-radius: 20px;
+            cursor: pointer;
+            transition: all 0.2s;
+            font-size: 14px;
+        }
+
+      .part-btn.active {
+            background: #3498db;
+            color: white;
+            border-color: #3498db;
+        }
+
+      .color-options {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 15px;
+            justify-content: center;
+            margin-bottom: 30px;
+            padding: 15px;
+            background-color: white;
+            border-radius: 10px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+        }
+
+      .color-option {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 2px solid #eee;
+            transition: all 0.3s ease;
+            position: relative;
+            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+        }
+
+      .color-option:hover {
+            transform: scale(1.15);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+            z-index: 10;
+        }
+
+      .color-option.active {
+            border-color: #3498db;
+            box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
+        }
+
+      .color-name {
+            position: absolute;
+            bottom: -25px;
+            left: 50%;
+            transform: translateX(-50%);
+            white-space: nowrap;
+            font-size: 12px;
+            color: #7f8c8d;
+        }
+
+      .canvas-container {
+            display: flex;
+            justify-content: center;
+            position: relative;
+            margin-bottom: 30px;
+        }
+
+      .canvas-card {
+            position: absolute;
+            background: transparent;
+            border-radius: 10px;
+            overflow: hidden;
+            box-shadow: none;
+            width: 400px;
+            height: 400px;
+        }
+
+      .canvas-header {
+            padding: 12px 20px;
+            background: #3498db;
+            color: white;
+            font-weight: bold;
+            text-align: center;
+        }
+
+        canvas {
+            display: block;
+            max-width: 100%;
+        }
+
+      .controls {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 20px;
+            background: white;
+            padding: 20px;
+            border-radius: 10px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+            margin-top: 20px;
+        }
+
+      .control-group {
+            flex: 1;
+            min-width: 250px;
+        }
+
+      .control-group h3 {
+            margin-top: 0;
+            color: #2c3e50;
+            font-size: 16px;
+            border-bottom: 1px solid #eee;
+            padding-bottom: 8px;
+        }
+
+      .slider-container {
+            margin: 15px 0;
+        }
+
+        label {
+            display: block;
+            margin-bottom: 6px;
+            font-weight: 500;
+            color: #34495e;
+        }
+
+        input[type="range"] {
+            width: 100%;
+        }
+
+      .value-display {
+            display: inline-block;
+            width: 40px;
+            text-align: right;
+            font-family: monospace;
+        }
+
+      .status-bar {
+            text-align: center;
+            padding: 15px;
+            font-size: 14px;
+            color: #7f8c8d;
+        }
+
+      .algorithm-info {
+            background: #e3f2fd;
+            padding: 15px;
+            border-radius: 8px;
+            margin-top: 20px;
+            font-size: 14px;
+        }
+
+      .algorithm-info h3 {
+            margin-top: 0;
+            color: #0d47a1;
+        }
+
+      .part-indicator {
+            text-align: center;
+            margin: 5px 0 15px;
+            font-weight: bold;
+            color: #3498db;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>羽绒布颜色更换系统</h1>
+
+    <div class="part-selector" id="partSelector">
+        <button class="part-btn" data-part="all">整体</button>
+        <button class="part-btn" data-part="left">左袖</button>
+        <button class="part-btn" data-part="right">右袖</button>
+        <button class="part-btn active" data-part="body">身体</button>
+        <button class="part-btn" data-part="head">帽子</button>
+    </div>
+
+    <div class="part-indicator" id="partIndicator">当前编辑: 整体</div>
+
+    <div class="color-options" id="colorOptions"></div>
+
+    <div class="canvas-container" id="canvasContainer">
+        <!-- 整体部位的canvas -->
+        <div class="canvas-card" id="allCanvasCard">
+            <div class="canvas-header">整体</div>
+            <canvas id="allCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 身体部位的canvas -->
+        <div class="canvas-card" id="bodyCanvasCard">
+            <div class="canvas-header">身体</div>
+            <canvas id="bodyCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 左袖部位的canvas -->
+        <div class="canvas-card" id="leftCanvasCard">
+            <div class="canvas-header">左袖</div>
+            <canvas id="leftCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 右袖部位的canvas -->
+        <div class="canvas-card" id="rightCanvasCard">
+            <div class="canvas-header">右袖</div>
+            <canvas id="rightCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 帽子部位的canvas -->
+        <div class="canvas-card" id="headCanvasCard">
+            <div class="canvas-header">帽子</div>
+            <canvas id="headCanvas" width="750" height="650"></canvas>
+        </div>
+    </div>
+
+    <div class="status-bar" id="statusBar">正在加载基础图片...</div>
+
+    <div class="controls">
+        <div class="control-group">
+            <h3>色彩调整</h3>
+            <div class="slider-container">
+                <label>色相旋转: <span id="hueValue" class="value-display">0°</span></label>
+                                <input type="range" id="hueRotate" min="-180" max="180" value="0" step="1">
+            </div>
+            <div class="slider-container">
+                <label>饱和度: <span id="saturationValue" class="value-display">100%</span></label>
+                <input type="range" id="saturation" min="0" max="200" value="100" step="1">
+            </div>
+            <div class="slider-container">
+                <label>亮度: <span id="brightnessValue" class="value-display">100%</span></label>
+                <input type="range" id="brightness" min="50" max="150" value="100" step="1">
+            </div>
+        </div>
+
+        <div class="control-group">
+            <h3>高级控制</h3>
+            <div class="slider-container">
+                <label>颜色强度: <span id="intensityValue" class="value-display">100%</span></label>
+                <input type="range" id="intensity" min="0" max="100" value="100" step="1">
+            </div>
+            <div class="slider-container">
+                <label>阴影保留: <span id="shadowValue" class="value-display">80%</span></label>
+                <input type="range" id="shadowPreserve" min="0" max="100" value="80" step="1">
+            </div>
+            <button id="resetBtn" style="margin-top: 20px; padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">重置调整参数</button>
+        </div>
+    </div>
+
+    <script>
+        // 颜色映射表
+        const colorMap = {
+            '宝蓝色': '#0033A0',
+            '浅驼色': '#D2B48C',
+            '测试色': '#FF0000',
+            '深灰绿': '#5E716A',
+            '深紫色': '#36013F',
+            '深绿色': '#013220',
+            '真朱': '#B3424A',
+            '睿智金': '#D4AF37',
+            '红色': '#FF0000',
+            '萌萌绿': '#A5D152',
+            '蒂芙尼蓝': '#81D8D0',
+            '长春花蓝': '#6667AB',
+            '青紫': '#6A0DAD',
+            '靓丽黄': '#FFD700'
+        };
+
+        // 部位信息(定义渲染顺序,确保正确叠加)
+        const parts = {
+            all: { name: '整体', image: '' }, // 整体不需要实际图片
+            body: { name: '身体', image: 'body.png' },
+            left: { name: '左袖', image: 'left.png' },
+            right: { name: '右袖', image: 'right.png' },
+            head: { name: '帽子', image: 'head.png' }
+        };
+
+        // 存储各部位的颜色设置
+        let partColors = {
+            all: colorMap['深灰绿'],
+            left: colorMap['深灰绿'],
+            right: colorMap['深灰绿'],
+            body: colorMap['深灰绿'],
+            head: colorMap['深灰绿']
+        };
+
+        // 存储各部位的图像数据
+        let partImageData = {};
+        let originalPartImageData = {}; // 存储原始图像数据
+
+        // 当前选中的部位
+        let currentPart = 'all';
+
+        // 初始化颜色选择器
+        const colorOptions = document.getElementById('colorOptions');
+        Object.entries(colorMap).forEach(([name, hex]) => {
+            const option = document.createElement('div');
+            option.className = 'color-option';
+            option.title = name;
+            option.style.backgroundColor = hex;
+            option.dataset.name = name;
+            option.dataset.hex = hex;
+
+            const nameSpan = document.createElement('span');
+            nameSpan.className = 'color-name';
+            nameSpan.textContent = name;
+
+            option.appendChild(nameSpan);
+            option.addEventListener('click', () => {
+                setPartColor(hex, name);
+            });
+
+            colorOptions.appendChild(option);
+        });
+
+        // 初始化部位选择器
+        const partSelector = document.getElementById('partSelector');
+        const partIndicator = document.getElementById('partIndicator');
+
+        partSelector.addEventListener('click', (e) => {
+            if (e.target.classList.contains('part-btn')) {
+                document.querySelectorAll('.part-btn').forEach(btn => {
+                    btn.classList.remove('active');
+                });
+                e.target.classList.add('active');
+
+                currentPart = e.target.dataset.part;
+                partIndicator.textContent = `当前编辑: ${parts[currentPart].name}`;
+
+                updateColorSelector();
+                updateAdjustmentDisplay();
+                renderAllParts(); // 切换部位时重新渲染所有
+            }
+        });
+
+        // 获取各部位的Canvas元素
+        const allCanvas = document.getElementById('allCanvas');
+        const bodyCanvas = document.getElementById('bodyCanvas');
+        const leftCanvas = document.getElementById('leftCanvas');
+        const rightCanvas = document.getElementById('rightCanvas');
+        const headCanvas = document.getElementById('headCanvas');
+        const originalCanvas = document.createElement('canvas');
+        const colorizedCanvas = document.createElement('canvas');
+        const originalCtx = originalCanvas.getContext('2d');
+        const colorizedCtx = colorizedCanvas.getContext('2d');
+        const statusBar = document.getElementById('statusBar');
+
+        // 控制元素
+        const hueRotate = document.getElementById('hueRotate');
+        const saturation = document.getElementById('saturation');
+        const brightness = document.getElementById('brightness');
+        const intensity = document.getElementById('intensity');
+        const shadowPreserve = document.getElementById('shadowPreserve');
+        const resetBtn = document.getElementById('resetBtn');
+
+        // 值显示元素
+        const hueValue = document.getElementById('hueValue');
+        const saturationValue = document.getElementById('saturationValue');
+        const brightnessValue = document.getElementById('brightnessValue');
+        const intensityValue = document.getElementById('intensityValue');
+        const shadowValue = document.getElementById('shadowValue');
+
+        // 存储各部位的调整参数
+        let partAdjustments = {
+            all: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            left: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            right: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            body: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            head: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 }
+        };
+
+        // 加载所有图像
+        function loadAllImages() {
+            let loadedCount = 0;
+            const totalImages = Object.keys(parts).length - 1; // 减去all
+
+            // 按渲染顺序加载各部位图像
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            renderOrder.forEach(part => {
+                const img = new Image();
+                img.crossOrigin = "Anonymous";
+                img.src = `./cloth/${parts[part].image}`;
+
+                img.onload = function () {
+                    // 处理着色画布
+                    const tempCanvas = document.createElement('canvas');
+                    tempCanvas.width = img.width;
+                    tempCanvas.height = img.height;
+                    const tempCtx = tempCanvas.getContext('2d');
+                    tempCtx.drawImage(img, 0, 0);
+                    partImageData[part] = tempCtx.getImageData(0, 0, img.width, img.height);
+
+                    // 处理原始画布
+                    const origTempCanvas = document.createElement('canvas');
+                    origTempCanvas.width = img.width;
+                    origTempCanvas.height = img.height;
+                    const origTempCtx = origTempCanvas.getContext('2d');
+                    origTempCtx.drawImage(img, 0, 0);
+                    originalPartImageData[part] = origTempCtx.getImageData(0, 0, img.width, img.height);
+
+                    loadedCount++;
+                    updateLoadStatus(loadedCount, totalImages);
+
+                    // 如果所有图像加载完成,设置画布大小并渲染
+                    if (loadedCount === totalImages) {
+                        // 使用第一个图像的大小作为基准
+                        const firstPart = renderOrder[0];
+                        originalCanvas.width = partImageData[firstPart].width;
+                        originalCanvas.height = partImageData[firstPart].height;
+                        colorizedCanvas.width = partImageData[firstPart].width;
+                        colorizedCanvas.height = partImageData[firstPart].height;
+
+                        // 设置所有画布大小一致
+                        [allCanvas, bodyCanvas, leftCanvas, rightCanvas, headCanvas].forEach(canvas => {
+                            canvas.width = partImageData[firstPart].width;
+                            canvas.height = partImageData[firstPart].height;
+                        });
+
+                        renderOriginalParts();
+                        renderAllParts();
+                    }
+                };
+
+                img.onerror = function () {
+                    console.error(`加载${parts[part].name}失败:./cloth/${parts[part].image}`);
+                    statusBar.textContent = `错误: 加载${parts[part].name}图像失败`;
+                };
+            });
+        }
+
+        // 更新加载状态
+        function updateLoadStatus(loaded, total) {
+            statusBar.textContent = `正在加载资源: ${loaded}/${total}`;
+            if (loaded === total) {
+                statusBar.textContent = '资源加载完成!可以开始编辑颜色';
+                updateColorSelector();
+            }
+        }
+
+        // 设置部位颜色
+        function setPartColor(hex, name) {
+            if (currentPart === 'all') {
+                // 如果是整体模式,更新所有部位的颜色
+                ['left', 'right', 'body', 'head'].forEach(part => {
+                    partColors[part] = hex;
+                });
+            } else {
+                // 否则只更新当前部位
+                partColors[currentPart] = hex;
+            }
+            
+            updateColorSelector();
+            renderAllParts();
+            
+            if (currentPart === 'all') {
+                statusBar.textContent = `所有部位已设置为: ${name}`;
+            } else {
+                statusBar.textContent = `${parts[currentPart].name}已设置为: ${name}`;
+            }
+        }
+
+        // 更新颜色选择器状态
+        function updateColorSelector() {
+            const currentColor = partColors[currentPart];
+            document.querySelectorAll('.color-option').forEach(option => {
+                option.classList.toggle('active', option.dataset.hex === currentColor);
+            });
+        }
+
+        // 更新调整参数显示
+        function updateAdjustmentDisplay() {
+            const adjustments = partAdjustments[currentPart];
+            hueRotate.value = adjustments.hue;
+            saturation.value = adjustments.saturation;
+            brightness.value = adjustments.brightness;
+            intensity.value = adjustments.intensity;
+            shadowPreserve.value = adjustments.shadowPreserve;
+
+            hueValue.textContent = `${adjustments.hue}°`;
+            saturationValue.textContent = `${adjustments.saturation}%`;
+            brightnessValue.textContent = `${adjustments.brightness}%`;
+            intensityValue.textContent = `${adjustments.intensity}%`;
+            shadowValue.textContent = `${adjustments.shadowPreserve}%`;
+        }
+
+        // 渲染原始部位(右侧画布)
+        function renderOriginalParts() {
+            originalCtx.clearRect(0, 0, originalCanvas.width, originalCanvas.height);
+
+            // 按正确顺序渲染所有部位(从下到上)
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            renderOrder.forEach(part => {
+                if (originalPartImageData[part]) {
+                    originalCtx.putImageData(originalPartImageData[part], 0, 0);
+                }
+            });
+        }
+
+        // 渲染所有着色部位(左侧画布)
+        function renderAllParts() {
+            // 按正确顺序渲染所有部位(从下到上)
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            
+            // 清除所有画布
+            [allCanvas, bodyCanvas, leftCanvas, rightCanvas, headCanvas].forEach(canvas => {
+                const ctx = canvas.getContext('2d');
+                ctx.clearRect(0, 0, canvas.width, canvas.height);
+            });
+            
+            // 分别渲染每个部位
+            renderOrder.forEach(part => {
+                if (partImageData[part]) {
+                    renderPart(part);
+                }
+            });
+            
+            // 最后渲染整体效果
+            renderAllCombined();
+        }
+
+        // 渲染单个部位
+        function renderPart(part) {
+            const canvas = document.getElementById(`${part}Canvas`);
+            const ctx = canvas.getContext('2d');
+            
+            // 复制原始图像数据
+            const imageData = new ImageData(
+                new Uint8ClampedArray(originalPartImageData[part].data),
+                originalPartImageData[part].width,
+                originalPartImageData[part].height
+            );
+
+            const targetColor = partColors[part];
+            const adjustments = partAdjustments[part];
+            const targetRgb = hexToRgb(targetColor);
+            const baseRgb = { r: 128, g: 128, b: 128 }; // 基础灰色
+
+            // 颜色比率
+            const rRatio = targetRgb.r / baseRgb.r;
+            const gRatio = targetRgb.g / baseRgb.g;
+            const bRatio = targetRgb.b / baseRgb.b;
+
+            // 调整参数
+            const hueAngle = adjustments.hue;
+            const satFactor = adjustments.saturation / 100;
+            const brightFactor = adjustments.brightness / 100;
+            const intensityFactor = adjustments.intensity / 100;
+            const shadowPreserveFactor = adjustments.shadowPreserve / 100;
+
+            // 处理每个像素
+            const data = imageData.data;
+            for (let i = 0; i < data.length; i += 4) {
+                const sourceAlpha = data[i + 3];
+                if (sourceAlpha === 0) continue; // 跳过透明像素
+
+                const r = data[i];
+                const g = data[i + 1];
+                const b = data[i + 2];
+                const gray = 0.299 * r + 0.587 * g + 0.114 * b; // 灰度值
+
+                // 应用颜色比率
+                let newR = r * rRatio;
+                let newG = g * gRatio;
+                let newB = b * bRatio;
+
+                // 限制范围
+                newR = Math.min(255, Math.max(0, newR));
+                newG = Math.min(255, Math.max(0, newG));
+                newB = Math.min(255, Math.max(0, newB));
+
+                // 颜色强度混合
+                newR = newR * intensityFactor + r * (1 - intensityFactor);
+                newG = newG * intensityFactor + g * (1 - intensityFactor);
+                newB = newB * intensityFactor + b * (1 - intensityFactor);
+
+                // 亮度调整(保留阴影)
+                if (gray < 30) {
+                    // 阴影区域少调整
+                    newR *= 0.3 + 0.7 * shadowPreserveFactor;
+                    newG *= 0.3 + 0.7 * shadowPreserveFactor;
+                    newB *= 0.3 + 0.7 * shadowPreserveFactor;
+                } else {
+                    newR *= brightFactor;
+                    newG *= brightFactor;
+                    newB *= brightFactor;
+                }
+
+                // HSV调整
+                let hsv = rgbToHsv(newR, newG, newB);
+                hsv.h = (hsv.h + hueAngle) % 360;
+                if (hsv.h < 0) hsv.h += 360;
+                hsv.s = Math.min(1, Math.max(0, hsv.s * satFactor));
+
+                const adjustedRgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
+
+                // 设置像素值
+                data[i] = Math.min(255, Math.max(0, adjustedRgb.r));
+                data[i + 1] = Math.min(255, Math.max(0, adjustedRgb.g));
+                data[i + 2] = Math.min(255, Math.max(0, adjustedRgb.b));
+                data[i + 3] = sourceAlpha; // 保持原有的Alpha值
+            }
+
+            // 绘制到画布
+            ctx.putImageData(imageData, 0, 0);
+        }
+
+        // 渲染整体效果
+        function renderAllCombined() {
+            const ctx = allCanvas.getContext('2d');
+            ctx.clearRect(0, 0, allCanvas.width, allCanvas.height);
+            
+            // 按顺序绘制各部位
+            ['body', 'left', 'right', 'head'].forEach(part => {
+                const partCanvas = document.getElementById(`${part}Canvas`);
+                ctx.drawImage(partCanvas, 0, 0);
+            });
+        }
+
+        // 控制事件
+        hueRotate.addEventListener('input', () => {
+            partAdjustments[currentPart].hue = parseInt(hueRotate.value);
+            hueValue.textContent = `${hueRotate.value}°`;
+            renderAllParts();
+        });
+
+        saturation.addEventListener('input', () => {
+            partAdjustments[currentPart].saturation = parseInt(saturation.value);
+            saturationValue.textContent = `${saturation.value}%`;
+            renderAllParts();
+        });
+
+        brightness.addEventListener('input', () => {
+            partAdjustments[currentPart].brightness = parseInt(brightness.value);
+            brightnessValue.textContent = `${brightness.value}%`;
+            renderAllParts();
+        });
+
+        intensity.addEventListener('input', () => {
+            partAdjustments[currentPart].intensity = parseInt(intensity.value);
+            intensityValue.textContent = `${intensity.value}%`;
+            renderAllParts();
+        });
+
+        shadowPreserve.addEventListener('input', () => {
+            partAdjustments[currentPart].shadowPreserve = parseInt(shadowPreserve.value);
+            shadowValue.textContent = `${shadowPreserve.value}%`;
+            renderAllParts();
+        });
+
+        resetBtn.addEventListener('click', () => {
+            partAdjustments[currentPart] = {
+                hue: 0,
+                saturation: 100,
+                brightness: 100,
+                intensity: 100,
+                shadowPreserve: 80
+            };
+            updateAdjustmentDisplay();
+            renderAllParts();
+        });
+
+        // 工具函数:HEX转RGB
+        function hexToRgb(hex) {
+            if (hex.length === 4) {
+                hex = hex.replace(/#([a-f\d])([a-f\d])([a-f\d])/i, (m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`);
+            }
+            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+            return result? {
+                r: parseInt(result[1], 16),
+                g: parseInt(result[2], 16),
+                b: parseInt(result[3], 16)
+            } : { r: 0, g: 0, b: 0 };
+        }
+
+        // RGB转HSV
+        function rgbToHsv(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 = 0, s = 0, v = max;
+            const d = max - min;
+            s = max? d / max : 0;
+
+            if (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 *= 60;
+            }
+            return { h, s, v };
+        }
+
+        // HSV转RGB
+        function hsvToRgb(h, s, v) {
+            let r, g, b;
+            const i = Math.floor(h / 60);
+            const f = h / 60 - i;
+            const p = v * (1 - s);
+            const q = v * (1 - f * s);
+            const t = v * (1 - (1 - f) * s);
+
+            switch (i % 6) {
+                case 0: r = v, g = t, b = p; break;
+                case 1: r = q, g = v, b = p; break;
+                case 2: r = p, g = v, b = t; break;
+                case 3: r = p, g = q, b = v; break;
+                case 4: r = t, g = p, b = v; break;
+                case 5: r = v, g = p, b = q; break;
+            }
+            return { r: r * 255, g: g * 255, b: b * 255 };
+        }
+
+        // 初始化加载
+        loadAllImages();
+    </script>
+</body>
+
+</html>

+ 170 - 0
cloth-design/src/lib/completion.ts

@@ -0,0 +1,170 @@
+
+export interface TestMessage{
+    role:string
+    content:string
+}
+
+export class TestCompletion{
+    token:string = "r:60abef69e7cd8181b146ceaba1fdbf02"
+    messageList:any = []
+    stream:boolean = true;
+    constructor(messageList:any){
+        this.messageList = messageList || this.messageList
+    }
+    async sendMessage(messageList?:null|Array<TestMessage>,onMessage?: (content: string) => void):Promise<any>{
+        
+        this.messageList = messageList || this.messageList
+        let body = {
+            "messages": this.messageList,
+            "stream": this.stream,
+            "model": "fmode-4.5-128k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "token": "Bearer "+this.token
+        }
+
+        let response = await fetch("https://server.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+            },
+            "body": JSON.stringify(body),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+
+        
+        /** 单次响应 HTTP短连接请求
+         {"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"您好!我是一个人工智能助手,旨在帮助您回答问题、提供信息和解决各种问题。我可以处理许多主题,包括科技、历史、文化、语言学习等。如果您有任何具体的问题或需要了解的内容,请随时告诉我!","refusal":null,"role":"assistant"}}],"created":1751509370,"id":"chatcmpl-Bp3t41MP4pb2NR38n1ylrJw922SBZ","model":"gpt-4o-mini-2024-07-18","object":"chat.completion","system_fingerprint":"fp_efad92c60b","usage":{"completion_tokens":55,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":15,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":70}}
+        */
+        if(this.stream == false){
+            let data = await response.json()
+            console.log(data)
+            let lastContent = data?.choices?.[0]?.message?.content
+            return lastContent
+        }
+        /**
+         * 流式加载 HTTP Event Stream 模式 长连接获取
+         */
+        
+        // Stream mode handling
+        if (!response.body) {
+            throw new Error("No response body in stream mode");
+        }
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder("utf-8");
+        let accumulatedContent = "";
+        try {
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done) break;
+
+                const chunk = decoder.decode(value, { stream: true });
+                const lines = chunk.split('\n').filter(line => line.trim() !== '');
+
+                for (const line of lines) {
+                    if (line.startsWith('data:') && !line.includes('[DONE]')) {
+                        try {
+                            const jsonStr = line.substring(5).trim();
+                            const data = JSON.parse(jsonStr);
+                            const content = data?.choices?.[0]?.delta?.content || '';
+                            
+                            if (content) {
+                                accumulatedContent += content;
+                                if (onMessage) {
+                                    onMessage(accumulatedContent);
+                                }
+                            }
+                        } catch (e) {
+                            console.error("Error parsing stream data:", e);
+                        }
+                    }
+                }
+            }
+        } finally {
+            reader.releaseLock();
+        }
+
+        return accumulatedContent;
+    }
+}
+
+/**
+ * 使用AI生成符合指定结构的JSON数据
+ * @param prompt 任务要求的整体提示词
+ * @param jsonSchema 期望的JSON结构描述(用于提示词)
+ * @param example 可选的JSON示例(帮助AI理解格式)
+ * @param onMessage 实时生成内容回调
+ * @returns 解析后的JSON对象
+ */
+export async function completionJSON(
+    prompt:string,
+  jsonSchema: string,
+  example: object | null = null,
+  onMessage?: (content: string) => void
+): Promise<any> {
+  // 1. 构建提示词
+  const JsonResultParsePrompt = `请严格按照以下要求生成JSON数据:
+- 数据结构要求:${jsonSchema}
+- 只能返回一个完整的JSON对象 确保JSON格式正确
+- 注意返回的JSON格式每个KEY都有""包裹和Value之间都有:`;
+// ${example ? `2. 参考示例格式:\n${JSON.stringify(example, null, 2)}` : ''}
+
+  // 2. 初始化消息列表
+  const messages: TestMessage[] = [
+    {
+      role: "user",
+      content: prompt+JsonResultParsePrompt
+    }
+  ];
+
+  // 3. 创建TestCompletion实例
+  const completion = new TestCompletion(messages);
+  
+  // 4. 存储累积内容和JSON对象
+  let fullContent = "";
+  let jsonObject: any = null;
+  
+  // 5. 发送请求并处理流式响应
+  const result = await completion.sendMessage(null, (content) => {
+    fullContent = content;
+  });
+  console.log("fullContent",fullContent)
+  // 6. 最终JSON提取(确保获取完整内容)
+  try {
+    // 再次尝试从完整内容中提取JSON
+      jsonObject = extractJSON(fullContent);
+  } catch (e) {
+    console.error("JSON解析失败:", e);
+    console.log("原始内容:", fullContent);
+    throw new Error("生成的响应不符合JSON格式");
+  }
+  
+  return jsonObject;
+}
+
+function extractJSON(str:string){
+  let stack = 0;
+  let startIndex = -1;
+  let result = null;
+
+  for (let i = 0; i < str.length; i++) {
+    if (str[i] === '{') {
+      if (stack === 0) startIndex = i;
+      stack++;
+    } else if (str[i] === '}') {
+      stack--;
+      if (stack === 0 && startIndex !== -1) {
+        try {
+          result = JSON.parse(str.slice(startIndex, i + 1));
+          break;
+        } catch (e) {
+          // 继续尝试下一个可能的 JSON
+          startIndex = -1;
+        }
+      }
+    }
+  }
+  return result;
+};