animaciones

This commit is contained in:
2025-08-05 22:10:03 +02:00
parent 2a41db4dce
commit a21513943f
7 changed files with 542 additions and 46 deletions

View File

@@ -1,9 +1,17 @@
@if(thereIsFullscreenImage) {
<mid-res-image [src]="midResImage!.src" [alt]="midResImage!.alt" (closed)="onClose()"></mid-res-image>
<mid-res-image
[src]="midResImage!.src"
[alt]="midResImage!.alt"
(closed)="onClose()"
></mid-res-image>
}
<ul class="low-res-image-list">
@for (image of images; track image.id) {
<low-res-image [src]="image.src" [alt]="image.alt" (clicked)="onClickedImage($event)"></low-res-image>
<low-res-image
[src]="image.src"
[alt]="image.alt"
(clicked)="onClickedImageWithEvent($event)"
></low-res-image>
}
</ul>

View File

@@ -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 {

View File

@@ -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 */

View File

@@ -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<MidResImage>();
@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});
}
}

View File

@@ -1,7 +1,17 @@
<div class="mid-res-image-container">
<img [src]="src" [alt]="alt" class="img" loading="lazy" />
<div class="panel" (click)="openFullscreen($event)">
<div class="infoPanelMenu">
<div
class="mid-res-image-container"
[class.panel-shown]="!isInfoPanelHidden"
[class.panel-hidden]="isInfoPanelHidden"
(click)="closeFullscreen($event)"
>
<div class="image-container">
<img
[src]="src"
[alt]="alt"
loading="lazy"
(click)="$event.stopPropagation()"
/>
</div>
@if(isInfoPanelHidden) {
<app-button
label="ShowInfoPanel"
@@ -11,6 +21,8 @@
icon="assets/icons/push-left-svgrepo-com.svg"
></app-button>
} @else {
<div class="panel" (click)="$event.stopPropagation()">
<div class="infoPanelMenu">
<app-button
label="HideInfoPanel"
(click)="hideInfoPanel($event)"
@@ -18,7 +30,6 @@
tooltip="Hide Image Info"
icon="assets/icons/push-right-svgrepo-com.svg"
></app-button>
}
<app-button
label="CloseFullscreen"
(click)="closeFullscreen($event)"
@@ -72,14 +83,15 @@
></app-button>
</div>
<div class="comment-section">
<div class="header">
<!-- <div class="header">
<h4>Comment</h4>
<button (click)="toggleCommentForm($event)">Add Comment</button>
</div>
<div class="content">
<h4>Comments</h4>
<div class="list"></div>
</div> -->
</div>
</div>
</div>
}
</div>

View File

@@ -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;
}

View File

@@ -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<void>();
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
}
}