学习threejs,使用kokomi、gsap实现图片环效果
本文详细介绍如何基于threejs在三维场景中使用kokomi、gsap实现图片环效果,亲测可用。希望能帮助到您。一起学习,加油!加油!
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
一、🍀前言
本文详细介绍如何基于threejs在三维场景中使用kokomi、gsap实现图片环效果,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️kokomi.js
kokomi.js 是一个基于 Three.js 的轻量级 3D 开发辅助库,旨在简化 Web 端 3D 场景的搭建流程,提升开发效率。其设计灵感源于《原神》角色珊瑚宫心海“统筹全局”的能力特点,通过封装常用功能降低 Three.js 的学习曲线,使开发者能更专注于创意实现。
代码示例:
import * as kokomi from "kokomi.js";
class Sketch extends kokomi.Base {
create() {
new kokomi.OrbitControls(this); // 添加轨道控制器
const box = new kokomi.Box(this); // 创建立方体
box.addExisting();
this.update((time) => {
box.spin(time); // 立方体旋转动画
});
}
}
const createSketch = () => {
const sketch = new Sketch();
sketch.create();
return sketch;
};
createSketch();
1.1.1 ☘️核心功能
快速场景构建
继承 kokomi.Base 类即可快速启动 3D 场景,无需编写繁琐的初始化代码。
组件化架构
通过 kokomi.Component 封装 3D 元素,保持组件独立状态与动画管理,便于大型项目代码组织。
资源管理
内置 AssetManager 工具,支持 GLTF 模型、纹理、立方体贴图、字体等资源的预加载与配置。
const assetManager = new kokomi.AssetManager(this);
assetManager.load({
gltfModel: { url: "model.gltf" },
texture: { url: "texture.jpg" }
});
交互集成
集成 three.interactive 库,轻松实现鼠标与触控交互(如拖拽、点击)。
扩展性
提供丰富预置组件(如 OrbitControls、Box),并支持自定义组件开发,满足多样化 3D 场景需求。
1.1.2 ☘️技术实现与依赖
基于 TypeScript 开发,兼容现代 JavaScript 生态,需配合 Three.js 使用。
安装方式
npm install kokomi.js three
开发环境
需 Node.js 环境,支持通过 Git 克隆仓库获取源码,并使用 npm 脚本运行示例或启动开发服务器。
1.1.3 ☘️文档与社区支持
1.2 ☘️gsap.js
GSAP(GreenSock Animation Platform)是一个高性能、跨平台的 JavaScript 动画库,旨在简化复杂动画效果的实现。其核心优势在于提供精确的动画控制、丰富的缓动效果和广泛的浏览器兼容性,适用于网页设计、游戏开发、移动应用开发等多个领域。
代码示例:
gsap.to("#navbar", { height: "100px", duration: 0.5, ease: "power1.inOut" });
1.2.1 ☘️核心功能
动画控制
补间动画:通过 gsap.to()、gsap.from() 和 gsap.fromTo() 方法,可以轻松实现元素的位移、旋转、缩放、透明度变化等动画效果。
时间轴管理:使用 gsap.timeline() 可以创建复杂的动画序列,控制多个动画的顺序和并行执行。
响应式动画:支持根据视口大小或其他条件动态调整动画参数。
缓动效果
GSAP 内置了丰富的缓动函数库,涵盖线性、弹性、弹跳、正弦等多种类型,如 “power1.in”、“elastic.out” 等。通过设置 ease 属性,可以为动画添加自然和富有表现力的效果。
插件系统
ScrollTrigger:允许用最少的代码创建滚动动画,例如当元素滚动到视口特定位置时触发动画。
MorphSVG:支持 SVG 路径的变形动画。
Draggable:实现拖拽功能,增强交互性。
跨平台兼容性
支持所有主流浏览器,包括移动设备,确保动画在不同环境下的一致性表现。
1.2.2 ☘️技术实现与依赖
安装方式 : Object
// 通过 npm 安装
npm install gsap
// 通过 CDN 引入
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
模块化设计 : Object
GSAP 3.x 版本将 TweenMax 和 TweenLite 的功能整合到 gsap 对象中,支持按需引入插件,减少体积。
1.2.3 ☘️文档与社区支持
二、🍀图片环效果
1. ☘️实现思路
使用kokomi、gsap以及自定义着色器实现图片环效果
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://cdn.jsdelivr.net/gh/alphardex/aqua.css/dist/aqua.min.css" rel="stylesheet">
<title>图片环</title>
<style>
body {
margin: 0;
overflow: hidden;
background: black;
font-family: "Inter", sans-serif;
}
#sketch {
width: 100vw;
height: 100vh;
background: black;
}
.loading span {
animation: blur 1.5s calc(var(--i) / 5 * 1s) alternate infinite;
}
@keyframes blur {
to {
filter: blur(5px);
}
}
</style>
</head>
<body>
<!-- On-screen Mobile Controls -->
<div id="sketch"></div>
<div class="fixed z-5 top-0 left-0 loader-screen w-screen h-screen transition-all duration-300 bg-white">
<div class="absolute hv-center">
<div class="loading text-3xl tracking-widest whitespace-no-wrap">
<span style="--i: 0">L</span>
<span style="--i: 1">O</span>
<span style="--i: 2">A</span>
<span style="--i: 3">D</span>
<span style="--i: 4">I</span>
<span style="--i: 5">N</span>
<span style="--i: 6">G</span>
</div>
</div>
</div>
<div class="hero-dom opacity-0">
<div class="absolute hv-center">
<div class="flex flex-col items-center space-y-2 whitespace-no-wrap">
<div class="text-8xl text-white">RING</div>
<div class="text-xl" style="color: #8a8a8a;">Just drag and scroll~</div>
</div>
</div>
</div>
<!-- Import map for Three.js ES Modules -->
<script type="module">
import * as kokomi from "https://esm.sh/kokomi.js";
import * as THREE from "https://esm.sh/three";
import gsap from "https://esm.sh/gsap";
const fragmentShader = /* glsl */ `
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
uniform sampler2D tDiffuse;
varying vec2 vUv;
uniform vec3 uBgColor;
uniform float uRGBShiftIntensity;
uniform float uGrainIntensity;
uniform float uVignetteIntensity;
uniform float uTransitionProgress;
highp float random(vec2 co)
{
highp float a=12.9898;
highp float b=78.233;
highp float c=43758.5453;
highp float dt=dot(co.xy,vec2(a,b));
highp float sn=mod(dt,3.14);
return fract(sin(sn)*c);
}
vec3 grain(vec2 uv,vec3 col,float amount){
float noise=random(uv+iTime);
col+=(noise-.5)*amount;
return col;
}
vec4 RGBShift(sampler2D tex,vec2 uv,float amount){
vec2 rUv=uv;
vec2 gUv=uv;
vec2 bUv=uv;
float noise=random(uv+iTime)*.5+.5;
vec2 offset=amount*vec2(cos(noise),sin(noise));
rUv+=offset;
gUv+=offset*.5;
bUv+=offset*.25;
vec4 rTex=texture(tex,rUv);
vec4 gTex=texture(tex,gUv);
vec4 bTex=texture(tex,bUv);
vec4 col=vec4(rTex.r,gTex.g,bTex.b,gTex.a);
return col;
}
vec3 vignette(vec2 uv,vec3 col,vec3 vigColor,float amount){
vec2 p=uv;
p-=.5;
float d=length(p);
float mask=smoothstep(.5,.3,d);
mask=pow(mask,.6);
float mixFactor=(1.-mask)*amount;
col=mix(col,vigColor,mixFactor);
return col;
}
float sdCircle(vec2 p,float r)
{
return length(p)-r;
}
vec3 transition(vec2 uv,vec3 col,float progress){
float ratio=iResolution.x/iResolution.y;
// circle
vec2 p=uv;
p-=.5;
p.x*=ratio;
float d=sdCircle(p,progress*sqrt(2.2));
float c=smoothstep(-.2,0.,d);
col=mix(vec3(1.),col,1.-c);
return col;
}
void main(){
vec2 uv=vUv;
vec4 tex=RGBShift(tDiffuse,uv,uRGBShiftIntensity);
vec3 col=tex.xyz;
col=grain(uv,col,uGrainIntensity);
col=vignette(uv,col,uBgColor,uVignetteIntensity);
col=transition(uv,col,uTransitionProgress);
gl_FragColor=vec4(col,1.);
}`;
class Sketch extends kokomi.Base {
create() {
const config = {
bgColor: "#0c0c0c"
};
const params = {
transitionProgress: 0,
enterProgress: 0,
rotateSpeed: 15
};
this.renderer.setClearColor(new THREE.Color(config.bgColor), 1);
this.camera.position.set(0, 0, 16);
// new kokomi.OrbitControls(this);
const sumFormula = (n) => {
return (n * (n + 1)) / 2;
};
const isOdd = (n) => {
return n % 2 === 1;
};
const circleCount = 3;
const circleImgCountUnit = 12;
const circleImgTotalCount = circleImgCountUnit * sumFormula(circleCount);
const resourceList = [...Array(circleImgTotalCount).keys()].map((_, i) => ({
name: `tex${i + 1}`,
type: "texture",
// path: `https://picsum.photos/id/${i + 1}/320/400`
path: `./images/photos/${i + 1}.jpg`
}));
const am = new kokomi.AssetManager(this, resourceList);
am.on("ready", () => {
document.querySelector(".loader-screen")?.classList.add("hollow");
const material = new THREE.MeshBasicMaterial();
const r = 6.4;
const scale = 0.8;
const rings = [];
const lines = [];
for (let i = 0; i < circleCount; i++) {
const c1 = sumFormula(i) * circleImgCountUnit;
const c2 = sumFormula(i + 1) * circleImgCountUnit;
const textures = Object.values(am.items).slice(c1, c2);
const ring = new THREE.Group();
this.scene.add(ring);
rings.push(ring);
const meshes = textures.map((tex, j) => {
const line = new THREE.Group();
ring.add(line);
lines.push(line);
const imgScale = 0.005 * scale * (i * 0.36 + 1);
const width = tex.image.width * imgScale;
const height = tex.image.height * imgScale;
const geometry = new THREE.PlaneGeometry(width, height);
const mat = material.clone();
mat.map = tex;
mat.needsUpdate = true;
const mesh = new THREE.Mesh(geometry, mat);
const r2 = r * (i + 1);
const ratio = j / (c2 - c1);
const angle = ratio * Math.PI * 2;
mesh.position.x = r2;
mesh.rotation.z = -Math.PI / 2;
line.rotation.z = angle;
line.add(mesh);
return mesh;
});
}
const ce = new kokomi.CustomEffect(this, {
fragmentShader,
uniforms: {
uBgColor: {
value: new THREE.Color(config.bgColor)
},
uRGBShiftIntensity: {
value: 0.0025
},
uGrainIntensity: {
value: 0.025
},
uVignetteIntensity: {
value: 0.8
},
uTransitionProgress: {
value: 0
}
}
});
ce.addExisting();
this.ce = ce;
const wheelScroller = new kokomi.WheelScroller();
wheelScroller.listenForScroll();
const dragDetecter = new kokomi.DragDetecter(this);
dragDetecter.detectDrag();
dragDetecter.on("drag", (delta) => {
wheelScroller.scroll.target -= (delta.x || delta.y) * 2;
});
this.update(() => {
wheelScroller.syncScroll();
rings.forEach((ring, i) => {
ring.rotation.z +=
0.0025 *
(isOdd(i) ? -1 : 1) *
(1 + wheelScroller.scroll.delta) *
params.rotateSpeed;
});
lines.forEach((line) => {
line.position.z =
-THREE.MathUtils.lerp(
0,
100,
THREE.MathUtils.mapLinear(
wheelScroller.scroll.delta,
0,
1000,
0,
1
)
) + THREE.MathUtils.lerp(10, 0, params.enterProgress);
});
this.ce.customPass.material.uniforms.uTransitionProgress.value =
params.transitionProgress;
});
const anime = () => {
const t1 = gsap.timeline();
t1.to(params, {
transitionProgress: 1,
duration: 1,
ease: "power1.inOut"
})
.fromTo(
params,
{
enterProgress: 0,
rotateSpeed: 10
},
{
enterProgress: 1,
rotateSpeed: 1,
duration: 1.5,
ease: "power1.inOut"
},
"-=1"
)
.to(
".hero-dom",
{
opacity: 1
},
"-=1"
);
};
anime();
});
}
}
const sketch = new Sketch("#sketch");
sketch.create();
</script>
</body>
</html
效果如下:
参考源码
更多推荐
所有评论(0)