Files
vue-bits/public/r/FlyingPosters.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
18 KiB
JSON

{"name":"FlyingPosters","title":"FlyingPosters","description":"3D posters rotate on scroll infinitely.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"containerRef\" :class=\"['w-full h-full overflow-hidden relative z-2', className]\" v-bind=\"$attrs\">\n <canvas ref=\"canvasRef\" class=\"block w-full h-full\" />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { Renderer, Camera, Transform, Plane, Program, Mesh, Texture, type OGLRenderingContext } from 'ogl';\n\ntype GL = OGLRenderingContext;\ntype OGLProgram = Program;\ntype OGLMesh = Mesh;\ntype OGLTransform = Transform;\ntype OGLPlane = Plane;\n\ninterface ScreenSize {\n width: number;\n height: number;\n}\n\ninterface ViewportSize {\n width: number;\n height: number;\n}\n\ninterface ScrollState {\n position?: number;\n ease: number;\n current: number;\n target: number;\n last: number;\n}\n\ninterface AutoBindOptions {\n include?: Array<string | RegExp>;\n exclude?: Array<string | RegExp>;\n}\n\ninterface MediaParams {\n gl: GL;\n geometry: OGLPlane;\n scene: OGLTransform;\n screen: ScreenSize;\n viewport: ViewportSize;\n image: string;\n length: number;\n index: number;\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n}\n\ninterface CanvasParams {\n container: HTMLElement;\n canvas: HTMLCanvasElement;\n items: string[];\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n scrollEase: number;\n cameraFov: number;\n cameraZ: number;\n}\n\nconst vertexShader = `\nprecision highp float;\n\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat3 normalMatrix;\n\nuniform float uPosition;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec3 distortionAxis;\nuniform vec3 rotationAxis;\nuniform float uDistortion;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\n\nfloat PI = 3.141592653589793238;\nmat4 rotationMatrix(vec3 axis, float angle) {\n axis = normalize(axis);\n float s = sin(angle);\n float c = cos(angle);\n float oc = 1.0 - c;\n \n return mat4(\n oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,\n oc * axis.x * axis.y + axis.z * s,oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,\n oc * axis.z * axis.x - axis.y * s,oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n}\n\nvec3 rotate(vec3 v, vec3 axis, float angle) {\n mat4 m = rotationMatrix(axis, angle);\n return (m * vec4(v, 1.0)).xyz;\n}\n\nfloat qinticInOut(float t) {\n return t < 0.5\n ? 16.0 * pow(t, 5.0)\n : -0.5 * abs(pow(2.0 * t - 2.0, 5.0)) + 1.0;\n}\n\nvoid main() {\n vUv = uv;\n \n float norm = 0.5;\n vec3 newpos = position;\n float offset = (dot(distortionAxis, position) + norm / 2.) / norm;\n float localprogress = clamp(\n (fract(uPosition * 5.0 * 0.01) - 0.01 * uDistortion * offset) / (1. - 0.01 * uDistortion),\n 0.,\n 2.\n );\n localprogress = qinticInOut(localprogress) * PI;\n newpos = rotate(newpos, rotationAxis, localprogress);\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(newpos, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform vec2 uImageSize;\nuniform vec2 uPlaneSize;\nuniform sampler2D tMap;\n\nvarying vec2 vUv;\n\nvoid main() {\n vec2 imageSize = uImageSize;\n vec2 planeSize = uPlaneSize;\n\n float imageAspect = imageSize.x / imageSize.y;\n float planeAspect = planeSize.x / planeSize.y;\n vec2 scale = vec2(1.0, 1.0);\n\n if (planeAspect > imageAspect) {\n scale.x = imageAspect / planeAspect;\n } else {\n scale.y = planeAspect / imageAspect;\n }\n\n vec2 uv = vUv * scale + (1.0 - scale) * 0.5;\n\n gl_FragColor = texture2D(tMap, uv);\n}\n`;\n\ntype MethodNames<T> = {\n [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;\n}[keyof T];\n\nfunction AutoBind<T extends object>(self: T, { include, exclude }: AutoBindOptions = {}) {\n const getAllProperties = (object: object): Set<[object, string | symbol]> => {\n const properties = new Set<[object, string | symbol]>();\n let currentObject: object | null = object;\n do {\n for (const key of Reflect.ownKeys(currentObject)) {\n properties.add([currentObject, key]);\n }\n } while ((currentObject = Reflect.getPrototypeOf(currentObject)) && currentObject !== Object.prototype);\n return properties;\n };\n\n const filter = (key: string | symbol) => {\n const match = (pattern: string | RegExp) =>\n typeof pattern === 'string' ? key === pattern : (pattern as RegExp).test(key.toString());\n\n if (include) return include.some(match);\n if (exclude) return !exclude.some(match);\n return true;\n };\n\n const proto = Object.getPrototypeOf(self);\n if (!proto) return self;\n for (const [object, key] of getAllProperties(proto)) {\n if (key === 'constructor' || !filter(key)) continue;\n const descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n if (descriptor && typeof descriptor.value === 'function' && typeof key === 'string') {\n const current = (self as Record<string, unknown>)[key];\n if (typeof current === 'function') {\n (self as Record<MethodNames<T>, unknown>)[key as MethodNames<T>] = (\n current as (...a: unknown[]) => unknown\n ).bind(self);\n }\n }\n }\n return self;\n}\n\nfunction lerp(p1: number, p2: number, t: number): number {\n return p1 + (p2 - p1) * t;\n}\n\nfunction map(num: number, min1: number, max1: number, min2: number, max2: number, round = false): number {\n const num1 = (num - min1) / (max1 - min1);\n const num2 = num1 * (max2 - min2) + min2;\n return round ? Math.round(num2) : num2;\n}\n\nclass Media {\n gl: GL;\n geometry: OGLPlane;\n scene: OGLTransform;\n screen: ScreenSize;\n viewport: ViewportSize;\n image: string;\n length: number;\n index: number;\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n\n program!: OGLProgram;\n plane!: OGLMesh;\n extra = 0;\n padding = 0;\n height = 0;\n heightTotal = 0;\n y = 0;\n\n constructor({\n gl,\n geometry,\n scene,\n screen,\n viewport,\n image,\n length,\n index,\n planeWidth,\n planeHeight,\n distortion\n }: MediaParams) {\n this.gl = gl;\n this.geometry = geometry;\n this.scene = scene;\n this.screen = screen;\n this.viewport = viewport;\n this.image = image;\n this.length = length;\n this.index = index;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n\n this.createShader();\n this.createMesh();\n this.onResize();\n }\n\n createShader() {\n const texture = new Texture(this.gl, { generateMipmaps: false });\n this.program = new Program(this.gl, {\n depthTest: false,\n depthWrite: false,\n fragment: fragmentShader,\n vertex: vertexShader,\n uniforms: {\n tMap: { value: texture },\n uPosition: { value: 0 },\n uPlaneSize: { value: [0, 0] },\n uImageSize: { value: [0, 0] },\n uSpeed: { value: 0 },\n rotationAxis: { value: [0, 1, 0] },\n distortionAxis: { value: [1, 1, 0] },\n uDistortion: { value: this.distortion },\n uViewportSize: { value: [this.viewport.width, this.viewport.height] },\n uTime: { value: 0 }\n },\n cullFace: false\n });\n\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = this.image;\n img.onload = () => {\n texture.image = img;\n this.program.uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n };\n }\n\n createMesh() {\n this.plane = new Mesh(this.gl, {\n geometry: this.geometry,\n program: this.program\n });\n this.plane.setParent(this.scene);\n }\n\n setScale() {\n this.plane.scale.x = (this.viewport.width * this.planeWidth) / this.screen.width;\n this.plane.scale.y = (this.viewport.height * this.planeHeight) / this.screen.height;\n this.plane.position.x = 0;\n this.program.uniforms.uPlaneSize.value = [this.plane.scale.x, this.plane.scale.y];\n }\n\n onResize({ screen, viewport }: { screen?: ScreenSize; viewport?: ViewportSize } = {}) {\n if (screen) this.screen = screen;\n if (viewport) {\n this.viewport = viewport;\n this.program.uniforms.uViewportSize.value = [viewport.width, viewport.height];\n }\n this.setScale();\n\n this.padding = 5;\n this.height = this.plane.scale.y + this.padding;\n this.heightTotal = this.height * this.length;\n this.y = -this.heightTotal / 2 + (this.index + 0.5) * this.height;\n }\n\n update(scroll: ScrollState) {\n this.plane.position.y = this.y - scroll.current - this.extra;\n const position = map(this.plane.position.y, -this.viewport.height, this.viewport.height, 5, 15);\n\n this.program.uniforms.uPosition.value = position;\n this.program.uniforms.uTime.value += 0.04;\n this.program.uniforms.uSpeed.value = scroll.current;\n\n const planeHeight = this.plane.scale.y;\n const viewportHeight = this.viewport.height;\n const topEdge = this.plane.position.y + planeHeight / 2;\n const bottomEdge = this.plane.position.y - planeHeight / 2;\n\n if (topEdge < -viewportHeight / 2) {\n this.extra -= this.heightTotal;\n } else if (bottomEdge > viewportHeight / 2) {\n this.extra += this.heightTotal;\n }\n }\n}\n\nclass Canvas {\n container: HTMLElement;\n canvas: HTMLCanvasElement;\n items: string[];\n planeWidth: number;\n planeHeight: number;\n distortion: number;\n scroll: ScrollState;\n cameraFov: number;\n cameraZ: number;\n\n renderer!: Renderer;\n gl!: GL;\n camera!: Camera;\n scene!: OGLTransform;\n planeGeometry!: OGLPlane;\n medias!: Media[];\n screen!: ScreenSize;\n viewport!: ViewportSize;\n isDown = false;\n start = 0;\n loaded = 0;\n\n constructor({\n container,\n canvas,\n items,\n planeWidth,\n planeHeight,\n distortion,\n scrollEase,\n cameraFov,\n cameraZ\n }: CanvasParams) {\n this.container = container;\n this.canvas = canvas;\n this.items = items;\n this.planeWidth = planeWidth;\n this.planeHeight = planeHeight;\n this.distortion = distortion;\n this.scroll = {\n ease: scrollEase,\n current: 0,\n target: 0,\n last: 0\n };\n this.cameraFov = cameraFov;\n this.cameraZ = cameraZ;\n\n AutoBind(this);\n this.createRenderer();\n this.createCamera();\n this.createScene();\n this.onResize();\n this.createGeometry();\n this.createMedias();\n this.initializeScrollPosition();\n this.update();\n this.addEventListeners();\n this.createPreloader();\n }\n\n createRenderer() {\n this.renderer = new Renderer({\n canvas: this.canvas,\n alpha: true,\n antialias: true,\n dpr: Math.min(window.devicePixelRatio, 2)\n });\n this.gl = this.renderer.gl;\n }\n\n createCamera() {\n this.camera = new Camera(this.gl);\n this.camera.fov = this.cameraFov;\n this.camera.position.z = this.cameraZ;\n }\n\n createScene() {\n this.scene = new Transform();\n }\n\n createGeometry() {\n this.planeGeometry = new Plane(this.gl, {\n heightSegments: 1,\n widthSegments: 100\n });\n }\n\n createMedias() {\n this.medias = this.items.map(\n (image, index) =>\n new Media({\n gl: this.gl,\n geometry: this.planeGeometry,\n scene: this.scene,\n screen: this.screen,\n viewport: this.viewport,\n image,\n length: this.items.length,\n index,\n planeWidth: this.planeWidth,\n planeHeight: this.planeHeight,\n distortion: this.distortion\n })\n );\n }\n\n initializeScrollPosition() {\n if (this.medias && this.medias.length > 0) {\n const centerIndex = Math.floor(this.medias.length / 2);\n const centerMedia = this.medias[centerIndex];\n this.scroll.current = centerMedia.y;\n this.scroll.target = centerMedia.y;\n }\n }\n\n createPreloader() {\n this.loaded = 0;\n this.items.forEach(src => {\n const image = new Image();\n image.crossOrigin = 'anonymous';\n image.src = src;\n image.onload = () => {\n if (++this.loaded === this.items.length) {\n document.documentElement.classList.remove('loading');\n document.documentElement.classList.add('loaded');\n }\n };\n });\n }\n\n onResize() {\n const rect = this.container.getBoundingClientRect();\n this.screen = { width: rect.width, height: rect.height };\n this.renderer.setSize(this.screen.width, this.screen.height);\n\n this.camera.perspective({\n aspect: this.gl.canvas.width / this.gl.canvas.height\n });\n\n const fov = (this.camera.fov * Math.PI) / 180;\n const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n const width = height * this.camera.aspect;\n this.viewport = { width, height };\n\n this.medias?.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n }\n\n onTouchDown(e: MouseEvent | TouchEvent) {\n this.isDown = true;\n this.scroll.position = this.scroll.current;\n this.start = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY;\n }\n\n onTouchMove(e: MouseEvent | TouchEvent) {\n if (!this.isDown || !this.scroll.position) return;\n const y = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY;\n const distance = (this.start - y) * 0.1;\n this.scroll.target = this.scroll.position + distance;\n }\n\n onTouchUp() {\n this.isDown = false;\n }\n\n onWheel(e: WheelEvent) {\n this.scroll.target += e.deltaY * 0.005;\n }\n\n update() {\n this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n this.medias?.forEach(media => media.update(this.scroll));\n this.renderer.render({ scene: this.scene, camera: this.camera });\n this.scroll.last = this.scroll.current;\n requestAnimationFrame(this.update);\n }\n\n addEventListeners() {\n window.addEventListener('resize', this.onResize);\n window.addEventListener('wheel', this.onWheel);\n window.addEventListener('mousedown', this.onTouchDown);\n window.addEventListener('mousemove', this.onTouchMove);\n window.addEventListener('mouseup', this.onTouchUp);\n window.addEventListener('touchstart', this.onTouchDown as EventListener);\n window.addEventListener('touchmove', this.onTouchMove as EventListener);\n window.addEventListener('touchend', this.onTouchUp as EventListener);\n }\n\n destroy() {\n window.removeEventListener('resize', this.onResize);\n window.removeEventListener('wheel', this.onWheel);\n window.removeEventListener('mousedown', this.onTouchDown);\n window.removeEventListener('mousemove', this.onTouchMove);\n window.removeEventListener('mouseup', this.onTouchUp);\n window.removeEventListener('touchstart', this.onTouchDown as EventListener);\n window.removeEventListener('touchmove', this.onTouchMove as EventListener);\n window.removeEventListener('touchend', this.onTouchUp as EventListener);\n }\n}\n\nexport interface FlyingPostersProps {\n items?: string[];\n planeWidth?: number;\n planeHeight?: number;\n distortion?: number;\n scrollEase?: number;\n cameraFov?: number;\n cameraZ?: number;\n className?: string;\n}\n\nexport { Canvas, Media };\n</script>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\n\nconst props = withDefaults(defineProps<FlyingPostersProps>(), {\n items: () => [],\n planeWidth: 320,\n planeHeight: 320,\n distortion: 3,\n scrollEase: 0.01,\n cameraFov: 45,\n cameraZ: 20,\n className: ''\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\nconst instanceRef = ref<Canvas | null>(null);\n\nconst initCanvas = () => {\n if (!containerRef.value || !canvasRef.value) return;\n\n instanceRef.value = new Canvas({\n container: containerRef.value,\n canvas: canvasRef.value,\n items: props.items,\n planeWidth: props.planeWidth,\n planeHeight: props.planeHeight,\n distortion: props.distortion,\n scrollEase: props.scrollEase,\n cameraFov: props.cameraFov,\n cameraZ: props.cameraZ\n });\n};\n\nconst destroyCanvas = () => {\n if (instanceRef.value) {\n instanceRef.value.destroy();\n instanceRef.value = null;\n }\n};\n\nconst handleWheel = (e: WheelEvent) => {\n e.preventDefault();\n if (instanceRef.value) {\n instanceRef.value.onWheel(e);\n }\n};\n\nconst handleTouchMove = (e: TouchEvent) => {\n e.preventDefault();\n};\n\nwatch(\n () => [\n props.items,\n props.planeWidth,\n props.planeHeight,\n props.distortion,\n props.scrollEase,\n props.cameraFov,\n props.cameraZ\n ],\n () => {\n destroyCanvas();\n initCanvas();\n },\n { deep: true }\n);\n\nonMounted(() => {\n initCanvas();\n\n if (canvasRef.value) {\n const canvasEl = canvasRef.value;\n canvasEl.addEventListener('wheel', handleWheel, { passive: false });\n canvasEl.addEventListener('touchmove', handleTouchMove, { passive: false });\n }\n});\n\nonUnmounted(() => {\n destroyCanvas();\n\n if (canvasRef.value) {\n const canvasEl = canvasRef.value;\n canvasEl.removeEventListener('wheel', handleWheel);\n canvasEl.removeEventListener('touchmove', handleTouchMove);\n }\n});\n</script>\n","path":"FlyingPosters/FlyingPosters.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Components"]}