react-native-smooth-clip-view 0.4.2 → 0.4.3
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/CHANGELOG.md +44 -0
- package/README.md +74 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +1 -1
- package/android/src/main/cpp/SmoothClipBindings.cpp +14 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +6 -6
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +23 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +2 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +2 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +124 -32
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +16 -0
- package/cpp/SmoothClipAnimationCurve.h +13 -10
- package/cpp/SmoothClipRegistry.h +2 -0
- package/cpp/SmoothClipSharedGeometry.h +20 -0
- package/cpp/SmoothClipVelocityTracker.h +6 -0
- package/ios/SmoothClipTurboModule.cpp +17 -6
- package/ios/SmoothClipTurboModule.h +3 -1
- package/ios/SmoothClipView.mm +241 -22
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +3 -0
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +5 -1
- package/lib/module/geometry.js +22 -2
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/presentationCodec.js +5 -3
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +3 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/controllerTypes.d.ts +5 -1
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +9 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/NativeSmoothClipModule.ts +3 -1
- package/src/SmoothClipView.ios.tsx +3 -0
- package/src/SmoothClipViewNativeComponent.ts +5 -1
- package/src/controllerTypes.ts +5 -1
- package/src/geometry.ts +35 -0
- package/src/index.ts +1 -0
- package/src/presentationCodec.ts +8 -2
|
@@ -5,6 +5,7 @@ import android.graphics.BlurMaskFilter
|
|
|
5
5
|
import android.graphics.Canvas
|
|
6
6
|
import android.graphics.Color
|
|
7
7
|
import android.graphics.Outline
|
|
8
|
+
import android.graphics.Matrix
|
|
8
9
|
import android.graphics.Paint
|
|
9
10
|
import android.graphics.Path
|
|
10
11
|
import android.graphics.Region
|
|
@@ -20,6 +21,8 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
20
21
|
import kotlin.math.roundToInt
|
|
21
22
|
import kotlin.math.abs
|
|
22
23
|
import kotlin.math.pow
|
|
24
|
+
import kotlin.math.cos
|
|
25
|
+
import kotlin.math.sin
|
|
23
26
|
|
|
24
27
|
class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
25
28
|
private val clipPath = Path()
|
|
@@ -41,6 +44,16 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
// One compositing group; arbitrary React children may overlap when faded.
|
|
48
|
+
internal val presentationContainer = object : ReactViewGroup(context) {
|
|
49
|
+
override fun hasOverlappingRendering(): Boolean = true
|
|
50
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
51
|
+
drawBoxShadow(canvas)
|
|
52
|
+
super.dispatchDraw(canvas)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
private var requestedRotation = 0.0
|
|
56
|
+
private var requestedOpacity = 1f
|
|
44
57
|
internal val contentContainer = ReactViewGroup(context)
|
|
45
58
|
private var requestedX = 0f
|
|
46
59
|
private var requestedY = 0f
|
|
@@ -78,6 +91,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
78
91
|
private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
79
92
|
private var autonomousMotion = false
|
|
80
93
|
private var acceptingTouchStream = false
|
|
94
|
+
private var forwardingOverflowTouchStream = false
|
|
95
|
+
private val inversePresentationMatrix = Matrix()
|
|
96
|
+
private val touchPoint = FloatArray(2)
|
|
81
97
|
|
|
82
98
|
/** Driver this view is registered with in the native registry (0 = none). */
|
|
83
99
|
internal var boundDriverId: Double = 0.0
|
|
@@ -104,7 +120,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
104
120
|
|
|
105
121
|
init {
|
|
106
122
|
clipContainer.addView(contentContainer)
|
|
107
|
-
|
|
123
|
+
presentationContainer.addView(clipContainer)
|
|
124
|
+
super.addView(presentationContainer)
|
|
108
125
|
if (supportsPathOutlineClipping) {
|
|
109
126
|
clipContainer.outlineProvider = clipOutlineProvider
|
|
110
127
|
clipContainer.clipToOutline = true
|
|
@@ -139,6 +156,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
139
156
|
shadowOffsetY: Double = 0.0,
|
|
140
157
|
shadowBlurRadius: Double = 0.0,
|
|
141
158
|
shadowSpreadDistance: Double = 0.0,
|
|
159
|
+
rotation: Double = 0.0,
|
|
160
|
+
opacity: Double = 1.0,
|
|
142
161
|
) {
|
|
143
162
|
if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
|
|
144
163
|
!height.isFinite() || !topLeftRadius.isFinite() ||
|
|
@@ -149,7 +168,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
149
168
|
!shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
|
|
150
169
|
!shadowOffsetX.isFinite() ||
|
|
151
170
|
!shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
|
|
152
|
-
!shadowSpreadDistance.isFinite() || contentScale <= 0.0 ||
|
|
171
|
+
!shadowSpreadDistance.isFinite() || !rotation.isFinite() || !opacity.isFinite() || contentScale <= 0.0 ||
|
|
153
172
|
shadowRed !in 0.0..1.0 || shadowGreen !in 0.0..1.0 ||
|
|
154
173
|
shadowBlue !in 0.0..1.0 || shadowAlpha !in 0.0..1.0 ||
|
|
155
174
|
shadowBlurRadius < 0.0 ||
|
|
@@ -197,6 +216,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
197
216
|
requestedCurveCode = curveCode
|
|
198
217
|
requestedContentTranslateX = nextContentTranslateX
|
|
199
218
|
requestedContentTranslateY = nextContentTranslateY
|
|
219
|
+
requestedRotation = rotation
|
|
220
|
+
requestedOpacity = opacity.coerceIn(0.0, 1.0).toFloat()
|
|
200
221
|
requestedContentScale = nextContentScale
|
|
201
222
|
storeShadow(
|
|
202
223
|
shadowEnabled,
|
|
@@ -236,6 +257,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
236
257
|
shadowOffsetY: Float = 0f,
|
|
237
258
|
shadowBlurRadius: Float = 0f,
|
|
238
259
|
shadowSpreadDistance: Float = 0f,
|
|
260
|
+
rotation: Double = 0.0,
|
|
261
|
+
opacity: Float = 1f,
|
|
239
262
|
) {
|
|
240
263
|
if (!left.isFinite() || !top.isFinite() || !right.isFinite() ||
|
|
241
264
|
!bottom.isFinite() || !topLeftRadius.isFinite() ||
|
|
@@ -246,7 +269,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
246
269
|
!shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
|
|
247
270
|
!shadowOffsetX.isFinite() ||
|
|
248
271
|
!shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
|
|
249
|
-
!shadowSpreadDistance.isFinite() || contentScale <= 0f ||
|
|
272
|
+
!shadowSpreadDistance.isFinite() || !rotation.isFinite() || !opacity.isFinite() || contentScale <= 0f ||
|
|
250
273
|
shadowRed !in 0f..1f || shadowGreen !in 0f..1f ||
|
|
251
274
|
shadowBlue !in 0f..1f || shadowAlpha !in 0f..1f ||
|
|
252
275
|
shadowBlurRadius < 0f ||
|
|
@@ -259,6 +282,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
259
282
|
try {
|
|
260
283
|
requestedContentTranslateX = contentTranslateX
|
|
261
284
|
requestedContentTranslateY = contentTranslateY
|
|
285
|
+
requestedRotation = rotation
|
|
286
|
+
requestedOpacity = opacity.coerceIn(0f, 1f)
|
|
262
287
|
requestedContentScale = contentScale
|
|
263
288
|
storeShadow(
|
|
264
289
|
shadowEnabled,
|
|
@@ -342,14 +367,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
342
367
|
requestedShadowBlurRadius = blurRadius
|
|
343
368
|
requestedShadowSpreadDistance = spreadDistance
|
|
344
369
|
if (!isVisible) {
|
|
345
|
-
if (wasVisible) invalidate()
|
|
370
|
+
if (wasVisible) presentationContainer.invalidate()
|
|
346
371
|
return
|
|
347
372
|
}
|
|
348
373
|
if (paintChanged || boxShadowPaint == null) updateBoxShadowPaint()
|
|
349
374
|
if (pathChanged || !wasVisible || boxShadowPath == null) {
|
|
350
375
|
rebuildBoxShadowPath()
|
|
351
376
|
}
|
|
352
|
-
if (paintChanged || pathChanged || !wasVisible) invalidate()
|
|
377
|
+
if (paintChanged || pathChanged || !wasVisible) presentationContainer.invalidate()
|
|
353
378
|
}
|
|
354
379
|
|
|
355
380
|
private fun updateBoxShadowPaint() {
|
|
@@ -414,28 +439,31 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
414
439
|
val hostSaveCount = canvas.save()
|
|
415
440
|
canvas.clipRect(0f, 0f, width.toFloat(), height.toFloat())
|
|
416
441
|
try {
|
|
417
|
-
val shadowPath = boxShadowPath
|
|
418
|
-
val shadowPaint = boxShadowPaint
|
|
419
|
-
if (requestedShadowEnabled && !clipIsEmpty &&
|
|
420
|
-
requestedShadowAlpha > 0f && shadowPath != null &&
|
|
421
|
-
shadowPaint != null && !shadowPath.isEmpty
|
|
422
|
-
) {
|
|
423
|
-
val apertureSaveCount = canvas.save()
|
|
424
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
425
|
-
canvas.clipOutPath(clipPath)
|
|
426
|
-
} else {
|
|
427
|
-
@Suppress("DEPRECATION")
|
|
428
|
-
canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
|
|
429
|
-
}
|
|
430
|
-
canvas.drawPath(shadowPath, shadowPaint)
|
|
431
|
-
canvas.restoreToCount(apertureSaveCount)
|
|
432
|
-
}
|
|
433
442
|
super.dispatchDraw(canvas)
|
|
434
443
|
} finally {
|
|
435
444
|
canvas.restoreToCount(hostSaveCount)
|
|
436
445
|
}
|
|
437
446
|
}
|
|
438
447
|
|
|
448
|
+
private fun drawBoxShadow(canvas: Canvas) {
|
|
449
|
+
val shadowPath = boxShadowPath
|
|
450
|
+
val shadowPaint = boxShadowPaint
|
|
451
|
+
if (requestedShadowEnabled && !clipIsEmpty &&
|
|
452
|
+
requestedShadowAlpha > 0f && shadowPath != null &&
|
|
453
|
+
shadowPaint != null && !shadowPath.isEmpty
|
|
454
|
+
) {
|
|
455
|
+
val apertureSaveCount = canvas.save()
|
|
456
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
457
|
+
canvas.clipOutPath(clipPath)
|
|
458
|
+
} else {
|
|
459
|
+
@Suppress("DEPRECATION")
|
|
460
|
+
canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
|
|
461
|
+
}
|
|
462
|
+
canvas.drawPath(shadowPath, shadowPaint)
|
|
463
|
+
canvas.restoreToCount(apertureSaveCount)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
439
467
|
private fun applyCanonicalClipPx(
|
|
440
468
|
left: Float,
|
|
441
469
|
top: Float,
|
|
@@ -470,10 +498,12 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
470
498
|
applyContentTransform()
|
|
471
499
|
|
|
472
500
|
clipIsEmpty = isEmpty
|
|
501
|
+
applyPresentationTransform()
|
|
473
502
|
reapplyClipPresentation()
|
|
474
503
|
|
|
475
504
|
if (!geometryChanged) return
|
|
476
505
|
|
|
506
|
+
updateOverflowInsets()
|
|
477
507
|
clipPath.reset()
|
|
478
508
|
if (!isEmpty) {
|
|
479
509
|
if (curveCode == CLIP_CURVE_CIRCULAR && radiiAreUniform) {
|
|
@@ -507,7 +537,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
507
537
|
} else {
|
|
508
538
|
clipContainer.invalidate()
|
|
509
539
|
}
|
|
510
|
-
if (requestedShadowEnabled) invalidate()
|
|
540
|
+
if (requestedShadowEnabled) presentationContainer.invalidate()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private fun updateOverflowInsets() {
|
|
544
|
+
// React Native's touch-target walk also needs to see the unrotated overflow.
|
|
545
|
+
val left = kotlin.math.floor(minOf(0f, clipLeft).toDouble()).toInt()
|
|
546
|
+
val top = kotlin.math.floor(minOf(0f, clipTop).toDouble()).toInt()
|
|
547
|
+
val right = kotlin.math.floor(minOf(0f, width - clipRight).toDouble()).toInt()
|
|
548
|
+
val bottom = kotlin.math.floor(minOf(0f, height - clipBottom).toDouble()).toInt()
|
|
549
|
+
presentationContainer.setOverflowInset(left, top, right, bottom)
|
|
550
|
+
clipContainer.setOverflowInset(left, top, right, bottom)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private fun applyPresentationTransform() {
|
|
554
|
+
// The pivot is immaterial at zero rotation; avoid dirtying a stationary parent.
|
|
555
|
+
val cx = if (requestedRotation == 0.0) 0f else (clipLeft + clipRight) / 2f
|
|
556
|
+
val cy = if (requestedRotation == 0.0) 0f else (clipTop + clipBottom) / 2f
|
|
557
|
+
// Keep unwrapped doubles in the registry. Only the render transform is periodic.
|
|
558
|
+
val degrees = Math.toDegrees(requestedRotation % (2 * Math.PI)).toFloat()
|
|
559
|
+
if (presentationContainer.pivotX != cx) presentationContainer.pivotX = cx
|
|
560
|
+
if (presentationContainer.pivotY != cy) presentationContainer.pivotY = cy
|
|
561
|
+
if (presentationContainer.rotation != degrees) presentationContainer.rotation = degrees
|
|
562
|
+
if (presentationContainer.alpha != requestedOpacity) presentationContainer.alpha = requestedOpacity
|
|
511
563
|
}
|
|
512
564
|
|
|
513
565
|
private fun applyContentTransform() {
|
|
@@ -525,8 +577,10 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
525
577
|
|
|
526
578
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
527
579
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
580
|
+
presentationContainer.layout(0, 0, w, h)
|
|
528
581
|
clipContainer.layout(0, 0, w, h)
|
|
529
582
|
contentContainer.layout(0, 0, w, h)
|
|
583
|
+
updateOverflowInsets()
|
|
530
584
|
applyContentTransform()
|
|
531
585
|
if (boundDriverId != 0.0) {
|
|
532
586
|
// Host metrics only gate lifecycle readiness. Redelivery remains
|
|
@@ -602,7 +656,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
602
656
|
private fun containsRoundedPoint(x: Float, y: Float): Boolean {
|
|
603
657
|
// Reuse the exact path supplied to the Outline. Hit testing therefore
|
|
604
658
|
// follows the rendered aperture, including continuous corners.
|
|
605
|
-
|
|
659
|
+
if (clipIsEmpty || requestedOpacity <= 0f) return false
|
|
660
|
+
val cx = (clipLeft + clipRight) / 2.0
|
|
661
|
+
val cy = (clipTop + clipBottom) / 2.0
|
|
662
|
+
val c = cos(requestedRotation)
|
|
663
|
+
val sn = sin(requestedRotation)
|
|
664
|
+
val localX = cx + c * (x - cx) + sn * (y - cy)
|
|
665
|
+
val localY = cy - sn * (x - cx) + c * (y - cy)
|
|
666
|
+
return containsPathPoint(clipPath, localX.toFloat(), localY.toFloat())
|
|
606
667
|
}
|
|
607
668
|
|
|
608
669
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
@@ -611,12 +672,34 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
611
672
|
// The aperture gates admission only. Once accepted, the stream
|
|
612
673
|
// belongs to Android until its real UP or CANCEL arrives.
|
|
613
674
|
clearTouchState()
|
|
614
|
-
if (
|
|
675
|
+
if (event.x < 0 || event.y < 0 || event.x >= width || event.y >= height ||
|
|
676
|
+
!containsRoundedPoint(event.x, event.y)) return false
|
|
615
677
|
acceptingTouchStream = true
|
|
678
|
+
presentationContainer.matrix.invert(inversePresentationMatrix)
|
|
679
|
+
touchPoint[0] = event.x
|
|
680
|
+
touchPoint[1] = event.y
|
|
681
|
+
inversePresentationMatrix.mapPoints(touchPoint)
|
|
682
|
+
forwardingOverflowTouchStream = touchPoint[0] < 0 || touchPoint[1] < 0 ||
|
|
683
|
+
touchPoint[0] >= width || touchPoint[1] >= height
|
|
616
684
|
}
|
|
617
685
|
|
|
618
686
|
if (!acceptingTouchStream) return false
|
|
619
|
-
|
|
687
|
+
// ViewGroup's native dispatch rejects children outside their layout bounds,
|
|
688
|
+
// even when clipChildren=false. Bypass only the internal presentation wrapper
|
|
689
|
+
// for an overflow stream; the clip container still dispatches to transformed
|
|
690
|
+
// content normally. Retain this route through the real UP/CANCEL.
|
|
691
|
+
val result = if (forwardingOverflowTouchStream) {
|
|
692
|
+
presentationContainer.matrix.invert(inversePresentationMatrix)
|
|
693
|
+
val localEvent = MotionEvent.obtain(event)
|
|
694
|
+
try {
|
|
695
|
+
localEvent.transform(inversePresentationMatrix)
|
|
696
|
+
clipContainer.dispatchTouchEvent(localEvent)
|
|
697
|
+
} finally {
|
|
698
|
+
localEvent.recycle()
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
super.dispatchTouchEvent(event)
|
|
702
|
+
}
|
|
620
703
|
if ((action == MotionEvent.ACTION_DOWN && !result) ||
|
|
621
704
|
action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
|
|
622
705
|
) {
|
|
@@ -627,6 +710,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
627
710
|
|
|
628
711
|
private fun clearTouchState() {
|
|
629
712
|
acceptingTouchStream = false
|
|
713
|
+
forwardingOverflowTouchStream = false
|
|
630
714
|
}
|
|
631
715
|
|
|
632
716
|
fun setRequestedImportantForAccessibility(value: Int) {
|
|
@@ -644,14 +728,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
644
728
|
fun reapplyClipPresentation() {
|
|
645
729
|
val apertureVisible = apertureIntersectsHost()
|
|
646
730
|
val expectedVisibility = renderVisibility(
|
|
647
|
-
apertureVisible || shadowIntersectsHost(),
|
|
731
|
+
requestedOpacity > 0f && (apertureVisible || shadowIntersectsHost()),
|
|
648
732
|
)
|
|
649
733
|
if (visibility != expectedVisibility) {
|
|
650
734
|
visibility = expectedVisibility
|
|
651
735
|
}
|
|
652
736
|
|
|
653
737
|
val expectedAccessibility = clipAccessibility(
|
|
654
|
-
autonomousMotion || !apertureVisible,
|
|
738
|
+
autonomousMotion || !apertureVisible || requestedOpacity <= 0f,
|
|
655
739
|
requestedImportantForAccessibility,
|
|
656
740
|
)
|
|
657
741
|
if (importantForAccessibility != expectedAccessibility) {
|
|
@@ -660,8 +744,11 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
660
744
|
}
|
|
661
745
|
|
|
662
746
|
private fun apertureIntersectsHost(): Boolean =
|
|
663
|
-
!clipIsEmpty &&
|
|
664
|
-
clipLeft
|
|
747
|
+
!clipIsEmpty && rotatedRectIntersectsHost(
|
|
748
|
+
clipLeft.toDouble(), clipTop.toDouble(), clipRight.toDouble(), clipBottom.toDouble(),
|
|
749
|
+
(clipLeft + clipRight) / 2.0, (clipTop + clipBottom) / 2.0,
|
|
750
|
+
requestedRotation, width.toDouble(), height.toDouble(),
|
|
751
|
+
)
|
|
665
752
|
|
|
666
753
|
private fun shadowIntersectsHost(): Boolean {
|
|
667
754
|
if (clipIsEmpty || !requestedShadowEnabled || requestedShadowAlpha <= 0f) {
|
|
@@ -677,9 +764,12 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
677
764
|
// CSS blur is specified as a diameter-like radius. Expanding by the
|
|
678
765
|
// full value is conservative and prevents culling a faint blur tail.
|
|
679
766
|
val blurOutset = requestedShadowBlurRadius
|
|
680
|
-
return
|
|
681
|
-
pathLeft - blurOutset
|
|
682
|
-
|
|
767
|
+
return rotatedRectIntersectsHost(
|
|
768
|
+
(pathLeft - blurOutset).toDouble(), (pathTop - blurOutset).toDouble(),
|
|
769
|
+
(pathRight + blurOutset).toDouble(), (pathBottom + blurOutset).toDouble(),
|
|
770
|
+
(clipLeft + clipRight) / 2.0, (clipTop + clipBottom) / 2.0,
|
|
771
|
+
requestedRotation, width.toDouble(), height.toDouble(),
|
|
772
|
+
)
|
|
683
773
|
}
|
|
684
774
|
|
|
685
775
|
fun resetClipState() {
|
|
@@ -697,6 +787,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
697
787
|
requestedContentTranslateX = 0f
|
|
698
788
|
requestedContentTranslateY = 0f
|
|
699
789
|
requestedContentScale = 1f
|
|
790
|
+
requestedRotation = 0.0
|
|
791
|
+
requestedOpacity = 1f
|
|
700
792
|
requestedShadowEnabled = false
|
|
701
793
|
requestedShadowRed = 0f
|
|
702
794
|
requestedShadowGreen = 0f
|
|
@@ -37,6 +37,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
37
37
|
var shadowOffsetY: Double = 0.0,
|
|
38
38
|
var shadowBlurRadius: Double = 0.0,
|
|
39
39
|
var shadowSpreadDistance: Double = 0.0,
|
|
40
|
+
var rotation: Double = 0.0,
|
|
41
|
+
var opacity: Double = 1.0,
|
|
40
42
|
var driverId: Double = 0.0,
|
|
41
43
|
// Whether any prop setter ran in the current update transaction.
|
|
42
44
|
var dirty: Boolean = false,
|
|
@@ -173,6 +175,12 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
173
175
|
@ReactProp(name = "initialClipBoxShadowSpreadDistance")
|
|
174
176
|
override fun setInitialClipBoxShadowSpreadDistance(view: SmoothClipView, value: Double) { pending(view).shadowSpreadDistance = value }
|
|
175
177
|
|
|
178
|
+
@ReactProp(name = "initialRotation")
|
|
179
|
+
override fun setInitialRotation(view: SmoothClipView, value: Double) { pending(view).rotation = value }
|
|
180
|
+
|
|
181
|
+
@ReactProp(name = "initialOpacity", defaultDouble = 1.0)
|
|
182
|
+
override fun setInitialOpacity(view: SmoothClipView, value: Double) { pending(view).opacity = value }
|
|
183
|
+
|
|
176
184
|
override fun setClipPresentation(
|
|
177
185
|
view: SmoothClipView,
|
|
178
186
|
x: Double,
|
|
@@ -196,6 +204,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
196
204
|
shadowOffsetY: Double,
|
|
197
205
|
shadowBlurRadius: Double,
|
|
198
206
|
shadowSpreadDistance: Double,
|
|
207
|
+
rotation: Double,
|
|
208
|
+
opacity: Double,
|
|
199
209
|
) {
|
|
200
210
|
view.commandIsAuthoritative = true
|
|
201
211
|
view.setClipPresentationDip(
|
|
@@ -220,6 +230,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
220
230
|
shadowOffsetY,
|
|
221
231
|
shadowBlurRadius,
|
|
222
232
|
shadowSpreadDistance,
|
|
233
|
+
rotation,
|
|
234
|
+
opacity,
|
|
223
235
|
)
|
|
224
236
|
}
|
|
225
237
|
|
|
@@ -262,6 +274,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
262
274
|
initial.shadowOffsetY,
|
|
263
275
|
initial.shadowBlurRadius,
|
|
264
276
|
initial.shadowSpreadDistance,
|
|
277
|
+
initial.rotation,
|
|
278
|
+
initial.opacity,
|
|
265
279
|
view.densityScale(),
|
|
266
280
|
view.width.toDouble(),
|
|
267
281
|
view.height.toDouble(),
|
|
@@ -292,6 +306,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
292
306
|
initial.shadowOffsetY,
|
|
293
307
|
initial.shadowBlurRadius,
|
|
294
308
|
initial.shadowSpreadDistance,
|
|
309
|
+
initial.rotation,
|
|
310
|
+
initial.opacity,
|
|
295
311
|
)
|
|
296
312
|
} else {
|
|
297
313
|
view.reapplyClipPresentation()
|
|
@@ -138,10 +138,10 @@ inline double cubicBezier(
|
|
|
138
138
|
|
|
139
139
|
// --- Presentation channels ------------------------------------------------
|
|
140
140
|
|
|
141
|
-
// The first
|
|
141
|
+
// The first thirteen channels are geometry/content/appearance; the final eight are shadow.
|
|
142
142
|
// Curve and shadow presence are categorical and stay outside the scalar array.
|
|
143
|
-
constexpr std::size_t kBaseChannelCount =
|
|
144
|
-
constexpr std::size_t kChannelCount =
|
|
143
|
+
constexpr std::size_t kBaseChannelCount = 13;
|
|
144
|
+
constexpr std::size_t kChannelCount = 21;
|
|
145
145
|
using Channels = std::array<double, kChannelCount>;
|
|
146
146
|
|
|
147
147
|
inline double resolvedRadius(double overrideValue, double shorthand) {
|
|
@@ -177,6 +177,8 @@ inline Channels toChannels(const Presentation &presentation) {
|
|
|
177
177
|
presentation.contentTranslateX,
|
|
178
178
|
presentation.contentTranslateY,
|
|
179
179
|
presentation.contentScale,
|
|
180
|
+
presentation.rotation,
|
|
181
|
+
presentation.opacity,
|
|
180
182
|
presentation.shadow.red,
|
|
181
183
|
presentation.shadow.green,
|
|
182
184
|
presentation.shadow.blue,
|
|
@@ -219,6 +221,7 @@ inline bool canonicalizePresentation(Presentation &presentation) {
|
|
|
219
221
|
CanonicalClip clip;
|
|
220
222
|
if (!SmoothClipCanonicalize(presentation.clip, clip)) return false;
|
|
221
223
|
presentation.clip = SmoothClipGeometry(clip);
|
|
224
|
+
presentation.opacity = clamp01(presentation.opacity);
|
|
222
225
|
return true;
|
|
223
226
|
}
|
|
224
227
|
|
|
@@ -444,16 +447,16 @@ inline Presentation fromChannels(
|
|
|
444
447
|
geometry.curve = curve;
|
|
445
448
|
Shadow shadow{
|
|
446
449
|
shadowEnabled,
|
|
447
|
-
std::clamp(channels[11], 0.0, 1.0),
|
|
448
|
-
std::clamp(channels[12], 0.0, 1.0),
|
|
449
450
|
std::clamp(channels[13], 0.0, 1.0),
|
|
450
451
|
std::clamp(channels[14], 0.0, 1.0),
|
|
451
|
-
channels[15],
|
|
452
|
-
channels[16],
|
|
453
|
-
|
|
454
|
-
channels[18]
|
|
452
|
+
std::clamp(channels[15], 0.0, 1.0),
|
|
453
|
+
std::clamp(channels[16], 0.0, 1.0),
|
|
454
|
+
channels[17],
|
|
455
|
+
channels[18],
|
|
456
|
+
std::max(0.0, channels[19]),
|
|
457
|
+
channels[20]};
|
|
455
458
|
return Presentation{
|
|
456
|
-
geometry, channels[8], channels[9], channels[10], shadow};
|
|
459
|
+
geometry, channels[8], channels[9], channels[10], shadow, channels[11], clamp01(channels[12])};
|
|
457
460
|
}
|
|
458
461
|
|
|
459
462
|
inline Presentation interpolate(
|
package/cpp/SmoothClipRegistry.h
CHANGED
|
@@ -156,4 +156,24 @@ inline Geometry SmoothClipGeometry(const CanonicalClip &clip) {
|
|
|
156
156
|
return geometry;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Exact rectangle/host SAT; conservative for rounded corners, as before rotation.
|
|
160
|
+
inline bool rotatedRectIntersectsHost(double x, double y, double width, double height,
|
|
161
|
+
double rotation, double hostWidth, double hostHeight) {
|
|
162
|
+
if (width <= 0 || height <= 0 || hostWidth <= 0 || hostHeight <= 0) return false;
|
|
163
|
+
if (rotation == 0) return x + width > 0 && y + height > 0 && x < hostWidth && y < hostHeight;
|
|
164
|
+
const double c = std::cos(rotation), s = std::sin(rotation);
|
|
165
|
+
const double ac = std::abs(c), as = std::abs(s);
|
|
166
|
+
const double dx = x + width / 2 - hostWidth / 2;
|
|
167
|
+
const double dy = y + height / 2 - hostHeight / 2;
|
|
168
|
+
return std::abs(dx) < hostWidth / 2 + ac * width / 2 + as * height / 2 &&
|
|
169
|
+
std::abs(dy) < hostHeight / 2 + as * width / 2 + ac * height / 2 &&
|
|
170
|
+
std::abs(c * dx + s * dy) < width / 2 + ac * hostWidth / 2 + as * hostHeight / 2 &&
|
|
171
|
+
std::abs(-s * dx + c * dy) < height / 2 + as * hostWidth / 2 + ac * hostHeight / 2;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
inline double unwrapRotation(double principal, double reference) {
|
|
175
|
+
constexpr double turn = 6.28318530717958647692;
|
|
176
|
+
return principal + std::round((reference - principal) / turn) * turn;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
} // namespace smoothclip
|
|
@@ -11,6 +11,12 @@ namespace smoothclip {
|
|
|
11
11
|
// the four radius channels contributes one quarter of the uniform-radius weight.
|
|
12
12
|
// Both platforms record raw canonical geometry, including off-host coordinates.
|
|
13
13
|
// The projection only needs both samples and the target in the same space.
|
|
14
|
+
// Rotation, opacity, and shadow channels are deliberately absent: their units
|
|
15
|
+
// must not enter this geometry/translation projection. Appearance-only motion
|
|
16
|
+
// cannot supply inherited velocity. The resulting normalized scalar can still
|
|
17
|
+
// seed every changing animation channel, including rotation and opacity.
|
|
18
|
+
// This inheritance path is internal to standalone native animations; public
|
|
19
|
+
// controllers/groups use an explicit progress velocity, defaulting to zero.
|
|
14
20
|
// Shared by ios/SmoothClipView.mm and android/.../SmoothClipRegistry.cpp; a
|
|
15
21
|
// behavior change here changes both platforms.
|
|
16
22
|
struct VelocitySampleHistory {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
namespace facebook::react {
|
|
11
11
|
namespace {
|
|
12
12
|
|
|
13
|
-
constexpr size_t kPresentationStride =
|
|
13
|
+
constexpr size_t kPresentationStride = 23;
|
|
14
14
|
constexpr size_t kSnapshotStride = kPresentationStride + 1;
|
|
15
15
|
constexpr size_t kMotionEntryStride = kPresentationStride * 2 + 2;
|
|
16
16
|
|
|
@@ -65,7 +65,9 @@ smoothclip::Presentation makePresentation(
|
|
|
65
65
|
double shadowOffsetX = 0,
|
|
66
66
|
double shadowOffsetY = 0,
|
|
67
67
|
double shadowBlurRadius = 0,
|
|
68
|
-
double shadowSpreadDistance = 0
|
|
68
|
+
double shadowSpreadDistance = 0,
|
|
69
|
+
double rotation = 0,
|
|
70
|
+
double opacity = 1) {
|
|
69
71
|
const bool uniform = topLeftRadius == topRightRadius &&
|
|
70
72
|
topLeftRadius == bottomRightRadius &&
|
|
71
73
|
topLeftRadius == bottomLeftRadius;
|
|
@@ -82,7 +84,8 @@ smoothclip::Presentation makePresentation(
|
|
|
82
84
|
contentTranslateY,
|
|
83
85
|
contentScale,
|
|
84
86
|
{shadowEnabled, shadowRed, shadowGreen, shadowBlue, shadowAlpha,
|
|
85
|
-
shadowOffsetX, shadowOffsetY, shadowBlurRadius, shadowSpreadDistance}
|
|
87
|
+
shadowOffsetX, shadowOffsetY, shadowBlurRadius, shadowSpreadDistance},
|
|
88
|
+
rotation, opacity};
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
bool finitePresentation(const smoothclip::Presentation &presentation) {
|
|
@@ -97,6 +100,7 @@ bool finitePresentation(const smoothclip::Presentation &presentation) {
|
|
|
97
100
|
std::isfinite(presentation.contentTranslateY) &&
|
|
98
101
|
std::isfinite(presentation.contentScale) &&
|
|
99
102
|
presentation.contentScale > 0 &&
|
|
103
|
+
std::isfinite(presentation.rotation) && std::isfinite(presentation.opacity) &&
|
|
100
104
|
std::isfinite(presentation.shadow.red) &&
|
|
101
105
|
std::isfinite(presentation.shadow.green) &&
|
|
102
106
|
std::isfinite(presentation.shadow.blue) &&
|
|
@@ -141,7 +145,7 @@ bool presentationAt(
|
|
|
141
145
|
values[11],
|
|
142
146
|
values[12] == 1,
|
|
143
147
|
values[13], values[14], values[15], values[16],
|
|
144
|
-
values[17], values[18], values[19], values[20]);
|
|
148
|
+
values[17], values[18], values[19], values[20], values[21], values[22]);
|
|
145
149
|
return finitePresentation(result);
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -181,6 +185,8 @@ void writePresentation(
|
|
|
181
185
|
result.setValueAtIndex(runtime, offset + 18, presentation.shadow.offsetY);
|
|
182
186
|
result.setValueAtIndex(runtime, offset + 19, presentation.shadow.blurRadius);
|
|
183
187
|
result.setValueAtIndex(runtime, offset + 20, presentation.shadow.spreadDistance);
|
|
188
|
+
result.setValueAtIndex(runtime, offset + 21, presentation.rotation);
|
|
189
|
+
result.setValueAtIndex(runtime, offset + 22, presentation.opacity);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
jsi::Array snapshotArray(
|
|
@@ -226,6 +232,8 @@ std::vector<double> snapshotValues(
|
|
|
226
232
|
result.push_back(presentation.shadow.offsetY);
|
|
227
233
|
result.push_back(presentation.shadow.blurRadius);
|
|
228
234
|
result.push_back(presentation.shadow.spreadDistance);
|
|
235
|
+
result.push_back(presentation.rotation);
|
|
236
|
+
result.push_back(presentation.opacity);
|
|
229
237
|
}
|
|
230
238
|
return result;
|
|
231
239
|
}
|
|
@@ -504,13 +512,16 @@ void SmoothClipTurboModule::setClipFrameScalars(
|
|
|
504
512
|
double shadowOffsetX,
|
|
505
513
|
double shadowOffsetY,
|
|
506
514
|
double shadowBlurRadius,
|
|
507
|
-
double shadowSpreadDistance
|
|
515
|
+
double shadowSpreadDistance,
|
|
516
|
+
double rotation,
|
|
517
|
+
double opacity) {
|
|
508
518
|
const smoothclip::Presentation presentation = makePresentation(
|
|
509
519
|
x, y, width, height, topLeftRadius, topRightRadius,
|
|
510
520
|
bottomRightRadius, bottomLeftRadius, curveCode,
|
|
511
521
|
contentTranslateX, contentTranslateY, contentScale,
|
|
512
522
|
shadowEnabled, shadowRed, shadowGreen, shadowBlue, shadowAlpha,
|
|
513
|
-
shadowOffsetX, shadowOffsetY, shadowBlurRadius, shadowSpreadDistance
|
|
523
|
+
shadowOffsetX, shadowOffsetY, shadowBlurRadius, shadowSpreadDistance,
|
|
524
|
+
rotation, opacity);
|
|
514
525
|
if (!validDriverId(driverId) || !finitePresentation(presentation)) return;
|
|
515
526
|
smoothclip::setPresentation(
|
|
516
527
|
static_cast<uint64_t>(driverId), presentation, true, false, true);
|
|
@@ -160,7 +160,9 @@ class SmoothClipTurboModule final
|
|
|
160
160
|
double shadowOffsetX,
|
|
161
161
|
double shadowOffsetY,
|
|
162
162
|
double shadowBlurRadius,
|
|
163
|
-
double shadowSpreadDistance
|
|
163
|
+
double shadowSpreadDistance,
|
|
164
|
+
double rotation,
|
|
165
|
+
double opacity);
|
|
164
166
|
jsi::Array beginInteraction(jsi::Runtime &runtime, double driverId);
|
|
165
167
|
jsi::Array snapshotCurrent(jsi::Runtime &runtime, double driverId);
|
|
166
168
|
|