diff --git a/JS/JustifedGallery/assets/css/lib/gallery.css b/JS/JustifedGallery/assets/css/lib/gallery/gallery.css
similarity index 100%
rename from JS/JustifedGallery/assets/css/lib/gallery.css
rename to JS/JustifedGallery/assets/css/lib/gallery/gallery.css
diff --git a/JS/JustifedGallery/assets/css/lib/gallery/hover.css b/JS/JustifedGallery/assets/css/lib/gallery/hover.css
new file mode 100644
index 0000000..6e87312
--- /dev/null
+++ b/JS/JustifedGallery/assets/css/lib/gallery/hover.css
@@ -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;
+}
\ No newline at end of file
diff --git a/JS/JustifedGallery/assets/js/index.js b/JS/JustifedGallery/assets/js/index.js
index 08ba64d..103f508 100644
--- a/JS/JustifedGallery/assets/js/index.js
+++ b/JS/JustifedGallery/assets/js/index.js
@@ -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",
});
})
diff --git a/JS/JustifedGallery/assets/js/lib/gallery.js b/JS/JustifedGallery/assets/js/lib/gallery.js
deleted file mode 100644
index 56fc447..0000000
--- a/JS/JustifedGallery/assets/js/lib/gallery.js
+++ /dev/null
@@ -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 = `
- ×
-
-
- ❮
- ❯
-
- `;
- 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`;
- });
- }
-}
-
diff --git a/JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js b/JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js
new file mode 100644
index 0000000..f700155
--- /dev/null
+++ b/JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js
@@ -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');
+ });
+ });
+}
\ No newline at end of file
diff --git a/JS/JustifedGallery/assets/js/lib/gallery/effects/ripple-animate.js b/JS/JustifedGallery/assets/js/lib/gallery/effects/ripple-animate.js
new file mode 100644
index 0000000..25d6546
--- /dev/null
+++ b/JS/JustifedGallery/assets/js/lib/gallery/effects/ripple-animate.js
@@ -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());
+ });
+ });
+}
\ No newline at end of file
diff --git a/JS/JustifedGallery/assets/js/lib/gallery/effects/spotlight.js b/JS/JustifedGallery/assets/js/lib/gallery/effects/spotlight.js
new file mode 100644
index 0000000..7ffeabd
--- /dev/null
+++ b/JS/JustifedGallery/assets/js/lib/gallery/effects/spotlight.js
@@ -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%
+ )`;
+ });
+ });
+}
diff --git a/JS/JustifedGallery/assets/js/lib/gallery/gallery.js b/JS/JustifedGallery/assets/js/lib/gallery/gallery.js
new file mode 100644
index 0000000..a00b06a
--- /dev/null
+++ b/JS/JustifedGallery/assets/js/lib/gallery/gallery.js
@@ -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 () and attach hover class
+ this.images.forEach(img => this._wrapImage(img));
+
+ // Refresh internal image list (now wrapped by )
+ 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 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 = `
+ ×
+
+
+ ❮
+ ❯
+
+ `;
+ 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());
+ }
+}
diff --git a/JS/JustifedGallery/index.html b/JS/JustifedGallery/index.html
index a33301d..333e1f1 100644
--- a/JS/JustifedGallery/index.html
+++ b/JS/JustifedGallery/index.html
@@ -8,7 +8,8 @@
Test de création d'une gallerie justifié
-
+
+
@@ -22,6 +23,6 @@
-