function makeDemoImage() { var canvas = document.createElement('canvas'); canvas.width = 1000; canvas.height = 720; var ctx = canvas.getContext('2d'); var sky = ctx.createLinearGradient(0, 0, 0, canvas.height); sky.addColorStop(0, '#f7f3e7'); sky.addColorStop(.62, '#cdded8'); sky.addColorStop(1, '#7fa5a3'); ctx.fillStyle = sky; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(33,70,74,.15)'; for (let i = 0; i < 6; i++) { ctx.beginPath(); ctx.ellipse(160 + i * 175, 164 + (i % 2) * 42, 126, 28, -.08, 0, Math.PI * 2); ctx.fill(); } var ridges = [{ y: 420, color: '#7b9995', amp: 130, step: 145 }, { y: 510, color: '#496f72', amp: 170, step: 125 }, { y: 610, color: '#193f48', amp: 190, step: 112 }]; ridges.forEach( (ridge, layer) => { ctx.beginPath(); ctx.moveTo(0, canvas.height); ctx.lineTo(0, ridge.y); for (let x = 0; x <= canvas.width + ridge.step; x += ridge.step) { var peak = ridge.y - ridge.amp * (.54 + .46 * Math.sin(x * .014 + layer)); ctx.quadraticCurveTo(x + ridge.step * .45, peak, x + ridge.step, ridge.y + Math.sin(x * .021) * 22); } ctx.lineTo(canvas.width, canvas.height); ctx.closePath(); ctx.fillStyle = ridge.color; ctx.globalAlpha = .44 + layer * .16; ctx.fill(); } ); ctx.globalAlpha = 1; ctx.strokeStyle = '#163d45'; ctx.lineWidth = 7; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(742, 620); ctx.bezierCurveTo(716, 476, 760, 340, 700, 192); ctx.stroke(); ctx.lineWidth = 3; for (let i = 0; i < 13; i++) { var y = 230 + i * 25; var side = i % 2 ? -1 : 1; ctx.beginPath(); ctx.moveTo(716 + Math.sin(i) * 16, y); ctx.quadraticCurveTo(790 + side * 18, y - 32, 842 + side * 44, y - 45); ctx.stroke(); } ctx.fillStyle = '#244f55'; for (let i = 0; i < 42; i++) { var x = 690 + Math.sin(i * 2.1) * (55 + i % 7 * 9); var y = 205 + (i * 17) % 310; ctx.beginPath(); ctx.ellipse(x, y, 13, 5, (i % 5) * .42, 0, Math.PI * 2); ctx.fill(); } var img = new Image(); img.src = canvas.toDataURL('image/png'); return img; } function imageLuminance(image, u, v) { if (!image) return 1; var px = clamp(u * (image.width - 1), 0, image.width - 1); var py = clamp(v * (image.height - 1), 0, image.height - 1); var x0 = Math.floor(px) , y0 = Math.floor(py); var x1 = Math.min(x0 + 1, image.width - 1) , y1 = Math.min(y0 + 1, image.height - 1); var tx = px - x0 , ty = py - y0; var sample = (x, y) => { var i = (y * image.width + x) * 4; return (image.data[i] * .2126 + image.data[i + 1] * .7152 + image.data[i + 2] * .0722) / 255; } ; var top = sample(x0, y0) * (1 - tx) + sample(x1, y0) * tx; var bottom = sample(x0, y1) * (1 - tx) + sample(x1, y1) * tx; var lum = top * (1 - ty) + bottom * ty; var contrast = Number(ui.contrast.value) / 100; lum = clamp((lum - .5) * contrast + .5, 0, 1); if (ui.invert.checked) lum = 1 - lum; return lum; } function sampleLuminance(x, y) { var bounds = modelBounds(); var u = clamp((y - bounds.yMin) / (bounds.yMax - bounds.yMin), 0, 1); var v = clamp((x - bounds.xMin) / (bounds.xMax - bounds.xMin), 0, 1); return imageLuminance(state.sourcePixels, u, v); } function sampleBladeLuminance(bladeIndex, radius, cross) { var pixels = state.bladeConfigs[bladeIndex].pixels; if (!pixels) return 1; var u = clamp((radius - FAN.rMin) / (FAN.rMax - FAN.rMin), 0, 1); var v = clamp((1 - cross) / 2, 0, 1); return imageLuminance(pixels, u, v); } function readImageFile(file) { return new Promise( (resolve, reject) => { var reader = new FileReader(); reader.onload = () => { var img = new Image(); img.onload = () => resolve({ file, img }); img.onerror = () => reject(new Error(`${file.name} 无法读取`)); img.src = reader.result; } ; reader.onerror = () => reject(new Error(`${file.name} 无法读取`)); reader.readAsDataURL(file); } ); } async function loadFiles(fileList) { var files = [...(fileList || [])].filter( (file) => file.type.startsWith('image/')); if (!files.length) { ui.status.textContent = '请选择 JPG、PNG 或 WEBP 图片。'; return; } try { showBusy(true); if (state.imageMode === 'whole') { var loaded = await readImageFile(files[0]); state.globalConfig.image = loaded.img; state.globalConfig.name = loaded.file.name; state.globalConfig.isDemo = false; ui.status.textContent = '整扇图片已载入,正在生成浮雕。'; } else { var available = FAN.angles.length - state.selectedBlade; var loaded = await Promise.all(files.slice(0, available).map(readImageFile)); loaded.forEach( ({file, img}, offset) => { var config = state.bladeConfigs[state.selectedBlade + offset]; config.image = img; config.name = file.name; config.isDemo = false; } ); ui.status.textContent = `已从第 ${state.selectedBlade + 1} 片开始载入 ${loaded.length} 张图片。`; } ui.fileInput.value = ''; applyConfigToControls(); scheduleRebuild({ redrawImage: true }); } catch (error) { console.error(error); ui.status.textContent = `图片载入失败:${error.message}`; showBusy(false); } }