👨‍⚕️ 主页: 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 ☘️文档与社区支持

kokomi 官方文档

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 ☘️文档与社区支持

gasp 官网

二、🍀图片环效果

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

效果如下:
在这里插入图片描述
参考源码

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