react-native-theme-reveal 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +734 -2
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +4 -0
- package/android/src/main/java/expo/modules/themereveal/CapturePlan.kt +199 -0
- package/android/src/main/java/expo/modules/themereveal/CaptureRules.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/GpuSnapshot.kt +367 -0
- package/android/src/main/java/expo/modules/themereveal/KeptBitmaps.kt +102 -0
- package/android/src/main/java/expo/modules/themereveal/KeptTarget.kt +103 -0
- package/android/src/main/java/expo/modules/themereveal/Motion.kt +79 -0
- package/android/src/main/java/expo/modules/themereveal/Once.kt +13 -0
- package/android/src/main/java/expo/modules/themereveal/RenderThreadReveal.kt +107 -0
- package/android/src/main/java/expo/modules/themereveal/RevealOverlayView.kt +192 -0
- package/android/src/main/java/expo/modules/themereveal/SecondSnapshotWait.kt +112 -0
- package/android/src/main/java/expo/modules/themereveal/ShapePath.kt +47 -0
- package/android/src/main/java/expo/modules/themereveal/ThemeRevealModule.kt +904 -0
- package/android/src/main/java/expo/modules/themereveal/TransitionArgs.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/WindowSnapshot.kt +536 -0
- package/build/ThemeRevealModule.d.ts +11 -0
- package/build/ThemeRevealModule.d.ts.map +1 -0
- package/build/ThemeRevealModule.js +11 -0
- package/build/ThemeRevealModule.js.map +1 -0
- package/build/createThemeReveal.d.ts +39 -0
- package/build/createThemeReveal.d.ts.map +1 -0
- package/build/createThemeReveal.js +186 -0
- package/build/createThemeReveal.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/originFromPress.d.ts +12 -0
- package/build/originFromPress.d.ts.map +1 -0
- package/build/originFromPress.js +26 -0
- package/build/originFromPress.js.map +1 -0
- package/build/resolveTransition.d.ts +13 -0
- package/build/resolveTransition.d.ts.map +1 -0
- package/build/resolveTransition.js +43 -0
- package/build/resolveTransition.js.map +1 -0
- package/build/store.d.ts +8 -0
- package/build/store.d.ts.map +1 -0
- package/build/store.js +23 -0
- package/build/store.js.map +1 -0
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Motion.swift +171 -0
- package/ios/RevealOverlay.swift +115 -0
- package/ios/SVGPath.swift +373 -0
- package/ios/ThemeReveal.podspec +26 -0
- package/ios/ThemeRevealModule.swift +264 -0
- package/mocks/ThemeReveal.js +10 -0
- package/package.json +67 -4
- package/src/ThemeRevealModule.ts +24 -0
- package/src/createThemeReveal.ts +262 -0
- package/src/index.ts +14 -0
- package/src/originFromPress.ts +40 -0
- package/src/resolveTransition.ts +63 -0
- package/src/store.ts +30 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// Window snapshot on top of the app content, animated away to reveal the new theme.
|
|
4
|
+
final class RevealOverlay {
|
|
5
|
+
let view: UIView
|
|
6
|
+
let size: CGSize
|
|
7
|
+
private var playing = 0
|
|
8
|
+
|
|
9
|
+
convenience init?(window: UIWindow) {
|
|
10
|
+
guard let snapshot = Self.snapshot(of: window) else { return nil }
|
|
11
|
+
self.init(snapshot: snapshot, window: window)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Puts a snapshot from snapshot(of:) on top of the window.
|
|
15
|
+
init(snapshot: UIView, window: UIWindow) {
|
|
16
|
+
window.addSubview(snapshot)
|
|
17
|
+
view = snapshot
|
|
18
|
+
size = snapshot.frame.size
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Window's content right now, as a view that isn't in the window yet.
|
|
22
|
+
static func snapshot(of window: UIWindow) -> UIView? {
|
|
23
|
+
guard let snapshot = window.snapshotView(afterScreenUpdates: false) else { return nil }
|
|
24
|
+
snapshot.frame = window.bounds
|
|
25
|
+
snapshot.isUserInteractionEnabled = false
|
|
26
|
+
snapshot.isAccessibilityElement = false
|
|
27
|
+
snapshot.accessibilityElementsHidden = true
|
|
28
|
+
return snapshot
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func reveal(_ shape: RevealShape, origin: CGPoint, inverse: Bool, duration: Double, onEnd: @escaping () -> Void) {
|
|
32
|
+
let bounds = view.bounds
|
|
33
|
+
let from = Double(Motion.startScale(for: shape))
|
|
34
|
+
let to = Double(Motion.coverScale(
|
|
35
|
+
originX: origin.x, originY: origin.y, width: bounds.width, height: bounds.height,
|
|
36
|
+
inscribedRadius: shape.inscribedRadius))
|
|
37
|
+
let steps = Motion.sampleCount(duration: duration)
|
|
38
|
+
let paths = Motion.scales(from: from, to: to, steps: steps, inverse: inverse).map { scale in
|
|
39
|
+
Motion.maskPath(shape, origin: origin, scale: CGFloat(scale), bounds: bounds, inverse: inverse)
|
|
40
|
+
}
|
|
41
|
+
play(onEnd) {
|
|
42
|
+
let mask = CAShapeLayer()
|
|
43
|
+
mask.frame = bounds
|
|
44
|
+
mask.fillRule = inverse ? .nonZero : .evenOdd
|
|
45
|
+
mask.path = paths.last
|
|
46
|
+
view.layer.mask = mask
|
|
47
|
+
let animation = CAKeyframeAnimation(keyPath: "path")
|
|
48
|
+
animation.values = paths
|
|
49
|
+
animation.calculationMode = .linear
|
|
50
|
+
animation.duration = duration
|
|
51
|
+
mask.add(animation, forKey: "reveal")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func wipe(_ direction: WipeDirection, duration: Double, onEnd: @escaping () -> Void) {
|
|
56
|
+
let bounds = view.bounds
|
|
57
|
+
let offset = Motion.wipeOffset(direction, size: bounds.size)
|
|
58
|
+
play(onEnd) {
|
|
59
|
+
let mask = CALayer()
|
|
60
|
+
mask.frame = bounds
|
|
61
|
+
mask.backgroundColor = UIColor.black.cgColor
|
|
62
|
+
let start = mask.position
|
|
63
|
+
let end = CGPoint(x: start.x + offset.x, y: start.y + offset.y)
|
|
64
|
+
mask.position = end
|
|
65
|
+
view.layer.mask = mask
|
|
66
|
+
let animation = CABasicAnimation(keyPath: "position")
|
|
67
|
+
animation.fromValue = NSValue(cgPoint: start)
|
|
68
|
+
animation.toValue = NSValue(cgPoint: end)
|
|
69
|
+
animation.duration = duration
|
|
70
|
+
animation.timingFunction = Self.easing
|
|
71
|
+
mask.add(animation, forKey: "wipe")
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func crossfade(duration: Double, onEnd: @escaping () -> Void) {
|
|
76
|
+
play(onEnd) {
|
|
77
|
+
view.layer.opacity = 0
|
|
78
|
+
let animation = CABasicAnimation(keyPath: "opacity")
|
|
79
|
+
animation.fromValue = 1
|
|
80
|
+
animation.toValue = 0
|
|
81
|
+
animation.duration = duration
|
|
82
|
+
animation.timingFunction = Self.easing
|
|
83
|
+
view.layer.add(animation, forKey: "crossfade")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func remove() {
|
|
88
|
+
playing += 1
|
|
89
|
+
view.layer.mask?.removeAllAnimations()
|
|
90
|
+
view.layer.removeAllAnimations()
|
|
91
|
+
view.removeFromSuperview()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Calls onEnd when the animation added in body ends. An earlier animation that body
|
|
95
|
+
// replaces never calls its onEnd.
|
|
96
|
+
private func play(_ onEnd: @escaping () -> Void, _ body: () -> Void) {
|
|
97
|
+
playing += 1
|
|
98
|
+
let ticket = playing
|
|
99
|
+
view.layer.mask?.removeAllAnimations()
|
|
100
|
+
view.layer.removeAllAnimations()
|
|
101
|
+
view.layer.mask = nil
|
|
102
|
+
view.layer.opacity = 1
|
|
103
|
+
CATransaction.begin()
|
|
104
|
+
CATransaction.setDisableActions(true)
|
|
105
|
+
CATransaction.setCompletionBlock { [weak self] in
|
|
106
|
+
guard let self, self.playing == ticket else { return }
|
|
107
|
+
onEnd()
|
|
108
|
+
}
|
|
109
|
+
body()
|
|
110
|
+
CATransaction.commit()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static let easing = CAMediaTimingFunction(
|
|
114
|
+
controlPoints: Float(Motion.easeX1), Float(Motion.easeY1), Float(Motion.easeX2), Float(Motion.easeY2))
|
|
115
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Parses SVG path data (the `d` attribute) into a `CGPath`.
|
|
5
|
+
///
|
|
6
|
+
/// Supports `M L H V C S Q T A Z` in absolute and relative forms, implicit
|
|
7
|
+
/// repeated commands, and the SVG number syntax (`1e-3`, `.5.5`, `-1-2`).
|
|
8
|
+
/// Returns `nil` for malformed input.
|
|
9
|
+
enum SVGPath {
|
|
10
|
+
static func parse(_ d: String) -> CGPath? {
|
|
11
|
+
var parser = SVGPathParser(Array(d.utf8))
|
|
12
|
+
return parser.parse()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private struct SVGPathParser {
|
|
17
|
+
private let bytes: [UInt8]
|
|
18
|
+
private var index = 0
|
|
19
|
+
private let path = CGMutablePath()
|
|
20
|
+
private var current = CGPoint.zero
|
|
21
|
+
private var subpathStart = CGPoint.zero
|
|
22
|
+
private var lastCubicControl: CGPoint?
|
|
23
|
+
private var lastQuadControl: CGPoint?
|
|
24
|
+
private var needsMove = false
|
|
25
|
+
|
|
26
|
+
init(_ bytes: [UInt8]) {
|
|
27
|
+
self.bytes = bytes
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
mutating func parse() -> CGPath? {
|
|
31
|
+
skipSeparators()
|
|
32
|
+
guard let first = peek(), first == ascii("M") || first == ascii("m") else { return nil }
|
|
33
|
+
|
|
34
|
+
var command = first
|
|
35
|
+
while true {
|
|
36
|
+
skipSeparators()
|
|
37
|
+
guard let byte = peek() else { break }
|
|
38
|
+
if Self.commands.contains(byte) {
|
|
39
|
+
index += 1
|
|
40
|
+
command = byte
|
|
41
|
+
} else if isNumberStart(byte) {
|
|
42
|
+
// A number with no command letter repeats the previous command. Extra coordinate
|
|
43
|
+
// pairs after M or m are L or l.
|
|
44
|
+
switch command {
|
|
45
|
+
case ascii("M"): command = ascii("L")
|
|
46
|
+
case ascii("m"): command = ascii("l")
|
|
47
|
+
case ascii("Z"), ascii("z"): return nil
|
|
48
|
+
default: break
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
guard execute(command) else { return nil }
|
|
54
|
+
}
|
|
55
|
+
return path.copy()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Commands
|
|
59
|
+
|
|
60
|
+
private static let commands = Set("MmLlHhVvCcSsQqTtAaZz".utf8)
|
|
61
|
+
|
|
62
|
+
private mutating func execute(_ command: UInt8) -> Bool {
|
|
63
|
+
let relative = command >= ascii("a")
|
|
64
|
+
let origin = relative ? current : .zero
|
|
65
|
+
let previousCubicControl = lastCubicControl
|
|
66
|
+
let previousQuadControl = lastQuadControl
|
|
67
|
+
lastCubicControl = nil
|
|
68
|
+
lastQuadControl = nil
|
|
69
|
+
|
|
70
|
+
switch command | 0x20 {
|
|
71
|
+
case ascii("m"):
|
|
72
|
+
guard let point = point(from: origin) else { return false }
|
|
73
|
+
path.move(to: point)
|
|
74
|
+
subpathStart = point
|
|
75
|
+
current = point
|
|
76
|
+
needsMove = false
|
|
77
|
+
|
|
78
|
+
case ascii("l"):
|
|
79
|
+
guard let point = point(from: origin) else { return false }
|
|
80
|
+
line(to: point)
|
|
81
|
+
|
|
82
|
+
case ascii("h"):
|
|
83
|
+
guard let x = number(), isFinite(origin.x + x) else { return false }
|
|
84
|
+
line(to: CGPoint(x: origin.x + x, y: current.y))
|
|
85
|
+
|
|
86
|
+
case ascii("v"):
|
|
87
|
+
guard let y = number(), isFinite(origin.y + y) else { return false }
|
|
88
|
+
line(to: CGPoint(x: current.x, y: origin.y + y))
|
|
89
|
+
|
|
90
|
+
case ascii("c"):
|
|
91
|
+
guard let control1 = point(from: origin),
|
|
92
|
+
let control2 = point(from: origin),
|
|
93
|
+
let end = point(from: origin)
|
|
94
|
+
else { return false }
|
|
95
|
+
cubic(to: end, control1: control1, control2: control2)
|
|
96
|
+
|
|
97
|
+
case ascii("s"):
|
|
98
|
+
guard let control2 = point(from: origin), let end = point(from: origin) else { return false }
|
|
99
|
+
let control1 = previousCubicControl.map(reflect) ?? current
|
|
100
|
+
guard isFinite(control1) else { return false }
|
|
101
|
+
cubic(to: end, control1: control1, control2: control2)
|
|
102
|
+
|
|
103
|
+
case ascii("q"):
|
|
104
|
+
guard let control = point(from: origin), let end = point(from: origin) else { return false }
|
|
105
|
+
quad(to: end, control: control)
|
|
106
|
+
|
|
107
|
+
case ascii("t"):
|
|
108
|
+
guard let end = point(from: origin) else { return false }
|
|
109
|
+
let control = previousQuadControl.map(reflect) ?? current
|
|
110
|
+
guard isFinite(control) else { return false }
|
|
111
|
+
quad(to: end, control: control)
|
|
112
|
+
|
|
113
|
+
case ascii("a"):
|
|
114
|
+
guard let rx = number(),
|
|
115
|
+
let ry = number(),
|
|
116
|
+
let rotation = number(),
|
|
117
|
+
let largeArc = flag(),
|
|
118
|
+
let sweep = flag(),
|
|
119
|
+
let end = point(from: origin)
|
|
120
|
+
else { return false }
|
|
121
|
+
guard arc(to: end, rx: rx, ry: ry, rotation: rotation, largeArc: largeArc, sweep: sweep) else {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case ascii("z"):
|
|
126
|
+
path.closeSubpath()
|
|
127
|
+
current = subpathStart
|
|
128
|
+
needsMove = true
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// After Z, the next drawing command starts at the closed subpath's start point.
|
|
137
|
+
private mutating func beginSegment() {
|
|
138
|
+
if needsMove {
|
|
139
|
+
path.move(to: current)
|
|
140
|
+
needsMove = false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private mutating func line(to point: CGPoint) {
|
|
145
|
+
beginSegment()
|
|
146
|
+
path.addLine(to: point)
|
|
147
|
+
current = point
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private mutating func cubic(to end: CGPoint, control1: CGPoint, control2: CGPoint) {
|
|
151
|
+
beginSegment()
|
|
152
|
+
path.addCurve(to: end, control1: control1, control2: control2)
|
|
153
|
+
lastCubicControl = control2
|
|
154
|
+
current = end
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private mutating func quad(to end: CGPoint, control: CGPoint) {
|
|
158
|
+
beginSegment()
|
|
159
|
+
path.addQuadCurve(to: end, control: control)
|
|
160
|
+
lastQuadControl = control
|
|
161
|
+
current = end
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func reflect(_ control: CGPoint) -> CGPoint {
|
|
165
|
+
CGPoint(x: 2 * current.x - control.x, y: 2 * current.y - control.y)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Elliptical arc per the SVG 1.1 implementation notes: endpoint to centre conversion
|
|
169
|
+
/// (F.6.5) with out-of-range radii correction (F.6.6), drawn as cubic segments of at most
|
|
170
|
+
/// 90 degrees each. Returns false if the arithmetic leaves the finite range, so the parse
|
|
171
|
+
/// fails instead of trapping.
|
|
172
|
+
private mutating func arc(
|
|
173
|
+
to end: CGPoint, rx: CGFloat, ry: CGFloat, rotation: CGFloat, largeArc: Bool, sweep: Bool
|
|
174
|
+
) -> Bool {
|
|
175
|
+
let start = current
|
|
176
|
+
guard isFinite(start), isFinite(end) else { return false }
|
|
177
|
+
guard start != end else { return true }
|
|
178
|
+
var rx = abs(rx)
|
|
179
|
+
var ry = abs(ry)
|
|
180
|
+
guard rx > 0, ry > 0 else {
|
|
181
|
+
line(to: end)
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let phi = rotation * .pi / 180
|
|
186
|
+
let cosPhi = cos(phi)
|
|
187
|
+
let sinPhi = sin(phi)
|
|
188
|
+
|
|
189
|
+
// F.6.5.1: the start point in the ellipse's rotated frame.
|
|
190
|
+
let halfDx = (start.x - end.x) / 2
|
|
191
|
+
let halfDy = (start.y - end.y) / 2
|
|
192
|
+
let x1 = cosPhi * halfDx + sinPhi * halfDy
|
|
193
|
+
let y1 = -sinPhi * halfDx + cosPhi * halfDy
|
|
194
|
+
|
|
195
|
+
// F.6.6: scale the radii up if they cannot span the end points.
|
|
196
|
+
let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry)
|
|
197
|
+
if lambda > 1 {
|
|
198
|
+
let scale = lambda.squareRoot()
|
|
199
|
+
rx *= scale
|
|
200
|
+
ry *= scale
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// F.6.5.2: the centre in the rotated frame.
|
|
204
|
+
let rx2 = rx * rx
|
|
205
|
+
let ry2 = ry * ry
|
|
206
|
+
let denominator = rx2 * y1 * y1 + ry2 * x1 * x1
|
|
207
|
+
let ratio = (rx2 * ry2 - denominator) / denominator
|
|
208
|
+
// End points too close for these radii to place a centre, so draw a line.
|
|
209
|
+
guard denominator != 0, ratio != .infinity else {
|
|
210
|
+
line(to: end)
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
var coefficient = max(0, ratio).squareRoot()
|
|
214
|
+
if largeArc == sweep {
|
|
215
|
+
coefficient = -coefficient
|
|
216
|
+
}
|
|
217
|
+
let centreX1 = coefficient * rx * y1 / ry
|
|
218
|
+
let centreY1 = -coefficient * ry * x1 / rx
|
|
219
|
+
|
|
220
|
+
// F.6.5.3: the centre in user space.
|
|
221
|
+
let centreX = cosPhi * centreX1 - sinPhi * centreY1 + (start.x + end.x) / 2
|
|
222
|
+
let centreY = sinPhi * centreX1 + cosPhi * centreY1 + (start.y + end.y) / 2
|
|
223
|
+
|
|
224
|
+
// F.6.5.5 and F.6.5.6: the start angle and the sweep angle.
|
|
225
|
+
let startAngle = atan2((y1 - centreY1) / ry, (x1 - centreX1) / rx)
|
|
226
|
+
let endAngle = atan2((-y1 - centreY1) / ry, (-x1 - centreX1) / rx)
|
|
227
|
+
var sweepAngle = endAngle - startAngle
|
|
228
|
+
if sweep, sweepAngle < 0 {
|
|
229
|
+
sweepAngle += 2 * .pi
|
|
230
|
+
} else if !sweep, sweepAngle > 0 {
|
|
231
|
+
sweepAngle -= 2 * .pi
|
|
232
|
+
}
|
|
233
|
+
guard centreX.isFinite, centreY.isFinite, startAngle.isFinite, sweepAngle.isFinite else {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func onEllipse(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
|
|
238
|
+
CGPoint(
|
|
239
|
+
x: centreX + rx * cosPhi * x - ry * sinPhi * y,
|
|
240
|
+
y: centreY + rx * sinPhi * x + ry * cosPhi * y
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let segments = max(1, Int((abs(sweepAngle) / (.pi / 2) - 1e-7).rounded(.up)))
|
|
245
|
+
let step = sweepAngle / CGFloat(segments)
|
|
246
|
+
let handle = 4 / 3 * tan(step / 4)
|
|
247
|
+
var angle = startAngle
|
|
248
|
+
var curves: [(point: CGPoint, control1: CGPoint, control2: CGPoint)] = []
|
|
249
|
+
for segment in 0..<segments {
|
|
250
|
+
let next = angle + step
|
|
251
|
+
let cos1 = cos(angle)
|
|
252
|
+
let sin1 = sin(angle)
|
|
253
|
+
let cos2 = cos(next)
|
|
254
|
+
let sin2 = sin(next)
|
|
255
|
+
let control1 = onEllipse(cos1 - handle * sin1, sin1 + handle * cos1)
|
|
256
|
+
let control2 = onEllipse(cos2 + handle * sin2, sin2 - handle * cos2)
|
|
257
|
+
let point = segment == segments - 1 ? end : onEllipse(cos2, sin2)
|
|
258
|
+
guard isFinite(control1), isFinite(control2), isFinite(point) else { return false }
|
|
259
|
+
curves.append((point, control1, control2))
|
|
260
|
+
angle = next
|
|
261
|
+
}
|
|
262
|
+
beginSegment()
|
|
263
|
+
for curve in curves {
|
|
264
|
+
path.addCurve(to: curve.point, control1: curve.control1, control2: curve.control2)
|
|
265
|
+
}
|
|
266
|
+
current = end
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// MARK: - Tokens
|
|
271
|
+
|
|
272
|
+
private func peek() -> UInt8? {
|
|
273
|
+
index < bytes.count ? bytes[index] : nil
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private mutating func skipSeparators() {
|
|
277
|
+
while let byte = peek(), Self.separators.contains(byte) {
|
|
278
|
+
index += 1
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private static let separators = Set(" \t\n\r\u{0C},".utf8)
|
|
283
|
+
|
|
284
|
+
private func isDigit(_ byte: UInt8) -> Bool {
|
|
285
|
+
byte >= ascii("0") && byte <= ascii("9")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private func isNumberStart(_ byte: UInt8) -> Bool {
|
|
289
|
+
isDigit(byte) || byte == ascii(".") || byte == ascii("-") || byte == ascii("+")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private mutating func point(from origin: CGPoint) -> CGPoint? {
|
|
293
|
+
guard let x = number(), let y = number() else { return nil }
|
|
294
|
+
let point = CGPoint(x: origin.x + x, y: origin.y + y)
|
|
295
|
+
return isFinite(point) ? point : nil
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Scans `[sign] digits [. digits] [e [sign] digits]`. A sign or a second
|
|
299
|
+
/// dot ends the number, so `-1-2` and `.5.5` are two numbers each.
|
|
300
|
+
private mutating func number() -> CGFloat? {
|
|
301
|
+
skipSeparators()
|
|
302
|
+
let start = index
|
|
303
|
+
if let byte = peek(), byte == ascii("-") || byte == ascii("+") {
|
|
304
|
+
index += 1
|
|
305
|
+
}
|
|
306
|
+
var digits = 0
|
|
307
|
+
while let byte = peek(), isDigit(byte) {
|
|
308
|
+
index += 1
|
|
309
|
+
digits += 1
|
|
310
|
+
}
|
|
311
|
+
// SVG allows `5.` but Double(String) rejects it, so drop that trailing dot from the
|
|
312
|
+
// text.
|
|
313
|
+
var bareDot: Int?
|
|
314
|
+
if peek() == ascii(".") {
|
|
315
|
+
bareDot = index
|
|
316
|
+
index += 1
|
|
317
|
+
while let byte = peek(), isDigit(byte) {
|
|
318
|
+
index += 1
|
|
319
|
+
digits += 1
|
|
320
|
+
bareDot = nil
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
guard digits > 0 else {
|
|
324
|
+
index = start
|
|
325
|
+
return nil
|
|
326
|
+
}
|
|
327
|
+
if let byte = peek(), byte == ascii("e") || byte == ascii("E") {
|
|
328
|
+
var exponent = index + 1
|
|
329
|
+
if exponent < bytes.count, bytes[exponent] == ascii("-") || bytes[exponent] == ascii("+") {
|
|
330
|
+
exponent += 1
|
|
331
|
+
}
|
|
332
|
+
if exponent < bytes.count, isDigit(bytes[exponent]) {
|
|
333
|
+
while exponent < bytes.count, isDigit(bytes[exponent]) {
|
|
334
|
+
exponent += 1
|
|
335
|
+
}
|
|
336
|
+
index = exponent
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
var text = bytes[start..<index]
|
|
340
|
+
if let bareDot {
|
|
341
|
+
text.remove(at: bareDot)
|
|
342
|
+
}
|
|
343
|
+
guard let value = Double(String(decoding: text, as: UTF8.self)), value.isFinite else { return nil }
|
|
344
|
+
return CGFloat(value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Arc flags are a single `0` or `1` and need no separator after them.
|
|
348
|
+
private mutating func flag() -> Bool? {
|
|
349
|
+
skipSeparators()
|
|
350
|
+
switch peek() {
|
|
351
|
+
case ascii("0"):
|
|
352
|
+
index += 1
|
|
353
|
+
return false
|
|
354
|
+
case ascii("1"):
|
|
355
|
+
index += 1
|
|
356
|
+
return true
|
|
357
|
+
default:
|
|
358
|
+
return nil
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private func isFinite(_ value: CGFloat) -> Bool {
|
|
364
|
+
value.isFinite
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private func isFinite(_ point: CGPoint) -> Bool {
|
|
368
|
+
point.x.isFinite && point.y.isFinite
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private func ascii(_ scalar: Unicode.Scalar) -> UInt8 {
|
|
372
|
+
UInt8(ascii: scalar)
|
|
373
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ThemeReveal'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = 'Bohdan Pomohaibo'
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = { :ios => '16.4' }
|
|
14
|
+
s.swift_version = '5.9'
|
|
15
|
+
s.source = { git: 'https://github.com/chocky335/react-native-theme-reveal.git', tag: "v#{s.version}" }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
s.pod_target_xcconfig = {
|
|
21
|
+
'DEFINES_MODULE' => 'YES',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
s.source_files = '**/*.{h,m,mm,swift}'
|
|
25
|
+
s.exclude_files = 'Tests/**'
|
|
26
|
+
end
|