目录
二阶魔方

1,二阶魔方与三阶魔方的关系
可以理解为,二阶魔方就是三阶魔方的八个角块。
三阶魔方的层先法:做好底层十字、调整底面角块、复原中层棱块、做好顶面十字、顶角归位、调整顶角位置、调整顶棱位置。
二阶魔方的层先法:调整底面角块、顶角归位、调整顶角位置
二阶魔方只有3步,而且这3步和三阶魔方的对应3步一模一样。
资源分享汇总_csuzhucong的博客-CSDN博客_csdn资源
当然,二阶魔方和三阶魔方一样,除了层先法还有更快的高阶方法,以及盲拧公式。
2,二阶魔方的定位
偶数阶魔方和奇数阶魔方的差别在于,奇数阶魔方的中心块是固定的,整个魔方六个面的颜色也就固定了。
而偶数阶魔方没有中心块,六个面能确定吗?
对于二阶魔方,还是形成了共识的,以任意一个角块为基准,都可以确定另外七个角块的位置,即确定了整个魔方。
3,二阶魔方的盲拧
二阶魔方的盲拧一共需要2个公式,是从三阶魔方的3个公式演变来的。
用层先法复原魔方一共分七步,分别为:做好底层十字、调整底面角块、复原中层棱块、做好顶面十字、顶角归位、调整顶角位置、调整顶棱位置。
这里需要顶角归位的小鱼1、小鱼2,还有调整顶角位置的公式(姑且叫做换角公式)
我在研究三阶魔方的时候就研究过小鱼1和小鱼2的关系,对一个复原的魔方用小鱼1,只要再用一次小鱼2就可以直接完成顶角归位、调整顶角位置这两步,只需要进行第七步即可复原。
而对于二阶魔方,没有棱,用小鱼1再用小鱼2直接就是复原的。
用小鱼1:(拍摄角度即为拿魔方的角度,为了看得清楚,镜头稍微往右偏了一点)

如果把整个魔方转个方向:

在这个方向再用小鱼2的话就直接复原了。
但是如果整个魔方转到另外一个方向:

在这个方向使用小鱼2就会变成:

这里,我们就得到了二阶魔方盲拧的第一个公式:
小鱼1+整个魔方转90度+小鱼2 = 把2个相邻的角快原地转动(一个顺时针一个逆时针),另外6个角快不动
至于换角公式:

把上面一层逆时针旋转90度,即U'
(这里为了看得清楚,镜头往左偏移了一点)

这里,我们就得到了二阶魔方盲拧的第二个公式:
换角公式+U' = 相邻的2个角快交换位置
利用这2个公式,即可盲拧二阶魔方。
二阶魔方网页版
1,平面展开图形式
提示词:
生成一个网页小游戏,里面是一个打乱的二阶魔方,可以同时显示6个面的内容,通过点击按钮实现旋转,最终复原。
deepseek生成结果:。。。。。。略
直接保存成本地的单个html文件,打开就能玩。

glm生成结果:。。。。。。略

2,立体形式
提示词:
换成正方体的呈现方式,不要这种展开图的方式
deepseek生成结果:。。。。。。略
依赖:Three.js 通过 CDN 加载,首次打开需要联网;断网环境下可把 three.module.js 和 OrbitControls.js 下载到本地后改 import 路径

