<!-- Main layout — 3D cube on the left, UV atlas on the right -->
<div class="container">
<div class="panel">
<p class="label">3D Cube</p>
<canvas id="cube3d" width="200" height="200" title="Click a face to highlight it"></canvas>
<p class="hint">Click a face to see its UV island</p>
</div>
<div class="panel">
<p class="label">UV Atlas</p>
<canvas id="uvmap" width="200" height="200" title="UV layout of the cube faces"></canvas>
<p class="hint">Each colored square is one face of the cube</p>
</div>
</div>
<div class="info" id="info">Click a cube face to see its UV coordinates.</div>
<button id="resetBtn" type="button">Reset</button>
/* Global styles for the two-panel layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.container { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; padding: .5rem; }
.panel { display: flex; flex-direction: column; align-items: center; }
.label { font-weight: 700; font-size: .85rem; margin: 0 0 .3rem; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; cursor: pointer; background: #f4f7fa; }
.hint { font-size: .78rem; color: #666; margin: .3rem 0 0; max-width: 200px; text-align: center; }
.info { text-align: center; font-size: .9rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; color: #1d3557; }
button { display: block; margin: 0 auto; font: 600 14px system-ui; padding: .4rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
// Human-readable face names (one per face, same order as FACES array)
const FACE_NAMES = ['Top','Bottom','Front','Back','Right','Left'];
// Fill colors — one per face index 0..5 — one per face
const FACE_COLORS = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261','#8ecae6'];
// Cross-unfold UV layout: pixel positions [left, top] of each 50x50 UV island inside the 200x200 atlas
// Cross unfold layout in UV space (200x200 canvas, each face = 50x50 px).
// Face indices: 0=top, 1=bottom, 2=front, 3=back, 4=right, 5=left
// UV pixel positions [col*50, row*50] within the 200x200 atlas:
const UV_RECTS = [
[50, 0], // Top face — center column, top row
[50, 150], // Bottom face — center column, bottom row
[50, 50], // Front face — center column, middle row
[50, 100], // Back face — center column, second row from bottom — placed below front in cross unfold for clarity
[100, 50], // Right face — right column, middle row
[0, 50], // Left face — left column, middle row
];
// 8 corners of a unit cube [-1,+1]^3, projected to 2D with an isometric-like view
// 8 corners of a unit cube centered at origin, projected to 2D with a simple isometric-like view
const W = 200, H = 200;
function project([x, y, z]) {
// Simplified oblique projection: x right, y up, z depth
const angle = Math.PI / 6;
const sx = Math.cos(angle), sy = Math.sin(angle);
return [
W/2 + (x - z) * sx * 60,
H/2 + (x + z) * sy * 30 - y * 60
];
}
// Vertex list: indices 0-3 = back face, 4-7 = front face
const V = [
[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1], // back face vertices (z=-1)
[-1,-1, 1],[1,-1, 1],[1,1, 1],[-1,1, 1] // front face vertices (z=+1)
];
// Face definitions: 4 vertex indices per face (CCW winding from outside)
// Each face: 4 vertex indices (CCW winding when viewed from outside), face index 0..5
const FACES = [
[3,2,6,7], // top face vertices
[0,1,5,4], // bottom face vertices
[4,5,6,7], // front face vertices
[1,0,3,2], // back face vertices
[5,1,2,6], // right face vertices
[0,4,7,3], // left face vertices
];
// Faces visible in this isometric-ish projection — only 3 faces visible in this isometric-ish view
const VISIBLE = [0, 2, 4]; // top, front, right — the three visible faces in this view
let selected = -1;
const canvas3d = document.getElementById('cube3d');
const ctxC = canvas3d.getContext('2d');
const canvasUV = document.getElementById('uvmap');
const ctxU = canvasUV.getContext('2d');
function drawCube() {
ctxC.clearRect(0, 0, W, H);
// Draw each visible cube face as a filled polygon
for (const fi of VISIBLE) {
const verts = FACES[fi].map(i => project(V[i]));
ctxC.beginPath();
ctxC.moveTo(...verts[0]);
for (let i = 1; i < verts.length; i++) ctxC.lineTo(...verts[i]);
ctxC.closePath();
const baseColor = FACE_COLORS[fi];
if (fi === selected) {
ctxC.fillStyle = baseColor;
ctxC.strokeStyle = '#000';
ctxC.lineWidth = 2.5;
} else {
// Unselected faces are dimmed when one is selected
ctxC.fillStyle = selected === -1 ? baseColor + 'cc' : baseColor + '44';
ctxC.strokeStyle = '#888';
ctxC.lineWidth = 1;
}
ctxC.fill();
ctxC.stroke();
// Draw face name label in the center of the face
const cx = verts.reduce((s,p)=>s+p[0],0)/4;
const cy = verts.reduce((s,p)=>s+p[1],0)/4;
ctxC.fillStyle = fi === selected ? '#fff' : (selected === -1 ? '#fff' : '#aaa');
ctxC.font = `${fi===selected?'700 12':'600 10'}px system-ui`;
ctxC.textAlign = 'center';
ctxC.textBaseline = 'middle';
ctxC.fillText(FACE_NAMES[fi], cx, cy);
}
}
function drawUV() {
ctxU.clearRect(0, 0, W, H);
// Draw faint grid lines every 50 px (= 0.25 UV units)
ctxU.strokeStyle = '#cdd9e3';
ctxU.lineWidth = 0.5;
for (let i = 0; i <= 4; i++) {
ctxU.beginPath(); ctxU.moveTo(i*50, 0); ctxU.lineTo(i*50, H); ctxU.stroke();
ctxU.beginPath(); ctxU.moveTo(0, i*50); ctxU.lineTo(W, i*50); ctxU.stroke();
}
// Draw each UV island as a colored rectangle
for (let fi = 0; fi < 6; fi++) {
const [px, py] = UV_RECTS[fi];
const alpha = (selected === -1) ? 'cc' : (fi === selected ? 'ff' : '33');
ctxU.fillStyle = FACE_COLORS[fi] + alpha;
ctxU.strokeStyle = fi === selected ? '#000' : '#999';
ctxU.lineWidth = fi === selected ? 2 : 1;
ctxU.fillRect(px, py, 50, 50);
ctxU.strokeRect(px, py, 50, 50);
// Face name label in the center of each UV island
ctxU.fillStyle = fi === selected ? '#fff' : (selected === -1 ? '#fff' : '#aaa');
ctxU.font = `${fi===selected?'700 11':'600 9'}px system-ui`;
ctxU.textAlign = 'center';
ctxU.textBaseline = 'middle';
ctxU.fillText(FACE_NAMES[fi], px+25, py+25);
}
}
function getClickedFace(e) {
// Convert mouse position to canvas coordinates and test each visible face
const rect = canvas3d.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
// Check faces back-to-front so the topmost visible face wins
for (const fi of [...VISIBLE].reverse()) {
const verts = FACES[fi].map(i => project(V[i]));
if (pointInPolygon(mx, my, verts)) return fi;
}
return -1;
}
function pointInPolygon(px, py, poly) {
// Ray-casting point-in-polygon test (even-odd rule)
let inside = false;
for (let i = 0, j = poly.length-1; i < poly.length; j=i++) {
const [xi,yi] = poly[i], [xj,yj] = poly[j];
if (((yi>py)!==(yj>py)) && px < (xj-xi)*(py-yi)/(yj-yi)+xi) inside = !inside;
}
return inside;
}
function setInfo(fi) {
const el = document.getElementById('info');
if (fi === -1) { el.textContent = 'Click a cube face to see its UV coordinates.'; return; }
// Show face name and its UV bounding box coordinates
el.textContent = FACE_NAMES[fi] + ' → UV ' + formatUV(fi);
}
function formatUV(fi) {
const [px, py] = UV_RECTS[fi];
const u0 = (px/200).toFixed(2), v0 = (py/200).toFixed(2);
const u1 = ((px+50)/200).toFixed(2), v1 = ((py+50)/200).toFixed(2);
return `(${u0}, ${v0}) – (${u1}, ${v1})`;
}
canvas3d.addEventListener('click', e => {
const fi = getClickedFace(e);
selected = (fi === selected) ? -1 : fi;
drawCube(); drawUV(); setInfo(selected);
});
document.getElementById('resetBtn').addEventListener('click', () => {
selected = -1;
drawCube(); drawUV(); setInfo(-1);
});
drawCube(); drawUV(); setInfo(-1);
Comments
Loading comments...