H5 three.js 实现六年级观察物体

o( ̄▽ ̄)ブ 我又带着新的demo来啦~

预览

功能点

  • 立方体的阴影
  • 立方体的添加
  • 位置记录
  • 最大限制
  • 三视图展示
  • 立方体的移除
  • 答题模式
  • 随机出题
  • 题库出题

源码

注释算是比较全了,可能部分会有点绕,还能够再优化一下~

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/global.css">
    <style>
        body {
            user-select: none;
        }

        .panel-container {
            position: absolute;
            top: 0;
            left: 0;
            /* height: 200px; */
            width: 100%;
            background-color: rgba(255, 255, 255, .8);
            padding: 20px;
            box-sizing: border-box;
            display: flex;
            justify-content: space-between;
        }

        .mode-switch {
            display: inline-flex;
            overflow: hidden;
            height: 32px;
            border-radius: 6px;
            background-color: #ccc;
        }

        .mode-switch .mode-switch-item {
            display: inline-block;
            padding: 0 8px;
            line-height: 32px;
            font-size: 12px;
            background-color: transparent;
            cursor: pointer;
        }

        .mode-switch .mode-switch-item.active {
            background-color: #88ccee;
        }

        .mode-switch .mode-switch-item.del.active {
            background-color: #cc3333;
        }

        .mode-switch-item.clear.active,
        .mode-switch-item.again.active {
            display: inline-block;
        }

        .mode-switch-item.clear,
        .mode-switch-item.again {
            display: none;
        }

        .mode-switch-item.clear.active,
        .mode-switch-item.del.active {
            background-color: #cc3333;
            color: #fff;
        }



        .panel-container.bottom {
            top: auto;
            bottom: 0;
            padding-top: 32px;
        }

        .front-view,
        .top-view,
        .left-view {
            width: 25vmin;
            height: 25vmin;
            position: relative;
        }

        .front-view::before,
        .top-view::before,
        .left-view::before {
            position: absolute;
            font-size: 12px;
            top: -16px;
        }

        .front-view::before {
            content: "正视图";
        }

        .top-view::before {
            content: "顶视图";
        }

        .left-view::before {
            content: "左视图";
        }



        .front-view.right::after,
        .top-view.right::after,
        .left-view.right::after {
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            z-index: 0;
            background-color: rgba(255, 255, 255, .8);
            background-image: url('./assets/observing-objects/right.png');
            background-size: 50%;
            background-repeat: no-repeat;
            background-position: center;
            content: '';
        }


        .view-cude {
            position: absolute;
            background-image: url('./assets/cubeTexture.jpg');
            background-size: 110%;
            background-position: center;
            transform: translateY(-100%);
        }

        .view-cude.active {
            background: #404040;
        }

        .hint-container {
            position: absolute;
            top: 72px;
            color: #cc3333;
            opacity: .6;
            line-height: 20px;
            font-size: 12px;
            padding: 10px 10px;
        }

        .win-container {
            position: absolute;
            top: 72px;
            bottom: 0;
            left: 0;
            right: 0;
            background-color: rgba(255, 255, 255, .8);
            display: flex;
            justify-content: center;
            align-items: center;
            display: none
        }

        .win-container img {
            width: 50%;
            height: 80%;
            object-fit: contain;
        }
    </style>
</head>

