西部建筑 · 夜景

为西部建筑绘制灯光方案,古都城墙下的大漠光影

📍 选择地区

🌸 默认季节:春季

⏱ 模拟时间

城墙照明

沿古城墙轮廓布置灯带,再现西安城墙夜间的恢弘气势

民族纹理

投射灯突出伊斯兰纹样与民族装饰细节,展现西部建筑独特美感

街巷氛围

回民街等历史街区的暖色调氛围照明,营造烟火气与历史感交融的夜景

let canvas, ctx; let originalImage = null; let originalImageSrc = null; let currentImageSrc = null; let currentTool = null; let isDrawing = false; let hasMask = false; let startX = 0, startY = 0; let currentPath = []; let drawnObjects = []; let historyStack = []; let isCtrlPressed = false; let lastClickTime = 0; let selectedObjectIndex = -1; let isDragging = false; let dragOffsetX = 0, dragOffsetY = 0; let currentTaskId = null; let pollInterval = null; let lightSettings = { linear: { brightness: 53, colorTemp: 4500 }, point: { brightness: 53, colorTemp: 4500 }, polygon: { brightness: 53, colorTemp: 4500 }, block: { brightness: 53, colorTemp: 4500 }, halfcircle: { brightness: 53, colorTemp: 4500 }, spotlight: { brightness: 53, colorTemp: 4500 }, fire: { brightness: 70, colorTemp: 2000 } }; let smoothnessValue = 80; let moonlightValue = 40; document.addEventListener('DOMContentLoaded', function() { canvas = document.getElementById('imageCanvas'); ctx = canvas.getContext('2d'); importFromPrev(); document.getElementById('fileInput').addEventListener('change', function(e) { if (e.target.files && e.target.files[0]) { const reader = new FileReader(); reader.onload = function(event) { loadImage(event.target.result); }; reader.readAsDataURL(e.target.files[0]); } }); // 粘贴支持 document.addEventListener('paste', function(e) { if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return; let imgFile = null; if (e.clipboardData && e.clipboardData.items) { for (let i = 0; i < e.clipboardData.items.length; i++) { if (e.clipboardData.items[i].type.indexOf('image') !== -1) { imgFile = e.clipboardData.items[i].getAsFile(); if (imgFile) break; } } } if (!imgFile && e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) { for (let i = 0; i < e.clipboardData.files.length; i++) { if (e.clipboardData.files[i].type.indexOf('image') !== -1) { imgFile = e.clipboardData.files[i]; break; } } } if (imgFile) { e.preventDefault(); const reader = new FileReader(); reader.onload = function(event) { loadImage(event.target.result); }; reader.readAsDataURL(imgFile); } }); // 拖拽支持 const dropZone = document.getElementById("originalImageZone"); if (dropZone) { dropZone.addEventListener("dragover", function(e) { e.preventDefault(); dropZone.style.borderColor = "var(--gold)"; }); dropZone.addEventListener("dragleave", function(e) { e.preventDefault(); dropZone.style.borderColor = "var(--border-light)"; }); dropZone.addEventListener("drop", function(e) { e.preventDefault(); dropZone.style.borderColor = "var(--border-light)"; if (e.dataTransfer.files && e.dataTransfer.files[0]) { const reader = new FileReader(); reader.onload = function(event) { loadImage(event.target.result); }; reader.readAsDataURL(e.dataTransfer.files[0]); } }); } if (canvas) { canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseleave', stopDrawing); canvas.addEventListener('dblclick', handleDoubleClick); canvas.addEventListener('touchstart', handleTouchStart); canvas.addEventListener('touchmove', handleTouchMove); canvas.addEventListener('touchend', stopDrawing); } document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undo(); } if (e.key === 'Control') isCtrlPressed = true; if (e.key === 'Escape' && currentTool === 'polygon' && currentPath.length > 0) { currentPath = []; redrawCanvas(); } }); document.addEventListener('keyup', function(e) { if (e.key === 'Control') isCtrlPressed = false; }); }); function handleDoubleClick(e) { if (currentTool === 'polygon' && currentPath.length >= 3) { saveToHistory(); drawnObjects.push({ type: 'polygon', points: [...currentPath], smoothness: smoothnessValue / 100, brightness: (lightSettings.polygon?.brightness || 53) / 100, colorTemp: lightSettings.polygon?.colorTemp || 4500, scale: 1, rotation: 0 }); currentPath = []; redrawCanvas(); } } function saveToHistory() { historyStack.push(JSON.stringify(drawnObjects)); if (historyStack.length > 50) historyStack.shift(); } function undo() { if (historyStack.length > 0) { drawnObjects = JSON.parse(historyStack.pop()); selectedObjectIndex = -1; document.getElementById('transformControls').style.display = 'none'; redrawCanvas(); } } function handleTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const rect = canvas.getBoundingClientRect(); startDrawing(new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY })); } function handleTouchMove(e) { e.preventDefault(); const touch = e.touches[0]; const rect = canvas.getBoundingClientRect(); draw(new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY })); } function loadImage(src) { const zone = document.getElementById('originalImageZone'); const container = document.getElementById('canvasContainer'); const toolbar = document.getElementById('lightToolbar'); const smoothnessControl = document.getElementById('smoothnessControl'); const moonlightControl = document.getElementById('moonlightControl'); const controlsSection = document.getElementById('lightControlsSection'); const renderBtn = document.getElementById('renderBtn'); const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { originalImage = img; originalImageSrc = src; canvas.width = img.width; canvas.height = img.height; redrawCanvas(); if (zone) zone.style.display = 'none'; if (container) container.style.display = 'flex'; if (toolbar) toolbar.style.display = 'flex'; if (smoothnessControl) smoothnessControl.style.display = 'flex'; if (moonlightControl) moonlightControl.style.display = 'flex'; if (controlsSection) controlsSection.style.display = 'block'; if (renderBtn) renderBtn.style.display = 'inline-block'; currentImageSrc = src; localStorage.setItem('selectedLightingImage', src); }; img.src = src; } function redrawCanvas() { if (!originalImage) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height); if (hasMask) { ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } drawnObjects.forEach((obj, i) => { drawObject(obj); if (i === selectedObjectIndex) drawSelectionBox(obj); }); if (currentTool === 'polygon' && currentPath.length > 0) { ctx.save(); ctx.strokeStyle = 'rgba(255,200,100,0.8)'; ctx.fillStyle = 'rgba(255,250,200,0.4)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(currentPath[0].x, currentPath[0].y); for (let i = 1; i < currentPath.length; i++) ctx.lineTo(currentPath[i].x, currentPath[i].y); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#c9a96e'; currentPath.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill(); }); ctx.restore(); } } function drawSelectionBox(obj) { ctx.save(); ctx.strokeStyle = '#c9a96e'; ctx.lineWidth = 2; ctx.setLineDash([5,5]); let b = getObjectBounds(obj); ctx.strokeRect(b.x-5, b.y-5, b.width+10, b.height+10); ctx.setLineDash([]); ctx.fillStyle = '#c9a96e'; [{x:b.x-5,y:b.y-5},{x:b.x+b.width+5,y:b.y-5},{x:b.x-5,y:b.y+b.height+5},{x:b.x+b.width+5,y:b.y+b.height+5}].forEach(c => { ctx.fillRect(c.x-3, c.y-3, 6, 6); }); ctx.restore(); } function getObjectBounds(obj) { switch(obj.type) { case 'point': case 'halfcircle': case 'fire': const s = obj.radius || 30; return {x:obj.x-s, y:obj.y-s, width:s*2, height:s*2}; case 'polygon': if (obj.points && obj.points.length > 0) { let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity; obj.points.forEach(p => { minX=Math.min(minX,p.x); minY=Math.min(minY,p.y); maxX=Math.max(maxX,p.x); maxY=Math.max(maxY,p.y); }); return {x:minX, y:minY, width:maxX-minX, height:maxY-minY}; } return {x:obj.x, y:obj.y, width:50, height:50}; case 'block': return {x:obj.x, y:obj.y, width:obj.width, height:obj.height}; case 'spotlight': const ss = obj.radius || 30; return {x:obj.x-ss, y:obj.y-ss, width:ss*2, height:ss*4}; default: return {x:obj.x, y:obj.y, width:50, height:50}; } } function isPointInObject(x, y, obj) { let b = getObjectBounds(obj); return x >= b.x-10 && x <= b.x+b.width+10 && y >= b.y-10 && y <= b.y+b.height+10; } function drawObject(obj) { ctx.save(); if (obj.rotation || obj.scale) { const b = getObjectBounds(obj); const cx = b.x + b.width/2, cy = b.y + b.height/2; ctx.translate(cx, cy); ctx.rotate((obj.rotation||0)*Math.PI/180); ctx.scale(obj.scale||1, obj.scale||1); ctx.translate(-cx, -cy); } const brightness = obj.brightness || 0.53; switch(obj.type) { case 'linear': ctx.strokeStyle = `rgba(255,230,150,${0.6+brightness*0.4})`; ctx.lineWidth = obj.strokeWidth || 8; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); if (obj.path && obj.path.length > 1) { ctx.moveTo(obj.path[0].x, obj.path[0].y); for (let i=1;i= 3) { ctx.fillStyle = `rgba(255,250,200,${0.6+brightness*0.4})`; ctx.shadowColor = `rgba(255,230,150,${0.5+brightness*0.3})`; ctx.shadowBlur = 20*(1+brightness); ctx.beginPath(); ctx.moveTo(obj.points[0].x, obj.points[0].y); for (let i=1;i btn.classList.remove('active')); event.target.closest('.tool-btn').classList.add('active'); currentTool = tool; if (tool !== 'polygon' && currentPath.length > 0) { currentPath = []; redrawCanvas(); } if (tool !== 'select') { selectedObjectIndex = -1; document.getElementById('transformControls').style.display = 'none'; redrawCanvas(); } } function updateSmoothness() { smoothnessValue = parseInt(document.getElementById('smoothnessInput').value) || 0; if (smoothnessValue < 0) smoothnessValue = 0; if (smoothnessValue > 100) smoothnessValue = 100; document.getElementById('smoothnessInput').value = smoothnessValue; } function updateScale() { if (selectedObjectIndex >= 0) { const s = parseFloat(document.getElementById('scaleSlider').value); document.getElementById('scaleValue').textContent = s + 'x'; drawnObjects[selectedObjectIndex].scale = s; redrawCanvas(); } } function updateRotation() { if (selectedObjectIndex >= 0) { const r = parseInt(document.getElementById('rotateSlider').value); document.getElementById('rotateValue').textContent = r + '°'; drawnObjects[selectedObjectIndex].rotation = r; redrawCanvas(); } } function updateLightSetting(type, setting) { const id = type + setting.charAt(0).toUpperCase() + setting.slice(1); let v = parseInt(document.getElementById(id).value) || 0; if (setting === 'brightness') { if (v<0) v=0; if (v>100) v=100; } else if (setting === 'colorTemp') { const mn = type==='fire'?1500:2700, mx = type==='fire'?3000:6500; if (vmx) v=mx; } document.getElementById(id).value = v; lightSettings[type][setting] = v; } function getCanvasCoords(e) { const rect = canvas.getBoundingClientRect(); const sx = canvas.width / rect.width, sy = canvas.height / rect.height; return { x: (e.clientX - rect.left) * sx, y: (e.clientY - rect.top) * sy }; } function startDrawing(e) { const coords = getCanvasCoords(e); const now = Date.now(); if (now - lastClickTime < 300) { if (currentTool === 'polygon' && currentPath.length >= 3) { handleDoubleClick(e); return; } } lastClickTime = now; if (currentTool === 'select') { for (let i = drawnObjects.length-1; i >= 0; i--) { if (isPointInObject(coords.x, coords.y, drawnObjects[i])) { selectedObjectIndex = i; isDragging = true; const b = getObjectBounds(drawnObjects[i]); dragOffsetX = coords.x - (b.x+b.width/2); dragOffsetY = coords.y - (b.y+b.height/2); document.getElementById('scaleSlider').value = drawnObjects[i].scale || 1; document.getElementById('scaleValue').textContent = (drawnObjects[i].scale||1)+'x'; document.getElementById('rotateSlider').value = drawnObjects[i].rotation || 0; document.getElementById('rotateValue').textContent = (drawnObjects[i].rotation||0)+'°'; document.getElementById('transformControls').style.display = 'flex'; redrawCanvas(); return; } } selectedObjectIndex = -1; document.getElementById('transformControls').style.display = 'none'; redrawCanvas(); return; } if (currentTool === 'eraser') { for (let i = drawnObjects.length-1; i >= 0; i--) { if (isPointInObject(coords.x, coords.y, drawnObjects[i])) { saveToHistory(); drawnObjects.splice(i,1); redrawCanvas(); return; } } return; } if (currentTool === 'polygon') { currentPath.push({x:coords.x, y:coords.y}); redrawCanvas(); return; } if (!currentTool) return; saveToHistory(); isDrawing = true; startX = coords.x; startY = coords.y; currentPath = [{x:startX, y:startY}]; } function draw(e) { const coords = getCanvasCoords(e); if (currentTool === 'select' && isDragging && selectedObjectIndex >= 0) { const obj = drawnObjects[selectedObjectIndex]; const b = getObjectBounds(obj); const cx = coords.x - dragOffsetX, cy = coords.y - dragOffsetY; const dx = cx - (b.x+b.width/2), dy = cy - (b.y+b.height/2); if (obj.type === 'polygon' && obj.points) { obj.points.forEach(p => { p.x+=dx; p.y+=dy; }); } else { obj.x += dx; obj.y += dy; } redrawCanvas(); return; } if (!isDrawing || !currentTool || currentTool === 'polygon') return; redrawCanvas(); drawPreview(coords.x, coords.y); } function drawPreview(x, y) { ctx.save(); const brightness = (lightSettings[currentTool]?.brightness || 53) / 100; switch(currentTool) { case 'point': case 'fire': let r = Math.sqrt(Math.pow(x-startX,2)+Math.pow(y-startY,2)); r = Math.max(r, currentTool==='fire'?15:10); if (currentTool === 'fire') { ctx.shadowColor = 'rgba(255,100,0,0.7)'; ctx.shadowBlur = r*2; const fg = ctx.createRadialGradient(startX,startY,0,startX,startY,r); fg.addColorStop(0,'rgba(255,255,200,0.9)'); fg.addColorStop(0.3,'rgba(255,200,50,0.8)'); fg.addColorStop(0.6,'rgba(255,100,0,0.6)'); fg.addColorStop(1,'rgba(255,50,0,0)'); ctx.fillStyle = fg; } else { ctx.fillStyle = `rgba(255,255,220,${0.5+brightness*0.3})`; ctx.shadowColor = `rgba(255,255,150,${0.4+brightness*0.3})`; ctx.shadowBlur = r*1.5; } ctx.beginPath(); ctx.arc(startX, startY, r, 0, Math.PI*2); ctx.fill(); break; case 'block': let w = x-startX, h = y-startY; ctx.fillStyle = `rgba(255,250,200,${0.5+brightness*0.3})`; ctx.shadowColor = `rgba(255,230,150,${0.4+brightness*0.3})`; ctx.shadowBlur = 10; ctx.fillRect(startX, startY, w, h); break; case 'halfcircle': let hr = Math.sqrt(Math.pow(x-startX,2)+Math.pow(y-startY,2)); hr = Math.max(hr,10); ctx.fillStyle = `rgba(255,240,200,${0.5+brightness*0.3})`; ctx.shadowColor = `rgba(255,220,150,${0.4+brightness*0.3})`; ctx.shadowBlur = hr; ctx.beginPath(); ctx.arc(startX, startY, hr, 0, Math.PI, true); ctx.closePath(); ctx.fill(); break; case 'spotlight': let ss = Math.sqrt(Math.pow(x-startX,2)+Math.pow(y-startY,2)); ss = Math.max(ss,10); const sg = ctx.createLinearGradient(startX, startY-ss, startX, startY+ss*3); sg.addColorStop(0,`rgba(255,255,255,${0.6+brightness*0.3})`); sg.addColorStop(1,'rgba(255,255,200,0)'); ctx.fillStyle = sg; ctx.beginPath(); ctx.moveTo(startX-ss, startY); ctx.lineTo(startX+ss, startY); ctx.lineTo(startX+ss*0.5, startY+ss*3); ctx.lineTo(startX-ss*0.5, startY+ss*3); ctx.closePath(); ctx.fill(); break; } ctx.restore(); } function stopDrawing(e) { isDragging = false; if (!isDrawing || !currentTool) { isDrawing = false; return; } isDrawing = false; const coords = getCanvasCoords(e); let newObj = { type:currentTool, x:startX, y:startY, smoothness:smoothnessValue/100, brightness:(lightSettings[currentTool]?.brightness||53)/100, colorTemp:lightSettings[currentTool]?.colorTemp||4500, scale:1, rotation:0 }; switch(currentTool) { case 'point': case 'fire': let r = Math.sqrt(Math.pow(coords.x-startX,2)+Math.pow(coords.y-startY,2)); newObj.radius = Math.max(r, currentTool==='fire'?15:10); newObj.x = startX; newObj.y = startY; drawnObjects.push(newObj); break; case 'block': newObj.width = Math.abs(coords.x-startX); newObj.height = Math.abs(coords.y-startY); newObj.x = Math.min(startX, coords.x); newObj.y = Math.min(startY, coords.y); drawnObjects.push(newObj); break; case 'halfcircle': let hr = Math.sqrt(Math.pow(coords.x-startX,2)+Math.pow(coords.y-startY,2)); newObj.radius = Math.max(hr,10); newObj.x = startX; newObj.y = startY; drawnObjects.push(newObj); break; case 'spotlight': let ss = Math.sqrt(Math.pow(coords.x-startX,2)+Math.pow(coords.y-startY,2)); newObj.radius = Math.max(ss,10); newObj.x = startX; newObj.y = startY; drawnObjects.push(newObj); break; } redrawCanvas(); currentPath = []; } function clearCanvas() { saveToHistory(); hasMask = false; drawnObjects = []; currentPath = []; selectedObjectIndex = -1; document.getElementById('transformControls').style.display = 'none'; redrawCanvas(); document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active')); currentTool = null; } // ===== 渲染逻辑 ===== function generatePrompt() { const zones = drawnObjects.filter(o => o.type === "polygon"); const moon = document.getElementById("moonlightCheckbox").checked ? "冷蓝色月光洒落城墙" : "无月光,仅环境光"; const brightness = document.getElementById("smoothnessInput").value; const zonesText = zones.length > 0 ? "仅在用户用多边形标注的蒙版区域内安装暖色LED灯带,蒙版区域外不要放置任何灯带。" : ""; return "将此西部建筑渲染为照片级真实的夜景效果图。自动识别西部建筑特征:古城墙、回民街建筑、伊斯兰风格穹顶、土黄色夯土墙体、民族纹理装饰。为建筑添加专业夜景灯光设计:城墙轮廓灯带、民族纹样投射灯、街巷暖光氛围灯、景观节点重点照明。" + "时间设定为夜晚,整体环境亮度约 " + brightness + "%," + moon + "。" + zonesText + "严格保持所有建筑结构、几何形态、布局、材质和比例与输入图像完全一致,仅修改灯光效果。" + "展现古都西安的厚重历史感与西域风情,灯光大气磅礴又不失温暖。" + "物理精确的照片级真实渲染质量。" + "输出必须是干净的摄影图像,不含任何文字、数字、字母、标签、注释、区域标记、叠加图形、图表、UI元素或水印。"; } function startRender() { if (!originalImageSrc) { alert('请先上传图片'); return; } document.getElementById('statusSection').style.display = 'block'; updateStatus('queued', '正在准备图片和提示词...'); // 生成蒙版 const maskCanvas = document.createElement('canvas'); maskCanvas.width = canvas.width; maskCanvas.height = canvas.height; const maskCtx = maskCanvas.getContext('2d'); maskCtx.fillStyle = '#000'; maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); drawnObjects.forEach(obj => { drawObjectForMask(obj, maskCtx); }); const maskImage = maskCanvas.toDataURL('image/png'); const prompt = generatePrompt(); submitTask(originalImageSrc, maskImage, prompt); } function drawObjectForMask(obj, mCtx) { const brightness = obj.brightness || 0.53; if (obj.rotation || obj.scale) { let b = getObjectBounds(obj); const cx = b.x+b.width/2, cy = b.y+b.height/2; mCtx.translate(cx,cy); mCtx.rotate((obj.rotation||0)*Math.PI/180); mCtx.scale(obj.scale||1,obj.scale||1); mCtx.translate(-cx,-cy); } mCtx.fillStyle = '#fff'; mCtx.strokeStyle = '#fff'; switch(obj.type) { case 'linear': mCtx.lineWidth = 10; mCtx.lineCap = 'round'; mCtx.lineJoin = 'round'; mCtx.beginPath(); if (obj.path && obj.path.length > 1) { mCtx.moveTo(obj.path[0].x, obj.path[0].y); for (let i=1;i= 3) { mCtx.beginPath(); mCtx.moveTo(obj.points[0].x, obj.points[0].y); for (let i=1;i { updateStatus('completed', '渲染完成!(演示模式)'); // 模拟结果展示 const resultZone = document.getElementById('resultZone'); const resultImage = document.getElementById('resultImage'); resultImage.src = originalImage; // 演示模式:显示原图 resultZone.style.display = 'block'; resultZone.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 2000); } function updateStatus(status, text) { const badge = document.getElementById('statusBadge'); const statusText = document.getElementById('statusText'); badge.className = 'status-badge ' + status; const labels = { 'queued':'排队中', 'in-progress':'渲染中', 'completed':'已完成', 'failed':'失败' }; badge.textContent = labels[status] || status.toUpperCase(); statusText.textContent = text; }