react-native-image-collage 0.2.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/LICENSE +21 -0
- package/README.md +564 -0
- package/dist/CollageImage.d.ts +7 -0
- package/dist/CollageImage.d.ts.map +1 -0
- package/dist/CollageImage.js +49 -0
- package/dist/CollageTile.d.ts +18 -0
- package/dist/CollageTile.d.ts.map +1 -0
- package/dist/CollageTile.js +58 -0
- package/dist/CollageWithViewer.d.ts +4 -0
- package/dist/CollageWithViewer.d.ts.map +1 -0
- package/dist/CollageWithViewer.js +65 -0
- package/dist/ImageCollage.d.ts +4 -0
- package/dist/ImageCollage.d.ts.map +1 -0
- package/dist/ImageCollage.js +135 -0
- package/dist/ImageCollageWithViewer.d.ts +4 -0
- package/dist/ImageCollageWithViewer.d.ts.map +1 -0
- package/dist/ImageCollageWithViewer.js +59 -0
- package/dist/ImageViewer.d.ts +4 -0
- package/dist/ImageViewer.d.ts.map +1 -0
- package/dist/ImageViewer.js +76 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/expo/createExpoImageRenderer.d.ts +9 -0
- package/dist/expo/createExpoImageRenderer.d.ts.map +1 -0
- package/dist/expo/createExpoImageRenderer.js +20 -0
- package/dist/expo/index.d.ts +15 -0
- package/dist/expo/index.d.ts.map +1 -0
- package/dist/expo/index.js +59 -0
- package/dist/hooks/useContainerWidth.d.ts +10 -0
- package/dist/hooks/useContainerWidth.d.ts.map +1 -0
- package/dist/hooks/useContainerWidth.js +22 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/utils/imageSources.d.ts +11 -0
- package/dist/utils/imageSources.d.ts.map +1 -0
- package/dist/utils/imageSources.js +131 -0
- package/dist/utils/layoutHeight.d.ts +9 -0
- package/dist/utils/layoutHeight.d.ts.map +1 -0
- package/dist/utils/layoutHeight.js +41 -0
- package/dist/utils/renderCollageLayouts.d.ts +97 -0
- package/dist/utils/renderCollageLayouts.d.ts.map +1 -0
- package/dist/utils/renderCollageLayouts.js +183 -0
- package/dist/viewer/ImageCollageWithViewer.d.ts +4 -0
- package/dist/viewer/ImageCollageWithViewer.d.ts.map +1 -0
- package/dist/viewer/ImageCollageWithViewer.js +43 -0
- package/dist/viewer/ImageViewer.d.ts +5 -0
- package/dist/viewer/ImageViewer.d.ts.map +1 -0
- package/dist/viewer/ImageViewer.js +85 -0
- package/dist/viewer/index.d.ts +4 -0
- package/dist/viewer/index.d.ts.map +1 -0
- package/dist/viewer/index.js +8 -0
- package/package.json +68 -0
- package/src/CollageImage.tsx +41 -0
- package/src/CollageTile.tsx +69 -0
- package/src/CollageWithViewer.tsx +53 -0
- package/src/ImageCollage.tsx +168 -0
- package/src/constants.ts +11 -0
- package/src/expo/createExpoImageRenderer.tsx +43 -0
- package/src/expo/index.tsx +99 -0
- package/src/hooks/useContainerWidth.ts +29 -0
- package/src/index.ts +42 -0
- package/src/types.ts +120 -0
- package/src/utils/imageSources.ts +170 -0
- package/src/utils/layoutHeight.ts +54 -0
- package/src/utils/renderCollageLayouts.tsx +329 -0
- package/src/viewer/ImageCollageWithViewer.tsx +24 -0
- package/src/viewer/ImageViewer.tsx +93 -0
- package/src/viewer/index.ts +10 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ImageCollage as BaseImageCollage } from "../ImageCollage";
|
|
3
|
+
import type {
|
|
4
|
+
ExpoImageCollageOptions,
|
|
5
|
+
ImageCollageProps,
|
|
6
|
+
ImageCollageWithViewerProps,
|
|
7
|
+
ImagePriority,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { ImageCollageWithViewer as BaseImageCollageWithViewer } from "../viewer/ImageCollageWithViewer";
|
|
10
|
+
import { createExpoImageRenderer } from "./createExpoImageRenderer";
|
|
11
|
+
|
|
12
|
+
export { CollageImage, renderCollageImage } from "../CollageImage";
|
|
13
|
+
export { CollageTile } from "../CollageTile";
|
|
14
|
+
export { CollageWithViewer } from "../CollageWithViewer";
|
|
15
|
+
export { ImageCollage as ImageCollague } from "../ImageCollage";
|
|
16
|
+
export { ImageViewer, createDefaultViewerRenderer } from "../viewer/ImageViewer";
|
|
17
|
+
export { ImageCollageWithViewer as BaseImageCollageWithViewer } from "../viewer/ImageCollageWithViewer";
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
CollageImageInput,
|
|
21
|
+
CollageImageRenderProps,
|
|
22
|
+
CollageImageRenderer,
|
|
23
|
+
CollageViewerRenderProps,
|
|
24
|
+
CollageViewerRenderer,
|
|
25
|
+
CollageWithViewerProps,
|
|
26
|
+
ExpoImageCollageOptions,
|
|
27
|
+
ImageCollageProps,
|
|
28
|
+
ImageCollageWithViewerProps,
|
|
29
|
+
ImagePriority,
|
|
30
|
+
ImageViewerImage,
|
|
31
|
+
ImageViewerProps,
|
|
32
|
+
NormalizedCollageImage,
|
|
33
|
+
} from "../types";
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
ANDROID_RIPPLE,
|
|
37
|
+
DEFAULT_BLURHASH,
|
|
38
|
+
DEFAULT_BORDER_RADIUS,
|
|
39
|
+
DEFAULT_LAYOUT_MAX_HEIGHT,
|
|
40
|
+
DEFAULT_LAYOUT_MIN_HEIGHT,
|
|
41
|
+
DEFAULT_MAX_VISIBLE_IMAGES,
|
|
42
|
+
DEFAULT_PLACEHOLDER_BG,
|
|
43
|
+
DEFAULT_PLACEHOLDER_COLOR,
|
|
44
|
+
DEFAULT_SPACING,
|
|
45
|
+
} from "../constants";
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
createExpoImageRenderer,
|
|
49
|
+
expoImageRenderer,
|
|
50
|
+
} from "./createExpoImageRenderer";
|
|
51
|
+
|
|
52
|
+
type ExpoCollageProps = ImageCollageProps & ExpoImageCollageOptions;
|
|
53
|
+
|
|
54
|
+
function buildExpoCollageProps({
|
|
55
|
+
blurhash,
|
|
56
|
+
prioritizeFirstImage = true,
|
|
57
|
+
renderImage,
|
|
58
|
+
getImagePriority,
|
|
59
|
+
...props
|
|
60
|
+
}: ExpoCollageProps): ImageCollageProps {
|
|
61
|
+
return {
|
|
62
|
+
...props,
|
|
63
|
+
renderImage: renderImage ?? createExpoImageRenderer({ blurhash }),
|
|
64
|
+
getImagePriority:
|
|
65
|
+
getImagePriority ??
|
|
66
|
+
((index: number): ImagePriority =>
|
|
67
|
+
index === 0 && prioritizeFirstImage ? "high" : "normal"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ImageCollage(props: ExpoCollageProps) {
|
|
72
|
+
return <BaseImageCollage {...buildExpoCollageProps(props)} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ImageCollageWithViewer({
|
|
76
|
+
blurhash,
|
|
77
|
+
prioritizeFirstImage,
|
|
78
|
+
renderImage,
|
|
79
|
+
getImagePriority,
|
|
80
|
+
viewerProps,
|
|
81
|
+
renderViewer,
|
|
82
|
+
onImagePress,
|
|
83
|
+
...collageProps
|
|
84
|
+
}: ImageCollageWithViewerProps & ExpoImageCollageOptions) {
|
|
85
|
+
return (
|
|
86
|
+
<BaseImageCollageWithViewer
|
|
87
|
+
{...buildExpoCollageProps({
|
|
88
|
+
...collageProps,
|
|
89
|
+
blurhash,
|
|
90
|
+
prioritizeFirstImage,
|
|
91
|
+
renderImage,
|
|
92
|
+
getImagePriority,
|
|
93
|
+
})}
|
|
94
|
+
viewerProps={viewerProps}
|
|
95
|
+
renderViewer={renderViewer}
|
|
96
|
+
onImagePress={onImagePress}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { useWindowDimensions, type LayoutChangeEvent } from "react-native";
|
|
3
|
+
|
|
4
|
+
export function useContainerWidth({
|
|
5
|
+
width,
|
|
6
|
+
horizontalInset = 0,
|
|
7
|
+
}: {
|
|
8
|
+
width?: number;
|
|
9
|
+
horizontalInset?: number;
|
|
10
|
+
}) {
|
|
11
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
12
|
+
const [measuredWidth, setMeasuredWidth] = useState<number | null>(null);
|
|
13
|
+
|
|
14
|
+
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
15
|
+
const nextWidth = event.nativeEvent.layout.width;
|
|
16
|
+
if (nextWidth > 0) {
|
|
17
|
+
setMeasuredWidth(nextWidth);
|
|
18
|
+
}
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const fallbackWidth = Math.max(0, windowWidth - horizontalInset);
|
|
22
|
+
const containerWidth = width ?? measuredWidth ?? fallbackWidth;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
containerWidth,
|
|
26
|
+
onLayout,
|
|
27
|
+
isMeasured: measuredWidth != null,
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export { ImageCollage } from "./ImageCollage";
|
|
2
|
+
export { CollageWithViewer } from "./CollageWithViewer";
|
|
3
|
+
export { CollageImage, renderCollageImage } from "./CollageImage";
|
|
4
|
+
export { CollageTile } from "./CollageTile";
|
|
5
|
+
|
|
6
|
+
/** @deprecated Use `ImageCollage` instead. Kept for backwards compatibility. */
|
|
7
|
+
export { ImageCollage as ImageCollague } from "./ImageCollage";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
CollageImageInput,
|
|
11
|
+
CollageImageRenderProps,
|
|
12
|
+
CollageImageRenderer,
|
|
13
|
+
CollageViewerRenderProps,
|
|
14
|
+
CollageViewerRenderer,
|
|
15
|
+
CollageWithViewerProps,
|
|
16
|
+
ImageCollageProps,
|
|
17
|
+
ImagePriority,
|
|
18
|
+
NormalizedCollageImage,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
DEFAULT_PLACEHOLDER_COLOR,
|
|
23
|
+
DEFAULT_PLACEHOLDER_BG,
|
|
24
|
+
DEFAULT_SPACING,
|
|
25
|
+
DEFAULT_BORDER_RADIUS,
|
|
26
|
+
DEFAULT_LAYOUT_MIN_HEIGHT,
|
|
27
|
+
DEFAULT_LAYOUT_MAX_HEIGHT,
|
|
28
|
+
DEFAULT_MAX_VISIBLE_IMAGES,
|
|
29
|
+
ANDROID_RIPPLE,
|
|
30
|
+
} from "./constants";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
normalizeImageInput,
|
|
34
|
+
normalizeImages,
|
|
35
|
+
measureImageAspectRatio,
|
|
36
|
+
resolveImagesWithAspectRatios,
|
|
37
|
+
toViewerImages,
|
|
38
|
+
getRemoteUri,
|
|
39
|
+
} from "./utils/imageSources";
|
|
40
|
+
|
|
41
|
+
export { computeLayoutHeight } from "./utils/layoutHeight";
|
|
42
|
+
export { useContainerWidth } from "./hooks/useContainerWidth";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ImageSourcePropType,
|
|
4
|
+
ImageStyle,
|
|
5
|
+
StyleProp,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
|
|
9
|
+
export type ImagePriority = "low" | "normal" | "high";
|
|
10
|
+
|
|
11
|
+
/** Remote URL, RN image source, or object with optional aspect ratio. */
|
|
12
|
+
export type CollageImageInput =
|
|
13
|
+
| string
|
|
14
|
+
| ImageSourcePropType
|
|
15
|
+
| {
|
|
16
|
+
uri: string;
|
|
17
|
+
aspectRatio?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type NormalizedCollageImage = {
|
|
21
|
+
source: ImageSourcePropType;
|
|
22
|
+
aspectRatio?: number;
|
|
23
|
+
cacheKey: string;
|
|
24
|
+
remoteUri?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CollageImageRenderProps = {
|
|
28
|
+
source: ImageSourcePropType;
|
|
29
|
+
remoteUri?: string;
|
|
30
|
+
style?: StyleProp<ImageStyle>;
|
|
31
|
+
priority?: ImagePriority;
|
|
32
|
+
transition: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type CollageImageRenderer = (
|
|
36
|
+
props: CollageImageRenderProps,
|
|
37
|
+
) => ReactElement;
|
|
38
|
+
|
|
39
|
+
export type ImageViewerImage = {
|
|
40
|
+
uri: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type CollageViewerRenderProps = {
|
|
44
|
+
images: ImageViewerImage[];
|
|
45
|
+
visible: boolean;
|
|
46
|
+
imageIndex: number;
|
|
47
|
+
onRequestClose: () => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CollageViewerRenderer = (
|
|
51
|
+
props: CollageViewerRenderProps,
|
|
52
|
+
) => ReactElement | null;
|
|
53
|
+
|
|
54
|
+
export type ImageCollageProps = {
|
|
55
|
+
images: CollageImageInput[] | null | undefined;
|
|
56
|
+
/** Fixed layout height; when omitted, height is derived from container width. */
|
|
57
|
+
height?: number;
|
|
58
|
+
/** Explicit container width; when omitted, width is measured via `onLayout`. */
|
|
59
|
+
width?: number;
|
|
60
|
+
/**
|
|
61
|
+
* @deprecated Prefer automatic `onLayout` sizing. Subtracted from screen width
|
|
62
|
+
* only before the container has been measured.
|
|
63
|
+
*/
|
|
64
|
+
horizontalInset?: number;
|
|
65
|
+
borderRadius?: number;
|
|
66
|
+
spacing?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum tiles to show. When there are more images, the last visible tile
|
|
69
|
+
* displays a `+N` overlay for the remaining count.
|
|
70
|
+
*
|
|
71
|
+
* @example `maxVisibleImages={3}` with 4 images → 3-tile layout, third tile shows `+1`
|
|
72
|
+
* @example `maxVisibleImages={4}` with 5 images → 2×2 grid, fourth tile shows `+1`
|
|
73
|
+
*/
|
|
74
|
+
maxVisibleImages?: number;
|
|
75
|
+
onImagePress?: (index: number) => void;
|
|
76
|
+
layoutMinHeight?: number;
|
|
77
|
+
layoutMaxHeight?: number;
|
|
78
|
+
placeholderColor?: string;
|
|
79
|
+
/** Measure missing aspect ratios before rendering. */
|
|
80
|
+
measureAspectRatios?: boolean;
|
|
81
|
+
/** Optional per-tile loading priority (used by custom renderers such as expo-image). */
|
|
82
|
+
getImagePriority?: (index: number) => ImagePriority;
|
|
83
|
+
/** Custom image renderer. Defaults to React Native `Image`. */
|
|
84
|
+
renderImage?: CollageImageRenderer;
|
|
85
|
+
style?: StyleProp<ViewStyle>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type CollageWithViewerProps = ImageCollageProps & {
|
|
89
|
+
renderViewer: CollageViewerRenderer;
|
|
90
|
+
onImagePress?: (index: number) => void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type ImageViewerProps = {
|
|
94
|
+
images: ImageViewerImage[];
|
|
95
|
+
visible: boolean;
|
|
96
|
+
imageIndex?: number;
|
|
97
|
+
onRequestClose: () => void;
|
|
98
|
+
swipeToCloseEnabled?: boolean;
|
|
99
|
+
doubleTapToZoomEnabled?: boolean;
|
|
100
|
+
presentationStyle?: "fullScreen" | "pageSheet" | "formSheet" | "overFullScreen";
|
|
101
|
+
showCloseButton?: boolean;
|
|
102
|
+
showIndexFooter?: boolean;
|
|
103
|
+
closeButtonLabel?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type ImageCollageWithViewerProps = Omit<ImageCollageProps, "onImagePress"> & {
|
|
107
|
+
viewerProps?: Omit<
|
|
108
|
+
ImageViewerProps,
|
|
109
|
+
"images" | "visible" | "imageIndex" | "onRequestClose"
|
|
110
|
+
>;
|
|
111
|
+
/** Override the built-in `react-native-image-viewing` viewer. */
|
|
112
|
+
renderViewer?: CollageViewerRenderer;
|
|
113
|
+
onImagePress?: (index: number) => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Expo-only collage options (`react-native-image-collage/expo`). */
|
|
117
|
+
export type ExpoImageCollageOptions = {
|
|
118
|
+
blurhash?: string;
|
|
119
|
+
prioritizeFirstImage?: boolean;
|
|
120
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Image, type ImageSourcePropType } from "react-native";
|
|
2
|
+
import type { CollageImageInput, NormalizedCollageImage } from "../types";
|
|
3
|
+
|
|
4
|
+
function cacheKeyForSource(source: ImageSourcePropType): string {
|
|
5
|
+
if (typeof source === "number") {
|
|
6
|
+
return `asset-${source}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof source === "object" && source) {
|
|
10
|
+
if ("uri" in source && source.uri) {
|
|
11
|
+
return source.uri;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if ("testUri" in source && typeof source.testUri === "string") {
|
|
15
|
+
return source.testUri;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return JSON.stringify(source);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getRemoteUri(
|
|
23
|
+
source: ImageSourcePropType,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
if (typeof source === "number") {
|
|
26
|
+
return Image.resolveAssetSource(source)?.uri;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof source === "object" && source && "uri" in source && source.uri) {
|
|
30
|
+
return source.uri;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeImageInput(
|
|
37
|
+
input: CollageImageInput,
|
|
38
|
+
): NormalizedCollageImage | null {
|
|
39
|
+
if (input == null || input === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof input === "string") {
|
|
44
|
+
const source = { uri: input };
|
|
45
|
+
return {
|
|
46
|
+
source,
|
|
47
|
+
aspectRatio: undefined,
|
|
48
|
+
cacheKey: input,
|
|
49
|
+
remoteUri: input,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof input === "number") {
|
|
54
|
+
const source = input;
|
|
55
|
+
const resolved = Image.resolveAssetSource(source);
|
|
56
|
+
const aspectRatio =
|
|
57
|
+
resolved?.width && resolved?.height
|
|
58
|
+
? resolved.width / resolved.height
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
source,
|
|
63
|
+
aspectRatio,
|
|
64
|
+
cacheKey: cacheKeyForSource(source),
|
|
65
|
+
remoteUri: resolved?.uri,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof input === "object" && input != null && "uri" in input) {
|
|
70
|
+
const uri = (input as { uri: unknown }).uri;
|
|
71
|
+
if (typeof uri === "string") {
|
|
72
|
+
const keys = Object.keys(input);
|
|
73
|
+
const isUriDescriptor = keys.every(
|
|
74
|
+
(key) => key === "uri" || key === "aspectRatio",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (isUriDescriptor) {
|
|
78
|
+
const aspectRatio =
|
|
79
|
+
"aspectRatio" in input
|
|
80
|
+
? (input as { aspectRatio?: number }).aspectRatio
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
source: { uri },
|
|
85
|
+
aspectRatio,
|
|
86
|
+
cacheKey: uri,
|
|
87
|
+
remoteUri: uri,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const source = input as ImageSourcePropType;
|
|
94
|
+
const remoteUri = getRemoteUri(source);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
source,
|
|
98
|
+
aspectRatio: undefined,
|
|
99
|
+
cacheKey: cacheKeyForSource(source),
|
|
100
|
+
remoteUri,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function normalizeImages(
|
|
105
|
+
images: CollageImageInput[] | null | undefined,
|
|
106
|
+
): NormalizedCollageImage[] {
|
|
107
|
+
return (images ?? [])
|
|
108
|
+
.map(normalizeImageInput)
|
|
109
|
+
.filter((image): image is NormalizedCollageImage => image != null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function measureImageAspectRatio(
|
|
113
|
+
image: NormalizedCollageImage,
|
|
114
|
+
): Promise<number> {
|
|
115
|
+
if (image.aspectRatio != null) {
|
|
116
|
+
return Promise.resolve(image.aspectRatio);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof image.source === "number") {
|
|
120
|
+
const resolved = Image.resolveAssetSource(image.source);
|
|
121
|
+
if (resolved?.width && resolved?.height) {
|
|
122
|
+
return Promise.resolve(resolved.width / resolved.height);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Promise.reject(new Error("Unable to resolve local image dimensions"));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const uri = getRemoteUri(image.source);
|
|
129
|
+
if (!uri) {
|
|
130
|
+
return Promise.reject(new Error("Unable to measure image without a URI"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
Image.getSize(
|
|
135
|
+
uri,
|
|
136
|
+
(width, height) => resolve(width / height),
|
|
137
|
+
reject,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function resolveImagesWithAspectRatios(
|
|
143
|
+
images: NormalizedCollageImage[],
|
|
144
|
+
): Promise<NormalizedCollageImage[]> {
|
|
145
|
+
return Promise.all(
|
|
146
|
+
images.map(async (image) => {
|
|
147
|
+
if (image.aspectRatio != null) {
|
|
148
|
+
return image;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const aspectRatio = await measureImageAspectRatio(image);
|
|
153
|
+
return { ...image, aspectRatio };
|
|
154
|
+
} catch {
|
|
155
|
+
return image;
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function toViewerImages(
|
|
162
|
+
images: NormalizedCollageImage[],
|
|
163
|
+
): { uri: string }[] {
|
|
164
|
+
return images
|
|
165
|
+
.map((image) => {
|
|
166
|
+
const uri = image.remoteUri ?? getRemoteUri(image.source);
|
|
167
|
+
return uri ? { uri } : null;
|
|
168
|
+
})
|
|
169
|
+
.filter((image): image is { uri: string } => image != null);
|
|
170
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { NormalizedCollageImage } from "../types";
|
|
2
|
+
|
|
3
|
+
function heightRatioForImageCount(count: number) {
|
|
4
|
+
if (count <= 1) return 0.9;
|
|
5
|
+
if (count === 2) return 0.75;
|
|
6
|
+
if (count === 3) return 0.9;
|
|
7
|
+
if (count === 4) return 1.0;
|
|
8
|
+
return 1.05;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function averageAspectRatio(images: NormalizedCollageImage[]): number | undefined {
|
|
12
|
+
const ratios = images
|
|
13
|
+
.map((image) => image.aspectRatio)
|
|
14
|
+
.filter((ratio): ratio is number => ratio != null && ratio > 0);
|
|
15
|
+
|
|
16
|
+
if (!ratios.length) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return ratios.reduce((sum, ratio) => sum + ratio, 0) / ratios.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function computeLayoutHeight({
|
|
24
|
+
contentWidth,
|
|
25
|
+
images,
|
|
26
|
+
height,
|
|
27
|
+
layoutMinHeight,
|
|
28
|
+
layoutMaxHeight,
|
|
29
|
+
}: {
|
|
30
|
+
contentWidth: number;
|
|
31
|
+
images: NormalizedCollageImage[];
|
|
32
|
+
height?: number;
|
|
33
|
+
layoutMinHeight: number;
|
|
34
|
+
layoutMaxHeight: number;
|
|
35
|
+
}): number {
|
|
36
|
+
if (height != null) {
|
|
37
|
+
return Math.max(layoutMinHeight, Math.min(height, layoutMaxHeight));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const count = images.length;
|
|
41
|
+
let ratio = heightRatioForImageCount(count);
|
|
42
|
+
|
|
43
|
+
if (count === 1 && images[0]?.aspectRatio) {
|
|
44
|
+
ratio = 1 / images[0].aspectRatio;
|
|
45
|
+
} else if (count === 2) {
|
|
46
|
+
const averageRatio = averageAspectRatio(images);
|
|
47
|
+
if (averageRatio) {
|
|
48
|
+
ratio = 1 / averageRatio;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const computedHeight = Math.round(contentWidth * ratio);
|
|
53
|
+
return Math.max(layoutMinHeight, Math.min(computedHeight, layoutMaxHeight));
|
|
54
|
+
}
|