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,198 @@
1
+ package expo.modules.gradientmask
2
+
3
+ import android.content.Context
4
+ import android.graphics.*
5
+ import android.view.View
6
+ import expo.modules.kotlin.AppContext
7
+ import expo.modules.kotlin.views.ExpoView
8
+
9
+ /**
10
+ * GradientMaskView - 原生漸層透明遮罩
11
+ *
12
+ * 使用 Bitmap 作為 mask,配合 setLayerType + PorterDuff.Mode.DST_IN
13
+ *
14
+ * 顏色語意(與 iOS CAGradientLayer mask 一致):
15
+ * - 顏色的 alpha 值決定該區域內容的可見度
16
+ * - alpha = 0 → 內容透明(看到背景)
17
+ * - alpha = 255 → 內容不透明(看到內容)
18
+ *
19
+ * maskOpacity 控制漸層 mask 效果的顯示程度:
20
+ * - maskOpacity = 0 → 無漸層效果,內容完全可見(全部 alpha=255)
21
+ * - maskOpacity = 1 → 完整漸層效果(使用原始 alpha)
22
+ */
23
+ class GradientMaskView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
24
+
25
+ // Gradient mask 的參數
26
+ private var colors: IntArray? = null
27
+ private var locations: FloatArray? = null
28
+ private var direction: String = "top"
29
+
30
+ // maskOpacity: 0 = 無漸層效果, 1 = 完整漸層效果
31
+ private var maskOpacity: Float = 1f
32
+
33
+ // Mask bitmap 和相關的 Paint
34
+ private var maskBitmap: Bitmap? = null
35
+ private var maskBitmapInvalidated = true
36
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
37
+ private val porterDuffXferMode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
38
+
39
+ init {
40
+ // 確保背景是透明的
41
+ setBackgroundColor(Color.TRANSPARENT)
42
+ // 始終使用 SOFTWARE 模式,避免動態切換層模式造成的黑色閃爍
43
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null)
44
+
45
+ android.util.Log.d("GradientMask", "=== GradientMaskView init ===")
46
+ }
47
+
48
+ // MARK: - Props setters
49
+
50
+ fun setColors(colorArray: List<Int>?) {
51
+ colors = colorArray?.toIntArray()
52
+ maskBitmapInvalidated = true
53
+ invalidate()
54
+ }
55
+
56
+ fun setLocations(locationArray: List<Double>?) {
57
+ locations = locationArray?.map { it.toFloat() }?.toFloatArray()
58
+ maskBitmapInvalidated = true
59
+ invalidate()
60
+ }
61
+
62
+ fun setDirection(dir: String) {
63
+ direction = dir
64
+ maskBitmapInvalidated = true
65
+ invalidate()
66
+ }
67
+
68
+ fun setMaskOpacity(opacity: Double) {
69
+ maskOpacity = opacity.toFloat().coerceIn(0f, 1f)
70
+ maskBitmapInvalidated = true
71
+ invalidate()
72
+ }
73
+
74
+ // MARK: - Layout
75
+
76
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
77
+ super.onSizeChanged(w, h, oldw, oldh)
78
+ if (w > 0 && h > 0) {
79
+ updateMaskBitmap()
80
+ maskBitmapInvalidated = false
81
+ }
82
+ }
83
+
84
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
85
+ super.onLayout(changed, l, t, r, b)
86
+ if (changed) {
87
+ maskBitmapInvalidated = true
88
+ }
89
+ }
90
+
91
+ // MARK: - Drawing
92
+
93
+ override fun dispatchDraw(canvas: Canvas) {
94
+ // 檢查尺寸是否有效
95
+ if (width <= 0 || height <= 0) {
96
+ super.dispatchDraw(canvas)
97
+ return
98
+ }
99
+
100
+ // 如果 mask bitmap 需要更新,重新創建
101
+ if (maskBitmapInvalidated) {
102
+ updateMaskBitmap()
103
+ maskBitmapInvalidated = false
104
+ }
105
+
106
+ val bitmap = maskBitmap
107
+ // 如果沒有 mask bitmap 或 maskOpacity=0,直接繪製子元件(無 mask 效果)
108
+ if (bitmap == null || maskOpacity <= 0f) {
109
+ super.dispatchDraw(canvas)
110
+ return
111
+ }
112
+
113
+ // 使用 saveLayer 創建離屏緩衝區
114
+ val saveCount = canvas.saveLayer(
115
+ 0f, 0f,
116
+ width.toFloat(), height.toFloat(),
117
+ null
118
+ )
119
+
120
+ try {
121
+ // 先繪製所有子元件到離屏緩衝區
122
+ super.dispatchDraw(canvas)
123
+
124
+ // 應用 mask(使用 DST_IN 模式)
125
+ paint.xfermode = porterDuffXferMode
126
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
127
+ paint.xfermode = null
128
+ } finally {
129
+ canvas.restoreToCount(saveCount)
130
+ }
131
+ }
132
+
133
+ private fun updateMaskBitmap() {
134
+ if (width <= 0 || height <= 0) return
135
+
136
+ // 回收舊的 bitmap
137
+ maskBitmap?.recycle()
138
+
139
+ // 創建新的 mask bitmap
140
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
141
+ val bitmapCanvas = Canvas(bitmap)
142
+
143
+ val currentColors = colors
144
+ val currentLocations = locations
145
+
146
+ // 如果沒有 colors/locations,創建全白 mask(內容完全可見)
147
+ if (currentColors == null || currentLocations == null ||
148
+ currentColors.size != currentLocations.size ||
149
+ currentColors.isEmpty()) {
150
+ bitmapCanvas.drawColor(Color.WHITE)
151
+ maskBitmap = bitmap
152
+ return
153
+ }
154
+
155
+ // 計算 effective colors(根據 maskOpacity 混合)
156
+ val effectiveColors = IntArray(currentColors.size) { i ->
157
+ val originalColor = currentColors[i]
158
+ val originalAlpha = Color.alpha(originalColor)
159
+ // 當 maskOpacity = 0,alpha = 255(完全不透明,內容完全可見)
160
+ // 當 maskOpacity = 1,alpha = originalAlpha
161
+ val blendedAlpha = (255 + (originalAlpha - 255) * maskOpacity).toInt()
162
+ Color.argb(blendedAlpha, 255, 255, 255)
163
+ }
164
+
165
+ // 建立 gradient shader
166
+ val (startX, startY, endX, endY) = getGradientCoordinates()
167
+ val shader = LinearGradient(
168
+ startX, startY, endX, endY,
169
+ effectiveColors,
170
+ currentLocations,
171
+ Shader.TileMode.CLAMP
172
+ )
173
+
174
+ // 繪製漸層到 bitmap
175
+ val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
176
+ this.shader = shader
177
+ }
178
+ bitmapCanvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
179
+
180
+ maskBitmap = bitmap
181
+ }
182
+
183
+ override fun onDetachedFromWindow() {
184
+ super.onDetachedFromWindow()
185
+ maskBitmap?.recycle()
186
+ maskBitmap = null
187
+ }
188
+
189
+ private fun getGradientCoordinates(): List<Float> {
190
+ return when (direction) {
191
+ "top" -> listOf(0f, 0f, 0f, height.toFloat())
192
+ "bottom" -> listOf(0f, height.toFloat(), 0f, 0f)
193
+ "left" -> listOf(0f, 0f, width.toFloat(), 0f)
194
+ "right" -> listOf(width.toFloat(), 0f, 0f, 0f)
195
+ else -> listOf(0f, 0f, 0f, height.toFloat())
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import { GradientMaskViewProps } from './GradientMask.types';
4
+ export type AnimatedGradientMaskViewProps = Omit<GradientMaskViewProps, 'maskOpacity'> & {
5
+ /**
6
+ * Mask effect intensity (0-1) as a Reanimated SharedValue
7
+ * 0 = no gradient effect (content fully visible)
8
+ * 1 = full gradient effect
9
+ */
10
+ maskOpacity: SharedValue<number>;
11
+ };
12
+ /**
13
+ * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件
14
+ *
15
+ * 使用方式:
16
+ * ```tsx
17
+ * const maskOpacity = useSharedValue(0);
18
+ *
19
+ * // 動畫顯示
20
+ * maskOpacity.value = withTiming(1, {
21
+ * duration: 600,
22
+ * easing: Easing.in(Easing.quad),
23
+ * });
24
+ *
25
+ * <AnimatedGradientMaskView
26
+ * colors={colors}
27
+ * locations={locations}
28
+ * direction="top"
29
+ * maskOpacity={maskOpacity}
30
+ * >
31
+ * <FlashList ... />
32
+ * </AnimatedGradientMaskView>
33
+ * ```
34
+ */
35
+ export default function AnimatedGradientMaskView(props: AnimatedGradientMaskViewProps): React.JSX.Element;
36
+ //# sourceMappingURL=AnimatedGradientMaskView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.d.ts","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAO7D,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAC9C,qBAAqB,EACrB,aAAa,CACd,GAAG;IACF;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,KAAK,EAAE,6BAA6B,qBASrC"}
@@ -0,0 +1,37 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+ import { useAnimatedProps } from 'react-native-reanimated';
4
+ import Animated from 'react-native-reanimated';
5
+ const NativeView = requireNativeView('GradientMask');
6
+ const AnimatedNativeView = Animated.createAnimatedComponent(NativeView);
7
+ /**
8
+ * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件
9
+ *
10
+ * 使用方式:
11
+ * ```tsx
12
+ * const maskOpacity = useSharedValue(0);
13
+ *
14
+ * // 動畫顯示
15
+ * maskOpacity.value = withTiming(1, {
16
+ * duration: 600,
17
+ * easing: Easing.in(Easing.quad),
18
+ * });
19
+ *
20
+ * <AnimatedGradientMaskView
21
+ * colors={colors}
22
+ * locations={locations}
23
+ * direction="top"
24
+ * maskOpacity={maskOpacity}
25
+ * >
26
+ * <FlashList ... />
27
+ * </AnimatedGradientMaskView>
28
+ * ```
29
+ */
30
+ export default function AnimatedGradientMaskView(props) {
31
+ const { maskOpacity, ...restProps } = props;
32
+ const animatedProps = useAnimatedProps(() => ({
33
+ maskOpacity: maskOpacity.value,
34
+ }));
35
+ return <AnimatedNativeView {...restProps} animatedProps={animatedProps}/>;
36
+ }
37
+ //# sourceMappingURL=AnimatedGradientMaskView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.js","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AAI/C,MAAM,UAAU,GACd,iBAAiB,CAAC,cAAc,CAAC,CAAC;AAEpC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;AAcxE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,KAAoC;IAEpC,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IAE5C,MAAM,aAAa,GAAG,gBAAgB,CAA6C,GAAG,EAAE,CAAC,CAAC;QACxF,WAAW,EAAE,WAAW,CAAC,KAAK;KAC/B,CAAC,CAAC,CAAC;IAEJ,OAAO,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC,CAAC,aAAa,CAAC,CAAC,aAAa,CAAC,EAAG,CAAC;AAC7E,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { useAnimatedProps } from 'react-native-reanimated';\nimport type { SharedValue } from 'react-native-reanimated';\nimport Animated from 'react-native-reanimated';\n\nimport { GradientMaskViewProps } from './GradientMask.types';\n\nconst NativeView: React.ComponentType<GradientMaskViewProps> =\n requireNativeView('GradientMask');\n\nconst AnimatedNativeView = Animated.createAnimatedComponent(NativeView);\n\nexport type AnimatedGradientMaskViewProps = Omit<\n GradientMaskViewProps,\n 'maskOpacity'\n> & {\n /**\n * Mask effect intensity (0-1) as a Reanimated SharedValue\n * 0 = no gradient effect (content fully visible)\n * 1 = full gradient effect\n */\n maskOpacity: SharedValue<number>;\n};\n\n/**\n * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件\n *\n * 使用方式:\n * ```tsx\n * const maskOpacity = useSharedValue(0);\n *\n * // 動畫顯示\n * maskOpacity.value = withTiming(1, {\n * duration: 600,\n * easing: Easing.in(Easing.quad),\n * });\n *\n * <AnimatedGradientMaskView\n * colors={colors}\n * locations={locations}\n * direction=\"top\"\n * maskOpacity={maskOpacity}\n * >\n * <FlashList ... />\n * </AnimatedGradientMaskView>\n * ```\n */\nexport default function AnimatedGradientMaskView(\n props: AnimatedGradientMaskViewProps\n) {\n const { maskOpacity, ...restProps } = props;\n\n const animatedProps = useAnimatedProps<Pick<GradientMaskViewProps, 'maskOpacity'>>(() => ({\n maskOpacity: maskOpacity.value,\n }));\n\n return <AnimatedNativeView {...restProps} animatedProps={animatedProps} />;\n}\n\n"]}
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import { GradientMaskViewProps } from './GradientMask.types';
4
+ export type AnimatedGradientMaskViewProps = Omit<GradientMaskViewProps, 'maskOpacity'> & {
5
+ /**
6
+ * Mask effect intensity (0-1) as a Reanimated SharedValue
7
+ * 0 = no gradient effect (content fully visible)
8
+ * 1 = full gradient effect
9
+ */
10
+ maskOpacity: SharedValue<number>;
11
+ };
12
+ /**
13
+ * AnimatedGradientMaskView - Web 實作
14
+ * 使用 CSS mask-image 搭配 useAnimatedReaction 監聽 SharedValue 變化
15
+ */
16
+ export default function AnimatedGradientMaskView(props: AnimatedGradientMaskViewProps): React.JSX.Element;
17
+ //# sourceMappingURL=AnimatedGradientMaskView.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.web.d.ts","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAC9C,qBAAqB,EACrB,aAAa,CACd,GAAG;IACF;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClC,CAAC;AAgFF;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,qBAyCpF"}
@@ -0,0 +1,100 @@
1
+ import * as React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { useAnimatedReaction, runOnJS } from 'react-native-reanimated';
4
+ /**
5
+ * 將 processColor 處理過的顏色值轉回 rgba 字串
6
+ */
7
+ function colorToRgba(color) {
8
+ if (color === null || color === undefined) {
9
+ return 'rgba(0, 0, 0, 0)';
10
+ }
11
+ const intValue = color >>> 0;
12
+ const a = ((intValue >> 24) & 0xff) / 255;
13
+ const r = (intValue >> 16) & 0xff;
14
+ const g = (intValue >> 8) & 0xff;
15
+ const b = intValue & 0xff;
16
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
17
+ }
18
+ /**
19
+ * 根據 direction 取得 CSS linear-gradient 的方向
20
+ */
21
+ function getGradientDirection(direction) {
22
+ switch (direction) {
23
+ case 'top':
24
+ return 'to bottom';
25
+ case 'bottom':
26
+ return 'to top';
27
+ case 'left':
28
+ return 'to right';
29
+ case 'right':
30
+ return 'to left';
31
+ default:
32
+ return 'to bottom';
33
+ }
34
+ }
35
+ /**
36
+ * 建立 CSS linear-gradient 字串
37
+ */
38
+ function buildGradientString(colors, locations, direction) {
39
+ const gradientDirection = getGradientDirection(direction);
40
+ const colorStops = colors.map((color, index) => {
41
+ const rgba = colorToRgba(color);
42
+ const location = locations[index] !== undefined ? locations[index] * 100 : (index / (colors.length - 1)) * 100;
43
+ return `${rgba} ${location}%`;
44
+ });
45
+ return `linear-gradient(${gradientDirection}, ${colorStops.join(', ')})`;
46
+ }
47
+ /**
48
+ * 根據 maskOpacity 調整顏色的 alpha 值
49
+ */
50
+ function adjustColorsForOpacity(colors, maskOpacity) {
51
+ if (maskOpacity >= 1)
52
+ return colors;
53
+ if (maskOpacity <= 0) {
54
+ return colors.map(() => 0xff000000);
55
+ }
56
+ return colors.map((color) => {
57
+ if (color === null || color === undefined)
58
+ return color;
59
+ const intValue = color >>> 0;
60
+ const a = ((intValue >> 24) & 0xff) / 255;
61
+ const r = (intValue >> 16) & 0xff;
62
+ const g = (intValue >> 8) & 0xff;
63
+ const b = intValue & 0xff;
64
+ const adjustedAlpha = a + (1 - a) * (1 - maskOpacity);
65
+ return ((Math.round(adjustedAlpha * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;
66
+ });
67
+ }
68
+ /**
69
+ * AnimatedGradientMaskView - Web 實作
70
+ * 使用 CSS mask-image 搭配 useAnimatedReaction 監聽 SharedValue 變化
71
+ */
72
+ export default function AnimatedGradientMaskView(props) {
73
+ const { colors, locations, direction = 'top', maskOpacity, style, children, } = props;
74
+ const [currentOpacity, setCurrentOpacity] = React.useState(maskOpacity.value);
75
+ // 監聽 SharedValue 變化並更新 state
76
+ useAnimatedReaction(() => maskOpacity.value, (value) => {
77
+ runOnJS(setCurrentOpacity)(value);
78
+ }, [maskOpacity]);
79
+ const maskStyle = React.useMemo(() => {
80
+ if (currentOpacity <= 0) {
81
+ return {};
82
+ }
83
+ const adjustedColors = adjustColorsForOpacity(colors, currentOpacity);
84
+ const gradientString = buildGradientString(adjustedColors, locations, direction);
85
+ return {
86
+ WebkitMaskImage: gradientString,
87
+ maskImage: gradientString,
88
+ transition: 'mask-image 0.1s ease-out, -webkit-mask-image 0.1s ease-out',
89
+ };
90
+ }, [colors, locations, direction, currentOpacity]);
91
+ return (<View style={[styles.container, style, maskStyle]}>
92
+ {children}
93
+ </View>);
94
+ }
95
+ const styles = StyleSheet.create({
96
+ container: {
97
+ overflow: 'hidden',
98
+ },
99
+ });
100
+ //# sourceMappingURL=AnimatedGradientMaskView.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.web.js","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from 'react';\nimport { View, StyleSheet } from 'react-native';\nimport { useAnimatedReaction, runOnJS } from 'react-native-reanimated';\nimport type { SharedValue } from 'react-native-reanimated';\n\nimport { GradientMaskViewProps } from './GradientMask.types';\n\nexport type AnimatedGradientMaskViewProps = Omit<\n GradientMaskViewProps,\n 'maskOpacity'\n> & {\n /**\n * Mask effect intensity (0-1) as a Reanimated SharedValue\n * 0 = no gradient effect (content fully visible)\n * 1 = full gradient effect\n */\n maskOpacity: SharedValue<number>;\n};\n\n/**\n * 將 processColor 處理過的顏色值轉回 rgba 字串\n */\nfunction colorToRgba(color: number | null): string {\n if (color === null || color === undefined) {\n return 'rgba(0, 0, 0, 0)';\n }\n\n const intValue = color >>> 0;\n const a = ((intValue >> 24) & 0xff) / 255;\n const r = (intValue >> 16) & 0xff;\n const g = (intValue >> 8) & 0xff;\n const b = intValue & 0xff;\n\n return `rgba(${r}, ${g}, ${b}, ${a})`;\n}\n\n/**\n * 根據 direction 取得 CSS linear-gradient 的方向\n */\nfunction getGradientDirection(direction: GradientMaskViewProps['direction']): string {\n switch (direction) {\n case 'top':\n return 'to bottom';\n case 'bottom':\n return 'to top';\n case 'left':\n return 'to right';\n case 'right':\n return 'to left';\n default:\n return 'to bottom';\n }\n}\n\n/**\n * 建立 CSS linear-gradient 字串\n */\nfunction buildGradientString(\n colors: (number | null)[],\n locations: number[],\n direction: GradientMaskViewProps['direction']\n): string {\n const gradientDirection = getGradientDirection(direction);\n\n const colorStops = colors.map((color, index) => {\n const rgba = colorToRgba(color);\n const location = locations[index] !== undefined ? locations[index] * 100 : (index / (colors.length - 1)) * 100;\n return `${rgba} ${location}%`;\n });\n\n return `linear-gradient(${gradientDirection}, ${colorStops.join(', ')})`;\n}\n\n/**\n * 根據 maskOpacity 調整顏色的 alpha 值\n */\nfunction adjustColorsForOpacity(\n colors: (number | null)[],\n maskOpacity: number\n): (number | null)[] {\n if (maskOpacity >= 1) return colors;\n if (maskOpacity <= 0) {\n return colors.map(() => 0xff000000);\n }\n\n return colors.map((color) => {\n if (color === null || color === undefined) return color;\n const intValue = color >>> 0;\n const a = ((intValue >> 24) & 0xff) / 255;\n const r = (intValue >> 16) & 0xff;\n const g = (intValue >> 8) & 0xff;\n const b = intValue & 0xff;\n const adjustedAlpha = a + (1 - a) * (1 - maskOpacity);\n return ((Math.round(adjustedAlpha * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;\n });\n}\n\n/**\n * AnimatedGradientMaskView - Web 實作\n * 使用 CSS mask-image 搭配 useAnimatedReaction 監聽 SharedValue 變化\n */\nexport default function AnimatedGradientMaskView(props: AnimatedGradientMaskViewProps) {\n const {\n colors,\n locations,\n direction = 'top',\n maskOpacity,\n style,\n children,\n } = props;\n\n const [currentOpacity, setCurrentOpacity] = React.useState(maskOpacity.value);\n\n // 監聽 SharedValue 變化並更新 state\n useAnimatedReaction(\n () => maskOpacity.value,\n (value) => {\n runOnJS(setCurrentOpacity)(value);\n },\n [maskOpacity]\n );\n\n const maskStyle = React.useMemo(() => {\n if (currentOpacity <= 0) {\n return {};\n }\n\n const adjustedColors = adjustColorsForOpacity(colors, currentOpacity);\n const gradientString = buildGradientString(adjustedColors, locations, direction);\n\n return {\n WebkitMaskImage: gradientString,\n maskImage: gradientString,\n transition: 'mask-image 0.1s ease-out, -webkit-mask-image 0.1s ease-out',\n };\n }, [colors, locations, direction, currentOpacity]);\n\n return (\n <View style={[styles.container, style, maskStyle as any]}>\n {children}\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n overflow: 'hidden',\n },\n});\n"]}
@@ -0,0 +1,38 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ export type GradientMaskViewProps = {
3
+ /**
4
+ * Gradient colors array (processed colors from processColor)
5
+ * Use alpha values to control opacity
6
+ * e.g., ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'] = transparent to opaque
7
+ */
8
+ colors: (number | null)[];
9
+ /**
10
+ * Position of each color (0-1)
11
+ * e.g., [0, 0.3, 1] means first color at 0%, second at 30%, third at 100%
12
+ */
13
+ locations: number[];
14
+ /**
15
+ * Gradient direction
16
+ * 'top' = top transparent, bottom opaque
17
+ * 'bottom' = bottom transparent, top opaque
18
+ * 'left' = left transparent, right opaque
19
+ * 'right' = right transparent, left opaque
20
+ */
21
+ direction?: 'top' | 'bottom' | 'left' | 'right';
22
+ /**
23
+ * Mask effect intensity (0-1)
24
+ * 0 = no gradient effect (content fully visible)
25
+ * 1 = full gradient effect
26
+ * @default 1
27
+ */
28
+ maskOpacity?: number;
29
+ /**
30
+ * Style
31
+ */
32
+ style?: StyleProp<ViewStyle>;
33
+ /**
34
+ * Children
35
+ */
36
+ children?: React.ReactNode;
37
+ };
38
+ //# sourceMappingURL=GradientMask.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMask.types.d.ts","sourceRoot":"","sources":["../src/GradientMask.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAE1B;;;OAGG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAEhD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=GradientMask.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMask.types.js","sourceRoot":"","sources":["../src/GradientMask.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type GradientMaskViewProps = {\n /**\n * Gradient colors array (processed colors from processColor)\n * Use alpha values to control opacity\n * e.g., ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'] = transparent to opaque\n */\n colors: (number | null)[];\n\n /**\n * Position of each color (0-1)\n * e.g., [0, 0.3, 1] means first color at 0%, second at 30%, third at 100%\n */\n locations: number[];\n\n /**\n * Gradient direction\n * 'top' = top transparent, bottom opaque\n * 'bottom' = bottom transparent, top opaque\n * 'left' = left transparent, right opaque\n * 'right' = right transparent, left opaque\n */\n direction?: 'top' | 'bottom' | 'left' | 'right';\n\n /**\n * Mask effect intensity (0-1)\n * 0 = no gradient effect (content fully visible)\n * 1 = full gradient effect\n * @default 1\n */\n maskOpacity?: number;\n\n /**\n * Style\n */\n style?: StyleProp<ViewStyle>;\n\n /**\n * Children\n */\n children?: React.ReactNode;\n};\n"]}
@@ -0,0 +1,3 @@
1
+ declare const _default: any;
2
+ export default _default;
3
+ //# sourceMappingURL=GradientMaskModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskModule.d.ts","sourceRoot":"","sources":["../src/GradientMaskModule.ts"],"names":[],"mappings":";AAGA,wBAAmD"}
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo';
2
+ // We only use the View, no module functions needed
3
+ export default requireNativeModule('GradientMask');
4
+ //# sourceMappingURL=GradientMaskModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskModule.js","sourceRoot":"","sources":["../src/GradientMaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAE3C,mDAAmD;AACnD,eAAe,mBAAmB,CAAC,cAAc,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo';\n\n// We only use the View, no module functions needed\nexport default requireNativeModule('GradientMask');\n"]}
@@ -0,0 +1,3 @@
1
+ declare const _default: {};
2
+ export default _default;
3
+ //# sourceMappingURL=GradientMaskModule.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskModule.web.d.ts","sourceRoot":"","sources":["../src/GradientMaskModule.web.ts"],"names":[],"mappings":";AACA,wBAAkB"}
@@ -0,0 +1,3 @@
1
+ // Web module stub - GradientMask is primarily for native platforms
2
+ export default {};
3
+ //# sourceMappingURL=GradientMaskModule.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskModule.web.js","sourceRoot":"","sources":["../src/GradientMaskModule.web.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,eAAe,EAAE,CAAC","sourcesContent":["// Web module stub - GradientMask is primarily for native platforms\nexport default {};\n"]}
@@ -0,0 +1,4 @@
1
+ import * as React from 'react';
2
+ import { GradientMaskViewProps } from './GradientMask.types';
3
+ export default function GradientMaskView(props: GradientMaskViewProps): React.JSX.Element;
4
+ //# sourceMappingURL=GradientMaskView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskView.d.ts","sourceRoot":"","sources":["../src/GradientMaskView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAK7D,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,qBAEpE"}
@@ -0,0 +1,7 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+ const NativeView = requireNativeView('GradientMask');
4
+ export default function GradientMaskView(props) {
5
+ return <NativeView {...props}/>;
6
+ }
7
+ //# sourceMappingURL=GradientMaskView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskView.js","sourceRoot":"","sources":["../src/GradientMaskView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,UAAU,GACd,iBAAiB,CAAC,cAAc,CAAC,CAAC;AAEpC,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAA4B;IACnE,OAAO,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACnC,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\n\nimport { GradientMaskViewProps } from './GradientMask.types';\n\nconst NativeView: React.ComponentType<GradientMaskViewProps> =\n requireNativeView('GradientMask');\n\nexport default function GradientMaskView(props: GradientMaskViewProps) {\n return <NativeView {...props} />;\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ import { GradientMaskViewProps } from './GradientMask.types';
3
+ /**
4
+ * GradientMaskView - Web 實作
5
+ * 使用 CSS mask-image 搭配 linear-gradient 實現漸層遮罩效果
6
+ */
7
+ export default function GradientMaskView(props: GradientMaskViewProps): React.JSX.Element;
8
+ //# sourceMappingURL=GradientMaskView.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientMaskView.web.d.ts","sourceRoot":"","sources":["../src/GradientMaskView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAqF7D;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,qBA8BpE"}
@@ -0,0 +1,99 @@
1
+ import * as React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ /**
4
+ * 將 processColor 處理過的顏色值轉回 rgba 字串
5
+ */
6
+ function colorToRgba(color) {
7
+ if (color === null || color === undefined) {
8
+ return 'rgba(0, 0, 0, 0)';
9
+ }
10
+ // processColor 在 web 上回傳的格式是 AARRGGBB (32-bit integer)
11
+ const intValue = color >>> 0; // 確保是 unsigned
12
+ const a = ((intValue >> 24) & 0xff) / 255;
13
+ const r = (intValue >> 16) & 0xff;
14
+ const g = (intValue >> 8) & 0xff;
15
+ const b = intValue & 0xff;
16
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
17
+ }
18
+ /**
19
+ * 根據 direction 取得 CSS linear-gradient 的方向
20
+ */
21
+ function getGradientDirection(direction) {
22
+ switch (direction) {
23
+ case 'top':
24
+ return 'to bottom'; // 頂部透明 → 底部不透明
25
+ case 'bottom':
26
+ return 'to top'; // 底部透明 → 頂部不透明
27
+ case 'left':
28
+ return 'to right'; // 左側透明 → 右側不透明
29
+ case 'right':
30
+ return 'to left'; // 右側透明 → 左側不透明
31
+ default:
32
+ return 'to bottom';
33
+ }
34
+ }
35
+ /**
36
+ * 建立 CSS linear-gradient 字串
37
+ */
38
+ function buildGradientString(colors, locations, direction) {
39
+ const gradientDirection = getGradientDirection(direction);
40
+ const colorStops = colors.map((color, index) => {
41
+ const rgba = colorToRgba(color);
42
+ const location = locations[index] !== undefined ? locations[index] * 100 : (index / (colors.length - 1)) * 100;
43
+ return `${rgba} ${location}%`;
44
+ });
45
+ return `linear-gradient(${gradientDirection}, ${colorStops.join(', ')})`;
46
+ }
47
+ /**
48
+ * 根據 maskOpacity 調整顏色的 alpha 值
49
+ * maskOpacity = 0 時,所有顏色變為完全不透明(無遮罩效果)
50
+ * maskOpacity = 1 時,保持原本的 alpha 值
51
+ */
52
+ function adjustColorsForOpacity(colors, maskOpacity) {
53
+ if (maskOpacity >= 1)
54
+ return colors;
55
+ if (maskOpacity <= 0) {
56
+ // 全部變成不透明黑色,表示內容完全可見
57
+ return colors.map(() => 0xff000000);
58
+ }
59
+ return colors.map((color) => {
60
+ if (color === null || color === undefined)
61
+ return color;
62
+ const intValue = color >>> 0;
63
+ const a = ((intValue >> 24) & 0xff) / 255;
64
+ const r = (intValue >> 16) & 0xff;
65
+ const g = (intValue >> 8) & 0xff;
66
+ const b = intValue & 0xff;
67
+ // 根據 maskOpacity 調整 alpha:當 maskOpacity = 0 時 alpha 趨近於 1(完全可見)
68
+ const adjustedAlpha = a + (1 - a) * (1 - maskOpacity);
69
+ return ((Math.round(adjustedAlpha * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;
70
+ });
71
+ }
72
+ /**
73
+ * GradientMaskView - Web 實作
74
+ * 使用 CSS mask-image 搭配 linear-gradient 實現漸層遮罩效果
75
+ */
76
+ export default function GradientMaskView(props) {
77
+ const { colors, locations, direction = 'top', maskOpacity = 1, style, children, } = props;
78
+ const maskStyle = React.useMemo(() => {
79
+ if (maskOpacity <= 0) {
80
+ // 無遮罩效果
81
+ return {};
82
+ }
83
+ const adjustedColors = adjustColorsForOpacity(colors, maskOpacity);
84
+ const gradientString = buildGradientString(adjustedColors, locations, direction);
85
+ return {
86
+ WebkitMaskImage: gradientString,
87
+ maskImage: gradientString,
88
+ };
89
+ }, [colors, locations, direction, maskOpacity]);
90
+ return (<View style={[styles.container, style, maskStyle]}>
91
+ {children}
92
+ </View>);
93
+ }
94
+ const styles = StyleSheet.create({
95
+ container: {
96
+ overflow: 'hidden',
97
+ },
98
+ });
99
+ //# sourceMappingURL=GradientMaskView.web.js.map