<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>攝像機粒子交互界面</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
#videoElement {
display: none; /* 隱藏原始視頻,只顯示 Canvas 粒子 */
}
</style>
</head>
<body>
<video id="videoElement" autoplay playsinline></video>
<canvas id="particleCanvas"></canvas>
<script>
const canvas = document.getElementById('particleCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const video = document.getElementById('videoElement');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let particlesArray = [];
let mappedImage = [];
// 鼠標交互設定
const mouse = {
x: null,
y: null,
radius: 100 // 鼠標排斥範圍
};
window.addEventListener('mousemove', function(event){
mouse.x = event.x;
mouse.y = event.y;
});
// 1. 調用攝像頭
navigator.mediaDevices.getUserMedia({ video: true })
.then(function(stream) {
video.srcObject = stream;
video.play();
})
.catch(function(err) {
console.error("無法調用攝像頭: ", err);
alert("請允許攝像頭權限以查看粒子效果!");
});
// 粒子類別設計
class Particle {
constructor(x, y, color, size) {
this.x = x;
this.y = y;
this.baseX = x;
this.baseY = y;
this.color = color;
this.size = size;
this.density = (Math.random() * 30) + 1; // 控制粒子彈回的速度
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
update() {
// 計算鼠標與粒子的距離
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
// 物理交互:鼠標靠近時推開粒子
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
let maxDistance = mouse.radius;
let force = (maxDistance - distance) / maxDistance;
let directionX = forceDirectionX * force * this.density;
let directionY = forceDirectionY * force * this.density;
if (distance < mouse.radius) {
this.x -= directionX;
this.y -= directionY;
} else {
// 粒子回歸原位
if (this.x !== this.baseX) {
let dx = this.x - this.baseX;
this.x -= dx / 10;
}
if (this.y !== this.baseY) {
let dy = this.y - this.baseY;
this.y -= dy / 10;
}
}
}
}
// 2. 初始化粒子系統
function init() {
particlesArray = [];
// 為了效能,我們不需要處理每一個像素,而是每隔一段距離採樣一次(分辨率)
const resolution = 10;
// 將視頻按比例縮放並畫到 canvas 上以獲取數據
let width = canvas.width;
let height = canvas.height;
ctx.drawImage(video, 0, 0, width, height);
let pixels = ctx.getImageData(0, 0, width, height);
ctx.clearRect(0, 0, width, height);
// 3. 遍歷像素數據並生成粒子
for (let y = 0; y < height; y += resolution) {
for (let x = 0; x < width; x += resolution) {
let index = (y * width + x) * 4;
let r = pixels.data[index];
let g = pixels.data[index + 1];
let b = pixels.data[index + 2];
let a = pixels.data[index + 3];
if (a > 0) { // 確保像素不是透明的
let color = `rgb(${r},${g},${b})`;
// 可以根據亮度來調整粒子大小
let brightness = Math.sqrt(r * r * 0.299 + g * g * 0.587 + b * b * 0.114);
let size = (brightness / 255) * 4; // 亮度越高,粒子越大
particlesArray.push(new Particle(x, y, color, size));
}
}
}
}
// 渲染循環
function animate() {
// 定期重新採樣視頻幀(這會帶來動態的攝像頭更新)
if (video.readyState === 4) {
init();
}
// 使用帶有透明度的黑色背景,產生拖影效果 (Trail effect)
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particlesArray.length; i++) {
particlesArray[i].update();
particlesArray[i].draw();
}
requestAnimationFrame(animate);
}
// 確保視頻準備好後再開始動畫
video.addEventListener('play', () => {
animate();
});
// 處理視窗大小調整
window.addEventListener('resize', function(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
</script>
</body>
</html>