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"
/>
使用画像・動画
- こちらを参考にダウンロードした各ファイルを配置してください。