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}