Friday, June 28, 2019

create photo grid - photo collage - photo album in angular

A photo collage / photo grid is a collection of different photos in one like as album type which display all or some images in single. In this article / blog we will create a Photo Collage / Photo Grid using Angular 7. You can also use it in any other angular versions like as angular 2, angular 4, angular 5, angular 6, angular 7, angular 8, etc.

Here, we will make Flexible Layout and Grid Layout for Angular Component for photo collage / photo grid like as facebook style photos grid without any dependencies.

Demo:
Directory structure in app directory

=>(src/app/photo-collage)
photo-collage.component.spec.ts
photo-collage.component.css
photo-collage.component.html
photo-collage.module.ts
photo-collage.component.ts

=>(src/app/news-feed-post)
news-feed-post.module.ts
news-feed-post.component.html
news-feed-post.component.css
news-feed-post.component.ts
news-feed-post.component.spec.ts

photo-collage.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {UIRouterModule} from "@uirouter/angular";
import { PhotoCollageComponent } from './photo-collage.component';

@NgModule({
  imports: [
    CommonModule,
    UIRouterModule
  ],
  declarations: [PhotoCollageComponent],
  exports: [PhotoCollageComponent]
})
export class PhotoCollageModule { }

photo-collage.component.ts
import { Component, OnInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';

declare const $: any;

@Component({
    selector: 'app-photo-collage',
    templateUrl: './photo-collage.component.html',
    styleUrls: ['./photo-collage.component.css'],
    encapsulation: ViewEncapsulation.None
})
export class PhotoCollageComponent implements OnInit {

  @Input() imageArr: any =[];
  @Input() maxLength: number = 5;
  @Input() dirPath: string = "/uploads/";

  @Output() photoGridClicked = new EventEmitter();

    loadedImages = [];
    loadedTakenImages = [];
    parentStyle: any;
    takenImages: any;

    IS_SQUARE = true;
    MARGIN = 5;
    GRID_WIDTH = 500;

    commonStyle: any = {
        display: 'block',
        overflow: 'hidden',
        cssFloat: 'left',
        cursor: 'pointer',
        position: 'relative'
    };

    constructor() {

    }

    ngOnInit() {
        this.preloadImages();
    }

    preloadImages() {

        //console.log("imageArr ::::::::::::::::",this.imageArr);

        this.takenImages = this.imageArr.slice(0, this.maxLength);
        let len = this.takenImages.length;
        this.takenImages.map((image, index) => {
            let img;
            img = new Image();
            img.onload = (loadedImage) => {
                this.loadedTakenImages.push(loadedImage);
                // store the original dimension of image
                image.naturalWidth = loadedImage.target.naturalWidth;
                image.naturalHeight = loadedImage.target.naturalHeight;

                if (this.loadedTakenImages.length === len) {
                    this.buildPhotoCollageGrid();
                }
            };
            img.onerror = () => {
                image.naturalWidth = 200;
                image.naturalHeight = 200;
                this.loadedTakenImages.push({});
             
                if (this.loadedTakenImages.length === len) {
                    this.buildPhotoCollageGrid();
                }
            };
            img.src = this.dirPath+image.photo_name;
        });
    };

    buildPhotoCollageGrid = function () {
        this.parentStyle = { width: this.GRID_WIDTH + "px", overflow: "hidden", position: "relative", margin: 0, padding: 0 }

        var firstImage, imageStyle, smallCellHeight,
            smallCellWidth, bigCellWidth, bigCellHeight, cellCount, is2First;

        // get cell style & build options
        var styles = this.getPhotoCollageCellStyles();
        smallCellHeight = styles.options.smallCellHeight;
        smallCellWidth = styles.options.smallCellWidth;
        bigCellWidth = styles.options.bigCellWidth;
        bigCellHeight = styles.options.bigCellHeight;
        cellCount = styles.options.cellCount;
        is2First = styles.options.is2First;

        //console.log("this.takenImages>>>>>>>>>>>>>>>@@@", this.takenImages);

        this.takenImages.map((image, index) => {
            console.log("image loop....", index, image);
            if (is2First) { //case the grid has 2 image big first
                var bigCellStyle, smallCellStyle;
                bigCellStyle = $.extend({}, styles.big);
                smallCellStyle = $.extend({}, styles.small);
                if (index == 0) {
                    bigCellStyle.top = "0";
                    image.cellStyle = bigCellStyle;
                } else if (index == 1) {
                    bigCellStyle.top = bigCellHeight + this.MARGIN + "px";
                    image.cellStyle = bigCellStyle;
                } else {
                    var margin, smallCellIndex;

                    // fix the last cell of 2 first was not fit the grid height
                    if (index == this.takenImages.length - 1) {
                        smallCellStyle.height = smallCellHeight + this.MARGIN + "px"
                    }

                    smallCellIndex = index - 2;
                    margin = smallCellIndex == 0 ? 0 : this.MARGIN;
                    smallCellStyle.top = smallCellIndex * smallCellHeight + (margin * smallCellIndex) + "px";
                    image.cellStyle = smallCellStyle;

                }
            } else if (index == 0) { //big cell style
                image.cellStyle = styles.big;
            } else if (index != cellCount - 1 || cellCount == 2) { //small cells
                image.cellStyle = styles.small;
            } else { //last small cell style (remove redundant margin right or bottom)
                image.cellStyle = styles.last;
            }

            if (cellCount === 1 && image.naturalWidth < this.GRID_WIDTH && image.naturalHeight < this.GRID_WIDTH) {
                //Remove height and width for display image like as original size
                image.cellStyle.width = image.cellStyle.height = undefined;
                //here Math.min is used to get maximum 1000 means width and height must be less than or equal to 1000.
                image.scaleSize = Math.min((Math.ceil(parseInt(image.naturalWidth) / 50) * 50), 1000) + "x" + Math.min((Math.ceil(parseInt(image.naturalHeight) / 50) * 50), 1000);
                image.imageStyle = { width: image.naturalWidth + "px", height: image.naturalHeight + "px" };
            } else {
                //get expected ratio if image is more than 1.
                //here Math.min is used to get maximum 1000 means width and height must be less than or equal to 1000.
                image.scaleSize = Math.min((Math.ceil(parseInt(image.cellStyle.width.split("px")[0]) / 50) * 50), 1000) + "x" + Math.min((Math.ceil(parseInt(image.cellStyle.height.split("px")[0]) / 50) * 50), 1000);
                image.imageStyle = { width: "100%", height: "100%" };
            }
        });
        this.loadedImages = this.takenImages;
        console.log("loadedImages:", this.loadedImages);
    }

    buildPhotoCollageCellStyle(firstImage, secondImage, cellCount) {
        var firstRatio, secondRatio, bigCellStyle, smallCellStyle, lastCellStyle,
            WIDTH_RATE, bigCellWidth, bigCellHeight, smallCellHeight, smallCellWidth, is2First,
            case2BigImage1, case2BigImage2;
        firstRatio = firstImage.naturalWidth / firstImage.naturalHeight;

        if (secondImage)
            secondRatio = secondImage.naturalWidth / secondImage.naturalHeight;
        else
            secondRatio = 1.5 //fail all cases below

        bigCellStyle = $.extend({}, this.commonStyle);
        smallCellStyle = $.extend({}, this.commonStyle);
        lastCellStyle = $.extend({}, this.commonStyle);
        WIDTH_RATE = this.getWidthRate(firstRatio, cellCount);
        case2BigImage1 = firstRatio > 0.8 && firstRatio < 1.2 &&
            secondRatio > 0.8 && secondRatio < 1.2
        case2BigImage2 = firstRatio >= 2 && secondRatio >= 2

        if (cellCount == 2) { //build style for grid has 2 images and first image has firstRatio > 1

            if (firstRatio > 1.3) {
                bigCellStyle.marginBottom = this.MARGIN;
                bigCellStyle.width = this.GRID_WIDTH;
                bigCellStyle.height = this.GRID_WIDTH / 2;
                smallCellStyle.width = this.GRID_WIDTH;
                smallCellStyle.height = this.GRID_WIDTH / 2 - this.MARGIN;
            } else {
                var marginSize = this.MARGIN / cellCount;
                bigCellStyle.marginRight = marginSize;
                smallCellStyle.marginLeft = marginSize;

                if (this.IS_SQUARE) {
                    bigCellWidth = Math.floor(this.GRID_WIDTH / 2) - this.MARGIN;
                    bigCellStyle.width = bigCellWidth;
                    bigCellStyle.height = this.GRID_WIDTH;

                    smallCellWidth = Math.floor(this.GRID_WIDTH / 2) - this.MARGIN;
                    smallCellStyle.width = smallCellWidth;
                    smallCellStyle.height = this.GRID_WIDTH;
                } else {
                    bigCellWidth = Math.floor(this.GRID_WIDTH * WIDTH_RATE) - this.MARGIN;
                    bigCellStyle.width = bigCellWidth;
                    bigCellStyle.height = bigCellWidth;

                    smallCellWidth = this.GRID_WIDTH - bigCellWidth - this.MARGIN;
                    smallCellHeight = bigCellWidth;
                    smallCellStyle.width = smallCellWidth;
                    smallCellStyle.height = smallCellHeight;
                }
            }
        }
        // add style for first column contain 2 big images, only support for grid has more than 4 cells
        //NOTE: need check when 2 first were same size!!!
        else if (cellCount >= 4 && (case2BigImage1 || case2BigImage2)) {
            var GRID_HEIGHT;
            WIDTH_RATE = case2BigImage1 ? 1 / 2 : 2 / 3;
            this.parentStyle.position = "relative";
            bigCellStyle.cssFloat = smallCellStyle.cssFloat = lastCellStyle.cssFloat = null;
            bigCellStyle.position = smallCellStyle.position = lastCellStyle.position = "absolute";

            //determine the height of the big cell
            //height == width / 2 if the grid in case2BigImage1
            if (case2BigImage1) {
                bigCellHeight = this.GRID_WIDTH / 2;
            } else {
                bigCellHeight = WIDTH_RATE * this.GRID_WIDTH / firstRatio;
            }

            GRID_HEIGHT = bigCellHeight * 2 + this.MARGIN; //margin bottom the first big image
            this.parentStyle.height = GRID_HEIGHT + "px";

            bigCellStyle.width = this.GRID_WIDTH * WIDTH_RATE - this.MARGIN;
            bigCellStyle.height = bigCellHeight;
            bigCellStyle.left = 0;

            smallCellStyle.width = this.GRID_WIDTH - bigCellStyle.width - this.MARGIN;
            smallCellStyle.height = Math.floor((GRID_HEIGHT / (cellCount - 2))) - this.MARGIN;
            smallCellStyle.right = 0;

            is2First = true; //flag this style is has 2 big image style
            lastCellStyle.height = smallCellStyle.height + this.MARGIN;

        } else if (firstRatio > 1.3) { //build style for grid more than 2 images and first image has firstRatio > 1 (if uncomment this line then comment next line)

            bigCellStyle.marginBottom = this.MARGIN;
            smallCellStyle.marginRight = this.MARGIN;
            var smallCellCount = cellCount - 1;

            if (this.IS_SQUARE) {
                bigCellStyle.height = this.GRID_WIDTH * 2 / 3;
                bigCellStyle.width = this.GRID_WIDTH;
                smallCellStyle.height = this.GRID_WIDTH * 1 / 3 - this.MARGIN;
            } else {
                bigCellStyle.width = this.GRID_WIDTH;
                bigCellStyle.height = this.GRID_WIDTH * 2 / 4; //For decrease hight of big images cell (bigCellStyle.height = this.GRID_WIDTH * 2 / 3;).
            }
            var maxSmallCellHeight = bigCellStyle.height / 2;
            smallCellStyle.width = (this.GRID_WIDTH - smallCellCount * this.MARGIN) / smallCellCount;
            // determine the height of smallCell below
            if (this.IS_SQUARE) {
                smallCellStyle.height = this.GRID_WIDTH - bigCellStyle.height - this.MARGIN;
            } else if (firstRatio > 1.3 && firstRatio < 1.5) { // 4:3 < firstRatio < 5:3
                smallCellStyle.height = Math.min(smallCellStyle.width / firstRatio, maxSmallCellHeight);
            } else if (firstRatio > 1.5) {
                smallCellStyle.height = Math.min(smallCellStyle.width / 1.5, maxSmallCellHeight);
            }
            else {
                smallCellStyle.height = Math.min(smallCellStyle.width, maxSmallCellHeight);
            }

            lastCellStyle.height = smallCellStyle.height;
            lastCellStyle.width = smallCellStyle.width;
        } else { //build style for grid more than 2 images and first image has firstRatio <= 1
            //This condition true for make vertical collage(image must be greater than 2)(collage in tow column)

            bigCellStyle.marginRight = this.MARGIN;
            smallCellStyle.marginBottom = this.MARGIN;

            if (this.IS_SQUARE) {
                bigCellHeight = this.GRID_WIDTH;
                bigCellWidth = this.GRID_WIDTH * WIDTH_RATE;
            } else {
                bigCellWidth = Math.floor(this.GRID_WIDTH * WIDTH_RATE);
                bigCellHeight = Math.min(bigCellWidth / firstRatio, this.GRID_WIDTH);
                if (cellCount === 3) {
                    bigCellWidth = bigCellWidth * 0.85;
                    bigCellHeight = bigCellHeight * 0.80;
                }
            }
         
            bigCellStyle.width = bigCellWidth;
            bigCellStyle.height = bigCellHeight;

            smallCellCount = cellCount - 1;
            smallCellWidth = this.GRID_WIDTH - bigCellWidth - this.MARGIN;
            smallCellHeight = bigCellHeight / smallCellCount - this.MARGIN

            smallCellStyle.width = this.GRID_WIDTH - bigCellWidth - this.MARGIN;
            smallCellStyle.height = smallCellHeight;
            lastCellStyle.width = smallCellWidth;
            lastCellStyle.height = smallCellHeight;
        }

        return {
            big: bigCellStyle,
            small: smallCellStyle,
            last: lastCellStyle,
            options: {
                firstRatio: firstRatio,
                smallCellWidth: smallCellStyle.width,
                smallCellHeight: smallCellStyle.height,
                bigCellWidth: bigCellStyle.width,
                bigCellHeight: bigCellStyle.height,
                cellCount: cellCount,
                is2First: is2First
            }
        }
    };

    getWidthRate(firstRatio, cellCount) {
        if (cellCount == 2) { //build style for 2 images
            if (firstRatio > 1.3) {
                return 2 / 3;
            } else {
                return 1 / 2;
            }
        } else if (firstRatio > 1.3) {
            //build style for >= 3 images, first image has firstRatio > 1
            return 1
        } else {
            //build style for >= 3 images, first image has firstRatio < 1
            return 2 / 3
        }
    };

    getPhotoCollageCellStyles() {
        var firstImage, secondImage, cellCount, buildedStyle;

        firstImage = this.takenImages[0];
        secondImage = this.takenImages[1];
        cellCount = this.takenImages.length;

        buildedStyle = this.buildPhotoCollageCellStyle(firstImage, secondImage, cellCount);

        // remove margin right of last small cell in the bottom
        if (buildedStyle.small.marginRight) {
            buildedStyle.last.marginRight = 0;
            buildedStyle.last.width = buildedStyle.small.width + this.MARGIN;
        }

        // remove margin bottom of last small cell in the right
        if (buildedStyle.small.marginBottom) {
            buildedStyle.last.marginBottom = 0;
            buildedStyle.last.height = buildedStyle.small.height + this.MARGIN;
        }

        // add suffix px for margin and size for ng-style working
        var attrs = ["width", "height", "marginRight", "marginLeft", "marginBottom", "left", "right"];
        attrs.map((attr, index) => {
            if (buildedStyle.big[attr]) {
                buildedStyle.big[attr] += "px";
            }
            if (buildedStyle.small[attr]) {
                buildedStyle.small[attr] += "px";
            }
            if (buildedStyle.last[attr]) {
                buildedStyle.last[attr] += "px";
            }
        })
        return buildedStyle;
    };

}

photo-collage.component.html
<ul class='photo-grid-wrapper' [ngStyle] = 'parentStyle'>
    <li class='grid-cell' *ngFor= 'let image of loadedImages; let index = index;  let isLast = last' [ngStyle] = 'image.cellStyle' (click)='photoGridClicked.next({imageArr : imageArr, index : index})'>
      <img class='grid-cell-image' [ngStyle] = 'image.imageStyle' [src]="dirPath+image.photo_name" />
      <div class='more-grid-cell-image-1' *ngIf='isLast && imageArr.length > maxLength'>
          <div class='more-grid-cell-image-2'><div class='more-grid-cell-image-3'><b>+{{(imageArr.length) - maxLength}}</b></div></div>
      </div>
    </li>
 </ul>

photo-collage.component.css
.photo-grid-item{
  overflow: hidden;
}

.photo-grid-photos {
  position: relative;
}

.photo-grid-photos.collageLoading::after {
  content: "";
  height: 100%;
  width: 100%;
  background: rgba(0,0,0,.6) url("/images/collageLoader.gif") center center no-repeat;
  left:0;
  top:0;
  position: absolute;

}
.photo-grid-photos.collageLoading .grid-cell-image{
  width: 100%;
}

.op{
  opacity: 0.1;
}

.more-grid-cell-image-1 {
    background-color: rgba(0, 0, 0, .4);
    bottom: 0;
    color: #fff;
    font-size: 2em;
    font-weight: normal;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
}
.more-grid-cell-image-2 {
    display: table;
    height: 100%;
    width: 100%;
}
.more-grid-cell-image-3 {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}

news-feed-post.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { NewsFeedPostComponent } from './news-feed-post.component';

import { PhotoCollageModule } from '../photo-collage/photo-collage.module';
import { PhotoCollageComponent } from '../photo-collage/photo-collage.component';

@NgModule({
  imports: [
    CommonModule,
    PhotoCollageModule
  ],
  declarations: [NewsFeedPostComponent]
})
export class NewsFeedPostModule { }

news-feed-post.component.html
      -----
      -----
      -----
      <div *ngFor="let postListData of postList; let i = index;">
<div style="width: 500px;height: 500px;">
        <!-- Start : photo-collage-grid-->
        <app-photo-collage [imageArr]="postListData.photos" (photoGridClicked)="getClickedPhotoInfo($event);"></app-photo-collage>
        <!-- End : photo-collage-grid-->
</div>
       </div>
      -----
      -----
      -----

news-feed-post.component.ts
import { Component, OnInit, ElementRef } from '@angular/core';

@Component({
  selector: 'app-news-feed-post',
  templateUrl: './news-feed-post.component.html',
  styleUrls: ['./news-feed-post.component.css']
})
export class HomeComponent implements OnInit {

  postList:any;

  constructor() {
 
  }

  ngOnInit() {
      this.postList = [
      {
        "_id": "5d15a9f62438d5248125a45",
        "user_id": {
          "_id": "5d15a9f62438d5248125a41",
          "firstName": "laxman",
          "lastName": "chavda"
        },
          "photos": [
              {
                "_id": "5d15a9f62438d5248125a05",
                "photo_name": "photos-1561700854069.jpg"
              },
              {
                "_id": "5d15a9f62438d5248125a07",
                "photo_name": "photos-1561700854090.jpg"
              },
              {
                "_id": "5d15a9f62438d5248125a08",
                "photo_name": "photos-1561700854099.jpg"
              }
            ],
        "createdAt": 1561700854154
      },
      {
        "_id": "5d15a9f62438d5248125a78",
        "user_id": {
          "_id": "5d15a9f62438d5248125a88",
          "firstName": "laxman",
          "lastName": "chavda"
        },
          "photos": [
              {
                "_id": "5d15a9f62438d5248125a90",
                "photo_name": "photos-1561700854584.jpg"
              },
              {
                "_id": "5d15a9f62438d5248125a07",
                "photo_name": "photos-1561700854685.jpg"
              },
              {
                "_id": "5d15a9f62438d5248125a08",
                "photo_name": "photos-1561700854777.jpg"
              }
            ],
        "createdAt": 1561700854158
      }
      ];
  }

  getClickedPhotoInfo(infoObj){
    console.log("here.............getClickedPhotoInfo:", infoObj);
  }

}

Outputs :





if you have notice, in last image +7 display because we have set @Input() maxLength: number = 5; in 'photo-collage.component.ts' file so it will maximum 5 images and remaining image counter will be display. You can change it by simply pass attribute this from 'news-feed-post.component.html' file like as [maxLength]="7".

No comments:

Post a Comment