react-native-fast-image 9.2.2 → 9.3.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/dist/index.cjs.js CHANGED
@@ -46,30 +46,61 @@ const cacheControl = {
46
46
  cacheOnly: "cacheOnly"
47
47
  };
48
48
  //#endregion
49
- //#region src/index.tsx
50
- const resolveDefaultSource = (defaultSource) => {
51
- if (!defaultSource) return null;
52
- if (react_native.Platform.OS === "android") {
53
- const resolved = react_native.Image.resolveAssetSource(defaultSource);
54
- if (resolved) return resolved.uri;
55
- return null;
56
- }
57
- return defaultSource;
58
- };
59
- function tintColorFromStyle(style) {
49
+ //#region src/objectFit.ts
50
+ function fromStyle(style, key) {
60
51
  if (Array.isArray(style)) {
61
52
  for (let i = style.length - 1; i >= 0; i--) {
62
- const found = tintColorFromStyle(style[i]);
53
+ const found = fromStyle(style[i], key);
63
54
  if (found !== void 0) return found;
64
55
  }
65
56
  return;
66
57
  }
67
58
  if (typeof style === "number") {
68
59
  const flattened = react_native.StyleSheet.flatten(style);
69
- return flattened ? flattened.tintColor : void 0;
60
+ return flattened ? flattened[key] : void 0;
70
61
  }
71
- return style && typeof style === "object" ? style.tintColor : void 0;
62
+ return style && typeof style === "object" ? style[key] : void 0;
63
+ }
64
+ const RESIZE_MODE_FIT = {
65
+ contain: "contain",
66
+ cover: "cover",
67
+ stretch: "fill",
68
+ center: "center",
69
+ repeat: "repeat"
70
+ };
71
+ const FITS = {
72
+ fill: true,
73
+ contain: true,
74
+ cover: true,
75
+ none: true,
76
+ "scale-down": true
77
+ };
78
+ function resolveObjectFit(objectFit, style, resizeMode) {
79
+ var _RESIZE_MODE_FIT$resi;
80
+ const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
81
+ if (fit && FITS[fit] === true) return fit;
82
+ return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
72
83
  }
84
+ //#endregion
85
+ //#region src/index.tsx
86
+ const resolveDefaultSource = (defaultSource) => {
87
+ if (!defaultSource) return null;
88
+ if (react_native.Platform.OS === "android") {
89
+ const resolved = react_native.Image.resolveAssetSource(defaultSource);
90
+ if (resolved) return resolved.uri;
91
+ return null;
92
+ }
93
+ return defaultSource;
94
+ };
95
+ const NATIVE_RESIZE_MODE = {
96
+ fill: "stretch",
97
+ contain: "contain",
98
+ cover: "cover",
99
+ none: "none",
100
+ "scale-down": "scale-down",
101
+ center: "center",
102
+ repeat: "repeat"
103
+ };
73
104
  function loopCount(loop) {
74
105
  if (loop === void 0) return -1;
75
106
  if (loop === true) return 0;
@@ -111,7 +142,7 @@ function withoutCache(source) {
111
142
  const { cache: _cache, ...rest } = source || {};
112
143
  return rest;
113
144
  }
114
- function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
145
+ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, fallback, children, objectFit, resizeMode = "cover", loop, transition, forwardedRef, onLayout, pointerEvents, hitSlop, onStartShouldSetResponder, onStartShouldSetResponderCapture, onMoveShouldSetResponder, onMoveShouldSetResponderCapture, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminationRequest, onResponderTerminate, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, onTouchEndCapture, ...viewProps }) {
115
146
  const { onClick, ...props } = viewProps;
116
147
  const fallbackResult = (0, react.useRef)(void 0);
117
148
  const wrapperProps = {
@@ -141,7 +172,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
141
172
  ...props,
142
173
  pointerEvents: pointerEvents === "box-none" ? "none" : void 0
143
174
  };
144
- const resolvedTintColor = tintColor != null ? tintColor : tintColorFromStyle(style);
175
+ const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
176
+ const mode = NATIVE_RESIZE_MODE[resolveObjectFit(objectFit, style, resizeMode)];
145
177
  if (fallback) {
146
178
  const cleanedSource = typeof source === "number" ? source : Array.isArray(source) ? source.map(withoutCache) : withoutCache(source);
147
179
  const resolvedSource = react_native.Image.resolveAssetSource(cleanedSource);
@@ -179,7 +211,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
179
211
  error: "Failed to load the image"
180
212
  });
