|
@@ -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>
|