<body>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three/build/three.module.js",
                "three/addons/": "https://unpkg.com/three/examples/jsm/"
            }
        }
    </script>

    <div class="panel-container">
        <div class="mode-switch">
            <div class="mode-switch-item new">新增模式</div>
            <div class="mode-switch-item del">删除模式</div>
            <div class="mode-switch-item que">答题模式</div>
        </div>
        <div class="mode-switch">
            <div class="mode-switch-item again">再来一题</div>
            <div class="mode-switch-item clear">清空方块</div>
        </div>
    </div>
    <div class="hint-container">提示:答题模式无法编辑添加方块,通过观察立方体,单击三视图中的方块显示和隐藏来回答问题。</div>

    <div class="panel-container bottom">
        <div class="front-view"></div>
        <div class="top-view"></div>
        <div class="left-view"></div>
    </div>

    <div class="win-container">
        <img src="./assets/rainbow-fart/1.gif">
        <img src="./assets/rainbow-fart/2.gif">
    </div>


    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
        import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";





        let newModeDom = document.querySelector('.mode-switch-item.new')
        let delModeDom = document.querySelector('.mode-switch-item.del')
        let queModeDom = document.querySelector('.mode-switch-item.que')
        let cleModeDom = document.querySelector('.mode-switch-item.clear')
        let agaModeDom = document.querySelector('.mode-switch-item.again')
        let opeModeLis = [newModeDom, delModeDom, queModeDom, cleModeDom, agaModeDom]
        let cubeMap = {}; // 方块存储
        let maximumSize = 4;  // 新建底座 4 x 4 
        let cubeMaterial = null;  // 方块材质
        let mode = 'new' // new 新增 del 删除 que 答题 

        function removeActiveOpeMode() {
            opeModeLis.forEach(item =>
                item.classList.remove('active')
            )
        }
        newModeDom.addEventListener('click', () => {
            mode = 'new';
            removeActiveOpeMode()
            newModeDom.classList.add('active')
            cleModeDom.classList.add('active')
            refreshView()
            winHide()
        })
        delModeDom.addEventListener('click', () => {
            mode = 'del';
            removeActiveOpeMode()
            delModeDom.classList.add('active')
            cleModeDom.classList.add('active')
            refreshView()
            winHide()
        })
        queModeDom.addEventListener('click', () => {
            mode = 'que';
            removeActiveOpeMode()
            queModeDom.classList.add('active')
            agaModeDom.classList.add('active')
            agaModeDom.click()
        })
        cleModeDom.addEventListener('click', () => {
            if (cubeMap) {
                for (const cude of Object.values(cubeMap)) {
                    scene.remove(cude)
                }
                cubeMap = {}
                refreshView()
            }
        })
        agaModeDom.addEventListener('click', () => {
            winHide()
            cleModeDom.click();
            randomMap()
        })



        // newModeDom.click()
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

        const renderer = new THREE.WebGLRenderer();

        scene.background = new THREE.Color(0x88ccee);
        camera.position.set(-10.5, 5, -10.5);
        renderer.shadowMap.enabled = true;
        renderer.setSize(window.innerWidth, window.innerHeight);

        document.body.appendChild(renderer.domElement);

        // 创建平行光
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
        directionalLight.position.set(4, 4, -4)
        directionalLight.castShadow = true;
        scene.add(directionalLight);
        // 创建环境光源
        const ambientLight = new THREE.AmbientLight(0x404040); // 灰色的环境光
        scene.add(ambientLight);

        // 材质获取
        const textareaLoader = new THREE.TextureLoader();
        textareaLoader.load("./assets/cubeTexture.jpg", texture => {
            cubeMaterial = new THREE.MeshLambertMaterial({
                map: texture //材质的贴图为加载的图片
            });


            // 材质加载完成
            queModeDom.click();
        })
        // 模型加载
        const gltfLoader = new GLTFLoader().setPath('./assets/observing-objects/');
        gltfLoader.load('left.glb', (gltf) => {
            gltf.scene.position.x = 4.5;
            gltf.scene.position.y = 1.5;
            gltf.scene.position.z = -0.5;
            scene.add(gltf.scene);
        })
        gltfLoader.load('up.glb', (gltf) => {
            gltf.scene.position.x = 0.5;
            gltf.scene.position.y = 4.5;
            gltf.scene.position.z = -0.5;
            scene.add(gltf.scene);
        })
        gltfLoader.load('front.glb', (gltf) => {
            gltf.scene.position.x = 0.5;
            gltf.scene.position.y = 0;
            gltf.scene.position.z = -4.5;
            scene.add(gltf.scene);
        })

        // 平面预设
        for (let x = 0; x < maximumSize; x++) {
            for (let z = 0; z < maximumSize; z++) {
                console.log(x, z);
                const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x999999, side: THREE.DoubleSide });
                const plane = new THREE.Mesh(new THREE.BoxGeometry(1, .01, 1), planeMaterial);
                plane.position.x = x - maximumSize / 2;
                plane.position.z = z - maximumSize / 2;
                plane.name = `plane(${x},${z})`
                plane.receiveShadow = true;
                scene.add(plane);
            }
        }


        // <div class="front-view"></div>
        // <div class="top-view"></div>
        // <div class="left-view"></div>
        // 三视图预设
        let frontViewDom = document.querySelector('.front-view')
        let topViewDom = document.querySelector('.top-view')
        let leftViewDom = document.querySelector('.left-view')
        let viewDomLis = [frontViewDom, topViewDom, leftViewDom]
        // 创建视图方块
        function createViewCude(x, y) {
            let viewCudeDom = document.createElement('div')
            viewCudeDom.classList.add('view-cude')
            viewCudeDom.style.width = `${100 / maximumSize}%`
            viewCudeDom.style.height = `${100 / maximumSize}%`
            viewCudeDom.style.left = (x * 100 / maximumSize) + '%'
            viewCudeDom.style.top = (100 - y * 100 / maximumSize) + '%'
            viewCudeDom.setAttribute('x', x)
            viewCudeDom.setAttribute('y', y)

            viewCudeDom.addEventListener('click', function () {
                if (mode == 'que') {
                    if (viewCudeDom.classList.contains('active')) {
                        viewCudeDom.classList.remove("active")
                    }
                    else {
                        viewCudeDom.classList.add("active")
                    }
                    checkQuestionIsRight()
                }
            })
            return viewCudeDom
        }

        for (let i = 0; i < maximumSize; i++) {
            for (let j = 0; j < maximumSize; j++) {
                frontViewDom.appendChild(createViewCude(j, i))
                topViewDom.appendChild(createViewCude(j, i))
                leftViewDom.appendChild(createViewCude(j, i))
            }
        }
        // 刷新视图
        function refreshView() {
            console.log(cubeMap);
            let cubeMapKeys = Object.keys(cubeMap)

            // 移除正确标签 
            viewDomLis.map(item => {
                item.classList.remove('right')
            })

            // 正视图
            for (let i = 0; i < frontViewDom.children.length; i++) {
                const viewCudeDom = frontViewDom.children.item(i);
                let tmpX = viewCudeDom.getAttribute('x')
                let tmpY = viewCudeDom.getAttribute('y')
                if (cubeMapKeys.find(key => key.startsWith(`${tmpX},${tmpY}`))) viewCudeDom.classList.add('active')
                else viewCudeDom.classList.remove('active')
            }
            // 顶视图
            for (let i = 0; i < topViewDom.children.length; i++) {
                const viewCudeDom = topViewDom.children.item(i);
                let tmpX = viewCudeDom.getAttribute('x')
                let tmpZ = viewCudeDom.getAttribute('y')
                if (cubeMapKeys.find(key => key.startsWith(`${tmpX}`) && key.endsWith(`${tmpZ}`))) viewCudeDom.classList.add('active')
                else viewCudeDom.classList.remove('active')
            }
            // 左视图
            for (let i = 0; i < leftViewDom.children.length; i++) {
                const viewCudeDom = leftViewDom.children.item(i);
                let tmpZ = maximumSize - 1 - +viewCudeDom.getAttribute('x')
                let tmpY = viewCudeDom.getAttribute('y')
                if (cubeMapKeys.find(key => key.endsWith(`${tmpY},${tmpZ}`))) viewCudeDom.classList.add('active')
                else viewCudeDom.classList.remove('active')
            }
        }

        let winDom = document.querySelector('.win-container')   // 胜利
        function winHide() {
            winDom.setAttribute('style', 'display:none')
        }
        function winShow() {
            winDom.setAttribute('style', 'display:flex')
            let winImgCount = winDom.children.length
            let winImgShowIndex = Randoms.getRandomInt(0, winImgCount) // 随机高度 [)

            for (let i = 0; i < winImgCount; i++) {
                const winImg = winDom.children.item(i);
                winImg.setAttribute('style', winImgShowIndex == i ? 'display:flex' : 'display:none')
            }
        }


        // 控制器
        let controls = new OrbitControls(camera, renderer.domElement);
        controls.screenSpacePanning = true;
        controls.minDistance = 5;
        controls.maxDistance = 40;
        controls.target.set(0, 0, 0);
        controls.update();

        //获取鼠标坐标 处理点击某个模型的事件
        let mouse = new THREE.Vector2();
        let raycaster = new THREE.Raycaster();
        // 模型点击事件
        function onmodelclick(event) {
            if (event.type == 'touchstart') {
                let touch = event.changedTouches[0]
                mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
            }
            else {
                mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            }

            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(scene.children);
            const lastObject = intersects.find(o =>
                o.object.name.startsWith('plane') ||
                o.object.name.startsWith('cube')
            ) // 点击的物体是平面或者是立方体

            if (lastObject) { // 存在
                let lastObjectNormal = lastObject.normal // 点击的方向 
                let lastObjectPosition = lastObject.object.position // 物体的位置 
                let isPlane = lastObject.object.name.startsWith('plane'); // 是否点击的是平面

                if (mode == 'new') { // 是否是新增 
                    let x = lastObjectNormal.x + lastObjectPosition.x
                    let y = lastObjectNormal.y + lastObjectPosition.y - (isPlane ? .5 : 0)
                    let z = lastObjectNormal.z + lastObjectPosition.z
                    let checkCanPlaceRes = checkCanPlace(x, y, z)
                    if (isRange(x, y, z) && checkCanPlaceRes) {
                        x = checkCanPlaceRes.x
                        y = checkCanPlaceRes.y
                        z = checkCanPlaceRes.z
                        createCube(x, y, z); // 新建物体 
                    }
                    else {
                        console.log('不能添加');
                    }
                    refreshView()
                }
                else if (mode == 'del' && !isPlane) {
                    let x = lastObjectPosition.x
                    let y = lastObjectPosition.y
                    let z = lastObjectPosition.z
                    removeUpperAndSelf(x, y, z)
                    refreshView()
                }


                console.log('当前地图', Object.keys(cubeMap).map(v => {
                    let [x, y, z] = v.split(',').map(p => +p)
                    return { x, y, z }
                }));
            }

        }
        if (navigator.userAgent.includes('Android') || navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad')) {
            window.addEventListener("touchstart", onmodelclick)
        }
        else {
            window.addEventListener("click", onmodelclick);
        }
        // 是否在范围内
        function isRange(x, y, z) {
            let maxX = maximumSize / 2;
            let minX = -maximumSize / 2;
            let maxZ = maximumSize / 2;
            let minZ = -maximumSize / 2;
            let maxY = maximumSize;
            let minY = 0;

            if (y > maxY) return false
            if (y < minY) return false
            if (x >= maxX) return false;
            if (x < minX) return false;
            if (z >= maxZ) return false;
            if (z < minZ) return false;
            return true;
        }
        // 检查当前位置是否存在物体
        function checkPositionExist(key) {
            return cubeMap[key]
        }

        // 检测是否可以放置
        function checkCanPlace(x, y, z) {
            let standardKey = mapStandardKey(x, y, z)
            let standerdY = +standardKey.split(',')[1]

            for (let i = 0; i <= standerdY; i++) {
                if (!checkPositionExist(mapStandardKey(x, i, z))) {
                    return {
                        x,
                        y: y - standerdY + i,
                        z
                    };
                }
            }
            return false
        }
        // 删除它上面的物体及自身
        function removeUpperAndSelf(x, y, z) {
            let standardKey = mapStandardKey(x, y, z)
            let standerdY = +standardKey.split(',')[1]
            for (let i = standerdY; i < maximumSize; i++) {
                let tmpStandardKey = mapStandardKey(x, i, z)
                let cube = checkPositionExist(tmpStandardKey)

                if (cube) {
                    delete cubeMap[tmpStandardKey]
                    scene.remove(cube)
                }
            }
        }

        // 坐标处理 便于存储
        function mapStandardKey(x, y, z) {
            y = Math.floor(y)
            x = x * -1 + maximumSize / 2 - 1;
            z = z + maximumSize / 2;
            return `${x},${y},${z}`
        }

        // 标准位置 用于3d展示
        function standardToScene(x, y, z) {
            y += .5
            x = x * -1 + maximumSize / 2 - 1;
            z = z - maximumSize / 2;
            return {
                x, y, z
            }
        }




        // 创建方块
        function createCube(x, y, z) {
            const geometry = new THREE.BoxGeometry(1, 1, 1);
            const material = cubeMaterial || new THREE.MeshStandardMaterial({ color: 0x00ff00 });
            const cube = new THREE.Mesh(geometry, material);
            cube.receiveShadow = true
            cube.castShadow = true;

            cube.position.x = x
            cube.position.y = y
            cube.position.z = z

            let standardKey = mapStandardKey(x, y, z) // 获取x,y,z的标准
            cubeMap[standardKey] = cube
            cube.name = `cube(${x},${y},${z})`
            scene.add(cube)
            return cube
        }

        // 出一题
        function randomMap() {
            // 题库出题
            let questionList = [
                [
                    {
                        "x": 0,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 2,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 3,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 1,
                        "y": 1,
                        "z": 2
                    }
                ],
                [
                    {
                        "x": 1,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 0,
                        "y": 0,
                        "z": 1
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 1
                    },
                    {
                        "x": 2,
                        "y": 0,
                        "z": 1
                    },
                    {
                        "x": 1,
                        "y": 1,
                        "z": 1
                    }
                ],
                [
                    {
                        "x": 0,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 2,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 0,
                        "y": 1,
                        "z": 2
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 1
                    }
                ],
                [
                    {
                        "x": 0,
                        "y": 0,
                        "z": 3
                    },
                    {
                        "x": 0,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 0,
                        "y": 0,
                        "z": 1
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 2
                    },
                    {
                        "x": 1,
                        "y": 0,
                        "z": 3
                    },
                    {
                        "x": 2,
                        "y": 0,
                        "z": 3
                    },
                    {
                        "x": 0,
                        "y": 1,
                        "z": 3
                    },
                    {
                        "x": 0,
                        "y": 1,
                        "z": 2
                    },
                    {
                        "x": 0,
                        "y": 2,
                        "z": 3
                    },
                    {
                        "x": 1,
                        "y": 1,
                        "z": 3
                    }
                ]
            ]
            let queIndex = Randoms.getRandomInt(0, questionList.length)
            let que = questionList[queIndex]
            for (let i = 0; i < que.length; i++) {
                let { x, y, z } = que[i];
                createCube(...Object.values(standardToScene(x, y, z)))
            }

            // 随机出题
            // for (let i = 0; i < maximumSize; i++) {
            //     for (let j = 0; j < maximumSize; j++) {
            //         let h = Randoms.getRandomInt(0, maximumSize + 1) // 随机高度 [)
            //         console.log(h);
            //         for (let k = 0; k < h; k++) {
            //             let { x, y, z } = standardToScene(j, k, i) // 输出的坐标
            //             console.log(x, y, z);
            //             createCube(x, y, z)
            //         }
            //     }
            // }
        }

        // 检查问题是否正确
        function checkQuestionIsRight() {
            let cubeMapKeys = Object.keys(cubeMap)
            let frontViewIsRight = true;
            let topViewIsRight = true
            let leftViewIsRight = true
            // 正视图
            for (let i = 0; i < frontViewDom.children.length; i++) {
                const viewCudeDom = frontViewDom.children.item(i);
                let tmpX = viewCudeDom.getAttribute('x')
                let tmpY = viewCudeDom.getAttribute('y')
                let active = viewCudeDom.classList.contains('active')
                let userActive = !!cubeMapKeys.find(key => key.startsWith(`${tmpX},${tmpY}`))
                console.log(tmpX, tmpY, active, userActive);
                if (active != userActive) {
                    frontViewIsRight = false;
                    break;
                }
            }
            // 顶视图
            for (let i = 0; i < topViewDom.children.length; i++) {
                const viewCudeDom = topViewDom.children.item(i);
                let tmpX = viewCudeDom.getAttribute('x')
                let tmpZ = viewCudeDom.getAttribute('y')
                let active = viewCudeDom.classList.contains('active')
                let userActive = !!cubeMapKeys.find(key => key.startsWith(`${tmpX}`) && key.endsWith(`${tmpZ}`))
                if (active != userActive) {
                    topViewIsRight = false;
                    break;
                }
            }

            // 左视图
            for (let i = 0; i < leftViewDom.children.length; i++) {
                const viewCudeDom = leftViewDom.children.item(i);
                let tmpZ = maximumSize - 1 - +viewCudeDom.getAttribute('x')
                let tmpY = viewCudeDom.getAttribute('y')
                let active = viewCudeDom.classList.contains('active')
                let userActive = !!cubeMapKeys.find(key => key.endsWith(`${tmpY},${tmpZ}`))
                if (active != userActive) {
                    leftViewIsRight = false;
                    break;
                }
            }

            if (frontViewIsRight) {
                frontViewDom.classList.add('right')
            }
            if (topViewIsRight) {
                topViewDom.classList.add('right')
            }
            if (leftViewIsRight) {
                leftViewDom.classList.add('right')
            }
            if (frontViewIsRight && topViewIsRight && leftViewIsRight) {
                winShow();
            }
        }




        // 渲染动画
        function animate() {
            renderer.render(scene, camera);
            requestAnimationFrame(animate);

        }

        animate();
    </script>


</body>

</html>

在线预览
https://linyisonger.github.io/H5.Examples/

源码仓库
https://github.com/linyisonger/H5.Examples.git

相关推荐
程序员爱技术1 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
悦涵仙子2 小时前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
兔老大的胡萝卜2 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
一点媛艺3 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风3 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生4 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程5 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk6 小时前
Go-性能调优实战案例
开发语言·后端·golang