3,棋谱功能
提示词:
不要把部件隔开,挨着放,像真实的魔方一样。添加棋谱功能,能一键复制打乱之后的初始局面,当前局面,以及完整的操作序列
deepseek生成结果:。。。。。。略
提示词:
部件和部件还是隔开的,二阶魔方的8个角快要完全挨着,另外页面布局调一下,把棋谱相关内容挪到右边,不要放下面
deepseek生成结果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>二阶魔方 3D</title>
<style>
:root {
--bg1: #161b26;
--bg2: #1f2635;
--txt: #e8edf5;
--dim: #8fa0bf;
--panel: #1b2231;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 700px at 50% -10%, var(--bg2), var(--bg1));
color: var(--txt);
padding: 20px 16px 30px;
overflow: auto;
}
h1 {
margin: 0 0 4px;
font-size: 24px;
letter-spacing: 3px;
text-align: center;
}
#status {
min-height: 24px;
margin: 2px 0 10px;
font-size: 14px;
color: var(--dim);
transition: color .3s;
text-align: center;
}
#status.solved {
color: #7ee08a;
font-weight: bold;
animation: pop .55s ease;
}
@keyframes pop {
0% { transform: scale(.6); opacity: 0; }
70% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
/* 左右两栏布局 */
#layout {
display: flex;
gap: 22px;
width: 100%;
max-width: 1100px;
margin: 0 auto;
align-items: flex-start;
justify-content: center;
}
#leftCol {
flex: 1 1 auto;
min-width: 0;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
#rightCol {
flex: 0 0 330px;
width: 330px;
position: sticky;
top: 20px;
}
#stage {
width: 100%;
max-width: 560px;
height: 56vh;
min-height: 340px;
max-height: 540px;
cursor: grab;
touch-action: none;
}
#stage:active { cursor: grabbing; }
#controls {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.move-row { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; }
button {
border: none;
border-radius: 9px;
padding: 10px 15px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
background: #37425c;
color: #fff;
box-shadow: 0 3px 0 #1a2130;
transition: transform .06s, background .15s, box-shadow .06s;
min-width: 48px;
letter-spacing: .5px;
}
button:hover { background: #46557a; }
button:active { transform: translateY(3px); box-shadow: 0 0 0 #1a2130; }
button.cw { background: #3153a8; }
button.cw:hover { background: #436bcb; }
button.ccw { background: #a84f33; }
button.ccw:hover { background: #c9633f; }
button.tool { background: #2f7a55; }
button.tool:hover { background: #3b9768; }
button.tool.warn { background: #8a3d3d; }
button.tool.warn:hover { background: #a84c4c; }
button.copy { background: #2a6e8f; }
button.copy:hover { background: #3588ad; }
.hint {
margin-top: 10px;
font-size: 12.5px;
color: #6c7b99;
text-align: center;
line-height: 1.8;
}
kbd {
background: #2a3344;
border: 1px solid #4a5a7a;
border-radius: 5px;
padding: 1px 7px;
font-size: 12px;
}
/* 棋谱面板(右侧) */
#recordBox {
display: flex;
flex-direction: column;
gap: 10px;
}
#recordTitle {
font-size: 15px;
font-weight: 700;
color: #cfd9ea;
letter-spacing: 1px;
text-align: center;
}
#recordPanel {
background: var(--panel);
border: 1px solid #2a3344;
border-radius: 10px;
padding: 14px 16px;
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.65;
color: #cfd9ea;
white-space: pre-wrap;
word-break: break-all;
max-height: 58vh;
overflow: auto;
margin: 0;
}
#copyBtn { width: 100%; }
@media (max-width: 920px) {
#layout { flex-direction: column; align-items: center; }
#rightCol {
width: 100%;
max-width: 560px;
position: static;
}
#recordPanel { max-height: 300px; }
#stage { height: 46vh; min-height: 280px; }
}
@media (max-width: 560px) {
button { padding: 8px 10px; font-size: 13px; min-width: 40px; }
}
</style>
</head>
<body>
<h1>二阶魔方</h1>
<div id="status">加载中…</div>
<div id="layout">
<!-- 左侧:3D 魔方 + 操作 -->
<div id="leftCol">
<div id="stage"></div>
<div id="controls">
<div class="move-row" id="moveButtons"></div>
<div class="move-row">
<button class="tool" id="scrambleBtn">打乱</button>
<button class="tool warn" id="resetBtn">复原</button>
</div>
</div>
<div class="hint">
拖动鼠标旋转视角,滚轮缩放。<br>
键盘:<kbd>U</kbd><kbd>R</kbd><kbd>F</kbd><kbd>D</kbd><kbd>L</kbd><kbd>B</kbd> 顺时针,
<kbd>Shift</kbd> + 字母为逆时针。
</div>
</div>
<!-- 右侧:棋谱 -->
<div id="rightCol">
<div id="recordBox">
<div id="recordTitle">📋 棋谱</div>
<button class="tool copy" id="copyBtn">📋 一键复制棋谱</button>
<pre id="recordPanel"></pre>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ---------- 场景 ----------
const stage = document.getElementById('stage');
const statusEl = document.getElementById('status');
const recordPanel = document.getElementById('recordPanel');
const copyBtn = document.getElementById('copyBtn');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, stage.clientWidth / stage.clientHeight, 0.1, 100);
camera.position.set(2.9, 2.4, 3.8);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.12;
controls.minDistance = 1.8;
controls.maxDistance = 7;
controls.update();
scene.add(new THREE.AmbientLight(0xffffff, 0.85));
const light1 = new THREE.DirectionalLight(0xffffff, 1.3);
light1.position.set(5, 8, 6);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.4);
light2.position.set(-4, -3, -2);
scene.add(light2);
// ---------- 颜色 ----------
const COL_HEX = {
U: '#f5f5f5', // 上:白
D: '#ffd500', // 下:黄
R: '#e33c3c', // 右:红
L: '#ff8c1a', // 左:橙
F: '#3aab4e', // 前:绿
B: '#3b82f6' // 后:蓝
};
const COL_CHAR = {
U: 'W', D: 'Y', R: 'R', L: 'O', F: 'G', B: 'B'
};
// ---------- 坐标体系 ----------
// 二阶魔方:8 个角块中心坐标在 ±0.5,块宽 1.0 → 完全紧贴,零缝隙
const H = 0.5; // 半层距
const SIZE = 1.0; // 块宽,与中心距 2H 相等 → 完全贴合
const FACE_DEF = {
U: { axis: 'y', side: H, vec: new THREE.Vector3(0, 1, 0) },
D: { axis: 'y', side: -H, vec: new THREE.Vector3(0,-1, 0) },
R: { axis: 'x', side: H, vec: new THREE.Vector3(1, 0, 0) },
L: { axis: 'x', side: -H, vec: new THREE.Vector3(-1,0, 0) },
F: { axis: 'z', side: H, vec: new THREE.Vector3(0, 0, 1) },
B: { axis: 'z', side: -H, vec: new THREE.Vector3(0, 0,-1) }
};
const FACE_ORDER = ['U','R','F','D','L','B'];
// ---------- 贴纸纹理(黑色塑料底座 + 彩色贴纸) ----------
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function makeStickerTexture(hex) {
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0b0b0d';
roundedRect(ctx, 0, 0, size, size, 14);
ctx.fill();
const m = 17;
ctx.fillStyle = hex;
roundedRect(ctx, m, m, size - 2 * m, size - 2 * m, 9);
ctx.fill();
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
return tex;
}
const TEX = {};
for (const f in COL_HEX) TEX[f] = makeStickerTexture(COL_HEX[f]);
const blackPlastic = new THREE.MeshStandardMaterial({ color: 0x0a0a0c, roughness: 0.62, metalness: 0.08 });
function stickerMaterial(face) {
return new THREE.MeshStandardMaterial({ map: TEX[face], roughness: 0.3, metalness: 0.05 });
}
// ---------- 8 个角块 ----------
let cubies = [];
const pivot = new THREE.Group();
scene.add(pivot);
function createCubie(x, y, z) {
// 材质顺序:+x, -x, +y, -y, +z, -z
const mats = [
x === H ? stickerMaterial('R') : blackPlastic,
x === -H ? stickerMaterial('L') : blackPlastic,
y === H ? stickerMaterial('U') : blackPlastic,
y === -H ? stickerMaterial('D') : blackPlastic,
z === H ? stickerMaterial('F') : blackPlastic,
z === -H ? stickerMaterial('B') : blackPlastic
];
const geo = new THREE.BoxGeometry(SIZE, SIZE, SIZE);
const mesh = new THREE.Mesh(geo, mats);
mesh.position.set(x, y, z);
scene.add(mesh);
// 逻辑颜色
const colors = {
px: x === H ? COL_CHAR.R : null,
nx: x === -H ? COL_CHAR.L : null,
py: y === H ? COL_CHAR.U : null,
ny: y === -H ? COL_CHAR.D : null,
pz: z === H ? COL_CHAR.F : null,
nz: z === -H ? COL_CHAR.B : null
};
return { mesh, home: new THREE.Vector3(x, y, z), colors };
}
function buildCube() {
cubies.forEach(c => scene.remove(c.mesh));
cubies = [];
for (const x of [-H, H])
for (const y of [-H, H])
for (const z of [-H, H])
cubies.push(createCubie(x, y, z));
}
// ---------- 旋转 ----------
const moveQueue = [];
let busy = false;
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function snap(v) { return Math.round(v * 2) / 2; }
function performMove(face, dir, duration, done) {
const def = FACE_DEF[face];
const axis = def.axis;
const side = def.side;
const selected = cubies.filter(c => Math.abs(c.mesh.position[axis] - side) < 0.01);
selected.forEach(c => pivot.attach(c.mesh));
const angle = -dir * Math.PI / 2;
const start = performance.now();
function frame(now) {
let t = Math.min(1, (now - start) / duration);
const e = easeInOutCubic(t);
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle * e;
if (t < 1) {
requestAnimationFrame(frame);
} else {
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle;
selected.forEach(c => {
scene.attach(c.mesh);
c.mesh.position.x = snap(c.mesh.position.x);
c.mesh.position.y = snap(c.mesh.position.y);
c.mesh.position.z = snap(c.mesh.position.z);
});
pivot.rotation.set(0, 0, 0);
done();
}
}
requestAnimationFrame(frame);
}
function enqueueMove(face, dir, duration, opts = {}) {
moveQueue.push({
face, dir, duration,
count: opts.count !== false,
record: !!opts.record,
onDone: opts.onDone
});
if (!busy) processQueue();
}
function processQueue() {
if (moveQueue.length === 0) { busy = false; return; }
busy = true;
const m = moveQueue.shift();
if (m.record) userSeq.push({ face: m.face, dir: m.dir });
performMove(m.face, m.dir, m.duration, () => {
if (m.count) {
moveCount++;
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
} else {
statusEl.textContent = '步数:' + moveCount;
statusEl.classList.remove('solved');
}
}
if (m.onDone) m.onDone();
updateRecordPanel();
processQueue();
});
}
function isSolved() {
return cubies.every(c => {
const p = c.mesh.position;
if (snap(p.x) !== c.home.x ||
snap(p.y) !== c.home.y ||
snap(p.z) !== c.home.z) return false;
const q = c.mesh.quaternion;
return Math.abs(q.w) > 0.9999 &&
Math.abs(q.x) + Math.abs(q.y) + Math.abs(q.z) < 0.0001;
});
}
// ---------- 局面读取 ----------
function getFaceSlots(face) {
const s = [];
for (let r = 0; r < 2; r++)
for (let c = 0; c < 2; c++) {
const x = c === 0 ? -H : H;
const yTop = r === 0 ? H : -H;
let p;
if (face === 'U') p = [x, H, r === 0 ? -H : H];
else if (face === 'D') p = [x, -H, r === 0 ? H : -H];
else if (face === 'F') p = [x, yTop, H];
else if (face === 'B') p = [c === 0 ? H : -H, yTop, -H];
else if (face === 'R') p = [H, yTop, c === 0 ? H : -H];
else p = [-H, yTop, c === 0 ? -H : H];
s.push(new THREE.Vector3(p[0], p[1], p[2]));
}
return s;
}
function axisToKey(v) {
const x = Math.round(v.x), y = Math.round(v.y), z = Math.round(v.z);
if (x === 1) return 'px';
if (x === -1) return 'nx';
if (y === 1) return 'py';
if (y === -1) return 'ny';
if (z === 1) return 'pz';
if (z === -1) return 'nz';
return '';
}
function faceColorString(face) {
const slots = getFaceSlots(face);
const n = FACE_DEF[face].vec;
let out = '';
for (const p of slots) {
const cubie = cubies.find(c => c.mesh.position.distanceTo(p) < 0.01);
if (!cubie) { out += '?'; continue; }
const n0 = n.clone().applyQuaternion(cubie.mesh.quaternion.clone().invert());
out += cubie.colors[axisToKey(n0)] || '?';
}
return out;
}
function getState() {
const state = {};
for (const f of FACE_ORDER) state[f] = faceColorString(f);
return state;
}
function stateToText(state) {
return FACE_ORDER.map(f => f + ': ' + state[f]).join('\n');
}
function moveNotation(m) {
return m.face + (m.dir === -1 ? '′' : '');
}
function seqToText(seq) {
return seq.length ? seq.map(moveNotation).join(' ') : '(无)';
}
// ---------- 棋谱 ----------
function buildRecordText() {
const lines = [];
lines.push('二阶魔方棋谱');
lines.push('');
lines.push('【打乱序列】');
lines.push(seqToText(scrambleSeq));
lines.push('');
lines.push('【初始局面(打乱后)】');
lines.push(initialState ? stateToText(initialState) : '(打乱中…)');
lines.push('');
lines.push('【当前局面】');
lines.push(stateToText(getState()));
lines.push('');
lines.push('【完整操作序列】');
lines.push(seqToText([...scrambleSeq, ...userSeq]));
return lines.join('\n');
}
function updateRecordPanel() {
recordPanel.textContent = buildRecordText();
}
async function copyRecord() {
const text = buildRecordText();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
const old = copyBtn.textContent;
copyBtn.textContent = ok ? '✅ 已复制' : '❌ 复制失败';
setTimeout(() => { copyBtn.textContent = old; }, 1500);
}
// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
moveQueue.length = 0;
busy = false;
cubies.forEach(c => { if (c.mesh.parent === pivot) scene.attach(c.mesh); });
pivot.rotation.set(0, 0, 0);
cubies.forEach(c => {
c.mesh.position.copy(c.home);
c.mesh.quaternion.identity();
});
}
function generateScramble(n = 25) {
const faces = Object.keys(FACE_DEF);
const seq = [];
let last = null;
for (let i = 0; i < n; i++) {
let f, d;
do {
f = faces[Math.floor(Math.random() * faces.length)];
d = Math.random() < 0.5 ? 1 : -1;
} while (last && last === f);
seq.push({ face: f, dir: d });
last = f;
}
return seq;
}
function scramble() {
resetCubeInternal();
userSeq = [];
initialState = null;
moveCount = 0;
statusEl.textContent = '打乱中…';
statusEl.classList.remove('solved');
scrambleSeq = generateScramble(25);
scrambleSeq.forEach((m, idx) => {
enqueueMove(m.face, m.dir, 90, {
count: false,
record: false,
onDone: idx === scrambleSeq.length - 1 ? () => {
initialState = getState();
moveCount = 0;
statusEl.textContent = '已打乱,开始复原吧';
} : undefined
});
});
}
function resetCube() {
resetCubeInternal();
moveCount = 0;
scrambleSeq = [];
userSeq = [];
initialState = null;
statusEl.textContent = '已还原到初始状态';
statusEl.classList.remove('solved');
updateRecordPanel();
}
// ---------- 按钮 & 键盘 ----------
function buildButtons() {
const box = document.getElementById('moveButtons');
for (const face of FACE_ORDER) {
const cw = document.createElement('button');
cw.className = 'cw';
cw.textContent = face;
cw.title = face + ' 顺时针';
cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: true, record: true }));
const ccw = document.createElement('button');
ccw.className = 'ccw';
ccw.textContent = face + '′';
ccw.title = face + ' 逆时针';
ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: true, record: true }));
box.appendChild(cw);
box.appendChild(ccw);
}
document.getElementById('scrambleBtn').addEventListener('click', scramble);
document.getElementById('resetBtn').addEventListener('click', resetCube);
copyBtn.addEventListener('click', copyRecord);
window.addEventListener('keydown', e => {
const k = e.key.toUpperCase();
if (!FACE_DEF[k]) return;
const dir = e.shiftKey ? -1 : 1;
enqueueMove(k, dir, 220, { count: true, record: true });
});
}
// ---------- 渲染循环 & 自适应 ----------
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
const w = stage.clientWidth, h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
scramble();
updateRecordPanel();
animate();
</script>
</body>
</html>

