react-native-gradient-mask 0.1.0-beta.1

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.
Files changed (54) hide show
  1. package/.eslintrc.js +5 -0
  2. package/PUBLISHING.md +171 -0
  3. package/README.md +360 -0
  4. package/android/build.gradle +43 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/gradientmask/GradientMaskModule.kt +33 -0
  7. package/android/src/main/java/expo/modules/gradientmask/GradientMaskView.kt +198 -0
  8. package/build/AnimatedGradientMaskView.d.ts +36 -0
  9. package/build/AnimatedGradientMaskView.d.ts.map +1 -0
  10. package/build/AnimatedGradientMaskView.js +37 -0
  11. package/build/AnimatedGradientMaskView.js.map +1 -0
  12. package/build/AnimatedGradientMaskView.web.d.ts +17 -0
  13. package/build/AnimatedGradientMaskView.web.d.ts.map +1 -0
  14. package/build/AnimatedGradientMaskView.web.js +100 -0
  15. package/build/AnimatedGradientMaskView.web.js.map +1 -0
  16. package/build/GradientMask.types.d.ts +38 -0
  17. package/build/GradientMask.types.d.ts.map +1 -0
  18. package/build/GradientMask.types.js +2 -0
  19. package/build/GradientMask.types.js.map +1 -0
  20. package/build/GradientMaskModule.d.ts +3 -0
  21. package/build/GradientMaskModule.d.ts.map +1 -0
  22. package/build/GradientMaskModule.js +4 -0
  23. package/build/GradientMaskModule.js.map +1 -0
  24. package/build/GradientMaskModule.web.d.ts +3 -0
  25. package/build/GradientMaskModule.web.d.ts.map +1 -0
  26. package/build/GradientMaskModule.web.js +3 -0
  27. package/build/GradientMaskModule.web.js.map +1 -0
  28. package/build/GradientMaskView.d.ts +4 -0
  29. package/build/GradientMaskView.d.ts.map +1 -0
  30. package/build/GradientMaskView.js +7 -0
  31. package/build/GradientMaskView.js.map +1 -0
  32. package/build/GradientMaskView.web.d.ts +8 -0
  33. package/build/GradientMaskView.web.d.ts.map +1 -0
  34. package/build/GradientMaskView.web.js +99 -0
  35. package/build/GradientMaskView.web.js.map +1 -0
  36. package/build/index.d.ts +6 -0
  37. package/build/index.d.ts.map +1 -0
  38. package/build/index.js +7 -0
  39. package/build/index.js.map +1 -0
  40. package/expo-module.config.json +9 -0
  41. package/ios/GradientMask.podspec +29 -0
  42. package/ios/GradientMaskModule.swift +29 -0
  43. package/ios/GradientMaskView.swift +133 -0
  44. package/package.json +83 -0
  45. package/rn-gradient-mask-design.md +657 -0
  46. package/src/AnimatedGradientMaskView.tsx +60 -0
  47. package/src/AnimatedGradientMaskView.web.tsx +149 -0
  48. package/src/GradientMask.types.ts +43 -0
  49. package/src/GradientMaskModule.ts +4 -0
  50. package/src/GradientMaskModule.web.ts +2 -0
  51. package/src/GradientMaskView.tsx +11 -0
  52. package/src/GradientMaskView.web.tsx +129 -0
  53. package/src/index.ts +7 -0
  54. package/tsconfig.json +9 -0
