全景图(这里特指球面全景图)是指一种图片宽高比为2比1的包含了360°x180°空间的图片,例如8000*4000的jpg图片或tiff图片等。 注:宽高比2:1为水平方向(360):垂直方向(180)
全景漫游(英文:panorama)技术可以让体验者在全景图像构建的全景空间里切换视角的浏览。它是通过拍摄全景图像,再采用计算机图形图像技术构建出全景空间,让使用者能用控制浏览的方向,或左或右、或上或下观看物体或场景,仿佛身临其境一般。与传统的3D建模相比,全景漫游技术制作简单,数据量小,系统消耗低,且更有真实感。
波浪粒子效果封装three.js
<template>
<div id="indexLizi" />
</template>
<script>
import * as THREE from 'three'
export default {
name: 'Pointwave',
props: {
amountX: {
type: Number,
default: 100
},
amountY: {
type: Number,
default: 100
},
color: {
type: Number,
default: 0xffffff
},
top: {
type: Number,
default: 350
}
},
data() {
return {
count: 0,
// 用来跟踪鼠标水平位置
mouseX: 0,
windowHalfX: null,
// 相机
camera: null,
// 场景
scene: null,
// 批量管理粒子
particles: null,
// 渲染器
renderer: null
}
},
mounted() {
this.init()
this.animate()
},
methods: {
init: function() {
const SEPARATION = 100
const SCREEN_WIDTH = window.innerWidth
const SCREEN_HEIGHT = window.innerHeight
const container = document.createElement('div')
this.windowHalfX = window.innerWidth / 2
container.style.position = 'relative'
container.style.top = `${this.top}px`
container.style.height = `${(SCREEN_HEIGHT - this.top)}px`
document.getElementById('indexLizi').appendChild(container)
this.camera = new THREE.PerspectiveCamera(75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 10000)
this.camera.position.z = 1000
this.scene = new THREE.Scene()
const numParticles = this.amountX * this.amountY
const positions = new Float32Array(numParticles * 3)
const scales = new Float32Array(numParticles)
// 初始化粒子位置和大小
let i = 0
let j = 0
for (let ix = 0; ix < this.amountX; ix++) {
for (let iy = 0; iy < this.amountY; iy++) {
positions[i] = ix * SEPARATION - ((this.amountX * SEPARATION) / 2)
positions[i + 1] = 0
positions[i + 2] = iy * SEPARATION - ((this.amountY * SEPARATION) / 2)
scales[j] = 1
i += 3
j++
}
}
const geometry = new THREE.BufferGeometry()
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.addAttribute('scale', new THREE.BufferAttribute(scales, 1))
// 初始化粒子材质
const material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(this.color) }
},
vertexShader: `
attribute float scale;
void main() {
vec4 mvPosition = modelViewMatrix * vec4( position, 2.0 );
gl_PointSize = scale * ( 300.0 / - mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 color;
void main() {
if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;
gl_FragColor = vec4( color, 1.0 );
}
`
})
this.particles = new THREE.Points(geometry, material)
this.scene.add(this.particles)
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
this.renderer.setSize(container.clientWidth, container.clientHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setClearAlpha(0)
container.appendChild(this.renderer.domElement)
window.addEventListener('resize', this.onWindowResize, { passive: false })
document.addEventListener('mousemove', this.onDocumentMouseMove, { passive: false })
document.addEventListener('touchstart', this.onDocumentTouchStart, { passive: false })
document.addEventListener('touchmove', this.onDocumentTouchMove, { passive: false })
},
render: function() {
this.camera.position.x += (this.mouseX - this.camera.position.x) * 0.05
this.camera.position.y = 400
this.camera.lookAt(this.scene.position)
const positions = this.particles.geometry.attributes.position.array
const scales = this.particles.geometry.attributes.scale.array
// 计算粒子位置及大小
let i = 0
let j = 0
for (let ix = 0; ix < this.amountX; ix++) {
for (let iy = 0; iy < this.amountY; iy++) {
positions[i + 1] = (Math.sin((ix + this.count) * 0.3) * 100) + (Math.sin((iy + this.count) * 0.5) * 100)
scales[j] = (Math.sin((ix + this.count) * 0.3) + 1) * 8 + (Math.sin((iy + this.count) * 0.5) + 1) * 8
i += 3
j++
}
}
// 重新渲染粒子
this.particles.geometry.attributes.position.needsUpdate = true
this.particles.geometry.attributes.scale.needsUpdate = true
this.renderer.render(this.scene, this.camera)
this.count += 0.1
},
animate: function() {
requestAnimationFrame(this.animate)
this.render()
},
onDocumentMouseMove: function(event) {
this.mouseX = event.clientX - this.windowHalfX
},
onDocumentTouchStart: function(event) {
if (event.touches.length === 1) {
this.mouseX = event.touches[0].pageX - this.windowHalfX
}
},
onDocumentTouchMove: function(event) {
if (event.touches.length === 1) {
event.preventDefault()
this.mouseX = event.touches[0].pageX - this.windowHalfX
}
},
onWindowResize: function() {
this.windowHalfX = window.innerWidth / 2
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
}
}
}
</script>
粒子图片切换渲染
<!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">
<title>新年关键词</title>
<style>
.ui {
position: absolute;
left: 50%;
bottom: 5%;
width: 300px;
margin-left: -150px;
}
.ui-input {
width: 100%;
height: 50px;
background: none;
font-size: 20px;
font-weight: bold;
color: #fff;
text-align: center;
border: none;
border-bottom: 2px solid white;
}
.ui-input:focus {
outline: none;
border: none;
border-bottom: 2px solid white;
}
.ui--wide {
width: 76%;
margin-left: 12%;
left: 0;
}
body {
background-color: #010101;
width: 100vw;
height: 100vh;
overflow: hidden;
margin: 0;
}
#canvas {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 10%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
function rand(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function randomColor() {
return `rgb(${Math.round(rand(0, 255))}, ${Math.round(rand(0, 255))}, ${Math.round(rand(0, 255))})`;
}
function splitKeyList(text) {
console.log(text);
let keyList = text.split(",");
if (keyList.length === 1) {
keyList = text.split(",");
}
if (keyList.length === 1) {
keyList = text.split(" ");
}
return keyList;
}
const defaultKeyList = [
"./111.png",
"./222.png",
"./333.png",
];
let timer = null;
let currentIndex = 0;
class Particle {
constructor(particle) {
this.x = particle.x;
this.y = particle.y;
this.tx = particle.tx;
this.ty = particle.ty;
this.radius = particle.radius || 2;
this.color = particle.color || "#F00000";
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
return this;
}
}
function drawFrame(particles, finished) {
timer = window.requestAnimationFrame(() => {
drawFrame(particles, finished);
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
const easing = 0.3; // Increased easing value to make particles move faster
const finishedParticles = particles.filter((particle) => {
const dx = particle.tx - particle.x;
const dy = particle.ty - particle.y;
let vx = dx * easing;
let vy = dy * easing;
if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
particle.finished = true;
particle.x = particle.tx;
particle.y = particle.ty;
} else {
particle.x += vx;
particle.y += vy;
}
particle.draw(ctx);
return particle.finished;
});
if (finishedParticles.length === particles.length) {
window.cancelAnimationFrame(timer);
finished && finished();
}
return particles;
}
function getWordPxInfo(target, interval = 3) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const viewWidth = window.innerWidth * 0.5;
const viewHeight = window.innerHeight * 0.5;
if (window.innerWidth < 1080) {
interval = 2;
}
canvas.width = viewWidth;
canvas.height = viewHeight;
return new Promise((resolve) => {
if (typeof target === "string" && (target.startsWith('http') || target.startsWith('./') || target.startsWith('/'))) {
const img = new Image();
img.crossOrigin = "Anonymous"; // 设置跨域
img.src = target;
img.onload = () => {
ctx.drawImage(
img,
(viewWidth - img.width) / 2,
(viewHeight - img.height) / 2,
img.width,
img.height
);
resolve(extractPixels(ctx, viewWidth, viewHeight, interval));
};
img.onerror = () => resolve([]);
} else {
resolve([]);
}
});
}
function extractPixels(ctx, width, height, interval) {
const { data } = ctx.getImageData(0, 0, width, height);
const pixels = [];
for (let x = 0; x < width; x += interval) {
for (let y = 0; y < height; y += interval) {
const pos = (y * width + x) * 4;
if (data[pos + 3] > 128) {
pixels.push({
x,
y,
rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]],
});
}
}
}
return pixels;
}
function createParticles({ text, radius, interval }) {
return getWordPxInfo(text, interval).then((pixels) => {
return pixels.map(({ x, y, rgba: color }) => {
return new Particle({
x: Math.random() * (50 + window.innerWidth * 0.5) - 50,
y: Math.random() * (50 + window.innerHeight * 0.5) - 50,
tx: x,
ty: y,
radius,
color: `rgba(${color.join(",")})`,
});
});
});
}
function displayNextImage() {
// 取消之前的动画帧(如果有的话)
window.cancelAnimationFrame(timer);
// 选择当前索引映像
createParticles({ text: defaultKeyList[currentIndex], radius: 1, interval: 2 }).then((particles) => {
drawFrame(particles);
});
// 增加下一张图像的索引(通过defaultKeyList循环)
currentIndex = (currentIndex + 1) % defaultKeyList.length;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth * 0.5;
canvas.height = window.innerHeight * 0.5;
canvas.addEventListener("click", displayNextImage);
displayNextImage(); // 初次加载时显示第一张图片
</script>
</body>
</html>