@thangdevalone/meet-layout-grid-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/index.cjs +349 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.mts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.mjs +340 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @meet-layout-grid/core
|
|
2
|
+
|
|
3
|
+
Core grid calculation logic for meet-layout-grid library. Zero dependencies, framework-agnostic.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @meet-layout-grid/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createMeetGrid, createGrid } from '@meet-layout-grid/core'
|
|
15
|
+
|
|
16
|
+
// Basic grid
|
|
17
|
+
const grid = createGrid({
|
|
18
|
+
dimensions: { width: 800, height: 600 },
|
|
19
|
+
count: 6,
|
|
20
|
+
aspectRatio: '16:9',
|
|
21
|
+
gap: 8,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
console.log(`Item size: ${grid.width}x${grid.height}`)
|
|
25
|
+
console.log(`Grid: ${grid.cols} cols, ${grid.rows} rows`)
|
|
26
|
+
|
|
27
|
+
// Position each item
|
|
28
|
+
for (let i = 0; i < 6; i++) {
|
|
29
|
+
const { top, left } = grid.getPosition(i)
|
|
30
|
+
console.log(`Item ${i}: top=${top}, left=${left}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Meet-style grid with layout modes
|
|
34
|
+
const meetGrid = createMeetGrid({
|
|
35
|
+
dimensions: { width: 800, height: 600 },
|
|
36
|
+
count: 6,
|
|
37
|
+
aspectRatio: '16:9',
|
|
38
|
+
gap: 8,
|
|
39
|
+
layoutMode: 'speaker', // 'gallery' | 'speaker' | 'spotlight' | 'sidebar'
|
|
40
|
+
speakerIndex: 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Items may have different sizes in speaker mode
|
|
44
|
+
for (let i = 0; i < 6; i++) {
|
|
45
|
+
const { width, height } = meetGrid.getItemDimensions(i)
|
|
46
|
+
const isMain = meetGrid.isMainItem(i)
|
|
47
|
+
console.log(`Item ${i}: ${width}x${height}, main: ${isMain}`)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
### `createGrid(options)`
|
|
54
|
+
|
|
55
|
+
Creates a basic responsive grid.
|
|
56
|
+
|
|
57
|
+
**Options:**
|
|
58
|
+
- `dimensions: { width, height }` - Container dimensions
|
|
59
|
+
- `count: number` - Number of items
|
|
60
|
+
- `aspectRatio: string` - Aspect ratio (e.g., "16:9")
|
|
61
|
+
- `gap: number` - Gap between items in pixels
|
|
62
|
+
|
|
63
|
+
**Returns:**
|
|
64
|
+
- `width: number` - Item width
|
|
65
|
+
- `height: number` - Item height
|
|
66
|
+
- `rows: number` - Number of rows
|
|
67
|
+
- `cols: number` - Number of columns
|
|
68
|
+
- `getPosition(index): { top, left }` - Position getter
|
|
69
|
+
|
|
70
|
+
### `createMeetGrid(options)`
|
|
71
|
+
|
|
72
|
+
Creates a meet-style grid with layout modes.
|
|
73
|
+
|
|
74
|
+
**Additional Options:**
|
|
75
|
+
- `layoutMode: 'gallery' | 'speaker' | 'spotlight' | 'sidebar'`
|
|
76
|
+
- `pinnedIndex?: number` - Index of pinned item
|
|
77
|
+
- `speakerIndex?: number` - Index of active speaker
|
|
78
|
+
- `sidebarPosition?: 'left' | 'right' | 'bottom'`
|
|
79
|
+
- `sidebarRatio?: number` - Sidebar width ratio (0-1)
|
|
80
|
+
|
|
81
|
+
**Additional Returns:**
|
|
82
|
+
- `layoutMode: LayoutMode` - Current layout mode
|
|
83
|
+
- `getItemDimensions(index): { width, height }` - Per-item dimensions
|
|
84
|
+
- `isMainItem(index): boolean` - Check if item is featured
|
|
85
|
+
|
|
86
|
+
## Layout Modes
|
|
87
|
+
|
|
88
|
+
- **Gallery** - Equal-sized tiles in a responsive grid
|
|
89
|
+
- **Speaker** - Active speaker takes larger space (65% height)
|
|
90
|
+
- **Spotlight** - Single participant in focus, others hidden
|
|
91
|
+
- **Sidebar** - Main view with thumbnail strip
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function getAspectRatio(ratio) {
|
|
4
|
+
const [width, height] = ratio.split(":");
|
|
5
|
+
if (!width || !height) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return Number.parseInt(height) / Number.parseInt(width);
|
|
11
|
+
}
|
|
12
|
+
function parseAspectRatio(ratio) {
|
|
13
|
+
const [width, height] = ratio.split(":").map(Number);
|
|
14
|
+
if (!width || !height || isNaN(width) || isNaN(height)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return { widthRatio: width, heightRatio: height };
|
|
20
|
+
}
|
|
21
|
+
function getGridItemDimensions({
|
|
22
|
+
count,
|
|
23
|
+
dimensions,
|
|
24
|
+
aspectRatio,
|
|
25
|
+
gap
|
|
26
|
+
}) {
|
|
27
|
+
let { width: W, height: H } = dimensions;
|
|
28
|
+
if (W === 0 || H === 0 || count === 0) {
|
|
29
|
+
return { width: 0, height: 0, rows: 1, cols: 1 };
|
|
30
|
+
}
|
|
31
|
+
W -= gap * 2;
|
|
32
|
+
H -= gap * 2;
|
|
33
|
+
const s = gap;
|
|
34
|
+
const N = count;
|
|
35
|
+
const r = getAspectRatio(aspectRatio);
|
|
36
|
+
let w = 0;
|
|
37
|
+
let h = 0;
|
|
38
|
+
let a = 1;
|
|
39
|
+
let b = 1;
|
|
40
|
+
const widths = [];
|
|
41
|
+
for (let n = 1; n <= N; n++) {
|
|
42
|
+
widths.push((W - s * (n - 1)) / n, (H - s * (n - 1)) / (n * r));
|
|
43
|
+
}
|
|
44
|
+
widths.sort((a2, b2) => b2 - a2);
|
|
45
|
+
for (const width of widths) {
|
|
46
|
+
w = width;
|
|
47
|
+
h = w * r;
|
|
48
|
+
a = Math.floor((W + s) / (w + s));
|
|
49
|
+
b = Math.floor((H + s) / (h + s));
|
|
50
|
+
if (a * b >= N) {
|
|
51
|
+
a = Math.ceil(N / b);
|
|
52
|
+
b = Math.ceil(N / a);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { width: w, height: h, rows: b, cols: a };
|
|
57
|
+
}
|
|
58
|
+
function createGridItemPositioner({
|
|
59
|
+
parentDimensions,
|
|
60
|
+
dimensions,
|
|
61
|
+
rows,
|
|
62
|
+
cols,
|
|
63
|
+
count,
|
|
64
|
+
gap
|
|
65
|
+
}) {
|
|
66
|
+
const { width: W, height: H } = parentDimensions;
|
|
67
|
+
const { width: w, height: h } = dimensions;
|
|
68
|
+
const firstTop = (H - (h * rows + (rows - 1) * gap)) / 2;
|
|
69
|
+
let firstLeft = (W - (w * cols + (cols - 1) * gap)) / 2;
|
|
70
|
+
const topAdd = h + gap;
|
|
71
|
+
const leftAdd = w + gap;
|
|
72
|
+
let col = 0;
|
|
73
|
+
let row = 0;
|
|
74
|
+
const incompleteRowCols = count % cols;
|
|
75
|
+
function getPosition(index) {
|
|
76
|
+
const remaining = count - index;
|
|
77
|
+
if (remaining === incompleteRowCols && incompleteRowCols > 0) {
|
|
78
|
+
firstLeft = (W - (w * remaining + (remaining - 1) * gap)) / 2;
|
|
79
|
+
}
|
|
80
|
+
const top = firstTop + row * topAdd;
|
|
81
|
+
const left = firstLeft + col * leftAdd;
|
|
82
|
+
col++;
|
|
83
|
+
if ((index + 1) % cols === 0) {
|
|
84
|
+
row++;
|
|
85
|
+
col = 0;
|
|
86
|
+
}
|
|
87
|
+
return { top, left };
|
|
88
|
+
}
|
|
89
|
+
return getPosition;
|
|
90
|
+
}
|
|
91
|
+
function createGrid({ aspectRatio, count, dimensions, gap }) {
|
|
92
|
+
const { width, height, rows, cols } = getGridItemDimensions({
|
|
93
|
+
aspectRatio,
|
|
94
|
+
count,
|
|
95
|
+
dimensions,
|
|
96
|
+
gap
|
|
97
|
+
});
|
|
98
|
+
const getPosition = createGridItemPositioner({
|
|
99
|
+
parentDimensions: dimensions,
|
|
100
|
+
dimensions: { width, height },
|
|
101
|
+
rows,
|
|
102
|
+
cols,
|
|
103
|
+
count,
|
|
104
|
+
gap
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
109
|
+
rows,
|
|
110
|
+
cols,
|
|
111
|
+
getPosition
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createSidebarGrid(options) {
|
|
115
|
+
const { dimensions, gap, aspectRatio, count, sidebarPosition = "right", sidebarRatio = 0.25, pinnedIndex = 0 } = options;
|
|
116
|
+
if (count === 0) {
|
|
117
|
+
return createEmptyMeetGridResult("sidebar");
|
|
118
|
+
}
|
|
119
|
+
const { width: W, height: H } = dimensions;
|
|
120
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
121
|
+
const isVertical = sidebarPosition === "bottom";
|
|
122
|
+
let mainWidth;
|
|
123
|
+
let mainHeight;
|
|
124
|
+
let sidebarWidth;
|
|
125
|
+
let sidebarHeight;
|
|
126
|
+
if (isVertical) {
|
|
127
|
+
mainHeight = H * (1 - sidebarRatio) - gap;
|
|
128
|
+
mainWidth = W - gap * 2;
|
|
129
|
+
sidebarHeight = H * sidebarRatio - gap;
|
|
130
|
+
sidebarWidth = W - gap * 2;
|
|
131
|
+
} else {
|
|
132
|
+
mainWidth = W * (1 - sidebarRatio) - gap * 2;
|
|
133
|
+
mainHeight = H - gap * 2;
|
|
134
|
+
sidebarWidth = W * sidebarRatio - gap;
|
|
135
|
+
sidebarHeight = H - gap * 2;
|
|
136
|
+
}
|
|
137
|
+
let mainItemWidth = mainWidth;
|
|
138
|
+
let mainItemHeight = mainItemWidth * ratio;
|
|
139
|
+
if (mainItemHeight > mainHeight) {
|
|
140
|
+
mainItemHeight = mainHeight;
|
|
141
|
+
mainItemWidth = mainItemHeight / ratio;
|
|
142
|
+
}
|
|
143
|
+
const sidebarCount = count - 1;
|
|
144
|
+
let thumbWidth;
|
|
145
|
+
let thumbHeight;
|
|
146
|
+
if (sidebarCount > 0) {
|
|
147
|
+
if (isVertical) {
|
|
148
|
+
thumbWidth = Math.min((sidebarWidth - (sidebarCount - 1) * gap) / sidebarCount, sidebarHeight / ratio);
|
|
149
|
+
thumbHeight = thumbWidth * ratio;
|
|
150
|
+
} else {
|
|
151
|
+
thumbHeight = Math.min((sidebarHeight - (sidebarCount - 1) * gap) / sidebarCount, sidebarWidth / ratio);
|
|
152
|
+
thumbWidth = thumbHeight / ratio;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
thumbWidth = 0;
|
|
156
|
+
thumbHeight = 0;
|
|
157
|
+
}
|
|
158
|
+
const positions = [];
|
|
159
|
+
const mainLeft = isVertical ? gap + (mainWidth - mainItemWidth) / 2 : sidebarPosition === "left" ? sidebarWidth + gap * 2 + (mainWidth - mainItemWidth) / 2 : gap + (mainWidth - mainItemWidth) / 2;
|
|
160
|
+
const mainTop = isVertical ? gap + (mainHeight - mainItemHeight) / 2 : gap + (mainHeight - mainItemHeight) / 2;
|
|
161
|
+
positions[pinnedIndex] = {
|
|
162
|
+
position: { top: mainTop, left: mainLeft },
|
|
163
|
+
dimensions: { width: mainItemWidth, height: mainItemHeight }
|
|
164
|
+
};
|
|
165
|
+
let sidebarIndex = 0;
|
|
166
|
+
for (let i = 0; i < count; i++) {
|
|
167
|
+
if (i === pinnedIndex)
|
|
168
|
+
continue;
|
|
169
|
+
let left;
|
|
170
|
+
let top;
|
|
171
|
+
if (isVertical) {
|
|
172
|
+
const totalThumbWidth = sidebarCount * thumbWidth + (sidebarCount - 1) * gap;
|
|
173
|
+
const startLeft = gap + (sidebarWidth - totalThumbWidth) / 2;
|
|
174
|
+
left = startLeft + sidebarIndex * (thumbWidth + gap);
|
|
175
|
+
top = mainHeight + gap * 2;
|
|
176
|
+
} else {
|
|
177
|
+
left = sidebarPosition === "left" ? gap : mainWidth + gap * 2;
|
|
178
|
+
const totalThumbHeight = sidebarCount * thumbHeight + (sidebarCount - 1) * gap;
|
|
179
|
+
const startTop = gap + (sidebarHeight - totalThumbHeight) / 2;
|
|
180
|
+
top = startTop + sidebarIndex * (thumbHeight + gap);
|
|
181
|
+
}
|
|
182
|
+
positions[i] = {
|
|
183
|
+
position: { top, left },
|
|
184
|
+
dimensions: { width: thumbWidth, height: thumbHeight }
|
|
185
|
+
};
|
|
186
|
+
sidebarIndex++;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
width: mainItemWidth,
|
|
190
|
+
height: mainItemHeight,
|
|
191
|
+
rows: isVertical ? 2 : 1,
|
|
192
|
+
cols: isVertical ? 1 : 2,
|
|
193
|
+
layoutMode: "sidebar",
|
|
194
|
+
getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
|
|
195
|
+
getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
|
|
196
|
+
isMainItem: (index) => index === pinnedIndex
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function createSpeakerGrid(options) {
|
|
200
|
+
const { dimensions, gap, aspectRatio, count, speakerIndex = 0 } = options;
|
|
201
|
+
if (count === 0) {
|
|
202
|
+
return createEmptyMeetGridResult("speaker");
|
|
203
|
+
}
|
|
204
|
+
if (count === 1) {
|
|
205
|
+
const grid = createGrid({ ...options, count: 1 });
|
|
206
|
+
return {
|
|
207
|
+
...grid,
|
|
208
|
+
layoutMode: "speaker",
|
|
209
|
+
getItemDimensions: () => ({ width: grid.width, height: grid.height }),
|
|
210
|
+
isMainItem: () => true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const { width: W, height: H } = dimensions;
|
|
214
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
215
|
+
const speakerHeight = (H - gap * 3) * 0.65;
|
|
216
|
+
const othersHeight = (H - gap * 3) * 0.35;
|
|
217
|
+
let speakerW = W - gap * 2;
|
|
218
|
+
let speakerH = speakerW * ratio;
|
|
219
|
+
if (speakerH > speakerHeight) {
|
|
220
|
+
speakerH = speakerHeight;
|
|
221
|
+
speakerW = speakerH / ratio;
|
|
222
|
+
}
|
|
223
|
+
const othersCount = count - 1;
|
|
224
|
+
let otherW = Math.min((W - gap * 2 - (othersCount - 1) * gap) / othersCount, othersHeight / ratio);
|
|
225
|
+
let otherH = otherW * ratio;
|
|
226
|
+
if (otherH > othersHeight) {
|
|
227
|
+
otherH = othersHeight;
|
|
228
|
+
otherW = otherH / ratio;
|
|
229
|
+
}
|
|
230
|
+
const positions = [];
|
|
231
|
+
positions[speakerIndex] = {
|
|
232
|
+
position: {
|
|
233
|
+
top: gap + (speakerHeight - speakerH) / 2,
|
|
234
|
+
left: gap + (W - gap * 2 - speakerW) / 2
|
|
235
|
+
},
|
|
236
|
+
dimensions: { width: speakerW, height: speakerH }
|
|
237
|
+
};
|
|
238
|
+
const totalOthersWidth = othersCount * otherW + (othersCount - 1) * gap;
|
|
239
|
+
const startLeft = gap + (W - gap * 2 - totalOthersWidth) / 2;
|
|
240
|
+
let otherIndex = 0;
|
|
241
|
+
for (let i = 0; i < count; i++) {
|
|
242
|
+
if (i === speakerIndex)
|
|
243
|
+
continue;
|
|
244
|
+
positions[i] = {
|
|
245
|
+
position: {
|
|
246
|
+
top: speakerHeight + gap * 2 + (othersHeight - otherH) / 2,
|
|
247
|
+
left: startLeft + otherIndex * (otherW + gap)
|
|
248
|
+
},
|
|
249
|
+
dimensions: { width: otherW, height: otherH }
|
|
250
|
+
};
|
|
251
|
+
otherIndex++;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
width: speakerW,
|
|
255
|
+
height: speakerH,
|
|
256
|
+
rows: 2,
|
|
257
|
+
cols: othersCount,
|
|
258
|
+
layoutMode: "speaker",
|
|
259
|
+
getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
|
|
260
|
+
getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
|
|
261
|
+
isMainItem: (index) => index === speakerIndex
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function createSpotlightGrid(options) {
|
|
265
|
+
const { dimensions, gap, aspectRatio, pinnedIndex = 0 } = options;
|
|
266
|
+
const { width: W, height: H } = dimensions;
|
|
267
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
268
|
+
let spotWidth = W - gap * 2;
|
|
269
|
+
let spotHeight = spotWidth * ratio;
|
|
270
|
+
if (spotHeight > H - gap * 2) {
|
|
271
|
+
spotHeight = H - gap * 2;
|
|
272
|
+
spotWidth = spotHeight / ratio;
|
|
273
|
+
}
|
|
274
|
+
const position = {
|
|
275
|
+
top: gap + (H - gap * 2 - spotHeight) / 2,
|
|
276
|
+
left: gap + (W - gap * 2 - spotWidth) / 2
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
width: spotWidth,
|
|
280
|
+
height: spotHeight,
|
|
281
|
+
rows: 1,
|
|
282
|
+
cols: 1,
|
|
283
|
+
layoutMode: "spotlight",
|
|
284
|
+
getPosition: (index) => index === pinnedIndex ? position : { top: -9999, left: -9999 },
|
|
285
|
+
getItemDimensions: (index) => index === pinnedIndex ? { width: spotWidth, height: spotHeight } : { width: 0, height: 0 },
|
|
286
|
+
isMainItem: (index) => index === pinnedIndex
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function createEmptyMeetGridResult(layoutMode) {
|
|
290
|
+
return {
|
|
291
|
+
width: 0,
|
|
292
|
+
height: 0,
|
|
293
|
+
rows: 0,
|
|
294
|
+
cols: 0,
|
|
295
|
+
layoutMode,
|
|
296
|
+
getPosition: () => ({ top: 0, left: 0 }),
|
|
297
|
+
getItemDimensions: () => ({ width: 0, height: 0 }),
|
|
298
|
+
isMainItem: () => false
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function createMeetGrid(options) {
|
|
302
|
+
const { layoutMode = "gallery", count } = options;
|
|
303
|
+
if (count === 0) {
|
|
304
|
+
return createEmptyMeetGridResult(layoutMode);
|
|
305
|
+
}
|
|
306
|
+
switch (layoutMode) {
|
|
307
|
+
case "spotlight":
|
|
308
|
+
return createSpotlightGrid(options);
|
|
309
|
+
case "speaker":
|
|
310
|
+
return createSpeakerGrid(options);
|
|
311
|
+
case "sidebar":
|
|
312
|
+
return createSidebarGrid(options);
|
|
313
|
+
case "gallery":
|
|
314
|
+
default: {
|
|
315
|
+
const grid = createGrid(options);
|
|
316
|
+
return {
|
|
317
|
+
...grid,
|
|
318
|
+
layoutMode: "gallery",
|
|
319
|
+
getItemDimensions: () => ({ width: grid.width, height: grid.height }),
|
|
320
|
+
isMainItem: () => false
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const springPresets = {
|
|
326
|
+
/** Snappy animations for UI interactions */
|
|
327
|
+
snappy: { stiffness: 400, damping: 30 },
|
|
328
|
+
/** Smooth animations for layout changes */
|
|
329
|
+
smooth: { stiffness: 300, damping: 30 },
|
|
330
|
+
/** Gentle animations for subtle effects */
|
|
331
|
+
gentle: { stiffness: 200, damping: 25 },
|
|
332
|
+
/** Bouncy animations for playful effects */
|
|
333
|
+
bouncy: { stiffness: 400, damping: 15 }
|
|
334
|
+
};
|
|
335
|
+
function getSpringConfig(preset = "smooth") {
|
|
336
|
+
return {
|
|
337
|
+
type: "spring",
|
|
338
|
+
...springPresets[preset]
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
exports.createGrid = createGrid;
|
|
343
|
+
exports.createGridItemPositioner = createGridItemPositioner;
|
|
344
|
+
exports.createMeetGrid = createMeetGrid;
|
|
345
|
+
exports.getAspectRatio = getAspectRatio;
|
|
346
|
+
exports.getGridItemDimensions = getGridItemDimensions;
|
|
347
|
+
exports.getSpringConfig = getSpringConfig;
|
|
348
|
+
exports.parseAspectRatio = parseAspectRatio;
|
|
349
|
+
exports.springPresets = springPresets;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimensions of an element (width and height in pixels)
|
|
3
|
+
*/
|
|
4
|
+
interface GridDimensions {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Position of a grid item
|
|
10
|
+
*/
|
|
11
|
+
interface Position {
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Layout modes for the grid
|
|
17
|
+
* - gallery: Equal-sized tiles in a responsive grid
|
|
18
|
+
* - speaker: Active speaker takes larger space (2x size)
|
|
19
|
+
* - spotlight: Single participant in focus, others hidden
|
|
20
|
+
* - sidebar: Main view with thumbnail strip on the side
|
|
21
|
+
*/
|
|
22
|
+
type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
|
|
23
|
+
/**
|
|
24
|
+
* Options for creating a basic grid
|
|
25
|
+
*/
|
|
26
|
+
interface GridOptions {
|
|
27
|
+
/** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
|
|
28
|
+
aspectRatio: string;
|
|
29
|
+
/** Number of items in the grid */
|
|
30
|
+
count: number;
|
|
31
|
+
/** Container dimensions */
|
|
32
|
+
dimensions: GridDimensions;
|
|
33
|
+
/** Gap between items in pixels */
|
|
34
|
+
gap: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extended options for meet-style grid with layout modes
|
|
38
|
+
*/
|
|
39
|
+
interface MeetGridOptions extends GridOptions {
|
|
40
|
+
/** Layout mode for the grid */
|
|
41
|
+
layoutMode?: LayoutMode;
|
|
42
|
+
/** Index of pinned item (for speaker/spotlight modes) */
|
|
43
|
+
pinnedIndex?: number;
|
|
44
|
+
/** Index of active speaker */
|
|
45
|
+
speakerIndex?: number;
|
|
46
|
+
/** Sidebar position (for sidebar mode) */
|
|
47
|
+
sidebarPosition?: 'left' | 'right' | 'bottom';
|
|
48
|
+
/** Sidebar width ratio (0-1) */
|
|
49
|
+
sidebarRatio?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Result from grid calculations
|
|
53
|
+
*/
|
|
54
|
+
interface GridResult {
|
|
55
|
+
/** Width of each grid item */
|
|
56
|
+
width: number;
|
|
57
|
+
/** Height of each grid item */
|
|
58
|
+
height: number;
|
|
59
|
+
/** Number of rows */
|
|
60
|
+
rows: number;
|
|
61
|
+
/** Number of columns */
|
|
62
|
+
cols: number;
|
|
63
|
+
/** Function to get position of item at index */
|
|
64
|
+
getPosition: (index: number) => Position;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extended result for meet-style grid
|
|
68
|
+
*/
|
|
69
|
+
interface MeetGridResult extends GridResult {
|
|
70
|
+
/** Layout mode used */
|
|
71
|
+
layoutMode: LayoutMode;
|
|
72
|
+
/** Get item dimensions (may vary by index in some modes) */
|
|
73
|
+
getItemDimensions: (index: number) => GridDimensions;
|
|
74
|
+
/** Check if item is the main/featured item */
|
|
75
|
+
isMainItem: (index: number) => boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses the Aspect Ratio string to actual ratio (height/width)
|
|
79
|
+
* @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
|
|
80
|
+
* @returns The parsed value of aspect ratio (height/width)
|
|
81
|
+
*/
|
|
82
|
+
declare function getAspectRatio(ratio: string): number;
|
|
83
|
+
/**
|
|
84
|
+
* Parse aspect ratio to get width/height multiplier
|
|
85
|
+
*/
|
|
86
|
+
declare function parseAspectRatio(ratio: string): {
|
|
87
|
+
widthRatio: number;
|
|
88
|
+
heightRatio: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Calculates grid item dimensions for items that can fit in a container.
|
|
92
|
+
* Adapted from: https://stackoverflow.com/a/28268965
|
|
93
|
+
*/
|
|
94
|
+
declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
rows: number;
|
|
98
|
+
cols: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Creates a utility function which helps you position grid items in a container.
|
|
102
|
+
*/
|
|
103
|
+
declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
|
|
104
|
+
parentDimensions: GridDimensions;
|
|
105
|
+
dimensions: GridDimensions;
|
|
106
|
+
rows: number;
|
|
107
|
+
cols: number;
|
|
108
|
+
count: number;
|
|
109
|
+
gap: number;
|
|
110
|
+
}): (index: number) => Position;
|
|
111
|
+
/**
|
|
112
|
+
* Calculates data required for making a responsive grid.
|
|
113
|
+
*/
|
|
114
|
+
declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
|
|
115
|
+
/**
|
|
116
|
+
* Create a meet-style grid with support for different layout modes.
|
|
117
|
+
* This is the main function for creating video conferencing-style layouts.
|
|
118
|
+
*/
|
|
119
|
+
declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
|
|
120
|
+
/**
|
|
121
|
+
* Spring animation configuration presets
|
|
122
|
+
*/
|
|
123
|
+
declare const springPresets: {
|
|
124
|
+
/** Snappy animations for UI interactions */
|
|
125
|
+
readonly snappy: {
|
|
126
|
+
readonly stiffness: 400;
|
|
127
|
+
readonly damping: 30;
|
|
128
|
+
};
|
|
129
|
+
/** Smooth animations for layout changes */
|
|
130
|
+
readonly smooth: {
|
|
131
|
+
readonly stiffness: 300;
|
|
132
|
+
readonly damping: 30;
|
|
133
|
+
};
|
|
134
|
+
/** Gentle animations for subtle effects */
|
|
135
|
+
readonly gentle: {
|
|
136
|
+
readonly stiffness: 200;
|
|
137
|
+
readonly damping: 25;
|
|
138
|
+
};
|
|
139
|
+
/** Bouncy animations for playful effects */
|
|
140
|
+
readonly bouncy: {
|
|
141
|
+
readonly stiffness: 400;
|
|
142
|
+
readonly damping: 15;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
type SpringPreset = keyof typeof springPresets;
|
|
146
|
+
/**
|
|
147
|
+
* Get spring configuration for Motion animations
|
|
148
|
+
*/
|
|
149
|
+
declare function getSpringConfig(preset?: SpringPreset): {
|
|
150
|
+
stiffness: 400;
|
|
151
|
+
damping: 30;
|
|
152
|
+
type: "spring";
|
|
153
|
+
} | {
|
|
154
|
+
stiffness: 300;
|
|
155
|
+
damping: 30;
|
|
156
|
+
type: "spring";
|
|
157
|
+
} | {
|
|
158
|
+
stiffness: 200;
|
|
159
|
+
damping: 25;
|
|
160
|
+
type: "spring";
|
|
161
|
+
} | {
|
|
162
|
+
stiffness: 400;
|
|
163
|
+
damping: 15;
|
|
164
|
+
type: "spring";
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
|
|
168
|
+
export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimensions of an element (width and height in pixels)
|
|
3
|
+
*/
|
|
4
|
+
interface GridDimensions {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Position of a grid item
|
|
10
|
+
*/
|
|
11
|
+
interface Position {
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Layout modes for the grid
|
|
17
|
+
* - gallery: Equal-sized tiles in a responsive grid
|
|
18
|
+
* - speaker: Active speaker takes larger space (2x size)
|
|
19
|
+
* - spotlight: Single participant in focus, others hidden
|
|
20
|
+
* - sidebar: Main view with thumbnail strip on the side
|
|
21
|
+
*/
|
|
22
|
+
type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
|
|
23
|
+
/**
|
|
24
|
+
* Options for creating a basic grid
|
|
25
|
+
*/
|
|
26
|
+
interface GridOptions {
|
|
27
|
+
/** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
|
|
28
|
+
aspectRatio: string;
|
|
29
|
+
/** Number of items in the grid */
|
|
30
|
+
count: number;
|
|
31
|
+
/** Container dimensions */
|
|
32
|
+
dimensions: GridDimensions;
|
|
33
|
+
/** Gap between items in pixels */
|
|
34
|
+
gap: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extended options for meet-style grid with layout modes
|
|
38
|
+
*/
|
|
39
|
+
interface MeetGridOptions extends GridOptions {
|
|
40
|
+
/** Layout mode for the grid */
|
|
41
|
+
layoutMode?: LayoutMode;
|
|
42
|
+
/** Index of pinned item (for speaker/spotlight modes) */
|
|
43
|
+
pinnedIndex?: number;
|
|
44
|
+
/** Index of active speaker */
|
|
45
|
+
speakerIndex?: number;
|
|
46
|
+
/** Sidebar position (for sidebar mode) */
|
|
47
|
+
sidebarPosition?: 'left' | 'right' | 'bottom';
|
|
48
|
+
/** Sidebar width ratio (0-1) */
|
|
49
|
+
sidebarRatio?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Result from grid calculations
|
|
53
|
+
*/
|
|
54
|
+
interface GridResult {
|
|
55
|
+
/** Width of each grid item */
|
|
56
|
+
width: number;
|
|
57
|
+
/** Height of each grid item */
|
|
58
|
+
height: number;
|
|
59
|
+
/** Number of rows */
|
|
60
|
+
rows: number;
|
|
61
|
+
/** Number of columns */
|
|
62
|
+
cols: number;
|
|
63
|
+
/** Function to get position of item at index */
|
|
64
|
+
getPosition: (index: number) => Position;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extended result for meet-style grid
|
|
68
|
+
*/
|
|
69
|
+
interface MeetGridResult extends GridResult {
|
|
70
|
+
/** Layout mode used */
|
|
71
|
+
layoutMode: LayoutMode;
|
|
72
|
+
/** Get item dimensions (may vary by index in some modes) */
|
|
73
|
+
getItemDimensions: (index: number) => GridDimensions;
|
|
74
|
+
/** Check if item is the main/featured item */
|
|
75
|
+
isMainItem: (index: number) => boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses the Aspect Ratio string to actual ratio (height/width)
|
|
79
|
+
* @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
|
|
80
|
+
* @returns The parsed value of aspect ratio (height/width)
|
|
81
|
+
*/
|
|
82
|
+
declare function getAspectRatio(ratio: string): number;
|
|
83
|
+
/**
|
|
84
|
+
* Parse aspect ratio to get width/height multiplier
|
|
85
|
+
*/
|
|
86
|
+
declare function parseAspectRatio(ratio: string): {
|
|
87
|
+
widthRatio: number;
|
|
88
|
+
heightRatio: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Calculates grid item dimensions for items that can fit in a container.
|
|
92
|
+
* Adapted from: https://stackoverflow.com/a/28268965
|
|
93
|
+
*/
|
|
94
|
+
declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
rows: number;
|
|
98
|
+
cols: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Creates a utility function which helps you position grid items in a container.
|
|
102
|
+
*/
|
|
103
|
+
declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
|
|
104
|
+
parentDimensions: GridDimensions;
|
|
105
|
+
dimensions: GridDimensions;
|
|
106
|
+
rows: number;
|
|
107
|
+
cols: number;
|
|
108
|
+
count: number;
|
|
109
|
+
gap: number;
|
|
110
|
+
}): (index: number) => Position;
|
|
111
|
+
/**
|
|
112
|
+
* Calculates data required for making a responsive grid.
|
|
113
|
+
*/
|
|
114
|
+
declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
|
|
115
|
+
/**
|
|
116
|
+
* Create a meet-style grid with support for different layout modes.
|
|
117
|
+
* This is the main function for creating video conferencing-style layouts.
|
|
118
|
+
*/
|
|
119
|
+
declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
|
|
120
|
+
/**
|
|
121
|
+
* Spring animation configuration presets
|
|
122
|
+
*/
|
|
123
|
+
declare const springPresets: {
|
|
124
|
+
/** Snappy animations for UI interactions */
|
|
125
|
+
readonly snappy: {
|
|
126
|
+
readonly stiffness: 400;
|
|
127
|
+
readonly damping: 30;
|
|
128
|
+
};
|
|
129
|
+
/** Smooth animations for layout changes */
|
|
130
|
+
readonly smooth: {
|
|
131
|
+
readonly stiffness: 300;
|
|
132
|
+
readonly damping: 30;
|
|
133
|
+
};
|
|
134
|
+
/** Gentle animations for subtle effects */
|
|
135
|
+
readonly gentle: {
|
|
136
|
+
readonly stiffness: 200;
|
|
137
|
+
readonly damping: 25;
|
|
138
|
+
};
|
|
139
|
+
/** Bouncy animations for playful effects */
|
|
140
|
+
readonly bouncy: {
|
|
141
|
+
readonly stiffness: 400;
|
|
142
|
+
readonly damping: 15;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
type SpringPreset = keyof typeof springPresets;
|
|
146
|
+
/**
|
|
147
|
+
* Get spring configuration for Motion animations
|
|
148
|
+
*/
|
|
149
|
+
declare function getSpringConfig(preset?: SpringPreset): {
|
|
150
|
+
stiffness: 400;
|
|
151
|
+
damping: 30;
|
|
152
|
+
type: "spring";
|
|
153
|
+
} | {
|
|
154
|
+
stiffness: 300;
|
|
155
|
+
damping: 30;
|
|
156
|
+
type: "spring";
|
|
157
|
+
} | {
|
|
158
|
+
stiffness: 200;
|
|
159
|
+
damping: 25;
|
|
160
|
+
type: "spring";
|
|
161
|
+
} | {
|
|
162
|
+
stiffness: 400;
|
|
163
|
+
damping: 15;
|
|
164
|
+
type: "spring";
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
|
|
168
|
+
export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimensions of an element (width and height in pixels)
|
|
3
|
+
*/
|
|
4
|
+
interface GridDimensions {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Position of a grid item
|
|
10
|
+
*/
|
|
11
|
+
interface Position {
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Layout modes for the grid
|
|
17
|
+
* - gallery: Equal-sized tiles in a responsive grid
|
|
18
|
+
* - speaker: Active speaker takes larger space (2x size)
|
|
19
|
+
* - spotlight: Single participant in focus, others hidden
|
|
20
|
+
* - sidebar: Main view with thumbnail strip on the side
|
|
21
|
+
*/
|
|
22
|
+
type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
|
|
23
|
+
/**
|
|
24
|
+
* Options for creating a basic grid
|
|
25
|
+
*/
|
|
26
|
+
interface GridOptions {
|
|
27
|
+
/** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
|
|
28
|
+
aspectRatio: string;
|
|
29
|
+
/** Number of items in the grid */
|
|
30
|
+
count: number;
|
|
31
|
+
/** Container dimensions */
|
|
32
|
+
dimensions: GridDimensions;
|
|
33
|
+
/** Gap between items in pixels */
|
|
34
|
+
gap: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extended options for meet-style grid with layout modes
|
|
38
|
+
*/
|
|
39
|
+
interface MeetGridOptions extends GridOptions {
|
|
40
|
+
/** Layout mode for the grid */
|
|
41
|
+
layoutMode?: LayoutMode;
|
|
42
|
+
/** Index of pinned item (for speaker/spotlight modes) */
|
|
43
|
+
pinnedIndex?: number;
|
|
44
|
+
/** Index of active speaker */
|
|
45
|
+
speakerIndex?: number;
|
|
46
|
+
/** Sidebar position (for sidebar mode) */
|
|
47
|
+
sidebarPosition?: 'left' | 'right' | 'bottom';
|
|
48
|
+
/** Sidebar width ratio (0-1) */
|
|
49
|
+
sidebarRatio?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Result from grid calculations
|
|
53
|
+
*/
|
|
54
|
+
interface GridResult {
|
|
55
|
+
/** Width of each grid item */
|
|
56
|
+
width: number;
|
|
57
|
+
/** Height of each grid item */
|
|
58
|
+
height: number;
|
|
59
|
+
/** Number of rows */
|
|
60
|
+
rows: number;
|
|
61
|
+
/** Number of columns */
|
|
62
|
+
cols: number;
|
|
63
|
+
/** Function to get position of item at index */
|
|
64
|
+
getPosition: (index: number) => Position;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extended result for meet-style grid
|
|
68
|
+
*/
|
|
69
|
+
interface MeetGridResult extends GridResult {
|
|
70
|
+
/** Layout mode used */
|
|
71
|
+
layoutMode: LayoutMode;
|
|
72
|
+
/** Get item dimensions (may vary by index in some modes) */
|
|
73
|
+
getItemDimensions: (index: number) => GridDimensions;
|
|
74
|
+
/** Check if item is the main/featured item */
|
|
75
|
+
isMainItem: (index: number) => boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses the Aspect Ratio string to actual ratio (height/width)
|
|
79
|
+
* @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
|
|
80
|
+
* @returns The parsed value of aspect ratio (height/width)
|
|
81
|
+
*/
|
|
82
|
+
declare function getAspectRatio(ratio: string): number;
|
|
83
|
+
/**
|
|
84
|
+
* Parse aspect ratio to get width/height multiplier
|
|
85
|
+
*/
|
|
86
|
+
declare function parseAspectRatio(ratio: string): {
|
|
87
|
+
widthRatio: number;
|
|
88
|
+
heightRatio: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Calculates grid item dimensions for items that can fit in a container.
|
|
92
|
+
* Adapted from: https://stackoverflow.com/a/28268965
|
|
93
|
+
*/
|
|
94
|
+
declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
rows: number;
|
|
98
|
+
cols: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Creates a utility function which helps you position grid items in a container.
|
|
102
|
+
*/
|
|
103
|
+
declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
|
|
104
|
+
parentDimensions: GridDimensions;
|
|
105
|
+
dimensions: GridDimensions;
|
|
106
|
+
rows: number;
|
|
107
|
+
cols: number;
|
|
108
|
+
count: number;
|
|
109
|
+
gap: number;
|
|
110
|
+
}): (index: number) => Position;
|
|
111
|
+
/**
|
|
112
|
+
* Calculates data required for making a responsive grid.
|
|
113
|
+
*/
|
|
114
|
+
declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
|
|
115
|
+
/**
|
|
116
|
+
* Create a meet-style grid with support for different layout modes.
|
|
117
|
+
* This is the main function for creating video conferencing-style layouts.
|
|
118
|
+
*/
|
|
119
|
+
declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
|
|
120
|
+
/**
|
|
121
|
+
* Spring animation configuration presets
|
|
122
|
+
*/
|
|
123
|
+
declare const springPresets: {
|
|
124
|
+
/** Snappy animations for UI interactions */
|
|
125
|
+
readonly snappy: {
|
|
126
|
+
readonly stiffness: 400;
|
|
127
|
+
readonly damping: 30;
|
|
128
|
+
};
|
|
129
|
+
/** Smooth animations for layout changes */
|
|
130
|
+
readonly smooth: {
|
|
131
|
+
readonly stiffness: 300;
|
|
132
|
+
readonly damping: 30;
|
|
133
|
+
};
|
|
134
|
+
/** Gentle animations for subtle effects */
|
|
135
|
+
readonly gentle: {
|
|
136
|
+
readonly stiffness: 200;
|
|
137
|
+
readonly damping: 25;
|
|
138
|
+
};
|
|
139
|
+
/** Bouncy animations for playful effects */
|
|
140
|
+
readonly bouncy: {
|
|
141
|
+
readonly stiffness: 400;
|
|
142
|
+
readonly damping: 15;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
type SpringPreset = keyof typeof springPresets;
|
|
146
|
+
/**
|
|
147
|
+
* Get spring configuration for Motion animations
|
|
148
|
+
*/
|
|
149
|
+
declare function getSpringConfig(preset?: SpringPreset): {
|
|
150
|
+
stiffness: 400;
|
|
151
|
+
damping: 30;
|
|
152
|
+
type: "spring";
|
|
153
|
+
} | {
|
|
154
|
+
stiffness: 300;
|
|
155
|
+
damping: 30;
|
|
156
|
+
type: "spring";
|
|
157
|
+
} | {
|
|
158
|
+
stiffness: 200;
|
|
159
|
+
damping: 25;
|
|
160
|
+
type: "spring";
|
|
161
|
+
} | {
|
|
162
|
+
stiffness: 400;
|
|
163
|
+
damping: 15;
|
|
164
|
+
type: "spring";
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
|
|
168
|
+
export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
function getAspectRatio(ratio) {
|
|
2
|
+
const [width, height] = ratio.split(":");
|
|
3
|
+
if (!width || !height) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
return Number.parseInt(height) / Number.parseInt(width);
|
|
9
|
+
}
|
|
10
|
+
function parseAspectRatio(ratio) {
|
|
11
|
+
const [width, height] = ratio.split(":").map(Number);
|
|
12
|
+
if (!width || !height || isNaN(width) || isNaN(height)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return { widthRatio: width, heightRatio: height };
|
|
18
|
+
}
|
|
19
|
+
function getGridItemDimensions({
|
|
20
|
+
count,
|
|
21
|
+
dimensions,
|
|
22
|
+
aspectRatio,
|
|
23
|
+
gap
|
|
24
|
+
}) {
|
|
25
|
+
let { width: W, height: H } = dimensions;
|
|
26
|
+
if (W === 0 || H === 0 || count === 0) {
|
|
27
|
+
return { width: 0, height: 0, rows: 1, cols: 1 };
|
|
28
|
+
}
|
|
29
|
+
W -= gap * 2;
|
|
30
|
+
H -= gap * 2;
|
|
31
|
+
const s = gap;
|
|
32
|
+
const N = count;
|
|
33
|
+
const r = getAspectRatio(aspectRatio);
|
|
34
|
+
let w = 0;
|
|
35
|
+
let h = 0;
|
|
36
|
+
let a = 1;
|
|
37
|
+
let b = 1;
|
|
38
|
+
const widths = [];
|
|
39
|
+
for (let n = 1; n <= N; n++) {
|
|
40
|
+
widths.push((W - s * (n - 1)) / n, (H - s * (n - 1)) / (n * r));
|
|
41
|
+
}
|
|
42
|
+
widths.sort((a2, b2) => b2 - a2);
|
|
43
|
+
for (const width of widths) {
|
|
44
|
+
w = width;
|
|
45
|
+
h = w * r;
|
|
46
|
+
a = Math.floor((W + s) / (w + s));
|
|
47
|
+
b = Math.floor((H + s) / (h + s));
|
|
48
|
+
if (a * b >= N) {
|
|
49
|
+
a = Math.ceil(N / b);
|
|
50
|
+
b = Math.ceil(N / a);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { width: w, height: h, rows: b, cols: a };
|
|
55
|
+
}
|
|
56
|
+
function createGridItemPositioner({
|
|
57
|
+
parentDimensions,
|
|
58
|
+
dimensions,
|
|
59
|
+
rows,
|
|
60
|
+
cols,
|
|
61
|
+
count,
|
|
62
|
+
gap
|
|
63
|
+
}) {
|
|
64
|
+
const { width: W, height: H } = parentDimensions;
|
|
65
|
+
const { width: w, height: h } = dimensions;
|
|
66
|
+
const firstTop = (H - (h * rows + (rows - 1) * gap)) / 2;
|
|
67
|
+
let firstLeft = (W - (w * cols + (cols - 1) * gap)) / 2;
|
|
68
|
+
const topAdd = h + gap;
|
|
69
|
+
const leftAdd = w + gap;
|
|
70
|
+
let col = 0;
|
|
71
|
+
let row = 0;
|
|
72
|
+
const incompleteRowCols = count % cols;
|
|
73
|
+
function getPosition(index) {
|
|
74
|
+
const remaining = count - index;
|
|
75
|
+
if (remaining === incompleteRowCols && incompleteRowCols > 0) {
|
|
76
|
+
firstLeft = (W - (w * remaining + (remaining - 1) * gap)) / 2;
|
|
77
|
+
}
|
|
78
|
+
const top = firstTop + row * topAdd;
|
|
79
|
+
const left = firstLeft + col * leftAdd;
|
|
80
|
+
col++;
|
|
81
|
+
if ((index + 1) % cols === 0) {
|
|
82
|
+
row++;
|
|
83
|
+
col = 0;
|
|
84
|
+
}
|
|
85
|
+
return { top, left };
|
|
86
|
+
}
|
|
87
|
+
return getPosition;
|
|
88
|
+
}
|
|
89
|
+
function createGrid({ aspectRatio, count, dimensions, gap }) {
|
|
90
|
+
const { width, height, rows, cols } = getGridItemDimensions({
|
|
91
|
+
aspectRatio,
|
|
92
|
+
count,
|
|
93
|
+
dimensions,
|
|
94
|
+
gap
|
|
95
|
+
});
|
|
96
|
+
const getPosition = createGridItemPositioner({
|
|
97
|
+
parentDimensions: dimensions,
|
|
98
|
+
dimensions: { width, height },
|
|
99
|
+
rows,
|
|
100
|
+
cols,
|
|
101
|
+
count,
|
|
102
|
+
gap
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
rows,
|
|
108
|
+
cols,
|
|
109
|
+
getPosition
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createSidebarGrid(options) {
|
|
113
|
+
const { dimensions, gap, aspectRatio, count, sidebarPosition = "right", sidebarRatio = 0.25, pinnedIndex = 0 } = options;
|
|
114
|
+
if (count === 0) {
|
|
115
|
+
return createEmptyMeetGridResult("sidebar");
|
|
116
|
+
}
|
|
117
|
+
const { width: W, height: H } = dimensions;
|
|
118
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
119
|
+
const isVertical = sidebarPosition === "bottom";
|
|
120
|
+
let mainWidth;
|
|
121
|
+
let mainHeight;
|
|
122
|
+
let sidebarWidth;
|
|
123
|
+
let sidebarHeight;
|
|
124
|
+
if (isVertical) {
|
|
125
|
+
mainHeight = H * (1 - sidebarRatio) - gap;
|
|
126
|
+
mainWidth = W - gap * 2;
|
|
127
|
+
sidebarHeight = H * sidebarRatio - gap;
|
|
128
|
+
sidebarWidth = W - gap * 2;
|
|
129
|
+
} else {
|
|
130
|
+
mainWidth = W * (1 - sidebarRatio) - gap * 2;
|
|
131
|
+
mainHeight = H - gap * 2;
|
|
132
|
+
sidebarWidth = W * sidebarRatio - gap;
|
|
133
|
+
sidebarHeight = H - gap * 2;
|
|
134
|
+
}
|
|
135
|
+
let mainItemWidth = mainWidth;
|
|
136
|
+
let mainItemHeight = mainItemWidth * ratio;
|
|
137
|
+
if (mainItemHeight > mainHeight) {
|
|
138
|
+
mainItemHeight = mainHeight;
|
|
139
|
+
mainItemWidth = mainItemHeight / ratio;
|
|
140
|
+
}
|
|
141
|
+
const sidebarCount = count - 1;
|
|
142
|
+
let thumbWidth;
|
|
143
|
+
let thumbHeight;
|
|
144
|
+
if (sidebarCount > 0) {
|
|
145
|
+
if (isVertical) {
|
|
146
|
+
thumbWidth = Math.min((sidebarWidth - (sidebarCount - 1) * gap) / sidebarCount, sidebarHeight / ratio);
|
|
147
|
+
thumbHeight = thumbWidth * ratio;
|
|
148
|
+
} else {
|
|
149
|
+
thumbHeight = Math.min((sidebarHeight - (sidebarCount - 1) * gap) / sidebarCount, sidebarWidth / ratio);
|
|
150
|
+
thumbWidth = thumbHeight / ratio;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
thumbWidth = 0;
|
|
154
|
+
thumbHeight = 0;
|
|
155
|
+
}
|
|
156
|
+
const positions = [];
|
|
157
|
+
const mainLeft = isVertical ? gap + (mainWidth - mainItemWidth) / 2 : sidebarPosition === "left" ? sidebarWidth + gap * 2 + (mainWidth - mainItemWidth) / 2 : gap + (mainWidth - mainItemWidth) / 2;
|
|
158
|
+
const mainTop = isVertical ? gap + (mainHeight - mainItemHeight) / 2 : gap + (mainHeight - mainItemHeight) / 2;
|
|
159
|
+
positions[pinnedIndex] = {
|
|
160
|
+
position: { top: mainTop, left: mainLeft },
|
|
161
|
+
dimensions: { width: mainItemWidth, height: mainItemHeight }
|
|
162
|
+
};
|
|
163
|
+
let sidebarIndex = 0;
|
|
164
|
+
for (let i = 0; i < count; i++) {
|
|
165
|
+
if (i === pinnedIndex)
|
|
166
|
+
continue;
|
|
167
|
+
let left;
|
|
168
|
+
let top;
|
|
169
|
+
if (isVertical) {
|
|
170
|
+
const totalThumbWidth = sidebarCount * thumbWidth + (sidebarCount - 1) * gap;
|
|
171
|
+
const startLeft = gap + (sidebarWidth - totalThumbWidth) / 2;
|
|
172
|
+
left = startLeft + sidebarIndex * (thumbWidth + gap);
|
|
173
|
+
top = mainHeight + gap * 2;
|
|
174
|
+
} else {
|
|
175
|
+
left = sidebarPosition === "left" ? gap : mainWidth + gap * 2;
|
|
176
|
+
const totalThumbHeight = sidebarCount * thumbHeight + (sidebarCount - 1) * gap;
|
|
177
|
+
const startTop = gap + (sidebarHeight - totalThumbHeight) / 2;
|
|
178
|
+
top = startTop + sidebarIndex * (thumbHeight + gap);
|
|
179
|
+
}
|
|
180
|
+
positions[i] = {
|
|
181
|
+
position: { top, left },
|
|
182
|
+
dimensions: { width: thumbWidth, height: thumbHeight }
|
|
183
|
+
};
|
|
184
|
+
sidebarIndex++;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
width: mainItemWidth,
|
|
188
|
+
height: mainItemHeight,
|
|
189
|
+
rows: isVertical ? 2 : 1,
|
|
190
|
+
cols: isVertical ? 1 : 2,
|
|
191
|
+
layoutMode: "sidebar",
|
|
192
|
+
getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
|
|
193
|
+
getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
|
|
194
|
+
isMainItem: (index) => index === pinnedIndex
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function createSpeakerGrid(options) {
|
|
198
|
+
const { dimensions, gap, aspectRatio, count, speakerIndex = 0 } = options;
|
|
199
|
+
if (count === 0) {
|
|
200
|
+
return createEmptyMeetGridResult("speaker");
|
|
201
|
+
}
|
|
202
|
+
if (count === 1) {
|
|
203
|
+
const grid = createGrid({ ...options, count: 1 });
|
|
204
|
+
return {
|
|
205
|
+
...grid,
|
|
206
|
+
layoutMode: "speaker",
|
|
207
|
+
getItemDimensions: () => ({ width: grid.width, height: grid.height }),
|
|
208
|
+
isMainItem: () => true
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const { width: W, height: H } = dimensions;
|
|
212
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
213
|
+
const speakerHeight = (H - gap * 3) * 0.65;
|
|
214
|
+
const othersHeight = (H - gap * 3) * 0.35;
|
|
215
|
+
let speakerW = W - gap * 2;
|
|
216
|
+
let speakerH = speakerW * ratio;
|
|
217
|
+
if (speakerH > speakerHeight) {
|
|
218
|
+
speakerH = speakerHeight;
|
|
219
|
+
speakerW = speakerH / ratio;
|
|
220
|
+
}
|
|
221
|
+
const othersCount = count - 1;
|
|
222
|
+
let otherW = Math.min((W - gap * 2 - (othersCount - 1) * gap) / othersCount, othersHeight / ratio);
|
|
223
|
+
let otherH = otherW * ratio;
|
|
224
|
+
if (otherH > othersHeight) {
|
|
225
|
+
otherH = othersHeight;
|
|
226
|
+
otherW = otherH / ratio;
|
|
227
|
+
}
|
|
228
|
+
const positions = [];
|
|
229
|
+
positions[speakerIndex] = {
|
|
230
|
+
position: {
|
|
231
|
+
top: gap + (speakerHeight - speakerH) / 2,
|
|
232
|
+
left: gap + (W - gap * 2 - speakerW) / 2
|
|
233
|
+
},
|
|
234
|
+
dimensions: { width: speakerW, height: speakerH }
|
|
235
|
+
};
|
|
236
|
+
const totalOthersWidth = othersCount * otherW + (othersCount - 1) * gap;
|
|
237
|
+
const startLeft = gap + (W - gap * 2 - totalOthersWidth) / 2;
|
|
238
|
+
let otherIndex = 0;
|
|
239
|
+
for (let i = 0; i < count; i++) {
|
|
240
|
+
if (i === speakerIndex)
|
|
241
|
+
continue;
|
|
242
|
+
positions[i] = {
|
|
243
|
+
position: {
|
|
244
|
+
top: speakerHeight + gap * 2 + (othersHeight - otherH) / 2,
|
|
245
|
+
left: startLeft + otherIndex * (otherW + gap)
|
|
246
|
+
},
|
|
247
|
+
dimensions: { width: otherW, height: otherH }
|
|
248
|
+
};
|
|
249
|
+
otherIndex++;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
width: speakerW,
|
|
253
|
+
height: speakerH,
|
|
254
|
+
rows: 2,
|
|
255
|
+
cols: othersCount,
|
|
256
|
+
layoutMode: "speaker",
|
|
257
|
+
getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
|
|
258
|
+
getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
|
|
259
|
+
isMainItem: (index) => index === speakerIndex
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function createSpotlightGrid(options) {
|
|
263
|
+
const { dimensions, gap, aspectRatio, pinnedIndex = 0 } = options;
|
|
264
|
+
const { width: W, height: H } = dimensions;
|
|
265
|
+
const ratio = getAspectRatio(aspectRatio);
|
|
266
|
+
let spotWidth = W - gap * 2;
|
|
267
|
+
let spotHeight = spotWidth * ratio;
|
|
268
|
+
if (spotHeight > H - gap * 2) {
|
|
269
|
+
spotHeight = H - gap * 2;
|
|
270
|
+
spotWidth = spotHeight / ratio;
|
|
271
|
+
}
|
|
272
|
+
const position = {
|
|
273
|
+
top: gap + (H - gap * 2 - spotHeight) / 2,
|
|
274
|
+
left: gap + (W - gap * 2 - spotWidth) / 2
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
width: spotWidth,
|
|
278
|
+
height: spotHeight,
|
|
279
|
+
rows: 1,
|
|
280
|
+
cols: 1,
|
|
281
|
+
layoutMode: "spotlight",
|
|
282
|
+
getPosition: (index) => index === pinnedIndex ? position : { top: -9999, left: -9999 },
|
|
283
|
+
getItemDimensions: (index) => index === pinnedIndex ? { width: spotWidth, height: spotHeight } : { width: 0, height: 0 },
|
|
284
|
+
isMainItem: (index) => index === pinnedIndex
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function createEmptyMeetGridResult(layoutMode) {
|
|
288
|
+
return {
|
|
289
|
+
width: 0,
|
|
290
|
+
height: 0,
|
|
291
|
+
rows: 0,
|
|
292
|
+
cols: 0,
|
|
293
|
+
layoutMode,
|
|
294
|
+
getPosition: () => ({ top: 0, left: 0 }),
|
|
295
|
+
getItemDimensions: () => ({ width: 0, height: 0 }),
|
|
296
|
+
isMainItem: () => false
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function createMeetGrid(options) {
|
|
300
|
+
const { layoutMode = "gallery", count } = options;
|
|
301
|
+
if (count === 0) {
|
|
302
|
+
return createEmptyMeetGridResult(layoutMode);
|
|
303
|
+
}
|
|
304
|
+
switch (layoutMode) {
|
|
305
|
+
case "spotlight":
|
|
306
|
+
return createSpotlightGrid(options);
|
|
307
|
+
case "speaker":
|
|
308
|
+
return createSpeakerGrid(options);
|
|
309
|
+
case "sidebar":
|
|
310
|
+
return createSidebarGrid(options);
|
|
311
|
+
case "gallery":
|
|
312
|
+
default: {
|
|
313
|
+
const grid = createGrid(options);
|
|
314
|
+
return {
|
|
315
|
+
...grid,
|
|
316
|
+
layoutMode: "gallery",
|
|
317
|
+
getItemDimensions: () => ({ width: grid.width, height: grid.height }),
|
|
318
|
+
isMainItem: () => false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const springPresets = {
|
|
324
|
+
/** Snappy animations for UI interactions */
|
|
325
|
+
snappy: { stiffness: 400, damping: 30 },
|
|
326
|
+
/** Smooth animations for layout changes */
|
|
327
|
+
smooth: { stiffness: 300, damping: 30 },
|
|
328
|
+
/** Gentle animations for subtle effects */
|
|
329
|
+
gentle: { stiffness: 200, damping: 25 },
|
|
330
|
+
/** Bouncy animations for playful effects */
|
|
331
|
+
bouncy: { stiffness: 400, damping: 15 }
|
|
332
|
+
};
|
|
333
|
+
function getSpringConfig(preset = "smooth") {
|
|
334
|
+
return {
|
|
335
|
+
type: "spring",
|
|
336
|
+
...springPresets[preset]
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thangdevalone/meet-layout-grid-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core grid calculation logic for meet-layout-grid",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "unbuild",
|
|
26
|
+
"dev": "unbuild --watch",
|
|
27
|
+
"clean": "rimraf dist",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"grid",
|
|
33
|
+
"responsive",
|
|
34
|
+
"meeting",
|
|
35
|
+
"video",
|
|
36
|
+
"layout"
|
|
37
|
+
],
|
|
38
|
+
"author": "ThangDevAlone",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/thangdevalone/meet-layout-grid"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"unbuild": "^2.0.0",
|
|
46
|
+
"vitest": "^1.0.0",
|
|
47
|
+
"rimraf": "^5.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|