4,整体旋转+固定视角
提示词:
取消魔方的自由旋转,改成特定的旋转动作,并把所有的旋转操作纳入常规操作集,类似URD这些
deepseek生成结果:。。。。。。略
5,底面镜子
经过一些列的优化之后,终于得到带底面镜子的版本:

6,bug修复
提示词:
把y和z操作互换,另外已复原的判定有问题
deepseek生成结果:

提示词:
还有4个地方需要调整:
(1)初始局面应该是上白下黄左绿右蓝前红后橙,复原状态下显示的是上白右蓝前红,你现在是白绿红
(2)局面显示改成UDLRFB的顺序
(3)局面显示里面,下面D这一面的4个格子,要按照和上面U这一面的顺序一样的顺序
(4)后面、左边、下面的转动,你现在的顺时针和逆时针理解是反的
deepseek生成结果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>二阶魔方 3D</title>
<style>
:root {
--bg1: #161b26;
--bg2: #1f2635;
--txt: #e8edf5;
--dim: #8fa0bf;
--panel: #1b2231;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 700px at 50% -10%, var(--bg2), var(--bg1));
color: var(--txt);
padding: 20px 16px 30px;
overflow: auto;
}
h1 { margin: 0 0 4px; font-size: 24px; letter-spacing: 3px; text-align: center; }
#status {
min-height: 24px;
margin: 2px 0 10px;
font-size: 14px;
color: var(--dim);
transition: color .3s;
text-align: center;
}
#status.solved { color: #7ee08a; font-weight: bold; animation: pop .55s ease; }
@keyframes pop {
0% { transform: scale(.6); opacity: 0; }
70% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
#layout {
display: flex;
gap: 22px;
width: 100%;
max-width: 1100px;
margin: 0 auto;
align-items: flex-start;
justify-content: center;
}
#leftCol {
flex: 1 1 auto;
min-width: 0;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
#rightCol {
flex: 0 0 330px;
width: 330px;
position: sticky;
top: 20px;
}
#stage {
width: 100%;
max-width: 560px;
height: 58vh;
min-height: 360px;
max-height: 560px;
}
#controls { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; align-items: center; }
.move-row { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; }
#faceButtons { flex-wrap: nowrap; gap: 5px; }
#faceButtons button { padding: 8px 6px; min-width: 36px; font-size: 13px; }
.move-label { font-size: 12px; color: #6c7b99; letter-spacing: 1px; margin-bottom: -2px; }
button {
border: none;
border-radius: 9px;
padding: 10px 15px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
background: #37425c;
color: #fff;
box-shadow: 0 3px 0 #1a2130;
transition: transform .06s, background .15s, box-shadow .06s;
min-width: 48px;
letter-spacing: .5px;
}
button:hover { background: #46557a; }
button:active { transform: translateY(3px); box-shadow: 0 0 0 #1a2130; }
button.cw { background: #3153a8; }
button.cw:hover { background: #436bcb; }
button.ccw { background: #a84f33; }
button.ccw:hover { background: #c9633f; }
button.rot { background: #6b4fa0; }
button.rot:hover { background: #8462c4; }
button.tool { background: #2f7a55; }
button.tool:hover { background: #3b9768; }
button.tool.warn { background: #8a3d3d; }
button.tool.warn:hover { background: #a84c4c; }
button.copy { background: #2a6e8f; }
button.copy:hover { background: #3588ad; }
.hint {
margin-top: 10px;
font-size: 12.5px;
color: #6c7b99;
text-align: center;
line-height: 1.8;
}
kbd {
background: #2a3344;
border: 1px solid #4a5a7a;
border-radius: 5px;
padding: 1px 7px;
font-size: 12px;
}
#recordBox { display: flex; flex-direction: column; gap: 10px; }
#recordTitle { font-size: 15px; font-weight: 700; color: #cfd9ea; letter-spacing: 1px; text-align: center; }
#recordPanel {
background: var(--panel);
border: 1px solid #2a3344;
border-radius: 10px;
padding: 14px 16px;
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.65;
color: #cfd9ea;
white-space: pre-wrap;
word-break: break-all;
max-height: 58vh;
overflow: auto;
margin: 0;
}
#copyBtn { width: 100%; }
@media (max-width: 920px) {
#layout { flex-direction: column; align-items: center; }
#rightCol { width: 100%; max-width: 560px; position: static; }
#recordPanel { max-height: 300px; }
#stage { height: 46vh; min-height: 280px; }
}
@media (max-width: 560px) {
button { padding: 8px 10px; font-size: 13px; min-width: 40px; }
}
</style>
</head>
<body>
<h1>二阶魔方</h1>
<div id="status">加载中…</div>
<div id="layout">
<div id="leftCol">
<div id="stage"></div>
<div id="controls">
<div class="move-row" id="faceButtons"></div>
<div class="move-label">整体转体(不计步数)</div>
<div class="move-row" id="rotButtons"></div>
<div class="move-row">
<button class="tool" id="scrambleBtn">打乱</button>
<button class="tool warn" id="resetBtn">复原</button>
</div>
</div>
<div class="hint">
键盘:<kbd>U</kbd><kbd>R</kbd><kbd>F</kbd><kbd>D</kbd><kbd>L</kbd><kbd>B</kbd> 转层,
<kbd>X</kbd><kbd>Y</kbd><kbd>Z</kbd> 整体转体,<kbd>Shift</kbd> + 字母为逆时针。<br>
下方镜面面板会完整显示底面颜色。
</div>
</div>
<div id="rightCol">
<div id="recordBox">
<div id="recordTitle">📋 棋谱</div>
<button class="tool copy" id="copyBtn">📋 一键复制棋谱</button>
<pre id="recordPanel"></pre>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ---------- 场景 ----------
const stage = document.getElementById('stage');
const statusEl = document.getElementById('status');
const recordPanel = document.getElementById('recordPanel');
const copyBtn = document.getElementById('copyBtn');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, stage.clientWidth / stage.clientHeight, 0.1, 100);
camera.position.set(3.4, 3.2, 5.6);
camera.lookAt(0, -0.4, 0);
scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const light1 = new THREE.DirectionalLight(0xffffff, 1.3);
light1.position.set(5, 9, 6);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.45);
light2.position.set(-4, -3, -2);
scene.add(light2);
// ---------- 颜色:上白 下黄 左绿 右蓝 前红 后橙 ----------
const COL_HEX = {
U: '#f5f5f5', // 上:白
D: '#ffd500', // 下:黄
R: '#3b82f6', // 右:蓝
L: '#3aab4e', // 左:绿
F: '#e33c3c', // 前:红
B: '#ff8c1a' // 后:橙
};
const COL_CHAR = { U: 'W', D: 'Y', R: 'B', L: 'G', F: 'R', B: 'O' };
const CHAR_HEX = { W: '#f5f5f5', Y: '#ffd500', B: '#3b82f6', G: '#3aab4e', R: '#e33c3c', O: '#ff8c1a' };
// ---------- 坐标体系:块中心 ±0.5,块宽 1.0,完全紧贴 ----------
const H = 0.5;
const SIZE = 1.0;
const FACE_DEF = {
U: { axis: 'y', side: H, vec: new THREE.Vector3(0, 1, 0) },
D: { axis: 'y', side: -H, vec: new THREE.Vector3(0,-1, 0) },
R: { axis: 'x', side: H, vec: new THREE.Vector3(1, 0, 0) },
L: { axis: 'x', side: -H, vec: new THREE.Vector3(-1,0, 0) },
F: { axis: 'z', side: H, vec: new THREE.Vector3(0, 0, 1) },
B: { axis: 'z', side: -H, vec: new THREE.Vector3(0, 0,-1) }
};
// 整体转体:y 和 z 已互换 —— y 操作绕 z 轴,z 操作绕 y 轴
const ROT_DEF = {
x: { axis: 'x' },
y: { axis: 'z' },
z: { axis: 'y' }
};
// 局面显示顺序:U D L R F B
const FACE_ORDER = ['U','D','L','R','F','B'];
// ---------- 贴纸纹理 ----------
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function makeStickerTexture(hex) {
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0b0b0d';
roundedRect(ctx, 0, 0, size, size, 14);
ctx.fill();
const m = 17;
ctx.fillStyle = hex;
roundedRect(ctx, m, m, size - 2 * m, size - 2 * m, 9);
ctx.fill();
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
return tex;
}
const TEX = {};
for (const f in COL_HEX) TEX[f] = makeStickerTexture(COL_HEX[f]);
const blackPlastic = new THREE.MeshStandardMaterial({ color: 0x0a0a0c, roughness: 0.62, metalness: 0.08 });
function stickerMaterial(face) {
return new THREE.MeshStandardMaterial({ map: TEX[face], roughness: 0.3, metalness: 0.05 });
}
// ---------- 镜面面板(直接绘制底面颜色) ----------
const mirrorCanvas = document.createElement('canvas');
mirrorCanvas.width = mirrorCanvas.height = 256;
const mctx = mirrorCanvas.getContext('2d');
const mirrorTex = new THREE.CanvasTexture(mirrorCanvas);
mirrorTex.colorSpace = THREE.SRGBColorSpace;
const mirrorMat = new THREE.MeshBasicMaterial({ map: mirrorTex });
const mirrorMesh = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 2.6), mirrorMat);
mirrorMesh.rotation.x = -Math.PI / 2;
mirrorMesh.position.y = -1.9;
scene.add(mirrorMesh);
function updateMirror() {
const d = getState().D || '????';
// D 面顺序与 U 面一致:0=后左 1=后右 2=前左 3=前右
mctx.clearRect(0, 0, 256, 256);
mctx.fillStyle = '#0d0f14';
roundedRect(mctx, 0, 0, 256, 256, 18);
mctx.fill();
// 镜面朝向:面板上方=后排,面板下方(近相机)=前排
const cell = 96, gap = 10;
const ox = (256 - cell * 2 - gap) / 2;
const oy = ox;
for (let i = 0; i < 4; i++) {
const r = i >> 1, c = i & 1;
mctx.fillStyle = CHAR_HEX[d[i]] || '#999';
roundedRect(mctx, ox + c * (cell + gap), oy + r * (cell + gap), cell, cell, 10);
mctx.fill();
}
mirrorTex.needsUpdate = true;
}
// ---------- 8 个角块 ----------
let cubies = [];
const pivot = new THREE.Group();
scene.add(pivot);
function createCubie(x, y, z) {
const mats = [
x === H ? stickerMaterial('R') : blackPlastic,
x === -H ? stickerMaterial('L') : blackPlastic,
y === H ? stickerMaterial('U') : blackPlastic,
y === -H ? stickerMaterial('D') : blackPlastic,
z === H ? stickerMaterial('F') : blackPlastic,
z === -H ? stickerMaterial('B') : blackPlastic
];
const geo = new THREE.BoxGeometry(SIZE, SIZE, SIZE);
const mesh = new THREE.Mesh(geo, mats);
mesh.position.set(x, y, z);
scene.add(mesh);
const colors = {
px: x === H ? COL_CHAR.R : null,
nx: x === -H ? COL_CHAR.L : null,
py: y === H ? COL_CHAR.U : null,
ny: y === -H ? COL_CHAR.D : null,
pz: z === H ? COL_CHAR.F : null,
nz: z === -H ? COL_CHAR.B : null
};
return { mesh, home: new THREE.Vector3(x, y, z), colors };
}
function buildCube() {
cubies.forEach(c => scene.remove(c.mesh));
cubies = [];
for (const x of [-H, H])
for (const y of [-H, H])
for (const z of [-H, H])
cubies.push(createCubie(x, y, z));
}
// ---------- 旋转 ----------
const moveQueue = [];
let busy = false;
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function snap(v) { return Math.round(v * 2) / 2; }
function performRotation(selected, axis, dir, duration, done) {
selected.forEach(c => pivot.attach(c.mesh));
const angle = -dir * Math.PI / 2;
const start = performance.now();
function frame(now) {
let t = Math.min(1, (now - start) / duration);
const e = easeInOutCubic(t);
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle * e;
if (t < 1) {
requestAnimationFrame(frame);
} else {
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle;
selected.forEach(c => {
scene.attach(c.mesh);
c.mesh.position.x = snap(c.mesh.position.x);
c.mesh.position.y = snap(c.mesh.position.y);
c.mesh.position.z = snap(c.mesh.position.z);
});
pivot.rotation.set(0, 0, 0);
done();
}
}
requestAnimationFrame(frame);
}
function performMove(face, dir, duration, done) {
if (FACE_DEF[face]) {
const def = FACE_DEF[face];
const selected = cubies.filter(c => Math.abs(c.mesh.position[def.axis] - def.side) < 0.01);
// 顺时针 = 从该面外侧看。轴正侧(U/R/F)与轴负侧(D/L/B)旋转角符号相反
const flip = def.side > 0 ? 1 : -1;
performRotation(selected, def.axis, dir * flip, duration, done);
} else if (ROT_DEF[face]) {
performRotation(cubies.slice(), ROT_DEF[face].axis, dir, duration, done);
}
}
function enqueueMove(face, dir, duration, opts = {}) {
moveQueue.push({
face, dir, duration,
count: opts.count !== false,
record: !!opts.record,
onDone: opts.onDone
});
if (!busy) processQueue();
}
function processQueue() {
if (moveQueue.length === 0) { busy = false; return; }
busy = true;
const m = moveQueue.shift();
if (m.record) userSeq.push({ face: m.face, dir: m.dir });
performMove(m.face, m.dir, m.duration, () => {
if (m.count) {
moveCount++;
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
} else {
statusEl.textContent = '步数:' + moveCount;
statusEl.classList.remove('solved');
}
}
if (m.onDone) m.onDone();
updateRecordPanel();
processQueue();
});
}
// ---------- 轴判定(抗浮点误差) ----------
function axisToKey(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
if (ax >= ay && ax >= az) return v.x >= 0 ? 'px' : 'nx';
if (ay >= ax && ay >= az) return v.y >= 0 ? 'py' : 'ny';
return v.z >= 0 ? 'pz' : 'nz';
}
// ---------- 复原判定:6 个面每面 4 个贴纸颜色一致 ----------
function isSolved() {
const st = getState();
for (const f of FACE_ORDER) {
const s = st[f];
if (!s || s.length !== 4) return false;
for (let i = 1; i < 4; i++) {
if (s[i] !== s[0]) return false;
}
}
return true;
}
// ---------- 局面读取 ----------
function getFaceSlots(face) {
const s = [];
for (let r = 0; r < 2; r++)
for (let c = 0; c < 2; c++) {
const x = c === 0 ? -H : H;
const yTop = r === 0 ? H : -H;
let p;
if (face === 'U') p = [x, H, r === 0 ? -H : H];
else if (face === 'D') p = [x, -H, r === 0 ? -H : H]; // 与 U 面顺序一致
else if (face === 'F') p = [x, yTop, H];
else if (face === 'B') p = [c === 0 ? H : -H, yTop, -H];
else if (face === 'R') p = [H, yTop, c === 0 ? H : -H];
else p = [-H, yTop, c === 0 ? -H : H]; // L
s.push(new THREE.Vector3(p[0], p[1], p[2]));
}
return s;
}
function faceColorString(face) {
const slots = getFaceSlots(face);
const n = FACE_DEF[face].vec;
let out = '';
for (const p of slots) {
const cubie = cubies.find(c => c.mesh.position.distanceTo(p) < 0.01);
if (!cubie) { out += '?'; continue; }
const n0 = n.clone().applyQuaternion(cubie.mesh.quaternion.clone().invert());
out += cubie.colors[axisToKey(n0)] || '?';
}
return out;
}
function getState() {
const state = {};
for (const f of FACE_ORDER) state[f] = faceColorString(f);
return state;
}
function stateToText(state) {
return FACE_ORDER.map(f => f + ': ' + state[f]).join('\n');
}
function moveNotation(m) {
return m.face + (m.dir === -1 ? '′' : '');
}
function seqToText(seq) {
return seq.length ? seq.map(moveNotation).join(' ') : '(无)';
}
// ---------- 棋谱 ----------
function buildRecordText() {
const lines = [];
lines.push('二阶魔方棋谱');
lines.push('');
lines.push('【打乱序列】');
lines.push(seqToText(scrambleSeq));
lines.push('');
lines.push('【初始局面(打乱后)】');
lines.push(initialState ? stateToText(initialState) : '(打乱中…)');
lines.push('');
lines.push('【当前局面】');
lines.push(stateToText(getState()));
lines.push('');
lines.push('【完整操作序列】');
lines.push(seqToText([...scrambleSeq, ...userSeq]));
return lines.join('\n');
}
function updateRecordPanel() {
recordPanel.textContent = buildRecordText();
updateMirror();
}
async function copyRecord() {
const text = buildRecordText();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
const old = copyBtn.textContent;
copyBtn.textContent = ok ? '✅ 已复制' : '❌ 复制失败';
setTimeout(() => { copyBtn.textContent = old; }, 1500);
}
// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
moveQueue.length = 0;
busy = false;
cubies.forEach(c => { if (c.mesh.parent === pivot) scene.attach(c.mesh); });
pivot.rotation.set(0, 0, 0);
cubies.forEach(c => {
c.mesh.position.copy(c.home);
c.mesh.quaternion.identity();
});
}
function generateScramble(n = 25) {
const faces = Object.keys(FACE_DEF);
const seq = [];
let last = null;
for (let i = 0; i < n; i++) {
let f, d;
do {
f = faces[Math.floor(Math.random() * faces.length)];
d = Math.random() < 0.5 ? 1 : -1;
} while (last && last === f);
seq.push({ face: f, dir: d });
last = f;
}
return seq;
}
function scramble() {
resetCubeInternal();
userSeq = [];
initialState = null;
moveCount = 0;
statusEl.textContent = '打乱中…';
statusEl.classList.remove('solved');
scrambleSeq = generateScramble(25);
scrambleSeq.forEach((m, idx) => {
enqueueMove(m.face, m.dir, 90, {
count: false,
record: false,
onDone: idx === scrambleSeq.length - 1 ? () => {
initialState = getState();
moveCount = 0;
statusEl.textContent = '已打乱,开始复原吧';
} : undefined
});
});
}
function resetCube() {
resetCubeInternal();
moveCount = 0;
scrambleSeq = [];
userSeq = [];
initialState = null;
statusEl.textContent = '已还原到初始状态';
statusEl.classList.remove('solved');
updateRecordPanel();
}
// ---------- 按钮 & 键盘 ----------
function buildButtons() {
const faceBox = document.getElementById('faceButtons');
for (const face of ['U','R','F','D','L','B']) {
const cw = document.createElement('button');
cw.className = 'cw';
cw.textContent = face;
cw.title = face + ' 顺时针';
cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: true, record: true }));
const ccw = document.createElement('button');
ccw.className = 'ccw';
ccw.textContent = face + '′';
ccw.title = face + ' 逆时针';
ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: true, record: true }));
faceBox.appendChild(cw);
faceBox.appendChild(ccw);
}
const rotBox = document.getElementById('rotButtons');
for (const face of ['x','y','z']) {
const cw = document.createElement('button');
cw.className = 'rot';
cw.textContent = face;
cw.title = face + ' 整体顺时针';
cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: false, record: true }));
const ccw = document.createElement('button');
ccw.className = 'rot';
ccw.textContent = face + '′';
ccw.title = face + ' 整体逆时针';
ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: false, record: true }));
rotBox.appendChild(cw);
rotBox.appendChild(ccw);
}
document.getElementById('scrambleBtn').addEventListener('click', scramble);
document.getElementById('resetBtn').addEventListener('click', resetCube);
copyBtn.addEventListener('click', copyRecord);
window.addEventListener('keydown', e => {
const k = e.key.toUpperCase();
if (!/^[URFDLBXYZ]$/.test(k)) return;
let moveKey = k;
if (k === 'X' || k === 'Y' || k === 'Z') moveKey = k.toLowerCase();
const isLayer = !!FACE_DEF[moveKey];
const dir = e.shiftKey ? -1 : 1;
enqueueMove(moveKey, dir, 220, { count: isLayer, record: true });
});
}
// ---------- 渲染循环 & 自适应 ----------
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
const w = stage.clientWidth, h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
scramble();
updateRecordPanel();
animate();
</script>
</body>
</html>