@@ -0,0 +1,60 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+ import { useAnimatedProps } from 'react-native-reanimated';
4
+ import type { SharedValue } from 'react-native-reanimated';
5
+ import Animated from 'react-native-reanimated';
6
+
7
+ import { GradientMaskViewProps } from './GradientMask.types';
8
+
9
+ const NativeView: React.ComponentType<GradientMaskViewProps> =
10
+ requireNativeView('GradientMask');
11
+
12
+ const AnimatedNativeView = Animated.createAnimatedComponent(NativeView);
13
+
14
+ export type AnimatedGradientMaskViewProps = Omit<
15
+ GradientMaskViewProps,
16
+ 'maskOpacity'
17
+ > & {
18
+ /**
19
+ * Mask effect intensity (0-1) as a Reanimated SharedValue
20
+ * 0 = no gradient effect (content fully visible)
21
+ * 1 = full gradient effect
22
+ */
23
+ maskOpacity: SharedValue<number>;
24
+ };
25
+
26
+ /**
27
+ * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件
28
+ *
29
+ * 使用方式:
30
+ * ```tsx
31
+ * const maskOpacity = useSharedValue(0);
32
+ *
33
+ * // 動畫顯示
34
+ * maskOpacity.value = withTiming(1, {
35
+ * duration: 600,
36
+ * easing: Easing.in(Easing.quad),
37
+ * });
38
+ *
39
+ * <AnimatedGradientMaskView
40
+ * colors={colors}
41
+ * locations={locations}
42
+ * direction="top"
43
+ * maskOpacity={maskOpacity}
44
+ * >
45
+ * <FlashList ... />
46
+ * </AnimatedGradientMaskView>
47
+ * ```
48
+ */
49
+ export default function AnimatedGradientMaskView(
50
+ props: AnimatedGradientMaskViewProps
51
+ ) {
52
+ const { maskOpacity, ...restProps } = props;
53
+
54
+ const animatedProps = useAnimatedProps<Pick<GradientMaskViewProps, 'maskOpacity'>>(() => ({
55
+ maskOpacity: maskOpacity.value,
56
+ }));
57
+
58
+ return <AnimatedNativeView {...restProps} animatedProps={animatedProps} />;
59
+ }
60
+
@@ -0,0 +1,149 @@
1
+ import * as React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { useAnimatedReaction, runOnJS } from 'react-native-reanimated';
4
+ import type { SharedValue } from 'react-native-reanimated';
5
+
6
+ import { GradientMaskViewProps } from './GradientMask.types';
7
+
8
+ export type AnimatedGradientMaskViewProps = Omit<
9
+ GradientMaskViewProps,
10
+ 'maskOpacity'
11
+ > & {
12
+ /**
13
+ * Mask effect intensity (0-1) as a Reanimated SharedValue
14
+ * 0 = no gradient effect (content fully visible)
15
+ * 1 = full gradient effect
16
+ */
17
+ maskOpacity: SharedValue<number>;
18
+ };
19
+
20
+ /**
21
+ * 將 processColor 處理過的顏色值轉回 rgba 字串
22
+ */
23
+ function colorToRgba(color: number | null): string {
24
+ if (color === null || color === undefined) {
25
+ return 'rgba(0, 0, 0, 0)';
26
+ }
27
+
28
+ const intValue = color >>> 0;
29
+ const a = ((intValue >> 24) & 0xff) / 255;
30
+ const r = (intValue >> 16) & 0xff;
31
+ const g = (intValue >> 8) & 0xff;
32
+ const b = intValue & 0xff;
33
+
34
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
35
+ }
36
+
37
+ /**
38
+ * 根據 direction 取得 CSS linear-gradient 的方向
39
+ */
40
+ function getGradientDirection(direction: GradientMaskViewProps['direction']): string {
41
+ switch (direction) {
42
+ case 'top':
43
+ return 'to bottom';
44
+ case 'bottom':
45
+ return 'to top';
46
+ case 'left':
47
+ return 'to right';
48
+ case 'right':
49
+ return 'to left';
50
+ default:
51
+ return 'to bottom';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 建立 CSS linear-gradient 字串
57
+ */
58
+ function buildGradientString(
59
+ colors: (number | null)[],
60
+ locations: number[],
61
+ direction: GradientMaskViewProps['direction']
62
+ ): string {
63
+ const gradientDirection = getGradientDirection(direction);
64
+
65
+ const colorStops = colors.map((color, index) => {
66
+ const rgba = colorToRgba(color);
67
+ const location = locations[index] !== undefined ? locations[index] * 100 : (index / (colors.length - 1)) * 100;
68
+ return `${rgba} ${location}%`;
69
+ });
70
+
71
+ return `linear-gradient(${gradientDirection}, ${colorStops.join(', ')})`;
72
+ }
73
+
74
+ /**
75
+ * 根據 maskOpacity 調整顏色的 alpha 值
76
+ */
77
+ function adjustColorsForOpacity(
78
+ colors: (number | null)[],
79
+ maskOpacity: number
80
+ ): (number | null)[] {
81
+ if (maskOpacity >= 1) return colors;
82
+ if (maskOpacity <= 0) {
83
+ return colors.map(() => 0xff000000);
84
+ }
85
+
86
+ return colors.map((color) => {
87
+ if (color === null || color === undefined) return color;
88
+ const intValue = color >>> 0;
89
+ const a = ((intValue >> 24) & 0xff) / 255;
90
+ const r = (intValue >> 16) & 0xff;
91
+ const g = (intValue >> 8) & 0xff;
92
+ const b = intValue & 0xff;
93
+ const adjustedAlpha = a + (1 - a) * (1 - maskOpacity);
94
+ return ((Math.round(adjustedAlpha * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;
95
+ });
96
+ }
97
+
98
+ /**
99
+ * AnimatedGradientMaskView - Web 實作
100
+ * 使用 CSS mask-image 搭配 useAnimatedReaction 監聽 SharedValue 變化
101
+ */
102
+ export default function AnimatedGradientMaskView(props: AnimatedGradientMaskViewProps) {
103
+ const {
104
+ colors,
105
+ locations,
106
+ direction = 'top',
107
+ maskOpacity,
108
+ style,
109
+ children,
110
+ } = props;
111
+
112
+ const [currentOpacity, setCurrentOpacity] = React.useState(maskOpacity.value);
113
+
114
+ // 監聽 SharedValue 變化並更新 state
115
+ useAnimatedReaction(
116
+ () => maskOpacity.value,
117
+ (value) => {
118
+ runOnJS(setCurrentOpacity)(value);
119
+ },
120
+ [maskOpacity]
121
+ );
122
+
123
+ const maskStyle = React.useMemo(() => {
124
+ if (currentOpacity <= 0) {
125
+ return {};
126
+ }
127
+
128
+ const adjustedColors = adjustColorsForOpacity(colors, currentOpacity);
129
+ const gradientString = buildGradientString(adjustedColors, locations, direction);
130
+
131
+ return {
132
+ WebkitMaskImage: gradientString,
133
+ maskImage: gradientString,
134
+ transition: 'mask-image 0.1s ease-out, -webkit-mask-image 0.1s ease-out',
135
+ };
136
+ }, [colors, locations, direction, currentOpacity]);
137
+
138
+ return (
139
+ <View style={[styles.container, style, maskStyle as any]}>
140
+ {children}
141
+ </View>
142
+ );
143
+ }
144
+
145
+ const styles = StyleSheet.create({
146
+ container: {
147
+ overflow: 'hidden',
148
+ },
149
+ });
@@ -0,0 +1,43 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type GradientMaskViewProps = {
4
+ /**
5
+ * Gradient colors array (processed colors from processColor)
6
+ * Use alpha values to control opacity
7
+ * e.g., ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'] = transparent to opaque
8
+ */
9
+ colors: (number | null)[];
10
+
11
+ /**
12
+ * Position of each color (0-1)
13
+ * e.g., [0, 0.3, 1] means first color at 0%, second at 30%, third at 100%
14
+ */
15
+ locations: number[];
16
+
17
+ /**
18
+ * Gradient direction
19
+ * 'top' = top transparent, bottom opaque
20
+ * 'bottom' = bottom transparent, top opaque
21
+ * 'left' = left transparent, right opaque
22
+ * 'right' = right transparent, left opaque
23
+ */
24
+ direction?: 'top' | 'bottom' | 'left' | 'right';
25
+
26
+ /**
27
+ * Mask effect intensity (0-1)
28
+ * 0 = no gradient effect (content fully visible)
29
+ * 1 = full gradient effect
30
+ * @default 1
31
+ */
32
+ maskOpacity?: number;
33
+
34
+ /**
35
+ * Style
36
+ */
37
+ style?: StyleProp<ViewStyle>;
38
+
39
+ /**
40
+ * Children
41
+ */
42
+ children?: React.ReactNode;
43
+ };
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo';
2
+
3
+ // We only use the View, no module functions needed
4
+ export default requireNativeModule('GradientMask');
@@ -0,0 +1,2 @@
1
+ // Web module stub - GradientMask is primarily for native platforms
2
+ export default {};
@@ -0,0 +1,11 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+
4
+ import { GradientMaskViewProps } from './GradientMask.types';
5
+
6
+ const NativeView: React.ComponentType<GradientMaskViewProps> =
7
+ requireNativeView('GradientMask');
8
+
9
+ export default function GradientMaskView(props: GradientMaskViewProps) {
10
+ return <NativeView {...props} />;
11
+ }
@@ -0,0 +1,129 @@
1
+ import * as React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+
4
+ import { GradientMaskViewProps } from './GradientMask.types';
5
+
6
+ /**
7
+ * 將 processColor 處理過的顏色值轉回 rgba 字串
8
+ */
9
+ function colorToRgba(color: number | null): string {
10
+ if (color === null || color === undefined) {
11
+ return 'rgba(0, 0, 0, 0)';
12
+ }
13
+
14
+ // processColor 在 web 上回傳的格式是 AARRGGBB (32-bit integer)
15
+ const intValue = color >>> 0; // 確保是 unsigned
16
+ const a = ((intValue >> 24) & 0xff) / 255;
17
+ const r = (intValue >> 16) & 0xff;
18
+ const g = (intValue >> 8) & 0xff;
19
+ const b = intValue & 0xff;
20
+
21
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
22
+ }
23
+
24
+ /**
25
+ * 根據 direction 取得 CSS linear-gradient 的方向
26
+ */
27
+ function getGradientDirection(direction: GradientMaskViewProps['direction']): string {
28
+ switch (direction) {
29
+ case 'top':
30
+ return 'to bottom'; // 頂部透明 → 底部不透明
31
+ case 'bottom':
32
+ return 'to top'; // 底部透明 → 頂部不透明
33
+ case 'left':
34
+ return 'to right'; // 左側透明 → 右側不透明
35
+ case 'right':
36
+ return 'to left'; // 右側透明 → 左側不透明
37
+ default:
38
+ return 'to bottom';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 建立 CSS linear-gradient 字串
44
+ */
45
+ function buildGradientString(
46
+ colors: (number | null)[],
47
+ locations: number[],
48
+ direction: GradientMaskViewProps['direction']
49
+ ): string {
50
+ const gradientDirection = getGradientDirection(direction);
51
+
52
+ const colorStops = colors.map((color, index) => {
53
+ const rgba = colorToRgba(color);
54
+ const location = locations[index] !== undefined ? locations[index] * 100 : (index / (colors.length - 1)) * 100;
55
+ return `${rgba} ${location}%`;
56
+ });
57
+
58
+ return `linear-gradient(${gradientDirection}, ${colorStops.join(', ')})`;
59
+ }
60
+
61
+ /**
62
+ * 根據 maskOpacity 調整顏色的 alpha 值
63
+ * maskOpacity = 0 時,所有顏色變為完全不透明(無遮罩效果)
64
+ * maskOpacity = 1 時,保持原本的 alpha 值
65
+ */
66
+ function adjustColorsForOpacity(
67
+ colors: (number | null)[],
68
+ maskOpacity: number
69
+ ): (number | null)[] {
70
+ if (maskOpacity >= 1) return colors;
71
+ if (maskOpacity <= 0) {
72
+ // 全部變成不透明黑色,表示內容完全可見
73
+ return colors.map(() => 0xff000000);
74
+ }
75
+
76
+ return colors.map((color) => {
77
+ if (color === null || color === undefined) return color;
78
+ const intValue = color >>> 0;
79
+ const a = ((intValue >> 24) & 0xff) / 255;
80
+ const r = (intValue >> 16) & 0xff;
81
+ const g = (intValue >> 8) & 0xff;
82
+ const b = intValue & 0xff;
83
+ // 根據 maskOpacity 調整 alpha:當 maskOpacity = 0 時 alpha 趨近於 1(完全可見)
84
+ const adjustedAlpha = a + (1 - a) * (1 - maskOpacity);
85
+ return ((Math.round(adjustedAlpha * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;
86
+ });
87
+ }
88
+
89
+ /**
90
+ * GradientMaskView - Web 實作
91
+ * 使用 CSS mask-image 搭配 linear-gradient 實現漸層遮罩效果
92
+ */
93
+ export default function GradientMaskView(props: GradientMaskViewProps) {
94
+ const {
95
+ colors,
96
+ locations,
97
+ direction = 'top',
98
+ maskOpacity = 1,
99
+ style,
100
+ children,
101
+ } = props;
102
+
103
+ const maskStyle = React.useMemo(() => {
104
+ if (maskOpacity <= 0) {
105
+ // 無遮罩效果
106
+ return {};
107
+ }
108
+
109
+ const adjustedColors = adjustColorsForOpacity(colors, maskOpacity);
110
+ const gradientString = buildGradientString(adjustedColors, locations, direction);
111
+
112
+ return {
113
+ WebkitMaskImage: gradientString,
114
+ maskImage: gradientString,
115
+ };
116
+ }, [colors, locations, direction, maskOpacity]);
117
+
118
+ return (
119
+ <View style={[styles.container, style, maskStyle as any]}>
120
+ {children}
121
+ </View>
122
+ );
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ container: {
127
+ overflow: 'hidden',
128
+ },
129
+ });
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Reexport the native module. On web, it will be resolved to GradientMaskModule.web.ts
2
+ // and on native platforms to GradientMaskModule.ts
3
+ export { default } from './GradientMaskModule';
4
+ export { default as GradientMaskView } from './GradientMaskView';
5
+ export { default as AnimatedGradientMaskView } from './AnimatedGradientMaskView';
6
+ export type { AnimatedGradientMaskViewProps } from './AnimatedGradientMaskView';
7
+ export * from './GradientMask.types';
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
+ }