o( ̄▽ ̄)ブ 我又带着新的demo来啦~
预览
功能点
- 立方体的阴影
- 立方体的添加
- 位置记录
- 最大限制
- 三视图展示
- 立方体的移除
- 答题模式
- 随机出题
- 题库出题
源码
注释算是比较全了,可能部分会有点绕,还能够再优化一下~
<!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.lengthlet 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.xlet y = lastObjectNormal.y + lastObjectPosition.y - (isPlane ? .5 : 0)let z = lastObjectNormal.z + lastObjectPosition.zlet checkCanPlaceRes = checkCanPlace(x, y, z)if (isRange(x, y, z) && checkCanPlaceRes) {x = checkCanPlaceRes.xy = checkCanPlaceRes.yz = checkCanPlaceRes.zcreateCube(x, y, z); // 新建物体 }else {console.log('不能添加');}refreshView()}else if (mode == 'del' && !isPlane) {let x = lastObjectPosition.xlet y = lastObjectPosition.ylet z = lastObjectPosition.zremoveUpperAndSelf(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 falseif (y < minY) return falseif (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 += .5x = 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 = truecube.castShadow = true;cube.position.x = xcube.position.y = ycube.position.z = zlet standardKey = mapStandardKey(x, y, z) // 获取x,y,z的标准cubeMap[standardKey] = cubecube.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 = truelet 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