181 lines
8.5 KiB
JavaScript
181 lines
8.5 KiB
JavaScript
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);
|
|
}
|
|
}
|