看别人做了多屏互动的效果,觉得还挺有意思的,也顺便自己动手操作一下试试。
先来张效果图:
项目地址
参考地址
项目基于vue+threejs。
大体思路如下:
其中跨浏览器标签页通信用的是 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>