glm-5.1 3d 魔方简要测试

从上面两篇帖子继续 Deepseek v4 pro 3d魔方简要测试 国产替代 使用 cherry studio 客户端, 未设置系统提示词, 思考等级:auto 提示词: 你是一名精通图形学算法与 Web 交互的前端专家。请仅用一个 HTML 文件,基于 Three.js (ES Modules)...
glm-5.1 3d 魔方简要测试
glm-5.1 3d 魔方简要测试

从上面两篇帖子继续

Deepseek v4 pro 3d魔方简要测试 国产替代
使用 cherry studio 客户端, 未设置系统提示词, 思考等级:auto 提示词: 你是一名精通图形学算法与 Web 交互的前端专家。请仅用一个 HTML 文件,基于 Three.js (ES Modules) 实现一个物理级高保真、支持自然手势的 3x3 魔方。 一、 交付规范 单文件架构:HTML/CSS/JS 必须合并在一个文件中。 依赖管理:必须通过 importmap 从 …
小米mimo-v2.5-pro 3d 魔方简要测试 开发调优
从上篇帖子继续 Deepseek v4 pro 3d魔方简要测试 测试环境 cherry studio, 上游是opencode go, 思考模式默认 思考时间 937.6s 测试结果 https://imgbed.snemc.cn/i/f5400f9b7e02.gif(图片大于 4 MB) 概要: 相当完美,鼠标操作底部那一排似乎有些转不…

测试环境 cherry studio 思考等级 : auto 上游 opencode-go
思考时间149.7s

提示词

你是一名精通图形学算法与 Web 交互的前端专家。请仅用一个 HTML 文件,基于 Three.js (ES Modules) 实现一个物理级高保真、支持自然手势的 3x3 魔方。
一、 交付规范
单文件架构:HTML/CSS/JS 必须合并在一个文件中。
依赖管理:必须通过 importmap 从 unpkg 或 cdn.skypack 引入 Three.js 及其 OrbitControls、Tween.js。
零素材依赖:禁止加载任何外部图片/贴图,所有材质纹理必须使用 HTML5 Canvas API 程序化动态生成。
二、 视觉与物理标准
模型构建:
场景需包含 27 个独立的小方块(Cubies)。
物理间隙:小方块之间必须保留微小的物理间距(Spacing),不可紧贴。
倒角质感:通过 Canvas 绘制带有圆角矩形(Rounded Rect)的贴纸纹理,模拟真实魔方的塑料黑边与贴纸高光效果。
光影环境:
必须开启 ShadowMap。
配置环境光(Ambient)与平行光(Directional),确保魔方有清晰的立体感和阴影投射。
三、 核心逻辑考点(数据结构与变换)
禁止维护复杂的 3D 状态数组,请使用基于“空间位置”的动态计算方案:
动态层级筛选:
不要写死索引。当需要旋转某一层时,遍历所有方块,根据其在世界坐标系(World Position)下的 x, y, z 值与阈值(Epsilon)来判断它是否属于当前旋转层。
Pivot 变换机制(关键考点):
实现旋转时,必须创建一个临时的 Pivot(轴心对象)。
核心API:使用 pivot.attach(object) 将选中的方块挂载到轴心,旋转轴心,动画结束后使用 scene.attach(object) 将方块放回场景。
作用:利用 attach 自动计算世界矩阵变换,避免手动处理复杂的四元数乘法。
坐标清洗:
每次旋转结束后,必须对所有方块的位置(Position)和旋转(Rotation)进行 Math.round() 取整处理,消除浮点数累积误差,防止魔方“散架”。
四、 交互系统考点(算法重难点)
这是区分初级与高级开发者的核心点,请实现类似原生 App 的自然手势体验:
操作分离:
左键拖拽:旋转魔方的某一层。
右键拖拽:旋转视角(OrbitControls)。
基于投影向量的手势识别算法:
射线检测:点击时获取被点击方块的“表面法线(Face Normal)”。
意图判断:
根据法线,锁定潜在的两个旋转轴(例如点击前面,潜在轴为 X 或 Y)。
将这两个 3D 轴的向量**投影(Project)**到 2D 屏幕空间。
计算用户鼠标滑动的 2D 向量与这两个投影向量的点积(Dot Product),选择匹配度最高的轴作为旋转轴。
方向修正与实时跟随:
实现1:1 实时跟手:鼠标移动多少像素,魔方层转动对应角度。
符号修正:通过叉乘(Cross Product)或投影符号判断,确保无论从魔方正面、背面还是顶面操作,鼠标向右划动始终对应“向右转”的视觉逻辑(解决方向反转 bug)。
磁吸效果:
松开鼠标后,自动计算最近的 90 度倍数,使用 Tween.js 播放回弹动画并对齐网格。
五、 代码质量
代码需包含清晰的注释,解释“手势投影算法”和“Pivot 挂载逻辑”。
具备 Scramble(打乱)和 Reset(重置)功能按钮。

