WEBLEB
Accueil
Éditeur
Connexion
Pro
Français
English
Français
Español
Português
Deutsch
Italiano
हिंदी
Photo Déchirure
896
Andev.web
Ouvrir dans l'éditeur
Publiez votre code
Precisa de um site?
8 September 2024
Interface utilisateur de l'éditeur de photos
13 November 2024
Effet de cadre photo CSS 3D sur les images
HTML
Copy
Andev Web
CSS
Copy
html, body { width: 100%; min-height: 100%; margin: 0; background: #17181D; padding: 0; position: relative; overflow: hidden; } body { position: relative; -ms-scroll-chaining: none; overscroll-behavior: contain; } body.zoomed, body.loading { overflow: hidden; } .page { z-index: 3; position: relative; margin: 0; } .page.under { z-index: 1; position: absolute; top: 0; left: 0; } canvas { position: fixed; top: 0; left: 0; z-index: 2; cursor: -webkit-grab; cursor: grab; } .info { position: fixed; top: 0; left: 0; display: flex; justify-content: center; align-items: center; z-index: 200; pointer-events: none; right: 0; bottom: 0; } .info svg { opacity: 0; width: clamp(50px, 5vw, 120px); height: clamp(50px, 5vw, 120px); transform: translateX(150%); }
JS
Copy
console.clear(); import * as THREE from 'https://cdn.skypack.dev/three@0.125.0'; import gsap from "https://cdn.skypack.dev/gsap@3.5.1"; const BackgroundGradient = (colorA, colorB) => { var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry(2, 2, 1, 1), new THREE.ShaderMaterial({ uniforms: { uColorA: { value: new THREE.Color(colorA) }, uColorB: { value: new THREE.Color(colorB) } }, vertexShader: ` varying vec2 vUv; void main(){ vUv = uv; float depth = -1.; //or maybe 1. you can experiment gl_Position = vec4(position.xy, depth, 1.); } `, fragmentShader: ` varying vec2 vUv; uniform vec3 uColorA; uniform vec3 uColorB; void main(){ gl_FragColor = vec4( mix( uColorA, uColorB, vec3(vUv.y)), 1. ); } ` })); mesh.material.depthWrite = false; mesh.renderOrder = -99999; return mesh; }; class Stage { constructor(domCanvasElement, topColor, bottomColor) { this.canvas = domCanvasElement; this.scene = new THREE.Scene(); this.sizes = { width: 0, height: 0 }; /** * Background Gradient */ this.background = BackgroundGradient(bottomColor, topColor); this.scene.add(this.background); /** * Camera */ this.camera = new THREE.PerspectiveCamera(30, this.sizes.width / this.sizes.height, 0.1, 100); this.camera.position.set(0, 0, 6); this.scene.add(this.camera); /** * Camera Group */ this.cameraGroup = new THREE.Group(); this.scene.add(this.cameraGroup); /** * Renderer */ const renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: window.devicePixelRatio === 1 ? true : false }); renderer.physicallyCorrectLights = true; renderer.outputEncoding = THREE.sRGBEncoding; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1; this.renderer = renderer; window.addEventListener('resize', () => {this.onResize();}); this.onResize(); } onResize() { // Update sizes this.sizes.width = window.innerWidth; this.sizes.height = window.innerHeight; if (this.sizes.width < 800) this.camera.position.z = 10;else this.camera.position.z = 6; // Update camera this.camera.aspect = this.sizes.width / this.sizes.height; this.camera.updateProjectionMatrix(); // Update renderer this.renderer.setSize(this.sizes.width, this.sizes.height); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); } add(item) { this.scene.add(item); } cameraAdd(item) { this.cameraGroup.add(item); } render(elapsedTime) { this.cameraGroup.position.copy(this.camera.position); this.cameraGroup.rotation.copy(this.camera.rotation); this.renderer.render(this.scene, this.camera); }} class Loader { constructor(color = 'black', shadow = "white", size = 1) { this.mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry(2, 2, 1, 1), new THREE.ShaderMaterial({ uniforms: { uColor: { value: new THREE.Color(color) }, uColorShadow: { value: new THREE.Color(shadow) }, uProgress: { value: 0 }, uAlpha: { value: 1 }, uNoiseSize: { value: size } }, vertexShader: ` void main(){ gl_Position = vec4(position.xy, 0.5, 1.); } `, fragmentShader: ` uniform vec3 uColor; uniform float uAlpha; void main () { gl_FragColor = vec4(uColor, uAlpha); } `, transparent: true, depthTest: false })); } get progress() { return this.mesh.material.uniforms.uProgress.value; } set progress(newValue) { this.mesh.material.uniforms.uProgress.value = newValue; } get alpha() { return this.mesh.material.uniforms.uAlpha.value; } set alpha(newValue) { this.mesh.material.uniforms.uAlpha.value = newValue; } get noiseSize() { return this.mesh.material.uniforms.uNoiseSize.value; } set noiseSize(newValue) { this.mesh.material.uniforms.uNoiseSize.value = newValue; }} const ripVertexShader = ` uniform float uTearAmount; uniform float uTearWidth; uniform float uTearXAngle; uniform float uTearYAngle; uniform float uTearZAngle; uniform float uTearXOffset; uniform float uXDirection; uniform float uRipSide; uniform float uRipSeed; varying vec2 vUv; varying float vAmount; mat4 rotationX( in float angle ) { return mat4( 1.0, 0, 0, 0, 0, cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1); } mat4 rotationY( in float angle ) { return mat4( cos(angle), 0, sin(angle), 0, 0, 1.0, 0, 0, -sin(angle), 0, cos(angle), 0, 0, 0, 0, 1); } mat4 rotationZ( in float angle ) { return mat4( cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } void main(){ float ripAmount = 0.0; float yAmount = max(0.0, (uTearAmount - (1.0 - uv.y))); float zRotate = uTearZAngle * yAmount; float xRotate = uTearXAngle * yAmount; float yRotate = uTearYAngle * yAmount; vec3 rotation = vec3(xRotate* yAmount, yRotate* yAmount, zRotate* yAmount); float halfHeight = float(HEIGHT) * 0.5; float halfWidth = (float(WIDTH) - uTearWidth * 0.5) * 0.5; vec4 vertex = vec4(position.x + (halfWidth * uXDirection) - halfWidth, position.y + halfHeight, position.z, 1.0); vertex = vertex * rotationY(rotation.y ) * rotationX(rotation.x ) * rotationZ(rotation.z ); vertex.x += uTearXOffset * yAmount + ripAmount + halfWidth ; vertex.y -= halfHeight; vec4 modelPosition = modelMatrix * vertex; vec4 viewPosition = viewMatrix * modelPosition; vec4 projectedPosition = projectionMatrix * viewPosition; gl_Position = projectedPosition; vUv = uv; vAmount = yAmount; } `; const ripFragmentShader = ` uniform sampler2D uMap; uniform sampler2D uRip; uniform sampler2D uBorder; uniform vec3 uShadeColor; uniform float uUvOffset; uniform float uRipSide; uniform float uTearXAngle; uniform float uShadeAmount; uniform float uTearWidth; uniform float uWhiteThreshold; uniform float uTearOffset; varying vec2 vUv; varying float vAmount; void main () { bool rightSide = uRipSide == 1.0; float ripAmount = -1.0; float width = float(WIDTH); float widthOverlap = (uTearWidth * 0.5) + width; bool frontSheet = uTearXAngle > 0.0; float xScale = widthOverlap / float(FULL_WIDTH); vec2 uvOffset = vec2(vUv.x * xScale + uUvOffset, vUv.y); vec4 textureColor = texture2D(uMap, uvOffset); vec4 borderColor = texture2D(uBorder, uvOffset); if(borderColor.r > 0.0) textureColor = vec4(vec3(0.95), 1.0); float ripRange = uTearWidth / widthOverlap; float ripStart = rightSide ? 0.0 : 1.0 - ripRange; float alpha = 1.0; float ripX = (vUv.x - ripStart) / ripRange; float ripY = vUv.y * 0.5 + (0.5 * uTearOffset); vec4 ripCut = texture2D(uRip, vec2(ripX, ripY)); vec4 ripColor = texture2D(uRip, vec2(ripX * 0.9, ripY - 0.02)); float whiteness = dot(vec4(1.0), ripCut) / 4.0; if (!rightSide && whiteness <= uWhiteThreshold) { whiteness = dot(vec4(1.0), ripColor) / 4.0; if(whiteness >= uWhiteThreshold) textureColor = ripColor; else alpha = 0.0; } if (rightSide && whiteness >= uWhiteThreshold) alpha = 0.0; gl_FragColor = mix(vec4(textureColor.rgb, alpha), vec4(uShadeColor, alpha), vAmount * uShadeAmount); } `; class Photo { constructor(textures, destoryCallback) { this.destroyCallback = destoryCallback; this.photoTexture = textures.photo; this.borderTexture = textures.border; this.ripTexture = textures.rip; this.interactive = false; this.group = new THREE.Group(); this.group.rotation.z = (Math.random() * 2 - 1) * Math.PI; this.group.position.y = 10; setTimeout(() => { this.interactive = true; }, 400); const introAnimation = gsap.timeline({ delay: 0.3, defaults: { duration: 0.8, ease: 'power3.out' } }); introAnimation.to(this.group.rotation, { z: 0 }, 0); introAnimation.to(this.group.position, { y: 0 }, 0); const width = 3; const tearWidth = 0.4; this.sheetSettings = { widthSegments: 30, heightSegments: 50, tearOffset: Math.random(), width: width, height: 2, tearAmount: 0, tearWidth: tearWidth, ripWhiteThreshold: 0.7, left: { uvOffset: 0, ripSide: 0, tearXAngle: -0.01, tearYAngle: -0.1, tearZAngle: 0.05, tearXOffset: 0, direction: -1, shadeColor: new THREE.Color('white'), shadeAmount: 0.2 }, right: { uvOffset: (width - tearWidth) / width * 0.5, ripSide: 1, tearXAngle: 0.2, tearYAngle: 0.1, tearZAngle: -0.1, tearXOffset: 0, direction: 1, shadeColor: new THREE.Color('black'), shadeAmount: 0.4 } }; this.sides = [ { id: 'left', mesh: null, material: null }, { id: 'right', mesh: null, material: null }]; this.sheetPlane = new THREE.PlaneBufferGeometry(this.sheetSettings.width / 2 + this.sheetSettings.tearWidth / 2, this.sheetSettings.height, this.sheetSettings.widthSegments, this.sheetSettings.heightSegments); this.sides.forEach((side) => { side.material = this.getRipMaterial(side.id); side.mesh = new THREE.Mesh(this.sheetPlane, side.material); if (this.sheetSettings[side.id].tearXAngle > 0) { side.mesh.position.z += 0.0001; } this.group.add(side.mesh); }); } getRipMaterial(side) { const material = new THREE.ShaderMaterial({ defines: { HEIGHT: this.sheetSettings.height, WIDTH: this.sheetSettings.width / 2, FULL_WIDTH: this.sheetSettings.width, HEIGHT_SEGMENTS: this.sheetSettings.heightSegments, WIDTH_SEGMENTS: this.sheetSettings.widthSegments }, uniforms: { uMap: { value: this.photoTexture }, uRip: { value: this.ripTexture }, uBorder: { value: this.borderTexture }, uRipSide: { value: this.sheetSettings[side].ripSide }, uTearWidth: { value: this.sheetSettings.tearWidth }, uWhiteThreshold: { value: this.sheetSettings.ripWhiteThreshold }, uTearAmount: { value: this.sheetSettings.tearAmount }, uTearOffset: { value: this.sheetSettings.tearOffset }, uUvOffset: { value: this.sheetSettings[side].uvOffset }, uTearXAngle: { value: this.sheetSettings[side].tearXAngle }, uTearYAngle: { value: this.sheetSettings[side].tearYAngle }, uTearZAngle: { value: this.sheetSettings[side].tearZAngle }, uTearXOffset: { value: this.sheetSettings[side].tearXOffset }, uXDirection: { value: this.sheetSettings[side].direction }, uShadeColor: { value: this.sheetSettings[side].shadeColor }, uShadeAmount: { value: this.sheetSettings[side].shadeAmount } }, transparent: true, vertexShader: ripVertexShader, fragmentShader: ripFragmentShader }); return material; } shouldCompleteRip() { return this.sheetSettings.tearAmount >= 1.5; } updateUniforms() { if (this.interactive && this.shouldCompleteRip()) { this.remove(); } else { if (this.sheetSettings.tearAmount === 0) this.sheetSettings.tearOffset = Math.random(); this.sides.forEach((side) => { const uniforms = side.mesh.material.uniforms; uniforms.uTearAmount.value = this.sheetSettings.tearAmount; uniforms.uTearOffset.value = this.sheetSettings.tearOffset; uniforms.uUvOffset.value = this.sheetSettings[side.id].uvOffset; uniforms.uTearXAngle.value = this.sheetSettings[side.id].tearXAngle; uniforms.uTearYAngle.value = this.sheetSettings[side.id].tearYAngle; uniforms.uTearZAngle.value = this.sheetSettings[side.id].tearZAngle; uniforms.uTearXOffset.value = this.sheetSettings[side.id].tearXOffset; uniforms.uXDirection.value = this.sheetSettings[side.id].direction; uniforms.uShadeColor.value = this.sheetSettings[side.id].shadeColor; uniforms.uShadeAmount.value = this.sheetSettings[side.id].shadeAmount; uniforms.uWhiteThreshold.value = this.sheetSettings.ripWhiteThreshold; }); } } completeRip() { if (this.sheetSettings.tearAmount >= 1.15) this.remove();else this.reset(); } reset() { gsap.to(this.sheetSettings, { tearAmount: 0, onUpdate: () => this.updateUniforms() }); } remove() { this.interactive = false; this.destroyCallback(); const removeAnimation = gsap.timeline({ defaults: { duration: 1, ease: 'power2.in' }, onComplete: () => this.destroyMe() }); removeAnimation.to(this.sheetSettings, { tearAmount: 1.5 + Math.random() * 1.5, ease: 'power2.out', onUpdate: () => this.updateUniforms() }); removeAnimation.to(this.group.position, { z: 1 }, 0); this.sides.forEach((side) => { removeAnimation.to(side.mesh.position, { y: -3 + Math.random() * -3, x: (2 + Math.random() * 3) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0); removeAnimation.to(side.mesh.rotation, { z: (-2 + Math.random() * -3) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0); }); } destroyMe() { this.sheetPlane.dispose(); this.sides.forEach((side) => { side.material.dispose(); this.group.remove(side.mesh); }); }} let interacted = false; /** * Stage */ const canvas = document.querySelector('canvas.webgl'); const stage = new Stage(canvas, '#F1EBE4', '#D5C3AE'); /** * Loaders */ const loaderScreen = new Loader('black'); stage.add(loaderScreen.mesh); const loadingManager = new THREE.LoadingManager( () => { const tl = gsap.timeline(); tl.to(loaderScreen, { progress: 1, alpha: 0, duration: .5, ease: 'power4.inOut' }, 0); init(); }); const textureLoader = new THREE.TextureLoader(loadingManager); /** * Lights */ let envLight = new THREE.AmbientLight({ color: 'white', intensity: 6 }); stage.add(envLight); let pointLight = new THREE.PointLight({ color: 'white', intensity: 20 }); pointLight.position.z = -1; stage.add(pointLight); /** * Materials */ const images = [ { texture: textureLoader.load('https://images.unsplash.com/photo-1544027993-37dbfe43562a?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'), colors: [[192, 208, 220], [217, 190, 174]] }, { texture: textureLoader.load('https://images.unsplash.com/photo-1521839745427-1f25030b3a46?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'), colors: [[168, 163, 150], [218, 180, 141]] }]; let currentImage = -1; const textureRip = textureLoader.load('https://assets.codepen.io/557388/rip.jpg'); const textureBorder = textureLoader.load('https://assets.codepen.io/557388/border.png'); /** * Paper */ const photos = []; const mouseStart = new THREE.Vector2(); let mouseDown = false; const extraImages = [ { file: 'https://images.unsplash.com/photo-1554384032-56ff486b6cd4?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', colors: [[208, 229, 224], [209, 209, 220]] }, { file: 'https://images.unsplash.com/photo-1520261456177-6ec5aac5dbd2?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', colors: [[191, 192, 184], [217, 200, 170]] }, { file: 'https://assets.codepen.io/557388/photo-6.jpg', colors: [[217, 226, 233], [216, 220, 203]] }, { file: 'https://assets.codepen.io/557388/photo-7.jpg', colors: [[206, 221, 226], [219, 213, 222]] }, { file: 'https://assets.codepen.io/557388/photo-5.jpg', colors: [[199, 199, 210], [218, 203, 195]] }]; const postInitTextureLoader = new THREE.TextureLoader(); const getMousePos = (x, y) => { return { x: x / stage.sizes.width * 2 - 1, y: -(y / stage.sizes.height) * 2 + 1 }; }; const down = (x, y) => { if (photos.length && photos[0].interactive) { interacted = true; hideHand(); let pos = getMousePos(x, y); mouseStart.x = pos.x; mouseStart.y = pos.y; mouseDown = true; } }; const move = (x, y) => { if (mouseDown && photos.length && photos[0].interactive) { let pos = getMousePos(x, y); let distanceY = mouseStart.y - pos.y; photos[0].sheetSettings.tearAmount = Math.max(2 * distanceY, 0); photos[0].updateUniforms(); } }; const up = () => { if (mouseDown && photos.length && photos[0].interactive) { mouseDown = false; photos[0].completeRip(); } }; const loadExtraPhoto = () => { const nextImage = extraImages.shift(); images.push({ texture: postInitTextureLoader.load(nextImage.file), colors: nextImage.colors }); }; const addNewPhoto = () => { currentImage++; if (currentImage >= images.length) currentImage = 0; if (images.length - currentImage < 2 && extraImages.length) loadExtraPhoto(); mouseDown = false; const nextImage = images[currentImage]; let photo = new Photo( { photo: nextImage.texture, rip: textureRip, border: textureBorder }, () => addNewPhoto()); photos.unshift(photo); stage.add(photo.group); gsap.to(stage.background.material.uniforms.uColorB.value, { r: nextImage.colors[0][0] / 255, g: nextImage.colors[0][1] / 255, b: nextImage.colors[0][2] / 255, ease: 'power4.inOut', duration: 1 }); gsap.to(stage.background.material.uniforms.uColorA.value, { r: nextImage.colors[1][0] / 255, g: nextImage.colors[1][1] / 255, b: nextImage.colors[1][2] / 255, ease: 'power4.inOut', duration: 1 }); }; const init = () => { addNewPhoto(); window.addEventListener('mousedown', event => down(event.clientX, event.clientY)); window.addEventListener('touchstart', event => down(event.touches[0].clientX, event.touches[0].clientY)); window.addEventListener('mousemove', event => move(event.clientX, event.clientY)); window.addEventListener('touchmove', event => move(event.touches[0].clientX, event.touches[0].clientY)); window.addEventListener('mouseup', up); window.addEventListener('touchend', up); }; /** * Tick */ const clock = new THREE.Clock(); const tick = () => { const elapsedTime = clock.getElapsedTime(); stage.render(elapsedTime); window.requestAnimationFrame(tick); }; tick(); const hand = document.getElementById('hand'); const downDuration = 2; const upDuration = 0.7; const hintAnimation = gsap.timeline({ repeat: -1, defaults: { duration: downDuration, ease: 'power4.inOut' } }); hintAnimation.fromTo(hand, { y: '-100%' }, { y: '100%' }); hintAnimation.to(hand, { y: '-100%', duration: upDuration, motionPath: [{ rotate: '-10' }, { rotate: '0' }] }, downDuration); hintAnimation.to(hand, { rotate: '-25', scale: 1.1, duration: upDuration * 0.5, ease: 'power4.in' }, downDuration); hintAnimation.to(hand, { rotate: '0', scale: 1, duration: upDuration * 0.5, ease: 'power4.out' }, downDuration + upDuration * 0.5); hintAnimation.pause(); const showHand = () => { if (!interacted) { hintAnimation.play(); gsap.to(hand, { opacity: 1 }); } }; const hideHand = () => { gsap.to(hand, { opacity: 0, onComplete: () => hintAnimation.pause() }); }; setTimeout(() => showHand(), 5000);