Skip to content

Commit

Permalink
添加 Gallery Module
Browse files Browse the repository at this point in the history
  • Loading branch information
SuneBear committed Oct 14, 2023
1 parent a3d9feb commit 3fe5d9a
Show file tree
Hide file tree
Showing 35 changed files with 412 additions and 18 deletions.
5 changes: 3 additions & 2 deletions app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import Lenis from '@studio-freight/lenis'
const setupSmoothScroll = () => {
const lenis = new Lenis({
normalizeWheel: true
normalizeWheel: true,
lerp: 0.15,
})
lenis.on('scroll', ScrollTrigger.update)
Expand All @@ -30,7 +31,7 @@ const setupSmoothScroll = () => {
onMounted(() => {
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(ScrollToPlugin)
setupSmoothScroll()
// setupSmoothScroll()
})
</script>

Expand Down
5 changes: 5 additions & 0 deletions components/landing-sketch/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ import * as THREE from 'three'

// 解决循环依赖的问题
export const loadingManager = new THREE.LoadingManager()

export const LAYERS = {
DEFAULT: 0,
GALLERY: 1
}
153 changes: 153 additions & 0 deletions components/landing-sketch/gallery.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as THREE from 'three'
import gsap from 'gsap'

import { LAYERS } from './common'
import { Module } from './module'
import { ProjectCaseOptions, ProjectCaseObject } from './project-case.object'

const CASE_GAP = 1
const VIEWPORT_SIZE = 2.2

const projectCasesConfig: ProjectCaseOptions[] = [
{
mockupModelUrl: '/cases/rct-tv/mockup.glb',
recordVideoUrl: '/cases/rct-tv/record.mp4',
recordVideoRatio: 718/670,
name: 'rct TV',
meta: {
year: 2018
},
flipY: false,
modelSize: 1.4,
// scale: 0.28,
// rotation: [ -0.1, -0.52, 0],
// position: [0, -0, -0.2]
scale: 0.6,
rotation: [ -0.1, -0.36, 0],
position: [0, -0.5, 0]
},

{
mockupModelUrl: '/cases/rct-dna/mockup.glb',
recordVideoUrl: '/cases/rct-dna/record.mp4',
recordVideoRatio: 720/720,
name: 'rct DNA',
meta: {
year: 2019
},
flipY: false,
modelSize: 1.1,
scale: 0.28,
rotation: [ -0.1, -0.3, 0],
position: [0, -0.25, 0]
},

{
mockupModelUrl: '/cases/mirrorworld-space/mockup.glb',
recordVideoUrl: '/cases/mirrorworld-space/record.mp4',
recordVideoRatio: 1280/720,
name: 'Mirror World Crystal Space',
meta: {
year: 2021
},
flipY: false,
modelSize: 1.6,
scale: 0.7,
rotation: [ -0.1, -0.4, 0],
position: [0, -0.5, 0]
},

{
mockupModelUrl: '/cases/delysium-whitepaper/mockup.glb',
recordVideoUrl: '/cases/delysium-whitepaper/record.mp4',
recordVideoRatio: 1252/712,
name: 'Delysium Whitepaper',
meta: {
year: 2022
},
flipY: false,
modelSize: 1.8,
scale: 0.4,
rotation: [ -0.1, -0.45, 0],
position: [0, -0.3, 0]
},

{
mockupModelUrl: '/cases/affine-landing-v2/mockup.glb',
recordVideoUrl: '/cases/affine-landing-v2/record.mp4',
recordVideoRatio: 1222/638,
name: 'AFFiNE Landing V2',
meta: {
year: 2023
},
flipY: false,
modelSize: 1.7,
scale: 0.7,
rotation: [ -0.1, -0.4, 0],
position: [0, -0.48, 0]
}
]

export class GalleryModule extends Module {
group: THREE.Group
galleryWidthSize: number = 0

setup () {
const { scene } = this.world
this.group = new THREE.Group()
scene.add(this.group)

// 只给 Gallery 用
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
ambientLight.layers.set(LAYERS.GALLERY)
this.group.add(ambientLight)

// const light = new THREE.DirectionalLight(0x4ff54d)
const light = new THREE.DirectionalLight(0x808080)
light.position.y = 10
light.position.x = 10
light.layers.set(LAYERS.GALLERY)
this.group.add(light)

this.setupProjectCases()
this.listenToStore()
}

setupProjectCases () {
// const filteredCases = [projectCasesConfig[0], projectCasesConfig[1]]
const filteredCases = projectCasesConfig
filteredCases.map(config => {
const object = new ProjectCaseObject(config)
// 动态计算 X 轴位置
if (!config.position) {
config.position = [0, 0, 0]
}
config.position[0] = this.galleryWidthSize
this.group.add(object.$object)
if (config.modelSize) {
this.galleryWidthSize += config.modelSize + CASE_GAP
}
})
}

listenToStore () {
const store = useStore()

const scrollTimeline = gsap.timeline({
paused: true
})

scrollTimeline.fromTo(this.group.position, { x: 10 }, {
x: -this.galleryWidthSize - VIEWPORT_SIZE
})

watch(() => store.ui.galleryScrollProgress, (val) => {
scrollTimeline.progress(val)
})
}

update (delta: number) {

}

}
2 changes: 2 additions & 0 deletions components/landing-sketch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const listenToLoading = () => {
}

loadingManager.onLoad = () => {
if (!store.ui.isLoading) return

const delay = getTimeDelay()
// @TODO: 和 RAF 关联起来
setTimeout(() => {
Expand Down
6 changes: 4 additions & 2 deletions components/landing-sketch/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class LoaderManager {
})
}

load(_resources: Asset[] = [], callback?: (asset: Asset, data: any) => void) {
load<T = any>(_resources: Asset[] = [], callback?: (asset: Asset, data: T) => void) {
if (!Array.isArray(_resources)) {
_resources = [_resources]
}
Expand Down Expand Up @@ -125,7 +125,9 @@ class LoaderManager {
}

fileLoadEnd(asset: Asset, _data: any, callback: any) {
loadingManager.itemEnd(asset.url)
if (asset.id) {
loadingManager.itemEnd(asset.url)
}
callback && callback(asset, _data)
}
}
Expand Down
152 changes: 152 additions & 0 deletions components/landing-sketch/project-case.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as THREE from 'three'
import gsap from 'gsap'

import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { loaderManager } from './loader'
import { LAYERS } from './common'
import { clamp } from '@/utils/math'

import fragmentShader from './shaders/case-screen.frag'
import vertexShader from './shaders/case-screen.vert'

export interface ProjectCaseOptions {
// Media
mockupModelUrl: string
recordVideoUrl: string
recordVideoRatio: number
screenshotUrl?: string

// Meta
meta: {
year: number
client?: {
name: string
logoUrl: string
}
}
name?: string
content?: string

// Render params
flipY?: boolean
modelSize?: number
scale?: number
position?: number[]
rotation?: number[]
}

const DEFAULT_OPTIONS: Partial<ProjectCaseOptions> = {
modelSize: 1,
scale: 1,
flipY: true,
position: [],
rotation: []
}

export class ProjectCaseObject {
options: ProjectCaseOptions
videoTexture: THREE.VideoTexture
gltf: GLTF
$object: THREE.Group
screenUniforms: any

constructor(options: ProjectCaseOptions) {
this.options = {
...DEFAULT_OPTIONS,
...options
}

this.$object = new THREE.Group()

this.generateVideoTexture()
this.setupObject()
}

generateVideoTexture () {
const videoElement = document.createElement('video')
videoElement.playsInline = true
videoElement.autoplay = true
videoElement.loop = true
videoElement.muted = true
videoElement.src = this.options.recordVideoUrl
videoElement.play()
this.videoTexture = new THREE.VideoTexture(videoElement)
this.videoTexture.flipY = this.options.flipY || false
// this.videoTexture.magFilter = THREE.NearestFilter
// this.videoTexture.minFilter = THREE.NearestFilter
}

async setupObject () {
loaderManager.load<GLTF>([{
url: this.options.mockupModelUrl
} as any], (_, data) => {
this.gltf = data
this.$object.add(this.gltf.scene)
this.$object.traverseVisible((obj) => {
obj.layers.set(LAYERS.GALLERY)
})

if (this.options.position?.length) {
const position = this.options.position
this.$object.position.set(position[0], position[1], position[2])
}

if (this.options.rotation?.length) {
const rotation = this.options.rotation
this.$object.rotation.set(rotation[0], rotation[1], rotation[2])
}

if (this.options.scale) {
this.$object.scale.multiplyScalar(this.options.scale)
}

const screen = this.$object.getObjectByName('screen') as any
if (screen) {
screen.material.map = this.videoTexture
}

this.setupObjectMaterial()
})
}

// @TODO: 采用后处理的方式做 RGB Shift
// @TODO: 支持 Hover Distort
setupObjectMaterial () {
const store = useStore()

this.screenUniforms = {
uTexture: {
value: this.videoTexture
},
uOffset: {
//distortion strength
value: new THREE.Vector2(0.0, 0.0)
},
uAlpha: {
//opacity
value: 0.94
}
}
const material = new THREE.ShaderMaterial({
uniforms: this.screenUniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
side: THREE.DoubleSide
})
const screen = this.$object.getObjectByName('screen') as any
if (screen) {
screen.material = material
}

watch(() => store.ui.scrollSpeed, (val) => {
val = clamp(val, -100, 100)
gsap.to(this.screenUniforms.uOffset.value, {
x: val * -0.002,
overwrite: true,
duration: clamp(Math.random(), 0.3, 1)
})
})
}

}
16 changes: 16 additions & 0 deletions components/landing-sketch/shaders/case-screen.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
uniform sampler2D uTexture;
uniform float uAlpha;
uniform vec2 uOffset;
varying vec2 vUv;

vec3 rgbShift(sampler2D textureImage, vec2 uv, vec2 offset) {
float r = texture2D(textureImage,uv + offset).r;
vec2 gb = texture2D(textureImage,uv).gb;
return vec3(r,gb);
}

void main() {
vec3 color = rgbShift(uTexture,vUv,uOffset);
gl_FragColor = vec4(color,uAlpha);
// gl_FragColor = vec4(1.0 - gl_FragColor.r,1.0 -gl_FragColor.g,1.0 -gl_FragColor.b,1);
}
17 changes: 17 additions & 0 deletions components/landing-sketch/shaders/case-screen.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
uniform sampler2D uTexture;
uniform vec2 uOffset;
varying vec2 vUv;

#define M_PI 3.1415926535897932384626433832795

vec3 deformationCurve(vec3 position, vec2 uv, vec2 offset) {
position.x = position.x + (sin(uv.y * M_PI) * offset.x);
position.y = position.y + (sin(uv.x * M_PI) * offset.y);
return position;
}

void main() {
vUv = uv;
vec3 newPosition = deformationCurve(position, uv, uOffset);
gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}
Loading

0 comments on commit 3fe5d9a

Please sign in to comment.