181
213
  }),
182
- resizeMode
214
+ resizeMode: mode === "none" || mode === "scale-down" ? "center" : mode
183
215
  }), children);
184
216
  }
185
217
  const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
@@ -205,7 +237,7 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
205
237
  onFastImageLoad: onLoad,
206
238
  onFastImageError: onError,
207
239
  onFastImageLoadEnd: onLoadEnd && ((event) => onLoadEnd(loadResult(event.nativeEvent))),
208
- resizeMode
240
+ resizeMode: mode
209
241
  }), children);
210
242
  }
211
243
  const FastImageMemo = (0, react.memo)(FastImageBase);
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
49
49
  |}>
50
50
 
51
51
  export type ResizeModes = $Values<ResizeMode>
52
+ export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
52
53
  export type Priorities = $Values<Priority>
53
54
  export type CacheControls = $Values<CacheControl>
54
55
 
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
123
124
  defaultSource?: ?number,
124
125
 
125
126
  tintColor?: number | string,
127
+ objectFit?: ?ObjectFit,
128
+ // Deprecated: objectFit, except for 'repeat'.
126
129
  resizeMode?: ?ResizeModes,
127
130
  fallback?: ?boolean,
128
131
  recyclingKey?: ?string,
@@ -27,6 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  let react = require("react");
28
28
  react = __toESM(react);
29
29
  let react_native = require("react-native");
30
+ let react_native_web_dist_modules_AssetRegistry = require("react-native-web/dist/modules/AssetRegistry");
30
31
  //#region src/constants.ts
