钉钉官网动画
展示
请在这里滚动鼠标滚轮
天
生
我
材
必
有
用
千
金
散
尽
还
复
来
JinKe Blog
由于代码太长了我这边给代码分开了
模版加 TS 代码 点击展开
vue
<template>
<div class="container" ref="containerScroll">
<div class="header" ref="header">请在这里滚动鼠标滚轮</div>
<div class="content" ref="content">
<div class="animation-container">
<div class="waves" ref="waves"></div>
<div class="list" ref="list">
<div
:data-order="orderList[index]"
v-for="(item, index) in 14"
:key="index"
:ref="setItemsRef"
class="list-item"
>
{{ textList[index] }}
</div>
</div>
<!-- 散点 -->
<div class="scatter-point" ref="scatterPoint">
<div
class="point"
v-for="(_, index) in 18"
:key="index"
:ref="setPointsRef"
:style="getPointStyle(index)"
></div>
</div>
<div class="logo" ref="logo">
<img src="/logo.png" alt="" />
<p class="logo-text">JinKe Blog</p>
</div>
</div>
</div>
<div class="footer">滚完了</div>
</div>
</template>
<script lang="ts" setup>
import * as THREE from "three";
import { ref, onMounted } from "vue";
// 粒子位置数据
import { pointStyleList, optionMinMax, textList, orderList } from "./config.ts";
// 水波效果
const waves = ref<HTMLElement>();
// 间距
const separation = 150;
// 横向数量
const amountX = 50;
// 纵向数量
const amountY = 50;
// three容器
let container: HTMLElement;
let camera: any;
let scene: any;
let wavesRenderer: any;
let particles: any;
let count = 0;
let mouseX = 0;
let mouseY = -280;
let wavesWidth = 0;
let wavesHeight = 0;
/**
* 初始化波浪动画。
* - 创建一个容器并将其附加到波浪元素。
* - 设置相机和场景。
* - 创建粒子并将其添加到场景中。
* - 初始化渲染器并将其附加到容器。
*/
const wavesInit = () => {
if (!waves.value) return;
// 创建一个用于动画的容器
container = document.createElement("div");
waves.value.appendChild(container);
// 设置相机
camera = new THREE.PerspectiveCamera(80, wavesWidth / wavesHeight, 1, 6000);
camera.position.z = 1200;
// 创建场景
scene = new THREE.Scene();
// 设置粒子位置和比例
const numParticles = amountX * amountY;
const positions = new Float32Array(numParticles * 3);
const scales = new Float32Array(numParticles);
let i = 0;
let j = 0;
for (let ix = 0; ix < amountX; ix++) {
for (let iy = 0; iy < amountY; iy++) {
positions[i] = ix * separation - (amountX * separation) / 2; // x
positions[i + 1] = 0; // y
positions[i + 2] = iy * separation - (amountY * separation) / 2; // z
scales[j] = 1;
i += 3;
j++;
}
}
// 创建粒子几何图形。
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("scale", new THREE.BufferAttribute(scales, 1));
// 设置材质。
const material: any = new THREE.ShaderMaterial({
uniforms: {
// 设置粒子颜色。
color: { value: new THREE.Color("rgb(109,215,208)") },
},
// 设置粒子大小和位置。
vertexShader:
"attribute float scale; void main() {vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_PointSize = scale * ( 150.0 / - mvPosition.z );gl_Position = projectionMatrix * mvPosition;}",
// 设置粒子颜色。
fragmentShader:
"uniform vec3 color;void main() {if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;gl_FragColor = vec4( color, 1.0 );}",
});
// 创建粒子并将其添加到场景中。
particles = new THREE.Points(geometry, material);
scene.add(particles);
// 设置渲染器并将其附加到容器。
wavesRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
wavesRenderer.setPixelRatio(window.devicePixelRatio);
wavesRenderer.setSize(wavesWidth, wavesHeight);
container.appendChild(wavesRenderer.domElement);
// 在容器上禁用触摸操作。
container.style.touchAction = "none";
// container.addEventListener("pointermove", onPointerMove);
};
function onPointerMove(event: PointerEvent) {
if (event.isPrimary === false) return;
mouseX = event.clientX - window.innerWidth / 2;
mouseY = event.clientY - window.innerHeight / 2;
}
function wavesAnimate() {
requestAnimationFrame(wavesAnimate);
wavesRender();
}
/**
* 渲染波浪动画。
* - 计算粒子的大小和位置。
* - 更新粒子的几何图形。
* - 渲染场景。
*/
const wavesRender = () => {
// 如果波浪元素不存在,则什么也不做。
if (!waves.value) return;
// 调整视角
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (-mouseY - camera.position.y) * 0.05;
camera.lookAt(scene.position);
// 计算粒子的大小和位置。
let i = 0;
let j = 0;
const positions = particles.geometry.attributes.position.array;
const scales = particles.geometry.attributes.scale.array;
for (let ix = 0; ix < amountX; ix++) {
for (let iy = 0; iy < amountY; iy++) {
positions[i + 1] =
Math.sin((ix + count) * 0.3) * 50 + Math.sin((iy + count) * 0.5) * 50;
scales[j] =
(Math.sin((ix + count) * 0.3) + 1) * 8 +
(Math.sin((iy + count) * 0.5) + 1) * 8;
i += 3;
j++;
}
}
// 更新粒子的几何图形。
particles.geometry.attributes.position.needsUpdate = true;
particles.geometry.attributes.scale.needsUpdate = true;
// 渲染场景。
wavesRenderer.render(scene, camera);
// 更新计数器。
count += 0.1;
};
onMounted(() => {
if (waves.value) {
const { width, height } = waves.value.getBoundingClientRect();
wavesWidth = width;
wavesHeight = height;
wavesInit();
wavesAnimate();
}
});
const animationListMap = new Map();
let animationPoint: Function = () => {};
type AnimationLogo = {
transform: Function;
opacity: Function;
};
let animationLogo: AnimationLogo;
const itemsRef = ref<HTMLElement[]>([]);
function setItemsRef(el: any) {
if (el) {
itemsRef.value.push(el);
}
}
const pointsRef = ref<HTMLElement[]>([]);
function setPointsRef(el: any) {
if (el) {
pointsRef.value.push(el);
}
}
const content = ref<HTMLElement>();
const list = ref<HTMLElement>();
const containerScroll = ref<HTMLElement>();
const header = ref<HTMLElement>();
const logo = ref<HTMLElement>();
/**
* 生成随机颜色
*/
const randomHexColor = () => {
return Array.from({ length: 6 }).reduce((prev) => {
return prev + Math.floor(Math.random() * 16).toString(16);
}, "#");
};
/**
* 生成随机位置
*/
const randomOption = () => {
const left =
Math.floor(
Math.random() * (optionMinMax.leftMax - optionMinMax.leftMin + 1)
) + optionMinMax.leftMin;
const top =
Math.floor(
Math.random() * (optionMinMax.topMax - optionMinMax.topMin + 1)
) + optionMinMax.topMin;
return {
left,
top,
};
};
/**
* 随机位置控制方法 不允许落在中间位置
*/
const positionControl = () => {
let { left, top } = randomOption();
while (
left >= optionMinMax.centerLeftMin &&
left <= optionMinMax.centerLeftMax &&
top >= optionMinMax.centerTopMin &&
top <= optionMinMax.centerTopMax
) {
const { left: newLeft, top: newTop } = randomOption();
left = newLeft;
top = newTop;
}
return {
left,
top,
};
};
const getPointStyle = (index: number) => {
// 随机生成的粒子位置 很丑 所以最后使用自定义数据渲染了
// const widthNumber = 20 + Math.floor(Math.random() * 40);
// const width = `${widthNumber}px`;
// const { left, top } = positionControl();
// const background = `linear-gradient(135deg, ${randomHexColor()}, ${randomHexColor()})`;
// const borderRadius = `${widthNumber * 0.2}px`;
// return {
// left: `${left}px`,
// top: `${top}px`,
// width,
// borderRadius,
// background,
// };
const { left, top, width, background } = pointStyleList[index];
return {
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
background,
};
};
/**
* 根据传入值进行动态创建线性函数
* @param xStart x起始值
* @param xEnd x结束值
* @param yStart y起始值
* @param yEnd y结束值
*/
function createAnimation(
xStart: number,
xEnd: number,
yStart: number,
yEnd: number
) {
return function (x: number) {
if (x <= xStart) {
return yStart;
}
if (x >= xEnd) {
return yEnd;
}
return yStart + ((x - xStart) / (xEnd - xStart)) * (yEnd - yStart);
};
}
/**
* 获取list动画
* @param scrollStart 滚动开始位置
* @param scrollEnd 结束位置
* @param listWidth 当前list宽度
* @param listHeight 当前list高度
* @param dom 单个item-dom
*/
const getListAnimation = (
scrollStart: number,
scrollEnd: number,
listWidth: number,
listHeight: number,
dom: HTMLElement
) => {
scrollStart += +(dom.dataset?.order || 0) * 400;
const opacityAnimation = createAnimation(scrollStart, scrollEnd, 0, 1);
const opacity = (scroll: number) => {
return opacityAnimation(scroll);
};
const scaleAnimation = createAnimation(scrollStart, scrollEnd, 0.5, 1);
const xOffset = listWidth / 2 - dom.offsetLeft - dom.clientWidth / 2;
const yOffset = listHeight / 2 - dom.offsetTop - dom.clientHeight / 2;
const xAnimation = createAnimation(scrollStart, scrollEnd, xOffset, 0);
const yAnimation = createAnimation(scrollStart, scrollEnd, yOffset, 0);
const transform = (scroll: number) => {
return `translate(${xAnimation(scroll)}px,${yAnimation(
scroll
)}px) scale(${scaleAnimation(scroll)})`;
};
return {
opacity,
transform,
};
};
const getPointAnimation = (scrollStart: number, scrollEnd: number) => {
animationPoint = pointAnimation(scrollStart, scrollEnd);
};
const getLogoAnimation = (scrollStart: number, scrollEnd: number) => {
animationLogo = logoAnimation(scrollStart, scrollEnd);
};
/**
* 更新动画方法
*/
function updateAnimationMap() {
if (!content.value || !list.value || !header.value) return;
const contentRect = content.value.getBoundingClientRect();
const scrollY = containerScroll.value?.scrollTop || 0;
const scrollStart = contentRect.top + scrollY;
const { height: headerHeight } = header.value?.getBoundingClientRect();
const scrollEnd = contentRect.bottom + scrollY - headerHeight;
// 从content位置开始 到content底部结束
listAnimationMap(scrollStart, scrollEnd);
// 从头开始 到content上移一个都头部度结束
getPointAnimation(scrollStart - headerHeight, scrollEnd - headerHeight);
// 从content开始 到content上移一个头部位置结束
getLogoAnimation(scrollStart, scrollEnd - headerHeight);
}
function listAnimationMap(scrollStart: number, scrollEnd: number) {
animationListMap.clear();
if (!list.value) return;
const { width: listWidth, height: listHeight } =
list.value.getBoundingClientRect();
for (let i = 0; i < itemsRef.value.length; i++) {
const dom = itemsRef.value[i];
animationListMap.set(
dom,
getListAnimation(scrollStart, scrollEnd, listWidth, listHeight, dom)
);
}
}
function pointAnimation(scrollStart: number, scrollEnd: number) {
const translate3dZ = createAnimation(scrollStart, scrollEnd, 0, 500);
const transform = (scroll: number) => {
return `translate3d(-50%,-50%,${translate3dZ(scroll)}px)`;
};
return transform;
}
function logoAnimation(scrollStart: number, scrollEnd: number) {
const translateY = createAnimation(scrollStart, scrollEnd, 50, 700);
const scale = createAnimation(scrollStart, scrollEnd, 1, 5);
const transform = (scroll: number) => {
return `translate(-50%,${-translateY(scroll)}%) scale(${scale(scroll)})`;
};
// 为了提前看到全貌 结束为止减少2500滚动位置
const opacity = createAnimation(scrollStart, scrollEnd - 2500, 0.4, 1);
return { transform, opacity };
}
// 节流处理
let lastTimestamp = 0;
function updateStyles() {
// 获取当前滚动位置
const scrollY = containerScroll.value?.scrollTop;
// 节流处理
requestAnimationFrame((timestamp) => {
// chrome 浏览器在滚动的时候都是大于16毫秒的 别的浏览器不清楚这个处理暂时保留
if (timestamp - lastTimestamp < 16) return;
lastTimestamp = timestamp;
// 循环设置当前滚动路径上面的list的动画
for (const [dom, animations] of animationListMap) {
for (const cssProp in animations) {
dom.style[cssProp] = animations[cssProp](scrollY);
}
}
// 设置点动画
if (pointsRef.value) {
pointsRef.value.forEach((el) => {
el.style.transform = animationPoint(scrollY);
});
}
// 设置logo动画
if (logo.value) {
logo.value.style.transform = animationLogo.transform(scrollY);
logo.value.style.opacity = animationLogo.opacity(scrollY);
}
});
}
onMounted(() => {
updateAnimationMap();
// 监听滚动
containerScroll.value?.addEventListener("scroll", updateStyles);
// 监听页面大小变化
window.addEventListener("resize", () => {
updateAnimationMap();
updateStyles();
});
});
</script>
<style lang="scss" scoped>
@import "./index.scss";
</style>
css 样式代码 点击展开
scss
.container {
height: 400px;
overflow-y: auto;
.header,
.footer {
position: relative;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
}
.content {
position: relative;
height: 4000px;
.waves {
position: absolute;
bottom: -100px;
width: 100%;
height: 400px;
}
.animation-container {
position: sticky;
height: 400px;
top: 0;
overflow: hidden;
.scatter-point {
position: absolute;
width: 100%;
height: 100%;
// 设置3D变换
transform-style: preserve-3d;
// 设置观察者视角距离
perspective: 500px;
overflow: hidden;
z-index: 1;
&::before,
&::after {
content: "";
position: absolute;
height: 100%;
z-index: 2;
box-shadow: 0 0 200px 200px rgba(0, 0, 0, 0.4);
}
&::before {
left: 0;
}
&::after {
right: 0;
}
.point {
position: absolute;
// 为盒子规定了首选纵横比
aspect-ratio: 1/1;
border-radius: 10px;
transform: translate3d(-50%, -50%, 0);
border-radius: 6px;
}
}
.list {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
@media (max-width: 768px) {
width: 98%;
}
aspect-ratio: 2/1;
border-radius: 10px;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(2, 1fr);
place-items: center;
z-index: 3;
}
.list-item {
// 为盒子规定了首选纵横比
aspect-ratio: 1/1;
padding: 10px;
@media (max-width: 768px) {
padding: 2px;
}
color: #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
&:nth-child(3n + 1) {
background: linear-gradient(135deg, #2e7fff, #2878f9);
}
&:nth-child(3n + 2) {
background: linear-gradient(135deg, #2dba48, #00a037);
}
&:nth-child(3n + 3) {
background: linear-gradient(135deg, #ff5f00, #ff9207);
}
}
}
.logo {
position: absolute;
left: 50%;
top: 50%;
width: 100px;
height: 100px;
z-index: 5;
.logo-text {
text-align: center;
line-height: 24px;
margin-top: 6px;
}
}
}
.header {
background-color: rgb(110, 73, 73);
}
.content {
background-color: rgb(38, 31, 46);
}
.footer {
background-color: rgb(91, 155, 125);
}
}
配置数据 点击展开
ts
const lanzi = `linear-gradient(135deg, #002d9c, #8f35ff)`
const lan = `linear-gradient(135deg, #004fff, #007fff)`
const qianlan = `linear-gradient(135deg, #56ece4, #09baff)`
const lv = `linear-gradient(135deg, #00ebb6, #00ba46)`
const huang = `linear-gradient(135deg, #ffc400, #ff9200)`
const cheng = `linear-gradient(135deg, #ff5f00, #ff8d16)`
const zi = `linear-gradient(135deg, #e58dff, #8f35ff)`
export const pointStyleList = [
{
left: 50,
top: 50,
width: 60,
background: lanzi,
},
{
left: 270,
top: 30,
width: 50,
background: lanzi,
},
{
left: 60,
top: 340,
width: 60,
background: zi,
},
{
left: 240,
top: 190,
width: 20,
background: zi,
},
{
left: 600,
top: 80,
width: 50,
background: lan,
},
{
left: 640,
top: 330,
width: 60,
background: lan,
},
{
left: 350,
top: 50,
width: 35,
background: lan,
},
{
left: 330,
top: 150,
width: 20,
background: cheng,
},
{
left: 330,
top: 350,
width: 30,
background: lan,
},
{
left: 590,
top: 250,
width: 45,
background: qianlan,
},
{
left: 500,
top: 280,
width: 35,
background: qianlan,
},
{
left: 420,
top: 210,
width: 20,
background: qianlan,
},
{
left: 210,
top: 160,
width: 25,
background: qianlan,
},
{
left: 540,
top: 130,
width: 35,
background: lv,
},
{
left: 130,
top: 120,
width: 40,
background: huang,
},
{
left: 200,
top: 220,
width: 25,
background: huang,
},
{
left: 80,
top: 170,
width: 50,
background: cheng,
},
{
left: 110,
top: 250,
width: 45,
background: cheng,
},
]
const [centerLeftMin, centerLeftMax, centerTopMin, centerTopMax] = [
200, 500, 50, 300,
];
const [leftMin, leftMax, topMin, topMax] = [30, 640, 30, 370];
export const optionMinMax = {
centerLeftMin,
centerLeftMax,
centerTopMin,
centerTopMax,
leftMin,
leftMax,
topMin,
topMax,
}
// 展示文字
export const textList = [
"天",
"生",
"我",
"材",
"必",
"有",
"用",
"千",
"金",
"散",
"尽",
"还",
"复",
"来",
];
// 控制先后动画展示位置
export const orderList = [0, 1, 2, 3, 2, 1, 0, 0, 1, 2, 3, 2, 1, 0];