A photo manager for VRChat.
1import { PhotoListPhoto } from "../Structs/PhotoListElements/PhotoListPhoto"; 2import { PhotoListText } from "../Structs/PhotoListElements/PhotoListText"; 3import { PhotoListElementType } from "../Structs/PhotoListElementType"; 4import { PhotoListRow } from "../Structs/PhotoListRow"; 5 6const MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; 7 8let multilayerIcon = new Image(); 9multilayerIcon.src = '/icon/layer-group-solid-full.svg'; 10 11export class PhotoListRenderingManager{ 12 private _layout: PhotoListRow[] = []; 13 private _canvas!: HTMLCanvasElement; 14 15 private _isLoading = false; 16 17 constructor(){} 18 19 public SetCanvas( canvas: HTMLCanvasElement ){ 20 this._canvas = canvas; 21 } 22 23 public ComputeLayout(){ 24 this._layout = []; 25 26 let lastDateString = null; 27 let row = new PhotoListRow(); 28 row.Height = 0; 29 30 for (let i = 0; i < window.PhotoManager.FilteredPhotos.length; i++) { 31 let photo = window.PhotoManager.FilteredPhotos[i]; 32 33 // If date string has changed since the last photo, we should label the correct date above it 34 if(lastDateString !== photo.dateString){ 35 this._layout.push(row); 36 row = new PhotoListRow(); 37 38 row.Height = 50; 39 40 let dateParts = photo.dateString.split('-'); 41 lastDateString = photo.dateString; 42 43 row.Elements = [ new PhotoListText(dateParts[2] + ' ' + MONTHS[parseInt(dateParts[1]) - 1] + ' ' + dateParts[0]) ]; 44 45 this._layout.push(row); 46 row = new PhotoListRow(); 47 } 48 49 // Check if the current row width plus another photo is too big to fit, push this row to the 50 // layout and add the photo to the next row instead 51 if(row.Width + photo.scaledWidth! + 10 > this._canvas.width - 100){ 52 this._layout.push(row); 53 row = new PhotoListRow(); 54 } 55 56 // We should now add this photo to the current row 57 row.Elements.push(new PhotoListPhoto(photo)); 58 row.Width += photo.scaledWidth! + 10; 59 } 60 61 this._layout.push(row); 62 } 63 64 public Render( ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, scroll: number ){ 65 let currentY = 0; 66 67 // Loop through each row 68 for (let i = 0; i < this._layout.length; i++) { 69 let row = this._layout[i]; 70 71 // Cull rows that are out of frame 72 if(currentY - scroll > canvas.height){ 73 // Reset frames for out of frame rows so they fade back in 74 row.Elements.forEach(el => { 75 if(el.Type === PhotoListElementType.PHOTO){ 76 (el as PhotoListPhoto).Photo.frames = 0; 77 (el as PhotoListPhoto).Photo.shown = false; 78 } 79 }); 80 81 return; 82 } 83 84 if(currentY - scroll < -row.Height){ 85 // Reset frames for out of frame rows so they fade back in 86 row.Elements.forEach(el => { 87 if(el.Type === PhotoListElementType.PHOTO){ 88 (el as PhotoListPhoto).Photo.frames = 0; 89 (el as PhotoListPhoto).Photo.shown = false; 90 } 91 }); 92 93 currentY += row.Height + 10; 94 continue; 95 } 96 97 // === DEBUG === 98 // ctx.strokeStyle = '#f00'; 99 // ctx.strokeRect((canvas.width / 2) - row.Width / 2, currentY - 5 - scroll, row.Width, row.Height + 10); 100 101 // Loop through all elements in the row 102 let rowXPos = 10; 103 for (let j = 0; j < row.Elements.length; j++) { 104 let el = row.Elements[j]; 105 106 switch(el.Type){ 107 case PhotoListElementType.TEXT: 108 // If it is a text element we should centre the text in the middle of the canvas 109 // and then render that text 110 111 // === DEBUG === 112 // ctx.strokeStyle = '#f00'; 113 // ctx.strokeRect(0, currentY - scroll, canvas.width, row.Height); 114 115 ctx.textAlign = 'center'; 116 ctx.textBaseline = 'middle'; 117 ctx.globalAlpha = 1; 118 ctx.fillStyle = '#fff'; 119 ctx.font = '30px Rubik'; 120 121 ctx.fillText((el as PhotoListText).Text, canvas.width / 2, currentY - scroll + 25); 122 break; 123 case PhotoListElementType.PHOTO: 124 let photo = (el as PhotoListPhoto).Photo; 125 126 // === DEBUG === 127 // ctx.strokeStyle = '#f00'; 128 // ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 129 130 if(!photo.loaded) 131 // If the photo is not loaded, start a new task and load it in that task 132 setTimeout(() => photo.loadImage(), 1); 133 else{ 134 photo.shown = true; 135 136 photo.x = (rowXPos - row.Width / 2) + canvas.width / 2; 137 photo.y = currentY - scroll; 138 139 // Photo is already loaded so we should draw it on the application 140 ctx.globalAlpha = photo.frames / 100; 141 ctx.drawImage(photo.image!, (rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, photo.scaledHeight!); 142 143 if(photo.isMultiLayer) 144 ctx.drawImage(multilayerIcon, ((rowXPos - row.Width / 2) + canvas.width / 2) + 5, (currentY - scroll) + 5, 20, 20); 145 146 if(photo.frames < 100) 147 photo.frames += 10; 148 } 149 150 rowXPos += photo.scaledWidth! + 10; 151 break; 152 } 153 } 154 155 currentY += row.Height + 10; 156 } 157 158 if(!this._isLoading){ 159 console.log('Loading more photos...'); 160 this._isLoading = true; 161 162 window.PhotoManager.LoadSomeAndReloadFilters() 163 .then(() => { 164 this._isLoading = false; 165 }); 166 } 167 } 168}