From 8f52f38ac5277606b0e755672b88639f71623ce3 Mon Sep 17 00:00:00 2001 From: Skycel Date: Sat, 15 Nov 2025 03:20:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20-=20Suppression=20de=20la?= =?UTF-8?q?=20classe=20JustifiedGallery=20et=20ajout=20de=20nouveaux=20eff?= =?UTF-8?q?ets=20de=20survol=20pour=20les=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/css/lib/{ => gallery}/gallery.css | 0 .../assets/css/lib/gallery/hover.css | 864 ++++++++++++++++++ JS/JustifedGallery/assets/js/index.js | 3 +- JS/JustifedGallery/assets/js/lib/gallery.js | 387 -------- .../assets/js/lib/gallery/effects/lens.js | 37 + .../js/lib/gallery/effects/ripple-animate.js | 19 + .../js/lib/gallery/effects/spotlight.js | 33 + .../assets/js/lib/gallery/gallery.js | 659 +++++++++++++ JS/JustifedGallery/index.html | 5 +- 9 files changed, 1617 insertions(+), 390 deletions(-) rename JS/JustifedGallery/assets/css/lib/{ => gallery}/gallery.css (100%) create mode 100644 JS/JustifedGallery/assets/css/lib/gallery/hover.css delete mode 100644 JS/JustifedGallery/assets/js/lib/gallery.js create mode 100644 JS/JustifedGallery/assets/js/lib/gallery/effects/lens.js create mode 100644 JS/JustifedGallery/assets/js/lib/gallery/effects/ripple-animate.js create mode 100644 JS/JustifedGallery/assets/js/lib/gallery/effects/spotlight.js create mode 100644 JS/JustifedGallery/assets/js/lib/gallery/gallery.js 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 @@ - \ No newline at end of file