7,调试框
提示词:
新增一个文本框,输入操作序列,如F′ L U′ L U D F B′ D F U′ L′ R B′ R′ D′ R′ ,则依次执行对应操作
打乱序列和完整操作序列里面的’改成英文的’
deepseek生成结果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>二阶魔方 3D</title>
<style>
:root {
--bg1: #161b26;
--bg2: #1f2635;
--txt: #e8edf5;
--dim: #8fa0bf;
--panel: #1b2231;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 700px at 50% -10%, var(--bg2), var(--bg1));
color: var(--txt);
padding: 20px 16px 30px;
overflow: auto;
}
h1 { margin: 0 0 4px; font-size: 24px; letter-spacing: 3px; text-align: center; }
#status {
min-height: 24px;
margin: 2px 0 10px;
font-size: 14px;
color: var(--dim);
transition: color .3s;
text-align: center;
}
#status.solved { color: #7ee08a; font-weight: bold; animation: pop .55s ease; }
@keyframes pop {
0% { transform: scale(.6); opacity: 0; }
70% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
#layout {
display: flex;
gap: 22px;
width: 100%;
max-width: 1100px;
margin: 0 auto;
align-items: flex-start;
justify-content: center;
}
#leftCol {
flex: 1 1 auto;
min-width: 0;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
#rightCol {
flex: 0 0 330px;
width: 330px;
position: sticky;
top: 20px;
}
#stage {
width: 100%;
max-width: 560px;
height: 58vh;
min-height: 360px;
max-height: 560px;
}
#controls { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; align-items: center; }
.move-row { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; }
.move-label { font-size: 12px; color: #6c7b99; letter-spacing: 1px; margin-bottom: -2px; }
/* 12 个面按钮同一行 */
#faceButtons { flex-wrap: nowrap; gap: 5px; }
#faceButtons button { padding: 8px 6px; min-width: 36px; font-size: 13px; }
button {
border: none;
border-radius: 9px;
padding: 10px 15px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
background: #37425c;
color: #fff;
box-shadow: 0 3px 0 #1a2130;
transition: transform .06s, background .15s, box-shadow .06s;
min-width: 48px;
letter-spacing: .5px;
}
button:hover { background: #46557a; }
button:active { transform: translateY(3px); box-shadow: 0 0 0 #1a2130; }
button.cw { background: #3153a8; }
button.cw:hover { background: #436bcb; }
button.ccw { background: #a84f33; }
button.ccw:hover { background: #c9633f; }
button.rot { background: #6b4fa0; }
button.rot:hover { background: #8462c4; }
button.tool { background: #2f7a55; }
button.tool:hover { background: #3b9768; }
button.tool.warn { background: #8a3d3d; }
button.tool.warn:hover { background: #a84c4c; }
button.copy { background: #2a6e8f; }
button.copy:hover { background: #3588ad; }
.hint {
margin-top: 10px;
font-size: 12.5px;
color: #6c7b99;
text-align: center;
line-height: 1.8;
}
kbd {
background: #2a3344;
border: 1px solid #4a5a7a;
border-radius: 5px;
padding: 1px 7px;
font-size: 12px;
}
#recordBox { display: flex; flex-direction: column; gap: 10px; }
#recordTitle { font-size: 15px; font-weight: 700; color: #cfd9ea; letter-spacing: 1px; text-align: center; }
#recordPanel {
background: var(--panel);
border: 1px solid #2a3344;
border-radius: 10px;
padding: 14px 16px;
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.65;
color: #cfd9ea;
white-space: pre-wrap;
word-break: break-all;
max-height: 58vh;
overflow: auto;
margin: 0;
}
#copyBtn { width: 100%; }
/* 操作序列输入 */
#seqRow {
width: 100%;
max-width: 560px;
margin-top: 4px;
}
#seqInput {
flex: 1;
min-width: 220px;
max-width: 440px;
padding: 9px 12px;
border-radius: 8px;
border: 1px solid #4a5a7a;
background: #161b26;
color: #e8edf5;
font-size: 14px;
font-family: "Cascadia Code", "Consolas", monospace;
}
#seqInput:focus { outline: none; border-color: #6d88c8; }
@media (max-width: 920px) {
#layout { flex-direction: column; align-items: center; }
#rightCol { width: 100%; max-width: 560px; position: static; }
#recordPanel { max-height: 300px; }
#stage { height: 46vh; min-height: 280px; }
}
@media (max-width: 560px) {
button { padding: 8px 10px; font-size: 13px; min-width: 40px; }
#faceButtons { flex-wrap: wrap; }
}
</style>
</head>
<body>
<h1>二阶魔方</h1>
<div id="status">加载中…</div>
<div id="layout">
<div id="leftCol">
<div id="stage"></div>
<div id="controls">
<div class="move-row" id="faceButtons"></div>
<div class="move-label">整体转体(不计步数)</div>
<div class="move-row" id="rotButtons"></div>
<div class="move-row">
<button class="tool" id="scrambleBtn">打乱</button>
<button class="tool warn" id="resetBtn">复原</button>
</div>
<div class="move-row" id="seqRow">
<input id="seqInput" type="text"
placeholder="输入操作序列,如 F′ L U′ L U D F B′ D F U′ L′ R B′ R′ D′ R′" />
<button class="tool" id="seqBtn">执行</button>
</div>
</div>
<div class="hint">
键盘:<kbd>U</kbd><kbd>R</kbd><kbd>F</kbd><kbd>D</kbd><kbd>L</kbd><kbd>B</kbd> 转层,
<kbd>X</kbd><kbd>Y</kbd><kbd>Z</kbd> 整体转体,<kbd>Shift</kbd> + 字母为逆时针。<br>
下方镜面面板会完整显示底面颜色。
</div>
</div>
<div id="rightCol">
<div id="recordBox">
<div id="recordTitle">📋 棋谱</div>
<button class="tool copy" id="copyBtn">📋 一键复制棋谱</button>
<pre id="recordPanel"></pre>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ---------- 场景 ----------
const stage = document.getElementById('stage');
const statusEl = document.getElementById('status');
const recordPanel = document.getElementById('recordPanel');
const copyBtn = document.getElementById('copyBtn');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, stage.clientWidth / stage.clientHeight, 0.1, 100);
camera.position.set(3.4, 3.2, 5.6);
camera.lookAt(0, -0.4, 0);
scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const light1 = new THREE.DirectionalLight(0xffffff, 1.3);
light1.position.set(5, 9, 6);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.45);
light2.position.set(-4, -3, -2);
scene.add(light2);
// ---------- 颜色:上白 下黄 左绿 右蓝 前红 后橙 ----------
const COL_HEX = {
U: '#f5f5f5',
D: '#ffd500',
R: '#3b82f6',
L: '#3aab4e',
F: '#e33c3c',
B: '#ff8c1a'
};
const COL_CHAR = { U: 'W', D: 'Y', R: 'B', L: 'G', F: 'R', B: 'O' };
const CHAR_HEX = { W: '#f5f5f5', Y: '#ffd500', B: '#3b82f6', G: '#3aab4e', R: '#e33c3c', O: '#ff8c1a' };
// ---------- 坐标体系:块中心 ±0.5,块宽 1.0,完全紧贴 ----------
const H = 0.5;
const SIZE = 1.0;
const FACE_DEF = {
U: { axis: 'y', side: H, vec: new THREE.Vector3(0, 1, 0) },
D: { axis: 'y', side: -H, vec: new THREE.Vector3(0,-1, 0) },
R: { axis: 'x', side: H, vec: new THREE.Vector3(1, 0, 0) },
L: { axis: 'x', side: -H, vec: new THREE.Vector3(-1,0, 0) },
F: { axis: 'z', side: H, vec: new THREE.Vector3(0, 0, 1) },
B: { axis: 'z', side: -H, vec: new THREE.Vector3(0, 0,-1) }
};
// 整体转体:y 和 z 已互换 —— y 操作绕 z 轴,z 操作绕 y 轴
const ROT_DEF = {
x: { axis: 'x' },
y: { axis: 'z' },
z: { axis: 'y' }
};
// 局面显示顺序:U D L R F B
const FACE_ORDER = ['U','D','L','R','F','B'];
// ---------- 贴纸纹理 ----------
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function makeStickerTexture(hex) {
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0b0b0d';
roundedRect(ctx, 0, 0, size, size, 14);
ctx.fill();
const m = 17;
ctx.fillStyle = hex;
roundedRect(ctx, m, m, size - 2 * m, size - 2 * m, 9);
ctx.fill();
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
return tex;
}
const TEX = {};
for (const f in COL_HEX) TEX[f] = makeStickerTexture(COL_HEX[f]);
const blackPlastic = new THREE.MeshStandardMaterial({ color: 0x0a0a0c, roughness: 0.62, metalness: 0.08 });
function stickerMaterial(face) {
return new THREE.MeshStandardMaterial({ map: TEX[face], roughness: 0.3, metalness: 0.05 });
}
// ---------- 镜面面板(直接绘制底面颜色) ----------
const mirrorCanvas = document.createElement('canvas');
mirrorCanvas.width = mirrorCanvas.height = 256;
const mctx = mirrorCanvas.getContext('2d');
const mirrorTex = new THREE.CanvasTexture(mirrorCanvas);
mirrorTex.colorSpace = THREE.SRGBColorSpace;
const mirrorMat = new THREE.MeshBasicMaterial({ map: mirrorTex });
const mirrorMesh = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 2.6), mirrorMat);
mirrorMesh.rotation.x = -Math.PI / 2;
mirrorMesh.position.y = -1.9;
scene.add(mirrorMesh);
function updateMirror() {
const d = getState().D || '????';
// D 面顺序与 U 面一致:0=后左 1=后右 2=前左 3=前右
mctx.clearRect(0, 0, 256, 256);
mctx.fillStyle = '#0d0f14';
roundedRect(mctx, 0, 0, 256, 256, 18);
mctx.fill();
// 镜面朝向:面板上方=后排,面板下方(近相机)=前排
const cell = 96, gap = 10;
const ox = (256 - cell * 2 - gap) / 2;
const oy = ox;
for (let i = 0; i < 4; i++) {
const r = i >> 1, c = i & 1;
mctx.fillStyle = CHAR_HEX[d[i]] || '#999';
roundedRect(mctx, ox + c * (cell + gap), oy + r * (cell + gap), cell, cell, 10);
mctx.fill();
}
mirrorTex.needsUpdate = true;
}
// ---------- 8 个角块 ----------
let cubies = [];
const pivot = new THREE.Group();
scene.add(pivot);
function createCubie(x, y, z) {
const mats = [
x === H ? stickerMaterial('R') : blackPlastic,
x === -H ? stickerMaterial('L') : blackPlastic,
y === H ? stickerMaterial('U') : blackPlastic,
y === -H ? stickerMaterial('D') : blackPlastic,
z === H ? stickerMaterial('F') : blackPlastic,
z === -H ? stickerMaterial('B') : blackPlastic
];
const geo = new THREE.BoxGeometry(SIZE, SIZE, SIZE);
const mesh = new THREE.Mesh(geo, mats);
mesh.position.set(x, y, z);
scene.add(mesh);
const colors = {
px: x === H ? COL_CHAR.R : null,
nx: x === -H ? COL_CHAR.L : null,
py: y === H ? COL_CHAR.U : null,
ny: y === -H ? COL_CHAR.D : null,
pz: z === H ? COL_CHAR.F : null,
nz: z === -H ? COL_CHAR.B : null
};
return { mesh, home: new THREE.Vector3(x, y, z), colors };
}
function buildCube() {
cubies.forEach(c => scene.remove(c.mesh));
cubies = [];
for (const x of [-H, H])
for (const y of [-H, H])
for (const z of [-H, H])
cubies.push(createCubie(x, y, z));
}
// ---------- 旋转 ----------
const moveQueue = [];
let busy = false;
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function snap(v) { return Math.round(v * 2) / 2; }
function performRotation(selected, axis, dir, duration, done) {
selected.forEach(c => pivot.attach(c.mesh));
const angle = -dir * Math.PI / 2;
const start = performance.now();
function frame(now) {
let t = Math.min(1, (now - start) / duration);
const e = easeInOutCubic(t);
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle * e;
if (t < 1) {
requestAnimationFrame(frame);
} else {
pivot.rotation.set(0, 0, 0);
pivot.rotation[axis] = angle;
selected.forEach(c => {
scene.attach(c.mesh);
c.mesh.position.x = snap(c.mesh.position.x);
c.mesh.position.y = snap(c.mesh.position.y);
c.mesh.position.z = snap(c.mesh.position.z);
});
pivot.rotation.set(0, 0, 0);
done();
}
}
requestAnimationFrame(frame);
}
function performMove(face, dir, duration, done) {
if (FACE_DEF[face]) {
const def = FACE_DEF[face];
const selected = cubies.filter(c => Math.abs(c.mesh.position[def.axis] - def.side) < 0.01);
// 顺时针 = 从该面外侧看。轴正侧(U/R/F)与轴负侧(D/L/B)旋转角符号相反
const flip = def.side > 0 ? 1 : -1;
performRotation(selected, def.axis, dir * flip, duration, done);
} else if (ROT_DEF[face]) {
performRotation(cubies.slice(), ROT_DEF[face].axis, dir, duration, done);
}
}
function enqueueMove(face, dir, duration, opts = {}) {
moveQueue.push({
face, dir, duration,
count: opts.count !== false,
record: !!opts.record,
onDone: opts.onDone
});
if (!busy) processQueue();
}
function processQueue() {
if (moveQueue.length === 0) { busy = false; return; }
busy = true;
const m = moveQueue.shift();
if (m.record) userSeq.push({ face: m.face, dir: m.dir });
performMove(m.face, m.dir, m.duration, () => {
if (m.count) {
moveCount++;
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
} else {
statusEl.textContent = '步数:' + moveCount;
statusEl.classList.remove('solved');
}
}
if (m.onDone) m.onDone();
updateRecordPanel();
processQueue();
});
}
// ---------- 轴判定(抗浮点误差) ----------
function axisToKey(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
if (ax >= ay && ax >= az) return v.x >= 0 ? 'px' : 'nx';
if (ay >= ax && ay >= az) return v.y >= 0 ? 'py' : 'ny';
return v.z >= 0 ? 'pz' : 'nz';
}
// ---------- 复原判定:6 个面每面 4 个贴纸颜色一致 ----------
function isSolved() {
const st = getState();
for (const f of FACE_ORDER) {
const s = st[f];
if (!s || s.length !== 4) return false;
for (let i = 1; i < 4; i++) {
if (s[i] !== s[0]) return false;
}
}
return true;
}
// ---------- 局面读取 ----------
function getFaceSlots(face) {
const s = [];
for (let r = 0; r < 2; r++)
for (let c = 0; c < 2; c++) {
const x = c === 0 ? -H : H;
const yTop = r === 0 ? H : -H;
let p;
if (face === 'U') p = [x, H, r === 0 ? -H : H];
else if (face === 'D') p = [x, -H, r === 0 ? -H : H]; // 与 U 面顺序一致
else if (face === 'F') p = [x, yTop, H];
else if (face === 'B') p = [c === 0 ? H : -H, yTop, -H];
else if (face === 'R') p = [H, yTop, c === 0 ? H : -H];
else p = [-H, yTop, c === 0 ? -H : H]; // L
s.push(new THREE.Vector3(p[0], p[1], p[2]));
}
return s;
}
function faceColorString(face) {
const slots = getFaceSlots(face);
const n = FACE_DEF[face].vec;
let out = '';
for (const p of slots) {
const cubie = cubies.find(c => c.mesh.position.distanceTo(p) < 0.01);
if (!cubie) { out += '?'; continue; }
const n0 = n.clone().applyQuaternion(cubie.mesh.quaternion.clone().invert());
out += cubie.colors[axisToKey(n0)] || '?';
}
return out;
}
function getState() {
const state = {};
for (const f of FACE_ORDER) state[f] = faceColorString(f);
return state;
}
function stateToText(state) {
return FACE_ORDER.map(f => f + ': ' + state[f]).join('\n');
}
function moveNotation(m) {
return m.face + (m.dir === -1 ? '\'' : '');
}
function seqToText(seq) {
return seq.length ? seq.map(moveNotation).join(' ') : '(无)';
}
// ---------- 棋谱 ----------
function buildRecordText() {
const lines = [];
lines.push('二阶魔方棋谱');
lines.push('');
lines.push('【打乱序列】');
lines.push(seqToText(scrambleSeq));
lines.push('');
lines.push('【初始局面(打乱后)】');
lines.push(initialState ? stateToText(initialState) : '(打乱中…)');
lines.push('');
lines.push('【当前局面】');
lines.push(stateToText(getState()));
lines.push('');
lines.push('【完整操作序列】');
lines.push(seqToText([...scrambleSeq, ...userSeq]));
return lines.join('\n');
}
function updateRecordPanel() {
recordPanel.textContent = buildRecordText();
updateMirror();
}
async function copyRecord() {
const text = buildRecordText();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
const old = copyBtn.textContent;
copyBtn.textContent = ok ? '✅ 已复制' : '❌ 复制失败';
setTimeout(() => { copyBtn.textContent = old; }, 1500);
}
// ---------- 操作序列解析与执行 ----------
function parseSequence(text) {
const normalized = text.replace(/[’′]/g, "'");
const tokens = normalized.trim().split(/[\s,,]+/).filter(Boolean);
const moves = [];
for (const raw of tokens) {
const m = raw.match(/^([xyzurfdbl])([''])?$/i);
if (!m) continue;
const ch = m[1].toLowerCase();
const face = (ch === 'x' || ch === 'y' || ch === 'z') ? ch : ch.toUpperCase();
const dir = m[2] === "'" ? -1 : 1;
moves.push({ face, dir });
}
return moves;
}
function executeSequence(text) {
const moves = parseSequence(text);
if (moves.length === 0) {
statusEl.textContent = '无法解析的操作序列';
statusEl.classList.remove('solved');
return;
}
moves.forEach((mv, i) => {
const isLayer = !!FACE_DEF[mv.face];
enqueueMove(mv.face, mv.dir, 260, {
count: isLayer,
record: true,
onDone: i === moves.length - 1 ? () => {
if (!isSolved()) {
statusEl.textContent = '序列执行完成(' + moves.length + ' 步操作)';
}
} : undefined
});
});
}
// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
moveQueue.length = 0;
busy = false;
cubies.forEach(c => { if (c.mesh.parent === pivot) scene.attach(c.mesh); });
pivot.rotation.set(0, 0, 0);
cubies.forEach(c => {
c.mesh.position.copy(c.home);
c.mesh.quaternion.identity();
});
}
function generateScramble(n = 25) {
const faces = Object.keys(FACE_DEF);
const seq = [];
let last = null;
for (let i = 0; i < n; i++) {
let f, d;
do {
f = faces[Math.floor(Math.random() * faces.length)];
d = Math.random() < 0.5 ? 1 : -1;
} while (last && last === f);
seq.push({ face: f, dir: d });
last = f;
}
return seq;
}
function scramble() {
resetCubeInternal();
userSeq = [];
initialState = null;
moveCount = 0;
statusEl.textContent = '打乱中…';
statusEl.classList.remove('solved');
scrambleSeq = generateScramble(25);
scrambleSeq.forEach((m, idx) => {
enqueueMove(m.face, m.dir, 90, {
count: false,
record: false,
onDone: idx === scrambleSeq.length - 1 ? () => {
initialState = getState();
moveCount = 0;
statusEl.textContent = '已打乱,开始复原吧';
} : undefined
});
});
}
function resetCube() {
resetCubeInternal();
moveCount = 0;
scrambleSeq = [];
userSeq = [];
initialState = null;
statusEl.textContent = '已还原到初始状态';
statusEl.classList.remove('solved');
updateRecordPanel();
}
// ---------- 按钮 & 键盘 ----------
function buildButtons() {
const faceBox = document.getElementById('faceButtons');
for (const face of ['U','R','F','D','L','B']) {
const cw = document.createElement('button');
cw.className = 'cw';
cw.textContent = face;
cw.title = face + ' 顺时针';
cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: true, record: true }));
const ccw = document.createElement('button');
ccw.className = 'ccw';
ccw.textContent = face + '′';
ccw.title = face + ' 逆时针';
ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: true, record: true }));
faceBox.appendChild(cw);
faceBox.appendChild(ccw);
}
const rotBox = document.getElementById('rotButtons');
for (const face of ['x','y','z']) {
const cw = document.createElement('button');
cw.className = 'rot';
cw.textContent = face;
cw.title = face + ' 整体顺时针';
cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: false, record: true }));
const ccw = document.createElement('button');
ccw.className = 'rot';
ccw.textContent = face + '′';
ccw.title = face + ' 整体逆时针';
ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: false, record: true }));
rotBox.appendChild(cw);
rotBox.appendChild(ccw);
}
document.getElementById('scrambleBtn').addEventListener('click', scramble);
document.getElementById('resetBtn').addEventListener('click', resetCube);
copyBtn.addEventListener('click', copyRecord);
const seqInput = document.getElementById('seqInput');
const seqBtn = document.getElementById('seqBtn');
seqBtn.addEventListener('click', () => executeSequence(seqInput.value));
seqInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
executeSequence(seqInput.value);
}
});
window.addEventListener('keydown', e => {
const k = e.key.toUpperCase();
if (!/^[URFDLBXYZ]$/.test(k)) return;
let moveKey = k;
if (k === 'X' || k === 'Y' || k === 'Z') moveKey = k.toLowerCase();
const isLayer = !!FACE_DEF[moveKey];
const dir = e.shiftKey ? -1 : 1;
enqueueMove(moveKey, dir, 220, { count: isLayer, record: true });
});
}
// ---------- 渲染循环 & 自适应 ----------
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
const w = stage.clientWidth, h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
scramble();
updateRecordPanel();
animate();
</script>
</body>
</html>

二阶魔方&spm=1001.2101.3001.5002&articleId=164377600&d=1&t=3&u=616d671214e2440f9a2cd823574e60dd)
1万+

被折叠的 条评论
为什么被折叠?