31
32
  const resizeMode = {
32
33
  contain: "contain",
@@ -46,142 +47,279 @@ const cacheControl = {
46
47
  cacheOnly: "cacheOnly"
47
48
  };
48
49
  //#endregion
50
+ //#region src/objectFit.ts
51
+ function fromStyle(style, key) {
52
+ if (Array.isArray(style)) {
53
+ for (let i = style.length - 1; i >= 0; i--) {
54
+ const found = fromStyle(style[i], key);
55
+ if (found !== void 0) return found;
56
+ }
57
+ return;
58
+ }
59
+ if (typeof style === "number") {
60
+ const flattened = react_native.StyleSheet.flatten(style);
61
+ return flattened ? flattened[key] : void 0;
62
+ }
63
+ return style && typeof style === "object" ? style[key] : void 0;
64
+ }
65
+ const RESIZE_MODE_FIT = {
66
+ contain: "contain",
67
+ cover: "cover",
68
+ stretch: "fill",
69
+ center: "center",
70
+ repeat: "repeat"
71
+ };
72
+ const FITS = {
73
+ fill: true,
74
+ contain: true,
75
+ cover: true,
76
+ none: true,
77
+ "scale-down": true
78
+ };
79
+ function resolveObjectFit(objectFit, style, resizeMode) {
80
+ var _RESIZE_MODE_FIT$resi;
81
+ const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
82
+ if (fit && FITS[fit] === true) return fit;
83
+ return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
84
+ }
85
+ //#endregion
49
86
  //#region src/index.web.tsx
87
+ var _useId;
50
88
  const notSupported = {
51
89
  ok: false,
52
90
  error: "Not supported on the web"
53
91
  };
54
- function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
55
- const result = (0, react.useRef)(void 0);
56
- const sent = (0, react.useRef)(Promise.resolve());
57
- const image = (0, react.useRef)(null);
58
- const sizes = Array.isArray(source) && source.length > 1 ? source : void 0;
59
- const single = Array.isArray(source) ? source[0] : source;
60
- const uri = typeof single === "object" && single ? single.uri : void 0;
61
- const webSource = typeof single === "number" ? single : uri ? { uri } : void 0;
92
+ let ids = 0;
93
+ const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
94
+ const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
95
+ const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
96
+ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
97
+ const { accessibilityLabel, "aria-label": ariaLabel, ...viewProps } = props;
98
+ const label = ariaLabel || accessibilityLabel || void 0;
99
+ const several = Array.isArray(source) && source.length > 1 ? source : void 0;
100
+ const sized = several ? sizedSources(several) : void 0;
101
+ const src = sized ? sized.src : resolveUri(source);
102
+ const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
103
+ const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
104
+ const defaultUri = resolveUri(defaultSource);
105
+ const [result, setResult] = (0, react.useState)();
106
+ const ok = result && result.key === key ? result.ok : void 0;
107
+ const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
108
+ const resolved = resolveObjectFit(objectFit, style, mode);
109
+ const fit = resolved === "center" ? "scale-down" : several && resolved === "repeat" ? "cover" : resolved;
110
+ const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
111
+ const showDefault = !!defaultUri && ok !== true;
112
+ useClientLayoutEffect(() => {
113
+ if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
114
+ }, [key]);
62
115
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
116
+ ...viewProps,
117
+ "aria-label": label,
63
118
  style: [styles.container, style],
64
119
  onLayout,
65
120
  pointerEvents,
66
121
  ref: forwardedRef
67
- }, sizes ? /* @__PURE__ */ react.default.createElement(react_native.View, {
68
- ...props,
69
- style: styles.image
70
- }, /* @__PURE__ */ react.default.createElement(ImageSizes, {
71
- sources: sizes,
72
- resizeMode: mode,
73
- blurRadius,
74
- onLoadStart,
75
- onLoad,
76
- onError,
77
- onLoadEnd
78
- })) : /* @__PURE__ */ react.default.createElement(react_native.Image, {
79
- ...props,
80
- style: styles.image,
81
- source: webSource,
82
- defaultSource,
83
- resizeMode: mode,
84
- tintColor,
85
- blurRadius,
86
- ref: image,
87
- onLoadStart,
88
- onLoad: (event) => {
89
- var _event$nativeEvent;
90
- sent.current = loadedSize(event === null || event === void 0 || (_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.target, image.current, uri).then(({ width, height }) => {
91
- result.current = {
92
- ok: true,
93
- width,
94
- height
95
- };
96
- onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
97
- width,
98
- height
99
- } });
122
+ }, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
123
+ key: `default ${defaultUri}`,
124
+ src: defaultUri,
125
+ fit,
126
+ filter,
127
+ alt: key ? "" : label !== null && label !== void 0 ? label : ""
128
+ }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
129
+ key: several ? "sizes" : key,
130
+ src,
131
+ srcSet,
132
+ lazy: !!several,
133
+ fit,
134
+ filter,
135
+ hidden: ok === false,
136
+ alt: label !== null && label !== void 0 ? label : "",
137
+ onLoad: (image) => {
138
+ const { width, height } = loadedSize(image, sized);
139
+ setResult({
140
+ key,
141
+ ok: true
142
+ });
143
+ onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
144
+ width,
145
+ height
146
+ } });
147
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
148
+ ok: true,
149
+ width,
150
+ height
100
151
  });
101
152
  },
