threejs 多屏互动效果
看别人做了多屏互动的效果,觉得还挺有意思的,也顺便自己动手操作一下试试。
先来张效果图:
项目地址
参考地址
项目基于vue+threejs。
思路
大体思路如下:
- 架设一个正投影摄像机,在屏幕中间划一个球。
- 显示球的网格并让球转起来。
- 转动的时候发送当前球的ID、旋转角度,窗口距离屏幕偏移,窗口大小,时间戳等信息。
- 接收到其他窗口的信息,根据计算相对于当前球的位置在屏幕上画其他球。
其中跨浏览器标签页通信用的是 BroadcastChannel API。
实践
下边就开始动手做起来。
环境安装
threejs安装命令如下:
npm install --save three
其他部分
架设一个正投影摄像机,在屏幕中间划一个球。
import * as THREE from 'three';
import { onMounted, onUnmounted } from 'vue';
const scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.SphereGeometry( 100, 16, 16 );
// wireframe = true 的时候显示网格
const material = new THREE.MeshBasicMaterial( { color: 0xffff00, wireframe: true } );
const sphere = new THREE.Mesh( geometry, material );
scene.add(sphere)
var balls = {}
var ballsInfo = {}
function animate() {
requestAnimationFrame(animate);
// 让球动起来
sphere.rotation.x += 0.01;
sphere.rotation.y += 0.005;
renderer.render(scene, camera);
}
animate();
onMounted(() => {
let canvas = document.getElementById('can');
renderer = new THREE.WebGLRenderer({canvas: canvas});
resize()
});
window.addEventListener('resize', resize);
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
}
</script>
<template>
<div id="content">
<canvas id="can"></canvas>
</div>
</template>
<style scoped>
#content {
width: 100%;
height: 100%;
overflow: hidden;
}
#can {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
动的时候发送当前球的信息。
const ballId = `ball-${(new Date()).getTime()}`
const channel = new BroadcastChannel('ball')
channel.postMessage({
type: 'ball',
id: ballId,
rotation: {
x: sphere.rotation.x,
y: sphere.rotation.y,
z: sphere.rotation.z,
},
offset: {
x: window.screenX,
y: window.screenY,
},
size: {
width: window.innerWidth,
height: window.innerHeight
},
timestamp: (new Date()).getTime()
})
监听其他窗口信息:
channel.addEventListener('message', function(e) {
// console.log('message is ', e.data)
if (e.data.id in balls) {
} else {
const bgeo = new THREE.SphereGeometry( 100, 16, 16 );
const bmaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
const ball = new THREE.Mesh( bgeo, bmaterial )
balls[e.data.id] = ball;
ball.name = e.data.id
scene.add(balls[e.data.id])
}
ballsInfo[e.data.id] = e.data
})
并把其他球画在当前页面上。
let now = (new Date()).getTime()
for (var i in balls) {
if (ballsInfo[i].id == ballId) {
continue
}
if (now - ballsInfo[i].timestamp > 100) {
let ball = scene.getObjectByName(ballsInfo[i].id)
scene.remove(ball)
delete balls[i]
delete ballsInfo[i]
continue
}
balls[i].position.x = ( (ballsInfo[i].offset.x + ballsInfo[i].size.width / 2) - (window.screenX + window.innerWidth / 2 ) )
balls[i].position.y = ( (window.screenY + window.innerHeight / 2) - (ballsInfo[i].offset.y + ballsInfo[i].size.height / 2) )
balls[i].position.z = 0
balls[i].rotation.x = ballsInfo[i].rotation.x;
balls[i].rotation.y = ballsInfo[i].rotation.y;
balls[i].rotation.z = ballsInfo[i].rotation.z;
}
其中球的位置其实是有敞口相对于屏幕的偏移加上窗口的一半大小来确定。
这样所有的球的位置就都在同一个屏幕坐标系下了。
之后简单的加减就能确定球的相对位置了。
完整代码
import * as THREE from 'three';
import { onMounted, onUnmounted } from 'vue';
const scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.SphereGeometry( 100, 16, 16 );
const material = new THREE.MeshBasicMaterial( { color: 0xffff00, wireframe: true } );
const sphere = new THREE.Mesh( geometry, material );
scene.add(sphere)
const ballId = `ball-${(new Date()).getTime()}`
var balls = {}
var ballsInfo = {}
const channel = new BroadcastChannel('ball')
channel.addEventListener('message', function(e) {
// console.log('message is ', e.data)
if (e.data.id in balls) {
} else {
const bgeo = new THREE.SphereGeometry( 100, 16, 16 );
const bmaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
const ball = new THREE.Mesh( bgeo, bmaterial )
balls[e.data.id] = ball;
ball.name = e.data.id
scene.add(balls[e.data.id])
}
ballsInfo[e.data.id] = e.data
})
function animate() {
requestAnimationFrame(animate);
sphere.rotation.x += 0.01;
sphere.rotation.y += 0.005;
channel.postMessage({
type: 'ball',
id: ballId,
rotation: {
x: sphere.rotation.x,
y: sphere.rotation.y,
z: sphere.rotation.z,
},
offset: {
x: window.screenX,
y: window.screenY,
},
size: {
width: window.innerWidth,
height: window.innerHeight
},
timestamp: (new Date()).getTime()
})
let now = (new Date()).getTime()
for (var i in balls) {
if (ballsInfo[i].id == ballId) {
continue
}
if (now - ballsInfo[i].timestamp > 100) {
let ball = scene.getObjectByName(ballsInfo[i].id)
scene.remove(ball)
delete balls[i]
delete ballsInfo[i]
continue
}
balls[i].position.x = ( (ballsInfo[i].offset.x + ballsInfo[i].size.width / 2) - (window.screenX + window.innerWidth / 2 ) )
balls[i].position.y = ( (window.screenY + window.innerHeight / 2) - (ballsInfo[i].offset.y + ballsInfo[i].size.height / 2) )
balls[i].position.z = 0
balls[i].rotation.x = ballsInfo[i].rotation.x;
balls[i].rotation.y = ballsInfo[i].rotation.y;
balls[i].rotation.z = ballsInfo[i].rotation.z;
}
renderer.render(scene, camera);
}
animate();
onMounted(() => {
let canvas = document.getElementById('can');
renderer = new THREE.WebGLRenderer({canvas: canvas});
resize()
});
window.addEventListener('resize', resize);
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
}
</script>
<template>
<div id="content">
<canvas id="can"></canvas>
</div>
</template>
<style scoped>
#content {
width: 100%;
height: 100%;
overflow: hidden;
}
#can {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>