CSS3D实现相机移动旋转
export class CSS3DObject {
active = true;
#localMat = mat4.create();
#worldMat = mat4.create();
#parent = null;
#children = [];
get children() {
return this.#children;
}
#rotation = quat.create();
#position = vec3.create();
#scale = vec3.create();
#needUpdateLocal = false;
#needUpdateWorld = false;
element = null;
constructor(element) {
this.element = element;
vec3.set(this.#scale, 1, 1, 1);
}
setPosition(pos) {
if (vec3.equals(pos, this.#position)) return;
vec3.copy(this.#position, pos);
this.#needUpdateLocal = true;
}
setScale(scale) {
if (vec3.equals(scale, this.#scale)) return;
vec3.copy(this.#scale, scale);
this.#needUpdateLocal = true;
}
translate(val) {
vec3.add(this.#position, this.#position, val);
this.#needUpdateLocal = true;
}
rotate(angle, axis) {
const q = quat.create();
quat.setAxisAngle(q, axis, glMatrix.toRadian(angle));
quat.normalize(q, q);
quat.mul(this.#rotation, q);
this.#needUpdateLocal = true;
}
rotateX(val) {
quat.rotateX(this.#rotation, this.#rotation, glMatrix.toRadian(val));
quat.normalize(this.#rotation, this.#rotation);
this.#needUpdateLocal = true;
}
rotateY(val) {
quat.rotateY(this.#rotation, this.#rotation, glMatrix.toRadian(val));
quat.normalize(this.#rotation, this.#rotation);
this.#needUpdateLocal = true;
}
rotateZ(val) {
quat.rotateZ(this.#rotation, this.#rotation, glMatrix.toRadian(val));
quat.normalize(this.#rotation, this.#rotation);
this.#needUpdateLocal = true;
}
getLocalMatrix() {
if (this.#needUpdateLocal) this.#calcLocalMatrix();
return this.#localMat;
}
getWorldMatrix(force = false) {
this.#calcWorldMatrix(force);
return this.#worldMat;
}
#calcLocalMatrix() {
mat4.fromRotationTranslationScale(
this.#localMat,
this.#rotation,
this.#position,
this.#scale
);
this.#needUpdateLocal = false;
this.#needUpdateWorld = true;
}
#calcWorldMatrix(force = false) {
const local = this.getLocalMatrix();
if (force) {
if (this.#parent === null) {
mat4.copy(this.#worldMat, local);
} else {
mat4.mul(this.#worldMat, this.#parent.getWorldMatrix(true), local);
}
} else {
if (this.#needUpdateWorld) {
if (this.#parent === null) {
mat4.copy(this.#worldMat, local);
} else {
mat4.mul(this.#worldMat, this.#parent.getWorldMatrix(), local);
}
for (let i = 0; i < this.#children.length; i++) {
this.#children[i].#needUpdateWorld = true;
}
this.#needUpdateWorld = false;
}
}
}
setParent(obj) {
if (this.#parent === obj) return;
let parent = this.#parent;
if (parent) {
parent.removeChild(this);
}
if (obj) {
obj.addChild(this);
}
this.#parent = obj;
}
addChild(obj) {
if (this.#children.indexOf(obj) < 0) {
this.#children.push(obj);
obj.setParent(this);
}
}
removeChild(obj) {
let idx = this.#children.indexOf(obj);
this.removeChildAt(idx);
}
removeChildAt(idx) {
if (idx < 0 || idx >= this.#children.length) return;
this.#children[idx].setParent(null);
this.#children.splice(idx, 1);
}
}
xport class CSS3DScene extends CSS3DObject {
#objects = new WeakMap();
tranverse(func) {
function foreach(obj) {
for (let i = 0; i < obj.children.length; i++) {
if (!obj.active) continue;
func(obj.children[i]);
foreach(obj.children[i]);
}
}
func(this);
foreach(this);
}
}
export class CSS3DCamera extends CSS3DObject {
#fovy = 45;
#viewMatrix = mat4.create();
#near = 1;
constructor(element) {
super(element);
this.element.style.transformStyle = "preserve-3d";
}
setFovY(fovy) {
this.#fovy = fovy;
}
getPersperctive() {
return 1 / Math.tan(glMatrix.toRadian(this.#fovy / 2));
}
getViewMatrix(force = false) {
mat4.invert(this.#viewMatrix, this.getWorldMatrix(force));
return this.#viewMatrix;
}
}
export class Css3DRenderer {
#dom = null;
#width = 100;
#height = 100;
constructor(width, height, dom) {
this.#width = width;
this.#height = height;
this.#dom = dom || document.createElement("div");
if (!dom) {
this.#dom.style.width = width;
this.#dom.style.height = height;
document.body.appendChild(this.#dom);
}
}
resize(width, height) {
this.#width = width;
this.#height = height;
}
render(scene, camera) {
const vm = camera.getViewMatrix(true);
if (camera.element.parentElement !== this.#dom) {
camera.element.parentElement = this.#dom;
}
const fov = (camera.getPersperctive() * this.#height) / 2;
this.#dom.style.perspective = `${fov}px`;
camera.element.style.transform = `translateZ(${fov}px) matrix3d(${
vm[0]
},${-vm[1]},${vm[2]},${vm[3]},${vm[4]},${-vm[5]},${vm[6]},${vm[7]},${
vm[8]
},${-vm[9]},${vm[10]},${vm[11]},${vm[12]},${-vm[13]},${vm[14]},${
vm[15]
}) translate(${this.#width / 2}px,${this.#height / 2}px)`;
scene.tranverse(function (obj) {
const m = obj.getWorldMatrix();
if (obj.element !== null) {
obj.element.style.transform = `translate(-50%,-50%) matrix3d(${m[0]},${
m[1]
},${m[2]},${m[3]},${-m[4]},${-m[5]},${-m[6]},${-m[7]},${m[8]},${m[9]},${
m[10]
},${m[11]},${m[12]},${m[13]},${m[14]},${m[15]})`;
if (obj.element.parentElement !== camera.element) {
obj.element.parentElement = camera.element;
}
}
});
}
}
import { CSS3DCamera, CSS3DObject, Css3DRenderer, CSS3DScene } from "./css3d.js";
const scene = new CSS3DScene(null);
const camera = new CSS3DCamera(document.querySelector(".camera"));
const renderer = new Css3DRenderer(
window.innerWidth,
window.innerHeight,
document.querySelector(".scene")
);
function resize() {
renderer.resize(window.innerWidth, window.innerHeight);
}
const group = new CSS3DObject(null),
front = new CSS3DObject(document.querySelector(".front")),
back = new CSS3DObject(document.querySelector(".back")),
left = new CSS3DObject(document.querySelector(".left")),
right = new CSS3DObject(document.querySelector(".right")),
top = new CSS3DObject(document.querySelector(".top")),
down = new CSS3DObject(document.querySelector(".down"));
front.setPosition([0, 0, 50]);
group.addChild(front);
back.rotateY(180);
back.setPosition([0, 0, -50]);
group.addChild(back);
left.rotateY(-90);
left.setPosition([-50, 0, 0]);
group.addChild(left);
right.rotateY(90);
right.setPosition([50, 0, 0]);
group.addChild(right);
top.rotateX(-90);
top.setPosition([0, 50, 0]);
group.addChild(top);
down.rotateX(90);
down.setPosition([0, -50, 0]);
group.addChild(down);
scene.addChild(group);
renderer.render(scene, camera);
window.addEventListener("resize", resize);
camera.setPosition([0, 0, 1000]);
class Input {
static keyMap = new Map();
static isPressing(key) {}
}
let moveSpeed = 10,
rotateSpeed = 1;
window.onkeypress = function (e) {
if (e.code === "KeyA") {
pressA = true;
} else if (e.code === "KeyD") {
pressD = true;
} else if (e.code === "KeyW") {
pressW = true;
} else if (e.code === "KeyS") {
pressS = true;
} else if (e.code === "KeyQ") {
pressQ = true;
} else if (e.code === "KeyE") {
pressE = true;
}
};
window.onkeyup = function (e) {
if (e.code === "KeyA") {
pressA = false;
} else if (e.code === "KeyD") {
pressD = false;
} else if (e.code === "KeyW") {
pressW = false;
} else if (e.code === "KeyS") {
pressS = false;
} else if (e.code === "KeyQ") {
pressQ = false;
} else if (e.code === "KeyE") {
pressE = false;
}
};
let pressA = false,
pressD = false,
pressW = false,
pressS = false,
pressQ = false,
pressE = false;
function updateCamera() {
if (pressA) {
camera.translate([moveSpeed, 0, 0]);
}
if (pressD) {
camera.translate([-moveSpeed, 0, 0]);
}
if (pressW) {
camera.translate([0, -moveSpeed, 0]);
}
if (pressS) {
camera.translate([0, moveSpeed, 0]);
}
if (pressQ) {
camera.rotateY(rotateSpeed);
}
if (pressE) {
camera.rotateY(-rotateSpeed);
}
}
function animate() {
group.rotateX(1);
updateCamera();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Game</title>
<link rel="stylesheet" href="./main.css" />
</head>
<body>
<div class="scene">
<div class="camera">
<div class="plane front"></div>
<div class="plane back"></div>
<div class="plane left"></div>
<div class="plane right"></div>
<div class="plane top"></div>
<div class="plane down"></div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
:root {
font-size: calc(1em + 1vw);
box-sizing: border-box;
}
:root,
::before,
::after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
}
.scene {
overflow: hidden;
width: 100vw;
height: 100vh;
perspective: 200px;
}
.camera {
width: 100%;
height: 100%;
transform-style: preserve-3d;
transform: translateZ(-200px) rotateX(30px);
}
.plane {
display: block;
position: absolute;
width: 100px;
height: 100px;
}
.front {
background-color: red;
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 50, 1);
}
.back {
background-color: green;
transform: matrix3d(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, -50, 1);
}
.left {
background-color: blue;
transform: matrix3d(0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, -50, 0, 0, 1);
}
.right {
background-color: #ff0;
transform: matrix3d(0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 50, 0, 0, 1);
}
.top {
background-color: #0ff;
transform: matrix3d(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, -50, 0, 1);
}
.down {
background-color: #f0f;
transform: matrix3d(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 50, 0, 1);
}