Original tweet https://twitter.com/romanus_march/status/1250068202257776643
Created
November 23, 2020 03:19
-
-
Save ilteris/b4ce0b640765a323ca1074976b4e2b81 to your computer and use it in GitHub Desktop.
Rubber Mesh Swipe Transition
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id='root'></div> | |
<div id='message'>PULL & RELEASE IMAGE UP OR DOWN</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { useState, Component } = React; | |
const { useSpring } = ReactSpring; | |
const { useDrag } = ReactUseGesture; | |
function lerp(v0, v1, t) { | |
return v0*(1-t)+v1*t | |
} | |
const frag = ` | |
uniform float time; | |
uniform float progress; | |
uniform float width; | |
uniform float scaleX; | |
uniform float scaleY; | |
uniform float transition; | |
uniform float radius; | |
uniform float intensity; | |
uniform sampler2D texture1; | |
uniform sampler2D texture2; | |
uniform sampler2D displacement; | |
uniform vec4 resolution; | |
varying vec2 vUv; | |
void main() { | |
vec2 newUV = (vUv - vec2(0.5))*resolution.zw + vec2(0.5); | |
vec4 d1 = texture2D(texture1, newUV); | |
vec4 d2 = texture2D(texture2, newUV); | |
float displace1 = (d1.r + d1.g + d1.b)*0.33; | |
float displace2 = (d2.r + d2.g + d2.b)*0.33; | |
vec4 t1 = texture2D(texture1, vec2(newUV.x, newUV.y + progress * (displace2 * intensity))); | |
vec4 t2 = texture2D(texture2, vec2(newUV.x, newUV.y + (1.0 - progress) * (displace1 * intensity))); | |
gl_FragColor = mix(t1, t2, progress); | |
} | |
`; | |
const vert = ` | |
uniform float scale; | |
uniform float shift; | |
uniform float shiftx; | |
varying vec2 vUv; | |
void main() { | |
vec3 pos = position; | |
pos.y = pos.y + ((sin(uv.x * 3.1415926535897932384626433832795) * shift * 5.0) * 0.125); | |
pos.x = pos.x + ((sin(uv.y * 3.1415926535897932384626433832795) * shiftx * 5.0) * 0.125); | |
vUv = uv; | |
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.); | |
} | |
`; | |
class Noise extends Component { | |
uniforms = { | |
time: { type: "f", value: 0 }, | |
progress: { type: "f", value: 0 }, | |
border: { type: "f", value: 0 }, | |
intensity: { type: "f", value: 20 }, | |
scaleX: { type: "f", value: 40 }, | |
scaleY: { type: "f", value: 40 }, | |
transition: { type: "f", value: 40 }, | |
swipe: { type: "f", value: 0 }, | |
width: { type: "f", value: 400 }, | |
radius: { type: "f", value: 0 }, | |
scale: { value: 0 }, | |
shift: { value: 0 }, | |
shiftx: { value: 0 }, | |
intensity: { value: 0.3, type: "f" }, | |
resolution: { type: "v4", value: new THREE.Vector4() }, | |
}; | |
time = 0; | |
images = [ | |
"https://images.unsplash.com/photo-1514423140760-4479e12ca0cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2550&q=80", | |
"https://images.unsplash.com/photo-1470217407524-b1e77afc6ec5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2546&q=80", | |
"https://images.unsplash.com/photo-1505598872760-6090aa9ed603?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80", | |
]; | |
textures = []; | |
isRunning = false; | |
duration = 2; | |
easing = "easeInOut"; | |
current = 0; | |
componentDidMount() { | |
this.initRender(); | |
this.setTextures(() => { | |
this.addLighting(); | |
this.addPlane(); | |
this.handleResize(); | |
this.animateRender(); | |
}); | |
window.addEventListener("resize", this.handleResize, false); | |
} | |
componentWillUnmount() { | |
window.removeEventListener("resize", this.handleResize, false); | |
} | |
componentDidUpdate(prevProps) { | |
if (this.props.active !== prevProps.active) { | |
setTimeout(() => { | |
this.updateTexture(); | |
}, 400); | |
} | |
} | |
initRender() { | |
this.width = window.innerWidth; | |
this.height = window.innerHeight; | |
this.scene = new THREE.Scene(); | |
this.camera = new THREE.PerspectiveCamera( | |
75, | |
400 / window.innerHeight, | |
0.1, | |
1000 | |
); | |
this.camera.position.z = 5; | |
this.vertex = `varying vec2 vUv;void main() {vUv = uv;gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );}`; | |
this.renderer = new THREE.WebGLRenderer({ alpha: true }); | |
this.renderer.shadowMap.enabled = true; | |
this.renderer.antialias = true; | |
this.renderer.setPixelRatio(window.devicePixelRatio); | |
this.renderer.setSize(this.width, window.innerHeight); | |
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
this.renderer.interpolateneMapping = THREE.ACESFilmicToneMapping; | |
this.renderer.outputEncoding = THREE.sRGBEncoding; | |
this.renderer.setClearAlpha(0); | |
document.getElementById("world").appendChild(this.renderer.domElement); | |
this.renderer.render(this.scene, this.camera); | |
} | |
addLighting = () => { | |
const hemislight = new THREE.HemisphereLight(); | |
hemislight.intensity = 0.2; | |
this.scene.add(hemislight); | |
const pointlight = new THREE.PointLight(); | |
pointlight.distance = 1000; | |
pointlight.intensity = 0.7; | |
pointlight.position.set(30, 70, 20); | |
this.scene.add(pointlight); | |
}; | |
addPlane = () => { | |
this.geometry = new THREE.PlaneGeometry(2, 3, 10, 10); | |
this.material = new THREE.ShaderMaterial({ | |
vertexShader: vert, | |
fragmentShader: frag, | |
uniforms: { | |
...this.uniforms, | |
texture1: { type: "f", value: this.textures[0] }, | |
texture2: { type: "f", value: this.textures[1] }, | |
displacement: { | |
type: "f", | |
value: new THREE.TextureLoader().load( | |
"https://images.unsplash.com/photo-1470217407524-b1e77afc6ec5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2546&q=80" | |
), | |
}, | |
}, | |
}); | |
this.material.uniforms.resolution.value.x = this.width; | |
this.material.uniforms.resolution.value.y = this.height; | |
this.plane = new THREE.Mesh(this.geometry, this.material); | |
this.scene.add(this.plane); | |
}; | |
handleResize = () => { | |
this.width = window.innerWidth; | |
this.height = window.innerHeight; | |
this.camera.aspect = this.width / this.height; | |
const dist = this.camera.position.z; | |
const height = 1; | |
let a2; | |
this.camera.fov = 2 * (180 / Math.PI) * Math.atan(height / (2 * dist)); | |
this.imageAspect = | |
this.textures[this.current].image.height / | |
this.textures[this.current].image.width; | |
if (this.width < 700) { | |
if (this.height / this.width > this.imageAspect) { | |
a2 = 1; | |
} else { | |
a2 = this.height / 7000 / (this.width / 1800) / this.imageAspect; | |
} | |
this.plane.scale.x = this.width / 1800; | |
this.plane.scale.y = window.innerHeight / 7000; | |
} else if (this.height < 600) { | |
if (this.height / this.width > this.imageAspect) { | |
a2 = 1; | |
} else { | |
a2 = this.height / this.width / this.imageAspect; | |
} | |
this.plane.scale.x = this.width / 2000; | |
this.plane.scale.y = window.innerHeight / 2500; | |
} else { | |
if (this.height / this.width > this.imageAspect) { | |
a2 = 1; | |
} else { | |
a2 = this.height / this.width / this.imageAspect; | |
} | |
this.plane.scale.x = this.width / 3000; | |
this.plane.scale.y = window.innerHeight / 6000; | |
} | |
this.material.uniforms.resolution.value.x = this.width; | |
this.material.uniforms.resolution.value.y = this.height; | |
this.material.uniforms.resolution.value.z = a2; | |
this.material.uniforms.resolution.value.w = a2; | |
this.camera.updateProjectionMatrix(); | |
this.renderer.setSize(this.width, this.height); | |
}; | |
setTextures = (cb) => { | |
const promises = []; | |
let that = this; | |
this.images.forEach((url, i) => { | |
let promise = new Promise((resolve) => { | |
that.textures[i] = new THREE.TextureLoader().load(url, resolve); | |
}); | |
promises.push(promise); | |
}); | |
Promise.all(promises).then(() => { | |
cb(); | |
}); | |
}; | |
updateTexture = () => { | |
let next; | |
if (this.current === this.images.length - 1) { | |
this.current = 0; | |
next = 1; | |
} else { | |
this.current += 1; | |
next = this.current + 1; | |
if (next === this.images.length) { | |
next = 0; | |
} | |
} | |
if (this.props.active) { | |
this.material.uniforms.texture1.value = this.textures[next]; | |
} else { | |
this.material.uniforms.texture2.value = this.textures[next]; | |
} | |
}; | |
animateRender() { | |
this.renderer.render(this.scene, this.camera); | |
this.time += 0.05; | |
this.material.uniforms.time.value = this.time; | |
this.plane.geometry.vertices.forEach((element) => { | |
this.material.uniforms.shift.value = lerp( | |
this.material.uniforms.shift.value, | |
-this.props.Y / 100, | |
0.01 | |
); | |
this.material.uniforms.shiftx.value = lerp( | |
this.material.uniforms.shiftx.value, | |
this.props.X / 1000, | |
0.01 | |
); | |
}); | |
this.material.uniforms.progress.value = this.props.P; | |
this._frameId = window.requestAnimationFrame(this.animateRender.bind(this)); | |
} | |
render() { | |
return <div id="world" className="app"></div>; | |
} | |
} | |
const Parent = () => { | |
const [active, setActive] = useState(false); | |
const [current, setCurrent] = useState(0); | |
const [{ X, Y, P }, set] = useState({ | |
X: 0, | |
Y: 0, | |
P: 0, | |
}); | |
const [{ yPos, xPos, progress }, setPos] = useSpring(() => ({ | |
yPos: 0, | |
xPos: 0, | |
progress: 0, | |
config: { | |
mass: 2, | |
tension: 670, | |
}, | |
onFrame: ({ yPos, xPos, progress }) => { | |
set({ | |
Y: yPos, | |
X: xPos, | |
P: progress, | |
}); | |
}, | |
})); | |
const bind = useDrag( | |
({ down, movement: [x, y], distance, direction: [xDir], velocity }) => { | |
if (down) { | |
if (!active) { | |
setPos({ yPos: y, xPos: x, progress: Math.abs(y / 500) }); | |
} else { | |
setPos({ yPos: y, xPos: x, progress: 1 - Math.abs(y / 500) }); | |
} | |
} else { | |
if (Math.abs(Y) > 150) { | |
if (!active) { | |
setPos({ yPos: 0, xPos: 0, progress: 1 }); | |
setActive(true); | |
} else { | |
setPos({ yPos: 0, xPos: 0, progress: 0 }); | |
setActive(false); | |
} | |
if (current === 2) { | |
setCurrent(0); | |
} else { | |
setCurrent(current + 1); | |
} | |
} else { | |
if (!active) { | |
setPos({ yPos: 0, xPos: 0, progress: 0 }); | |
} else { | |
setPos({ yPos: 0, xPos: 0, progress: 1 }); | |
} | |
} | |
} | |
} | |
); | |
return ( | |
<div id="container" {...bind()}> | |
<Noise X={X} Y={Y} P={P} active={active} /> | |
<div id="textCont"> | |
<div | |
id="text" | |
style={{ | |
opacity: current === 0 ? 1 : 0, | |
transform: current === 0 ? `translateY(0px)` : `translateY(20px)`, | |
}} | |
> | |
ICELAND | |
</div> | |
<div | |
id="text" | |
style={{ | |
opacity: current === 1 ? 1 : 0, | |
transform: current === 1 ? `translateY(0px)` : `translateY(20px)`, | |
}} | |
> | |
MALDIVES | |
</div> | |
<div | |
id="text" | |
style={{ | |
opacity: current === 2 ? 1 : 0, | |
transform: current === 2 ? `translateY(0px)` : `translateY(20px)`, | |
}} | |
> | |
NAMIBIA | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
ReactDOM.render(<Parent />, document.getElementById("root")); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.min.js"></script> | |
<script src="https://odintsov.me/static/libs/react-spring.js"></script> | |
<script src="https://odintsov.me/static/libs/react-use-gesture.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
overflow: hidden; | |
} | |
body { | |
margin: 0; | |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", | |
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", | |
sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
user-select: none; | |
background-color: black; | |
overscroll-behavior-y: contain; | |
} | |
code { | |
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", | |
monospace; | |
} | |
#root { | |
align-items: center; | |
justify-content: center; | |
} | |
#container { | |
position: absolute; | |
width: 100%; | |
height: 100vh; | |
} | |
#textCont { | |
position: absolute; | |
align-items: center; | |
display: flex; | |
justify-content: center; | |
width: 100%; | |
top: 0; | |
height: 100vh; | |
} | |
#text { | |
color: rgb(255, 255, 255); | |
font-weight: 900; | |
font-size: 7vw; | |
position: absolute; | |
margin-left: 27%; | |
margin-top: 29vh; | |
transition: 0.4s; | |
} | |
@media only screen and (max-width: 700px) { | |
#container { | |
height: 90vh; | |
} | |
#text { | |
font-size: 13vw; | |
margin-top: 33vh; | |
} | |
} | |
#message { | |
position: absolute; | |
bottom: 4vh; | |
width: 100%; | |
color: #242424; | |
text-align: center; | |
font-weight: 900; | |
z-index: 1000; | |
font-size: 26px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment