509 lines
24 KiB
HTML
509 lines
24 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
|
<title>圣诞快乐</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
html, body { height: 100%; }
|
|
body { margin: 0; overflow: hidden; background: #000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, PingFangSC, "Microsoft YaHei", sans-serif; }
|
|
#app { position: relative; width: 100vw; height: 100vh; }
|
|
#canvas-container { position: absolute; inset: 0; }
|
|
|
|
.barrage { position: fixed; inset: 0; pointer-events: none; z-index: 9; }
|
|
.barrage-item { position: fixed; left: 100vw; white-space: nowrap; font-weight: 700; text-shadow: 0 0 8px rgba(255,255,255,0.35), 0 0 16px rgba(255,255,255,0.2); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); animation-name: fly; animation-timing-function: linear; animation-fill-mode: forwards; }
|
|
@keyframes fly { 0% { transform: translateX(0); } 100% { transform: translateX(-120vw); } }
|
|
.fullscreen-btn { position: fixed; right: 14px; bottom: 14px; z-index: 11; padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.12); color: #fff; font-weight: 600; letter-spacing: 0.5px; backdrop-filter: blur(4px); cursor: pointer; }
|
|
.fullscreen-btn:hover { background: rgba(255,255,255,0.18); }
|
|
@media (max-width: 768px) { .fullscreen-btn { padding: 9px 12px; right: 10px; bottom: 10px; font-size: 13px; } }
|
|
@media (max-width: 768px) {
|
|
.barrage-item { font-size: clamp(14px, 3.5vw, 20px); }
|
|
}
|
|
@media (min-width: 769px) {
|
|
.barrage-item { font-size: clamp(16px, 2.1vw, 28px); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module">
|
|
import { createApp, ref, onMounted, h } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
|
|
import * as THREE from 'https://unpkg.com/three@0.164.1/build/three.module.js';
|
|
|
|
const App = {
|
|
setup() {
|
|
const isFs = ref(false);
|
|
const barrageItems = ref([]);
|
|
const containerEl = ref(null);
|
|
|
|
let renderer, scene, camera, group;
|
|
let geoLeaves, matLeaves, pointsLeaves, posLeaves, colLeaves, velLeaves, tgtLeaves, startMorphLeaves;
|
|
let geoOrnaments, matOrnaments, pointsOrnaments, posOrnaments, colOrnaments, velOrnaments, tgtOrnaments, startMorphOrnaments;
|
|
let geoTrunk, matTrunk, pointsTrunk, posTrunk, colTrunk, velTrunk, tgtTrunk, startMorphTrunk;
|
|
let geoStar, matStar, pointsStar, posStar, colStar, velStar, tgtStar, startMorphStar;
|
|
let geoAccents, matAccents, pointsAccents, posAccents, colAccents, velAccents, tgtAccents, startMorphAccents;
|
|
let snowGroup, snowSprites, snowVel, snowRot, snowScale, snowOpacityPhase;
|
|
let startTime = 0;
|
|
const disperseDuration = 3500;
|
|
const morphDuration = 3000;
|
|
const particleCountLeaves = 5400;
|
|
const particleCountOrnaments = 400;
|
|
const particleCountTrunk = 1200;
|
|
const particleCountStar = 320;
|
|
const particleCountAccents = 160;
|
|
const snowCount = 200;
|
|
const treeHeight = 14;
|
|
const baseRadius = 6.2;
|
|
let running = true;
|
|
const topY = treeHeight + 1.2;
|
|
const bottomY = -1.4;
|
|
|
|
const messages = [
|
|
'圣诞快乐', '平安喜乐', '万事胜意', '心想事成', '前程似锦', '阖家幸福',
|
|
'福星高照', '岁岁常欢愉', '诸事顺遂', '新年好运常在', '健康平安', '幸福常伴'
|
|
];
|
|
|
|
function randomHsl() {
|
|
const h = Math.floor(Math.random() * 360);
|
|
const s = 68 + Math.random() * 22;
|
|
const l = 50 + Math.random() * 10;
|
|
return `hsl(${h}deg, ${s}%, ${l}%)`;
|
|
}
|
|
|
|
function createSnowflakeTexture() {
|
|
const c = document.createElement('canvas');
|
|
c.width = 128; c.height = 128;
|
|
const ctx = c.getContext('2d');
|
|
ctx.clearRect(0, 0, 128, 128);
|
|
ctx.translate(64, 64);
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.95)';
|
|
ctx.lineWidth = 2;
|
|
for (let i = 0; i < 6; i++) {
|
|
const ang = (Math.PI * 2 / 6) * i;
|
|
const x = Math.cos(ang) * 48;
|
|
const y = Math.sin(ang) * 48;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(x, y);
|
|
ctx.stroke();
|
|
for (let b = 1; b <= 3; b++) {
|
|
const t = b / 4;
|
|
const bx = Math.cos(ang) * 48 * t;
|
|
const by = Math.sin(ang) * 48 * t;
|
|
const sideAng = ang + Math.PI / 2;
|
|
const len = 10 * (1 - t);
|
|
ctx.beginPath();
|
|
ctx.moveTo(bx, by);
|
|
ctx.lineTo(bx + Math.cos(sideAng) * len, by + Math.sin(sideAng) * len);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(bx, by);
|
|
ctx.lineTo(bx - Math.cos(sideAng) * len, by - Math.sin(sideAng) * len);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
const tex = new THREE.CanvasTexture(c);
|
|
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
tex.magFilter = THREE.LinearFilter;
|
|
tex.needsUpdate = true;
|
|
return tex;
|
|
}
|
|
|
|
function createSnowGlowTexture() {
|
|
const c = document.createElement('canvas');
|
|
c.width = 128; c.height = 128;
|
|
const ctx = c.getContext('2d');
|
|
const grd = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
grd.addColorStop(0, 'rgba(255,255,255,0.57)');
|
|
grd.addColorStop(0.4, 'rgba(255,255,255,0.23)');
|
|
grd.addColorStop(1, 'rgba(255,255,255,0.0)');
|
|
ctx.fillStyle = grd;
|
|
ctx.fillRect(0, 0, 128, 128);
|
|
const tex = new THREE.CanvasTexture(c);
|
|
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
tex.magFilter = THREE.LinearFilter;
|
|
tex.needsUpdate = true;
|
|
return tex;
|
|
}
|
|
|
|
function spawnBarrage() {
|
|
const count = 2 + Math.floor(Math.random() * 3);
|
|
const now = Date.now();
|
|
for (let i = 0; i < count; i++) {
|
|
const text = messages[Math.floor(Math.random() * messages.length)];
|
|
const top = Math.random() * 90 + 5;
|
|
const duration = 10 + Math.random() * 8;
|
|
const color = randomHsl();
|
|
const id = `${now}-${i}-${Math.random().toString(36).slice(2,7)}`;
|
|
barrageItems.value.push({ id, text, top: `${top}%`, duration: `${duration}s`, color, shadow: color });
|
|
setTimeout(() => {
|
|
const idx = barrageItems.value.findIndex(x => x.id === id);
|
|
if (idx >= 0) barrageItems.value.splice(idx, 1);
|
|
}, duration * 1000 + 400);
|
|
}
|
|
}
|
|
|
|
function enterFs() {
|
|
const el = document.getElementById('app') || containerEl.value || document.documentElement;
|
|
const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
|
|
if (rfs) rfs.call(el);
|
|
}
|
|
|
|
function exitFs() {
|
|
const efs = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
|
|
if (efs) efs.call(document);
|
|
}
|
|
|
|
function toggleFs() { if (document.fullscreenElement) exitFs(); else enterFs(); }
|
|
|
|
function computeLeafTargets(count) {
|
|
const arr = new Float32Array(count * 3);
|
|
const layers = 12;
|
|
for (let i = 0; i < count; i++) {
|
|
const layer = Math.floor(Math.random() * layers);
|
|
const yBase = (layer / (layers - 1)) * treeHeight;
|
|
const y = yBase + (Math.random() - 0.5) * (treeHeight * 0.035 + (1 - layer / layers) * 0.12);
|
|
const t = y / treeHeight;
|
|
const shelf = 0.45 * Math.sin(3.5 * (1 - t));
|
|
const wobble = 0.25 * Math.sin(8 * t + Math.random() * 0.8);
|
|
const r = Math.max(0.04, (baseRadius * (1 - t)) + shelf + wobble);
|
|
const a = Math.random() * Math.PI * 2;
|
|
const x = r * Math.cos(a);
|
|
const z = r * Math.sin(a);
|
|
arr[i * 3] = x;
|
|
arr[i * 3 + 1] = y;
|
|
arr[i * 3 + 2] = z;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function computeTrunkTargets(count) {
|
|
const arr = new Float32Array(count * 3);
|
|
for (let i = 0; i < count; i++) {
|
|
const ty = -1 + Math.random() * (treeHeight * 0.22);
|
|
const tr = 0.35 + Math.random() * 0.22;
|
|
const ta = Math.random() * Math.PI * 2;
|
|
arr[i * 3] = tr * Math.cos(ta);
|
|
arr[i * 3 + 1] = ty;
|
|
arr[i * 3 + 2] = tr * Math.sin(ta);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function computeOrnamentTargets(count) {
|
|
const arr = new Float32Array(count * 3);
|
|
for (let i = 0; i < count; i++) {
|
|
const y = Math.random() * treeHeight * 0.95;
|
|
const t = y / treeHeight;
|
|
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.3 * Math.sin(7 * t + Math.random()));
|
|
const a = Math.random() * Math.PI * 2;
|
|
const x = r * Math.cos(a);
|
|
const z = r * Math.sin(a);
|
|
arr[i * 3] = x;
|
|
arr[i * 3 + 1] = y;
|
|
arr[i * 3 + 2] = z;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function computeStarTargets(count) {
|
|
const arr = new Float32Array(count * 3);
|
|
for (let i = 0; i < count; i++) {
|
|
const a = Math.random() * Math.PI * 2;
|
|
const rBase = 0.75;
|
|
const r = rBase + 0.25 * Math.cos(5 * a) + (Math.random() - 0.5) * 0.12;
|
|
const x = r * Math.cos(a);
|
|
const z = r * Math.sin(a);
|
|
const y = treeHeight + 0.9 + (Math.random() - 0.5) * 0.35;
|
|
arr[i * 3] = x;
|
|
arr[i * 3 + 1] = y;
|
|
arr[i * 3 + 2] = z;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function computeAccentsTargets(count) {
|
|
const arr = new Float32Array(count * 3);
|
|
for (let i = 0; i < count; i++) {
|
|
const y = Math.random() * treeHeight;
|
|
const t = y / treeHeight;
|
|
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.35 * Math.sin(6 * t + Math.random()));
|
|
const a = Math.random() * Math.PI * 2;
|
|
const x = r * Math.cos(a);
|
|
const z = r * Math.sin(a);
|
|
arr[i * 3] = x;
|
|
arr[i * 3 + 1] = y;
|
|
arr[i * 3 + 2] = z;
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function easeInOutCubic(t) {
|
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
}
|
|
|
|
function ornamentColors(index) {
|
|
const palette = [
|
|
new THREE.Color(0xff4d6d), new THREE.Color(0xffb703), new THREE.Color(0x32c3ff),
|
|
new THREE.Color(0x8ce99a), new THREE.Color(0xb197fc), new THREE.Color(0xffaad4)
|
|
];
|
|
return palette[index % palette.length];
|
|
}
|
|
|
|
|
|
function initSet(count, size, colorFn, computeTargets) {
|
|
const geo = new THREE.BufferGeometry();
|
|
const pos = new Float32Array(count * 3);
|
|
const col = new Float32Array(count * 3);
|
|
const vel = new Float32Array(count * 3);
|
|
for (let i = 0; i < count; i++) {
|
|
const ix = i * 3;
|
|
pos[ix] = (Math.random() - 0.5) * 0.4;
|
|
pos[ix + 1] = (Math.random() - 0.5) * 0.4;
|
|
pos[ix + 2] = (Math.random() - 0.5) * 0.4;
|
|
let vx = (Math.random() - 0.5);
|
|
let vy = (Math.random() - 0.5) * 0.6;
|
|
let vz = (Math.random() - 0.5);
|
|
const s = 0.012 + Math.random() * 0.018;
|
|
vel[ix] = vx * s;
|
|
vel[ix + 1] = vy * s;
|
|
vel[ix + 2] = vz * s;
|
|
const c = colorFn(i);
|
|
col[ix] = c.r;
|
|
col[ix + 1] = c.g;
|
|
col[ix + 2] = c.b;
|
|
}
|
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
|
geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
|
|
const mat = new THREE.PointsMaterial({ size, sizeAttenuation: true, transparent: true, opacity: 0.95, vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
|
const points = new THREE.Points(geo, mat);
|
|
const targets = computeTargets(count);
|
|
group.add(points);
|
|
return { geo, pos, col, vel, mat, points, targets };
|
|
}
|
|
|
|
function initThree() {
|
|
scene = new THREE.Scene();
|
|
scene.fog = new THREE.FogExp2(0x06101a, 0.035);
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 6.5, 20);
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
containerEl.value.appendChild(renderer.domElement);
|
|
|
|
group = new THREE.Group();
|
|
scene.add(group);
|
|
|
|
const ambient = new THREE.AmbientLight(0xffffff, 0.35);
|
|
scene.add(ambient);
|
|
const point = new THREE.PointLight(0xfff3e0, 3.2, 100);
|
|
point.position.set(4, treeHeight + 2, 6);
|
|
scene.add(point);
|
|
({ geo: geoLeaves, pos: posLeaves, col: colLeaves, vel: velLeaves, mat: matLeaves, points: pointsLeaves, targets: tgtLeaves } = initSet(
|
|
particleCountLeaves,
|
|
0.12,
|
|
() => {
|
|
const g = 0.35 + Math.random() * 0.65;
|
|
const r = 0.2 + 0.8 * Math.random();
|
|
const green = 0.5 + 0.5 * Math.random();
|
|
return new THREE.Color(r * g, green * g, 0.2 * g);
|
|
},
|
|
computeLeafTargets
|
|
));
|
|
|
|
({ geo: geoOrnaments, pos: posOrnaments, col: colOrnaments, vel: velOrnaments, mat: matOrnaments, points: pointsOrnaments, targets: tgtOrnaments } = initSet(
|
|
particleCountOrnaments,
|
|
0.14,
|
|
(i) => ornamentColors(i),
|
|
computeOrnamentTargets
|
|
));
|
|
|
|
({ geo: geoTrunk, pos: posTrunk, col: colTrunk, vel: velTrunk, mat: matTrunk, points: pointsTrunk, targets: tgtTrunk } = initSet(
|
|
particleCountTrunk,
|
|
0.12,
|
|
() => new THREE.Color(0.36, 0.24, 0.18),
|
|
computeTrunkTargets
|
|
));
|
|
({ geo: geoStar, pos: posStar, col: colStar, vel: velStar, mat: matStar, points: pointsStar, targets: tgtStar } = initSet(
|
|
particleCountStar,
|
|
0.12,
|
|
() => new THREE.Color(1.0, 0.83, 0.3),
|
|
computeStarTargets
|
|
));
|
|
({ geo: geoAccents, pos: posAccents, col: colAccents, vel: velAccents, mat: matAccents, points: pointsAccents, targets: tgtAccents } = initSet(
|
|
particleCountAccents,
|
|
0.22,
|
|
(i) => ornamentColors(i + 3),
|
|
computeAccentsTargets
|
|
));
|
|
|
|
const snowTex = createSnowflakeTexture();
|
|
const glowTex = createSnowGlowTexture();
|
|
snowGroup = new THREE.Group();
|
|
snowGroup.renderOrder = 0;
|
|
scene.add(snowGroup);
|
|
snowSprites = new Array(snowCount);
|
|
const snowHalos = new Array(snowCount);
|
|
snowVel = new Float32Array(snowCount * 2);
|
|
snowRot = new Float32Array(snowCount);
|
|
snowScale = new Float32Array(snowCount);
|
|
snowOpacityPhase = new Float32Array(snowCount);
|
|
for (let i = 0; i < snowCount; i++) {
|
|
const mat = new THREE.SpriteMaterial({ map: snowTex, color: 0xffffff, transparent: true, opacity: 1.0, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
|
|
const spr = new THREE.Sprite(mat);
|
|
const s = (1.6 + Math.random() * 1.6) * (2/3);
|
|
spr.scale.set(s, s, 1);
|
|
const x = (Math.random() - 0.5) * 120;
|
|
const y = 20 + Math.random() * 40;
|
|
const z = -22 - Math.random() * 12;
|
|
spr.position.set(x, y, z);
|
|
snowScale[i] = s;
|
|
snowVel[i * 2] = (Math.random() - 0.5) * 0.006;
|
|
snowVel[i * 2 + 1] = -0.008 - Math.random() * 0.01;
|
|
snowRot[i] = (-0.004 + Math.random() * 0.008);
|
|
snowOpacityPhase[i] = Math.random() * Math.PI * 2;
|
|
snowSprites[i] = spr;
|
|
snowGroup.add(spr);
|
|
const haloMat = new THREE.SpriteMaterial({ map: glowTex, color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
|
|
const halo = new THREE.Sprite(haloMat);
|
|
halo.scale.set(s * 2.6, s * 2.6, 1);
|
|
halo.position.copy(spr.position);
|
|
snowHalos[i] = halo;
|
|
snowGroup.add(halo);
|
|
}
|
|
pointsLeaves.renderOrder = 1;
|
|
pointsOrnaments.renderOrder = 1;
|
|
pointsTrunk.renderOrder = 1;
|
|
pointsStar.renderOrder = 1;
|
|
pointsAccents.renderOrder = 1;
|
|
fitCamera();
|
|
startTime = performance.now();
|
|
animate();
|
|
window.addEventListener('resize', onResize);
|
|
}
|
|
|
|
function animate() {
|
|
if (!running) return;
|
|
const now = performance.now();
|
|
const elapsed = now - startTime;
|
|
function updateSet(geo, posArr, velArr, startMorphArr, targetsArr, count) {
|
|
const attr = geo.getAttribute('position');
|
|
const arr = attr.array;
|
|
if (elapsed < disperseDuration) {
|
|
const dt = 16;
|
|
for (let i = 0; i < count; i++) {
|
|
const ix = i * 3;
|
|
arr[ix] += velArr[ix] * dt;
|
|
arr[ix + 1] += velArr[ix + 1] * dt;
|
|
arr[ix + 2] += velArr[ix + 2] * dt;
|
|
}
|
|
} else {
|
|
if (!startMorphArr) {
|
|
startMorphArr = Float32Array.from(arr);
|
|
if (geo === geoLeaves) startMorphLeaves = startMorphArr;
|
|
if (geo === geoOrnaments) startMorphOrnaments = startMorphArr;
|
|
if (geo === geoTrunk) startMorphTrunk = startMorphArr;
|
|
if (geo === geoStar) startMorphStar = startMorphArr;
|
|
if (geo === geoAccents) startMorphAccents = startMorphArr;
|
|
}
|
|
const t = Math.min(1, (elapsed - disperseDuration) / morphDuration);
|
|
const e = easeInOutCubic(t);
|
|
for (let i = 0; i < count; i++) {
|
|
const ix = i * 3;
|
|
arr[ix] = startMorphArr[ix] + (targetsArr[ix] - startMorphArr[ix]) * e;
|
|
arr[ix + 1] = startMorphArr[ix + 1] + (targetsArr[ix + 1] - startMorphArr[ix + 1]) * e;
|
|
arr[ix + 2] = startMorphArr[ix + 2] + (targetsArr[ix + 2] - startMorphArr[ix + 2]) * e;
|
|
}
|
|
}
|
|
attr.needsUpdate = true;
|
|
}
|
|
|
|
const dt = 16;
|
|
for (let i = 0; i < snowCount; i++) {
|
|
const spr = snowSprites[i];
|
|
spr.position.x += snowVel[i * 2] * dt;
|
|
spr.position.y += snowVel[i * 2 + 1] * dt;
|
|
spr.material.rotation += snowRot[i];
|
|
const f = Math.min(1.0, 0.9 + 0.45 * Math.abs(Math.sin(now * 0.0015 + snowOpacityPhase[i])));
|
|
spr.material.opacity = 0.67 * f;
|
|
const halo = snowGroup.children[(i * 2) + 1];
|
|
if (halo) {
|
|
halo.position.copy(spr.position);
|
|
halo.material.opacity = Math.min(1.0, 0.47 * f);
|
|
}
|
|
if (spr.position.y < -30) {
|
|
spr.position.set((Math.random() - 0.5) * 120, 20 + Math.random() * 40, -22 - Math.random() * 12);
|
|
spr.material.rotation = Math.random() * Math.PI * 2;
|
|
if (halo) {
|
|
halo.position.copy(spr.position);
|
|
halo.material.rotation = spr.material.rotation;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateSet(geoLeaves, posLeaves, velLeaves, startMorphLeaves, tgtLeaves, particleCountLeaves);
|
|
updateSet(geoOrnaments, posOrnaments, velOrnaments, startMorphOrnaments, tgtOrnaments, particleCountOrnaments);
|
|
updateSet(geoTrunk, posTrunk, velTrunk, startMorphTrunk, tgtTrunk, particleCountTrunk);
|
|
updateSet(geoStar, posStar, velStar, startMorphStar, tgtStar, particleCountStar);
|
|
updateSet(geoAccents, posAccents, velAccents, startMorphAccents, tgtAccents, particleCountAccents);
|
|
group.rotation.y += 0.0032;
|
|
renderer.render(scene, camera);
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
function onResize() {
|
|
const w = window.innerWidth;
|
|
const h = window.innerHeight;
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(w, h);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
fitCamera();
|
|
}
|
|
|
|
function fitCamera() {
|
|
const aspect = camera.aspect;
|
|
const vfov = THREE.MathUtils.degToRad(camera.fov);
|
|
const halfH = (topY - bottomY) * 0.5 * 1.06;
|
|
const radNeeded = (baseRadius + 1.6) * 1.06;
|
|
const zH = halfH / Math.tan(vfov / 2);
|
|
const zW = radNeeded / (Math.tan(vfov / 2) * aspect);
|
|
const z = Math.max(zH, zW);
|
|
camera.position.z = Math.max(z, 20);
|
|
const centerY = (topY + bottomY) * 0.5;
|
|
camera.position.y = centerY;
|
|
camera.lookAt(0, centerY, 0);
|
|
}
|
|
|
|
onMounted(() => {
|
|
const el = document.createElement('div');
|
|
el.id = 'canvas-container';
|
|
document.getElementById('app').appendChild(el);
|
|
containerEl.value = el;
|
|
initThree();
|
|
setInterval(spawnBarrage, 1200);
|
|
document.addEventListener('fullscreenchange', () => { isFs.value = !!document.fullscreenElement; });
|
|
});
|
|
|
|
return () => h('div', { style: { width: '100%', height: '100%' } }, [
|
|
h('div', { class: 'barrage' }, barrageItems.value.map(item => h('div', {
|
|
key: item.id,
|
|
class: 'barrage-item',
|
|
style: {
|
|
top: item.top,
|
|
color: item.color,
|
|
animationDuration: item.duration,
|
|
}
|
|
}, item.text))),
|
|
h('button', { class: 'fullscreen-btn', onClick: toggleFs }, isFs.value ? '退出全屏' : '全屏'),
|
|
]);
|
|
}
|
|
};
|
|
|
|
createApp(App).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html> |