102
- onError: (event) => {
103
- var _event$nativeEvent$er, _event$nativeEvent2;
104
- const error = String((_event$nativeEvent$er = event === null || event === void 0 || (_event$nativeEvent2 = event.nativeEvent) === null || _event$nativeEvent2 === void 0 ? void 0 : _event$nativeEvent2.error) !== null && _event$nativeEvent$er !== void 0 ? _event$nativeEvent$er : "Failed to load the image");
105
- result.current = {
153
+ onError: (uri) => {
154
+ const error = `Failed to load resource ${uri}`;
155
+ setResult({
156
+ key,
157
+ ok: false
158
+ });
159
+ onError === null || onError === void 0 || onError({ nativeEvent: { error } });
160
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
106
161
  ok: false,
107
162
  error
108
- };
109
- sent.current = Promise.resolve();
110
- onError === null || onError === void 0 || onError({ nativeEvent: { error } });
111
- },
112
- onLoadEnd: () => {
113
- sent.current.then(() => {
114
- var _result$current;
115
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd((_result$current = result.current) !== null && _result$current !== void 0 ? _result$current : {
116
- ok: false,
117
- error: "Failed to load the image"
118
- });
119
- result.current = void 0;
120
163
  });
121
164
  }
122
- }), children);
165
+ }) : null, tintColor ? /* @__PURE__ */ react.default.createElement(TintFilter, {
166
+ id: tintId,
167
+ color: tintColor
168
+ }) : null, children);
169
+ }
170
+ function resolveUri(source) {
171
+ let uri;
172
+ if (typeof source === "number") {
173
+ var _asset$scales$;
174
+ const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
175
+ if (!asset) return void 0;
176
+ const scale = asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
177
+ const suffix = scale !== 1 ? `@${scale}x` : "";
178
+ uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
179
+ } else if (typeof source === "string") uri = source;
180
+ else if (Array.isArray(source)) return resolveUri(source[0]);
181
+ else if (source && typeof source.uri === "string") uri = source.uri;
182
+ const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
183
+ return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
123
184
  }
124
- function ImageSizes({ sources, resizeMode: mode, blurRadius, onLoadStart, onLoad, onError, onLoadEnd }) {
185
+ function sizedSources(sources) {
125
186
  var _largest$uri, _sources$find;
126
- const sized = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
187
+ const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
127
188
  var _source$width, _source$scale;
128
189
  return {
129
190
  uri: source.uri,
130
191
  width: Math.round(((_source$width = source.width) !== null && _source$width !== void 0 ? _source$width : 0) * ((_source$scale = source.scale) !== null && _source$scale !== void 0 ? _source$scale : 1))
131
192
  };
132
193
  });