https://imgbed.snemc.cn/i/76632ecccb36.gif(图片大于 4 MB)

概述:
没有任何问题,相当丝滑

<!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>
        :root {
            --bg: #1a1a1a;
            --panel-bg: rgba(30, 30, 30, 0.85);
            --accent: #4fd1c5;
            --text: #e2e8f0;
            --btn-hover: #38b2ac;
        }
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-color: var(--bg);
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: var(--text);
        }
        #canvas-container {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }
        #ui-panel {
            position: absolute;
            top: 20px;
            left: 20px;
            z-index: 10;
            background: var(--panel-bg);
            padding: 15px 25px;
            border-radius: 12px;
            backdrop-filter: blur(8px);
            border: 1px solid rgba(255,255,255,0.1);
            box-shadow: 0 4px 20px rgba(0,0,0,0.4);
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        h1 {
            margin: 0 0 5px 0;
            font-size: 1.2rem;
            color: var(--accent);
            letter-spacing: 1px;
        }
        p {
            margin: 0;
            font-size: 0.85rem;
            color: #a0aec0;
        }
        .btn-group {
            display: flex;
            gap: 10px;
            margin-top: 5px;
        }
        button {
            background-color: var(--accent);
            color: #1a202c;
            border: none;
            padding: 8px 16px;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            font-size: 0.9rem;
        }
        button:hover {
            background-color: var(--btn-hover);
            transform: translateY(-1px);
            box-shadow: 0 2px 10px rgba(79, 209, 197, 0.4);
        }
        button:active {
            transform: translateY(0);
        }
        #toast {
            position: absolute;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%) translateY(100px);
            background: rgba(79, 209, 197, 0.9);
            color: #1a202c;
            padding: 10px 25px;
            border-radius: 20px;
            font-weight: 600;
            opacity: 0;
            transition: all 0.3s ease;
            z-index: 20;
        }
        #toast.show {
            opacity: 1;
            transform: translateX(-50%) translateY(0);
        }
    </style>
    <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@23.1.3/dist/tween.esm.js"
        }
    }
    </script>
</head>
<body>

<div id="ui-panel">
    <h1>3x3 魔方模拟器</h1>
    <p>左键拖拽旋转层 | 右键拖拽旋转视角</p>
    <div class="btn-group">
        <button id="btn-scramble">Scramble</button>
        <button id="btn-reset">Reset</button>
    </div>
</div>

<div id="toast"></div>
<div id="canvas-container"></div>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from '@tweenjs/tween.js';

// ==========================================
// 全局状态与配置
// ==========================================
let cubies = []; // 存储所有 27 个小方块
let isAnimating = false; // 是否正在播放动画(打乱或磁吸回弹)
let isDragging = false; // 是否正在拖拽旋转层
let hasChosenAxis = false; // 是否已经通过算法确定了旋转轴
let startMouse = new THREE.Vector2(); // 鼠标按下时的 NDC 坐标
let currentMouse = new THREE.Vector2(); // 当前鼠标的 NDC 坐标

