轮廓照明
灯带沿檐口屋脊勾勒北方建筑的庄重轮廓,飞檐斗拱在夜间依然气势恢宏
立面照明
投光灯和洗墙灯均匀照亮红墙灰瓦,展现青砖和汉白玉等北方建材的夜间质感
景观氛围
庭院灯和地灯点缀前院与甬道,对称布局呼应北方建筑的规制之美
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