react-native-fast-image 10.1.1 → 10.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/README.md +107 -14
- package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
- package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
- package/dist/index.cjs.js +47 -9
- package/dist/index.cjs.js.flow +11 -0
- package/dist/index.cjs.web.js +67 -14
- package/dist/index.cjs.web.js.flow +11 -0
- package/dist/index.d.ts +77 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +11 -0
- package/dist/index.web.js +68 -15
- package/dist/index.web.js.flow +11 -0
- package/dist/placeholders.d.ts +4 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
- package/dist/thumbhash.cjs.js +241 -0
- package/dist/thumbhash.d.ts +12 -0
- package/dist/thumbhash.js +240 -0
- package/ios/FastImage/FFFBoxBlur.h +7 -0
- package/ios/FastImage/FFFBoxBlur.m +29 -0
- package/ios/FastImage/FFFThumbHash.h +24 -0
- package/ios/FastImage/FFFThumbHash.m +269 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +93 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +11 -0
- package/src/index.tsx +166 -31
- package/src/index.web.tsx +166 -33
- package/src/placeholders.ts +45 -0
- package/src/specs/FastImageViewNativeComponent.ts +8 -2
- package/src/thumbhash.ts +363 -0
- package/thumbhash/package.json +8 -0
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, memo } from 'react'
|
|
1
|
+
import React, { forwardRef, memo, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ColorValue,
|
|
4
4
|
View,
|
|
@@ -16,6 +16,7 @@ import { fromStyle, resolveObjectFit } from './objectFit'
|
|
|
16
16
|
import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
|
|
17
17
|
import FastImageView from './specs/FastImageViewNativeComponent'
|
|
18
18
|
import NativeFastImageModule from './specs/NativeFastImageModule'
|
|
19
|
+
import { dataUri, hasSourcePlaceholder, isImageName } from './placeholders'
|
|
19
20
|
import { warnIfChildren, warnOnce } from './warnings'
|
|
20
21
|
|
|
21
22
|
/** How the image fits the view: see [`objectFit`](#objectfit). */
|
|
@@ -53,6 +54,8 @@ export type Source = {
|
|
|
53
54
|
* - `'normal'`: the default.
|
|
54
55
|
* - `'high'`: e.g. the image the screen is about.
|
|
55
56
|
*
|
|
57
|
+
* On the web, it's the image's `fetchpriority`.
|
|
58
|
+
*
|
|
56
59
|
* @default 'normal'
|
|
57
60
|
*/
|
|
58
61
|
priority?: Priority
|
|
@@ -129,6 +132,32 @@ export type Source = {
|
|
|
129
132
|
* @default true
|
|
130
133
|
*/
|
|
131
134
|
diskCache?: boolean
|
|
135
|
+
/**
|
|
136
|
+
* A [ThumbHash](https://evanw.github.io/thumbhash/) of the image, shown
|
|
137
|
+
* blurred while the image loads, and if it fails to load. It's a short
|
|
138
|
+
* string you make when the image is uploaded (e.g. with the `thumbhash`
|
|
139
|
+
* package) and send with the image's url.
|
|
140
|
+
*
|
|
141
|
+
* On the web, it needs `import 'react-native-fast-image/thumbhash'` (see
|
|
142
|
+
* [ThumbHashes on the web](#thumbhashes-on-the-web)).
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```jsx
|
|
146
|
+
* <FastImage source={{ uri: photo.url, thumbhash: photo.thumbhash }} />
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
thumbhash?: string
|
|
150
|
+
/**
|
|
151
|
+
* A tiny version of the image (e.g. 8 to 20 px) as a data uri, shown
|
|
152
|
+
* blurred while the image loads, and if it fails to load. On the web it
|
|
153
|
+
* shows without any JavaScript, e.g. in a server-rendered page.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```jsx
|
|
157
|
+
* <FastImage source={{ uri: photo.url, blurDataUri: photo.lqip }} />
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
blurDataUri?: string
|
|
132
161
|
/**
|
|
133
162
|
* With several sources (`source` as an array), the image's width at this
|
|
134
163
|
* uri, in pixels (times `scale`, if given).
|
|
@@ -143,6 +172,9 @@ export type Source = {
|
|
|
143
172
|
scale?: number
|
|
144
173
|
}
|
|
145
174
|
|
|
175
|
+
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
176
|
+
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
|
|
177
|
+
|
|
146
178
|
/** How the image fades in: see [`transition`](#transition). */
|
|
147
179
|
export type Transition = {
|
|
148
180
|
/**
|
|
@@ -154,7 +186,7 @@ export type Transition = {
|
|
|
154
186
|
/**
|
|
155
187
|
* Also fades between images: a new `source` replacing an image that's
|
|
156
188
|
* showing cross-dissolves from it. Otherwise only an image that appears
|
|
157
|
-
* over nothing (or over
|
|
189
|
+
* over nothing (or over a placeholder) fades in, and a new source
|
|
158
190
|
* replaces the image showing at once, once it has loaded.
|
|
159
191
|
* @default false
|
|
160
192
|
*/
|
|
@@ -323,14 +355,29 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
323
355
|
*/
|
|
324
356
|
source?: Source | Source[] | ImageRequireSource
|
|
325
357
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* `
|
|
358
|
+
* A placeholder shown while the image loads, and if it fails to load:
|
|
359
|
+
*
|
|
360
|
+
* - An image loaded with `require()`: `placeholder={require('./gray.png')}`.
|
|
361
|
+
* - A data uri: `placeholder="data:image/png;base64,…"`.
|
|
362
|
+
* - The name of an image bundled with the app (an iOS asset catalog image
|
|
363
|
+
* or an Android drawable): `placeholder="gray"`.
|
|
364
|
+
* - A [ThumbHash](#sourcethumbhash), shown blurred:
|
|
365
|
+
* `placeholder={{ thumbhash }}`.
|
|
366
|
+
* - A tiny image as a data uri, shown blurred:
|
|
367
|
+
* `placeholder={{ blurDataUri }}`.
|
|
368
|
+
*
|
|
369
|
+
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
370
|
+
* one.
|
|
330
371
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
372
|
+
* Notes:
|
|
373
|
+
*
|
|
374
|
+
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
375
|
+
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
376
|
+
* the dev server serves it. Release builds show it.
|
|
377
|
+
*/
|
|
378
|
+
placeholder?: ImageRequireSource | string | SourcePlaceholder
|
|
379
|
+
/**
|
|
380
|
+
* @deprecated Use `placeholder`, which does the same.
|
|
334
381
|
*/
|
|
335
382
|
defaultSource?: ImageRequireSource
|
|
336
383
|
/**
|
|
@@ -373,8 +420,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
373
420
|
* `{ right: 10, bottom: '20%' }`. A missing axis is centered.
|
|
374
421
|
*
|
|
375
422
|
* Points are dp on Android. An invalid value is ignored (the image is
|
|
376
|
-
* centered), with a warning in development.
|
|
377
|
-
*
|
|
423
|
+
* centered), with a warning in development. Placeholders are placed the
|
|
424
|
+
* same way.
|
|
378
425
|
*
|
|
379
426
|
* @default '50% 50%'
|
|
380
427
|
* @example
|
|
@@ -387,7 +434,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
387
434
|
* For views that get reused for other content, such as rows in FlashList
|
|
388
435
|
* or recyclerlistview. Set it to something that identifies the content,
|
|
389
436
|
* e.g. the item's id. When it changes, the image is cleared right away (to
|
|
390
|
-
* `
|
|
437
|
+
* `placeholder`, or blank) instead of staying until the new one has
|
|
391
438
|
* loaded. Unlike changing `key`, this keeps the view, which is what list
|
|
392
439
|
* recycling is for.
|
|
393
440
|
*
|
|
@@ -429,6 +476,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
429
476
|
* @default 'auto'
|
|
430
477
|
*/
|
|
431
478
|
imageRendering?: 'auto' | 'smooth' | 'pixelated'
|
|
479
|
+
/**
|
|
480
|
+
* Whether the image loads as it nears the screen (`'lazy'`) or at once
|
|
481
|
+
* (`'eager'`).
|
|
482
|
+
*
|
|
483
|
+
* @default 'lazy'
|
|
484
|
+
* @platform web
|
|
485
|
+
*/
|
|
486
|
+
loading?: 'lazy' | 'eager'
|
|
487
|
+
/**
|
|
488
|
+
* With several sources, the width the image is shown at, as the image's
|
|
489
|
+
* [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes),
|
|
490
|
+
* so the browser loads the size that fits. The default is the width the
|
|
491
|
+
* image is laid out at, which needs `loading="lazy"` (otherwise, or in
|
|
492
|
+
* browsers without it, the window's width).
|
|
493
|
+
*
|
|
494
|
+
* @default 'auto, 100vw'
|
|
495
|
+
* @platform web
|
|
496
|
+
*/
|
|
497
|
+
sizes?: string
|
|
432
498
|
/**
|
|
433
499
|
* Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
|
|
434
500
|
* the frame it's showing; `false` plays it again from there
|
|
@@ -499,7 +565,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
499
565
|
* blurring a photo behind a sheet). Each change blurs the image again on
|
|
500
566
|
* the CPU, so don't animate it.
|
|
501
567
|
*
|
|
502
|
-
* - Only the loaded image is blurred, not
|
|
568
|
+
* - Only the loaded image is blurred, not a placeholder.
|
|
503
569
|
* - An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame,
|
|
504
570
|
* blurred, and doesn't animate.
|
|
505
571
|
* - With `tintColor`, the blurred image is tinted.
|
|
@@ -588,7 +654,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
588
654
|
|
|
589
655
|
/**
|
|
590
656
|
* If supplied, changes the color of all the non-transparent pixels to the
|
|
591
|
-
* given color.
|
|
657
|
+
* given color, including the placeholder.
|
|
592
658
|
*/
|
|
593
659
|
tintColor?: ColorValue
|
|
594
660
|
|
|
@@ -601,13 +667,46 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
601
667
|
children?: "FastImage doesn't take children: use FastImageBackground for content over an image"
|
|
602
668
|
}
|
|
603
669
|
|
|
604
|
-
|
|
605
|
-
|
|
670
|
+
// A source without its thumbhash and blurDataUri.
|
|
671
|
+
function withoutPlaceholders<T>(source: T): T {
|
|
672
|
+
if (!hasSourcePlaceholder(source)) return source
|
|
673
|
+
const {
|
|
674
|
+
thumbhash: _thumbhash,
|
|
675
|
+
blurDataUri: _blurDataUri,
|
|
676
|
+
...rest
|
|
677
|
+
} = source as Source
|
|
678
|
+
return rest as T
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// What the native view shows while a source loads: its ThumbHash (decoded
|
|
682
|
+
// natively), or its blurDataUri, or '' for neither.
|
|
683
|
+
function sourcePlaceholder(source: unknown): string {
|
|
684
|
+
if (!source || typeof source !== 'object') return ''
|
|
685
|
+
const { thumbhash, blurDataUri } = source as Source
|
|
686
|
+
return thumbhash || dataUri(blurDataUri) || ''
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const resolvePlaceholder = (
|
|
690
|
+
placeholder?: ImageRequireSource | string,
|
|
606
691
|
): string | object | null => {
|
|
607
|
-
if (
|
|
692
|
+
if (typeof placeholder === 'string') {
|
|
693
|
+
// An image bundled with the app, by name, as React Native's Image
|
|
694
|
+
// takes it ({ uri: name }).
|
|
695
|
+
if (isImageName(placeholder)) {
|
|
696
|
+
return Platform.OS === 'android'
|
|
697
|
+
? placeholder
|
|
698
|
+
: { uri: placeholder }
|
|
699
|
+
}
|
|
700
|
+
const uri = dataUri(placeholder)
|
|
701
|
+
if (!uri) return null
|
|
702
|
+
// Android decodes it; iOS takes it as an image source, as it does a
|
|
703
|
+
// require()d image's.
|
|
704
|
+
return Platform.OS === 'android' ? uri : { uri }
|
|
705
|
+
}
|
|
706
|
+
if (!placeholder) {
|
|
608
707
|
return null
|
|
609
708
|
}
|
|
610
|
-
const resolved = Image.resolveAssetSource(
|
|
709
|
+
const resolved = Image.resolveAssetSource(placeholder)
|
|
611
710
|
if (!resolved) {
|
|
612
711
|
return null
|
|
613
712
|
}
|
|
@@ -719,6 +818,7 @@ declare const __DEV__: boolean
|
|
|
719
818
|
|
|
720
819
|
function FastImageBase({
|
|
721
820
|
source,
|
|
821
|
+
placeholder,
|
|
722
822
|
defaultSource,
|
|
723
823
|
tintColor,
|
|
724
824
|
onLoadStart,
|
|
@@ -732,6 +832,9 @@ function FastImageBase({
|
|
|
732
832
|
objectPosition,
|
|
733
833
|
loop,
|
|
734
834
|
transition,
|
|
835
|
+
// Web only.
|
|
836
|
+
loading: _loading,
|
|
837
|
+
sizes: _sizes,
|
|
735
838
|
forwardedRef,
|
|
736
839
|
...viewProps
|
|
737
840
|
}: FastImageProps & { forwardedRef: React.Ref<any> }) {
|
|
@@ -765,18 +868,49 @@ function FastImageBase({
|
|
|
765
868
|
const fit = resolveObjectFit(objectFit, style)
|
|
766
869
|
const position = objectPositionOrCentered(objectPosition)
|
|
767
870
|
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const resolvedSource =
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
871
|
+
// Memoized on source, so an unchanged source reaches the native view as
|
|
872
|
+
// the same objects, and React's prop diff stops at them.
|
|
873
|
+
// `placeholder` as a thumbhash or blurDataUri, for the sources without
|
|
874
|
+
// their own: a string, so an object made on every render doesn't make
|
|
875
|
+
// new sourcePlaceholders.
|
|
876
|
+
const shared = hasSourcePlaceholder(placeholder)
|
|
877
|
+
? sourcePlaceholder(placeholder)
|
|
878
|
+
: ''
|
|
879
|
+
const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
|
|
880
|
+
// Several sources are picked from natively, for the view's size; one
|
|
881
|
+
// in an array is a plain source.
|
|
882
|
+
const sources = Array.isArray(source)
|
|
883
|
+
? source.length === 1
|
|
884
|
+
? undefined
|
|
885
|
+
: source
|
|
886
|
+
: undefined
|
|
887
|
+
const single = Array.isArray(source) ? source[0] : source
|
|
888
|
+
// Sources' thumbhashes and blurDataUris go to the native view apart
|
|
889
|
+
// from the sources, so a new one for the same image doesn't load it
|
|
890
|
+
// again, by the source's index ('' for none). A source's own wins
|
|
891
|
+
// over `placeholder`'s.
|
|
892
|
+
const placeholders = (sources ?? [single]).map(
|
|
893
|
+
(entry) => sourcePlaceholder(entry) || shared,
|
|
894
|
+
)
|
|
895
|
+
return {
|
|
896
|
+
resolvedSource: sources
|
|
897
|
+
? undefined
|
|
898
|
+
: Image.resolveAssetSource(withoutPlaceholders(single) as any),
|
|
899
|
+
sources: sources?.some(hasSourcePlaceholder)
|
|
900
|
+
? sources.map(withoutPlaceholders)
|
|
901
|
+
: sources,
|
|
902
|
+
sourcePlaceholders: placeholders.some(Boolean)
|
|
903
|
+
? placeholders
|
|
904
|
+
: undefined,
|
|
905
|
+
}
|
|
906
|
+
}, [source, shared])
|
|
907
|
+
// A thumbhash or blurDataUri in `placeholder` goes in sourcePlaceholders
|
|
908
|
+
// instead: it isn't an image to show as it is, but defaultSource still is.
|
|
909
|
+
const resolvedPlaceholder = resolvePlaceholder(
|
|
910
|
+
hasSourcePlaceholder(placeholder)
|
|
911
|
+
? defaultSource
|
|
912
|
+
: (placeholder ?? defaultSource),
|
|
913
|
+
)
|
|
780
914
|
|
|
781
915
|
return (
|
|
782
916
|
<FastImageView
|
|
@@ -794,7 +928,8 @@ function FastImageBase({
|
|
|
794
928
|
)}
|
|
795
929
|
source={resolvedSource}
|
|
796
930
|
sources={sources}
|
|
797
|
-
|
|
931
|
+
sourcePlaceholders={sourcePlaceholders}
|
|
932
|
+
placeholder={resolvedPlaceholder}
|
|
798
933
|
onFastImageLoadStart={onLoadStart}
|
|
799
934
|
onFastImageProgress={onProgress}
|
|
800
935
|
// The native views only send the events with a handler (and only
|
package/src/index.web.tsx
CHANGED
|
@@ -5,20 +5,23 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The View (a div) has FastImage's style, ref and View props. The <img> fills
|
|
7
7
|
// it inside its borders, with object-fit for objectFit (see Picture).
|
|
8
|
-
//
|
|
8
|
+
// The placeholder is a second <img> under it until the image has loaded (the
|
|
9
9
|
// browser draws the image over it as it arrives), and tintColor is an SVG
|
|
10
10
|
// filter next to them.
|
|
11
11
|
//
|
|
12
|
-
// Works: source (uri, require(), or several sizes: see sizedSources
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
12
|
+
// Works: source (uri, require(), or several sizes: see sizedSources, and
|
|
13
|
+
// thumbhash and blurDataUri), placeholder (and defaultSource), objectFit,
|
|
14
|
+
// objectPosition, tintColor, blurRadius, style, source.priority (as
|
|
15
|
+
// fetchpriority), the web-only loading and sizes, onLoadStart, onLoad,
|
|
16
|
+
// onError, onLoadEnd, and View props (testID, accessibility, onLayout,
|
|
17
|
+
// pointerEvents). The native-only props (cache, headers, transition,
|
|
18
|
+
// downsample, loop, paused, imageRendering, recyclingKey) and onProgress
|
|
19
|
+
// are ignored.
|
|
18
20
|
import React, {
|
|
19
21
|
forwardRef,
|
|
20
22
|
memo,
|
|
21
23
|
useEffect,
|
|
24
|
+
useId,
|
|
22
25
|
useLayoutEffect,
|
|
23
26
|
useRef,
|
|
24
27
|
useState,
|
|
@@ -29,7 +32,8 @@ import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
|
|
|
29
32
|
import { cacheControl, priority, resizeMode } from './constants'
|
|
30
33
|
import { resolveObjectFit } from './objectFit'
|
|
31
34
|
import { cssPosition, objectPositionOrCentered } from './objectPosition'
|
|
32
|
-
import {
|
|
35
|
+
import { dataUri, hasSourcePlaceholder } from './placeholders'
|
|
36
|
+
import { warnIfChildren, warnOnce } from './warnings'
|
|
33
37
|
import type {
|
|
34
38
|
CachePathResult,
|
|
35
39
|
CacheState,
|
|
@@ -38,7 +42,9 @@ import type {
|
|
|
38
42
|
FastImageStaticProperties,
|
|
39
43
|
ObjectFit,
|
|
40
44
|
PreloadResult,
|
|
45
|
+
Priority,
|
|
41
46
|
Source,
|
|
47
|
+
SourcePlaceholder,
|
|
42
48
|
} from './index'
|
|
43
49
|
|
|
44
50
|
export type * from './index'
|
|
@@ -49,15 +55,6 @@ const notSupported: CachePathResult = {
|
|
|
49
55
|
error: 'Not supported on the web',
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
// An id that's the same on the server and in the browser: React's useId,
|
|
53
|
-
// from React 18 (this package's React types are older), without the
|
|
54
|
-
// characters an SVG id can't have (:r0: in React 18, «r0» in React 19). On
|
|
55
|
-
// React 17, a counter's (which can differ from the server's).
|
|
56
|
-
let ids = 0
|
|
57
|
-
const useCounterId = () => useState(() => `${++ids}`)[0]
|
|
58
|
-
const useSvgId: () => string =
|
|
59
|
-
(React as unknown as { useId?: () => string }).useId ?? useCounterId
|
|
60
|
-
|
|
61
58
|
// A layout effect in the browser (it runs before the image can show), and an
|
|
62
59
|
// effect when rendering on a server, where layout effects warn.
|
|
63
60
|
const useClientLayoutEffect =
|
|
@@ -71,6 +68,7 @@ declare const __DEV__: boolean | undefined
|
|
|
71
68
|
|
|
72
69
|
function FastImageBase({
|
|
73
70
|
source,
|
|
71
|
+
placeholder,
|
|
74
72
|
defaultSource,
|
|
75
73
|
tintColor,
|
|
76
74
|
blurRadius,
|
|
@@ -82,6 +80,8 @@ function FastImageBase({
|
|
|
82
80
|
children,
|
|
83
81
|
objectFit,
|
|
84
82
|
objectPosition,
|
|
83
|
+
loading,
|
|
84
|
+
sizes,
|
|
85
85
|
forwardedRef,
|
|
86
86
|
// On the wrapper, the view the layout and touches are for here (on
|
|
87
87
|
// native, FastImage's only view).
|
|
@@ -121,13 +121,29 @@ function FastImageBase({
|
|
|
121
121
|
// one doesn't stay while it loads; with several sizes, the <img> stays,
|
|
122
122
|
// and the browser shows the previous image until the new one has loaded.
|
|
123
123
|
const key = srcSet ?? src
|
|
124
|
-
const
|
|
124
|
+
const priority = sourcePriority(source)
|
|
125
|
+
// The source's blurDataUri, or its ThumbHash made into one (with
|
|
126
|
+
// `react-native-fast-image/thumbhash`), shows blurred (see PreviewBlur)
|
|
127
|
+
// in place of `placeholder` (or `placeholder`'s, for a source without its
|
|
128
|
+
// own). With several sizes the browser picks one itself (from the srcset,
|
|
129
|
+
// as it loads), so they only show if all the sizes have the same one.
|
|
130
|
+
const preview = hasSourcePlaceholder(placeholder) ? placeholder : undefined
|
|
131
|
+
const blurUri =
|
|
132
|
+
resolveUri(dataUri(shared(source, 'blurDataUri', preview))) ??
|
|
133
|
+
thumbhashUri(shared(source, 'thumbhash', preview))
|
|
134
|
+
// A thumbhash or blurDataUri in `placeholder` isn't an image to show as it
|
|
135
|
+
// is: defaultSource still is.
|
|
136
|
+
const defaultUri =
|
|
137
|
+
blurUri ??
|
|
138
|
+
resolveUri(preview ? defaultSource : (placeholder ?? defaultSource))
|
|
125
139
|
// Whether the image for `key` loaded, or failed.
|
|
126
140
|
const [result, setResult] = useState<{ key: string; ok: boolean }>()
|
|
127
141
|
const ok = result && result.key === key ? result.ok : undefined
|
|
128
142
|
// The same on the server and in the browser. React's ids have characters
|
|
129
143
|
// an SVG id can't (:r0: in React 18, «r0» in React 19).
|
|
130
|
-
const
|
|
144
|
+
const svgId = useId().replace(/[^\w-]/g, '')
|
|
145
|
+
const tintId = `fast-image-tint-${svgId}`
|
|
146
|
+
const blurId = `fast-image-blur-${svgId}`
|
|
131
147
|
// objectFit (the prop, then style's).
|
|
132
148
|
const fit = resolveObjectFit(objectFit, style)
|
|
133
149
|
const position = cssPosition(objectPositionOrCentered(objectPosition))
|
|
@@ -138,7 +154,7 @@ function FastImageBase({
|
|
|
138
154
|
]
|
|
139
155
|
.filter(Boolean)
|
|
140
156
|
.join(' ') || undefined
|
|
141
|
-
//
|
|
157
|
+
// The placeholder shows until the image has loaded, and if it fails.
|
|
142
158
|
const showDefault = !!defaultUri && ok !== true
|
|
143
159
|
|
|
144
160
|
useClientLayoutEffect(() => {
|
|
@@ -160,9 +176,31 @@ function FastImageBase({
|
|
|
160
176
|
<Picture
|
|
161
177
|
key={`default ${defaultUri}`}
|
|
162
178
|
src={defaultUri}
|
|
163
|
-
|
|
179
|
+
// Loaded when the image is (a data uri needs no request).
|
|
180
|
+
loading={loading}
|
|
181
|
+
priority={priority}
|
|
182
|
+
// A source's placeholder stands for an image whose size
|
|
183
|
+
// isn't known yet: as contain for scale-down and none,
|
|
184
|
+
// which show an image at its own size.
|
|
185
|
+
fit={
|
|
186
|
+
blurUri && (fit === 'scale-down' || fit === 'none')
|
|
187
|
+
? 'contain'
|
|
188
|
+
: fit
|
|
189
|
+
}
|
|
164
190
|
position={position}
|
|
165
|
-
|
|
191
|
+
// A source's preview gets its own blur and the tint, not
|
|
192
|
+
// blurRadius (it's blurred already), as on iOS and
|
|
193
|
+
// Android.
|
|
194
|
+
filter={
|
|
195
|
+
blurUri
|
|
196
|
+
? [
|
|
197
|
+
`url(#${blurId})`,
|
|
198
|
+
tintColor ? `url(#${tintId})` : '',
|
|
199
|
+
]
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join(' ')
|
|
202
|
+
: filter
|
|
203
|
+
}
|
|
166
204
|
// The image has the label, if there is one.
|
|
167
205
|
alt={key ? '' : (label ?? '')}
|
|
168
206
|
/>
|
|
@@ -172,11 +210,13 @@ function FastImageBase({
|
|
|
172
210
|
key={several ? 'sizes' : key}
|
|
173
211
|
src={src}
|
|
174
212
|
srcSet={srcSet}
|
|
175
|
-
|
|
213
|
+
sizes={sizes}
|
|
214
|
+
loading={loading}
|
|
215
|
+
priority={priority}
|
|
176
216
|
fit={fit}
|
|
177
217
|
position={position}
|
|
178
218
|
filter={filter}
|
|
179
|
-
// Shown as it loads, over
|
|
219
|
+
// Shown as it loads, over the placeholder, and not if it
|
|
180
220
|
// failed (no broken image icon or alt text).
|
|
181
221
|
hidden={ok === false}
|
|
182
222
|
alt={label ?? ''}
|
|
@@ -195,11 +235,66 @@ function FastImageBase({
|
|
|
195
235
|
/>
|
|
196
236
|
) : null}
|
|
197
237
|
{tintColor ? <TintFilter id={tintId} color={tintColor} /> : null}
|
|
238
|
+
{blurUri && showDefault ? <PreviewBlur id={blurId} /> : null}
|
|
198
239
|
</View>
|
|
199
240
|
)
|
|
200
241
|
}
|
|
201
242
|
|
|
202
|
-
// A
|
|
243
|
+
// A ThumbHash as a blurDataUri, with `react-native-fast-image/thumbhash`
|
|
244
|
+
// imported (see thumbhash.ts). Without it, ignored, with a warning in
|
|
245
|
+
// development: FastImage doesn't bundle the decoder on the web.
|
|
246
|
+
function thumbhashUri(hash: string | undefined): string | undefined {
|
|
247
|
+
if (!hash) return undefined
|
|
248
|
+
const decode = (globalThis as Record<symbol, unknown>)[
|
|
249
|
+
Symbol.for('react-native-fast-image/thumbhash')
|
|
250
|
+
] as ((hash: string) => string | undefined) | undefined
|
|
251
|
+
if (decode) return decode(hash)
|
|
252
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
253
|
+
warnOnce(
|
|
254
|
+
'thumbhash-web',
|
|
255
|
+
"react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.",
|
|
256
|
+
'warn',
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
return undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A source's priority: one source's, or the highest of several sizes'.
|
|
263
|
+
function sourcePriority(
|
|
264
|
+
source: FastImageProps['source'],
|
|
265
|
+
): Priority | undefined {
|
|
266
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) =>
|
|
267
|
+
entry && typeof entry === 'object'
|
|
268
|
+
? (entry as Source).priority
|
|
269
|
+
: undefined,
|
|
270
|
+
)
|
|
271
|
+
return (['high', 'normal', 'low'] as const).find((priority) =>
|
|
272
|
+
values.includes(priority),
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The thumbhash or blurDataUri a source shows: its own, or `placeholder`'s
|
|
277
|
+
// (preview) if it has neither. With several sources, the one they all show.
|
|
278
|
+
function shared(
|
|
279
|
+
source: FastImageProps['source'],
|
|
280
|
+
key: 'thumbhash' | 'blurDataUri',
|
|
281
|
+
preview: SourcePlaceholder | undefined,
|
|
282
|
+
): string | undefined {
|
|
283
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => {
|
|
284
|
+
const own =
|
|
285
|
+
entry && typeof entry === 'object' ? (entry as Source) : undefined
|
|
286
|
+
// A blurDataUri that isn't a data uri is ignored (with a warning), as
|
|
287
|
+
// on iOS and Android.
|
|
288
|
+
return (own?.thumbhash || dataUri(own?.blurDataUri) ? own : preview)?.[
|
|
289
|
+
key
|
|
290
|
+
]
|
|
291
|
+
})
|
|
292
|
+
return values.every((value) => value && value === values[0])
|
|
293
|
+
? values[0]
|
|
294
|
+
: undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A source's (or a placeholder's) uri. Expo's web builds make a require()d
|
|
203
298
|
// image a { uri, width, height } object (or a string). Other setups can make
|
|
204
299
|
// it a number in react-native-web's asset registry, resolved as its Image
|
|
205
300
|
// does: the file for the scale closest to the screen's.
|
|
@@ -242,9 +337,9 @@ type PackagerAsset = {
|
|
|
242
337
|
// Several sizes of an image, as a srcset (each size's width, times its
|
|
243
338
|
// scale), from which the browser loads the one for the image's width in
|
|
244
339
|
// device pixels, usually the smallest at least as wide, as expo-image does.
|
|
245
|
-
// sizes="auto" has the browser use the width the image
|
|
246
|
-
// only applies to lazy images, and
|
|
247
|
-
// 100vw (the viewport's width). src is the largest, for browsers without
|
|
340
|
+
// sizes="auto" (the default sizes) has the browser use the width the image
|
|
341
|
+
// is laid out at; it only applies to lazy images, and otherwise, or in
|
|
342
|
+
// browsers without it, the next value, 100vw (the viewport's width). src is the largest, for browsers without
|
|
248
343
|
// srcset.
|
|
249
344
|
function sizedSources(sources: Source[]) {
|
|
250
345
|
const widths = sources
|
|
@@ -316,7 +411,9 @@ const fill = {
|
|
|
316
411
|
function Picture({
|
|
317
412
|
src,
|
|
318
413
|
srcSet,
|
|
319
|
-
|
|
414
|
+
sizes,
|
|
415
|
+
loading,
|
|
416
|
+
priority,
|
|
320
417
|
fit,
|
|
321
418
|
position,
|
|
322
419
|
filter,
|
|
@@ -327,8 +424,11 @@ function Picture({
|
|
|
327
424
|
}: {
|
|
328
425
|
src: string
|
|
329
426
|
srcSet?: string
|
|
330
|
-
//
|
|
331
|
-
|
|
427
|
+
// The props (see sizedSources for sizes).
|
|
428
|
+
sizes?: string
|
|
429
|
+
loading?: 'lazy' | 'eager'
|
|
430
|
+
// source.priority, as the <img>'s fetchpriority.
|
|
431
|
+
priority?: Priority
|
|
332
432
|
fit: ObjectFit
|
|
333
433
|
// objectPosition, as CSS.
|
|
334
434
|
position: string
|
|
@@ -399,8 +499,12 @@ function Picture({
|
|
|
399
499
|
ref={image}
|
|
400
500
|
src={src}
|
|
401
501
|
srcSet={srcSet}
|
|
402
|
-
sizes={srcSet ? 'auto, 100vw' : undefined}
|
|
403
|
-
loading={
|
|
502
|
+
sizes={srcSet ? (sizes ?? 'auto, 100vw') : undefined}
|
|
503
|
+
loading={loading === 'eager' ? undefined : 'lazy'}
|
|
504
|
+
// For 'high' and 'low' (the browser's own for 'normal').
|
|
505
|
+
fetchPriority={
|
|
506
|
+
priority === 'high' || priority === 'low' ? priority : undefined
|
|
507
|
+
}
|
|
404
508
|
alt={alt}
|
|
405
509
|
draggable={false}
|
|
406
510
|
style={{
|
|
@@ -439,6 +543,35 @@ function TintFilter({ id, color }: { id: string; color: unknown }) {
|
|
|
439
543
|
)
|
|
440
544
|
}
|
|
441
545
|
|
|
546
|
+
// A source's blurDataUri, a few pixels, blurred as it's scaled up: by about
|
|
547
|
+
// half a pixel of an 8 px image, in units of its size, so the same filter
|
|
548
|
+
// works for any view, also rendered on a server (no layout needed). The blur
|
|
549
|
+
// fades the edges, which the alpha table restores (transparent areas stay
|
|
550
|
+
// transparent): Chrome ignores edgeMode, and forcing alpha to 1 would fill
|
|
551
|
+
// a transparent image's background.
|
|
552
|
+
function PreviewBlur({ id }: { id: string }) {
|
|
553
|
+
return (
|
|
554
|
+
<svg style={tintStyle}>
|
|
555
|
+
<defs>
|
|
556
|
+
<filter
|
|
557
|
+
id={id}
|
|
558
|
+
x="0"
|
|
559
|
+
y="0"
|
|
560
|
+
width="1"
|
|
561
|
+
height="1"
|
|
562
|
+
primitiveUnits="objectBoundingBox"
|
|
563
|
+
colorInterpolationFilters="sRGB"
|
|
564
|
+
>
|
|
565
|
+
<feGaussianBlur stdDeviation="0.06" />
|
|
566
|
+
<feComponentTransfer>
|
|
567
|
+
<feFuncA type="table" tableValues="0 1 1 1 1" />
|
|
568
|
+
</feComponentTransfer>
|
|
569
|
+
</filter>
|
|
570
|
+
</defs>
|
|
571
|
+
</svg>
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
442
575
|
const tintStyle = {
|
|
443
576
|
position: 'absolute',
|
|
444
577
|
width: 0,
|