let selectedCubie = null; // 射线检测到的初始方块
let faceNormal = new THREE.Vector3(); // 初始点击面的世界法线
let intersectPoint = new THREE.Vector3(); // 初始点击的交点

let currentAxis = ''; // 当前旋转的轴 'x', 'y', 'z'
let targetLayerIndex = 0; // 当前旋转的层级 -1, 0, 1
let pivot; // 临时轴心对象
let projDelta = new THREE.Vector2(); // 投影算法计算出的 2D 方向单位向量
let startRotation = 0; // 拖拽开始时 pivot 的旋转角度

// 魔方尺寸配置
const SPACING = 1.02; // 物理间隙
const CUBIE_SIZE = 0.95; // 小方块尺寸

// ==========================================
// 场景初始化
// ==========================================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);

const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(4, 4, 6);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('canvas-container').appendChild(renderer.domElement);

// 右键拖拽旋转视角配置
const controls = new OrbitControls(camera, renderer.domElement);
controls.mouseButtons = {
    LEFT: null, // 左键由我们自己的手势算法接管
    MIDDLE: THREE.MOUSE.DOLLY,
    RIGHT: THREE.MOUSE.ROTATE
};
controls.enablePan = false;
controls.minDistance = 5;
controls.maxDistance = 15;

// ==========================================
// 光影环境
// ==========================================
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -5;
dirLight.shadow.camera.right = 5;
dirLight.shadow.camera.top = 5;
dirLight.shadow.camera.bottom = -5;
dirLight.shadow.bias = -0.001;
scene.add(dirLight);

// ==========================================
// Canvas API 程序化生成贴图
// ==========================================
const FACE_COLORS = {
    right: '#B71234', // 红
    left: '#FF5800',  // 橙
    up: '#FFFFFF',    // 白
    down: '#FFD500',  // 黄
    front: '#0046AD', // 蓝
    back: '#009B48',  // 绿
    inside: '#111111' // 内部黑
};

function createFaceTexture(color) {
    const size = 256;
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext('2d');

    // 绘制黑色塑料底色(带微小圆角模拟边缘)
    ctx.fillStyle = '#1a1a1a';
    ctx.fillRect(0, 0, size, size);

    // 如果不是内部面,绘制带圆角的贴纸和高光
    if (color !== FACE_COLORS.inside) {
        const margin = 16;
        const radius = 30;
        const sx = margin, sy = margin, sw = size - margin * 2, sh = size - margin * 2;

        // 绘制圆角矩形贴纸路径
        ctx.beginPath();
        ctx.moveTo(sx + radius, sy);
        ctx.lineTo(sx + sw - radius, sy);
        ctx.quadraticCurveTo(sx + sw, sy, sx + sw, sy + radius);
        ctx.lineTo(sx + sw, sy + sh - radius);
        ctx.quadraticCurveTo(sx + sw, sy + sh, sx + sw - radius, sy + sh);
        ctx.lineTo(sx + radius, sy + sh);
        ctx.quadraticCurveTo(sx, sy + sh, sx, sy + sh - radius);
        ctx.lineTo(sx, sy + radius);
        ctx.quadraticCurveTo(sx, sy, sx + radius, sy);
        ctx.closePath();

        // 填充贴纸底色
        ctx.fillStyle = color;
        ctx.fill();

        // 添加从左上到右下的高光渐变,模拟塑料质感
        const gradient = ctx.createLinearGradient(sx, sy, sx + sw, sy + sh);
        gradient.addColorStop(0, 'rgba(255, 255, 255, 0.25)');
        gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.05)');
        gradient.addColorStop(1, 'rgba(0, 0, 0, 0.15)');
        ctx.fillStyle = gradient;
        ctx.fill();
    }

    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    return texture;
}

