♻️ - Suppression de la classe JustifiedGallery et ajout de nouveaux effets de survol pour les images
This commit is contained in:
864
JS/JustifedGallery/assets/css/lib/gallery/hover.css
Normal file
864
JS/JustifedGallery/assets/css/lib/gallery/hover.css
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
.hover-zoom {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hover-zoom img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tilt {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hover-tilt img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tilt:hover img {
|
||||||
|
transform: scale(1.04) rotate(1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-blur img {
|
||||||
|
filter: blur(2px);
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-blur:hover img {
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-blur-invert img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-blur-invert:hover img {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-darken img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-darken:hover img {
|
||||||
|
filter: brightness(0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lighten img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lighten:hover img {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-overlay {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-overlay::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-overlay:hover::after {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-border {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-border:hover {
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-colorize img {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-colorize:hover img {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide::after {
|
||||||
|
content: attr(data-caption);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: bottom 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide:hover::after {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-dezoom img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-dezoom:hover img {
|
||||||
|
transform: scale(.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-left img, .hover-slide-right img {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-left:hover img {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-right:hover img {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fade-zoom img {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: scale(1);
|
||||||
|
transition: transform 0.5s ease, opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fade-zoom:hover img {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-frosted img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-frosted:hover img {
|
||||||
|
filter: blur(1px) brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-underline {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-underline::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: #007cba;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-underline:hover::after {
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-desaturate img {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-desaturate:hover img {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glow {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glow:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 15px rgba(255, 225, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-polaroid {
|
||||||
|
background: white;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-polaroid:hover {
|
||||||
|
transform: translateY(-5px) rotate(-1deg);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-caption {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-caption::after {
|
||||||
|
content: attr(data-caption);
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-caption:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom-overlay {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom-overlay img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom-overlay::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom-overlay:hover img {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-zoom-overlay:hover::after {
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fadeout img {
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fadeout:hover img {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-wiggle:hover img {
|
||||||
|
animation: wiggle 0.4s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(0.8deg); }
|
||||||
|
75% { transform: rotate(-0.8deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-rotateY img {
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-rotateY:hover img {
|
||||||
|
transform: rotateY(8deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shine {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shine::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -75%;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.2) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
transform: skewX(-20deg);
|
||||||
|
transition: left 0.75s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shine:hover::before {
|
||||||
|
left: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mask {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mask::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s ease, background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mask:hover::after {
|
||||||
|
transform: translateY(0);
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-vignette img {
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-vignette:hover img {
|
||||||
|
filter: brightness(1.1) contrast(1.05) saturate(1.1) drop-shadow(0 0 15px rgba(0,0,0,0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-halo {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-halo::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0) 40%, rgba(255,255,255,0.3) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-halo:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift-up {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift-up:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-outline-glow {
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-outline-glow:hover {
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.8), 0 0 15px rgba(0, 150, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tint {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tint::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(45deg, rgba(0,123,255,0) 0%, rgba(0,123,255,0.25) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tint:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-grain img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-grain:hover img {
|
||||||
|
filter: contrast(1.1) brightness(1.05) saturate(1.05);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-up {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-up img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-slide-up:hover img {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-frame {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-frame::before,
|
||||||
|
.hover-frame::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background: #fff;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-parallax {
|
||||||
|
overflow: hidden;
|
||||||
|
perspective: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-parallax img {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-parallax:hover img {
|
||||||
|
transform: rotateY(3deg) rotateX(2deg) scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-ripple {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-ripple::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 70%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.5s ease, height 0.5s ease, opacity 0.6s ease;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-ripple:hover::after {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glass {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glass::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(120deg, rgba(255,255,255,0.3), rgba(255,255,255,0) 70%);
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glass:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-window {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-window img {
|
||||||
|
transition: transform 0.4s ease, clip-path 0.4s ease;
|
||||||
|
clip-path: inset(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-window:hover img {
|
||||||
|
clip-path: inset(5% 5%);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-diagonal {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-diagonal::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
transform: skewY(-10deg);
|
||||||
|
transition: top 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-diagonal:hover::before {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-analog img {
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-analog:hover img {
|
||||||
|
filter: contrast(1.15) brightness(1.05) saturate(1.1) sepia(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shutter {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shutter img {
|
||||||
|
transition: clip-path 0.6s ease;
|
||||||
|
clip-path: circle(80% at center);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shutter:hover img {
|
||||||
|
clip-path: circle(35% at center);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mist {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mist::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2), transparent 70%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-mist:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-sunset img {
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-sunset:hover img {
|
||||||
|
filter: sepia(0.3) saturate(1.2) hue-rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-spotlight {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-spotlight img {
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-spotlight:hover img {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-spotlight .spotlight-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
rgba(0, 0, 0, 0.4) 70%
|
||||||
|
);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hover-fracture {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fracture img {
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fracture::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: inherit;
|
||||||
|
background-attachment: fixed;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
transform: translate(5px, -5px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fracture:hover img {
|
||||||
|
transform: translate(-3px, 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fracture:hover::after {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-morph img {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: border-radius 0.6s ease, transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-morph:hover img {
|
||||||
|
border-radius: 40% 60% 50% 70% / 60% 50% 70% 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-grid {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-grid::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,0.05) 0, rgba(255,255,255,0.05) 1px, transparent 1px, transparent 10px),
|
||||||
|
repeating-linear-gradient(0deg, rgba(255,255,255,0.05) 0, rgba(255,255,255,0.05) 1px, transparent 1px, transparent 10px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-grid:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
animation: gridMove 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gridMove {
|
||||||
|
to { background-position: 40px 40px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-freeze {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-freeze img {
|
||||||
|
transition: filter 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-freeze:hover img {
|
||||||
|
filter: grayscale(.7) brightness(1.2) sepia(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-freeze::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 2px solid rgba(255,255,255,0);
|
||||||
|
transition: border-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-freeze:hover::after {
|
||||||
|
border-color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-pixelate img {
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
}
|
||||||
|
.hover-pixelate:hover img {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
filter: contrast(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-waveflow img {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.hover-waveflow:hover img {
|
||||||
|
animation: waveflow 1.2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes waveflow {
|
||||||
|
0% { transform: rotate(0.4deg) translateY(1px); }
|
||||||
|
100% { transform: rotate(-0.4deg) translateY(-1px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-prism img {
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
.hover-prism:hover img {
|
||||||
|
filter: contrast(1.1) saturate(1.2) drop-shadow(2px 0 red) drop-shadow(-2px 0 cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-vibrate:hover img {
|
||||||
|
animation: vibrate 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes vibrate {
|
||||||
|
0% { transform: translate(0); }
|
||||||
|
25% { transform: translate(-1px, 1px); }
|
||||||
|
50% { transform: translate(1px, -1px); }
|
||||||
|
75% { transform: translate(-1px, -1px); }
|
||||||
|
100% { transform: translate(1px, 1px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scan {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hover-scan::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.25) 50%, transparent 100%);
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
.hover-scan:hover::before {
|
||||||
|
animation: scanline 1s linear;
|
||||||
|
}
|
||||||
|
@keyframes scanline {
|
||||||
|
0% { transform: translateY(-100%); }
|
||||||
|
100% { transform: translateY(200%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-glitch img {
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
.hover-glitch:hover img {
|
||||||
|
animation: glitch .3s steps(2, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0% { clip-path: inset(10% 0 90% 0); transform: translate(2px); }
|
||||||
|
20% { clip-path: inset(80% 0 10% 0); transform: translate(-2px); }
|
||||||
|
40% { clip-path: inset(30% 0 60% 0); transform: translate(1px, -1px); }
|
||||||
|
60% { clip-path: inset(50% 0 30% 0); transform: translate(-1px, 1px); }
|
||||||
|
80% { clip-path: inset(40% 0 40% 0); transform: translate(0); }
|
||||||
|
100% { clip-path: inset(0 0 0 0); transform: translate(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tilt3d {
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-twist img {
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
.hover-twist:hover img {
|
||||||
|
transform: perspective(1300px) rotateY(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-fog {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hover-fog::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: url('https://assets.codepen.io/13471/fog.png') repeat;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fog-move 10s linear infinite;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
.hover-fog:hover::after {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
@keyframes fog-move {
|
||||||
|
from { background-position: 0 0; }
|
||||||
|
to { background-position: 1000px 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-ripple-animate {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-ripple-animate::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-effect {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
animation: ripple-wave 0.6s ease-out forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple-wave {
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) scale(4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lens {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
.hover-lens img {
|
||||||
|
transition: filter 0.4s ease;
|
||||||
|
cursor: none!important;
|
||||||
|
}
|
||||||
|
.hover-lens:hover img {
|
||||||
|
filter: brightness(0.6) blur(2px);
|
||||||
|
}
|
||||||
|
.hover-lens:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lens::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 170px;
|
||||||
|
height: 170px;
|
||||||
|
top: calc(var(--lens-y, 50px) - 60px);
|
||||||
|
left: calc(var(--lens-x, 50px) - 60px);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-image: var(--lens-bg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 250%;
|
||||||
|
background-position: var(--lens-pos);
|
||||||
|
opacity: var(--hover, 0);
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import JustifiedGallery from "./lib/gallery.js";
|
import JustifiedGallery from "./lib/gallery/gallery.js";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
let element = document.getElementById("gallery")
|
let element = document.getElementById("gallery")
|
||||||
|
|
||||||
new JustifiedGallery(element, {
|
new JustifiedGallery(element, {
|
||||||
wrapLinks: true,
|
wrapLinks: true,
|
||||||
|
hoverEffect: "hover-spotlight",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,387 +0,0 @@
|
|||||||
export default class JustifiedGallery {
|
|
||||||
|
|
||||||
constructor(container, options = {}) {
|
|
||||||
this.container = container;
|
|
||||||
this.options = {
|
|
||||||
margin: options.margin || 5,
|
|
||||||
columns: options.columns || 3,
|
|
||||||
breakpoints: options.breakpoints || { 768: 2, 480: 1 },
|
|
||||||
wrapLinks: options.wrapLinks || false, // active l'encapsulation
|
|
||||||
linkAttribute: options.linkAttribute || 'src', // attribut à utiliser pour href
|
|
||||||
justifyLastRow: options.justifyLastRow || false,
|
|
||||||
};
|
|
||||||
this.currentIndex = 0;
|
|
||||||
|
|
||||||
this.isOpenModal = false;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
window.addEventListener('resize', () => this.layoutGallery());
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.images = this.getImages();
|
|
||||||
|
|
||||||
// Encapsule les images si nécessaire
|
|
||||||
if (this.options.wrapLinks) {
|
|
||||||
this.images.forEach(img => this.wrapImage(img));
|
|
||||||
this.images = this.getImages(); // rafraîchit la liste
|
|
||||||
}
|
|
||||||
|
|
||||||
// S'assure que toutes les images sont chargées
|
|
||||||
const promises = this.images.map(img => this.waitForImageLoad(img));
|
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
this.layoutGallery();
|
|
||||||
this.setupModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.container.addEventListener('keydown', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.key === 'ArrowRight') {
|
|
||||||
e.target.nextSibling.nextSibling.focus()
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowLeft") {
|
|
||||||
e.target.previousSibling.previousSibling.focus()
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
|
|
||||||
const images = this.getImages();
|
|
||||||
let target = e.target;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.options.columns; i++) {
|
|
||||||
if (Math.ceil(images.length / this.options.columns) <= i) {
|
|
||||||
// On récupère le dernier élément si on dépasse la dernière ligne
|
|
||||||
target = images[images.length - 1].parentNode;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Utiliser nextElementSibling pour éviter les text nodes
|
|
||||||
if (target.nextElementSibling) {
|
|
||||||
target = target.nextElementSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
target.focus();
|
|
||||||
// Scroll pour voir la ligne où se trouve l'image focus
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
let target = e.target
|
|
||||||
for (let i = 0; this.options.columns > i; i++) {
|
|
||||||
if (this.getImages().length < i) {
|
|
||||||
target = this.getImages()[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
target = target.previousSibling.previousSibling
|
|
||||||
}
|
|
||||||
if (target) {
|
|
||||||
target.focus();
|
|
||||||
// Scroll pour voir la ligne où se trouve l'image focus
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.target.click()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapImage(img) {
|
|
||||||
const href = img.getAttribute(this.options.linkAttribute) || img.src;
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = href;
|
|
||||||
a.style.display = 'inline-block';
|
|
||||||
img.parentNode.insertBefore(a, img);
|
|
||||||
a.appendChild(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
getImages() {
|
|
||||||
return Array.from(this.container.querySelectorAll('img'));
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForImageLoad(img) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (img.complete) resolve();
|
|
||||||
else {
|
|
||||||
img.onload = () => resolve();
|
|
||||||
img.onerror = () => resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumns() {
|
|
||||||
const width = this.container.clientWidth;
|
|
||||||
let cols = this.options.columns;
|
|
||||||
for (let bp in this.options.breakpoints) {
|
|
||||||
if (width <= bp) cols = this.options.breakpoints[bp];
|
|
||||||
}
|
|
||||||
return cols;
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutGallery() {
|
|
||||||
const containerWidth = this.container.clientWidth;
|
|
||||||
const margin = this.options.margin;
|
|
||||||
const cols = this.getColumns();
|
|
||||||
|
|
||||||
let rowImages = [];
|
|
||||||
let totalRatio = 0;
|
|
||||||
|
|
||||||
this.images.forEach((img, index) => {
|
|
||||||
const ratio = img.naturalWidth / img.naturalHeight;
|
|
||||||
|
|
||||||
if (this.options.wrapLinks) {
|
|
||||||
rowImages.push({ img: img.parentNode, ratio });
|
|
||||||
} else {
|
|
||||||
rowImages.push({ img, ratio });
|
|
||||||
}
|
|
||||||
|
|
||||||
totalRatio += ratio;
|
|
||||||
|
|
||||||
const isLastRow = index === this.images.length - 1;
|
|
||||||
if (rowImages.length === cols || isLastRow) {
|
|
||||||
let rowHeight;
|
|
||||||
if (isLastRow && rowImages.length < cols && !this.options.justifyLastRow) {
|
|
||||||
// Ne pas étirer la dernière ligne
|
|
||||||
const avgRatio = totalRatio / rowImages.length;
|
|
||||||
rowHeight = Math.floor((containerWidth / cols) / avgRatio);
|
|
||||||
} else {
|
|
||||||
// Étire normalement pour remplir la ligne
|
|
||||||
rowHeight = Math.floor((containerWidth - margin * (rowImages.length - 1)) / totalRatio);
|
|
||||||
}
|
|
||||||
this.scaleRow(rowImages, rowHeight, margin);
|
|
||||||
rowImages = [];
|
|
||||||
totalRatio = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
scaleRow(row, rowHeight, margin) {
|
|
||||||
row.forEach((item, index) => {
|
|
||||||
const width = rowHeight * item.ratio;
|
|
||||||
|
|
||||||
|
|
||||||
if (this.options.wrapLinks) {
|
|
||||||
item.img.style.overflow = 'hidden';
|
|
||||||
item.img.firstChild.style.width = '100%';
|
|
||||||
}
|
|
||||||
|
|
||||||
item.img.style.width = `${width}px`;
|
|
||||||
item.img.style.height = `${rowHeight}px`;
|
|
||||||
item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0';
|
|
||||||
item.img.style.marginBottom = `${margin}px`;
|
|
||||||
item.img.style.cursor = 'pointer';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupModal() {
|
|
||||||
// Création du modal
|
|
||||||
this.modal = document.createElement('div');
|
|
||||||
this.modal.classList.add('jg-modal');
|
|
||||||
this.modal.innerHTML = `
|
|
||||||
<span class="jg-close">×</span>
|
|
||||||
<img class="jg-modal-img" src="">
|
|
||||||
<div class="jg-controls">
|
|
||||||
<span class="jg-prev">❮</span>
|
|
||||||
<span class="jg-next">❯</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(this.modal);
|
|
||||||
|
|
||||||
// Références
|
|
||||||
this.modalImg = this.modal.querySelector('.jg-modal-img');
|
|
||||||
this.closeBtn = this.modal.querySelector('.jg-close');
|
|
||||||
this.prevBtn = this.modal.querySelector('.jg-prev');
|
|
||||||
this.nextBtn = this.modal.querySelector('.jg-next');
|
|
||||||
|
|
||||||
// Événements
|
|
||||||
this.images.forEach((img, idx) => {
|
|
||||||
img.addEventListener('click', e => {
|
|
||||||
e.preventDefault(); // empêche le clic sur le lien
|
|
||||||
this.openModal(idx);
|
|
||||||
});
|
|
||||||
img.parentNode.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.openModal(idx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
this.closeBtn.addEventListener('click', () => this.closeModal());
|
|
||||||
this.prevBtn.addEventListener('click', () => this.prevImage());
|
|
||||||
this.nextBtn.addEventListener('click', () => this.nextImage());
|
|
||||||
this.modal.addEventListener('click', e => {
|
|
||||||
if (e.target === this.modal) this.closeModal();
|
|
||||||
});
|
|
||||||
document.addEventListener('keydown', e => {
|
|
||||||
if (this.modal.style.display === 'flex') {
|
|
||||||
if (e.key === 'ArrowLeft') this.prevImage();
|
|
||||||
if (e.key === 'ArrowRight') this.nextImage();
|
|
||||||
if (e.key === 'Escape') this.closeModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Variables pour drag + zoom
|
|
||||||
this.modalImg.currentScale = 1;
|
|
||||||
let isDragging = false;
|
|
||||||
let startX, startY;
|
|
||||||
let currentTranslateX = 0, currentTranslateY = 0;
|
|
||||||
|
|
||||||
// Drag start
|
|
||||||
this.modalImg.addEventListener('mousedown', e => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
isDragging = true;
|
|
||||||
startX = e.clientX;
|
|
||||||
startY = e.clientY;
|
|
||||||
this.modalImg.style.cursor = 'grabbing';
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag move
|
|
||||||
this.modal.addEventListener('mousemove', e => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
const dx = e.clientX - startX;
|
|
||||||
const dy = e.clientY - startY;
|
|
||||||
startX = e.clientX;
|
|
||||||
startY = e.clientY;
|
|
||||||
|
|
||||||
currentTranslateX += dx;
|
|
||||||
currentTranslateY += dy;
|
|
||||||
|
|
||||||
this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag end
|
|
||||||
this.modal.addEventListener('mouseup', () => {
|
|
||||||
isDragging = false;
|
|
||||||
this.modalImg.style.cursor = 'grab';
|
|
||||||
});
|
|
||||||
this.modal.addEventListener('mouseleave', () => {
|
|
||||||
isDragging = false;
|
|
||||||
this.modalImg.style.cursor = 'grab';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Scroll pour zoom
|
|
||||||
this.modalImg.addEventListener('wheel', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const scaleAmount = e.deltaY > 0 ? -0.1 : 0.1;
|
|
||||||
this.modalImg.currentScale = Math.min(Math.max(this.modalImg.currentScale + scaleAmount, 0.5), 3); // limites min/max
|
|
||||||
this.modalImg.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${this.modalImg.currentScale})`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openModal(index) {
|
|
||||||
this.isOpenModal = true;
|
|
||||||
this.currentIndex = index;
|
|
||||||
this.modal.style.display = 'flex';
|
|
||||||
this.modalImg.src = this.images[this.currentIndex].src;
|
|
||||||
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal() {
|
|
||||||
this.isOpenModal = false;
|
|
||||||
this.modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'scroll';
|
|
||||||
|
|
||||||
// Reset zoom et position
|
|
||||||
this.modalImg.currentScale = 1;
|
|
||||||
this.modalImg.style.transform = `translate(0px, 0px) scale(1)`;
|
|
||||||
currentTranslateX = 0;
|
|
||||||
currentTranslateY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevImage() {
|
|
||||||
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
|
|
||||||
this.modalImg.src = this.images[this.currentIndex].src;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextImage() {
|
|
||||||
this.currentIndex = (this.currentIndex + 1) % this.images.length;
|
|
||||||
this.modalImg.src = this.images[this.currentIndex].src;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Gallery {
|
|
||||||
constructor(container, options = {}) {
|
|
||||||
this.container = container;
|
|
||||||
this.options = {
|
|
||||||
margin: options.margin || 5,
|
|
||||||
columns: options.columns || 4, // nombre de colonnes par défaut
|
|
||||||
breakpoints: options.breakpoints || { 768: 2, 480: 1 }, // largeur => colonnes
|
|
||||||
};
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
window.addEventListener('resize', () => this.layoutGallery());
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.images = this.getImages();
|
|
||||||
|
|
||||||
// On attend que toutes les images soient chargées pour avoir leurs dimensions naturelles
|
|
||||||
const promises = this.images.map(img => this.waitForImageLoad(img));
|
|
||||||
Promise.all(promises).then(() => this.layoutGallery());
|
|
||||||
}
|
|
||||||
|
|
||||||
getImages() {
|
|
||||||
return Array.from(this.container.querySelectorAll('img'));
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForImageLoad(img) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (img.complete) resolve();
|
|
||||||
else {
|
|
||||||
img.onload = () => resolve();
|
|
||||||
img.onerror = () => resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateRatio(img) {
|
|
||||||
return img.naturalWidth / img.naturalHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumns() {
|
|
||||||
const width = this.container.clientWidth;
|
|
||||||
let cols = this.options.columns;
|
|
||||||
|
|
||||||
// Vérifie les breakpoints pour adapter le nombre de colonnes
|
|
||||||
for (let bp in this.options.breakpoints) {
|
|
||||||
if (width <= bp) {
|
|
||||||
cols = this.options.breakpoints[bp];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cols;
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutGallery() {
|
|
||||||
const containerWidth = this.container.clientWidth;
|
|
||||||
const margin = this.options.margin;
|
|
||||||
const cols = this.getColumns();
|
|
||||||
const rowHeight = (containerWidth - margin * (cols - 1)) / cols; // largeur d'une "colonne" comme base
|
|
||||||
let rowImages = [];
|
|
||||||
|
|
||||||
this.images.forEach((img, index) => {
|
|
||||||
const ratio = this.calculateRatio(img);
|
|
||||||
rowImages.push({ img, ratio });
|
|
||||||
|
|
||||||
if (rowImages.length === cols || index === this.images.length - 1) {
|
|
||||||
this.scaleRow(rowImages, rowHeight, margin);
|
|
||||||
rowImages = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
scaleRow(row, rowHeight, margin) {
|
|
||||||
row.forEach((item, index) => {
|
|
||||||
const width = rowHeight * item.ratio;
|
|
||||||
item.img.style.width = `${width}px`;
|
|
||||||
item.img.style.height = `${rowHeight}px`;
|
|
||||||
item.img.style.marginRight = index < row.length - 1 ? `${margin}px` : '0';
|
|
||||||
item.img.style.marginBottom = `${margin}px`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
37
JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js
Normal file
37
JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export default function enableLensEffect(container) {
|
||||||
|
container
|
||||||
|
.querySelectorAll('.hover-lens')
|
||||||
|
.forEach(link => {
|
||||||
|
const img = link.querySelector('img');
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
link.addEventListener('mousemove', e => {
|
||||||
|
const rect = link.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
link.style.setProperty('--lens-x', `${x}px`);
|
||||||
|
link.style.setProperty('--lens-y', `${y}px`);
|
||||||
|
link.style.setProperty('--lens-bg', `url(${img.src})`);
|
||||||
|
link.style.setProperty(
|
||||||
|
'--lens-pos',
|
||||||
|
`${(x / rect.width) * 100}% ${(y / rect.height) * 100}%`
|
||||||
|
);
|
||||||
|
link.style.backgroundImage = `url(${img.src})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
link.addEventListener('mouseenter', () => {
|
||||||
|
const rect = link.getBoundingClientRect();
|
||||||
|
link.style.setProperty('--lens-bg', `url(${img.src})`);
|
||||||
|
link.style.setProperty('--lens-pos', `center center`);
|
||||||
|
link.style.setProperty('--lens-x', `${rect.width / 2}px`);
|
||||||
|
link.style.setProperty('--lens-y', `${rect.height / 2}px`);
|
||||||
|
link.classList.add('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
link.addEventListener('mouseleave', () => {
|
||||||
|
link.classList.remove('active');
|
||||||
|
link.style.removeProperty('background-image');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default function enableRippleEffect(container) {
|
||||||
|
container
|
||||||
|
.querySelectorAll('.hover-ripple-animate')
|
||||||
|
.forEach(link => {
|
||||||
|
link.addEventListener('mouseenter', e => {
|
||||||
|
const rect = link.getBoundingClientRect();
|
||||||
|
const ripple = document.createElement('span');
|
||||||
|
ripple.classList.add('ripple-effect');
|
||||||
|
|
||||||
|
const size = Math.max(rect.width, rect.height);
|
||||||
|
ripple.style.width = ripple.style.height = `${size}px`;
|
||||||
|
ripple.style.left = `${e.clientX - rect.left}px`;
|
||||||
|
ripple.style.top = `${e.clientY - rect.top}px`;
|
||||||
|
|
||||||
|
link.appendChild(ripple);
|
||||||
|
ripple.addEventListener('animationend', () => ripple.remove());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export function enableSpotlightEffect(container) {
|
||||||
|
const images = container.querySelectorAll('.hover-spotlight');
|
||||||
|
|
||||||
|
images.forEach(image => {
|
||||||
|
// Crée un overlay lumineux via pseudo ou dataset
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.classList.add('spotlight-overlay');
|
||||||
|
image.appendChild(overlay);
|
||||||
|
|
||||||
|
// Mise à jour dynamique du gradient
|
||||||
|
image.addEventListener('mousemove', (e) => {
|
||||||
|
const rect = image.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const intensity = Math.min(rect.width, rect.height) / 400;
|
||||||
|
overlay.style.background = `radial-gradient(
|
||||||
|
circle at ${x}px ${y}px,
|
||||||
|
rgba(255,255,255,${0.1 * intensity}),
|
||||||
|
rgba(0,0,0,0.5) 60%
|
||||||
|
)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset progressif quand la souris sort
|
||||||
|
image.addEventListener('mouseleave', () => {
|
||||||
|
overlay.style.background = `radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255,255,255,0),
|
||||||
|
rgba(0,0,0,0.4) 70%
|
||||||
|
)`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
659
JS/JustifedGallery/assets/js/lib/gallery/gallery.js
Normal file
659
JS/JustifedGallery/assets/js/lib/gallery/gallery.js
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
/**
|
||||||
|
* JustifiedGallery
|
||||||
|
*
|
||||||
|
* A responsive justified image gallery with smooth transitions, modal preview,
|
||||||
|
* custom item styles, and dynamic hover effects.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Responsive columns & max row height (with breakpoints)
|
||||||
|
* - Optional modal (zoomable, draggable, navigable)
|
||||||
|
* - Configurable hover effects (Lens, Ripple, Spotlight, etc.)
|
||||||
|
* - Custom per-item inline styles via `itemStyle`
|
||||||
|
* - Smooth animated layout transitions when column count changes (FLIP via overlay)
|
||||||
|
*
|
||||||
|
* Author: Aranéite (Antoine)
|
||||||
|
* Version: 2.4
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class JustifiedGallery {
|
||||||
|
/* =========================================================
|
||||||
|
* Constructor & public state
|
||||||
|
* ======================================================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} container - The gallery container element.
|
||||||
|
* @param {Object} options - Configuration options.
|
||||||
|
*/
|
||||||
|
constructor(container, options = {}) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
this.container = container;
|
||||||
|
this.images = [];
|
||||||
|
this.imagesClone = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.isOpenModal = false;
|
||||||
|
this.currentCols = null; // track current column count for breakpoint animation
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
margin: options.margin ?? 5,
|
||||||
|
columns: options.columns ?? 5,
|
||||||
|
breakpoints: options.breakpoints ?? { 1248: 3, 768: 2, 480: 1 },
|
||||||
|
hasModal: options.hasModal ?? false,
|
||||||
|
linkAttribute: options.linkAttribute || 'src',
|
||||||
|
justifyLastRow: options.justifyLastRow ?? false,
|
||||||
|
hoverEffect: options.hoverEffect || null,
|
||||||
|
itemStyle: options.itemStyle || {},
|
||||||
|
|
||||||
|
maxRowHeight: options.maxRowHeight ?? 350,
|
||||||
|
maxRowHeightBreakpoints: options.maxRowHeightBreakpoints ?? {
|
||||||
|
1248: 300,
|
||||||
|
768: 250,
|
||||||
|
480: 200,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisation
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
// Handle responsive relayout on resize
|
||||||
|
this._onResize = () => {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const newCols = this._getColumns();
|
||||||
|
const shouldAnimateWrap =
|
||||||
|
this.currentCols !== null && newCols !== this.currentCols;
|
||||||
|
|
||||||
|
this.currentCols = newCols;
|
||||||
|
this.layoutGallery({ animateWrap: shouldAnimateWrap });
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', this._onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* Initialization
|
||||||
|
* ======================================================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize gallery:
|
||||||
|
* - wrap images in anchors
|
||||||
|
* - wait for load
|
||||||
|
* - initial layout
|
||||||
|
* - modal / hover / custom styles
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
// Start hidden → fade-in after first layout
|
||||||
|
this.container.style.opacity = '0';
|
||||||
|
|
||||||
|
// Initial raw images
|
||||||
|
this.images = this._getImages();
|
||||||
|
this.imagesClone = this._getImagesClones();
|
||||||
|
|
||||||
|
// Wrap each image in a link (<a>) and attach hover class
|
||||||
|
this.images.forEach(img => this._wrapImage(img));
|
||||||
|
|
||||||
|
// Refresh internal image list (now wrapped by <a>)
|
||||||
|
this.images = this._getImages();
|
||||||
|
|
||||||
|
// Wait for all images to be loaded (or failed)
|
||||||
|
const promises = this.images.map(img => this._waitForImageLoad(img));
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
// First layout (no animation on initial render)
|
||||||
|
this.currentCols = this._getColumns();
|
||||||
|
this.layoutGallery({ animateWrap: false });
|
||||||
|
|
||||||
|
// Modal, hover effects, and custom item styles
|
||||||
|
this.setupModal();
|
||||||
|
this.initHoverEffects();
|
||||||
|
this.applyItemStyles();
|
||||||
|
|
||||||
|
// Smooth fade-in
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.container.style.transition = 'opacity 0.6s ease';
|
||||||
|
this.container.style.opacity = '1';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply custom styles defined in options.itemStyle
|
||||||
|
* to each gallery item (link wrapper).
|
||||||
|
*/
|
||||||
|
applyItemStyles() {
|
||||||
|
this.images.forEach(img => {
|
||||||
|
const wrapper = img.parentNode;
|
||||||
|
for (const key in this.options.itemStyle) {
|
||||||
|
wrapper.style[key] = this.options.itemStyle[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hover effects (spotlight, ripple, lens...)
|
||||||
|
* once DOM is ready and images are wrapped.
|
||||||
|
*/
|
||||||
|
initHoverEffects() {
|
||||||
|
const effect = this.options.hoverEffect;
|
||||||
|
|
||||||
|
// Spotlight effect (dynamic import)
|
||||||
|
if (effect === 'hover-spotlight') {
|
||||||
|
import('./effects/spotlight.js')
|
||||||
|
.then(({ enableSpotlightEffect }) => {
|
||||||
|
enableSpotlightEffect(this.container);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ripple effect: radial animation on mouse enter
|
||||||
|
if (effect === 'hover-ripple-animate') {
|
||||||
|
import('./effects/ripple-animate.js')
|
||||||
|
.then(({ enableRippleEffect }) => {
|
||||||
|
enableRippleEffect(this.container);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lens effect: zoomed circular region following the cursor
|
||||||
|
if (effect === 'hover-lens') {
|
||||||
|
import('./effects/lens.js')
|
||||||
|
.then(({ enableLensEffect }) => {
|
||||||
|
enableLensEffect(this.container);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up listeners and DOM references.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
window.removeEventListener('resize', this._onResize);
|
||||||
|
|
||||||
|
if (this.modal && document.body.contains(this.modal)) {
|
||||||
|
this.modal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any listeners on images by cloning them
|
||||||
|
if (this.images && this.images.length) {
|
||||||
|
this.images.forEach(img => {
|
||||||
|
const clone = img.cloneNode(true);
|
||||||
|
img.replaceWith(clone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container = null;
|
||||||
|
this.images = null;
|
||||||
|
this.modal = null;
|
||||||
|
this.modalImg = null;
|
||||||
|
this.closeBtn = null;
|
||||||
|
this.prevBtn = null;
|
||||||
|
this.nextBtn = null;
|
||||||
|
this.isOpenModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* Layout computation & application
|
||||||
|
* ======================================================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute layout data for all rows without mutating the DOM.
|
||||||
|
*
|
||||||
|
* @returns {Array<{ item: HTMLElement, img: HTMLImageElement,
|
||||||
|
* width: number, height: number,
|
||||||
|
* marginRight: number, marginBottom: number }>}
|
||||||
|
*/
|
||||||
|
_computeLayout() {
|
||||||
|
const containerWidth = this.container.clientWidth;
|
||||||
|
const margin = this.options.margin;
|
||||||
|
const cols = this._getColumns();
|
||||||
|
const maxRowHeight = this._getMaxRowHeight();
|
||||||
|
const justifyLastRow = this.options.justifyLastRow;
|
||||||
|
|
||||||
|
const layout = [];
|
||||||
|
let rowImages = [];
|
||||||
|
let totalRatio = 0;
|
||||||
|
|
||||||
|
this.images.forEach((img, index) => {
|
||||||
|
const ratio = img.naturalWidth / img.naturalHeight || 1;
|
||||||
|
const item = img.parentNode;
|
||||||
|
|
||||||
|
rowImages.push({ item, img, ratio });
|
||||||
|
totalRatio += ratio;
|
||||||
|
|
||||||
|
const isLastRow = index === this.images.length - 1;
|
||||||
|
|
||||||
|
if (rowImages.length === cols || isLastRow) {
|
||||||
|
let rowHeight;
|
||||||
|
|
||||||
|
if (isLastRow && rowImages.length < cols && !justifyLastRow) {
|
||||||
|
// Do not stretch last row: use average ratio and target less height
|
||||||
|
const avgRatio = totalRatio / rowImages.length;
|
||||||
|
rowHeight = Math.floor((containerWidth / cols) / avgRatio);
|
||||||
|
rowHeight = Math.min(rowHeight, maxRowHeight);
|
||||||
|
} else {
|
||||||
|
// Normal justified row: fill container width
|
||||||
|
rowHeight = Math.floor(
|
||||||
|
(containerWidth - margin * (rowImages.length - 1)) / totalRatio
|
||||||
|
);
|
||||||
|
rowHeight = Math.min(rowHeight, maxRowHeight);
|
||||||
|
|
||||||
|
// Stretch to perfectly fill the row horizontally
|
||||||
|
const totalWidth =
|
||||||
|
rowImages.reduce((sum, r) => sum + rowHeight * r.ratio, 0) +
|
||||||
|
margin * (rowImages.length - 1);
|
||||||
|
|
||||||
|
if (totalWidth > 0) {
|
||||||
|
const stretchFactor = containerWidth / totalWidth;
|
||||||
|
rowHeight *= stretchFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rowImages.forEach((r, idx) => {
|
||||||
|
const width = rowHeight * r.ratio;
|
||||||
|
layout.push({
|
||||||
|
item: r.item,
|
||||||
|
img: r.img,
|
||||||
|
width,
|
||||||
|
height: rowHeight,
|
||||||
|
marginRight: idx < rowImages.length - 1 ? margin : 0,
|
||||||
|
marginBottom: margin,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rowImages = [];
|
||||||
|
totalRatio = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep in sync with actual columns used
|
||||||
|
this.currentCols = this._getColumns();
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply computed layout to DOM elements (without animation).
|
||||||
|
* @param {Array} layout - Layout entries from _computeLayout().
|
||||||
|
*/
|
||||||
|
_applyLayout(layout) {
|
||||||
|
layout.forEach(entry => {
|
||||||
|
const { item, img, width, height, marginRight, marginBottom } = entry;
|
||||||
|
|
||||||
|
// Anchor <a> wrapper sized using computed layout
|
||||||
|
item.style.width = `${width}px`;
|
||||||
|
item.style.height = `${height}px`;
|
||||||
|
item.style.display = 'inline-block';
|
||||||
|
item.style.marginRight = `${marginRight}px`;
|
||||||
|
item.style.marginBottom = `${marginBottom}px`;
|
||||||
|
item.style.transition = 'none';
|
||||||
|
|
||||||
|
// Image fills its wrapper
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.cursor = 'pointer';
|
||||||
|
img.style.transition = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main layout function.
|
||||||
|
* - If animateWrap = true, uses overlay + FLIP-like animation
|
||||||
|
* when the number of columns changes (breakpoint hit).
|
||||||
|
* - Otherwise only recomputes justification without wrap animation.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} options.animateWrap
|
||||||
|
*/
|
||||||
|
layoutGallery({ animateWrap = false } = {}) {
|
||||||
|
if (!this.container || !this.images || !this.images.length) return;
|
||||||
|
|
||||||
|
const layout = this._computeLayout();
|
||||||
|
|
||||||
|
// Simple re-justification when column count did not change
|
||||||
|
if (!animateWrap) {
|
||||||
|
this._applyLayout(layout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------
|
||||||
|
* Animated wrap change using fixed overlay + clones
|
||||||
|
* ------------------------------------------- */
|
||||||
|
|
||||||
|
// 1) Capture current positions BEFORE applying new layout
|
||||||
|
const prevRects = this._getWrapperRects();
|
||||||
|
const containerRect = this.container.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 2) Create overlay (fixed) containing clones at old positions
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.style.position = 'fixed';
|
||||||
|
overlay.style.left = `${containerRect.left}px`;
|
||||||
|
overlay.style.top = `${containerRect.top}px`;
|
||||||
|
overlay.style.width = `${containerRect.width}px`;
|
||||||
|
overlay.style.height = `${containerRect.height}px`;
|
||||||
|
overlay.style.pointerEvents = 'none';
|
||||||
|
overlay.style.zIndex = '9999';
|
||||||
|
|
||||||
|
const wrappers = this.images.map(img => img.parentNode);
|
||||||
|
const clones = [];
|
||||||
|
|
||||||
|
wrappers.forEach((wrapper, index) => {
|
||||||
|
const r = prevRects[index];
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
|
const clone = wrapper.cloneNode(true);
|
||||||
|
|
||||||
|
// Position & size BEFORE layout change
|
||||||
|
clone.style.position = 'absolute';
|
||||||
|
clone.style.left = `${r.left - containerRect.left}px`;
|
||||||
|
clone.style.top = `${r.top - containerRect.top}px`;
|
||||||
|
clone.style.width = `${r.width}px`;
|
||||||
|
clone.style.height = `${r.height}px`;
|
||||||
|
clone.style.margin = '0';
|
||||||
|
clone.style.boxSizing = 'border-box';
|
||||||
|
clone.style.transition = 'none';
|
||||||
|
|
||||||
|
clones.push(clone);
|
||||||
|
overlay.appendChild(clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Hide real items during animation to avoid flicker/misalignment
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.opacity = '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Apply NEW layout (on the real gallery, but hidden)
|
||||||
|
this._applyLayout(layout);
|
||||||
|
|
||||||
|
// 4) Measure final positions AFTER layout
|
||||||
|
const newRects = wrappers.map(w => w.getBoundingClientRect());
|
||||||
|
|
||||||
|
// 5) Animate clones from old to new rects
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
clones.forEach((clone, index) => {
|
||||||
|
const newRect = newRects[index];
|
||||||
|
const final = layout[index]; // final width/height from layout
|
||||||
|
if (!newRect || !final) return;
|
||||||
|
|
||||||
|
clone.style.transition = 'all 0.6s cubic-bezier(0.25, 1, 0.5, 1)';
|
||||||
|
|
||||||
|
// Final position
|
||||||
|
clone.style.left = `${newRect.left - containerRect.left}px`;
|
||||||
|
clone.style.top = `${newRect.top - containerRect.top }px`;
|
||||||
|
|
||||||
|
// Final size (should match real item => avoids jump)
|
||||||
|
clone.style.width = `${newRect.width }px`;
|
||||||
|
clone.style.height = `${newRect.height}px`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6) Cleanup: remove overlay & reveal real gallery
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.remove();
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.opacity = '1';
|
||||||
|
});
|
||||||
|
}, 650);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* Modal viewer
|
||||||
|
* ======================================================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup modal viewer if enabled.
|
||||||
|
*/
|
||||||
|
setupModal() {
|
||||||
|
if (!this.options.hasModal) return;
|
||||||
|
|
||||||
|
this.modal = document.createElement('div');
|
||||||
|
this.modal.classList.add('jg-modal');
|
||||||
|
this.modal.innerHTML = `
|
||||||
|
<span class="jg-close">×</span>
|
||||||
|
<img class="jg-modal-img" src="">
|
||||||
|
<div class="jg-controls">
|
||||||
|
<span class="jg-prev">❮</span>
|
||||||
|
<span class="jg-next">❯</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.modal);
|
||||||
|
|
||||||
|
this.modalImg = this.modal.querySelector('.jg-modal-img');
|
||||||
|
this.closeBtn = this.modal.querySelector('.jg-close');
|
||||||
|
this.prevBtn = this.modal.querySelector('.jg-prev');
|
||||||
|
this.nextBtn = this.modal.querySelector('.jg-next');
|
||||||
|
|
||||||
|
// Open modal on image click
|
||||||
|
this.images.forEach((img, idx) => {
|
||||||
|
img.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.openModal(idx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basic modal controls
|
||||||
|
this.closeBtn.addEventListener('click', () => this.closeModal());
|
||||||
|
this.prevBtn.addEventListener('click', () => this.prevImage());
|
||||||
|
this.nextBtn.addEventListener('click', () => this.nextImage());
|
||||||
|
|
||||||
|
// Close when clicking outside image
|
||||||
|
this.modal.addEventListener('click', e => {
|
||||||
|
if (e.target === this.modal) this.closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard navigation inside modal
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (this.modal?.style.display === 'flex') {
|
||||||
|
if (e.key === 'ArrowLeft') this.prevImage();
|
||||||
|
if (e.key === 'ArrowRight') this.nextImage();
|
||||||
|
if (e.key === 'Escape') this.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initModalInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize modal drag & zoom interactions.
|
||||||
|
*/
|
||||||
|
initModalInteractions() {
|
||||||
|
this.modalImg.currentScale = 1;
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let translateX = 0;
|
||||||
|
let translateY = 0;
|
||||||
|
|
||||||
|
// Mouse drag (panning)
|
||||||
|
this.modalImg.addEventListener('mousedown', e => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
this.modalImg.style.cursor = 'grabbing';
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.addEventListener('mousemove', e => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
translateX += dx;
|
||||||
|
translateY += dy;
|
||||||
|
this.modalImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${this.modalImg.currentScale})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop dragging on mouse up / leave
|
||||||
|
['mouseup', 'mouseleave'].forEach(event => {
|
||||||
|
this.modal.addEventListener(event, () => {
|
||||||
|
isDragging = false;
|
||||||
|
this.modalImg.style.cursor = 'grab';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to zoom
|
||||||
|
this.modalImg.addEventListener('wheel', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||||
|
this.modalImg.currentScale = Math.min(
|
||||||
|
Math.max(this.modalImg.currentScale + delta, 0.5),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
this.modalImg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${this.modalImg.currentScale})`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal(index) {
|
||||||
|
this.isOpenModal = true;
|
||||||
|
this.currentIndex = index;
|
||||||
|
this.modal.style.display = 'flex';
|
||||||
|
this.modalImg.src = this.images[this.currentIndex].src;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.isOpenModal = false;
|
||||||
|
this.modal.style.display = 'none';
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
this.modalImg.currentScale = 1;
|
||||||
|
this.modalImg.style.transform = 'translate(0px, 0px) scale(1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
prevImage() {
|
||||||
|
this.currentIndex =
|
||||||
|
(this.currentIndex - 1 + this.images.length) % this.images.length;
|
||||||
|
this.modalImg.src = this.images[this.currentIndex].src;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextImage() {
|
||||||
|
this.currentIndex =
|
||||||
|
(this.currentIndex + 1) % this.images.length;
|
||||||
|
this.modalImg.src = this.images[this.currentIndex].src;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* Private helpers
|
||||||
|
* ======================================================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {HTMLImageElement[]} - All images inside the container.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getImages() {
|
||||||
|
return Array.from(this.container.querySelectorAll('img'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {HTMLElement[]} - All elements marked as `.jg-clone` (if any).
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getImagesClones() {
|
||||||
|
return Array.from(this.container.querySelectorAll('.jg-clone'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an image inside an anchor and attach hover class.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_wrapImage(img) {
|
||||||
|
const href = img.getAttribute(this.options.linkAttribute) || img.src;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
|
||||||
|
a.href = href;
|
||||||
|
a.style.display = 'inline-block';
|
||||||
|
|
||||||
|
// Preserve caption if present
|
||||||
|
a.dataset.caption = img.dataset.caption || '';
|
||||||
|
img.dataset.caption = '';
|
||||||
|
|
||||||
|
// Attach hover class if defined
|
||||||
|
this._setHover(a);
|
||||||
|
|
||||||
|
img.parentNode.insertBefore(a, img);
|
||||||
|
a.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a hover class on the wrapper if hoverEffect is configured.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setHover(el) {
|
||||||
|
if (this.options.hoverEffect) {
|
||||||
|
el.classList.add(this.options.hoverEffect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current columns count according to container width & breakpoints.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getColumns() {
|
||||||
|
const width = this.container.clientWidth;
|
||||||
|
let cols = this.options.columns;
|
||||||
|
|
||||||
|
const breakpoints = Object
|
||||||
|
.keys(this.options.breakpoints)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const bp of breakpoints) {
|
||||||
|
if (width <= bp) {
|
||||||
|
cols = this.options.breakpoints[bp];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the max row height according to container width & maxRowHeightBreakpoints.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getMaxRowHeight() {
|
||||||
|
const width = this.container.clientWidth;
|
||||||
|
let maxHeight = this.options.maxRowHeight;
|
||||||
|
const breakpoints = this.options.maxRowHeightBreakpoints;
|
||||||
|
|
||||||
|
if (!breakpoints) return maxHeight;
|
||||||
|
|
||||||
|
const sorted = Object
|
||||||
|
.keys(breakpoints)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const bp of sorted) {
|
||||||
|
if (width <= bp) {
|
||||||
|
maxHeight = breakpoints[bp];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until an image is loaded (or fails) before resolving.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_waitForImageLoad(img) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (img.complete) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bounding rects of wrappers (anchors) before layout.
|
||||||
|
* @returns {DOMRect[]} rects ordered by image index
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getWrapperRects() {
|
||||||
|
return this.images.map(img => img.parentNode.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
<title>Test de création d'une gallerie justifié</title>
|
<title>Test de création d'une gallerie justifié</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
<link rel="stylesheet" href="assets/css/lib/gallery.css">
|
<link rel="stylesheet" href="assets/css/lib/gallery/gallery.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/lib/gallery/hover.css">
|
||||||
|
|
||||||
<script src="assets/js/index.js" type="module" defer></script>
|
<script src="assets/js/index.js" type="module" defer></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -22,6 +23,6 @@
|
|||||||
<img src="assets/img/PANA6308.jpg" alt="">
|
<img src="assets/img/PANA6308.jpg" alt="">
|
||||||
<img src="assets/img/PANA4649_1.jpg" alt="">
|
<img src="assets/img/PANA4649_1.jpg" alt="">
|
||||||
<img src="assets/img/PANA7846.jpg" alt="">
|
<img src="assets/img/PANA7846.jpg" alt="">
|
||||||
</di
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user