エフェクト
slider-world

Slider-world


デモ

ソースコード

import {
  CylinderGeometry,
  MeshBasicMaterial,
  Mesh,
  PlaneGeometry,
  Vector3,
  DoubleSide,
} from "three";
import gsap from "gsap";
import vertexShader from "./vertex.glsl";
import fragmentShader from "./fragment.glsl";
import { utils, INode, viewport } from "negl";
import { MultiMeshSlider } from "#/parts/helper/slider/MultiMeshSlider";

/**
 * slider-world
 *
 * シリンダー状にメッシュが表示されるスライダです。
 * Slider where the mesh is displayed in a cylindrical shape.
 */
export default class extends MultiMeshSlider {
  beforeCreateMesh() {
    super.beforeCreateMesh();
    this.radius = this.rect.width;
    this.rotateAxis = new Vector3(0.2, 0.8, 0.2).normalize();
    this.diffRad = 0;
    this.visibleThreshold = 0;
  }

  setupGeometry() {
    return new PlaneGeometry(this.rect.width, this.rect.height, 50, 1);
  }

  setupUniforms() {
    const uniforms = super.setupUniforms();
    uniforms.uDist = { value: 0.8 };
    uniforms.uRadius = { value: this.radius };
    return uniforms;
  }

  setupMesh() {
    const cylinderGeo = new CylinderGeometry(
      this.radius,
      this.radius,
      this.rect.height,
      100,
      1,
      true
    );

    const cylinderMate = new MeshBasicMaterial({
      transparent: true,
      opacity: 0,
      alphaTest: 0.5,
    });

    const cylinder = new Mesh(cylinderGeo, cylinderMate);

    let idx = 0;
    this.texes.forEach((tex) => {
      const planeMate = this.material.clone();
      planeMate.side = DoubleSide;
      planeMate.uniforms.tex1 = { value: tex };
      planeMate.uniforms.uSlideIdx.value = idx;
      planeMate.uniforms.uActiveSlideIdx = this.uniforms.uActiveSlideIdx;
      planeMate.uniforms.uTick = this.uniforms.uTick;
      planeMate.uniforms.uDist = this.uniforms.uDist;

      const planeGeo = this.geometry;
      const plane = new Mesh(planeGeo, planeMate);

      cylinder.add(plane);

      idx++;
    });

    this.slides = [...cylinder.children];

    utils.pointTo(cylinder, cylinder.up, this.rotateAxis);

    return cylinder;
  }

  async resize(duration = 1) {
    this.resizing = true;

    const {
      $: { el },
      mesh: cylinder,
      originalRect,
    } = this;

    const nextRect = INode.getRect(el);
    const { x, y } = this.getWorldPosition(nextRect, viewport);

    // 位置の変更. Changing the position
    const p1 = new Promise((onComplete) => {
      gsap.to(cylinder.position, {
        x,
        y,
        overwrite: true,
        duration,
        onComplete,
      });
    });

    // 大きさの変更. Changing the size
    const { position, normal } = cylinder.geometry.attributes;
    const ONE_LOOP = cylinder.geometry.attributes.position.count / 2;
    const step = Math.floor(ONE_LOOP / this.texes.size);
    const p2 = new Promise((onComplete) => {
      gsap.to(this.scale, {
        width: nextRect.width / originalRect.width,
        height: nextRect.height / originalRect.height,
        depth: 1,
        overwrite: true,
        duration,
        onUpdate: () => {
          cylinder.scale.set(
            this.scale.width,
            this.scale.height,
            this.scale.width
          );

          let idx = 0;
          this.slides.forEach((plane) => {
            const pickIdx = idx * step;
            plane.position.x = position.getX(pickIdx);
            plane.position.z = position.getZ(pickIdx);

            const originalDir = { x: 0, y: 0, z: 1 };
            const targetDir = {
              x: normal.getX(pickIdx),
              y: 0,
              z: normal.getZ(pickIdx),
            };

            utils.pointTo(plane, originalDir, targetDir);

            idx++;
          });

          this.radius = nextRect.width;
          cylinder.position.z = -this.radius;
        },
        onComplete,
      });
    });

    // 画像のアスペクトの変更. Changing the aspect of the image
    const p3 = new Promise((onComplete) => {
      const resolution = this.getResolution(nextRect);
      gsap.to(this.uniforms.uResolution.value, {
        x: resolution.x,
        y: resolution.y,
        z: resolution.z,
        w: resolution.w,
        overwrite: true,
        duration,
        onComplete,
      });
    });

    await Promise.all([p1, p2, p3]);

    this.rect = nextRect;

    this.resizing = false;
  }

  setupVertex() {
    return vertexShader;
  }

  setupFragment() {
    return fragmentShader;
  }

  render(tick) {
    super.render(tick);

    const idx = utils.lerp(
      this.easeSlideIdx,
      this.activeSlideIdx,
      this.slideSpeed
    );

    if (this.activeSlideIdx === idx) return;

    this.rotate(idx - this.easeSlideIdx);

    this.setUActiveSlideIdx(idx);

    this.easeSlideIdx = idx;
  }

  rotate(idx) {
    const diffRad = -(idx / this.slides.length) * 2 * Math.PI;

    this.mesh.rotateOnWorldAxis(this.rotateAxis, diffRad);
  }

  setUActiveSlideIdx(idx) {
    this.uniforms.uActiveSlideIdx.value = idx;
  }

  /**
   * アニメーションなしでスライダを指定の idx まで回転させます。
   * Rotates the slider to the specified idx without animation.
   *
   * @param {number} idx
   * @param {number} offsetRad
   */
  setTo(idx, offsetRad = 0) {
    this.rotate(idx - offsetRad - this.activeSlideIdx);
    super.setTo(idx);
    this.setUActiveSlideIdx(idx);
  }

  afterInit() {
    this.texes.forEach((tex) => {
      tex.source.data.pause?.();
    });

    this.setTo(this.activeSlideIdx, this.offsetRad);
  }

  debug(folder) {
    super.debug(folder);
    const changeRotateAxis = () => {
      const normalize = new Vector3().copy(this.rotateAxis).normalize();

      utils.pointTo(this.mesh, this.mesh.up, normalize);
    };
    folder.add(this.uniforms.uDist, "value", 0, 1, 0.01).name("uDist").listen();
    folder
      .add(this.rotateAxis, "x", -1, 1, 0.01)
      .name("rotation.x")
      .listen()
      .onChange(changeRotateAxis);
    folder
      .add(this.rotateAxis, "y", -1, 1, 0.01)
      .name("rotation.y")
      .listen()
      .onChange(changeRotateAxis);

    folder
      .add(this.rotateAxis, "z", -1, 1, 0.01)
      .name("rotation.z")
      .listen()
      .onChange(changeRotateAxis);
  }
}

利用方法

⚠️

ダウンロードしたコードをプロジェクトに配置し、以下のコードを記述してください。

index.html

<img
  class="slider slider-w-45"
  data-webgl="slider-world"
  data-tex-1="/sample1.jpg"
  data-tex-2="/sample2.jpg"
  data-tex-3="/sample3.jpg"
  data-tex-4="/sample4.jpg"
  data-tex-5="/sample5.jpg"
/>

使用画像・動画

  • こちらを参考にダウンロードした各ファイルを配置してください。