@umituz/react-native-onboarding 3.6.12 → 3.6.13
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/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/infrastructure/utils/layouts/collageLayout.ts +81 -0
- package/src/infrastructure/utils/layouts/gridLayouts.ts +68 -0
- package/src/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
- package/src/infrastructure/utils/layouts/index.ts +12 -0
- package/src/infrastructure/utils/layouts/layoutTypes.ts +25 -0
- package/src/infrastructure/utils/layouts/masonryLayout.ts +37 -0
- package/src/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
- package/src/infrastructure/utils/layouts/screenDimensions.ts +11 -0
- package/src/infrastructure/utils/layouts/tilesLayout.ts +34 -0
- package/src/presentation/components/BackgroundImageCollage.tsx +14 -23
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +3 -3
- package/src/infrastructure/utils/imageLayoutUtils.ts +0 -280
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.13",
|
|
4
4
|
"description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type { OnboardingTheme, OnboardingColors } from "./presentation/types/Onb
|
|
|
77
77
|
// =============================================================================
|
|
78
78
|
|
|
79
79
|
export { ensureArray, safeIncludes, safeFilter } from "./infrastructure/utils/arrayUtils";
|
|
80
|
-
export type { ImageLayoutItem, LayoutConfig } from "./infrastructure/utils/
|
|
80
|
+
export type { ImageLayoutItem, LayoutConfig } from "./infrastructure/utils/layouts";
|
|
81
81
|
|
|
82
82
|
// Export OnboardingSlide component
|
|
83
83
|
// Note: TypeScript doesn't allow exporting both a type and a value with the same name
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage Layout Generator
|
|
3
|
+
* Random collage with varying sizes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateCollageLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 8 } = config;
|
|
14
|
+
const layouts: ImageLayoutItem[] = [];
|
|
15
|
+
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
|
|
16
|
+
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
|
|
17
|
+
|
|
18
|
+
images.forEach((source) => {
|
|
19
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
20
|
+
const position = findNonOverlappingPosition(layouts, size);
|
|
21
|
+
|
|
22
|
+
layouts.push({
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: position.left,
|
|
27
|
+
top: position.top,
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return layouts;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const findNonOverlappingPosition = (
|
|
39
|
+
existing: ImageLayoutItem[],
|
|
40
|
+
size: number,
|
|
41
|
+
): { left: number; top: number } => {
|
|
42
|
+
const maxX = SCREEN_WIDTH - size;
|
|
43
|
+
const maxY = SCREEN_HEIGHT - size;
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
let left = 0;
|
|
46
|
+
let top = 0;
|
|
47
|
+
|
|
48
|
+
while (attempts < 50) {
|
|
49
|
+
left = Math.random() * maxX;
|
|
50
|
+
top = Math.random() * maxY;
|
|
51
|
+
|
|
52
|
+
if (!hasOverlap(existing, left, top, size)) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
attempts++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { left, top };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hasOverlap = (
|
|
62
|
+
existing: ImageLayoutItem[],
|
|
63
|
+
left: number,
|
|
64
|
+
top: number,
|
|
65
|
+
size: number,
|
|
66
|
+
): boolean => {
|
|
67
|
+
const threshold = 20;
|
|
68
|
+
|
|
69
|
+
for (const item of existing) {
|
|
70
|
+
const { style } = item;
|
|
71
|
+
if (
|
|
72
|
+
left < style.left + style.width - threshold &&
|
|
73
|
+
left + size > style.left + threshold &&
|
|
74
|
+
top < style.top + style.height - threshold &&
|
|
75
|
+
top + size > style.top + threshold
|
|
76
|
+
) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Layout Generators
|
|
3
|
+
* Standard and dense grid layouts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateGridLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns, gap = 0, borderRadius = 0 } = config;
|
|
14
|
+
const count = images.length;
|
|
15
|
+
const cols = columns ?? Math.ceil(Math.sqrt(count));
|
|
16
|
+
const rows = Math.ceil(count / cols);
|
|
17
|
+
const totalGapX = gap * (cols - 1);
|
|
18
|
+
const totalGapY = gap * (rows - 1);
|
|
19
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / cols;
|
|
20
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
21
|
+
|
|
22
|
+
return images.map((source, index) => {
|
|
23
|
+
const col = index % cols;
|
|
24
|
+
const row = Math.floor(index / cols);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
source,
|
|
28
|
+
style: {
|
|
29
|
+
position: "absolute" as const,
|
|
30
|
+
left: col * (cellWidth + gap),
|
|
31
|
+
top: row * (cellHeight + gap),
|
|
32
|
+
width: cellWidth,
|
|
33
|
+
height: cellHeight,
|
|
34
|
+
borderRadius,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const generateDenseGridLayout = (
|
|
41
|
+
images: unknown[],
|
|
42
|
+
config: LayoutConfig = {},
|
|
43
|
+
): ImageLayoutItem[] => {
|
|
44
|
+
const { columns = 6, gap = 2, borderRadius = 4 } = config;
|
|
45
|
+
const count = images.length;
|
|
46
|
+
const rows = Math.ceil(count / columns);
|
|
47
|
+
const totalGapX = gap * (columns - 1);
|
|
48
|
+
const totalGapY = gap * (rows - 1);
|
|
49
|
+
const cellWidth = (SCREEN_WIDTH - totalGapX) / columns;
|
|
50
|
+
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
51
|
+
|
|
52
|
+
return images.map((source, index) => {
|
|
53
|
+
const col = index % columns;
|
|
54
|
+
const row = Math.floor(index / columns);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
source,
|
|
58
|
+
style: {
|
|
59
|
+
position: "absolute" as const,
|
|
60
|
+
left: col * (cellWidth + gap),
|
|
61
|
+
top: row * (cellHeight + gap),
|
|
62
|
+
width: cellWidth,
|
|
63
|
+
height: cellHeight,
|
|
64
|
+
borderRadius,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeycomb Layout Generator
|
|
3
|
+
* Hexagonal pattern layout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateHoneycombLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 50 } = config;
|
|
14
|
+
const size = 80;
|
|
15
|
+
const horizontalSpacing = size * 0.85;
|
|
16
|
+
const verticalSpacing = size * 0.75;
|
|
17
|
+
const columns = Math.floor(SCREEN_WIDTH / horizontalSpacing);
|
|
18
|
+
|
|
19
|
+
return images.map((source, index) => {
|
|
20
|
+
const row = Math.floor(index / columns);
|
|
21
|
+
const col = index % columns;
|
|
22
|
+
const offsetX = row % 2 === 1 ? horizontalSpacing / 2 : 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
source,
|
|
26
|
+
style: {
|
|
27
|
+
position: "absolute" as const,
|
|
28
|
+
left: col * horizontalSpacing + offsetX,
|
|
29
|
+
top: row * verticalSpacing,
|
|
30
|
+
width: size,
|
|
31
|
+
height: size,
|
|
32
|
+
borderRadius,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Generators - Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { ImageLayoutItem, ImageLayoutStyle, LayoutConfig } from "./layoutTypes";
|
|
6
|
+
|
|
7
|
+
export { generateGridLayout, generateDenseGridLayout } from "./gridLayouts";
|
|
8
|
+
export { generateMasonryLayout } from "./masonryLayout";
|
|
9
|
+
export { generateCollageLayout } from "./collageLayout";
|
|
10
|
+
export { generateScatteredLayout } from "./scatteredLayout";
|
|
11
|
+
export { generateTilesLayout } from "./tilesLayout";
|
|
12
|
+
export { generateHoneycombLayout } from "./honeycombLayout";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Types
|
|
3
|
+
* Type definitions for image layout generators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ImageLayoutStyle {
|
|
7
|
+
position: "absolute";
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
borderRadius?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ImageLayoutItem {
|
|
16
|
+
source: unknown;
|
|
17
|
+
style: ImageLayoutStyle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LayoutConfig {
|
|
21
|
+
columns?: number;
|
|
22
|
+
gap?: number;
|
|
23
|
+
borderRadius?: number;
|
|
24
|
+
randomizeSize?: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masonry Layout Generator
|
|
3
|
+
* Pinterest-style masonry layout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateMasonryLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns = 3, gap = 2, borderRadius = 4 } = config;
|
|
14
|
+
const colWidth = (SCREEN_WIDTH - gap * (columns - 1)) / columns;
|
|
15
|
+
const columnHeights = new Array(columns).fill(0);
|
|
16
|
+
|
|
17
|
+
return images.map((source) => {
|
|
18
|
+
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
19
|
+
const aspectRatio = 0.7 + Math.random() * 0.6;
|
|
20
|
+
const height = colWidth * aspectRatio;
|
|
21
|
+
|
|
22
|
+
const layout: ImageLayoutItem = {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: shortestCol * (colWidth + gap),
|
|
27
|
+
top: columnHeights[shortestCol],
|
|
28
|
+
width: colWidth,
|
|
29
|
+
height,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
columnHeights[shortestCol] += height + gap;
|
|
35
|
+
return layout;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scattered Layout Generator
|
|
3
|
+
* Random small images scattered across screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateScatteredLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 6 } = config;
|
|
14
|
+
const minSize = 60;
|
|
15
|
+
const maxSize = 100;
|
|
16
|
+
|
|
17
|
+
return images.map((source) => {
|
|
18
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
19
|
+
const left = Math.random() * (SCREEN_WIDTH - size);
|
|
20
|
+
const top = Math.random() * (SCREEN_HEIGHT - size);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left,
|
|
27
|
+
top,
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Dimensions
|
|
3
|
+
* Centralized screen dimension values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Dimensions } from "react-native";
|
|
7
|
+
|
|
8
|
+
const dimensions = Dimensions.get("window");
|
|
9
|
+
|
|
10
|
+
export const SCREEN_WIDTH = dimensions.width;
|
|
11
|
+
export const SCREEN_HEIGHT = dimensions.height;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiles Layout Generator
|
|
3
|
+
* Fixed size tiles centered on screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateTilesLayout = (
|
|
10
|
+
images: unknown[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { columns = 5, gap = 4, borderRadius = 8 } = config;
|
|
14
|
+
const tileSize = (SCREEN_WIDTH - gap * (columns + 1)) / columns;
|
|
15
|
+
const rows = Math.ceil(images.length / columns);
|
|
16
|
+
const startY = (SCREEN_HEIGHT - rows * (tileSize + gap)) / 2;
|
|
17
|
+
|
|
18
|
+
return images.map((source, index) => {
|
|
19
|
+
const col = index % columns;
|
|
20
|
+
const row = Math.floor(index / columns);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: gap + col * (tileSize + gap),
|
|
27
|
+
top: startY + row * (tileSize + gap),
|
|
28
|
+
width: tileSize,
|
|
29
|
+
height: tileSize,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
generateHoneycombLayout,
|
|
17
17
|
type ImageLayoutItem,
|
|
18
18
|
type LayoutConfig,
|
|
19
|
-
} from "
|
|
20
|
-
import { ensureArray } from "
|
|
19
|
+
} from "@/infrastructure/utils/layouts";
|
|
20
|
+
import { ensureArray } from "@/infrastructure/utils/arrayUtils";
|
|
21
21
|
|
|
22
22
|
export type CollageLayout =
|
|
23
23
|
| "grid"
|
|
@@ -37,21 +37,16 @@ export interface BackgroundImageCollageProps {
|
|
|
37
37
|
opacity?: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
const generators: Record<
|
|
42
|
-
CollageLayout,
|
|
43
|
-
(images: unknown[], config: LayoutConfig) => ImageLayoutItem[]
|
|
44
|
-
> = {
|
|
45
|
-
grid: generateGridLayout,
|
|
46
|
-
dense: generateDenseGridLayout,
|
|
47
|
-
masonry: generateMasonryLayout,
|
|
48
|
-
collage: generateCollageLayout,
|
|
49
|
-
scattered: generateScatteredLayout,
|
|
50
|
-
tiles: generateTilesLayout,
|
|
51
|
-
honeycomb: generateHoneycombLayout,
|
|
52
|
-
};
|
|
40
|
+
type LayoutGenerator = (images: unknown[], config: LayoutConfig) => ImageLayoutItem[];
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
const LAYOUT_GENERATORS: Record<CollageLayout, LayoutGenerator> = {
|
|
43
|
+
grid: generateGridLayout,
|
|
44
|
+
dense: generateDenseGridLayout,
|
|
45
|
+
masonry: generateMasonryLayout,
|
|
46
|
+
collage: generateCollageLayout,
|
|
47
|
+
scattered: generateScatteredLayout,
|
|
48
|
+
tiles: generateTilesLayout,
|
|
49
|
+
honeycomb: generateHoneycombLayout,
|
|
55
50
|
};
|
|
56
51
|
|
|
57
52
|
export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
@@ -67,15 +62,11 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
|
67
62
|
const imageLayouts = useMemo(() => {
|
|
68
63
|
if (safeImages.length === 0) return [];
|
|
69
64
|
|
|
70
|
-
const generator =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return generator(safeImages, config);
|
|
65
|
+
const generator = LAYOUT_GENERATORS[layout] ?? generateGridLayout;
|
|
66
|
+
return generator(safeImages, { columns, gap, borderRadius });
|
|
74
67
|
}, [safeImages, layout, columns, gap, borderRadius]);
|
|
75
68
|
|
|
76
|
-
if (imageLayouts.length === 0)
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
69
|
+
if (imageLayouts.length === 0) return null;
|
|
79
70
|
|
|
80
71
|
return (
|
|
81
72
|
<View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import { useOnboardingProvider } from "
|
|
5
|
-
import { ensureArray } from "
|
|
6
|
-
import type { OnboardingQuestion, QuestionOption } from "
|
|
4
|
+
import { useOnboardingProvider } from "@/presentation/providers/OnboardingProvider";
|
|
5
|
+
import { ensureArray } from "@/infrastructure/utils/arrayUtils";
|
|
6
|
+
import type { OnboardingQuestion, QuestionOption } from "@/domain/entities/OnboardingQuestion";
|
|
7
7
|
|
|
8
8
|
export interface MultipleChoiceQuestionProps {
|
|
9
9
|
question: OnboardingQuestion;
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image Layout Utilities
|
|
3
|
-
* Layout generators for BackgroundImageCollage component
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Dimensions } from "react-native";
|
|
7
|
-
|
|
8
|
-
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
9
|
-
|
|
10
|
-
export interface ImageLayoutStyle {
|
|
11
|
-
position: "absolute";
|
|
12
|
-
top: number;
|
|
13
|
-
left: number;
|
|
14
|
-
width: number;
|
|
15
|
-
height: number;
|
|
16
|
-
borderRadius?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ImageLayoutItem {
|
|
20
|
-
source: unknown;
|
|
21
|
-
style: ImageLayoutStyle;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface LayoutConfig {
|
|
25
|
-
columns?: number;
|
|
26
|
-
gap?: number;
|
|
27
|
-
borderRadius?: number;
|
|
28
|
-
randomizeSize?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Generate a uniform grid layout
|
|
33
|
-
*/
|
|
34
|
-
export const generateGridLayout = (
|
|
35
|
-
images: unknown[],
|
|
36
|
-
config: LayoutConfig = {},
|
|
37
|
-
): ImageLayoutItem[] => {
|
|
38
|
-
const { columns, gap = 0, borderRadius = 0 } = config;
|
|
39
|
-
const count = images.length;
|
|
40
|
-
const cols = columns ?? Math.ceil(Math.sqrt(count));
|
|
41
|
-
const rows = Math.ceil(count / cols);
|
|
42
|
-
const totalGapX = gap * (cols - 1);
|
|
43
|
-
const totalGapY = gap * (rows - 1);
|
|
44
|
-
const cellWidth = (SCREEN_WIDTH - totalGapX) / cols;
|
|
45
|
-
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
46
|
-
|
|
47
|
-
return images.map((source, index) => {
|
|
48
|
-
const col = index % cols;
|
|
49
|
-
const row = Math.floor(index / cols);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
source,
|
|
53
|
-
style: {
|
|
54
|
-
position: "absolute" as const,
|
|
55
|
-
left: col * (cellWidth + gap),
|
|
56
|
-
top: row * (cellHeight + gap),
|
|
57
|
-
width: cellWidth,
|
|
58
|
-
height: cellHeight,
|
|
59
|
-
borderRadius,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Generate a dense grid layout with many small images
|
|
67
|
-
*/
|
|
68
|
-
export const generateDenseGridLayout = (
|
|
69
|
-
images: unknown[],
|
|
70
|
-
config: LayoutConfig = {},
|
|
71
|
-
): ImageLayoutItem[] => {
|
|
72
|
-
const { columns = 6, gap = 2, borderRadius = 4 } = config;
|
|
73
|
-
const count = images.length;
|
|
74
|
-
const rows = Math.ceil(count / columns);
|
|
75
|
-
const totalGapX = gap * (columns - 1);
|
|
76
|
-
const totalGapY = gap * (rows - 1);
|
|
77
|
-
const cellWidth = (SCREEN_WIDTH - totalGapX) / columns;
|
|
78
|
-
const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
|
|
79
|
-
|
|
80
|
-
return images.map((source, index) => {
|
|
81
|
-
const col = index % columns;
|
|
82
|
-
const row = Math.floor(index / columns);
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
source,
|
|
86
|
-
style: {
|
|
87
|
-
position: "absolute" as const,
|
|
88
|
-
left: col * (cellWidth + gap),
|
|
89
|
-
top: row * (cellHeight + gap),
|
|
90
|
-
width: cellWidth,
|
|
91
|
-
height: cellHeight,
|
|
92
|
-
borderRadius,
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Generate a masonry-style layout (Pinterest-like)
|
|
100
|
-
*/
|
|
101
|
-
export const generateMasonryLayout = (
|
|
102
|
-
images: unknown[],
|
|
103
|
-
config: LayoutConfig = {},
|
|
104
|
-
): ImageLayoutItem[] => {
|
|
105
|
-
const { columns = 3, gap = 2, borderRadius = 4 } = config;
|
|
106
|
-
const colWidth = (SCREEN_WIDTH - gap * (columns - 1)) / columns;
|
|
107
|
-
const columnHeights = new Array(columns).fill(0);
|
|
108
|
-
|
|
109
|
-
return images.map((source) => {
|
|
110
|
-
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
111
|
-
const aspectRatio = 0.7 + Math.random() * 0.6;
|
|
112
|
-
const height = colWidth * aspectRatio;
|
|
113
|
-
|
|
114
|
-
const layout: ImageLayoutItem = {
|
|
115
|
-
source,
|
|
116
|
-
style: {
|
|
117
|
-
position: "absolute" as const,
|
|
118
|
-
left: shortestCol * (colWidth + gap),
|
|
119
|
-
top: columnHeights[shortestCol],
|
|
120
|
-
width: colWidth,
|
|
121
|
-
height,
|
|
122
|
-
borderRadius,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
columnHeights[shortestCol] += height + gap;
|
|
127
|
-
return layout;
|
|
128
|
-
});
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Generate a random collage layout with varying sizes
|
|
133
|
-
*/
|
|
134
|
-
export const generateCollageLayout = (
|
|
135
|
-
images: unknown[],
|
|
136
|
-
config: LayoutConfig = {},
|
|
137
|
-
): ImageLayoutItem[] => {
|
|
138
|
-
const { borderRadius = 8 } = config;
|
|
139
|
-
const layouts: ImageLayoutItem[] = [];
|
|
140
|
-
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
|
|
141
|
-
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
|
|
142
|
-
|
|
143
|
-
images.forEach((source) => {
|
|
144
|
-
const size = minSize + Math.random() * (maxSize - minSize);
|
|
145
|
-
const maxX = SCREEN_WIDTH - size;
|
|
146
|
-
const maxY = SCREEN_HEIGHT - size;
|
|
147
|
-
|
|
148
|
-
let attempts = 0;
|
|
149
|
-
let overlaps = true;
|
|
150
|
-
let left = 0;
|
|
151
|
-
let top = 0;
|
|
152
|
-
|
|
153
|
-
while (overlaps && attempts < 50) {
|
|
154
|
-
left = Math.random() * maxX;
|
|
155
|
-
top = Math.random() * maxY;
|
|
156
|
-
overlaps = false;
|
|
157
|
-
|
|
158
|
-
for (const existing of layouts) {
|
|
159
|
-
const overlapThreshold = 20;
|
|
160
|
-
if (
|
|
161
|
-
left < existing.style.left + existing.style.width - overlapThreshold &&
|
|
162
|
-
left + size > existing.style.left + overlapThreshold &&
|
|
163
|
-
top < existing.style.top + existing.style.height - overlapThreshold &&
|
|
164
|
-
top + size > existing.style.top + overlapThreshold
|
|
165
|
-
) {
|
|
166
|
-
overlaps = true;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
attempts++;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
layouts.push({
|
|
175
|
-
source,
|
|
176
|
-
style: {
|
|
177
|
-
position: "absolute" as const,
|
|
178
|
-
left,
|
|
179
|
-
top,
|
|
180
|
-
width: size,
|
|
181
|
-
height: size,
|
|
182
|
-
borderRadius,
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
return layouts;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Generate a scattered layout with small images
|
|
192
|
-
*/
|
|
193
|
-
export const generateScatteredLayout = (
|
|
194
|
-
images: unknown[],
|
|
195
|
-
config: LayoutConfig = {},
|
|
196
|
-
): ImageLayoutItem[] => {
|
|
197
|
-
const { borderRadius = 6 } = config;
|
|
198
|
-
const minSize = 60;
|
|
199
|
-
const maxSize = 100;
|
|
200
|
-
|
|
201
|
-
return images.map((source) => {
|
|
202
|
-
const size = minSize + Math.random() * (maxSize - minSize);
|
|
203
|
-
const left = Math.random() * (SCREEN_WIDTH - size);
|
|
204
|
-
const top = Math.random() * (SCREEN_HEIGHT - size);
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
source,
|
|
208
|
-
style: {
|
|
209
|
-
position: "absolute" as const,
|
|
210
|
-
left,
|
|
211
|
-
top,
|
|
212
|
-
width: size,
|
|
213
|
-
height: size,
|
|
214
|
-
borderRadius,
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
});
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Generate a tiles layout with fixed small size
|
|
222
|
-
*/
|
|
223
|
-
export const generateTilesLayout = (
|
|
224
|
-
images: unknown[],
|
|
225
|
-
config: LayoutConfig = {},
|
|
226
|
-
): ImageLayoutItem[] => {
|
|
227
|
-
const { columns = 5, gap = 4, borderRadius = 8 } = config;
|
|
228
|
-
const tileSize = (SCREEN_WIDTH - gap * (columns + 1)) / columns;
|
|
229
|
-
const rows = Math.ceil(images.length / columns);
|
|
230
|
-
const startY = (SCREEN_HEIGHT - rows * (tileSize + gap)) / 2;
|
|
231
|
-
|
|
232
|
-
return images.map((source, index) => {
|
|
233
|
-
const col = index % columns;
|
|
234
|
-
const row = Math.floor(index / columns);
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
source,
|
|
238
|
-
style: {
|
|
239
|
-
position: "absolute" as const,
|
|
240
|
-
left: gap + col * (tileSize + gap),
|
|
241
|
-
top: startY + row * (tileSize + gap),
|
|
242
|
-
width: tileSize,
|
|
243
|
-
height: tileSize,
|
|
244
|
-
borderRadius,
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Generate a honeycomb/hex layout
|
|
252
|
-
*/
|
|
253
|
-
export const generateHoneycombLayout = (
|
|
254
|
-
images: unknown[],
|
|
255
|
-
config: LayoutConfig = {},
|
|
256
|
-
): ImageLayoutItem[] => {
|
|
257
|
-
const { borderRadius = 50 } = config;
|
|
258
|
-
const size = 80;
|
|
259
|
-
const horizontalSpacing = size * 0.85;
|
|
260
|
-
const verticalSpacing = size * 0.75;
|
|
261
|
-
const columns = Math.floor(SCREEN_WIDTH / horizontalSpacing);
|
|
262
|
-
|
|
263
|
-
return images.map((source, index) => {
|
|
264
|
-
const row = Math.floor(index / columns);
|
|
265
|
-
const col = index % columns;
|
|
266
|
-
const offsetX = row % 2 === 1 ? horizontalSpacing / 2 : 0;
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
source,
|
|
270
|
-
style: {
|
|
271
|
-
position: "absolute" as const,
|
|
272
|
-
left: col * horizontalSpacing + offsetX,
|
|
273
|
-
top: row * verticalSpacing,
|
|
274
|
-
width: size,
|
|
275
|
-
height: size,
|
|
276
|
-
borderRadius,
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
};
|