// 预生成所有纹理
const textures = {
    right: createFaceTexture(FACE_COLORS.right),
    left: createFaceTexture(FACE_COLORS.left),
    up: createFaceTexture(FACE_COLORS.up),
    down: createFaceTexture(FACE_COLORS.down),
    front: createFaceTexture(FACE_COLORS.front),
    back: createFaceTexture(FACE_COLORS.back),
    inside: createFaceTexture(FACE_COLORS.inside)
};

// ==========================================
// 模型构建 & 动态层级
// ==========================================
function buildCube() {
    // 清空旧方块
    cubies.forEach(c => scene.remove(c));
    cubies = [];

    const geometry = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE);

    for (let x = -1; x <= 1; x++) {
        for (let y = -1; y <= 1; y++) {
            for (let z = -1; z <= 1; z++) {
                // 根据方块所在空间位置决定6个面的贴纸
                const materials = [
                    new THREE.MeshStandardMaterial({ map: x === 1 ? textures.right : textures.inside, roughness: 0.4, metalness: 0.1 }),
                    new THREE.MeshStandardMaterial({ map: x === -1 ? textures.left : textures.inside, roughness: 0.4, metalness: 0.1 }),
                    new THREE.MeshStandardMaterial({ map: y === 1 ? textures.up : textures.inside, roughness: 0.4, metalness: 0.1 }),
                    new THREE.MeshStandardMaterial({ map: y === -1 ? textures.down : textures.inside, roughness: 0.4, metalness: 0.1 }),
                    new THREE.MeshStandardMaterial({ map: z === 1 ? textures.front : textures.inside, roughness: 0.4, metalness: 0.1 }),
                    new THREE.MeshStandardMaterial({ map: z === -1 ? textures.back : textures.inside, roughness: 0.4, metalness: 0.1 }),
                ];

                const mesh = new THREE.Mesh(geometry, materials);
                mesh.position.set(x * SPACING, y * SPACING, z * SPACING);
                mesh.castShadow = true;
                mesh.receiveShadow = true;

                scene.add(mesh);
                cubies.push(mesh);
            }
        }
    }
}
buildCube();

// ==========================================
// 核心交互算法:手势投影与旋转轴确定
// ==========================================
const raycaster = new THREE.Raycaster();

// 获取某3D轴投影到2D屏幕后的向量,以及对应的物理旋转方向
function getProjectionData(axisStr) {
    const axisVec = new THREE.Vector3();
    axisVec[axisStr] = 1; // 构造 3D 轴单位向量 (1,0,0) 等
    
    // 算法核心:3D旋转映射到2D屏幕
    // 假设绕 axisStr 轴发生正向微小旋转,被点击面的法线方向将发生偏移。
    // 偏移向量 deltaPos3D = cross(axisVec, faceNormal)
    const deltaPos3D = new THREE.Vector3().crossVectors(axisVec, faceNormal);

    // 将交点和偏移后的交点投影到屏幕空间,求差得到 2D 投影方向向量
    const p0 = intersectPoint.clone().project(camera);
    const p1 = intersectPoint.clone().add(deltaPos3D).project(camera);
    
    // 返回 2D 屏幕空间中的方向向量 (NDC坐标系)
    return new THREE.Vector2(p1.x - p0.x, p1.y - p0.y);
}

function onPointerDown(event) {
    if (event.button !== 0 || isAnimating) return; // 仅处理左键,且不在动画中

    startMouse.set(
        (event.clientX / window.innerWidth) * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1
    );

    raycaster.setFromCamera(startMouse, camera);
    const intersects = raycaster.intersectObjects(cubies);

    if (intersects.length > 0) {
        isDragging = true;
        controls.enabled = false; // 禁用视角控制,防止冲突

        selectedCubie = intersects[0].object;
        intersectPoint = intersects[0].point.clone();
        
        // 计算世界法线:将局部法线转换到世界空间,并四舍五入消除浮点误差
        faceNormal = intersects[0].face.normal.clone()
            .transformDirection(selectedCubie.matrixWorld)
            .round();

        hasChosenAxis = false;
        currentAxis = '';
        startRotation = 0;
    }
}

