♻️ - 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", () => {
|
||||
let element = document.getElementById("gallery")
|
||||
|
||||
new JustifiedGallery(element, {
|
||||
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>
|
||||
|
||||
<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>
|
||||
</head>
|
||||
@@ -22,6 +23,6 @@
|
||||
<img src="assets/img/PANA6308.jpg" alt="">
|
||||
<img src="assets/img/PANA4649_1.jpg" alt="">
|
||||
<img src="assets/img/PANA7846.jpg" alt="">
|
||||
</di
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user