feat: add three js

This commit is contained in:
2026-02-27 00:58:59 +08:00
parent af270b3d6a
commit f971782480
12 changed files with 336 additions and 32 deletions

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import AnimText from "@/components/special/AnimText.vue";
import { navigateTo } from "@/utils/navigator";
</script>
<template>
<div class="hero flex-1">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">
<AnimText text="home.welcome" />
</h1>
<div class="py-6">
<AnimText text="home.intro_line1" /><br />
<AnimText text="home.intro_line2" /><br />
<AnimText text="home.intro_line3" />
</div>
<button
class="btn btn-primary"
@click="navigateTo('http://localhost:5174')"
>
<AnimText text="home.read_doc" />
</button>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -7,7 +7,7 @@ import RedBookIcon from "../icon/RedBookIcon.vue";
<template>
<footer
class="fixed bottom-0 left-0 w-full z-1 pt-16 pb-10 px-6 md:px-12 lg:px-20 text-sm font-[LXGW] lg:h-102 md:h-122 h-154"
class="w-full pt-16 pb-10 px-6 md:px-12 lg:px-20 text-sm font-[LXGW] lg:h-102 md:h-122 h-154"
>
<!-- Background overlay -->
<div

View File

@@ -0,0 +1,130 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { gsap } from "gsap";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { M } from "motion-v";
const canvasRef = ref<HTMLCanvasElement | null>(null);
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let cube: THREE.Mesh;
// 动画循环的 raf id用于销毁时清理
let animationId: number;
function init() {
if (!canvasRef.value) return; // 防御性编程
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
camera.position.z = 5;
renderer = new THREE.WebGLRenderer({
canvas: canvasRef.value,
alpha: true,
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 透明
renderer.setClearColor(0x000000, 0);
// ------------------- 几何体 -------------------
const loader = new GLTFLoader();
loader.load(
"/model/christmas_ball.glb",
(gltf) => {
cube = gltf.scene;
// 可选:调整模型大小、位置
cube.scale.set(0.01, 0.01, 0.01);
cube.position.set(4, 0, 0);
scene.add(cube);
console.log("模型加载成功!", cube);
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% 已加载");
},
(error) => {
console.error("模型加载失败:", error);
},
);
// 加一点环境光 + 点光(让边缘更亮)
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
animate();
}
function animate() {
animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
}
onMounted(() => {
init();
// 监听滚动事件
window.addEventListener("scroll", () => {
// 计算滚动进度
const scrollY = window.scrollY;
const maxScroll =
document.documentElement.scrollHeight - window.innerHeight;
const progress = scrollY / maxScroll;
cube.position.set(4 + progress, 0, 0);
cube.rotation.y = -progress * Math.PI * 1;
cube.scale.set(
0.01 + progress * 0.001,
0.01 + progress * 0.001,
0.01 + progress * 0.001,
);
// 接近页的脚时候淡出
// if (progress > 0.8) {
// gsap.to(cube.material, {
// opacity: 0,
// duration: 0.5,
// });
// }
// if (progress < 0.8) {
// gsap.to(cube.material, {
// opacity: 1,
// duration: 0.5,
// });
// }
});
// 监听窗口大小变化
window.addEventListener("resize", () => {
if (!camera || !renderer) return;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
});
onUnmounted(() => {
cancelAnimationFrame(animationId);
window.removeEventListener("resize", onResize); // 如果加了
renderer?.dispose(); // 释放资源(推荐)
});
</script>
<template>
<canvas ref="canvasRef" class="pointer-events-none" />
</template>

View File

@@ -0,0 +1,120 @@
<template>
<div ref="container" class="three-container" />
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const container = ref(null);
let renderer = null;
let scene = null;
let camera = null;
let controls = null;
let animationFrameId = null;
onMounted(() => {
if (!container.value) return;
// ================== 初始化 ==================
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1f);
camera = new THREE.PerspectiveCamera(
60,
container.value.clientWidth / container.value.clientHeight,
0.1,
1000,
);
camera.position.set(0, 1.5, 5);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.value.appendChild(renderer.domElement);
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enablePan = true;
// 灯光(很重要,否则模型可能很暗)
scene.add(new THREE.AmbientLight(0xffffff, 1.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
// ================== 加载模型 ==================
const loader = new GLTFLoader();
const modelUrl = "/model/christmas_ball.glb"; // ← 修改成你自己的模型路径
loader.load(
modelUrl,
(gltf) => {
const model = gltf.scene;
// 可选调整
model.scale.set(0.01, 0.01, 0.01); // 放大倍数,根据模型大小调整
model.position.y = -0.5; // 稍微下移居中
// model.rotation.y = Math.PI / 4 // 初始旋转(可选)
scene.add(model);
console.log("模型加载成功", model);
},
(xhr) => {
console.log(`加载进度: ${((xhr.loaded / xhr.total) * 100).toFixed(1)}%`);
},
(error) => {
console.error("模型加载失败:", error);
},
);
// ================== 动画循环 ==================
function animate() {
animationFrameId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
});
onUnmounted(() => {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
if (controls) controls.dispose();
if (renderer) {
renderer.dispose();
renderer.forceContextLoss();
}
scene = null;
camera = null;
renderer = null;
controls = null;
});
// 窗口 resize 处理
const onResize = () => {
if (!container.value || !camera || !renderer) return;
const width = container.value.clientWidth;
const height = container.value.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
window.addEventListener("resize", onResize);
onUnmounted(() => window.removeEventListener("resize", onResize));
</script>
<style scoped>
.three-container {
width: 100%;
height: 100vh; /* 或你想要的高度,例如 600px */
overflow: hidden;
background: #000;
}
</style>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import NavBar from "@/components/menu/NavBar.vue";
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
import ModelViewer from "@/components/three/ModelViewer.vue";
</script>
<template>
@@ -11,9 +12,10 @@ import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
<ModelViewer />
</div>
</div>
<FooterBarV2 />
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
</div>
</template>

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import BasicIntroCard from "@/components/card/BasicIntroCard.vue";
import DatePickerDisplayCard from "@/components/card/DatePickerDisplayCard.vue";
import DevelopProgressCard from "@/components/card/DevelopProgressCard.vue";
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
import NavBar from "@/components/menu/NavBar.vue";
import AnimText from "@/components/special/AnimText.vue";
import { navigateTo } from "@/utils/navigator";
import LogoModel from "@/components/three/LogoModel.vue";
const progress = ref([
{
@@ -40,38 +40,20 @@ const progress = ref([
<template>
<div class="lg:pb-102 md:pb-122 pb-154">
<!-- 这里开 relative 形成 stack context -->
<div class="bg-base-200 relative z-2">
<!-- 主内容区域 footer 上方 -->
<div class="bg-base-200 relative z-5">
<!-- 顶部与首页内容 -->
<div class="h-screen flex flex-col">
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
<div class="hero flex-1">
<div class="hero-content text-center">
<div class="max-w-md">
<!-- <h1 class="text-5xl font-bold">{{ $t("home.welcome") }}</h1> -->
<h1 class="text-5xl font-bold">
<AnimText text="home.welcome" />
</h1>
<div class="py-6">
<!-- {{ $t("home.intro_line1") }}<br />
{{ $t("home.intro_line2") }}<br />
{{ $t("home.intro_line3") }} -->
<AnimText text="home.intro_line1" /><br />
<AnimText text="home.intro_line2" /><br />
<AnimText text="home.intro_line3" />
</div>
<button
class="btn btn-primary"
@click="navigateTo('http://localhost:5174')"
>
<!-- {{ $t("home.read_doc") }} -->
<AnimText text="home.read_doc" />
</button>
</div>
</div>
</div>
<BasicIntroCard class="isolate z-8" />
</div>
<!-- fixed three js 背景 -->
<LogoModel class="fixed left-0 top-0 h-screen w-full z-7" />
<!-- 脚手架开发进度 -->
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">脚手架开发进度</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
@@ -85,6 +67,8 @@ const progress = ref([
/>
</div>
</div>
<!-- 组件演示 -->
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">组件演示</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
@@ -92,7 +76,9 @@ const progress = ref([
</div>
</div>
</div>
<FooterBarV2 />
<!-- fixed footer -->
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
</div>
</template>