function onPointerMove(event) {
    if (!isDragging || isAnimating) return;

    currentMouse.set(
        (event.clientX / window.innerWidth) * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1
    );

    const NDC_delta2D = currentMouse.clone().sub(startMouse);

    // 若还未确定旋转轴(手势刚开始),需要达到一定阈值才锁定
    if (!hasChosenAxis) {
        if (NDC_delta2D.length() < 0.01) return; // 死区防抖
        
        let axis1, axis2;
        // 排除法线方向的轴,锁定两个潜在旋转轴
        if (Math.abs(faceNormal.x) > 0.5) { axis1 = 'y'; axis2 = 'z'; }
        else if (Math.abs(faceNormal.y) > 0.5) { axis1 = 'x'; axis2 = 'z'; }
        else { axis1 = 'x'; axis2 = 'y'; }

        // 基于投影向量的手势识别算法核心:
        // 将3D潜在轴的旋转效果投影到2D,与用户的2D滑动向量计算点积
        const proj1 = getProjectionData(axis1);
        const proj2 = getProjectionData(axis2);
        const dot1 = NDC_delta2D.dot(proj1);
        const dot2 = NDC_delta2D.dot(proj2);

        // 选择匹配度最高的轴
        if (Math.abs(dot1) > Math.abs(dot2)) {
            currentAxis = axis1;
            projDelta = proj1;
        } else {
            currentAxis = axis2;
            projDelta = proj2;
        }

        // 动态层级筛选:根据点击方块在当前轴的世界坐标确定层级
        targetLayerIndex = Math.round(selectedCubie.position[currentAxis] / SPACING);

        // 创建临时 Pivot 并挂载相应层级的方块
        pivot = new THREE.Object3D();
        pivot.position[currentAxis] = targetLayerIndex * SPACING;
        scene.add(pivot);

        // Pivot 变换机制核心:使用 attach 保持世界矩阵
        cubies.forEach(cubie => {
            if (Math.round(cubie.position[currentAxis] / SPACING) === targetLayerIndex) {
                pivot.attach(cubie);
            }
        });

        hasChosenAxis = true;
    }

    // 实时跟手:1:1 计算旋转角度
    // 公式:deltaAngle = dot(2D滑动距离, 2D投影方向) / dot(投影方向, 投影方向)
    // 这完美解决了方向反转的Bug,且从任何视角操作都直观!
    const deltaAngle = NDC_delta2D.dot(projDelta) / projDelta.dot(projDelta);
    pivot.rotation[currentAxis] = deltaAngle;
}

function onPointerUp(event) {
    if (!isDragging) return;
    isDragging = false;

    if (!hasChosenAxis) {
        controls.enabled = true;
        return;
    }

    // 磁吸效果:松开鼠标后,自动对齐到最近的 90 度倍数
    const currentAngle = pivot.rotation[currentAxis];
    const targetAngle = Math.round(currentAngle / (Math.PI / 2)) * (Math.PI / 2);

    isAnimating = true;
    
    // 使用 Tween.js 播放回弹动画
    const tweenObj = { r: currentAngle };
    new TWEEN.Tween(tweenObj)
        .to({ r: targetAngle }, 250)
        .easing(TWEEN.Easing.Back.Out)
        .onUpdate(() => {
            pivot.rotation[currentAxis] = tweenObj.r;
        })
        .onComplete(() => {
            finishRotation(targetAngle);
        })
        .start();
}

