react-native-smooth-clip-view 0.4.1 → 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 +92 -5
- package/android/src/main/cpp/SmoothClipAndroid.h +2 -1
- package/android/src/main/cpp/SmoothClipBindings.cpp +22 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -10
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +23 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +8 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +39 -1
- 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 +21 -10
- package/cpp/SmoothClipRegistry.h +2 -0
- package/cpp/SmoothClipSharedGeometry.h +20 -0
- package/cpp/SmoothClipVelocityTracker.h +6 -0
- package/ios/SmoothClipRegistry.mm +173 -17
- package/ios/SmoothClipTurboModule.cpp +17 -6
- package/ios/SmoothClipTurboModule.h +3 -1
- package/ios/SmoothClipView.mm +408 -20
- 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()
|
|
@@ -91,6 +91,14 @@ inline double timingFraction(
|
|
|
91
91
|
return clamp01((nowS - startedAtS) / durationS);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Springs clamp a long frame delta, so inactive wall time must remain visible
|
|
95
|
+
// to lastFrameS. Only the independent safety-cap origin moves forward.
|
|
96
|
+
inline double rebaseSpringStartAfterInactivity(
|
|
97
|
+
double startedAtS,
|
|
98
|
+
double inactiveDurationS) {
|
|
99
|
+
return startedAtS + std::max(0.0, inactiveDurationS);
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
// --- Timing curve --------------------------------------------------------
|
|
95
103
|
|
|
96
104
|
inline double cubicBezierParameterForX(
|
|
@@ -130,10 +138,10 @@ inline double cubicBezier(
|
|
|
130
138
|
|
|
131
139
|
// --- Presentation channels ------------------------------------------------
|
|
132
140
|
|
|
133
|
-
// The first
|
|
141
|
+
// The first thirteen channels are geometry/content/appearance; the final eight are shadow.
|
|
134
142
|
// Curve and shadow presence are categorical and stay outside the scalar array.
|
|
135
|
-
constexpr std::size_t kBaseChannelCount =
|
|
136
|
-
constexpr std::size_t kChannelCount =
|
|
143
|
+
constexpr std::size_t kBaseChannelCount = 13;
|
|
144
|
+
constexpr std::size_t kChannelCount = 21;
|
|
137
145
|
using Channels = std::array<double, kChannelCount>;
|
|
138
146
|
|
|
139
147
|
inline double resolvedRadius(double overrideValue, double shorthand) {
|
|
@@ -169,6 +177,8 @@ inline Channels toChannels(const Presentation &presentation) {
|
|
|
169
177
|
presentation.contentTranslateX,
|
|
170
178
|
presentation.contentTranslateY,
|
|
171
179
|
presentation.contentScale,
|
|
180
|
+
presentation.rotation,
|
|
181
|
+
presentation.opacity,
|
|
172
182
|
presentation.shadow.red,
|
|
173
183
|
presentation.shadow.green,
|
|
174
184
|
presentation.shadow.blue,
|
|
@@ -211,6 +221,7 @@ inline bool canonicalizePresentation(Presentation &presentation) {
|
|
|
211
221
|
CanonicalClip clip;
|
|
212
222
|
if (!SmoothClipCanonicalize(presentation.clip, clip)) return false;
|
|
213
223
|
presentation.clip = SmoothClipGeometry(clip);
|
|
224
|
+
presentation.opacity = clamp01(presentation.opacity);
|
|
214
225
|
return true;
|
|
215
226
|
}
|
|
216
227
|
|
|
@@ -436,16 +447,16 @@ inline Presentation fromChannels(
|
|
|
436
447
|
geometry.curve = curve;
|
|
437
448
|
Shadow shadow{
|
|
438
449
|
shadowEnabled,
|
|
439
|
-
std::clamp(channels[11], 0.0, 1.0),
|
|
440
|
-
std::clamp(channels[12], 0.0, 1.0),
|
|
441
450
|
std::clamp(channels[13], 0.0, 1.0),
|
|
442
451
|
std::clamp(channels[14], 0.0, 1.0),
|
|
443
|
-
channels[15],
|
|
444
|
-
channels[16],
|
|
445
|
-
|
|
446
|
-
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]};
|
|
447
458
|
return Presentation{
|
|
448
|
-
geometry, channels[8], channels[9], channels[10], shadow};
|
|
459
|
+
geometry, channels[8], channels[9], channels[10], shadow, channels[11], clamp01(channels[12])};
|
|
449
460
|
}
|
|
450
461
|
|
|
451
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 {
|