133
- const largest = sized.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
134
- const src = (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri;
135
- const srcSet = sized.length > 0 ? sized.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0;
194
+ const largest = widths.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
195
+ return {
196
+ src: (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri,
197
+ srcSet: widths.length > 0 ? widths.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0,
198
+ widths
199
+ };
200
+ }
201
+ function loadedSize(image, sized) {
202
+ var _picked$width;
203
+ const picked = (sized === null || sized === void 0 ? void 0 : sized.srcSet) ? sized.widths.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
204
+ return {
205
+ width: (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth,
206
+ height: picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight
207
+ };
208
+ }
209
+ const fill = {
210
+ position: "absolute",
211
+ top: 0,
212
+ left: 0,
213
+ width: "100%",
214
+ height: "100%"
215
+ };
216
+ function Picture({ src, srcSet, lazy, fit, filter, hidden, alt, onLoad, onError }) {
217
+ const repeat = fit === "repeat";
218
+ const image = (0, react.useRef)(null);
219
+ const tiles = (0, react.useRef)(null);
220
+ const mounted = (0, react.useRef)(true);
221
+ const handled = (0, react.useRef)(void 0);
222
+ const [natural, setNatural] = (0, react.useState)();
223
+ const [box, setBox] = (0, react.useState)();
224
+ const loaded = (target) => {
225
+ var _handled$current, _target$decode;
226
+ const file = target.currentSrc || src;
227
+ const sources = srcSet !== null && srcSet !== void 0 ? srcSet : src;
228
+ if (!mounted.current || ((_handled$current = handled.current) === null || _handled$current === void 0 ? void 0 : _handled$current.sources) === sources && handled.current.file === file) return;
229
+ handled.current = {
230
+ sources,
231
+ file
232
+ };
233
+ setNatural({
234
+ width: target.naturalWidth,
235
+ height: target.naturalHeight
236
+ });
237
+ const done = () => {
238
+ if (mounted.current) onLoad === null || onLoad === void 0 || onLoad(target);
239
+ };
240
+ Promise.resolve((_target$decode = target.decode) === null || _target$decode === void 0 ? void 0 : _target$decode.call(target)).then(done, done);
241
+ };
136
242
  (0, react.useEffect)(() => {
137
- onLoadStart === null || onLoadStart === void 0 || onLoadStart();
138
- }, [src, srcSet]);
139
- return /* @__PURE__ */ react.default.createElement("img", {
243
+ mounted.current = true;
244
+ return () => {
245
+ mounted.current = false;
246
+ };
247
+ }, []);
248
+ useClientLayoutEffect(() => {
249
+ var _target$decode2;
250
+ const target = image.current;
251
+ if (!(target === null || target === void 0 ? void 0 : target.complete)) return;
252
+ if (target.naturalWidth > 0) return loaded(target);
253
+ Promise.resolve((_target$decode2 = target.decode) === null || _target$decode2 === void 0 ? void 0 : _target$decode2.call(target)).then(() => loaded(target), () => {
254
+ if (mounted.current) onError === null || onError === void 0 || onError(target.currentSrc || src);
255
+ });
256
+ }, []);
257
+ useClientLayoutEffect(() => {
258
+ const Observer = globalThis.ResizeObserver;
259
+ const node = tiles.current;
260
+ if (!repeat || !node || !Observer) return;
261
+ const observer = new Observer((entries) => {
262
+ const { width, height } = entries[0].contentRect;
263
+ setBox({
264
+ width,
265
+ height
266
+ });
267
+ });
268
+ observer.observe(node);
269
+ return () => observer.disconnect();
270
+ }, [repeat]);
271
+ const img = /* @__PURE__ */ react.default.createElement("img", {
272
+ ref: image,
140
273
  src,
141
274
  srcSet,
142
275
  sizes: srcSet ? "auto, 100vw" : void 0,
143
- loading: "lazy",
144
- alt: "",
276
+ loading: lazy ? "lazy" : void 0,
277
+ alt,
145
278
  draggable: false,
146
279
  style: {
147
- position: "absolute",
148
- width: "100%",
149
- height: "100%",
150
- objectFit: OBJECT_FIT[mode !== null && mode !== void 0 ? mode : "cover"],
151
- filter: blurRadius ? `blur(${blurRadius}px)` : void 0
280
+ ...fill,
281
+ objectFit: repeat ? "cover" : fit,
282
+ filter: repeat ? void 0 : filter,
283
+ opacity: repeat || hidden ? 0 : void 0
152
284
  },
153
- onLoad: (event) => {
154
- var _picked$width;
155
- const image = event.currentTarget;
156
- const picked = srcSet ? sized.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
157
- const loadedWidth = (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth;
158
- const loadedHeight = picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight;
159
- onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
160
- width: loadedWidth,
161
- height: loadedHeight
162
- } });
163
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
164
- ok: true,
165
- width: loadedWidth,
166
- height: loadedHeight
167
- });
168
- },
169
- onError: () => {
170
- const error = "Failed to load the image";
171
- onError === null || onError === void 0 || onError({ nativeEvent: { error } });
172
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
173
- ok: false,
174
- error
175
- });
285
+ onLoad: (event) => loaded(event.currentTarget),
286
+ onError: (event) => {
287
+ const target = event.currentTarget;
288
+ if (mounted.current) onError === null || onError === void 0 || onError((target === null || target === void 0 ? void 0 : target.currentSrc) || src);
176
289
  }
177
290
  });
291
+ return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, repeat ? /* @__PURE__ */ react.default.createElement("div", {
292
+ ref: tiles,
293
+ style: {
294
+ ...fill,
295
+ backgroundImage: `url("${src}")`,
296
+ backgroundRepeat: "repeat",
297
+ backgroundPosition: "0",
298
+ backgroundSize: tileSize(natural, box),
299
+ filter,
300
+ opacity: hidden ? 0 : void 0
301
+ }
302
+ }) : null, img);
178
303
  }