function finishRotation(finalAngle) {
    pivot.rotation[currentAxis] = finalAngle;
    
    // 坐标清洗:消除浮点累积误差,防止魔方“散架”
    // 将方块从 pivot 放回 scene,利用 scene.attach 自动处理矩阵
    const children = [...pivot.children];
    children.forEach(child => {
        scene.attach(child); 
            
        // 位置清洗:四舍五入到 Spacing 的整数倍
        child.position.x = Math.round(child.position.x / SPACING) * SPACING;
        child.position.y = Math.round(child.position.y / SPACING) * SPACING;
        child.position.z = Math.round(child.position.z / SPACING) * SPACING;

        // 旋转清洗:四舍五入到 90 度倍数
        child.rotation.x = Math.round(child.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
        child.rotation.y = Math.round(child.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
        child.rotation.z = Math.round(child.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
    });

    // 清理临时轴心对象
    scene.remove(pivot);
    
    isAnimating = false;
    controls.enabled = true;
}

// ==========================================
// 打乱与重置功能
// ==========================================
function scrambleCube() {
    if (isAnimating || isDragging) return;
    isAnimating = true;
    showToast("Scrambling...");
    
    const axes = ['x', 'y', 'z'];
    const layers = [-1, 0, 1];
    const moves = 20;
    let moveList = [];
    
    for (let i = 0; i < moves; i++) {
        moveList.push({
            axis: axes[Math.floor(Math.random() * 3)],
            layer: layers[Math.floor(Math.random() * 3)],
            dir: Math.random() > 0.5 ? 1 : -1
        });
    }

    let index = 0;
    function doMove() {
        if (index >= moveList.length) {
            isAnimating = false;
            showToast("Ready!");
            return;
        }

        const move = moveList[index];
        const tempPivot = new THREE.Object3D();
        tempPivot.position[move.axis] = move.layer * SPACING;
        scene.add(tempPivot);

        cubies.forEach(cubie => {
            if (Math.round(cubie.position[move.axis] / SPACING) === move.layer) {
                tempPivot.attach(cubie);
            }
        });

        const targetAngle = move.dir * (Math.PI / 2);
        const tempTweenObj = { r: 0 };
        new TWEEN.Tween(tempTweenObj)
            .to({ r: targetAngle }, 100)
            .easing(TWEEN.Easing.Quadratic.InOut)
            .onUpdate(() => {
                tempPivot.rotation[move.axis] = tempTweenObj.r;
            })
            .onComplete(() => {
                finishRotationForScramble(tempPivot, targetAngle);
                index++;
                doMove();
            })
            .start();
    }
    doMove();
}

function finishRotationForScramble(pivotObj) {
    const children = [...pivotObj.children];
    children.forEach(child => {
        scene.attach(child);
        child.position.x = Math.round(child.position.x / SPACING) * SPACING;
        child.position.y = Math.round(child.position.y / SPACING) * SPACING;
        child.position.z = Math.round(child.position.z / SPACING) * SPACING;
        child.rotation.x = Math.round(child.rotation.x / (Math.PI / 2)) * (Math.PI / 2);
        child.rotation.y = Math.round(child.rotation.y / (Math.PI / 2)) * (Math.PI / 2);
        child.rotation.z = Math.round(child.rotation.z / (Math.PI / 2)) * (Math.PI / 2);
    });
    scene.remove(pivotObj);
}

function resetCube() {
    if (isAnimating || isDragging) return;
    buildCube();
    showToast("Reset Done!");
}

// 提示弹窗
function showToast(msg) {
    const toast = document.getElementById('toast');
    toast.innerText = msg;
    toast.classList.add('show');
    setTimeout(() => toast.classList.remove('show'), 1500);
}

// ==========================================
// 事件绑定与渲染循环
// ==========================================
window.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);

document.getElementById('btn-scramble').addEventListener('click', scrambleCube);
document.getElementById('btn-reset').addEventListener('click', resetCube);

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

function animate() {
    requestAnimationFrame(animate);
    TWEEN.update();
    controls.update();
    renderer.render(scene, camera);
}

animate();
</script>
</body>
</html>

1 个帖子 - 1 位参与者

阅读完整话题

来源: linux.do查看原文