Cool 3d photo gallery developed using HTML (Pug), CSS (Scss), JS (Babel) and Vue.Js. To see the effect, mouse hover any image and see the 3d effects when pointer is zig zagged.
Developed by Jouan Marcel and 3D photo gallery in a semi skeuomorphism style with realistic lighting reflection on mouse / cursor hover. Created with Vue.Js as web component for portability.
Find the demo below
See the Pen Realistic 3D Photo Cards (Hover Effect, Vue.Js) by Jouan Marcel (@jouanmarcel) on CodePen.
Find the code below
HTML
Pug View Compiled HTML Pug.grid#grid
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/37f408a5a9bfcb4b0e2de07499a560c9/5E632BE6/t51.2885-15/e35/c0.135.1080.1080a/s480x480/40552940_106792203550584_1954235443271646170_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BnrVNA_F95p/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/398cfc6bf836e3e227ee922936836bbe/5E4D6E72/t51.2885-15/e35/s480x480/36955358_287866911973922_429811318774562816_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=102" link="https://www.instagram.com/p/BloI_FblUVy/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/889f95936f24056538c7168915d639c3/5E51E9AC/t51.2885-15/e35/c135.0.809.809a/s480x480/41557300_447533602406412_6139564580899339847_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BoKSUPGg5al/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/ec6d1a1d6f459aba44c4a8cfd462f1ad/5E64975E/t51.2885-15/sh0.08/e35/c89.0.902.902a/s640x640/69834747_986466475037071_2879916583938753044_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=107" link="https://www.instagram.com/p/B1rXN_roTv4/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/9212c0823c2e2c82431843d44890db22/5E3EAC0D/t51.2885-15/e35/s480x480/47585211_2233880496884813_4296136872377091209_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=105" link="https://www.instagram.com/p/BsdpIh6BxPB/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/cc45be94aa39443f8f485e149e420718/5E5CC0A1/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/39956004_317471385681570_6819068332604391424_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=106" link="https://www.instagram.com/p/BnT_d4XFuJJ/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/dd0a7fe266706839b5ddd0fe4142c9c0/5E406CFA/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37684559_242343189922222_578856764234006528_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=110" link="https://www.instagram.com/p/Bl8mOXKlp7N/")
photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/ad1d8f6eca2a3116c6da1aad08406ce8/5E449EC6/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37189542_2061235184195776_8978574457354321920_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=104" link="https://www.instagram.com/p/BlYb96KlH99/")
View Compiled HTML<div class="grid" id="grid">
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/37f408a5a9bfcb4b0e2de07499a560c9/5E632BE6/t51.2885-15/e35/c0.135.1080.1080a/s480x480/40552940_106792203550584_1954235443271646170_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BnrVNA_F95p/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/398cfc6bf836e3e227ee922936836bbe/5E4D6E72/t51.2885-15/e35/s480x480/36955358_287866911973922_429811318774562816_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=102" link="https://www.instagram.com/p/BloI_FblUVy/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/889f95936f24056538c7168915d639c3/5E51E9AC/t51.2885-15/e35/c135.0.809.809a/s480x480/41557300_447533602406412_6139564580899339847_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BoKSUPGg5al/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/ec6d1a1d6f459aba44c4a8cfd462f1ad/5E64975E/t51.2885-15/sh0.08/e35/c89.0.902.902a/s640x640/69834747_986466475037071_2879916583938753044_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=107" link="https://www.instagram.com/p/B1rXN_roTv4/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/9212c0823c2e2c82431843d44890db22/5E3EAC0D/t51.2885-15/e35/s480x480/47585211_2233880496884813_4296136872377091209_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=105" link="https://www.instagram.com/p/BsdpIh6BxPB/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/cc45be94aa39443f8f485e149e420718/5E5CC0A1/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/39956004_317471385681570_6819068332604391424_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=106" link="https://www.instagram.com/p/BnT_d4XFuJJ/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/dd0a7fe266706839b5ddd0fe4142c9c0/5E406CFA/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37684559_242343189922222_578856764234006528_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=110" link="https://www.instagram.com/p/Bl8mOXKlp7N/"></photo-card>
<photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/ad1d8f6eca2a3116c6da1aad08406ce8/5E449EC6/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37189542_2061235184195776_8978574457354321920_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=104" link="https://www.instagram.com/p/BlYb96KlH99/"></photo-card>
</div>
CSS
Sass View Compiled CSS Sassbody
margin: 0
min-height: 100vh
display: flex
flex-direction: column
align-items: center
justify-content: center
background-image: radial-gradient(circle, #fff 30%, #ccc)
padding: 0 40px
font-family: "Source Sans Pro", Helvetica, sans-serif
font-weight: 300
#grid
display: grid
grid-template-columns: repeat(auto-fill, 150px)
grid-column-gap: 30px
grid-row-gap: 30px
align-items: center
justify-content: center
width: 100%
max-width: 700px
.card
background-color: #ccc
width: 150px
height: 150px
transition: all .1s ease
border-radius: 3px
position: relative
z-index: 1
box-shadow: 0 0 5px rgba(0, 0, 0, 0)
overflow: hidden
cursor: pointer
&:hover
transform: scale(2)
z-index: 2
box-shadow: 0 10px 20px rgba(0, 0, 0, .4)
img
filter: grayscale(0)
.reflection
position: absolute
width: 100%
height: 100%
z-index: 2
left: 0
top: 0
transition: all .1s ease
opacity: 0
mix-blend-mode: soft-light
img
width: 100%
height: 100%
object-fit: cover
filter: grayscale(.65)
transition: all .3s ease
View Compiled CSSbody {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-image: radial-gradient(circle, #fff 30%, #ccc);
padding: 0 40px;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-weight: 300;
}
#grid {
display: grid;
grid-template-columns: repeat(auto-fill, 150px);
grid-column-gap: 30px;
grid-row-gap: 30px;
align-items: center;
justify-content: center;
width: 100%;
max-width: 700px;
}
#grid .card {
background-color: #ccc;
width: 150px;
height: 150px;
transition: all 0.1s ease;
border-radius: 3px;
position: relative;
z-index: 1;
box-shadow: 0 0 5px rgba(0, 0, 0, 0);
overflow: hidden;
cursor: pointer;
}
#grid .card:hover {
-webkit-transform: scale(2);
transform: scale(2);
z-index: 2;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
}
#grid .card:hover img {
-webkit-filter: grayscale(0);
filter: grayscale(0);
}
#grid .card .reflection {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
left: 0;
top: 0;
transition: all 0.1s ease;
opacity: 0;
mix-blend-mode: soft-light;
}
#grid .card img {
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
-webkit-filter: grayscale(0.65);
filter: grayscale(0.65);
transition: all 0.3s ease;
}
JavaScript
Babel View Compiled JS BabelVue.component("photo-card", {
template: `<a class="card"
:href="link"
target="_blank"
ref="card"
@mousemove="move"
@mouseleave="leave"
@mouseover="over">
<div class="reflection" ref="refl"></div>
<img :src="img"/>
</a>`,
props: ["img", "link"],
mounted() {},
data: () => ({
debounce: null,
}),
methods: {
over() {
const refl = this.$refs.refl;
refl.style.opacity = 1;
},
leave() {
const card = this.$refs.card;
const refl = this.$refs.refl;
card.style.transform = `perspective(500px) scale(1)`;
refl.style.opacity = 0;
},
move() {
const card = this.$refs.card;
const refl = this.$refs.refl;
const relX = (event.offsetX + 1) / card.offsetWidth;
const relY = (event.offsetY + 1) / card.offsetHeight;
const rotY = `rotateY(${(relX - 0.5) * 60}deg)`;
const rotX = `rotateX(${(relY - 0.5) * -60}deg)`;
card.style.transform = `perspective(500px) scale(2) ${rotY} ${rotX}`;
const lightX = this.scale(relX, 0, 1, 150, -50);
const lightY = this.scale(relY, 0, 1, 30, -100);
const lightConstrain = Math.min(Math.max(relY, 0.3), 0.7);
const lightOpacity = this.scale(lightConstrain, 0.3, 1, 1, 0) * 255;
const lightShade = `rgba(${lightOpacity}, ${lightOpacity}, ${lightOpacity}, 1)`;
const lightShadeBlack = `rgba(0, 0, 0, 1)`;
refl.style.backgroundImage = `radial-gradient(circle at ${lightX}% ${lightY}%, ${lightShade} 20%, ${lightShadeBlack})`;
},
scale: (val, inMin, inMax, outMin, outMax) =>
outMin + (val - inMin) * (outMax - outMin) / (inMax - inMin)
}
});
const app = new Vue({
el: "#grid"
});
View Compiled JSVue.component("photo-card", {
template: `<a class="card"
:href="link"
target="_blank"
ref="card"
@mousemove="move"
@mouseleave="leave"
@mouseover="over">
<div class="reflection" ref="refl"></div>
<img :src="img"/>
</a>`,
props: ["img", "link"],
mounted() {},
data: () => ({
debounce: null }),
methods: {
over() {
const refl = this.$refs.refl;
refl.style.opacity = 1;
},
leave() {
const card = this.$refs.card;
const refl = this.$refs.refl;
card.style.transform = `perspective(500px) scale(1)`;
refl.style.opacity = 0;
},
move() {
const card = this.$refs.card;
const refl = this.$refs.refl;
const relX = (event.offsetX + 1) / card.offsetWidth;
const relY = (event.offsetY + 1) / card.offsetHeight;
const rotY = `rotateY(${(relX - 0.5) * 60}deg)`;
const rotX = `rotateX(${(relY - 0.5) * -60}deg)`;
card.style.transform = `perspective(500px) scale(2) ${rotY} ${rotX}`;
const lightX = this.scale(relX, 0, 1, 150, -50);
const lightY = this.scale(relY, 0, 1, 30, -100);
const lightConstrain = Math.min(Math.max(relY, 0.3), 0.7);
const lightOpacity = this.scale(lightConstrain, 0.3, 1, 1, 0) * 255;
const lightShade = `rgba(${lightOpacity}, ${lightOpacity}, ${lightOpacity}, 1)`;
const lightShadeBlack = `rgba(0, 0, 0, 1)`;
refl.style.backgroundImage = `radial-gradient(circle at ${lightX}% ${lightY}%, ${lightShade} 20%, ${lightShadeBlack})`;
},
scale: (val, inMin, inMax, outMin, outMax) =>
outMin + (val - inMin) * (outMax - outMin) / (inMax - inMin) } });
const app = new Vue({
el: "#grid" });