179
- const OBJECT_FIT = {
180
- cover: "cover",
181
- contain: "contain",
182
- stretch: "fill",
183
- center: "scale-down",
184
- repeat: "cover"
304
+ function tileSize(natural, box) {
305
+ if (!(natural === null || natural === void 0 ? void 0 : natural.width) || !natural.height || !(box === null || box === void 0 ? void 0 : box.width) || !box.height) return;
306
+ const scale = Math.min(1, box.width / natural.width, box.height / natural.height);
307
+ return `${Math.ceil(scale * natural.width)}px ${Math.ceil(scale * natural.height)}px`;
308
+ }
309
+ function TintFilter({ id, color }) {
310
+ return /* @__PURE__ */ react.default.createElement("svg", { style: tintStyle }, /* @__PURE__ */ react.default.createElement("defs", null, /* @__PURE__ */ react.default.createElement("filter", { id }, /* @__PURE__ */ react.default.createElement("feFlood", {
311
+ floodColor: String(color),
312
+ key: String(color)
313
+ }), /* @__PURE__ */ react.default.createElement("feComposite", {
314
+ in2: "SourceAlpha",
315
+ operator: "in"
316
+ }))));
317
+ }
318
+ const tintStyle = {
319
+ position: "absolute",
320
+ width: 0,
321
+ height: 0,
322
+ visibility: "hidden"
185
323
  };
186
324
  const FastImageMemo = (0, react.memo)(FastImageBase);
187
325
  const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
@@ -189,24 +327,6 @@ const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */
189
327
  ...props
190
328
  }));
191
329
  FastImageComponent.displayName = "FastImage";
