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.
- package/.eslintrc.js +5 -0
- package/PUBLISHING.md +171 -0
- package/README.md +360 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/gradientmask/GradientMaskModule.kt +33 -0
- package/android/src/main/java/expo/modules/gradientmask/GradientMaskView.kt +198 -0
- package/build/AnimatedGradientMaskView.d.ts +36 -0
- package/build/AnimatedGradientMaskView.d.ts.map +1 -0
- package/build/AnimatedGradientMaskView.js +37 -0
- package/build/AnimatedGradientMaskView.js.map +1 -0
- package/build/AnimatedGradientMaskView.web.d.ts +17 -0
- package/build/AnimatedGradientMaskView.web.d.ts.map +1 -0
- package/build/AnimatedGradientMaskView.web.js +100 -0
- package/build/AnimatedGradientMaskView.web.js.map +1 -0
- package/build/GradientMask.types.d.ts +38 -0
- package/build/GradientMask.types.d.ts.map +1 -0
- package/build/GradientMask.types.js +2 -0
- package/build/GradientMask.types.js.map +1 -0
- package/build/GradientMaskModule.d.ts +3 -0
- package/build/GradientMaskModule.d.ts.map +1 -0
- package/build/GradientMaskModule.js +4 -0
- package/build/GradientMaskModule.js.map +1 -0
- package/build/GradientMaskModule.web.d.ts +3 -0
- package/build/GradientMaskModule.web.d.ts.map +1 -0
- package/build/GradientMaskModule.web.js +3 -0
- package/build/GradientMaskModule.web.js.map +1 -0
- package/build/GradientMaskView.d.ts +4 -0
- package/build/GradientMaskView.d.ts.map +1 -0
- package/build/GradientMaskView.js +7 -0
- package/build/GradientMaskView.js.map +1 -0
- package/build/GradientMaskView.web.d.ts +8 -0
- package/build/GradientMaskView.web.d.ts.map +1 -0
- package/build/GradientMaskView.web.js +99 -0
- package/build/GradientMaskView.web.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/GradientMask.podspec +29 -0
- package/ios/GradientMaskModule.swift +29 -0
- package/ios/GradientMaskView.swift +133 -0
- package/package.json +83 -0
- package/rn-gradient-mask-design.md +657 -0
- package/src/AnimatedGradientMaskView.tsx +60 -0
- package/src/AnimatedGradientMaskView.web.tsx +149 -0
- package/src/GradientMask.types.ts +43 -0
- package/src/GradientMaskModule.ts +4 -0
- package/src/GradientMaskModule.web.ts +2 -0
- package/src/GradientMaskView.tsx +11 -0
- package/src/GradientMaskView.web.tsx +129 -0
- package/src/index.ts +7 -0
- 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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"GradientMaskModule.d.ts","sourceRoot":"","sources":["../src/GradientMaskModule.ts"],"names":[],"mappings":";AAGA,wBAAmD"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"GradientMaskModule.web.d.ts","sourceRoot":"","sources":["../src/GradientMaskModule.web.ts"],"names":[],"mappings":";AACA,wBAAkB"}
|
|
@@ -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 @@
|
|
|
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
|