<template>
<div class="login-container">
<div id="login-three-container"></div>
<div class="login-plane">
<div class="login-plane-container">
<img class="login-plane-human" src="@/assets/images/login_human.png" alt="" />
<div class="login-plane-title">
xx传媒 -- 登陆
<img
class="login-plane-title-line"
src="@/assets/images/login_horizontal_line.png"
alt=""
/>
</div>
<div class="login-plane-form">
<el-form :model="formField" :rules="formRules" ref="formRef">
<el-form-item prop="user">
<el-input placeholder="用户名 / 账号" type="text" v-model="formField.user"></el-input>
</el-form-item>
<el-form-item prop="pass">
<el-input placeholder="密码" type="password" v-model="formField.pass"></el-input>
</el-form-item>
<div class="login-code-container">
<el-form-item style="width: 50%" prop="code">
<el-input placeholder="验证码" type="text" v-model="formField.code"></el-input>
</el-form-item>
<div @click="getValidateCodeHandle" class="login-code">
<img :src="codeSrc" />
</div>
</div>
<el-form-item prop="autoLogin">
<el-checkbox v-model="formField.whetherAutoLogin" label="自动登陆"></el-checkbox>
</el-form-item>
</el-form>
<el-button @click="submitForm" style="width: 100%" type="primary">登录</el-button>
</div>
</div>
</div>
<div class="login-ground"></div>
</div>
</template>
<script lang="ts">
import * as THREE from 'three'
import { GUI } from 'three/examples/jsm/libs/dat.gui.module'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import Stats from 'three/examples/jsm/libs/stats.module.js'
import { defineComponent, onMounted, ref, reactive, toRefs, unref } from 'vue'
import _ from 'lodash'
import { ElMessage } from 'element-plus'
export default defineComponent({
setup() {
// 容器
let container: any
// 声明视口宽度
let width: number
// 声明视口高度
let height: number
// 盒模型的深度
const depth = 1400
// 声明场景
let scene: any
// 声明球组
let Sphere_Group: any
// 声明球体几何
let sphereGeometry: any
// 声明完整球
let sphere: any
// 声明相机
let camera: any
// 声明相机在z轴的位置
let zAxisNumber: number
// 声明相机目标点
let cameraTarget: any = new THREE.Vector3(0, 0, 0)
// 声明点材质
let materials: any = []
// 声明点的参数
let parameters: any
// 声明点在z轴上移动的进度
let zprogress: number
// 声明同上(第二个几何点)
let zprogress_second: number
// 声明粒子1
let particles_first: any[]
// 声明粒子1
let particles_second: any[]
// 声明粒子1的初始化位置
let particles_init_position: number
// 声明流动的云对象1(包含路径、云实例)
let cloudParameter_first: any
// 声明流动的云对象2(包含路径、云实例)
let cloudParameter_second: any
// 声明云流动的渲染函数1
let renderCloudMove_first: any
// 声明云流动的渲染函数1
let renderCloudMove_second: any
// 声明性能监控
let stats: any = new Stats()
// 声明渲染器
let renderer: any
// 声明调试工具
let gui = new GUI()
// 表单对象
const formRef = ref(null)
// 其他状态
const state = reactive({
codeSrc: '',
codetoken: ''
})
// 响应式对象 - 表单对象
const formField = reactive({
user: '',
pass: '',
code: ''
})
// 表单校验规则
const formRules = {
user: [{ required: true, message: '请输入用户名账号', trigger: 'blur' }],
pass: [{ required: true, message: '请输入密码', trigger: 'blur' }],
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
}
onMounted(() => {
container = document.getElementById('login-three-container')
width = container.clientWidth
height = container.clientHeight
initScene()
initSceneBg()
initCamera()
initLight()
initSphereModal()
initSphereGroup()
particles_init_position = -zAxisNumber - depth / 2
zprogress = particles_init_position
zprogress_second = particles_init_position * 2
particles_first = initSceneStar(particles_init_position)
particles_second = initSceneStar(zprogress_second)
cloudParameter_first = initTubeRoute(
[
new THREE.Vector3(-width / 10, 0, -depth / 2),
new THREE.Vector3(-width / 4, height / 8, 0),
new THREE.Vector3(-width / 4, 0, zAxisNumber)
],
400,
200
)
cloudParameter_second = initTubeRoute(
[
new THREE.Vector3(width / 8, height / 8, -depth / 2),
new THREE.Vector3(width / 8, height / 8, zAxisNumber)
],
200,
100
)
initRenderer()
// 控制器必须放在renderer函数后面
initOrbitControls()
animate()
// initGUI()
// const axesHelper = new THREE.AxesHelper(2000)
// scene.add(axesHelper)
})
// gui参数
function Params() {
this.color = '#000'
this.length = 10
this.size = 3
this.visible = true
this.x = 0
this.y = 0
this.z = 100
this.widthSegments = 64
this.heightSegments = 32
this.radius = 16
}
// 初始化gui
const initGUI = () => {
const params = new Params()
gui.add(params, 'x', -1500, 1500).onChange((x: number) => {
//点击颜色面板,e为返回的10进制颜色
Sphere_Group.position.x = x
})
gui.add(params, 'y', -50, 1500).onChange((y: number) => {
//点击颜色面板,e为返回的10进制颜色
Sphere_Group.position.y = y
})
gui.add(params, 'z', -200, 1000).onChange((z: number) => {
//点击颜色面板,e为返回的10进制颜色
Sphere_Group.position.z = z
})
gui.add(params, 'widthSegments', 0, 64).onChange((widthSegments: number) => {
//点击颜色面板,e为返回的10进制颜色
sphereGeometry.parameters.widthSegments = widthSegments
})
gui.add(params, 'heightSegments', 0, 32).onChange((heightSegments: number) => {
//点击颜色面板,e为返回的10进制颜色
sphereGeometry.parameters.heightSegments = heightSegments
})
gui.add(params, 'radius', 5, 30).onChange((radius: number) => {
//点击颜色面板,e为返回的10进制颜色
sphereGeometry.parameters.radius = radius
renderer.render(scene, camera)
})
gui.add(params, 'visible').onChange((e) => {
//这是一个单选框,因为params.visible是一个布尔值,e返回所选布尔值
// points.visible = e
})
gui.addColor(params, 'color').onChange((e) => {
//点击颜色面板,e为返回的10进制颜色
// pointsMaterial.color.set(e)
})
}
// 初始化场景
const initScene = () => {
scene = new THREE.Scene()
// 在场景中添加雾的效果,Fog参数分别代表‘雾的颜色’、‘开始雾化的视线距离’、刚好雾化至看不见的视线距离’
scene.fog = new THREE.Fog(0x000000, 0, 10000)
}
// 初始化背景(盒模型背景,视角在盒子里面,看到的是盒子内部)
const initSceneBg = () => {
new THREE.TextureLoader().load(require('@/assets/images/sky.png'), (texture) => {
const geometry = new THREE.BoxGeometry(width, height, depth) // 创建一个球形几何体 SphereGeometry
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide }) // 创建基础为网格基础材料
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
})
}
// 初始化轨道控制器
const initOrbitControls = () => {
const controls = new OrbitControls(camera, renderer.domElement)
// enabled设置为true是可以使用鼠标控制视角
controls.enabled = false
controls.update()
}
// 初始化相机
const initCamera = () => {
/**
* 方式1:固定视野的距离,求满足完整的视野画面需要多大的视域角度
* tan正切值(直角边除以临边)
* const mathTan_value = width / 2 / depth
* 视域角度
* const fov_angle = (Math.atan(mathTan_value) * 180) / Math.PI
* 创建透视相机
* new THREE.PerspectiveCamera(fov_angle, width / height, 1, depth)
* 场景是一个矩形容器(坐标(0, 0, 0)是矩形容器的中心),相机能看到的距离是depth,
* camera.position.set(0, 0, depth / 2)
*/
/**
* 使用透视相机
* 参数值分别是:
* fov(field of view) — 摄像机视锥体垂直视野角度
* aspect — 摄像机视锥体长宽比
* near — 摄像机视锥体近端面
* far — 摄像机视锥体远端面
* 这里需要注意:透视相机是鱼眼效果,如果视域越大,边缘变形越大。
* 为了避免边缘变形,可以将fov角度设置小一些,距离拉远一些
*/
/**
* 方式2:固定视域角度,求需要多少距离才能满足完整的视野画面
* 15度等于(Math.PI / 12)
*/
const fov = 15
const distance = width / 2 / Math.tan(Math.PI / 12)
zAxisNumber = Math.floor(distance - depth / 2)
camera = new THREE.PerspectiveCamera(fov, width / height, 1, 30000)
/**
* 这里给z轴的距离加了100,原因是做调整,使得视域更完整
* 这么做并不代表前面计算错误了,根据前面的计算值并不能很完整的看到
* 至于原因,我想大概就类似于0.1+0.2不等于0.3吧
* 所以我自作主张地加了100的值做调整(但是不建议,因为当屏幕足够宽时候会看到边缘)
*/
// camera.position.set(0, 0, zAxisNumber + 100)
camera.position.set(0, 0, zAxisNumber)
camera.lookAt(cameraTarget)
// const helper = new THREE.CameraHelper(camera)
// helper.update()
// scene.add(helper)
}
//光源
const initLight = () => {
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
// 右下角点光源
const light_rightBottom = new THREE.PointLight(0x0655fd, 5, 0)
light_rightBottom.position.set(0, 100, -200)
scene.add(light_rightBottom)
scene.add(ambientLight)
}
// 初始化球体模型
const initSphereModal = () => {
//材质
let material = new THREE.MeshPhongMaterial()
material.map = new THREE.TextureLoader().load(require('@/assets/images/earth_bg.png'))
material.blendDstAlpha = 1
//几何体
sphereGeometry = new THREE.SphereGeometry(50, 64, 32)
//模型
sphere = new THREE.Mesh(sphereGeometry, material)
}
// 初始化组 --- 球体
const initSphereGroup = () => {
Sphere_Group = new THREE.Group()
Sphere_Group.add(sphere)
Sphere_Group.position.x = -400
Sphere_Group.position.y = 200
Sphere_Group.position.z = -200
scene.add(Sphere_Group)
}
// 初始化流动路径
const initTubeRoute = (route?: any, geometryWidth?: number, geometryHeigh?: number) => {
const curve = new THREE.CatmullRomCurve3(route, false)
const tubeGeometry = new THREE.TubeGeometry(curve, 100, 2, 50, false)
const tubeMaterial = new THREE.MeshBasicMaterial({
// color: '0x4488ff',
opacity: 0,
transparent: true
})
const tube = new THREE.Mesh(tubeGeometry, tubeMaterial)
scene.add(tube)
const clondGeometry = new THREE.PlaneGeometry(geometryWidth, geometryHeigh)
const textureLoader = new THREE.TextureLoader()
const cloudTexture = textureLoader.load(require('@/assets/images/cloud.png'))
const clondMaterial = new THREE.MeshBasicMaterial({
map: cloudTexture,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true
})
const cloud = new THREE.Mesh(clondGeometry, clondMaterial)
scene.add(cloud)
return {
cloud,
curve
}
}
// 初始化场景星星效果
const initSceneStar = (initZposition: number): any => {
const geometry = new THREE.BufferGeometry()
const vertices: number[] = []
const pointsGeometry: any[] = []
const textureLoader = new THREE.TextureLoader()
const sprite1 = textureLoader.load(require('@/assets/images/starflake1.png'))
const sprite2 = textureLoader.load(require('@/assets/images/starflake2.png'))
parameters = [
[[0.6, 100, 0.75], sprite1, 50],
[[0, 0, 1], sprite2, 20]
]
// 初始化500个节点
for (let i = 0; i < 500; i++) {
/**
* const x: number = Math.random() * 2 * width - width
* 等价
* THREE.MathUtils.randFloatSpread(width)
*/
const x: number = THREE.MathUtils.randFloatSpread(width)
const y: number = _.random(0, height / 2)
const z: number = _.random(-depth / 2, zAxisNumber)
vertices.push(x, y, z)
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
// 创建2种不同的材质的节点(500 * 2)
for (let i = 0; i < parameters.length; i++) {
const color = parameters[i][0]
const sprite = parameters[i][1]
const size = parameters[i][2]
materials[i] = new THREE.PointsMaterial({
size,
map: sprite,
blending: THREE.AdditiveBlending,
depthTest: true,
transparent: true
})
materials[i].color.setHSL(color[0], color[1], color[2])
const particles = new THREE.Points(geometry, materials[i])
particles.rotation.x = Math.random() * 0.2 - 0.15
particles.rotation.z = Math.random() * 0.2 - 0.15
particles.rotation.y = Math.random() * 0.2 - 0.15
particles.position.setZ(initZposition)
pointsGeometry.push(particles)
scene.add(particles)
}
return pointsGeometry
}
// 渲染星球的自转
const renderSphereRotate = () => {
if (sphere) {
Sphere_Group.rotateY(0.001)
}
}
// 渲染星星的运动
const renderStarMove = () => {
const time = Date.now() * 0.00005
zprogress += 1
zprogress_second += 1
if (zprogress >= zAxisNumber + depth / 2) {
zprogress = particles_init_position
} else {
particles_first.forEach((item) => {
item.position.setZ(zprogress)
})
}
if (zprogress_second >= zAxisNumber + depth / 2) {
zprogress_second = particles_init_position
} else {
particles_second.forEach((item) => {
item.position.setZ(zprogress_second)
})
}
for (let i = 0; i < materials.length; i++) {
const color = parameters[i][0]
const h = ((360 * (color[0] + time)) % 360) / 360
materials[i].color.setHSL(color[0], color[1], parseFloat(h.toFixed(2)))
}
}
// 初始化云的运动函数
const initCloudMove = (
cloudParameter: any,
speed: number,
scaleSpeed = 0.0006,
maxScale = 1,
startScale = 0
) => {
let cloudProgress = 0
return () => {
if (startScale < maxScale) {
startScale += scaleSpeed
cloudParameter.cloud.scale.setScalar(startScale)
}
if (cloudProgress > 1) {
cloudProgress = 0
startScale = 0
} else {
cloudProgress += speed
if (cloudParameter.curve) {
const point = cloudParameter.curve.getPoint(cloudProgress)
if (point && point.x) {
cloudParameter.cloud.position.set(point.x, point.y, point.z)
}
}
}
}
}
//渲染器
const initRenderer = () => {
// 开启抗锯齿
// 在 css 中设置背景色透明显示渐变色
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
// 定义渲染器的尺寸;在这里它会填满整个屏幕
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
container.appendChild(renderer.domElement)
container.appendChild(stats.dom)
renderCloudMove_first = initCloudMove(cloudParameter_first, 0.0002)
renderCloudMove_second = initCloudMove(cloudParameter_second, 0.0008, 0.001)
}
//动画刷新
const animate = () => {
requestAnimationFrame(animate)
renderSphereRotate()
renderStarMove()
renderCloudMove_first()
renderCloudMove_second()
renderer.render(scene, camera)
}
// 获取验证码
const getValidateCodeHandle = async () => {
// 请求获取验证码 并设置验证码的图片以及验证码token
state.codeSrc = ''
state.codetoken = ''
}
// 提交表单
const submitForm = () => {
const form: any = unref(formRef)
if (!form) return
form.validate((valid: any) => {
if (valid) {
submitHandle()
} else {
ElMessage.warning({
message: '随便输入用户名、密码、验证码即可登陆',
type: 'warning'
})
}
})
}
// 提交请求
const submitHandle = async () => {
const params = {
password: formField.pass,
username: formField.user,
verifyCode: formField.code
}
// 提交登陆请求
}
const refsState = toRefs(state)
return {
...refsState,
formRules,
formField,
submitForm,
formRef,
getValidateCodeHandle
}
}
})
</script>
<style lang="scss" scoped>
.login-container {
width: 100%;
height: 100vh;
position: relative;
#login-three-container {
width: 100%;
height: 100%;
}
.login-plane {
position: absolute;
z-index: 9999;
width: 600px;
height: 500px;
background-image: url('~@/assets/images/login_border.png');
background-repeat: no-repeat;
background-size: 100% 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.login-plane-container {
width: 100%;
height: 100%;
border-radius: 18px;
background-color: #007eff2e;
position: relative;
@keyframes humanMove {
0% {
top: -100px;
}
25% {
top: -120px;
}
50% {
top: -100px;
}
75% {
top: -80px;
}
100% {
background: -100px;
}
}
.login-plane-human {
position: absolute;
width: 260px;
right: -120px;
top: -100px;
animation: humanMove 8s linear 0s infinite normal;
}
.login-plane-title {
width: 100%;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 35px;
color: #fff;
font-weight: 700;
img {
width: 50%;
}
.login-plane-title-line {
width: 80%;
position: absolute;
bottom: 0;
}
}
.login-plane-form {
padding: 45px 55px;
box-sizing: border-box;
.login-code-container {
display: flex;
align-items: flex-start;
justify-content: space-between;
.login-code {
cursor: pointer;
width: 45%;
height: 40px;
background-color: #c8c8c8;
img {
width: 100%;
height: 100%;
}
}
}
}
}
}
.login-ground {
position: absolute;
z-index: 9998;
width: 100%;
height: 400px;
background-image: url('~@/assets/images/ground.png');
background-repeat: no-repeat;
background-size: 100% 100%;
bottom: 0;
left: 0;
}
}
</style>