diff --git a/front/src/app/gallery/gallery.html b/front/src/app/gallery/gallery.html index 100b532..ceddc89 100644 --- a/front/src/app/gallery/gallery.html +++ b/front/src/app/gallery/gallery.html @@ -1,9 +1,17 @@ @if(thereIsFullscreenImage) { - + } diff --git a/front/src/app/gallery/gallery.ts b/front/src/app/gallery/gallery.ts index ec6cec4..33fb3f8 100644 --- a/front/src/app/gallery/gallery.ts +++ b/front/src/app/gallery/gallery.ts @@ -13,14 +13,47 @@ export class Gallery { protected images : LowResImage[] = []; thereIsFullscreenImage: boolean = false; midResImage: MidResImage | null = null; + isAnimatingToFullscreen: boolean = false; constructor() { this.loadImages(); } - onClickedImage(midResImage: MidResImage): void { + onClickedImage(midResImage: MidResImage, event?: MouseEvent): void { + // Capturar información de la animación si hay evento disponible + if (event && event.target) { + this.prepareImageTransition(event.target as HTMLElement); + } + + this.isAnimatingToFullscreen = true; this.midResImage = midResImage; this.thereIsFullscreenImage = this.midResImage !== null; + + // Resetear el estado de animación después de un breve delay + setTimeout(() => { + this.isAnimatingToFullscreen = false; + }, 100); + } + + onClickedImageWithEvent(data: {midResImage: MidResImage, event: MouseEvent}): void { + this.onClickedImage(data.midResImage, data.event); + } + + private prepareImageTransition(imageElement: HTMLElement): void { + // Capturar la posición inicial de la imagen para animación coordinada + const rect = imageElement.getBoundingClientRect(); + const initialPosition = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + width: rect.width, + height: rect.height + }; + + // Guardar información de transición en el document para uso del mid-res-image + (document as any).__imageTransition = { + ...initialPosition, + timestamp: Date.now() + }; } onClose(): void { diff --git a/front/src/app/low-res-image/low-res-image.scss b/front/src/app/low-res-image/low-res-image.scss index 4f44501..66b4564 100644 --- a/front/src/app/low-res-image/low-res-image.scss +++ b/front/src/app/low-res-image/low-res-image.scss @@ -11,6 +11,7 @@ $gap-size: 20px; transition: box-shadow 0.3s ease, transform 0.3s ease; margin-top: calc($gap-size / 2); margin-bottom: calc($gap-size / 2); + position: relative; } .low-res-image-container:hover { @@ -18,11 +19,22 @@ $gap-size: 20px; transform: scale(1.15); } +.low-res-image-container:active { + transform: scale(1.05); + transition: transform 0.1s ease; +} + .low-res-image { width: 100%; height: auto; object-fit: cover; display: block; + transition: transform 0.3s ease, filter 0.3s ease; +} + +.low-res-image-container:hover .low-res-image { + transform: scale(1.02); + filter: brightness(1.1); } /* Tablets in landscape - 1 column */ diff --git a/front/src/app/low-res-image/low-res-image.ts b/front/src/app/low-res-image/low-res-image.ts index ce00989..5bdc0e5 100644 --- a/front/src/app/low-res-image/low-res-image.ts +++ b/front/src/app/low-res-image/low-res-image.ts @@ -12,13 +12,13 @@ export class LowResImage { @Input({ required: true }) alt: string = ''; public id : string = 'low-res-image-' + Math.random().toString(36).substring(2, 15); - @Output() clicked = new EventEmitter(); + @Output() clicked = new EventEmitter<{midResImage: MidResImage, event: MouseEvent}>(); openFullscreen(event: MouseEvent): void { event.stopPropagation(); const midResImage = new MidResImage(); midResImage.src = this.src.replace('low', 'mid'); midResImage.alt = this.alt; - this.clicked.emit(midResImage); + this.clicked.emit({midResImage, event}); } } diff --git a/front/src/app/mid-res-image/mid-res-image.html b/front/src/app/mid-res-image/mid-res-image.html index cd12b14..2bf0628 100644 --- a/front/src/app/mid-res-image/mid-res-image.html +++ b/front/src/app/mid-res-image/mid-res-image.html @@ -1,16 +1,28 @@ -
- -
+
+
+ +
+ @if(isInfoPanelHidden) { + + } @else { +
- @if(isInfoPanelHidden) { - - } @else{ - }
-
+
+ }
diff --git a/front/src/app/mid-res-image/mid-res-image.scss b/front/src/app/mid-res-image/mid-res-image.scss index 32dc007..01cc393 100644 --- a/front/src/app/mid-res-image/mid-res-image.scss +++ b/front/src/app/mid-res-image/mid-res-image.scss @@ -4,27 +4,329 @@ position: fixed; top: 0; left: 0; - width: 100vw; - height: 100vh; + width: 100dvw; + height: 100dvh; background-color: rgba(42, 41, 38, 0.95); display: flex; z-index: 1000; + flex-grow: 1; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); + // Entrada animada del contenedor + animation: fadeInContainer 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; + + // Animación coordinada desde la posición de la imagen clickeada + &.coordinated-animation { + animation: fadeInContainerCoordinated 0.5s cubic-bezier(0.4, 0, 0.2, 1) + forwards; + + .image-container { + animation: coordinatedImageTransition 0.7s + cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } + + .panel { + animation: slideInPanelDelayed 0.5s cubic-bezier(0.4, 0, 0.2, 1) + 0.4s both; + } + + .show-button { + animation: fadeInButtonDelayed 0.4s cubic-bezier(0.4, 0, 0.2, 1) + 0.5s both; + } + } + + // Animaciones de salida coordinada + &.coordinated-exit { + animation: fadeOutContainerCoordinated 0.6s cubic-bezier(0.4, 0, 0.2, 1) + forwards; + + .image-container { + animation: coordinatedImageExitTransition 0.6s + cubic-bezier(0.7, 0, 0.84, 0) forwards; + } + + .panel { + animation: slideOutPanel 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .show-button { + animation: fadeOutButton 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + } + + // Animación de salida simple (sin coordinación) + &.simple-exit { + animation: fadeOutContainerSimple 0.4s cubic-bezier(0.4, 0, 0.2, 1) + forwards; + + .image-container { + animation: zoomOutImage 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .panel { + animation: slideOutPanel 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .show-button { + animation: fadeOutButton 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + } + + @keyframes fadeInContainer { + from { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); + } + to { + opacity: 1; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + } + // Image section - .img { + .image-container { width: 70%; - height: 100vh; - object-fit: contain; - cursor: zoom-in; - transition: transform 0.3s ease; + height: 100dvh; + transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + + // Animación de entrada de la imagen + animation: slideInImage 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; + + img { + width: fit-content; + height: fit-content; + max-width: 100%; + max-height: 100%; + object-fit: scale-down; + + // Animación de zoom elegante + animation: zoomInImage 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) + forwards; + } + } + + @keyframes slideInImage { + from { + transform: translateX(-30px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes zoomInImage { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } + } + + // When panel is hidden, image takes full width + &.panel-hidden { + .image-container { + width: 100%; + } + + .panel { + transform: translateX(100%); + opacity: 0; + } + } + + &.panel-shown { + .image-container { + width: 75%; + } + .panel { + width: 25%; + transform: translateX(0); + opacity: 1; + } + } + + // Show panel button - positioned to always be visible + .show-button { + position: fixed; + top: 4rem; + right: 4rem; + z-index: 1001; + padding: 0; + margin: 0; + + // Animación de entrada del botón con retraso + animation: fadeInButton 0.4s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both; + } + + @keyframes fadeInButton { + from { + transform: translateY(-20px) scale(0.8); + opacity: 0; + } + to { + transform: translateY(0) scale(1); + opacity: 1; + } + } + + // Animaciones coordinadas para transición suave desde galería + @keyframes fadeInContainerCoordinated { + from { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); + } + to { + opacity: 1; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + } + + @keyframes coordinatedImageTransition { + from { + transform: translate( + calc(var(--start-x, 50vw) - 50vw), + calc(var(--start-y, 50vh) - 50vh) + ) + scale(calc(var(--start-width, 200px) / 400)); + opacity: 0.8; + } + 50% { + transform: translate( + calc((var(--start-x, 50vw) - 50vw) * 0.3), + calc((var(--start-y, 50vh) - 50vh) * 0.3) + ) + scale(0.9); + opacity: 0.9; + } + to { + transform: translate(0, 0) scale(1); + opacity: 1; + } + } + + @keyframes slideInPanelDelayed { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes fadeInButtonDelayed { + from { + transform: translateY(-20px) scale(0.8); + opacity: 0; + } + to { + transform: translateY(0) scale(1); + opacity: 1; + } + } + + // Animaciones de salida coordinada + @keyframes fadeOutContainerCoordinated { + from { + opacity: 1; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + to { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); + } + } + + @keyframes coordinatedImageExitTransition { + from { + transform: translate(0, 0) scale(1); + opacity: 1; + } + 50% { + transform: translate( + calc((var(--end-x, 50vw) - 50vw) * 0.3), + calc((var(--end-y, 50vh) - 50vh) * 0.3) + ) + scale(0.7); + opacity: 0.8; + } + to { + transform: translate( + calc(var(--end-x, 50vw) - 50vw), + calc(var(--end-y, 50vh) - 50vh) + ) + scale(calc(var(--end-width, 200px) / 400)); + opacity: 0; + } + } + + @keyframes slideOutPanel { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + + @keyframes fadeOutButton { + from { + transform: translateY(0) scale(1); + opacity: 1; + } + to { + transform: translateY(-20px) scale(0.8); + opacity: 0; + } + } + + // Animaciones de salida simple + @keyframes fadeOutContainerSimple { + from { + opacity: 1; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + } + to { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); + } + } + + @keyframes zoomOutImage { + from { + transform: scale(1); + opacity: 1; + } + to { + transform: scale(0.8); + opacity: 0; + } } // Panel section .panel { - width: 30%; - height: 100vh; + height: 100dvh; background: variables.$primary-white; overflow-y: auto; display: flex; @@ -32,6 +334,12 @@ box-shadow: -4px 0 20px rgba(42, 41, 38, 0.2); border-left: 1px solid variables.$border-grey; padding: 2%; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), + width 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + // Animación de entrada del panel + animation: slideInPanel 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both; .infoPanelMenu { display: flex; @@ -178,11 +486,22 @@ } } + @keyframes slideInPanel { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + // Responsive design @media (max-width: 768px) { flex-direction: column; - .img { + .image-container { width: 100%; height: 60vh; } @@ -195,10 +514,17 @@ min-width: 100%; } } + + // Adjust show button position for mobile + .show-button { + top: 1rem; + right: 1rem; + padding: 0.5rem; + } } @media (max-width: 480px) { - .img { + .image-container { height: 50vh; } diff --git a/front/src/app/mid-res-image/mid-res-image.ts b/front/src/app/mid-res-image/mid-res-image.ts index d746601..4a51f98 100644 --- a/front/src/app/mid-res-image/mid-res-image.ts +++ b/front/src/app/mid-res-image/mid-res-image.ts @@ -1,14 +1,23 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { SvgLoader } from '../svg/svg'; +import { + Component, + EventEmitter, + Input, + Output, + OnInit, + OnDestroy, + HostListener, +} from '@angular/core'; import { Button } from '../button/button'; +// view: src/app/mid-res-image/mid-res-image.html + @Component({ selector: 'mid-res-image', imports: [Button], templateUrl: './mid-res-image.html', styleUrl: './mid-res-image.scss', }) -export class MidResImage { +export class MidResImage implements OnInit, OnDestroy { @Input({ required: true }) src: string = ''; @Input({ required: true }) alt: string = ''; @Input() title: string = 'Title'; @@ -17,25 +26,126 @@ export class MidResImage { @Output() closed = new EventEmitter(); isInfoPanelHidden: boolean = false; + hasCoordinatedAnimation: boolean = false; + private originalPosition: any = null; comments: string[] = []; + @HostListener('document:keydown', ['$event']) + handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + this.closeFullscreenAnimated(); + } + } + + ngOnInit(): void { + // Bloquear el scroll del body cuando se abre la imagen en pantalla completa + document.body.style.overflow = 'hidden'; + + // Verificar si hay información de transición disponible + const transitionData = (document as any).__imageTransition; + if (transitionData && Date.now() - transitionData.timestamp < 200) { + this.hasCoordinatedAnimation = true; + this.setupCoordinatedAnimation(transitionData); + // Limpiar la información de transición + delete (document as any).__imageTransition; + } + } + + private setupCoordinatedAnimation(transitionData: any): void { + // Guardar la posición original para la animación de salida + this.originalPosition = { ...transitionData }; + + // Aplicar CSS variables para la animación coordinada + const container = document.querySelector( + '.mid-res-image-container' + ) as HTMLElement; + if (container) { + container.style.setProperty('--start-x', `${transitionData.x}px`); + container.style.setProperty('--start-y', `${transitionData.y}px`); + container.style.setProperty( + '--start-width', + `${transitionData.width}px` + ); + container.style.setProperty( + '--start-height', + `${transitionData.height}px` + ); + container.classList.add('coordinated-animation'); + } + } + + ngOnDestroy(): void { + // Restaurar el scroll del body cuando se destruye el componente + document.body.style.overflow = 'auto'; + } + hideInfoPanel(event: MouseEvent): void { event.stopPropagation(); - this.isInfoPanelHidden = !this.isInfoPanelHidden; + this.isInfoPanelHidden = true; + // tienes que coger el div 'panel' y cambiar su display a none + // Esto es para que no se vea el panel de información cuando se cierra + // Ademas, tienes que mostrar un botón para mostrar de nuevo el panel + document + .querySelector('.panel') + ?.setAttribute('style', 'display: none;'); } showInfoPanel(event: MouseEvent): void { event.stopPropagation(); - this.isInfoPanelHidden = !this.isInfoPanelHidden; + this.isInfoPanelHidden = false; } closeFullscreen(event: MouseEvent): void { event.stopPropagation(); + this.closeFullscreenAnimated(); } - openFullscreen(event: MouseEvent): void { - event.stopPropagation(); + private closeFullscreenAnimated(): void { + // Iniciar animación de salida + this.startExitAnimation(); + + // Emitir el evento de cerrado después de la animación + setTimeout(() => { + // Restaurar el scroll del body + document.body.style.overflow = 'auto'; + this.closed.emit(); + }, 600); // Duración de la animación de salida + } + + private startExitAnimation(): void { + const container = document.querySelector( + '.mid-res-image-container' + ) as HTMLElement; + if (container) { + // Remover animaciones de entrada + container.classList.remove('coordinated-animation'); + + // Si tenemos información de la posición original, configurar para retorno coordinado + if (this.hasCoordinatedAnimation && this.originalPosition) { + // Volver a establecer las variables CSS para la animación de retorno + container.style.setProperty( + '--end-x', + `${this.originalPosition.x}px` + ); + container.style.setProperty( + '--end-y', + `${this.originalPosition.y}px` + ); + container.style.setProperty( + '--end-width', + `${this.originalPosition.width}px` + ); + container.style.setProperty( + '--end-height', + `${this.originalPosition.height}px` + ); + container.classList.add('coordinated-exit'); + } else { + // Animación de salida simple si no hay coordinación + container.classList.add('simple-exit'); + } + } } editImage(event: MouseEvent): void { @@ -48,21 +158,16 @@ export class MidResImage { downloadImage(event: MouseEvent): void { event.stopPropagation(); + // debe descargar la imagen en resolución media (720p) } shareImage(event: MouseEvent): void { event.stopPropagation(); + // tiene que abrir un modal para compartir la imagen en redes sociales } buyImage(event: MouseEvent): void { event.stopPropagation(); - } - - toggleCommentForm(event: MouseEvent): void { - event.stopPropagation(); - } - - close() { - this.closed.emit(); + // tiene que abrir un modal para comprar la imagen } }