ai_v/templates/kongzhiqi.html

460 lines
17 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 3D Camera Visualizer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.158.0/three.min.js"></script>
<style>
:root {
--bg-body: #f8fafc;
--bg-card: #ffffff;
--primary: #4f46e5;
--primary-hover: #4338ca;
--text-main: #1e293b;
--text-dim: #64748b;
--border: #e2e8f0;
--accent-cyan: #06b6d4;
--accent-pink: #d946ef;
--accent-orange: #f59e0b;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: transparent;
color: var(--text-main);
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
max-width: 100%;
background: transparent;
padding: 20px;
box-sizing: border-box;
}
header h2 {
margin: 0 0 4px 0;
font-size: 1rem;
font-weight: 800;
color: #0f172a;
}
.legend {
font-size: 11px;
font-weight: 600;
color: var(--text-dim);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
/* 3D 预览区 */
#viewport {
width: 100%;
height: 280px;
background: #f1f5f9;
border-radius: 24px;
position: relative;
overflow: hidden;
border: 1px solid var(--border);
margin-bottom: 24px;
}
.prompt-overlay {
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
padding: 8px 12px;
border-radius: 12px;
color: var(--primary);
font-weight: 700;
font-size: 10px;
text-align: center;
pointer-events: none;
border: 1px solid rgba(79, 70, 229, 0.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
}
/* 控制器样式 */
.controls {
display: flex;
flex-direction: column;
gap: 20px;
}
.control-group { position: relative; }
.label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.tag {
font-size: 10px;
font-weight: 800;
padding: 2px 8px;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.val-display { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; font-weight: 700; color: var(--text-main); }
input[type=range] {
width: 100%;
height: 4px;
background: #e2e8f0;
border-radius: 10px;
outline: none;
-webkit-appearance: none;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: white;
border: 2px solid var(--primary);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.1s;
}
input[type=range]:active::-webkit-slider-thumb { transform: scale(1.2); }
.range-labels {
display: flex;
justify-content: space-between;
font-size: 9px;
font-weight: 700;
color: #94a3b8;
margin-top: 6px;
text-transform: uppercase;
}
.generate-btn {
background: var(--primary);
color: white;
border: none;
padding: 16px;
width: 100%;
border-radius: 16px;
font-size: 14px;
font-weight: 800;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2);
margin-top: 10px;
}
.generate-btn:hover { background: var(--primary-hover); transform: translateY(-1px); }
.generate-btn:active { transform: translateY(0); }
/* 输出结果 (简化) */
.output-section {
margin-top: 24px;
background: #f8fafc;
padding: 12px;
border-radius: 16px;
border: 1px solid var(--border);
display: none; /* 简洁考虑,默认隐藏预览 */
}
/* 颜色类 */
.bg-cyan { background: #ecfeff; color: #0891b2; border: 1px solid #cffafe; }
.bg-pink { background: #fdf2f8; color: #db2777; border: 1px solid #fce7f3; }
.bg-orange { background: #fffbeb; color: #d97706; border: 1px solid #fef3c7; }
.dot-cyan { background: var(--accent-cyan); }
.dot-pink { background: var(--accent-pink); }
.dot-orange { background: var(--accent-orange); }
.file-input-wrapper { margin-bottom: 20px; text-align: left; }
.file-label {
font-size: 11px;
font-weight: 800;
color: var(--primary);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
background: #f5f3ff;
padding: 6px 12px;
border-radius: 8px;
transition: all 0.2s;
}
.file-label:hover { background: #ede9fe; }
</style>
</head>
<body>
<div class="container">
<div class="file-input-wrapper">
<label class="file-label" for="fileInput">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
更换参考图
</label>
<input type="file" id="fileInput" hidden accept="image/*">
</div>
<div id="viewport">
<div class="prompt-overlay" id="overlayPrompt">准备就绪</div>
</div>
<div class="controls">
<!-- Azimuth -->
<div class="control-group">
<div class="label-row">
<span class="tag bg-cyan">Azimuth (水平)</span>
<span class="val-display" id="val-azimuth"></span>
</div>
<input type="range" id="slider-azimuth" min="0" max="315" step="45" value="0">
<div class="range-labels">
<span>正前</span><span>右侧</span><span>正后</span><span>左侧</span>
</div>
</div>
<!-- Elevation -->
<div class="control-group">
<div class="label-row">
<span class="tag bg-pink">Elevation (高度)</span>
<span class="val-display" id="val-elevation"></span>
</div>
<input type="range" id="slider-elevation" min="-90" max="90" step="5" value="0">
<div class="range-labels">
<span>仰拍</span><span>平视</span><span>俯拍</span>
</div>
</div>
<!-- Distance -->
<div class="control-group">
<div class="label-row">
<span class="tag bg-orange">Distance (距离)</span>
<span class="val-display" id="val-distance">1.0</span>
</div>
<input type="range" id="slider-distance" min="2" max="25" step="1" value="10">
<div class="range-labels">
<span>特写</span><span>标准</span><span>远景</span>
</div>
</div>
<button class="generate-btn" id="applyBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg>
确认拍摄角度
</button>
</div>
<div class="output-section">
<div class="output-content" id="finalPrompt">...</div>
</div>
</div>
<script>
// --- 1. Three.js 初始化 ---
const viewport = document.getElementById('viewport');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, viewport.clientWidth / viewport.clientHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(viewport.clientWidth, viewport.clientHeight);
renderer.setClearColor(0xf1f5f9, 1); // 设置背景色与 viewport 一致
viewport.appendChild(renderer.domElement);
camera.position.set(6, 5, 8);
camera.lookAt(0, 0.5, 0);
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
const grid = new THREE.GridHelper(10, 20, 0xccd6e0, 0xe2e8f0);
scene.add(grid);
// --- 2. 场景物体 ---
// 参考图 Plane
const targetGeom = new THREE.PlaneGeometry(1.5, 2);
const targetMat = new THREE.MeshBasicMaterial({ color: 0x22222a, side: THREE.DoubleSide });
const targetPlane = new THREE.Mesh(targetGeom, targetMat);
targetPlane.position.y = 1;
scene.add(targetPlane);
// 相机小模型
const camGroup = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.35, 0.4), new THREE.MeshStandardMaterial({color: 0x3b82f6}));
const lens = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.2), new THREE.MeshStandardMaterial({color: 0x111111}));
lens.rotation.x = Math.PI / 2;
lens.position.z = -0.25;
camGroup.add(body, lens);
scene.add(camGroup);
// 轨道可视化
const aziRing = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(new THREE.EllipseCurve(0,0, 3,3).getPoints(64)),
new THREE.LineBasicMaterial({ color: 0x2dd4bf })
);
aziRing.rotation.x = Math.PI/2;
scene.add(aziRing);
let elevArc;
const distLine = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xfbbf24 }));
scene.add(distLine);
// --- 3. 逻辑控制 ---
const sAzi = document.getElementById('slider-azimuth');
const sElev = document.getElementById('slider-elevation');
const sDist = document.getElementById('slider-distance');
function update() {
const azi = parseInt(sAzi.value);
const elev = parseInt(sElev.value);
const distVal = parseInt(sDist.value) / 10;
document.getElementById('val-azimuth').innerText = azi + '°';
document.getElementById('val-elevation').innerText = elev + '°';
document.getElementById('val-distance').innerText = distVal.toFixed(1);
// 计算坐标
const visualR = distVal * 3;
const phi = (90 - elev) * (Math.PI / 180);
const theta = (azi) * (Math.PI / 180);
const x = visualR * Math.sin(phi) * Math.sin(theta);
const y = visualR * Math.cos(phi);
const z = visualR * Math.sin(phi) * Math.cos(theta);
camGroup.position.set(x, y, z);
camGroup.lookAt(0, 1, 0);
// 更新距离线
distLine.geometry.setFromPoints([new THREE.Vector3(0,0,0), new THREE.Vector3(x,y,z)]);
// 动态绘制高度弧线
if(elevArc) scene.remove(elevArc);
const arcPoints = [];
for(let i=-30; i<=90; i+=5){
const p = (90-i)*(Math.PI/180);
arcPoints.push(new THREE.Vector3(visualR*Math.sin(p)*Math.sin(theta), visualR*Math.cos(p), visualR*Math.sin(p)*Math.cos(theta)));
}
elevArc = new THREE.Line(new THREE.BufferGeometry().setFromPoints(arcPoints), new THREE.LineBasicMaterial({color: 0xe879f9}));
scene.add(elevArc);
renderPrompt(azi, elev, distVal);
}
function getAziDesc(a) {
if (a === 0) return "no horizontal rotation";
if (a > 0 && a < 180) return `rotate ${a}° clockwise to the right`;
if (a === 180) return "rotate 180° to the opposite side";
if (a > 180) return `rotate ${360 - a}° counter-clockwise to the left`;
return "angled view";
}
function renderPrompt(azi, elev, dist) {
// 1. 生成最纯粹的相对位移指令
let aziCmd = azi === 0 ? "no horizontal rotation" : `rotate camera ${azi}° clockwise around the subject`;
if (azi > 180) {
aziCmd = `rotate camera ${360 - azi}° counter-clockwise to the left`;
}
const elevCmd = elev === 0 ? "maintain height" : (elev > 0 ? `tilt camera up ${elev}°` : `tilt camera down ${Math.abs(elev)}°`);
// 2. 核心视角标签 (用于辅助定位)
let viewTag = "front view";
if (azi === 45) viewTag = "front-right 3/4 view";
else if (azi === 90) viewTag = "right side view";
else if (azi === 135) viewTag = "back-right view";
else if (azi === 180) viewTag = "back view";
else if (azi === 225) viewTag = "back-left view";
else if (azi === 270) viewTag = "left side view";
else if (azi === 315) viewTag = "front-left 3/4 view";
let elevTag = "eye-level shot";
if (elev >= 75) elevTag = "top-down view";
else if (elev > 20) elevTag = "high angle shot";
else if (elev < -75) elevTag = "bottom-up view";
else if (elev < -20) elevTag = "low angle shot";
let distTag = "medium shot";
if (dist <= 0.4) distTag = "close-up shot";
else if (dist >= 1.6) distTag = "wide shot";
// 3. 构建提示词块
// 格式:[机位移动指令]. <sks> [目标视角标签], level horizon.
const positive = `Camera adjustment from reference: ${aziCmd} and ${elevCmd}. <sks> ${viewTag}, ${elevTag}, ${distTag}, level horizon`;
const visualizer = `<Visualizer> Azimuth:${azi}| Elevation:${elev} | Distance:${dist.toFixed(1)}`;
const negative = `[Negative Prompt]: tilted, rotated, distorted viewpoint, inconsistent angle`;
const fullPrompt = `${positive}
${visualizer}
${negative}`;
document.getElementById('overlayPrompt').innerText = positive;
document.getElementById('finalPrompt').innerText = fullPrompt;
}
// 图片上传
document.getElementById('fileInput').onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (ev) => {
applyTexture(ev.target.result);
};
reader.readAsDataURL(file);
}
};
function applyTexture(dataUrl) {
new THREE.TextureLoader().load(dataUrl, (tex) => {
targetPlane.material = new THREE.MeshBasicMaterial({ map: tex, transparent: true });
});
}
// 监听来自主页面的图片同步消息
window.addEventListener('message', (e) => {
if (e.data.type === 'sync_image' && e.data.dataUrl) {
applyTexture(e.data.dataUrl);
}
});
[sAzi, sElev, sDist].forEach(s => s.addEventListener('input', update));
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// 应用到主窗口
document.getElementById('applyBtn').onclick = () => {
const prompt = document.getElementById('finalPrompt').innerText;
window.parent.postMessage({ type: 'apply_prompt', prompt: prompt }, '*');
// 提示用户已应用
const btn = document.getElementById('applyBtn');
const oldContent = btn.innerHTML;
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg> 已应用同步';
btn.style.background = '#10b981';
btn.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.2)';
setTimeout(() => {
btn.innerHTML = oldContent;
btn.style.background = '';
btn.style.boxShadow = '';
}, 1500);
};
update();
animate();
window.onresize = () => {
camera.aspect = viewport.clientWidth / viewport.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewport.clientWidth, viewport.clientHeight);
};
</script>
</body>
</html>