<!DOCTYPE html>
<body>
<div id="container"></div>
<!-- "開始" 按鈕 -->
<button id="startButton">開始</button>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/star.js"></script>
<script src="js/three.min.js"></script>
<script src="js/gsap.min.js"></script>
<script>
// 初始化場景、相機和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0); // 設置背景為透明
document.getElementById('container').appendChild(renderer.domElement);
// 創建圓角矩形形狀的函數
function createRoundedRectShape(width, height, radius) {
const shape = new THREE.Shape();
shape.moveTo(-width / 2 + radius, -height / 2);
shape.lineTo(width / 2 - radius, -height / 2);
shape.quadraticCurveTo(width / 2, -height / 2, width / 2, -height / 2 + radius);
shape.lineTo(width / 2, height / 2 - radius);
shape.quadraticCurveTo(width / 2, height / 2, width / 2 - radius, height / 2);
shape.lineTo(-width / 2 + radius, height / 2);
shape.quadraticCurveTo(-width / 2, height / 2, -width / 2, height / 2 - radius);
shape.lineTo(-width / 2, -height / 2 + radius);
shape.quadraticCurveTo(-width / 2, -height / 2, -width / 2 + radius, -height / 2);
return shape;
}
// 設置矩形區塊
const group = new THREE.Group();
scene.add(group);
// 動態計算卡片大小和間距
function calculateCardProperties() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const rectWidth = windowWidth / 25;
const rectHeight = windowHeight / 12;
const spacingX = rectWidth * 0.2;
const spacingY = rectHeight * 0.2;
return {
rectWidth,
rectHeight,
spacingX,
spacingY
};
}
// 創建卡片並按窗口大小自適應
function createCards() {
while (group.children.length) {
group.remove(group.children[0]);
}
const {
rectWidth,
rectHeight,
spacingX,
spacingY
} = calculateCardProperties();
const numRects = 119;
const cols = 17;
const rows = 7;
for (let i = 0; i < numRects; i++) {
const shape = createRoundedRectShape(rectWidth, rectHeight, 2);
const geometry = new THREE.ShapeGeometry(shape);
// 材質設置,包括白邊框、透明度和光暈效果
const material = new THREE.MeshBasicMaterial({
color: 0x00ffcc,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
depthTest: false, // 禁用深度測試,防止遮擋
depthWrite: false, // 禁用深度寫入,讓透明物體不會遮擋
blending: THREE.AdditiveBlending
});
const edgeMaterial = new THREE.LineBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.7,
depthTest: false, // 同樣禁用深度測試
depthWrite: false
});
const rect = new THREE.Mesh(geometry, material);
// 增加邊框
const edges = new THREE.EdgesGeometry(geometry);
const edgeMesh = new THREE.LineSegments(edges, edgeMaterial);
rect.add(edgeMesh);
const x = (i % cols) * (rectWidth + spacingX) - (cols * (rectWidth + spacingX) / 2);
const y = Math.floor(i / cols) * (rectHeight + spacingY) - (rows * (rectHeight + spacingY) / 2);
// 初始隨機位置設置
rect.position.set(
(Math.random() - 0.5) * window.innerWidth * 2,
(Math.random() - 0.5) * window.innerHeight * 2,
-1000
);
group.add(rect);
// 使用 GSAP 讓卡片飛入最終位置
gsap.to(rect.position, {
x: x,
y: y,
z: 0,
duration: 1.5 + Math.random(),
ease: "power2.out",
delay: Math.random() * 1
});
}
camera.position.z = Math.max(window.innerWidth, window.innerHeight) / 2;
}
// 使用 Fibonacci Sphere 演算法來均勻分佈卡片
function fibonacciSphere(samples, radius) {
const points = [];
const offset = 2 / samples;
const increment = Math.PI * (3 - Math.sqrt(5)); // φ = Golden angle
for (let i = 0; i < samples; i++) {
const y = ((i * offset) - 1) + (offset / 2);
const r = Math.sqrt(1 - Math.pow(y, 2));
const phi = i * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
points.push({
x: x * radius,
y: y * radius,
z: z * radius
});
}
return points;
}
// 球體半徑設定
const sphereRadius = 350;
const targetPositions = fibonacciSphere(119, sphereRadius);
// 過渡動畫,並且讓卡片的朝向改變
function transformToSphere() {
group.children.forEach((rect, index) => {
const {
x,
y,
z
} = targetPositions[index];
// 設置渲染順序,確保卡片按順序渲染,避免遮擋
rect.renderOrder = index;
// 為每個卡片在過渡過程中引入一些隨機偏移
const randomOffsetX = (Math.random() - 0.5) * 300;
const randomOffsetY = (Math.random() - 0.5) * 300;
const randomOffsetZ = (Math.random() - 0.5) * 300;
gsap.to(rect.position, {
x: x + randomOffsetX,
y: y + randomOffsetY,
z: z + randomOffsetZ,
duration: 1.5,
delay: Math.random() * 1,
ease: "power2.out",
onUpdate: () => {
rect.lookAt(new THREE.Vector3(0, 0, 0));
// 強制更新文字標籤的朝向和材質
rect.children.forEach(child => {
if (child instanceof THREE.Mesh) {
child.lookAt(new THREE.Vector3(0, 0, 0)); // 讓文字平面朝向球心
child.material.needsUpdate = true; // 強制更新材質,避免丟失
}
});
},
onComplete: () => {
gsap.to(rect.position, {
x: x,
y: y,
z: z,
duration: 1.5,
ease: "power2.inOut",
onUpdate: () => {
rect.lookAt(new THREE.Vector3(0, 0, 0));
// 持續保持文字平面朝向球心
rect.children.forEach(child => {
if (child instanceof THREE.Mesh) {
child.lookAt(new THREE.Vector3(0, 0, 0));
child.material.needsUpdate = true;
}
});
}
});
}
});
});
}
// 透過 Ajax 獲取參與者資料
function fetchParticipantData() {
return $.ajax({
url: 'includes/raffle_management.php',
method: 'GET',
data: {
action: 'get_participant_data'
},
dataType: 'json'
});
}
// 將參與者隨機分配到卡片上,工號與名字分行顯示
function assignParticipantsToCards(participantData) {
const shuffledData = participantData.sort(() => 0.5 - Math.random());
group.children.forEach((rect, index) => {
if (index < shuffledData.length) {
const participant = shuffledData[index];
const staffId = participant.user_staff;
const userName = participant.user_name;
// 確認卡片是否已有文字標籤,避免重複添加
const existingTextPlane = rect.children.find(child => child instanceof THREE.Mesh);
if (!existingTextPlane) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const context = canvas.getContext('2d');
context.font = '20px Arial';
context.fillStyle = 'white';
context.textAlign = 'center';
// 繪製工號在上方
context.fillText(staffId, canvas.width / 2, canvas.height / 3);
// 繪製姓名在下方
context.fillText(userName, canvas.width / 2, (canvas.height / 3) * 2);
// 使用 canvas 創建紋理
const texture = new THREE.CanvasTexture(canvas);
const textMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 1, // 設置為完全可見
depthTest: false, // 禁用深度測試,避免遮擋
depthWrite: false // 禁用深度寫入
});
// 創建平面顯示文字
const planeGeometry = new THREE.PlaneGeometry(150, 75);
const textMesh = new THREE.Mesh(planeGeometry, textMaterial);
textMesh.position.set(0, 0, 0.1); // 確保文字凸出顯示
textMesh.renderOrder = rect.renderOrder + 1; // 確保文字比卡片稍後渲染
rect.add(textMesh);
}
}
});
}
// 點擊按鈕後觸發過渡動畫
document.getElementById('startButton').addEventListener('click', function() {
fetchParticipantData().then(participantData => {
assignParticipantsToCards(participantData);
transformToSphere(); // 啟動動畫
this.style.display = 'none'; // 隱藏按鈕
});
});
// 渲染循環
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
// 自適應窗口大小變化
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
createCards();
});
// 初始化卡片並分配名單
fetchParticipantData().then(participantData => {
createCards();
assignParticipantsToCards(participantData);
});
</script>
</body>
這個是一個宇宙星空穿梭的背景
有看到一個抽獎的畫面蠻不錯的,想自己來練習練習
這兩天在要做個卡片轉場3D球體
點擊按鈕執行過渡時某些卡片的文字會不見
拚成球體後,都是正面的卡片文字消失
如圖
想請教各位前輩老師們
我是否遺漏了什麼環節??