192
- async function loadedSize(loaded, view, uri) {
193
- var _view$querySelector;
194
- if (loaded === null || loaded === void 0 ? void 0 : loaded.naturalWidth) return {
195
- width: loaded.naturalWidth,
196
- height: loaded.naturalHeight
197
- };
198
- await new Promise((resolve) => setTimeout(resolve, 0));
199
- const shown = view === null || view === void 0 || (_view$querySelector = view.querySelector) === null || _view$querySelector === void 0 ? void 0 : _view$querySelector.call(view, "img");
200
- if (shown === null || shown === void 0 ? void 0 : shown.naturalWidth) return {
201
- width: shown.naturalWidth,
202
- height: shown.naturalHeight
203
- };
204
- const src = uri !== null && uri !== void 0 ? uri : shown === null || shown === void 0 ? void 0 : shown.src;
205
- return src ? getSize(src) : {
206
- width: 0,
207
- height: 0
208
- };
209
- }
210
330
  function getSize(uri) {
211
331
  return new Promise((resolve) => {
212
332
  react_native.Image.getSize(uri, (width, height) => resolve({
@@ -244,16 +364,7 @@ FastImage.clearDiskCache = () => Promise.resolve(notSupported);
244
364
  FastImage.configureCache = () => Promise.resolve({});
245
365
  FastImage.writeToCache = () => Promise.resolve(notSupported);
246
366
  FastImage.getCachePath = () => Promise.resolve(notSupported);
247
- const styles = react_native.StyleSheet.create({
248
- container: { overflow: "hidden" },
249
- image: {
250
- position: "absolute",
251
- top: 0,
252
- left: 0,
253
- width: "100%",
254
- height: "100%"
255
- }
256
- });
367
+ const styles = react_native.StyleSheet.create({ container: { overflow: "hidden" } });
257
368
  const FastImageWithRef = FastImageComponent;
258
369
  const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
259
370
  style,
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
49
49
  |}>
50
50
 
51
51
  export type ResizeModes = $Values<ResizeMode>
52
+ export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
52
53
  export type Priorities = $Values<Priority>
53
54
  export type CacheControls = $Values<CacheControl>
54
55
 
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
123
124
  defaultSource?: ?number,
124
125
 
125
126
  tintColor?: number | string,
127
+ objectFit?: ?ObjectFit,
128
+ // Deprecated: objectFit, except for 'repeat'.
126
129
  resizeMode?: ?ResizeModes,
127
130
  fallback?: ?boolean,
128
131
  recyclingKey?: ?string,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  import { ColorValue, View, StyleProp, ViewStyle, ImageRequireSource, AccessibilityProps, ViewProps } from 'react-native';
3
3
  import { cacheControl, priority, resizeMode } from './constants';
4
+ /** How the image fits the view: see [`objectFit`](#objectfit). */
5
+ export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
4
6
  /** How the image fits the view: see [`resizeMode`](#resizemode). */
5
7
  export type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center' | 'repeat';
6
8
  /** An image's load priority: see [`source.priority`](#sourcepriority). */
@@ -234,6 +236,7 @@ export interface OnProgressEvent {
234
236
  export interface ImageStyle extends ViewStyle {
235
237
  overlayColor?: ColorValue;
236
238
  tintColor?: ColorValue;
239
+ objectFit?: ObjectFit;
237
240
  }
238
241
  export interface FastImageProps extends AccessibilityProps, ViewProps {
239
242
  /**
@@ -287,22 +290,47 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
287
290
  */
288
291
  defaultSource?: ImageRequireSource;
289
292
  /**
290
- * How the image fills the view.
293
+ * How the image fits the view, as CSS's `object-fit` does.
291
294
  *
292
- * - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of
293
- * it fits in the view (minus padding).
294
295
  * - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers
295
296
  * the view (minus padding), cropping what doesn't fit.
296
- * - `'stretch'`: scales its width and height separately to fill the view,
297
+ * - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of
298
+ * it fits in the view (minus padding).
299
+ * - `'fill'`: scales its width and height separately to fill the view,
297
300
  * which can change its aspect ratio.
298
- * - `'center'`: centers it at its own size, scaled down uniformly to fit
299
- * if it's larger than the view.
301
+ * - `'none'`: shows it at its own size, centered, cropped if it's larger
302
+ * than the view.
303
+ * - `'scale-down'`: shows it at its own size, centered, or scaled down
304
+ * uniformly to fit if it's larger than the view (the smaller of `'none'`
305
+ * and `'contain'`).
306
+ *
307
+ * An image's own size is its size in pixels, as points (dp on Android),
308
+ * as CSS counts an image's pixels: a 300 × 200 image is 300 × 200 points.
309
+ * A bundled image (`require()`) is its size in points.
310
+ *
311
+ * It can also be set in `style`; the prop wins. Either one overrides
312
+ * `resizeMode`.
313
+ *
314
+ * @default 'cover'
315
+ */
316
+ objectFit?: ObjectFit;
317
+ /**
318
+ * How the image fills the view.
319
+ *
320
+ * - `'contain'`: as `objectFit="contain"`.
321
+ * - `'cover'`: as `objectFit="cover"`.
322
+ * - `'stretch'`: as `objectFit="fill"`.
323
+ * - `'center'`: as `objectFit="scale-down"`, except that on Android an
324
+ * image smaller than the view is shown at its size in pixels on the
325
+ * screen, so smaller than on iOS and the web.
300
326
  * - `'repeat'`: repeats it to cover the view, from its top-left corner, at
301
327
  * the image's own size in pixels (a bundled image at its size in
302
328
  * points), scaled down to fit if it's larger than the view. An animated
303
329
  * image repeats its first frame, and `defaultSource` repeats too.
304
330
  *
305
331
  * @default 'cover'
332
+ * @deprecated Use `objectFit` instead, except for `'repeat'`, which has no
333
+ * `objectFit` value and isn't deprecated.
306
334
  */
307
335
  resizeMode?: ResizeMode;
308
336
  /**
@@ -539,6 +567,10 @@ export interface PreloadFailure {
539
567
  */
540
568
  export type PreloadResult = PreloadSuccess | PreloadFailure;
541
569
  export interface FastImageStaticProperties {
570
+ /**
571
+ * @deprecated Use the `objectFit` prop instead of `resizeMode`, except for
572
+ * `'repeat'`.
573
+ */
542
574
  resizeMode: typeof resizeMode;
543
575
  priority: typeof priority;
544
576
  cacheControl: typeof cacheControl;