Deepseek v4-pro 3d魔方重新测试
- 内容介绍
- 文章标签
- 相关推荐
从 Deepseek v4 pro 3d魔方简要测试 帖子继续
原本测试 只在cherry studio 里面使用auto模式测试
在cherry studio 不知道如何改用max思考模式
现在使用claude + max 思考等级测试
api 耗费 4.39元
思考加首次交付时间: 28m12s
image1195×818 223 KB
https://imgbed.snemc.cn/i/10a7ac09545f.gif(图片大于 4 MB)
测试结果:
非常丝滑
问题:
当视角转到背面的时候鼠标操作垂直方向拖动魔方,旋转角度是反的
在claude中看到deepseek完成一次写入后没有立马交付,而是又自己读了文件进行审阅, 又自己写了python脚本测试不知道怎样用python来测试html的
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3x3 魔方</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0f0f1a;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
user-select: none;
-webkit-user-select: none;
height: 100vh;
width: 100vw;
}
#container {
position: fixed;
inset: 0;
cursor: grab;
}
#container.grabbing { cursor: grabbing; }
#container.orbiting { cursor: move; }
#ui {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 14px;
z-index: 10;
}
#ui button {
padding: 12px 28px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 10px;
background: rgba(255,255,255,0.06);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
color: #ccc;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.03em;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, transform 0.15s;
}
#ui button:hover {
background: rgba(255,255,255,0.12);
border-color: rgba(255,255,255,0.28);
}
#ui button:active {
transform: scale(0.96);
}
#hint {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.38);
font-size: 12.5px;
letter-spacing: 0.04em;
pointer-events: none;
z-index: 10;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="hint">左键拖拽旋转层面 · 右键拖拽旋转视角 · 滚轮缩放</div>
<div id="ui">
<button id="scramble">Scramble</button>
<button id="reset">Reset</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"@tweenjs/tween.js": "https://unpkg.com/@tweenjs/tween.js@20.0.0/dist/tween.esm.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from '@tweenjs/tween.js';
// ============================================================
// 常量
// ============================================================
const CUBIE_SIZE = 0.85;
const EPSILON = 0.35;
const SENSITIVITY = 0.007;
const AXIS_THRESHOLD = 4;
const SNAP_MS = 180;
const SCRAMBLE_MS = 75;
const SCRAMBLE_N = 22;
// Rubik 标准配色 (白顶绿前)
const COLORS = {
right: '#B71234',
left: '#FF5800',
up: '#FFFFFF',
down: '#FFD500',
front: '#009B48',
back: '#0046AD',
};
const AXES = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1),
};
const AXIS_NAMES = ['x', 'y', 'z'];
// ============================================================
// DOM
// ============================================================
const container = document.getElementById('container');
const btnScramble = document.getElementById('scramble');
const btnReset = document.getElementById('reset');
// ============================================================
// Three.js 基础设施
// ============================================================
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0f0f1a');
scene.fog = new THREE.Fog('#0f0f1a', 9, 32);
const camera = new THREE.PerspectiveCamera(
40, window.innerWidth / window.innerHeight, 0.5, 40
);
camera.position.set(4.8, 3.0, 5.4);
camera.lookAt(0, 0, 0);
// ---- OrbitControls: 仅右键旋转视角, 滚轮缩放 ----
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.target.set(0, 0, 0);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.07;
orbitControls.minDistance = 3.5;
orbitControls.maxDistance = 12;
orbitControls.maxPolarAngle = Math.PI * 0.75;
orbitControls.mouseButtons = {
LEFT: null,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE,
};
orbitControls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN,
};
orbitControls.update();
// ============================================================
// 光照与阴影
// ============================================================
scene.add(new THREE.AmbientLight('#8899bb', 0.8));
scene.add(new THREE.HemisphereLight('#ffffff', '#334455', 0.45));
const dirLight = new THREE.DirectionalLight('#ffffff', 1.7);
dirLight.position.set(5, 12, 6);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 40;
dirLight.shadow.camera.left = -8;
dirLight.shadow.camera.right = 8;
dirLight.shadow.camera.top = 8;
dirLight.shadow.camera.bottom = -8;
dirLight.shadow.bias = -0.0002;
dirLight.shadow.normalBias = 0.015;
scene.add(dirLight);
// 阴影接收面
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(18, 18),
new THREE.ShadowMaterial({ opacity: 0.25 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -2.3;
ground.receiveShadow = true;
scene.add(ground);
// ============================================================
// Canvas 纹理 — 圆角贴纸 + 塑料黑边 + 高光
// ============================================================
function createStickerTexture(hexColor) {
const S = 256;
const cv = document.createElement('canvas');
cv.width = S;
cv.height = S;
const ctx = cv.getContext('2d');
// 塑料黑底
ctx.fillStyle = '#141414';
ctx.fillRect(0, 0, S, S);
// 圆角矩形
const m = 26;
const r = 15;
const x = m, y = m, w = S - 2 * m, h = S - 2 * m;
function roundRect(cx, cy, cw, ch, cr) {
ctx.beginPath();
ctx.moveTo(cx + cr, cy);
ctx.arcTo(cx + cw, cy, cx + cw, cy + cr, cr);
ctx.arcTo(cx + cw, cy + ch, cx + cw - cr, cy + ch, cr);
ctx.arcTo(cx, cy + ch, cx, cy + ch - cr, cr);
ctx.arcTo(cx, cy, cx + cr, cy, cr);
ctx.closePath();
}
roundRect(x, y, w, h, r);
ctx.fillStyle = hexColor;
ctx.fill();
// 对角线高光渐变 (模拟贴纸光泽)
const grad = ctx.createLinearGradient(x, y, x + w, y + h);
grad.addColorStop(0, 'rgba(255,255,255,0.24)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.05)');
grad.addColorStop(0.55, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.14)');
roundRect(x, y, w, h, r);
ctx.fillStyle = grad;
ctx.fill();
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = true;
return tex;
}
const stickerTextures = {};
for (const [key, color] of Object.entries(COLORS)) {
stickerTextures[key] = createStickerTexture(color);
}
// 贴纸材质
function stickerMat(tex) {
return new THREE.MeshStandardMaterial({ map: tex, roughness: 0.30, metalness: 0.02 });
}
const sMat = {
right: stickerMat(stickerTextures.right),
left: stickerMat(stickerTextures.left),
up: stickerMat(stickerTextures.up),
down: stickerMat(stickerTextures.down),
front: stickerMat(stickerTextures.front),
back: stickerMat(stickerTextures.back),
};
// 不可见面的黑色塑料
const blackMat = new THREE.MeshStandardMaterial({
color: '#181818', roughness: 0.55, metalness: 0.05,
});
// ============================================================
// 方块构建 — 3×3×3 = 27
// ============================================================
const cubies = [];
const geo = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE);
// 材质数组顺序: [+X, -X, +Y, -Y, +Z, -Z]
function buildMaterials(lx, ly, lz) {
return [
lx === 1 ? sMat.right : blackMat,
lx === -1 ? sMat.left : blackMat,
ly === 1 ? sMat.up : blackMat,
ly === -1 ? sMat.down : blackMat,
lz === 1 ? sMat.front : blackMat,
lz === -1 ? sMat.back : blackMat,
];
}
for (let lx = -1; lx <= 1; lx++) {
for (let ly = -1; ly <= 1; ly++) {
for (let lz = -1; lz <= 1; lz++) {
const mesh = new THREE.Mesh(geo, buildMaterials(lx, ly, lz));
mesh.position.set(lx, ly, lz);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.userData.origin = { x: lx, y: ly, z: lz };
scene.add(mesh);
cubies.push(mesh);
}
}
}
// ============================================================
// 交互状态机
// ============================================================
const IDLE = 'idle';
const AXIS_PENDING = 'axis_pending';
const ROTATING = 'rotating';
const ANIMATING = 'animating';
let state = IDLE;
let clickedCubie = null;
let clickedNormal = null; // 世界空间面法线
let candidateAxes = []; // [{ name, worldAxis, layer, screenDir, perpDir }]
let dragStart = new THREE.Vector2();
let chosenAxis = null; // 确定后的旋转轴信息 (含 perpDir)
let pivot = null;
let totalAngle = 0;
let animTween = null;
const raycaster = new THREE.Raycaster();
raycaster.far = 20;
// ============================================================
// 工具
// ============================================================
/** 3D 世界轴 → 2D 屏幕单位方向 */
function projectAxisToScreen(axis3D) {
const o = new THREE.Vector3(0, 0, 0);
const t = axis3D.clone();
o.project(camera);
t.project(camera);
const W = renderer.domElement.clientWidth;
const H = renderer.domElement.clientHeight;
const sO = new THREE.Vector2((o.x + 1) / 2 * W, (1 - o.y) / 2 * H);
const sT = new THREE.Vector2((t.x + 1) / 2 * W, (1 - t.y) / 2 * H);
const d = sT.clone().sub(sO);
return d.length() < 1e-8 ? d : d.normalize();
}
function getMouse(e) {
const r = renderer.domElement.getBoundingClientRect();
return new THREE.Vector2(e.clientX - r.left, e.clientY - r.top);
}
/** 射线检测 — 返回命中的方块及其世界空间面法线 */
function raycastCubie(mouse) {
const ndc = new THREE.Vector2(
(mouse.x / renderer.domElement.clientWidth) * 2 - 1,
-(mouse.y / renderer.domElement.clientHeight) * 2 + 1,
);
raycaster.setFromCamera(ndc, camera);
const hits = raycaster.intersectObjects(cubies, false);
if (hits.length === 0) return null;
const hit = hits[0];
const n = hit.face.normal.clone();
n.transformDirection(hit.object.matrixWorld);
return { cubie: hit.object, normal: n };
}
/**
* 根据面法线确定 2 个候选旋转轴。
* 逻辑: 法线沿某主轴的 → 排除该轴 → 候选为其余两轴。
*/
function getCandidateAxes(faceNormal) {
const a = [Math.abs(faceNormal.x), Math.abs(faceNormal.y), Math.abs(faceNormal.z)];
let dom = 'z';
if (a[0] >= a[1] && a[0] >= a[2]) dom = 'x';
else if (a[1] >= a[0] && a[1] >= a[2]) dom = 'y';
const others = AXIS_NAMES.filter(n => n !== dom);
return others.map(name => {
const comp = { x: 0, y: 1, z: 2 }[name];
const wp = new THREE.Vector3();
clickedCubie.getWorldPosition(wp);
return {
name,
worldAxis: AXES[name].clone(),
layer: Math.round(wp.getComponent(comp)),
screenDir: null,
perpDir: null,
};
});
}
/** 返回属于指定层的所有方块 (基于世界坐标) */
function findLayerCubies(axisName, layerValue) {
const ci = { x: 0, y: 1, z: 2 }[axisName];
return cubies.filter(c => {
const wp = new THREE.Vector3();
c.getWorldPosition(wp);
return Math.abs(wp.getComponent(ci) - layerValue) < EPSILON;
});
}
/** 创建临时轴心并将选中方块挂载上去 */
function createPivot(layerCubies) {
const p = new THREE.Group();
scene.add(p);
for (const c of layerCubies) p.attach(c);
return p;
}
/** 归还方块到场景, 消除浮点累积误差, 移除轴心 */
function cleanupPivot() {
if (!pivot) return;
const children = [...pivot.children];
for (const c of children) scene.attach(c);
for (const c of children) {
c.position.x = Math.round(c.position.x);
c.position.y = Math.round(c.position.y);
c.position.z = Math.round(c.position.z);
c.rotation.x = Math.round(c.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
c.rotation.y = Math.round(c.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
c.rotation.z = Math.round(c.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
}
scene.remove(pivot);
pivot = null;
}
/** 绝对设置轴心旋转 (非增量, 避免误差累积) */
function setPivotRotation(axis, rad) {
pivot.rotation.set(0, 0, 0);
pivot.rotateOnWorldAxis(axis, rad);
}
/** 重置交互状态 */
function resetInteraction() {
state = IDLE;
clickedCubie = null;
clickedNormal = null;
candidateAxes = [];
chosenAxis = null;
totalAngle = 0;
container.classList.remove('grabbing');
}
// ============================================================
// 程序化旋转 (供 Scramble 使用)
// ============================================================
function executeRotation(axisName, layerValue, targetAngle, duration, cb) {
if (state !== IDLE && state !== ANIMATING) { if (cb) cb(); return; }
state = ANIMATING;
const axis = AXES[axisName].clone();
const layerCubies = findLayerCubies(axisName, layerValue);
if (layerCubies.length === 0) { state = IDLE; if (cb) cb(); return; }
const p = createPivot(layerCubies);
const t = { angle: 0 };
animTween = new TWEEN.Tween(t)
.to({ angle: targetAngle }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ angle }) => {
p.rotation.set(0, 0, 0);
p.rotateOnWorldAxis(axis, angle);
})
.onComplete(() => {
const kids = [...p.children];
for (const k of kids) scene.attach(k);
for (const k of kids) {
k.position.x = Math.round(k.position.x);
k.position.y = Math.round(k.position.y);
k.position.z = Math.round(k.position.z);
k.rotation.x = Math.round(k.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
k.rotation.y = Math.round(k.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
k.rotation.z = Math.round(k.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
}
scene.remove(p);
animTween = null;
state = IDLE;
if (cb) cb();
})
.start();
}
// ============================================================
// 指针事件 — 手势交互核心
// ============================================================
function onPointerDown(e) {
if (e.button !== 0) return;
if (state !== IDLE) return;
if (animTween) { animTween.stop(); animTween = null; }
const m = getMouse(e);
const hit = raycastCubie(m);
if (!hit) return;
clickedCubie = hit.cubie;
clickedNormal = hit.normal.clone();
candidateAxes = getCandidateAxes(clickedNormal);
dragStart.copy(m);
state = AXIS_PENDING;
container.classList.add('grabbing');
}
function onPointerMove(e) {
const m = getMouse(e);
if (state === AXIS_PENDING) {
const delta = m.clone().sub(dragStart);
if (delta.length() < AXIS_THRESHOLD) return;
// ----------------------------------------------------
// 手势投影算法 — 选择旋转轴
// 将候选 3D 轴投影到 2D 屏幕空间, 计算拖拽方向
// 与各投影轴**垂直方向**的点积, 取最大者。
// 这确保无论视角如何, 拖拽方向总是匹配最自然
// 的旋转轴 (旋转在屏幕上表现为垂直于旋转轴的运动)。
// ----------------------------------------------------
const dragDir = delta.clone().normalize();
let bestAxis = null;
let bestDot = -Infinity;
for (const cand of candidateAxes) {
cand.screenDir = projectAxisToScreen(cand.worldAxis);
// 垂直于投影轴的方向 = 旋转在屏幕上的运动方向
cand.perpDir = new THREE.Vector2(-cand.screenDir.y, cand.screenDir.x);
const dot = Math.abs(dragDir.dot(cand.perpDir));
if (dot > bestDot) { bestDot = dot; bestAxis = cand; }
}
if (!bestAxis) return;
chosenAxis = bestAxis;
// 找层并挂载到临时轴心
const layerCubies = findLayerCubies(chosenAxis.name, chosenAxis.layer);
if (layerCubies.length === 0) { resetInteraction(); return; }
pivot = createPivot(layerCubies);
totalAngle = 0;
state = ROTATING;
}
if (state === ROTATING) {
// ----------------------------------------------------
// 1:1 跟手旋转
// - 将拖拽向量投影到 screenDir 的垂直方向
// - 摄像头位置修正符号, 保证各角度操作一致
// ----------------------------------------------------
const screenAxis = projectAxisToScreen(chosenAxis.worldAxis);
const perpDir = new THREE.Vector2(-screenAxis.y, screenAxis.x);
const dragTotal = m.clone().sub(dragStart);
const dragAmount = dragTotal.dot(perpDir); // 带符号的拖拽分量
// 符号修正: 摄像头位于旋转轴哪一侧决定旋转方向
const camDir = camera.position.clone().normalize();
const camSign = chosenAxis.worldAxis.dot(camDir) > 0 ? 1 : -1;
const angle = dragAmount * SENSITIVITY * camSign;
setPivotRotation(chosenAxis.worldAxis, angle);
totalAngle = angle;
}
}
function onPointerUp(_e) {
if (state === AXIS_PENDING) { resetInteraction(); return; }
if (state === ROTATING) {
// 磁吸到最近 90° 倍数
const snapTarget = Math.round(totalAngle / (Math.PI / 2)) * (Math.PI / 2);
if (Math.abs(snapTarget - totalAngle) < 0.0005) {
cleanupPivot();
resetInteraction();
return;
}
state = ANIMATING;
const axis = chosenAxis.worldAxis.clone();
const start = totalAngle;
const t = { angle: start };
animTween = new TWEEN.Tween(t)
.to({ angle: snapTarget }, SNAP_MS)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(({ angle }) => setPivotRotation(axis, angle))
.onComplete(() => {
cleanupPivot();
animTween = null;
resetInteraction();
})
.start();
}
}
// ============================================================
// 右键/中键视觉效果
// ============================================================
window.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('pointerdown', e => {
if (e.button === 2 || e.button === 1) container.classList.add('orbiting');
});
window.addEventListener('pointerup', () => container.classList.remove('orbiting'));
// ============================================================
// Scramble / Reset
// ============================================================
function scramble() {
if (state !== IDLE) return;
const moves = [];
let prevAxis = null, prevLayer = null;
for (let i = 0; i < SCRAMBLE_N; i++) {
let ax, la;
do {
ax = AXIS_NAMES[Math.floor(Math.random() * 3)];
la = [-1, 0, 1][Math.floor(Math.random() * 3)];
} while (ax === prevAxis && la === prevLayer);
prevAxis = ax; prevLayer = la;
moves.push({ axis: ax, layer: la, angle: (Math.random() < 0.5 ? 1 : -1) * Math.PI / 2 });
}
function run(idx) {
if (idx >= moves.length) { state = IDLE; return; }
const mv = moves[idx];
executeRotation(mv.axis, mv.layer, mv.angle, SCRAMBLE_MS, () => run(idx + 1));
}
state = ANIMATING;
run(0);
}
function resetCube() {
if (state === ANIMATING && animTween) { animTween.stop(); animTween = null; }
// 若有活跃 pivot, 先归还方块但不取整 (避免跳变)
if (pivot) {
const kids = [...pivot.children];
for (const k of kids) scene.attach(k);
scene.remove(pivot);
pivot = null;
}
resetInteraction();
state = ANIMATING;
let done = 0;
const N = cubies.length;
cubies.forEach(c => {
const o = c.userData.origin;
const sp = c.position.clone();
const sr = c.rotation.clone();
const tw = { px: sp.x, py: sp.y, pz: sp.z, rx: sr.x, ry: sr.y, rz: sr.z };
new TWEEN.Tween(tw)
.to({ px: o.x, py: o.y, pz: o.z, rx: 0, ry: 0, rz: 0 }, 520)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(v => { c.position.set(v.px, v.py, v.pz); c.rotation.set(v.rx, v.ry, v.rz); })
.onComplete(() => {
c.position.set(o.x, o.y, o.z);
c.rotation.set(0, 0, 0);
done++;
if (done >= N) state = IDLE;
})
.start();
});
}
btnScramble.addEventListener('click', scramble);
btnReset.addEventListener('click', resetCube);
// ============================================================
// 事件绑定
// ============================================================
renderer.domElement.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ============================================================
// 渲染循环
// ============================================================
function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
orbitControls.update();
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
</script>
</body>
</html>
网友解答:
--【壹】--:
效果还挺不错,不过应该是claude code的各种系统提示词起了作用吧,看到原帖的提示词了,感觉后续可以控制变量拿来测测别的模型
--【贰】--:
Max和High的实际体验差距极大,是可用和不可用的区别
从 Deepseek v4 pro 3d魔方简要测试 帖子继续
原本测试 只在cherry studio 里面使用auto模式测试
在cherry studio 不知道如何改用max思考模式
现在使用claude + max 思考等级测试
api 耗费 4.39元
思考加首次交付时间: 28m12s
image1195×818 223 KB
https://imgbed.snemc.cn/i/10a7ac09545f.gif(图片大于 4 MB)
测试结果:
非常丝滑
问题:
当视角转到背面的时候鼠标操作垂直方向拖动魔方,旋转角度是反的
在claude中看到deepseek完成一次写入后没有立马交付,而是又自己读了文件进行审阅, 又自己写了python脚本测试不知道怎样用python来测试html的
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3x3 魔方</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0f0f1a;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
user-select: none;
-webkit-user-select: none;
height: 100vh;
width: 100vw;
}
#container {
position: fixed;
inset: 0;
cursor: grab;
}
#container.grabbing { cursor: grabbing; }
#container.orbiting { cursor: move; }
#ui {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 14px;
z-index: 10;
}
#ui button {
padding: 12px 28px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 10px;
background: rgba(255,255,255,0.06);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
color: #ccc;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.03em;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, transform 0.15s;
}
#ui button:hover {
background: rgba(255,255,255,0.12);
border-color: rgba(255,255,255,0.28);
}
#ui button:active {
transform: scale(0.96);
}
#hint {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.38);
font-size: 12.5px;
letter-spacing: 0.04em;
pointer-events: none;
z-index: 10;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="hint">左键拖拽旋转层面 · 右键拖拽旋转视角 · 滚轮缩放</div>
<div id="ui">
<button id="scramble">Scramble</button>
<button id="reset">Reset</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"@tweenjs/tween.js": "https://unpkg.com/@tweenjs/tween.js@20.0.0/dist/tween.esm.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from '@tweenjs/tween.js';
// ============================================================
// 常量
// ============================================================
const CUBIE_SIZE = 0.85;
const EPSILON = 0.35;
const SENSITIVITY = 0.007;
const AXIS_THRESHOLD = 4;
const SNAP_MS = 180;
const SCRAMBLE_MS = 75;
const SCRAMBLE_N = 22;
// Rubik 标准配色 (白顶绿前)
const COLORS = {
right: '#B71234',
left: '#FF5800',
up: '#FFFFFF',
down: '#FFD500',
front: '#009B48',
back: '#0046AD',
};
const AXES = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1),
};
const AXIS_NAMES = ['x', 'y', 'z'];
// ============================================================
// DOM
// ============================================================
const container = document.getElementById('container');
const btnScramble = document.getElementById('scramble');
const btnReset = document.getElementById('reset');
// ============================================================
// Three.js 基础设施
// ============================================================
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0f0f1a');
scene.fog = new THREE.Fog('#0f0f1a', 9, 32);
const camera = new THREE.PerspectiveCamera(
40, window.innerWidth / window.innerHeight, 0.5, 40
);
camera.position.set(4.8, 3.0, 5.4);
camera.lookAt(0, 0, 0);
// ---- OrbitControls: 仅右键旋转视角, 滚轮缩放 ----
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.target.set(0, 0, 0);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.07;
orbitControls.minDistance = 3.5;
orbitControls.maxDistance = 12;
orbitControls.maxPolarAngle = Math.PI * 0.75;
orbitControls.mouseButtons = {
LEFT: null,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE,
};
orbitControls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN,
};
orbitControls.update();
// ============================================================
// 光照与阴影
// ============================================================
scene.add(new THREE.AmbientLight('#8899bb', 0.8));
scene.add(new THREE.HemisphereLight('#ffffff', '#334455', 0.45));
const dirLight = new THREE.DirectionalLight('#ffffff', 1.7);
dirLight.position.set(5, 12, 6);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 40;
dirLight.shadow.camera.left = -8;
dirLight.shadow.camera.right = 8;
dirLight.shadow.camera.top = 8;
dirLight.shadow.camera.bottom = -8;
dirLight.shadow.bias = -0.0002;
dirLight.shadow.normalBias = 0.015;
scene.add(dirLight);
// 阴影接收面
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(18, 18),
new THREE.ShadowMaterial({ opacity: 0.25 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -2.3;
ground.receiveShadow = true;
scene.add(ground);
// ============================================================
// Canvas 纹理 — 圆角贴纸 + 塑料黑边 + 高光
// ============================================================
function createStickerTexture(hexColor) {
const S = 256;
const cv = document.createElement('canvas');
cv.width = S;
cv.height = S;
const ctx = cv.getContext('2d');
// 塑料黑底
ctx.fillStyle = '#141414';
ctx.fillRect(0, 0, S, S);
// 圆角矩形
const m = 26;
const r = 15;
const x = m, y = m, w = S - 2 * m, h = S - 2 * m;
function roundRect(cx, cy, cw, ch, cr) {
ctx.beginPath();
ctx.moveTo(cx + cr, cy);
ctx.arcTo(cx + cw, cy, cx + cw, cy + cr, cr);
ctx.arcTo(cx + cw, cy + ch, cx + cw - cr, cy + ch, cr);
ctx.arcTo(cx, cy + ch, cx, cy + ch - cr, cr);
ctx.arcTo(cx, cy, cx + cr, cy, cr);
ctx.closePath();
}
roundRect(x, y, w, h, r);
ctx.fillStyle = hexColor;
ctx.fill();
// 对角线高光渐变 (模拟贴纸光泽)
const grad = ctx.createLinearGradient(x, y, x + w, y + h);
grad.addColorStop(0, 'rgba(255,255,255,0.24)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.05)');
grad.addColorStop(0.55, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.14)');
roundRect(x, y, w, h, r);
ctx.fillStyle = grad;
ctx.fill();
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = true;
return tex;
}
const stickerTextures = {};
for (const [key, color] of Object.entries(COLORS)) {
stickerTextures[key] = createStickerTexture(color);
}
// 贴纸材质
function stickerMat(tex) {
return new THREE.MeshStandardMaterial({ map: tex, roughness: 0.30, metalness: 0.02 });
}
const sMat = {
right: stickerMat(stickerTextures.right),
left: stickerMat(stickerTextures.left),
up: stickerMat(stickerTextures.up),
down: stickerMat(stickerTextures.down),
front: stickerMat(stickerTextures.front),
back: stickerMat(stickerTextures.back),
};
// 不可见面的黑色塑料
const blackMat = new THREE.MeshStandardMaterial({
color: '#181818', roughness: 0.55, metalness: 0.05,
});
// ============================================================
// 方块构建 — 3×3×3 = 27
// ============================================================
const cubies = [];
const geo = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE);
// 材质数组顺序: [+X, -X, +Y, -Y, +Z, -Z]
function buildMaterials(lx, ly, lz) {
return [
lx === 1 ? sMat.right : blackMat,
lx === -1 ? sMat.left : blackMat,
ly === 1 ? sMat.up : blackMat,
ly === -1 ? sMat.down : blackMat,
lz === 1 ? sMat.front : blackMat,
lz === -1 ? sMat.back : blackMat,
];
}
for (let lx = -1; lx <= 1; lx++) {
for (let ly = -1; ly <= 1; ly++) {
for (let lz = -1; lz <= 1; lz++) {
const mesh = new THREE.Mesh(geo, buildMaterials(lx, ly, lz));
mesh.position.set(lx, ly, lz);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.userData.origin = { x: lx, y: ly, z: lz };
scene.add(mesh);
cubies.push(mesh);
}
}
}
// ============================================================
// 交互状态机
// ============================================================
const IDLE = 'idle';
const AXIS_PENDING = 'axis_pending';
const ROTATING = 'rotating';
const ANIMATING = 'animating';
let state = IDLE;
let clickedCubie = null;
let clickedNormal = null; // 世界空间面法线
let candidateAxes = []; // [{ name, worldAxis, layer, screenDir, perpDir }]
let dragStart = new THREE.Vector2();
let chosenAxis = null; // 确定后的旋转轴信息 (含 perpDir)
let pivot = null;
let totalAngle = 0;
let animTween = null;
const raycaster = new THREE.Raycaster();
raycaster.far = 20;
// ============================================================
// 工具
// ============================================================
/** 3D 世界轴 → 2D 屏幕单位方向 */
function projectAxisToScreen(axis3D) {
const o = new THREE.Vector3(0, 0, 0);
const t = axis3D.clone();
o.project(camera);
t.project(camera);
const W = renderer.domElement.clientWidth;
const H = renderer.domElement.clientHeight;
const sO = new THREE.Vector2((o.x + 1) / 2 * W, (1 - o.y) / 2 * H);
const sT = new THREE.Vector2((t.x + 1) / 2 * W, (1 - t.y) / 2 * H);
const d = sT.clone().sub(sO);
return d.length() < 1e-8 ? d : d.normalize();
}
function getMouse(e) {
const r = renderer.domElement.getBoundingClientRect();
return new THREE.Vector2(e.clientX - r.left, e.clientY - r.top);
}
/** 射线检测 — 返回命中的方块及其世界空间面法线 */
function raycastCubie(mouse) {
const ndc = new THREE.Vector2(
(mouse.x / renderer.domElement.clientWidth) * 2 - 1,
-(mouse.y / renderer.domElement.clientHeight) * 2 + 1,
);
raycaster.setFromCamera(ndc, camera);
const hits = raycaster.intersectObjects(cubies, false);
if (hits.length === 0) return null;
const hit = hits[0];
const n = hit.face.normal.clone();
n.transformDirection(hit.object.matrixWorld);
return { cubie: hit.object, normal: n };
}
/**
* 根据面法线确定 2 个候选旋转轴。
* 逻辑: 法线沿某主轴的 → 排除该轴 → 候选为其余两轴。
*/
function getCandidateAxes(faceNormal) {
const a = [Math.abs(faceNormal.x), Math.abs(faceNormal.y), Math.abs(faceNormal.z)];
let dom = 'z';
if (a[0] >= a[1] && a[0] >= a[2]) dom = 'x';
else if (a[1] >= a[0] && a[1] >= a[2]) dom = 'y';
const others = AXIS_NAMES.filter(n => n !== dom);
return others.map(name => {
const comp = { x: 0, y: 1, z: 2 }[name];
const wp = new THREE.Vector3();
clickedCubie.getWorldPosition(wp);
return {
name,
worldAxis: AXES[name].clone(),
layer: Math.round(wp.getComponent(comp)),
screenDir: null,
perpDir: null,
};
});
}
/** 返回属于指定层的所有方块 (基于世界坐标) */
function findLayerCubies(axisName, layerValue) {
const ci = { x: 0, y: 1, z: 2 }[axisName];
return cubies.filter(c => {
const wp = new THREE.Vector3();
c.getWorldPosition(wp);
return Math.abs(wp.getComponent(ci) - layerValue) < EPSILON;
});
}
/** 创建临时轴心并将选中方块挂载上去 */
function createPivot(layerCubies) {
const p = new THREE.Group();
scene.add(p);
for (const c of layerCubies) p.attach(c);
return p;
}
/** 归还方块到场景, 消除浮点累积误差, 移除轴心 */
function cleanupPivot() {
if (!pivot) return;
const children = [...pivot.children];
for (const c of children) scene.attach(c);
for (const c of children) {
c.position.x = Math.round(c.position.x);
c.position.y = Math.round(c.position.y);
c.position.z = Math.round(c.position.z);
c.rotation.x = Math.round(c.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
c.rotation.y = Math.round(c.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
c.rotation.z = Math.round(c.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
}
scene.remove(pivot);
pivot = null;
}
/** 绝对设置轴心旋转 (非增量, 避免误差累积) */
function setPivotRotation(axis, rad) {
pivot.rotation.set(0, 0, 0);
pivot.rotateOnWorldAxis(axis, rad);
}
/** 重置交互状态 */
function resetInteraction() {
state = IDLE;
clickedCubie = null;
clickedNormal = null;
candidateAxes = [];
chosenAxis = null;
totalAngle = 0;
container.classList.remove('grabbing');
}
// ============================================================
// 程序化旋转 (供 Scramble 使用)
// ============================================================
function executeRotation(axisName, layerValue, targetAngle, duration, cb) {
if (state !== IDLE && state !== ANIMATING) { if (cb) cb(); return; }
state = ANIMATING;
const axis = AXES[axisName].clone();
const layerCubies = findLayerCubies(axisName, layerValue);
if (layerCubies.length === 0) { state = IDLE; if (cb) cb(); return; }
const p = createPivot(layerCubies);
const t = { angle: 0 };
animTween = new TWEEN.Tween(t)
.to({ angle: targetAngle }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ angle }) => {
p.rotation.set(0, 0, 0);
p.rotateOnWorldAxis(axis, angle);
})
.onComplete(() => {
const kids = [...p.children];
for (const k of kids) scene.attach(k);
for (const k of kids) {
k.position.x = Math.round(k.position.x);
k.position.y = Math.round(k.position.y);
k.position.z = Math.round(k.position.z);
k.rotation.x = Math.round(k.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
k.rotation.y = Math.round(k.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
k.rotation.z = Math.round(k.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
}
scene.remove(p);
animTween = null;
state = IDLE;
if (cb) cb();
})
.start();
}
// ============================================================
// 指针事件 — 手势交互核心
// ============================================================
function onPointerDown(e) {
if (e.button !== 0) return;
if (state !== IDLE) return;
if (animTween) { animTween.stop(); animTween = null; }
const m = getMouse(e);
const hit = raycastCubie(m);
if (!hit) return;
clickedCubie = hit.cubie;
clickedNormal = hit.normal.clone();
candidateAxes = getCandidateAxes(clickedNormal);
dragStart.copy(m);
state = AXIS_PENDING;
container.classList.add('grabbing');
}
function onPointerMove(e) {
const m = getMouse(e);
if (state === AXIS_PENDING) {
const delta = m.clone().sub(dragStart);
if (delta.length() < AXIS_THRESHOLD) return;
// ----------------------------------------------------
// 手势投影算法 — 选择旋转轴
// 将候选 3D 轴投影到 2D 屏幕空间, 计算拖拽方向
// 与各投影轴**垂直方向**的点积, 取最大者。
// 这确保无论视角如何, 拖拽方向总是匹配最自然
// 的旋转轴 (旋转在屏幕上表现为垂直于旋转轴的运动)。
// ----------------------------------------------------
const dragDir = delta.clone().normalize();
let bestAxis = null;
let bestDot = -Infinity;
for (const cand of candidateAxes) {
cand.screenDir = projectAxisToScreen(cand.worldAxis);
// 垂直于投影轴的方向 = 旋转在屏幕上的运动方向
cand.perpDir = new THREE.Vector2(-cand.screenDir.y, cand.screenDir.x);
const dot = Math.abs(dragDir.dot(cand.perpDir));
if (dot > bestDot) { bestDot = dot; bestAxis = cand; }
}
if (!bestAxis) return;
chosenAxis = bestAxis;
// 找层并挂载到临时轴心
const layerCubies = findLayerCubies(chosenAxis.name, chosenAxis.layer);
if (layerCubies.length === 0) { resetInteraction(); return; }
pivot = createPivot(layerCubies);
totalAngle = 0;
state = ROTATING;
}
if (state === ROTATING) {
// ----------------------------------------------------
// 1:1 跟手旋转
// - 将拖拽向量投影到 screenDir 的垂直方向
// - 摄像头位置修正符号, 保证各角度操作一致
// ----------------------------------------------------
const screenAxis = projectAxisToScreen(chosenAxis.worldAxis);
const perpDir = new THREE.Vector2(-screenAxis.y, screenAxis.x);
const dragTotal = m.clone().sub(dragStart);
const dragAmount = dragTotal.dot(perpDir); // 带符号的拖拽分量
// 符号修正: 摄像头位于旋转轴哪一侧决定旋转方向
const camDir = camera.position.clone().normalize();
const camSign = chosenAxis.worldAxis.dot(camDir) > 0 ? 1 : -1;
const angle = dragAmount * SENSITIVITY * camSign;
setPivotRotation(chosenAxis.worldAxis, angle);
totalAngle = angle;
}
}
function onPointerUp(_e) {
if (state === AXIS_PENDING) { resetInteraction(); return; }
if (state === ROTATING) {
// 磁吸到最近 90° 倍数
const snapTarget = Math.round(totalAngle / (Math.PI / 2)) * (Math.PI / 2);
if (Math.abs(snapTarget - totalAngle) < 0.0005) {
cleanupPivot();
resetInteraction();
return;
}
state = ANIMATING;
const axis = chosenAxis.worldAxis.clone();
const start = totalAngle;
const t = { angle: start };
animTween = new TWEEN.Tween(t)
.to({ angle: snapTarget }, SNAP_MS)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(({ angle }) => setPivotRotation(axis, angle))
.onComplete(() => {
cleanupPivot();
animTween = null;
resetInteraction();
})
.start();
}
}
// ============================================================
// 右键/中键视觉效果
// ============================================================
window.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('pointerdown', e => {
if (e.button === 2 || e.button === 1) container.classList.add('orbiting');
});
window.addEventListener('pointerup', () => container.classList.remove('orbiting'));
// ============================================================
// Scramble / Reset
// ============================================================
function scramble() {
if (state !== IDLE) return;
const moves = [];
let prevAxis = null, prevLayer = null;
for (let i = 0; i < SCRAMBLE_N; i++) {
let ax, la;
do {
ax = AXIS_NAMES[Math.floor(Math.random() * 3)];
la = [-1, 0, 1][Math.floor(Math.random() * 3)];
} while (ax === prevAxis && la === prevLayer);
prevAxis = ax; prevLayer = la;
moves.push({ axis: ax, layer: la, angle: (Math.random() < 0.5 ? 1 : -1) * Math.PI / 2 });
}
function run(idx) {
if (idx >= moves.length) { state = IDLE; return; }
const mv = moves[idx];
executeRotation(mv.axis, mv.layer, mv.angle, SCRAMBLE_MS, () => run(idx + 1));
}
state = ANIMATING;
run(0);
}
function resetCube() {
if (state === ANIMATING && animTween) { animTween.stop(); animTween = null; }
// 若有活跃 pivot, 先归还方块但不取整 (避免跳变)
if (pivot) {
const kids = [...pivot.children];
for (const k of kids) scene.attach(k);
scene.remove(pivot);
pivot = null;
}
resetInteraction();
state = ANIMATING;
let done = 0;
const N = cubies.length;
cubies.forEach(c => {
const o = c.userData.origin;
const sp = c.position.clone();
const sr = c.rotation.clone();
const tw = { px: sp.x, py: sp.y, pz: sp.z, rx: sr.x, ry: sr.y, rz: sr.z };
new TWEEN.Tween(tw)
.to({ px: o.x, py: o.y, pz: o.z, rx: 0, ry: 0, rz: 0 }, 520)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(v => { c.position.set(v.px, v.py, v.pz); c.rotation.set(v.rx, v.ry, v.rz); })
.onComplete(() => {
c.position.set(o.x, o.y, o.z);
c.rotation.set(0, 0, 0);
done++;
if (done >= N) state = IDLE;
})
.start();
});
}
btnScramble.addEventListener('click', scramble);
btnReset.addEventListener('click', resetCube);
// ============================================================
// 事件绑定
// ============================================================
renderer.domElement.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ============================================================
// 渲染循环
// ============================================================
function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
orbitControls.update();
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
</script>
</body>
</html>
网友解答:
--【壹】--:
效果还挺不错,不过应该是claude code的各种系统提示词起了作用吧,看到原帖的提示词了,感觉后续可以控制变量拿来测测别的模型
--【贰】--:
Max和High的实际体验差距极大,是可用和不可用的区别

