├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── RoughSwift.xcscheme
│ └── RoughSwiftTests.xcscheme
├── Example
└── RoughSwiftApp
│ ├── RoughSwiftApp.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── RoughSwiftApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── PlayView.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── RoughSwiftAppApp.swift
├── Package.swift
├── README.md
├── Screenshots
├── chart.png
├── circles.png
├── green_rectangle.png
├── s.png
├── s1.png
└── svg.png
├── Sources
└── RoughSwift
│ ├── Engine
│ ├── Color+Extensions.swift
│ ├── Drawable.swift
│ ├── Drawing.swift
│ ├── Engine.swift
│ ├── FillStyle.swift
│ ├── Generator.swift
│ ├── Operation.swift
│ ├── OperationSet.swift
│ ├── OperationSetType.swift
│ ├── OperationType.swift
│ ├── Operator.swift
│ ├── Options.swift
│ ├── Point.swift
│ └── Size.swift
│ ├── Render
│ ├── Renderer.swift
│ └── SVGPath.swift
│ ├── Resources
│ └── rough.js
│ ├── RoughUIView.swift
│ └── RoughView.swift
└── Tests
└── RoughSwiftTests
└── RoughSwiftTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/RoughSwift.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/RoughSwiftTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | D2C5CBF127EF63D200BBE97D /* RoughSwiftAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CBF027EF63D200BBE97D /* RoughSwiftAppApp.swift */; };
11 | D2C5CBF327EF63D300BBE97D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CBF227EF63D200BBE97D /* ContentView.swift */; };
12 | D2C5CBF527EF63D500BBE97D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2C5CBF427EF63D500BBE97D /* Assets.xcassets */; };
13 | D2C5CBF827EF63D500BBE97D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2C5CBF727EF63D500BBE97D /* Preview Assets.xcassets */; };
14 | D2C5CC0227EFABEB00BBE97D /* RoughSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D2C5CC0127EFABEB00BBE97D /* RoughSwift */; };
15 | D2C5CC0527EFCDA100BBE97D /* PlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CC0427EFCDA100BBE97D /* PlayView.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | D2C5CBED27EF63D200BBE97D /* RoughSwiftApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RoughSwiftApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | D2C5CBF027EF63D200BBE97D /* RoughSwiftAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoughSwiftAppApp.swift; sourceTree = ""; };
21 | D2C5CBF227EF63D200BBE97D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | D2C5CBF427EF63D500BBE97D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | D2C5CBF727EF63D500BBE97D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | D2C5CBFF27EF63E900BBE97D /* RoughSwift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = RoughSwift; path = ../..; sourceTree = ""; };
25 | D2C5CC0427EFCDA100BBE97D /* PlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayView.swift; sourceTree = ""; };
26 | /* End PBXFileReference section */
27 |
28 | /* Begin PBXFrameworksBuildPhase section */
29 | D2C5CBEA27EF63D200BBE97D /* Frameworks */ = {
30 | isa = PBXFrameworksBuildPhase;
31 | buildActionMask = 2147483647;
32 | files = (
33 | D2C5CC0227EFABEB00BBE97D /* RoughSwift in Frameworks */,
34 | );
35 | runOnlyForDeploymentPostprocessing = 0;
36 | };
37 | /* End PBXFrameworksBuildPhase section */
38 |
39 | /* Begin PBXGroup section */
40 | D2C5CBE427EF63D200BBE97D = {
41 | isa = PBXGroup;
42 | children = (
43 | D2C5CBFE27EF63E900BBE97D /* Packages */,
44 | D2C5CBEF27EF63D200BBE97D /* RoughSwiftApp */,
45 | D2C5CBEE27EF63D200BBE97D /* Products */,
46 | D2C5CC0027EFABEB00BBE97D /* Frameworks */,
47 | );
48 | sourceTree = "";
49 | };
50 | D2C5CBEE27EF63D200BBE97D /* Products */ = {
51 | isa = PBXGroup;
52 | children = (
53 | D2C5CBED27EF63D200BBE97D /* RoughSwiftApp.app */,
54 | );
55 | name = Products;
56 | sourceTree = "";
57 | };
58 | D2C5CBEF27EF63D200BBE97D /* RoughSwiftApp */ = {
59 | isa = PBXGroup;
60 | children = (
61 | D2C5CBF027EF63D200BBE97D /* RoughSwiftAppApp.swift */,
62 | D2C5CBF227EF63D200BBE97D /* ContentView.swift */,
63 | D2C5CC0427EFCDA100BBE97D /* PlayView.swift */,
64 | D2C5CBF427EF63D500BBE97D /* Assets.xcassets */,
65 | D2C5CBF627EF63D500BBE97D /* Preview Content */,
66 | );
67 | path = RoughSwiftApp;
68 | sourceTree = "";
69 | };
70 | D2C5CBF627EF63D500BBE97D /* Preview Content */ = {
71 | isa = PBXGroup;
72 | children = (
73 | D2C5CBF727EF63D500BBE97D /* Preview Assets.xcassets */,
74 | );
75 | path = "Preview Content";
76 | sourceTree = "";
77 | };
78 | D2C5CBFE27EF63E900BBE97D /* Packages */ = {
79 | isa = PBXGroup;
80 | children = (
81 | D2C5CBFF27EF63E900BBE97D /* RoughSwift */,
82 | );
83 | name = Packages;
84 | sourceTree = "";
85 | };
86 | D2C5CC0027EFABEB00BBE97D /* Frameworks */ = {
87 | isa = PBXGroup;
88 | children = (
89 | );
90 | name = Frameworks;
91 | sourceTree = "";
92 | };
93 | /* End PBXGroup section */
94 |
95 | /* Begin PBXNativeTarget section */
96 | D2C5CBEC27EF63D200BBE97D /* RoughSwiftApp */ = {
97 | isa = PBXNativeTarget;
98 | buildConfigurationList = D2C5CBFB27EF63D500BBE97D /* Build configuration list for PBXNativeTarget "RoughSwiftApp" */;
99 | buildPhases = (
100 | D2C5CBE927EF63D200BBE97D /* Sources */,
101 | D2C5CBEA27EF63D200BBE97D /* Frameworks */,
102 | D2C5CBEB27EF63D200BBE97D /* Resources */,
103 | );
104 | buildRules = (
105 | );
106 | dependencies = (
107 | );
108 | name = RoughSwiftApp;
109 | packageProductDependencies = (
110 | D2C5CC0127EFABEB00BBE97D /* RoughSwift */,
111 | );
112 | productName = RoughSwiftApp;
113 | productReference = D2C5CBED27EF63D200BBE97D /* RoughSwiftApp.app */;
114 | productType = "com.apple.product-type.application";
115 | };
116 | /* End PBXNativeTarget section */
117 |
118 | /* Begin PBXProject section */
119 | D2C5CBE527EF63D200BBE97D /* Project object */ = {
120 | isa = PBXProject;
121 | attributes = {
122 | BuildIndependentTargetsInParallel = 1;
123 | LastSwiftUpdateCheck = 1330;
124 | LastUpgradeCheck = 1330;
125 | TargetAttributes = {
126 | D2C5CBEC27EF63D200BBE97D = {
127 | CreatedOnToolsVersion = 13.3;
128 | };
129 | };
130 | };
131 | buildConfigurationList = D2C5CBE827EF63D200BBE97D /* Build configuration list for PBXProject "RoughSwiftApp" */;
132 | compatibilityVersion = "Xcode 13.0";
133 | developmentRegion = en;
134 | hasScannedForEncodings = 0;
135 | knownRegions = (
136 | en,
137 | Base,
138 | );
139 | mainGroup = D2C5CBE427EF63D200BBE97D;
140 | productRefGroup = D2C5CBEE27EF63D200BBE97D /* Products */;
141 | projectDirPath = "";
142 | projectRoot = "";
143 | targets = (
144 | D2C5CBEC27EF63D200BBE97D /* RoughSwiftApp */,
145 | );
146 | };
147 | /* End PBXProject section */
148 |
149 | /* Begin PBXResourcesBuildPhase section */
150 | D2C5CBEB27EF63D200BBE97D /* Resources */ = {
151 | isa = PBXResourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | D2C5CBF827EF63D500BBE97D /* Preview Assets.xcassets in Resources */,
155 | D2C5CBF527EF63D500BBE97D /* Assets.xcassets in Resources */,
156 | );
157 | runOnlyForDeploymentPostprocessing = 0;
158 | };
159 | /* End PBXResourcesBuildPhase section */
160 |
161 | /* Begin PBXSourcesBuildPhase section */
162 | D2C5CBE927EF63D200BBE97D /* Sources */ = {
163 | isa = PBXSourcesBuildPhase;
164 | buildActionMask = 2147483647;
165 | files = (
166 | D2C5CC0527EFCDA100BBE97D /* PlayView.swift in Sources */,
167 | D2C5CBF327EF63D300BBE97D /* ContentView.swift in Sources */,
168 | D2C5CBF127EF63D200BBE97D /* RoughSwiftAppApp.swift in Sources */,
169 | );
170 | runOnlyForDeploymentPostprocessing = 0;
171 | };
172 | /* End PBXSourcesBuildPhase section */
173 |
174 | /* Begin XCBuildConfiguration section */
175 | D2C5CBF927EF63D500BBE97D /* Debug */ = {
176 | isa = XCBuildConfiguration;
177 | buildSettings = {
178 | ALWAYS_SEARCH_USER_PATHS = NO;
179 | CLANG_ANALYZER_NONNULL = YES;
180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
182 | CLANG_ENABLE_MODULES = YES;
183 | CLANG_ENABLE_OBJC_ARC = YES;
184 | CLANG_ENABLE_OBJC_WEAK = YES;
185 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
186 | CLANG_WARN_BOOL_CONVERSION = YES;
187 | CLANG_WARN_COMMA = YES;
188 | CLANG_WARN_CONSTANT_CONVERSION = YES;
189 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
191 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
192 | CLANG_WARN_EMPTY_BODY = YES;
193 | CLANG_WARN_ENUM_CONVERSION = YES;
194 | CLANG_WARN_INFINITE_RECURSION = YES;
195 | CLANG_WARN_INT_CONVERSION = YES;
196 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
197 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
198 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
200 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
202 | CLANG_WARN_STRICT_PROTOTYPES = YES;
203 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
204 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
205 | CLANG_WARN_UNREACHABLE_CODE = YES;
206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
207 | COPY_PHASE_STRIP = NO;
208 | DEBUG_INFORMATION_FORMAT = dwarf;
209 | ENABLE_STRICT_OBJC_MSGSEND = YES;
210 | ENABLE_TESTABILITY = YES;
211 | GCC_C_LANGUAGE_STANDARD = gnu11;
212 | GCC_DYNAMIC_NO_PIC = NO;
213 | GCC_NO_COMMON_BLOCKS = YES;
214 | GCC_OPTIMIZATION_LEVEL = 0;
215 | GCC_PREPROCESSOR_DEFINITIONS = (
216 | "DEBUG=1",
217 | "$(inherited)",
218 | );
219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
221 | GCC_WARN_UNDECLARED_SELECTOR = YES;
222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
223 | GCC_WARN_UNUSED_FUNCTION = YES;
224 | GCC_WARN_UNUSED_VARIABLE = YES;
225 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
226 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
227 | MTL_FAST_MATH = YES;
228 | ONLY_ACTIVE_ARCH = YES;
229 | SDKROOT = iphoneos;
230 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
232 | };
233 | name = Debug;
234 | };
235 | D2C5CBFA27EF63D500BBE97D /* Release */ = {
236 | isa = XCBuildConfiguration;
237 | buildSettings = {
238 | ALWAYS_SEARCH_USER_PATHS = NO;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
242 | CLANG_ENABLE_MODULES = YES;
243 | CLANG_ENABLE_OBJC_ARC = YES;
244 | CLANG_ENABLE_OBJC_WEAK = YES;
245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
246 | CLANG_WARN_BOOL_CONVERSION = YES;
247 | CLANG_WARN_COMMA = YES;
248 | CLANG_WARN_CONSTANT_CONVERSION = YES;
249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
252 | CLANG_WARN_EMPTY_BODY = YES;
253 | CLANG_WARN_ENUM_CONVERSION = YES;
254 | CLANG_WARN_INFINITE_RECURSION = YES;
255 | CLANG_WARN_INT_CONVERSION = YES;
256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | COPY_PHASE_STRIP = NO;
268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
269 | ENABLE_NS_ASSERTIONS = NO;
270 | ENABLE_STRICT_OBJC_MSGSEND = YES;
271 | GCC_C_LANGUAGE_STANDARD = gnu11;
272 | GCC_NO_COMMON_BLOCKS = YES;
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
280 | MTL_ENABLE_DEBUG_INFO = NO;
281 | MTL_FAST_MATH = YES;
282 | SDKROOT = iphoneos;
283 | SWIFT_COMPILATION_MODE = wholemodule;
284 | SWIFT_OPTIMIZATION_LEVEL = "-O";
285 | VALIDATE_PRODUCT = YES;
286 | };
287 | name = Release;
288 | };
289 | D2C5CBFC27EF63D500BBE97D /* Debug */ = {
290 | isa = XCBuildConfiguration;
291 | buildSettings = {
292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
294 | CODE_SIGN_STYLE = Automatic;
295 | CURRENT_PROJECT_VERSION = 1;
296 | DEVELOPMENT_ASSET_PATHS = "\"RoughSwiftApp/Preview Content\"";
297 | DEVELOPMENT_TEAM = T78DK947F2;
298 | ENABLE_PREVIEWS = YES;
299 | GENERATE_INFOPLIST_FILE = YES;
300 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
301 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
302 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
304 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
305 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
306 | LD_RUNPATH_SEARCH_PATHS = (
307 | "$(inherited)",
308 | "@executable_path/Frameworks",
309 | );
310 | MARKETING_VERSION = 1.0;
311 | PRODUCT_BUNDLE_IDENTIFIER = com.onmyway133.RoughSwiftApp;
312 | PRODUCT_NAME = "$(TARGET_NAME)";
313 | SWIFT_EMIT_LOC_STRINGS = YES;
314 | SWIFT_VERSION = 5.0;
315 | TARGETED_DEVICE_FAMILY = "1,2";
316 | };
317 | name = Debug;
318 | };
319 | D2C5CBFD27EF63D500BBE97D /* Release */ = {
320 | isa = XCBuildConfiguration;
321 | buildSettings = {
322 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
323 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
324 | CODE_SIGN_STYLE = Automatic;
325 | CURRENT_PROJECT_VERSION = 1;
326 | DEVELOPMENT_ASSET_PATHS = "\"RoughSwiftApp/Preview Content\"";
327 | DEVELOPMENT_TEAM = T78DK947F2;
328 | ENABLE_PREVIEWS = YES;
329 | GENERATE_INFOPLIST_FILE = YES;
330 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
331 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
332 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
333 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
335 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
336 | LD_RUNPATH_SEARCH_PATHS = (
337 | "$(inherited)",
338 | "@executable_path/Frameworks",
339 | );
340 | MARKETING_VERSION = 1.0;
341 | PRODUCT_BUNDLE_IDENTIFIER = com.onmyway133.RoughSwiftApp;
342 | PRODUCT_NAME = "$(TARGET_NAME)";
343 | SWIFT_EMIT_LOC_STRINGS = YES;
344 | SWIFT_VERSION = 5.0;
345 | TARGETED_DEVICE_FAMILY = "1,2";
346 | };
347 | name = Release;
348 | };
349 | /* End XCBuildConfiguration section */
350 |
351 | /* Begin XCConfigurationList section */
352 | D2C5CBE827EF63D200BBE97D /* Build configuration list for PBXProject "RoughSwiftApp" */ = {
353 | isa = XCConfigurationList;
354 | buildConfigurations = (
355 | D2C5CBF927EF63D500BBE97D /* Debug */,
356 | D2C5CBFA27EF63D500BBE97D /* Release */,
357 | );
358 | defaultConfigurationIsVisible = 0;
359 | defaultConfigurationName = Release;
360 | };
361 | D2C5CBFB27EF63D500BBE97D /* Build configuration list for PBXNativeTarget "RoughSwiftApp" */ = {
362 | isa = XCConfigurationList;
363 | buildConfigurations = (
364 | D2C5CBFC27EF63D500BBE97D /* Debug */,
365 | D2C5CBFD27EF63D500BBE97D /* Release */,
366 | );
367 | defaultConfigurationIsVisible = 0;
368 | defaultConfigurationName = Release;
369 | };
370 | /* End XCConfigurationList section */
371 |
372 | /* Begin XCSwiftPackageProductDependency section */
373 | D2C5CC0127EFABEB00BBE97D /* RoughSwift */ = {
374 | isa = XCSwiftPackageProductDependency;
375 | productName = RoughSwift;
376 | };
377 | /* End XCSwiftPackageProductDependency section */
378 | };
379 | rootObject = D2C5CBE527EF63D200BBE97D /* Project object */;
380 | }
381 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "2x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "83.5x83.5"
82 | },
83 | {
84 | "idiom" : "ios-marketing",
85 | "scale" : "1x",
86 | "size" : "1024x1024"
87 | }
88 | ],
89 | "info" : {
90 | "author" : "xcode",
91 | "version" : 1
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // RoughSwiftApp
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import SwiftUI
9 | import RoughSwift
10 |
11 | struct ContentView: View {
12 | @State private var flag = false
13 | var body: some View {
14 | TabView {
15 | StylesView()
16 | .tabItem {
17 | Label("Styles", systemImage: "paintpalette.fill")
18 | }
19 | Chartview()
20 | .tabItem {
21 | Label("Chart", systemImage: "chart.bar")
22 | }
23 |
24 | SVGView()
25 | .tabItem {
26 | Label("SVG", systemImage: "swift")
27 | }
28 |
29 | CustomizeView()
30 | .tabItem {
31 | Label("Customize", systemImage: "paintbrush.pointed.fill")
32 | }
33 | }
34 | }
35 | }
36 |
37 | struct CustomizeView: View {
38 | @State var flag = false
39 |
40 | var body: some View {
41 | VStack {
42 | Button(action: {
43 | flag.toggle()
44 | }) {
45 | Text("Click")
46 | }
47 |
48 | RoughView()
49 | .fill(flag ? UIColor.green : UIColor.yellow)
50 | .fillStyle(flag ? .hachure : .dots)
51 | .circle()
52 | .frame(width: flag ? 200 : 100, height: flag ? 200 : 100)
53 | }
54 |
55 | }
56 | }
57 |
58 | struct SVGView: View {
59 | var apple: String {
60 | "M85 32C115 68 239 170 281 192 311 126 274 43 244 0c97 58 146 167 121 254 28 28 40 89 29 108 -25-45-67-39-93-24C176 409 24 296 0 233c68 56 170 65 226 27C165 217 56 89 36 54c42 38 116 96 161 122C159 137 108 72 85 32z"
61 | }
62 |
63 | var body: some View {
64 | VStack {
65 | RoughView()
66 | .stroke(.systemTeal)
67 | .fill(.red)
68 | .draw(Path(d: apple))
69 | .frame(width: 300, height: 300)
70 | }
71 | }
72 | }
73 |
74 | struct StylesView: View {
75 | var body: some View {
76 | LazyVGrid(columns: [.init(), .init(), .init()], spacing: 12) {
77 | RoughView()
78 | .fill(.red)
79 | .fillStyle(.crossHatch)
80 | .circle()
81 | .frame(width: 100, height: 100)
82 |
83 | RoughView()
84 | .fill(.green)
85 | .fillStyle(.dashed)
86 | .circle()
87 | .frame(width: 100, height: 100)
88 |
89 | RoughView()
90 | .fill(.purple)
91 | .fillStyle(.dots)
92 | .circle()
93 | .frame(width: 100, height: 100)
94 |
95 | RoughView()
96 | .fill(.cyan)
97 | .fillStyle(.hachure)
98 | .circle()
99 | .frame(width: 100, height: 100)
100 |
101 | RoughView()
102 | .fill(.orange)
103 | .fillStyle(.solid)
104 | .circle()
105 | .frame(width: 100, height: 100)
106 |
107 | RoughView()
108 | .fill(.gray)
109 | .fillStyle(.starBurst)
110 | .circle()
111 | .frame(width: 100, height: 100)
112 |
113 | RoughView()
114 | .fill(.yellow)
115 | .fillStyle(.zigzag)
116 | .circle()
117 | .frame(width: 100, height: 100)
118 |
119 | RoughView()
120 | .fill(.systemTeal)
121 | .fillStyle(.zigzagLine)
122 | .circle()
123 | .frame(width: 100, height: 100)
124 | }
125 | }
126 | }
127 |
128 | struct Chartview: View {
129 | var heights: [CGFloat] {
130 | Array(0 ..< 10).map { _ in CGFloat.random(in: 0 ..< 150) }
131 | }
132 |
133 | var body: some View {
134 | HStack {
135 | ForEach(0 ..< 10) { index in
136 | VStack {
137 | Spacer()
138 | RoughView()
139 | .fill(.yellow)
140 | .rectangle()
141 | .frame(height: heights[index])
142 | }
143 | }
144 | }
145 | .padding(.horizontal)
146 | .padding(.bottom, 100)
147 | }
148 | }
149 |
150 | struct ContentView_Previews: PreviewProvider {
151 | static var previews: some View {
152 | ContentView()
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/PlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlayView.swift
3 | // RoughSwiftApp
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import SwiftUI
9 | import RoughSwift
10 |
11 | struct PlayView: View {
12 | var heights: [CGFloat] {
13 | Array(0 ..< 10).map { _ in CGFloat.random(in: 0 ..< 150) }
14 | }
15 | var body: some View {
16 | HStack {
17 | ForEach(0 ..< 10) { index in
18 | VStack {
19 | Spacer()
20 | RoughView()
21 | .fill(.orange)
22 | .rectangle()
23 | .frame(height: heights[index])
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
30 | struct PlayView_Previews: PreviewProvider {
31 | static var previews: some View {
32 | PlayView()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/RoughSwiftApp/RoughSwiftApp/RoughSwiftAppApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoughSwiftAppApp.swift
3 | // RoughSwiftApp
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct RoughSwiftAppApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "RoughSwift",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "RoughSwift",
15 | targets: ["RoughSwift"]
16 | ),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "RoughSwift",
27 | resources: [
28 | .process("Resources")
29 | ]
30 | ),
31 | .testTarget(
32 | name: "RoughSwiftTests",
33 | dependencies: ["RoughSwift"]
34 | ),
35 | ]
36 | )
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Checkout https://indiegoodies.com/
4 |
5 | 
6 |
7 | ## Description
8 |
9 | RoughSwift allows us to easily make shapes in hand drawn, sketchy, comic style in SwiftUI.
10 |
11 | - [x] Support iOS, tvOS
12 | - [x] Support all shapes: line, rectangle, circle, ellipse, linear path, arc, curve, polygon, svg path
13 | - [x] Generate `UIBezierPath` for `CAShapeLayer`
14 | - [x] Easy cusomizations with Options
15 | - [x] Easy composable APIs
16 | - [x] Convenient draw functions
17 | - [x] Platform independant APIs which can easily support new platforms
18 | - [x] Test coverage
19 | - [x] Immutable and type safe data structure
20 | - [ ] SVG elliptical arc
21 |
22 | There are [Example](https://github.com/onmyway133/RoughSwift/tree/master/Example) project where you can explore further.
23 |
24 | ## Basic
25 |
26 | Use `generator` in `draw` function to specify which shape to render. The returned `CALayer` contains the rendered result in correct `size` and is updated everytime `generator` is instructed.
27 |
28 | Here's how to draw a green rectangle
29 |
30 | 
31 |
32 | ```swift
33 | RoughView()
34 | .fill(.yellow)
35 | .fillStyle(.hachure)
36 | .hachureAngle(-41)
37 | .hachureGap(-1)
38 | .fillWeight(-1)
39 | .stroke(.systemTeal)
40 | .strokeWidth(2)
41 | .curveTightness(0)
42 | .curveStepCount(9)
43 | .dashOffset(-1)
44 | .dashGap(-1)
45 | .zigzagOffset(-9)
46 | ```
47 |
48 | The beauty of `CALayer` is that we can further animate, transform (translate, scale, rotate) and compose them into more powerful shapes.
49 |
50 | ## Options
51 |
52 | `Options` is used to custimize shape. It is immutable struct and apply to one shape at a time. The following properties are configurable
53 |
54 | - maxRandomnessOffset
55 | - toughness
56 | - bowing
57 | - fill
58 | - stroke
59 | - strokeWidth
60 | - curveTightness
61 | - curveStepCount
62 | - fillStyle
63 | - fillWeight
64 | - hachureAngle
65 | - hachureGap
66 | - dashOffset
67 | - dashGap
68 | - zigzagOffset
69 |
70 | ## Shapes
71 |
72 | RoughSwift supports all primitive shapes, including SVG path
73 |
74 | - line
75 | - rectangle
76 | - ellipse
77 | - circle
78 | - linearPath
79 | - arc
80 | - curve
81 | - polygon
82 | - path
83 |
84 | ## Fill style
85 |
86 | Most of the time, we use `fill` for solid fill color inside shape, `stroke` for shape border, and `fillStyle` for sketchy fill style.
87 |
88 | Available fill styles
89 |
90 | - crossHatch
91 | - dashed
92 | - dots
93 | - hachure
94 | - solid
95 | - starBurst
96 | - zigzag
97 | - zigzagLine
98 |
99 | Here's how to draw circles in different fill styles. The default fill style is hachure
100 |
101 | 
102 |
103 | ```swift
104 | struct StylesView: View {
105 | var body: some View {
106 | LazyVGrid(columns: [.init(), .init(), .init()], spacing: 12) {
107 | RoughView()
108 | .fill(.red)
109 | .fillStyle(.crossHatch)
110 | .circle()
111 | .frame(width: 100, height: 100)
112 |
113 | RoughView()
114 | .fill(.green)
115 | .fillStyle(.dashed)
116 | .circle()
117 | .frame(width: 100, height: 100)
118 |
119 | RoughView()
120 | .fill(.purple)
121 | .fillStyle(.dots)
122 | .circle()
123 | .frame(width: 100, height: 100)
124 |
125 | RoughView()
126 | .fill(.cyan)
127 | .fillStyle(.hachure)
128 | .circle()
129 | .frame(width: 100, height: 100)
130 |
131 | RoughView()
132 | .fill(.orange)
133 | .fillStyle(.solid)
134 | .circle()
135 | .frame(width: 100, height: 100)
136 |
137 | RoughView()
138 | .fill(.gray)
139 | .fillStyle(.starBurst)
140 | .circle()
141 | .frame(width: 100, height: 100)
142 |
143 | RoughView()
144 | .fill(.yellow)
145 | .fillStyle(.zigzag)
146 | .circle()
147 | .frame(width: 100, height: 100)
148 |
149 | RoughView()
150 | .fill(.systemTeal)
151 | .fillStyle(.zigzagLine)
152 | .circle()
153 | .frame(width: 100, height: 100)
154 | }
155 | }
156 | }
157 | ```
158 |
159 | ## SVG
160 |
161 | 
162 |
163 | SVG shape can be bigger or smaller than the specifed layer size, so RoughSwift scales them to your requested `size`. This way we can compose and transform the SVG shape.
164 |
165 | ```swift
166 | struct SVGView: View {
167 | var apple: String {
168 | "M85 32C115 68 239 170 281 192 311 126 274 43 244 0c97 58 146 167 121 254 28 28 40 89 29 108 -25-45-67-39-93-24C176 409 24 296 0 233c68 56 170 65 226 27C165 217 56 89 36 54c42 38 116 96 161 122C159 137 108 72 85 32z"
169 | }
170 |
171 | var body: some View {
172 | VStack {
173 | RoughView()
174 | .stroke(.systemTeal)
175 | .fill(.red)
176 | .draw(Path(d: apple))
177 | .frame(width: 300, height: 300)
178 | }
179 | }
180 | }
181 | ```
182 |
183 | ## Creative shapes
184 |
185 | With all the primitive shapes, we can create more beautiful things. The only limit is your imagination.
186 |
187 | Here's how to create chart
188 |
189 | 
190 |
191 | ```swift
192 | struct Chartview: View {
193 | var heights: [CGFloat] {
194 | Array(0 ..< 10).map { _ in CGFloat.random(in: 0 ..< 150) }
195 | }
196 |
197 | var body: some View {
198 | HStack {
199 | ForEach(0 ..< 10) { index in
200 | VStack {
201 | Spacer()
202 | RoughView()
203 | .fill(.yellow)
204 | .rectangle()
205 | .frame(height: heights[index])
206 | }
207 | }
208 | }
209 | .padding(.horizontal)
210 | .padding(.bottom, 100)
211 | }
212 | }
213 | ```
214 |
215 |
216 | ## Advance with Drawable, Generator and Renderer
217 |
218 | Behind the screen, we composes `Generator` and `Renderer`.
219 |
220 | We can instantiate `Engine` or use a shared `Engine` for memory efficiency, to make `Generator`. Every time we instruct `Generator` to draw a shape, the engine works hard to figure out information about the sketchy shape in `Drawable`.
221 |
222 | The name of these concepts follow `rough.js` for better code reasoning.
223 |
224 | For iOS, there is a `Renderer` that can handle `Drawable` and transform it into `UIBezierPath` and `CALayer`. There will be more `Renderer` that can render into graphics context, image and for other platforms like macOS and watchOS.
225 |
226 |
227 | ```swift
228 | let layer = CALayer()
229 | let size = CGSize(width: 200, heigh: 200)
230 |
231 | let renderer = Renderer(layer: layer)
232 | let generator = Engine.shared.generator(size: bounds.size)
233 |
234 | let drawable: Drawable = Rectangle(x: 10, y: 10, width: 100, height: 50)
235 | let drawing = generate.generate(drawable: drawable)
236 |
237 | renderer.render(drawing: drawing)
238 | ```
239 |
240 | ## Installation
241 |
242 | Add the following line to the dependencies in your `Package.swift` file
243 |
244 | ```swift
245 | .package(url: "https://github.com/onmyway133/RoughSwift"),
246 | ```
247 |
248 | ## Author
249 |
250 | Khoa Pham, onmyway133@gmail.com
251 |
252 | ## Credit
253 |
254 | - [rough](https://github.com/pshihn/rough) for the generator that powers RoughSwift. All the hard work is done via rough in JavascriptCore.
255 | - [SVGPath](https://github.com/timrwood/SVGPath) for constructing UIBezierPath from SVG path
256 |
257 | ## Contributing
258 |
259 | We would love you to contribute to **RoughSwift**, check the [CONTRIBUTING](https://github.com/onmyway133/RoughSwift/blob/master/CONTRIBUTING.md) file for more info.
260 |
261 | ## License
262 |
263 | **RoughSwift** is available under the MIT license. See the [LICENSE](https://github.com/onmyway133/RoughSwift/blob/master/LICENSE.md) file for more info.
264 |
--------------------------------------------------------------------------------
/Screenshots/chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/chart.png
--------------------------------------------------------------------------------
/Screenshots/circles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/circles.png
--------------------------------------------------------------------------------
/Screenshots/green_rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/green_rectangle.png
--------------------------------------------------------------------------------
/Screenshots/s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/s.png
--------------------------------------------------------------------------------
/Screenshots/s1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/s1.png
--------------------------------------------------------------------------------
/Screenshots/svg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmyway133/RoughSwift/757b07c2867593e1b1fd887e8d34bd4abd827e8b/Screenshots/svg.png
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Color+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Extensions.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 20/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIColor {
12 | /// Constructing color from hex string
13 | ///
14 | /// - Parameter hex: A hex string, can either contain # or not
15 | convenience init(hex string: String) {
16 | if string == "none" {
17 | self.init(cgColor: UIColor.clear.cgColor)
18 | return
19 | }
20 |
21 | var hex = string.hasPrefix("#")
22 | ? String(string.dropFirst())
23 | : string
24 | guard hex.count == 3 || hex.count == 6
25 | else {
26 | self.init(white: 1.0, alpha: 0.0)
27 | return
28 | }
29 | if hex.count == 3 {
30 | for (index, char) in hex.enumerated() {
31 | hex.insert(char, at: hex.index(hex.startIndex, offsetBy: index * 2))
32 | }
33 | }
34 |
35 | guard let intCode = Int(hex, radix: 16) else {
36 | self.init(white: 1.0, alpha: 0.0)
37 | return
38 | }
39 |
40 | self.init(
41 | red: CGFloat((intCode >> 16) & 0xFF) / 255.0,
42 | green: CGFloat((intCode >> 8) & 0xFF) / 255.0,
43 | blue: CGFloat((intCode) & 0xFF) / 255.0, alpha: 1.0)
44 | }
45 |
46 | func toHex() -> String {
47 | var red: CGFloat = 0
48 | var green: CGFloat = 0
49 | var blue: CGFloat = 0
50 | var alpha: CGFloat = 0
51 |
52 | let multiplier = CGFloat(255.999999)
53 |
54 | guard !self.isEqual(UIColor.clear) else {
55 | return "none"
56 | }
57 |
58 | guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
59 | return "none"
60 | }
61 |
62 | if alpha == 1.0 {
63 | return String(
64 | format: "#%02lX%02lX%02lX",
65 | Int(red * multiplier),
66 | Int(green * multiplier),
67 | Int(blue * multiplier)
68 | )
69 | } else {
70 | return String(
71 | format: "#%02lX%02lX%02lX%02lX",
72 | Int(red * multiplier),
73 | Int(green * multiplier),
74 | Int(blue * multiplier),
75 | Int(alpha * multiplier)
76 | )
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Drawable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Drawable.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol Drawable {
11 | var method: String { get }
12 | var arguments: [Any] { get }
13 | }
14 |
15 | public struct Line: Drawable {
16 | public var method: String { "line" }
17 |
18 | public var arguments: [Any] {
19 | [
20 | from.x, from.y, to.x, to.y
21 | ]
22 | }
23 |
24 | let from: Point
25 | let to: Point
26 | }
27 |
28 | public struct Rectangle: Drawable {
29 | public var method: String { "rectangle" }
30 |
31 | public var arguments: [Any] {
32 | [
33 | x, y, width, height,
34 | ]
35 | }
36 |
37 | let x: Float
38 | let y: Float
39 | let width: Float
40 | let height: Float
41 |
42 | public init(
43 | x: Float,
44 | y: Float,
45 | width: Float,
46 | height: Float
47 | ) {
48 | self.x = x
49 | self.y = y
50 | self.width = width
51 | self.height = height
52 | }
53 | }
54 |
55 | public struct Ellipse: Drawable {
56 | public var method: String { "ellipse" }
57 |
58 | public var arguments: [Any] {
59 | [
60 | x, y, width, height,
61 | ]
62 | }
63 |
64 | let x: Float
65 | let y: Float
66 | let width: Float
67 | let height: Float
68 |
69 | public init(
70 | x: Float,
71 | y: Float,
72 | width: Float,
73 | height: Float
74 | ) {
75 | self.x = x
76 | self.y = y
77 | self.width = width
78 | self.height = height
79 | }
80 | }
81 |
82 | public struct Circle: Drawable {
83 | public var method: String { "circle" }
84 |
85 | public var arguments: [Any] {
86 | [
87 | x, y, diameter
88 | ]
89 | }
90 |
91 | let x: Float
92 | let y: Float
93 | let diameter: Float
94 |
95 | public init(
96 | x: Float,
97 | y: Float,
98 | diameter: Float
99 | ) {
100 | self.x = x
101 | self.y = y
102 | self.diameter = diameter
103 | }
104 | }
105 |
106 | public struct LinearPath: Drawable {
107 | public var method: String { "linearPath" }
108 |
109 | public var arguments: [Any] {
110 | points.map({ $0.toRoughPoint() })
111 | }
112 |
113 | let points: [Point]
114 |
115 | public init(
116 | points: [Point]
117 | ) {
118 | self.points = points
119 | }
120 | }
121 |
122 | public struct Arc: Drawable {
123 | public var method: String { "v" }
124 |
125 | public var arguments: [Any] {
126 | [
127 | x, y, width, height,
128 | start, stop, closed
129 | ]
130 | }
131 |
132 | let x: Float
133 | let y: Float
134 | let width: Float
135 | let height: Float
136 | let start: Float
137 | let stop: Float
138 | var closed: Bool
139 |
140 | public init(
141 | x: Float,
142 | y: Float,
143 | width: Float,
144 | height: Float,
145 | start: Float,
146 | stop: Float,
147 | closed: Bool = false
148 | ) {
149 | self.x = x
150 | self.y = y
151 | self.width = width
152 | self.height = height
153 | self.start = start
154 | self.stop = stop
155 | self.closed = closed
156 | }
157 | }
158 |
159 | public struct Curve: Drawable {
160 | public var method: String { "curve" }
161 |
162 | public var arguments: [Any] {
163 | points.map({ $0.toRoughPoint() })
164 | }
165 |
166 | let points: [Point]
167 |
168 | public init(
169 | points: [Point]
170 | ) {
171 | self.points = points
172 | }
173 | }
174 |
175 | public struct Polygon: Drawable {
176 | public var method: String { "polygon" }
177 |
178 | public var arguments: [Any] {
179 | points.map({ $0.toRoughPoint() })
180 | }
181 |
182 | let points: [Point]
183 |
184 | public init(
185 | points: [Point]
186 | ) {
187 | self.points = points
188 | }
189 | }
190 |
191 | public struct Path: Drawable {
192 | public var method: String { "path" }
193 |
194 | public var arguments: [Any] {
195 | [
196 | d
197 | ]
198 | }
199 |
200 | let d: String
201 |
202 | public init(
203 | d: String
204 | ) {
205 | self.d = d
206 | }
207 | }
208 |
209 | protocol Fulfillable {
210 | func arguments(size: Size) -> [Any]
211 | }
212 |
213 | struct FullRectangle: Drawable, Fulfillable {
214 | var method: String { "rectangle"}
215 | var arguments: [Any] { [] }
216 | func arguments(size: Size) -> [Any] {
217 | [
218 | 0, 0, size.width, size.height
219 | ]
220 | }
221 | }
222 |
223 | struct FullCircle: Drawable, Fulfillable {
224 | var method: String { "circle" }
225 | var arguments: [Any] { [] }
226 |
227 | func arguments(size: Size) -> [Any] {
228 | [
229 | size.width / 2, size.height / 2, min(size.width, size.height)
230 | ]
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Drawing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Drawing.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import JavaScriptCore
11 |
12 | /// Information from Generator about the drawble to render
13 | public struct Drawing {
14 | public let shape: String
15 | public let sets: [OperationSet]
16 | public let options: Options
17 | }
18 |
19 | public extension Drawing {
20 | init?(dictionary: JSONDictionary) {
21 | guard
22 | let shape = dictionary["shape"] as? String,
23 | let sets = dictionary["sets"] as? JSONArray,
24 | let options = dictionary["options"] as? JSONDictionary
25 | else {
26 | return nil
27 | }
28 |
29 | self.init(
30 | shape: shape,
31 | sets: sets.compactMap({ OperationSet.from(dictionary: $0) }),
32 | options: Options(dictionary: options)
33 | )
34 | }
35 |
36 | init?(roughDrawing: JSValue?) {
37 | guard
38 | let roughDrawing = roughDrawing,
39 | let dictionary = roughDrawing.toDictionary() as? JSONDictionary else {
40 | return nil
41 | }
42 |
43 | self.init(dictionary: dictionary)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Engine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Canvas.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import JavaScriptCore
11 |
12 | public typealias JSONDictionary = [String: Any]
13 | public typealias JSONArray = [JSONDictionary]
14 |
15 | public class Engine {
16 | private let context = JSContext()!
17 | private let rough: JSValue
18 |
19 | public static let shared = Engine()
20 |
21 | public init() {
22 | let bundle = Bundle.module
23 | let path = bundle.url(forResource: "rough", withExtension: "js")!
24 | let content = try! String(contentsOf: path)
25 | context.evaluateScript(content)
26 |
27 | context.exceptionHandler = { context, exception in
28 | print(exception!.toString() as Any)
29 | }
30 |
31 | rough = context.objectForKeyedSubscript("rough")!
32 | }
33 |
34 | public func generator(size: CGSize) -> Generator {
35 | let drawingSurface: JSONDictionary = [
36 | "width": size.width,
37 | "height": size.height
38 | ]
39 |
40 | let value = rough.invokeMethod("generator", withArguments: [drawingSurface])!
41 | return Generator(size: size, jsValue: value)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/FillStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FillStyle.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 20/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum FillStyle: String {
12 | case hachure
13 | case solid
14 | case zigzag
15 | case crossHatch = "cross-hatch"
16 | case dots
17 | case sunBurst = "sunburst"
18 | case starBurst = "starburst"
19 | case dashed
20 | case zigzagLine = "zigzag-line"
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Generator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generator.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import JavaScriptCore
10 |
11 | public class Generator {
12 | private let size: CGSize
13 | private let jsValue: JSValue
14 |
15 | public init(
16 | size: CGSize,
17 | jsValue: JSValue
18 | ) {
19 | self.size = size
20 | self.jsValue = jsValue
21 | }
22 |
23 | public func generate(drawable: Drawable, options: Options = .init()) -> Drawing? {
24 | let arguments: [Any]
25 | if let fullable = drawable as? Fulfillable {
26 | arguments = fullable.arguments(size: size.toSize)
27 | } else {
28 | arguments = drawable.arguments
29 | }
30 |
31 | return jsValue.invokeMethod(
32 | drawable.method,
33 | withArguments: arguments + [options.toRoughDictionary()]
34 | ).toDrawing
35 | }
36 | }
37 |
38 | private extension JSValue {
39 | var toDrawing: Drawing? {
40 | Drawing(roughDrawing: self)
41 | }
42 | }
43 |
44 | private extension CGSize {
45 | var toSize: Size {
46 | Size(width: Float(width), height: Float(height))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Operation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Operation.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Detailed instruction to convert to bezier path
12 | public class Operation {
13 | static func from(dictionary: JSONDictionary) -> Operation? {
14 | guard
15 | let op = dictionary["op"] as? String,
16 | let numberData = dictionary["data"] as? [NSNumber]
17 | else {
18 | return nil
19 | }
20 |
21 | let data = numberData.map({ $0.floatValue })
22 |
23 | switch op {
24 | case OperationType.move.rawValue where data.count == 2:
25 | return Move(data: data)
26 | case OperationType.lineTo.rawValue where data.count == 2:
27 | return LineTo(data: data)
28 | case OperationType.bezierCurveTo.rawValue where data.count == 6:
29 | return BezierCurveTo(data: data)
30 | case OperationType.quadraticCurveTo.rawValue where data.count == 4:
31 | return QuadraticCurveTo(data: data)
32 | default:
33 | return nil
34 | }
35 | }
36 | }
37 |
38 | public class Move: Operation {
39 | public let point: Point
40 |
41 | init(data: [Float]) {
42 | self.point = Point(x: data[0], y: data[1])
43 | }
44 | }
45 |
46 | public class LineTo: Operation {
47 | public let point: Point
48 |
49 | init(data: [Float]) {
50 | self.point = Point(x: data[0], y: data[1])
51 | }
52 | }
53 |
54 | public class BezierCurveTo: Operation {
55 | public let point: Point
56 | public let controlPoint1: Point
57 | public let controlPoint2: Point
58 |
59 | init(data: [Float]) {
60 | // void ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
61 | self.controlPoint1 = Point(x: data[0], y: data[1])
62 | self.controlPoint2 = Point(x: data[2], y: data[3])
63 | self.point = Point(x: data[4], y: data[5])
64 | }
65 | }
66 |
67 | public class QuadraticCurveTo: Operation {
68 | public let point: Point
69 | public let controlPoint: Point
70 |
71 | init(data: [Float]) {
72 | // void ctx.quadraticCurveTo(cpx, cpy, x, y);
73 | self.controlPoint = Point(x: data[0], y: data[1])
74 | self.point = Point(x: data[2], y: data[3])
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/OperationSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OperationSet.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Instructions about which set type to draw and operations
12 | public struct OperationSet {
13 | public let type: OperationSetType
14 | public let operations: [Operation]
15 |
16 | // For path
17 | public let path: String?
18 | public let size: Size?
19 |
20 | static func from(dictionary: JSONDictionary) -> OperationSet? {
21 | guard
22 | let rawType = dictionary["type"] as? String,
23 | let type = OperationSetType(rawValue: rawType),
24 | let ops = dictionary["ops"] as? JSONArray
25 | else {
26 | return nil
27 | }
28 |
29 | let path = dictionary["path"] as? String
30 | var size: Size? = nil
31 | if let sizeArray = dictionary["size"] as? [NSNumber], sizeArray.count == 2{
32 | size = Size(width: sizeArray[0].floatValue, height: sizeArray[1].floatValue)
33 | }
34 |
35 | return OperationSet(
36 | type: type,
37 | operations: ops.compactMap({ Operation.from(dictionary: $0) }),
38 | path: path,
39 | size: size
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/OperationSetType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OperationSetType.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum OperationSetType: String {
12 | /// border
13 | case path
14 | /// solid fill
15 | case fillPath
16 | /// sketch fill
17 | case fillSketch
18 | /// svg path solid fill
19 | case path2DFill = "path2Dfill"
20 | /// svg path sketch fill
21 | case path2DPattern = "path2Dpattern"
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/OperationType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OperationType.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum OperationType: String {
12 | case move
13 | case bezierCurveTo = "bcurveTo"
14 | case lineTo
15 | case quadraticCurveTo = "qcurveTo"
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Operator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Operator.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 20/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | infix operator <-?
12 |
13 | public func <-? (root: inout T, value: T?) {
14 | guard let value = value else { return }
15 | root = value
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Options.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Option.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct Options {
12 | public var maxRandomnessOffset: Float = 2
13 | public var roughness: Float = 1
14 | public var bowing: Float = 1
15 | public var fill: UIColor = .clear
16 | public var stroke: UIColor = .black
17 | public var strokeWidth: Float = 1
18 | public var curveTightness: Float = 0
19 | public var curveStepCount: Float = 9
20 | public var fillStyle: FillStyle = .hachure
21 | public var fillWeight: Float = -1
22 | public var hachureAngle: Float = -41
23 | public var hachureGap: Float = -1
24 | public var dashOffset: Float = -1
25 | public var dashGap: Float = -1
26 | public var zigzagOffset: Float = -1
27 |
28 | public init() {}
29 |
30 | func toRoughDictionary() -> JSONDictionary {
31 | return [
32 | "maxRandomnessOffset": maxRandomnessOffset,
33 | "roughness": roughness,
34 | "bowing": bowing,
35 | "stroke": stroke.toHex(),
36 | "fill": fill.toHex(),
37 | "strokeWidth": strokeWidth,
38 | "curveTightness": curveTightness,
39 | "curveStepCount": curveStepCount,
40 | "fillStyle": fillStyle.rawValue,
41 | "fillWeight": fillWeight,
42 | "hachureAngle": hachureAngle,
43 | "hachureGap": hachureGap,
44 | "dashOffset": dashOffset,
45 | "dashGap": dashGap,
46 | "zigzagOffset": zigzagOffset
47 | ]
48 | }
49 | }
50 |
51 | public extension Options {
52 | init(dictionary: JSONDictionary) {
53 | maxRandomnessOffset <-? (dictionary["maxRandomnessOffset"] as? NSNumber)?.floatValue
54 | roughness <-? (dictionary["roughness"] as? NSNumber)?.floatValue
55 | bowing <-? (dictionary["bowing"] as? NSNumber)?.floatValue
56 | strokeWidth <-? (dictionary["strokeWidth"] as? NSNumber)?.floatValue
57 | curveTightness <-? (dictionary["curveTightness"] as? NSNumber)?.floatValue
58 | curveStepCount <-? (dictionary["curveStepCount"] as? NSNumber)?.floatValue
59 | fillWeight <-? (dictionary["fillWeight"] as? NSNumber)?.floatValue
60 | hachureAngle <-? (dictionary["hachureAngle"] as? NSNumber)?.floatValue
61 | hachureGap <-? (dictionary["hachureGap"] as? NSNumber)?.floatValue
62 | dashOffset <-? (dictionary["dashOffset"] as? NSNumber)?.floatValue
63 | dashGap <-? (dictionary["dashGap"] as? NSNumber)?.floatValue
64 | zigzagOffset <-? (dictionary["zigzagOffset"] as? NSNumber)?.floatValue
65 |
66 | if let fillStyleRawValue = dictionary["fillStyle"] as? String,
67 | let fillStyle = FillStyle(rawValue: fillStyleRawValue) {
68 | self.fillStyle = fillStyle
69 | }
70 |
71 | stroke <-? (dictionary["stroke"] as? String).map(UIColor.init(hex:))
72 | fill <-? (dictionary["fill"] as? String).map(UIColor.init(hex:))
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Point.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Point.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Point: Equatable {
12 | let x: Float
13 | let y: Float
14 |
15 | public init(x: Float, y: Float) {
16 | self.x = x
17 | self.y = y
18 | }
19 |
20 | public func toRoughPoint() -> [Float] {
21 | return [x, y]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Engine/Size.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Size.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 19/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Size: Equatable {
12 | public let width: Float
13 | public let height: Float
14 |
15 | public init(width: Float, height: Float) {
16 | self.width = width
17 | self.height = height
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Render/Renderer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Renderer.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 20/03/2019.
6 | // Copyright © 2019 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Convert Drawable to UIBezierPath and add to CAShapeLayer
12 | public class Renderer {
13 | /// The layer that many shape layers will be rendered onto
14 | public let layer: CALayer
15 |
16 | public init(layer: CALayer) {
17 | self.layer = layer
18 | }
19 |
20 | public func render(drawing: Drawing) {
21 | let pairs = drawing.sets.map({
22 | return ($0, self.shapeLayer(set: $0, options: drawing.options))
23 | })
24 |
25 | pairs.forEach { pair in
26 | let shapeLayer = pair.1
27 | layer.addSublayer(shapeLayer)
28 | shapeLayer.frame = layer.bounds
29 | }
30 |
31 | handlePath2DIfAny(pairs: pairs, options: drawing.options)
32 | }
33 |
34 | private func shapeLayer(set: OperationSet, options: Options) -> CAShapeLayer {
35 | let layer = CAShapeLayer()
36 | let path = UIBezierPath()
37 | layer.fillColor = nil
38 |
39 | switch set.type {
40 | case .path:
41 | path.lineWidth = CGFloat(options.strokeWidth)
42 | layer.strokeColor = options.stroke.cgColor
43 | case .fillSketch:
44 | fillSketch(path: path, layer: layer, options: options)
45 | case .fillPath:
46 | fillPath(layer: layer, options: options)
47 | case .path2DFill:
48 | fillPath(layer: layer, options: options)
49 | case .path2DPattern:
50 | fillSketch(path: path, layer: layer, options: options)
51 | break
52 | }
53 |
54 | set.operations.forEach { op in
55 | operate(op: op, path: path)
56 | }
57 |
58 | layer.path = path.cgPath
59 | return layer
60 | }
61 |
62 | /// Sketch style fill, using many stroke paths
63 | private func fillSketch(path: UIBezierPath, layer: CAShapeLayer, options: Options) {
64 | var fweight = options.fillWeight
65 | if (fweight < 0) {
66 | fweight = options.strokeWidth / 2
67 | }
68 |
69 | path.lineWidth = CGFloat(fweight)
70 | layer.strokeColor = options.fill.cgColor
71 | }
72 |
73 | /// Solid fill, using fill layer
74 | private func fillPath(layer: CAShapeLayer, options: Options) {
75 | layer.fillColor = options.fill.cgColor
76 | }
77 |
78 | private func operate(op: Operation, path: UIBezierPath) {
79 | switch op {
80 | case let op as Move:
81 | path.move(to: op.point.toCGPoint())
82 | case let op as LineTo:
83 | path.addLine(to: op.point.toCGPoint())
84 | case let op as BezierCurveTo:
85 | path.addCurve(
86 | to: op.point.toCGPoint(),
87 | controlPoint1: op.controlPoint1.toCGPoint(),
88 | controlPoint2: op.controlPoint2.toCGPoint()
89 | )
90 | case let op as QuadraticCurveTo:
91 | path.addQuadCurve(
92 | to: op.point.toCGPoint(),
93 | controlPoint: op.controlPoint.toCGPoint()
94 | )
95 | default:
96 | break
97 | }
98 | }
99 |
100 | /// Apply mask for path2DFill or path2DPattern
101 | private func handlePath2DIfAny(pairs: [(OperationSet, CAShapeLayer)], options: Options) {
102 | guard let pair = pairs.first(where: { $0.0.path != nil }) else {
103 | return
104 | }
105 |
106 | let set = pair.0
107 | let fillLayer = pair.1
108 |
109 | // Apply mask
110 | let maskLayer = CAShapeLayer()
111 | maskLayer.path = UIBezierPath(svgPath: pair.0.path!).cgPath
112 | scalePathToFrame(shapeLayer: maskLayer)
113 | fillLayer.mask = maskLayer
114 |
115 | // Somehow fillLayer loses backgroundColor, set fillColor again
116 | if (set.type == .path2DFill) {
117 | fillLayer.backgroundColor = options.fill.cgColor
118 | }
119 |
120 | pairs.forEach {
121 | scalePathToFrame(shapeLayer: $0.1)
122 | }
123 | }
124 |
125 | /// For svg path, make all path within frame
126 | private func scalePathToFrame(shapeLayer: CAShapeLayer) {
127 | guard let path = shapeLayer.path else {
128 | return
129 | }
130 |
131 | let rect = CGRect(
132 | x: 0,
133 | y: 0,
134 | width: max(layer.frame.self.width, 1),
135 | height: max(layer.frame.size.height, 1)
136 | )
137 |
138 | let bezierPath = UIBezierPath(cgPath: path)
139 | _ = bezierPath.fit(into: rect).moveCenter(to: rect.center)
140 | shapeLayer.path = bezierPath.cgPath
141 | }
142 | }
143 |
144 | extension Point {
145 | func toCGPoint() -> CGPoint {
146 | return CGPoint(x: CGFloat(x), y: CGFloat(y))
147 | }
148 | }
149 |
150 | // https://github.com/onmyway133/blog/issues/232
151 |
152 | extension CGRect {
153 | var center: CGPoint {
154 | return CGPoint( x: self.size.width/2.0,y: self.size.height/2.0)
155 | }
156 | }
157 |
158 | extension CGPoint {
159 | func vector(to p1:CGPoint) -> CGVector {
160 | return CGVector(dx: p1.x - x, dy: p1.y - y)
161 | }
162 | }
163 |
164 | extension UIBezierPath {
165 | func moveCenter(to:CGPoint) -> Self {
166 | let bounds = self.cgPath.boundingBox
167 | let center = bounds.center
168 |
169 | let zeroedTo = CGPoint(x: to.x - bounds.origin.x, y: to.y - bounds.origin.y)
170 | let vector = center.vector(to: zeroedTo)
171 |
172 | _ = offset(to: CGSize(width: vector.dx, height: vector.dy))
173 | return self
174 | }
175 |
176 | func offset(to offset:CGSize) -> Self {
177 | let t = CGAffineTransform(translationX: offset.width, y: offset.height)
178 | _ = applyCentered(transform: t)
179 | return self
180 | }
181 |
182 | func fit(into:CGRect) -> Self {
183 | let bounds = self.cgPath.boundingBox
184 |
185 | let sw = into.size.width/bounds.width
186 | let sh = into.size.height/bounds.height
187 | let factor = min(sw, max(sh, 0.0))
188 |
189 | return scale(x: factor, y: factor)
190 | }
191 |
192 | func scale(x:CGFloat, y:CGFloat) -> Self{
193 | let scale = CGAffineTransform(scaleX: x, y: y)
194 | _ = applyCentered(transform: scale)
195 | return self
196 | }
197 |
198 |
199 | func applyCentered(transform: @autoclosure () -> CGAffineTransform ) -> Self{
200 | let bound = self.cgPath.boundingBox
201 | let center = CGPoint(x: bound.midX, y: bound.midY)
202 | var xform = CGAffineTransform.identity
203 |
204 | xform = xform.concatenating(CGAffineTransform(translationX: -center.x, y: -center.y))
205 | xform = xform.concatenating(transform())
206 | xform = xform.concatenating(CGAffineTransform(translationX: center.x, y: center.y))
207 | apply(xform)
208 |
209 | return self
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Render/SVGPath.swift:
--------------------------------------------------------------------------------
1 |
2 | //
3 | // SVGPath.swift
4 | // SVGPath
5 | //
6 | // Created by Tim Wood on 1/21/15.
7 | // Copyright (c) 2015 Tim Wood. All rights reserved.
8 | //
9 |
10 | import UIKit
11 | import CoreGraphics
12 |
13 | // MARK: UIBezierPath
14 |
15 | public extension UIBezierPath {
16 | convenience init (svgPath: String) {
17 | self.init()
18 | applyCommands(from: SVGPath(svgPath))
19 | }
20 | }
21 |
22 | private extension UIBezierPath {
23 | func applyCommands(from svgPath: SVGPath) {
24 | for command in svgPath.commands {
25 | switch command.type {
26 | case .move: move(to: command.point)
27 | case .line: addLine(to: command.point)
28 | case .quadCurve: addQuadCurve(to: command.point, controlPoint: command.control1)
29 | case .cubeCurve: addCurve(to: command.point, controlPoint1: command.control1, controlPoint2: command.control2)
30 | case .close: close()
31 | }
32 | }
33 | }
34 | }
35 |
36 | // MARK: Enums
37 |
38 | fileprivate enum Coordinates {
39 | case absolute
40 | case relative
41 | }
42 |
43 | // MARK: Class
44 |
45 | public class SVGPath {
46 | public var commands: [SVGCommand] = []
47 | private var builder: SVGCommandBuilder = move
48 | private var coords: Coordinates = .absolute
49 | private var increment: Int = 2
50 | private var numbers = ""
51 |
52 | public init (_ string: String) {
53 | for char in string {
54 | switch char {
55 | case "M": use(.absolute, 2, move)
56 | case "m": use(.relative, 2, move)
57 | case "L": use(.absolute, 2, line)
58 | case "l": use(.relative, 2, line)
59 | case "V": use(.absolute, 1, lineVertical)
60 | case "v": use(.relative, 1, lineVertical)
61 | case "H": use(.absolute, 1, lineHorizontal)
62 | case "h": use(.relative, 1, lineHorizontal)
63 | case "Q": use(.absolute, 4, quadBroken)
64 | case "q": use(.relative, 4, quadBroken)
65 | case "T": use(.absolute, 2, quadSmooth)
66 | case "t": use(.relative, 2, quadSmooth)
67 | case "C": use(.absolute, 6, cubeBroken)
68 | case "c": use(.relative, 6, cubeBroken)
69 | case "S": use(.absolute, 4, cubeSmooth)
70 | case "s": use(.relative, 4, cubeSmooth)
71 | case "Z": use(.absolute, 1, close)
72 | case "z": use(.absolute, 1, close)
73 | default: numbers.append(char)
74 | }
75 | }
76 | finishLastCommand()
77 | }
78 |
79 | private func use (_ coords: Coordinates, _ increment: Int, _ builder: @escaping SVGCommandBuilder) {
80 | finishLastCommand()
81 | self.builder = builder
82 | self.coords = coords
83 | self.increment = increment
84 | }
85 |
86 | private func finishLastCommand () {
87 | for command in take(SVGPath.parseNumbers(numbers), increment: increment, coords: coords, last: commands.last, callback: builder) {
88 | commands.append(coords == .relative ? command.relative(to: commands.last) : command)
89 | }
90 | numbers = ""
91 | }
92 | }
93 |
94 | // MARK: Numbers
95 |
96 | private let numberSet = CharacterSet(charactersIn: "-.0123456789eE")
97 | private let locale = Locale(identifier: "en_US")
98 |
99 |
100 | public extension SVGPath {
101 | class func parseNumbers (_ numbers: String) -> [CGFloat] {
102 | var all:[String] = []
103 | var curr = ""
104 | var last = ""
105 |
106 | for char in numbers.unicodeScalars {
107 | let next = String(char)
108 | if next == "-" && last != "" && last != "E" && last != "e" {
109 | if curr.utf16.count > 0 {
110 | all.append(curr)
111 | }
112 | curr = next
113 | } else if numberSet.contains(UnicodeScalar(char.value)!) {
114 | curr += next
115 | } else if curr.utf16.count > 0 {
116 | all.append(curr)
117 | curr = ""
118 | }
119 | last = next
120 | }
121 |
122 | all.append(curr)
123 |
124 | return all.map { CGFloat(truncating: NSDecimalNumber(string: $0, locale: locale)) }
125 | }
126 | }
127 |
128 | // MARK: Commands
129 |
130 | public struct SVGCommand {
131 | public var point:CGPoint
132 | public var control1:CGPoint
133 | public var control2:CGPoint
134 | public var type:Kind
135 |
136 | public enum Kind {
137 | case move
138 | case line
139 | case cubeCurve
140 | case quadCurve
141 | case close
142 | }
143 |
144 | public init () {
145 | let point = CGPoint()
146 | self.init(point, point, point, type: .close)
147 | }
148 |
149 | public init (_ x: CGFloat, _ y: CGFloat, type: Kind) {
150 | let point = CGPoint(x: x, y: y)
151 | self.init(point, point, point, type: type)
152 | }
153 |
154 | public init (_ cx: CGFloat, _ cy: CGFloat, _ x: CGFloat, _ y: CGFloat) {
155 | let control = CGPoint(x: cx, y: cy)
156 | self.init(control, control, CGPoint(x: x, y: y), type: .quadCurve)
157 | }
158 |
159 | public init (_ cx1: CGFloat, _ cy1: CGFloat, _ cx2: CGFloat, _ cy2: CGFloat, _ x: CGFloat, _ y: CGFloat) {
160 | self.init(CGPoint(x: cx1, y: cy1), CGPoint(x: cx2, y: cy2), CGPoint(x: x, y: y), type: .cubeCurve)
161 | }
162 |
163 | public init (_ control1: CGPoint, _ control2: CGPoint, _ point: CGPoint, type: Kind) {
164 | self.point = point
165 | self.control1 = control1
166 | self.control2 = control2
167 | self.type = type
168 | }
169 |
170 | fileprivate func relative (to other:SVGCommand?) -> SVGCommand {
171 | if let otherPoint = other?.point {
172 | return SVGCommand(control1 + otherPoint, control2 + otherPoint, point + otherPoint, type: type)
173 | }
174 | return self
175 | }
176 | }
177 |
178 | // MARK: CGPoint helpers
179 |
180 | private func +(a:CGPoint, b:CGPoint) -> CGPoint {
181 | return CGPoint(x: a.x + b.x, y: a.y + b.y)
182 | }
183 |
184 | private func -(a:CGPoint, b:CGPoint) -> CGPoint {
185 | return CGPoint(x: a.x - b.x, y: a.y - b.y)
186 | }
187 |
188 | // MARK: Command Builders
189 |
190 | private typealias SVGCommandBuilder = ([CGFloat], SVGCommand?, Coordinates) -> SVGCommand
191 |
192 | private func take (_ numbers: [CGFloat], increment: Int, coords: Coordinates, last: SVGCommand?, callback: SVGCommandBuilder) -> [SVGCommand] {
193 | var out: [SVGCommand] = []
194 | var lastCommand:SVGCommand? = last
195 |
196 | let count = (numbers.count / increment) * increment
197 | var nums:[CGFloat] = [0, 0, 0, 0, 0, 0];
198 |
199 | for i in stride(from: 0, to: count, by: increment) {
200 | for j in 0 ..< increment {
201 | nums[j] = numbers[i + j]
202 | }
203 | lastCommand = callback(nums, lastCommand, coords)
204 | out.append(lastCommand!)
205 | }
206 |
207 | return out
208 | }
209 |
210 | // MARK: Mm - Move
211 |
212 | private func move (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
213 | return SVGCommand(numbers[0], numbers[1], type: .move)
214 | }
215 |
216 | // MARK: Ll - Line
217 |
218 | private func line (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
219 | return SVGCommand(numbers[0], numbers[1], type: .line)
220 | }
221 |
222 | // MARK: Vv - Vertical Line
223 |
224 | private func lineVertical (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
225 | return SVGCommand(coords == .absolute ? last?.point.x ?? 0 : 0, numbers[0], type: .line)
226 | }
227 |
228 | // MARK: Hh - Horizontal Line
229 |
230 | private func lineHorizontal (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
231 | return SVGCommand(numbers[0], coords == .absolute ? last?.point.y ?? 0 : 0, type: .line)
232 | }
233 |
234 | // MARK: Qq - Quadratic Curve To
235 |
236 | private func quadBroken (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
237 | return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3])
238 | }
239 |
240 | // MARK: Tt - Smooth Quadratic Curve To
241 |
242 | private func quadSmooth (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
243 | var lastControl = last?.control1 ?? CGPoint()
244 | let lastPoint = last?.point ?? CGPoint()
245 | if (last?.type ?? .line) != .quadCurve {
246 | lastControl = lastPoint
247 | }
248 | var control = lastPoint - lastControl
249 | if coords == .absolute {
250 | control = control + lastPoint
251 | }
252 | return SVGCommand(control.x, control.y, numbers[0], numbers[1])
253 | }
254 |
255 | // MARK: Cc - Cubic Curve To
256 |
257 | private func cubeBroken (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
258 | return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5])
259 | }
260 |
261 | // MARK: Ss - Smooth Cubic Curve To
262 |
263 | private func cubeSmooth (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
264 | var lastControl = last?.control2 ?? CGPoint()
265 | let lastPoint = last?.point ?? CGPoint()
266 | if (last?.type ?? .line) != .cubeCurve {
267 | lastControl = lastPoint
268 | }
269 | var control = lastPoint - lastControl
270 | if coords == .absolute {
271 | control = control + lastPoint
272 | }
273 | return SVGCommand(control.x, control.y, numbers[0], numbers[1], numbers[2], numbers[3])
274 | }
275 |
276 | // MARK: Zz - Close Path
277 |
278 | private func close (_ numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand {
279 | return SVGCommand()
280 | }
281 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/Resources/rough.js:
--------------------------------------------------------------------------------
1 |
2 | var rough=function(){"use strict";const t="undefined"!=typeof self;class e{constructor(t,e){this.defaultOptions={maxRandomnessOffset:2,roughness:1,bowing:1,stroke:"#000",strokeWidth:1,curveTightness:0,curveStepCount:9,fillStyle:"hachure",fillWeight:-1,hachureAngle:-41,hachureGap:-1,dashOffset:-1,dashGap:-1,zigzagOffset:-1},this.config=t||{},this.surface=e,this.config.options&&(this.defaultOptions=this._options(this.config.options))}_options(t){return t?Object.assign({},this.defaultOptions,t):this.defaultOptions}_drawable(t,e,s){return{shape:t,sets:e||[],options:s||this.defaultOptions}}getCanvasSize(){const t=t=>t&&"object"==typeof t&&t.baseVal&&t.baseVal.value?t.baseVal.value:t||100;return this.surface?[t(this.surface.width),t(this.surface.height)]:[100,100]}computePolygonSize(t){if(t.length){let e=t[0][0],s=t[0][0],i=t[0][1],h=t[0][1];for(let n=1;n0?f-=2*Math.PI:n&&f<0&&(f+=2*Math.PI),this._numSegs=Math.ceil(Math.abs(f/(Math.PI/2))),this._delta=f/this._numSegs,this._T=8/3*Math.sin(this._delta/4)*Math.sin(this._delta/4)/Math.sin(this._delta/2)}getNextSegment(){if(this._segIndex===this._numSegs)return null;const t=Math.cos(this._theta),e=Math.sin(this._theta),s=this._theta+this._delta,i=Math.cos(s),h=Math.sin(s),n=[this._cosPhi*this._rx*i-this._sinPhi*this._ry*h+this._C[0],this._sinPhi*this._rx*i+this._cosPhi*this._ry*h+this._C[1]],a=[this._from[0]+this._T*(-this._cosPhi*this._rx*e-this._sinPhi*this._ry*t),this._from[1]+this._T*(-this._sinPhi*this._rx*e+this._cosPhi*this._ry*t)],o=[n[0]+this._T*(this._cosPhi*this._rx*h+this._sinPhi*this._ry*i),n[1]+this._T*(this._sinPhi*this._rx*h-this._cosPhi*this._ry*i)];return this._theta=s,this._from=[n[0],n[1]],this._segIndex++,{cp1:a,cp2:o,to:n}}calculateVectorAngle(t,e,s,i){const h=Math.atan2(e,t),n=Math.atan2(i,s);return n>=h?n-h:2*Math.PI-(h-n)}}class o{constructor(t,e){this.sets=t,this.closed=e}fit(t){const e=[];for(const s of this.sets){const i=s.length;let h=Math.floor(t*i);if(h<5){if(i<=5)continue;h=5}e.push(this.reduce(s,h))}let s="";for(const t of e){for(let e=0;ee;){let t=-1,e=-1;for(let i=1;i0))break;s.splice(e,1)}return s}}class r{constructor(t,e){this.xi=Number.MAX_VALUE,this.yi=Number.MAX_VALUE,this.px1=t[0],this.py1=t[1],this.px2=e[0],this.py2=e[1],this.a=this.py2-this.py1,this.b=this.px1-this.px2,this.c=this.px2*this.py1-this.px1*this.py2,this._undefined=0===this.a&&0===this.b&&0===this.c}isUndefined(){return this._undefined}intersects(t){if(this.isUndefined()||t.isUndefined())return!1;let e=Number.MAX_VALUE,s=Number.MAX_VALUE,i=0,h=0;const n=this.a,a=this.b,o=this.c;return Math.abs(a)>1e-5&&(e=-n/a,i=-o/a),Math.abs(t.b)>1e-5&&(s=-t.a/t.b,h=-t.c/t.b),e===Number.MAX_VALUE?s===Number.MAX_VALUE?-o/n==-t.c/t.a&&(this.py1>=Math.min(t.py1,t.py2)&&this.py1<=Math.max(t.py1,t.py2)?(this.xi=this.px1,this.yi=this.py1,!0):this.py2>=Math.min(t.py1,t.py2)&&this.py2<=Math.max(t.py1,t.py2)&&(this.xi=this.px2,this.yi=this.py2,!0)):(this.xi=this.px1,this.yi=s*this.xi+h,!((this.py1-this.yi)*(this.yi-this.py2)<-1e-5||(t.py1-this.yi)*(this.yi-t.py2)<-1e-5)&&(!(Math.abs(t.a)<1e-5)||!((t.px1-this.xi)*(this.xi-t.px2)<-1e-5))):s===Number.MAX_VALUE?(this.xi=t.px1,this.yi=e*this.xi+i,!((t.py1-this.yi)*(this.yi-t.py2)<-1e-5||(this.py1-this.yi)*(this.yi-this.py2)<-1e-5)&&(!(Math.abs(n)<1e-5)||!((this.px1-this.xi)*(this.xi-this.px2)<-1e-5))):e===s?i===h&&(this.px1>=Math.min(t.px1,t.px2)&&this.px1<=Math.max(t.py1,t.py2)?(this.xi=this.px1,this.yi=this.py1,!0):this.px2>=Math.min(t.px1,t.px2)&&this.px2<=Math.max(t.px1,t.px2)&&(this.xi=this.px2,this.yi=this.py2,!0)):(this.xi=(h-i)/(e-s),this.yi=e*this.xi+i,!((this.px1-this.xi)*(this.xi-this.px2)<-1e-5||(t.px1-this.xi)*(this.xi-t.px2)<-1e-5))}}function l(t,e){const s=t[1][1]-t[0][1],i=t[0][0]-t[1][0],h=s*t[0][0]+i*t[0][1],n=e[1][1]-e[0][1],a=e[0][0]-e[1][0],o=n*e[0][0]+a*e[0][1],r=s*a-n*i;return r?[Math.round((a*h-i*o)/r),Math.round((s*o-n*h)/r)]:null}class c{constructor(t,e,s,i,h,n,a,o){this.deltaX=0,this.hGap=0,this.top=t,this.bottom=e,this.left=s,this.right=i,this.gap=h,this.sinAngle=n,this.tanAngle=o,Math.abs(n)<1e-4?this.pos=s+h:Math.abs(n)>.9999?this.pos=t+h:(this.deltaX=(e-t)*Math.abs(o),this.pos=s-Math.abs(this.deltaX),this.hGap=Math.abs(h/a),this.sLeft=new r([s,e],[s,t]),this.sRight=new r([i,e],[i,t]))}nextLine(){if(Math.abs(this.sinAngle)<1e-4){if(this.pos.9999){if(this.posthis.right&&e>this.right;)if(this.pos+=this.hGap,t=this.pos-this.deltaX/2,e=this.pos+this.deltaX/2,this.pos>this.right+this.deltaX)return null;const h=new r([t,s],[e,i]);this.sLeft&&h.intersects(this.sLeft)&&(t=h.xi,s=h.yi),this.sRight&&h.intersects(this.sRight)&&(e=h.xi,i=h.yi),this.tanAngle>0&&(t=this.right-(t-this.left),e=this.right-(e-this.left));const n=[t,s,e,i];return this.pos+=this.hGap,n}}return null}}function p(t){const e=t[0],s=t[1];return Math.sqrt(Math.pow(e[0]-s[0],2)+Math.pow(e[1]-s[1],2))}function u(t,e){const s=[],i=new r([t[0],t[1]],[t[2],t[3]]);for(let t=0;t{s[0]=Math.min(s[0],t[0]),s[1]=Math.max(s[1],t[0]),i[0]=Math.min(i[0],t[1]),i[1]=Math.max(i[1],t[1])});const h=function(t){let e=0,s=0,i=0;for(let s=0;s0?e.hachureGap:4*e.strokeWidth,o=[];if(t.length>2)for(let e=0;e{const e=l(t,a);e&&e[0]>=s[0]&&e[0]<=s[1]&&e[1]>=i[0]&&e[1]<=i[1]&&r.push(e)})}r=this.removeDuplocatePoints(r);const p=this.createLinesFromCenter(h,r);return{type:"fillSketch",ops:this.drawLines(p,e)}}fillEllipse(t,e,s,i,h){return this.fillArcSegment(t,e,s,i,0,2*Math.PI,h)}fillArc(t,e,s,i,h,n,a){return this.fillArcSegment(t,e,s,i,h,n,a)}fillArcSegment(t,e,s,i,h,n,a){const o=[t,e],r=s/2,l=i/2,c=Math.max(s/2,i/2);let p=a.hachureGap;p<0&&(p=4*a.strokeWidth);const u=Math.max(1,Math.abs(n-h)*c/p);let f=[];for(let t=0;t{const i=t[0],h=t[1];s=s.concat(this.helper.doubleLineOps(i[0],i[1],h[0],h[1],e))}),s}createLinesFromCenter(t,e){return e.map(e=>[t,e])}removeDuplocatePoints(t){const e=new Set;return t.filter(t=>{const s=t.join(",");return!e.has(s)&&(e.add(s),!0)})}}class m{constructor(t){this.helper=t}fillPolygon(t,e){const s=d(t,e);return{type:"fillSketch",ops:this.dashedLine(s,e)}}fillEllipse(t,e,s,i,h){const n=g(this.helper,t,e,s,i,h);return{type:"fillSketch",ops:this.dashedLine(n,h)}}fillArc(t,e,s,i,h,n,a){return null}dashedLine(t,e){const s=e.dashOffset<0?e.hachureGap<0?4*e.strokeWidth:e.hachureGap:e.dashOffset,i=e.dashGap<0?e.hachureGap<0?4*e.strokeWidth:e.hachureGap:e.dashGap;let h=[];return t.forEach(t=>{const n=p(t),a=Math.floor(n/(s+i)),o=(n+i-a*(s+i))/2;let r=t[0],l=t[1];r[0]>l[0]&&(r=t[1],l=t[0]);const c=Math.atan((l[1]-r[1])/(l[0]-r[0]));for(let t=0;t{const h=p(t),n=Math.round(h/(2*e));let a=t[0],o=t[1];a[0]>o[0]&&(a=t[1],o=t[0]);const r=Math.atan((o[1]-a[1])/(o[0]-a[0]));for(let t=0;t2){let h=[];for(let e=0;e2*Math.PI&&(f=0,d=2*Math.PI);const g=2*Math.PI/r.curveStepCount,y=Math.min(g/2,(d-f)/2),M=G(y,l,c,p,u,f,d,1,r),x=G(y,l,c,p,u,f,d,1.5,r);let _=M.concat(x);return a&&(o?_=(_=_.concat(R(l,c,l+p*Math.cos(f),c+u*Math.sin(f),r))).concat(R(l,c,l+p*Math.cos(d),c+u*Math.sin(d),r)):(_.push({op:"lineTo",data:[l,c]}),_.push({op:"lineTo",data:[l+p*Math.cos(f),c+u*Math.sin(f)]}))),{type:"path",ops:_}}function z(t,e){const s=[];if(t.length){const i=e.maxRandomnessOffset||0,h=t.length;if(h>2){s.push({op:"move",data:[t[0][0]+W(i,e),t[0][1]+W(i,e)]});for(let n=1;no&&(r=Math.sqrt(o)/10);const l=r/2,c=.2+.2*Math.random();let p=h.bowing*h.maxRandomnessOffset*(i-e)/200,u=h.bowing*h.maxRandomnessOffset*(t-s)/200;p=W(p,h),u=W(u,h);const f=[],d=()=>W(l,h),g=()=>W(r,h);return n&&(a?f.push({op:"move",data:[t+d(),e+d()]}):f.push({op:"move",data:[t+W(r,h),e+W(r,h)]})),a?f.push({op:"bcurveTo",data:[p+t+(s-t)*c+d(),u+e+(i-e)*c+d(),p+t+2*(s-t)*c+d(),u+e+2*(i-e)*c+d(),s+d(),i+d()]}):f.push({op:"bcurveTo",data:[p+t+(s-t)*c+g(),u+e+(i-e)*c+g(),p+t+2*(s-t)*c+g(),u+e+2*(i-e)*c+g(),s+g(),i+g()]}),f}function D(t,e,s){const i=[];i.push([t[0][0]+W(e,s),t[0][1]+W(e,s)]),i.push([t[0][0]+W(e,s),t[0][1]+W(e,s)]);for(let h=1;h3){const n=[],a=1-s.curveTightness;h.push({op:"move",data:[t[1][0],t[1][1]]});for(let e=1;e+2=2){let n=+e.data[0],a=+e.data[1];s&&(n+=t.x,a+=t.y);const o=1*(i.maxRandomnessOffset||0);n+=W(o,i),a+=W(o,i),t.setPosition(n,a),h.push({op:"move",data:[n,a]})}break}case"L":case"l":{const s="l"===e.key;if(e.data.length>=2){let n=+e.data[0],a=+e.data[1];s&&(n+=t.x,a+=t.y),h=h.concat(R(t.x,t.y,n,a,i)),t.setPosition(n,a)}break}case"H":case"h":{const s="h"===e.key;if(e.data.length){let n=+e.data[0];s&&(n+=t.x),h=h.concat(R(t.x,t.y,n,t.y,i)),t.setPosition(n,t.y)}break}case"V":case"v":{const s="v"===e.key;if(e.data.length){let n=+e.data[0];s&&(n+=t.y),h=h.concat(R(t.x,t.y,t.x,n,i)),t.setPosition(t.x,n)}break}case"Z":case"z":t.first&&(h=h.concat(R(t.x,t.y,t.first[0],t.first[1],i)),t.setPosition(t.first[0],t.first[1]),t.first=null);break;case"C":case"c":{const s="c"===e.key;if(e.data.length>=6){let n=+e.data[0],a=+e.data[1],o=+e.data[2],r=+e.data[3],l=+e.data[4],c=+e.data[5];s&&(n+=t.x,o+=t.x,l+=t.x,a+=t.y,r+=t.y,c+=t.y);const p=B(n,a,o,r,l,c,t,i);h=h.concat(p),t.bezierReflectionPoint=[l+(l-o),c+(c-r)]}break}case"S":case"s":{const n="s"===e.key;if(e.data.length>=4){let a=+e.data[0],o=+e.data[1],r=+e.data[2],l=+e.data[3];n&&(a+=t.x,r+=t.x,o+=t.y,l+=t.y);let c=a,p=o;const u=s?s.key:"";let f=null;"c"!==u&&"C"!==u&&"s"!==u&&"S"!==u||(f=t.bezierReflectionPoint),f&&(c=f[0],p=f[1]);const d=B(c,p,a,o,r,l,t,i);h=h.concat(d),t.bezierReflectionPoint=[r+(r-a),l+(l-o)]}break}case"Q":case"q":{const s="q"===e.key;if(e.data.length>=4){let n=+e.data[0],a=+e.data[1],o=+e.data[2],r=+e.data[3];s&&(n+=t.x,o+=t.x,a+=t.y,r+=t.y);const l=1*(1+.2*i.roughness),c=1.5*(1+.22*i.roughness);h.push({op:"move",data:[t.x+W(l,i),t.y+W(l,i)]});let p=[o+W(l,i),r+W(l,i)];h.push({op:"qcurveTo",data:[n+W(l,i),a+W(l,i),p[0],p[1]]}),h.push({op:"move",data:[t.x+W(c,i),t.y+W(c,i)]}),p=[o+W(c,i),r+W(c,i)],h.push({op:"qcurveTo",data:[n+W(c,i),a+W(c,i),p[0],p[1]]}),t.setPosition(p[0],p[1]),t.quadReflectionPoint=[o+(o-n),r+(r-a)]}break}case"T":case"t":{const n="t"===e.key;if(e.data.length>=2){let a=+e.data[0],o=+e.data[1];n&&(a+=t.x,o+=t.y);let r=a,l=o;const c=s?s.key:"";let p=null;"q"!==c&&"Q"!==c&&"t"!==c&&"T"!==c||(p=t.quadReflectionPoint),p&&(r=p[0],l=p[1]);const u=1*(1+.2*i.roughness),f=1.5*(1+.22*i.roughness);h.push({op:"move",data:[t.x+W(u,i),t.y+W(u,i)]});let d=[a+W(u,i),o+W(u,i)];h.push({op:"qcurveTo",data:[r+W(u,i),l+W(u,i),d[0],d[1]]}),h.push({op:"move",data:[t.x+W(f,i),t.y+W(f,i)]}),d=[a+W(f,i),o+W(f,i)],h.push({op:"qcurveTo",data:[r+W(f,i),l+W(f,i),d[0],d[1]]}),t.setPosition(d[0],d[1]),t.quadReflectionPoint=[a+(a-r),o+(o-l)]}break}case"A":case"a":{const s="a"===e.key;if(e.data.length>=7){const n=+e.data[0],o=+e.data[1],r=+e.data[2],l=+e.data[3],c=+e.data[4];let p=+e.data[5],u=+e.data[6];if(s&&(p+=t.x,u+=t.y),p===t.x&&u===t.y)break;if(0===n||0===o)h=h.concat(R(t.x,t.y,p,u,i)),t.setPosition(p,u);else for(let e=0;e<1;e++){const e=new a([t.x,t.y],[p,u],[n,o],r,!!l,!!c);let s=e.getNextSegment();for(;s;){const n=B(s.cp1[0],s.cp1[1],s.cp2[0],s.cp2[1],s.to[0],s.to[1],t,i);h=h.concat(n),s=e.getNextSegment()}}}break}}return h}class U extends e{line(t,e,s,i,h){const n=this._options(h);return this._drawable("line",[S(t,e,s,i,n)],n)}rectangle(t,e,s,i,h){const n=this._options(h),a=[];if(n.fill){const h=[[t,e],[t+s,e],[t+s,e+i],[t,e+i]];"solid"===n.fillStyle?a.push(z(h,n)):a.push(L(h,n))}return a.push(E(t,e,s,i,n)),this._drawable("rectangle",a,n)}ellipse(t,e,s,i,h){const n=this._options(h),a=[];if(n.fill)if("solid"===n.fillStyle){const h=T(t,e,s,i,n);h.type="fillPath",a.push(h)}else a.push(function(t,e,s,i,h){return P(h,v).fillEllipse(t,e,s,i,h)}(t,e,s,i,n));return a.push(T(t,e,s,i,n)),this._drawable("ellipse",a,n)}circle(t,e,s,i){const h=this.ellipse(t,e,s,s,i);return h.shape="circle",h}linearPath(t,e){const s=this._options(e);return this._drawable("linearPath",[A(t,!1,s)],s)}arc(t,e,s,i,h,n,a=!1,o){const r=this._options(o),l=[];if(a&&r.fill)if("solid"===r.fillStyle){const a=C(t,e,s,i,h,n,!0,!1,r);a.type="fillPath",l.push(a)}else l.push(function(t,e,s,i,h,n,a){const o=P(a,v).fillArc(t,e,s,i,h,n,a);if(o)return o;const r=t,l=e;let c=Math.abs(s/2),p=Math.abs(i/2);c+=W(.01*c,a),p+=W(.01*p,a);let u=h,f=n;for(;u<0;)u+=2*Math.PI,f+=2*Math.PI;f-u>2*Math.PI&&(u=0,f=2*Math.PI);const d=(f-u)/a.curveStepCount,g=[];for(let t=u;t<=f;t+=d)g.push([r+c*Math.cos(t),l+p*Math.sin(t)]);return g.push([r+c*Math.cos(f),l+p*Math.sin(f)]),g.push([r,l]),L(g,a)}(t,e,s,i,h,n,r));return l.push(C(t,e,s,i,h,n,a,!0,r)),this._drawable("arc",l,r)}curve(t,e){const s=this._options(e);return this._drawable("curve",[O(t,s)],s)}polygon(t,e){const s=this._options(e),i=[];if(s.fill)if("solid"===s.fillStyle)i.push(z(t,s));else{const e=this.computePolygonSize(t),h=L([[0,0],[e[0],0],[e[0],e[1]],[0,e[1]]],s);h.type="path2Dpattern",h.size=e,h.path=this.polygonPath(t),i.push(h)}return i.push(A(t,!0,s)),this._drawable("polygon",i,s)}path(t,e){const s=this._options(e),i=[];if(!t)return this._drawable("path",i,s);if(s.fill)if("solid"===s.fillStyle){const e={type:"path2Dfill",path:t,ops:[]};i.push(e)}else{const e=this.computePathSize(t),h=L([[0,0],[e[0],0],[e[0],e[1]],[0,e[1]]],s);h.type="path2Dpattern",h.size=e,h.path=t,i.push(h)}return i.push(function(t,e){t=(t||"").replace(/\n/g," ").replace(/(-\s)/g,"-").replace("/(ss)/g"," ");let s=new n(t);if(e.simplification){const t=new o(s.linearPoints,s.closed).fit(e.simplification);s=new n(t)}let i=[];const h=s.segments||[];for(let t=0;t0?h[t-1]:null,e);n&&n.length&&(i=i.concat(n))}return{type:"path",ops:i}}(t,s)),this._drawable("path",i,s)}}const V="undefined"!=typeof document;class j{constructor(t){this.canvas=t,this.ctx=this.canvas.getContext("2d")}draw(t){const e=t.sets||[],s=t.options||this.getDefaultOptions(),i=this.ctx;for(const t of e)switch(t.type){case"path":i.save(),i.strokeStyle=s.stroke,i.lineWidth=s.strokeWidth,this._drawToContext(i,t),i.restore();break;case"fillPath":i.save(),i.fillStyle=s.fill||"",this._drawToContext(i,t),i.restore();break;case"fillSketch":this.fillSketch(i,t,s);break;case"path2Dfill":{this.ctx.save(),this.ctx.fillStyle=s.fill||"";const e=new Path2D(t.path);this.ctx.fill(e),this.ctx.restore();break}case"path2Dpattern":{const e=this.canvas.ownerDocument||V&&document;if(e){const i=t.size,h=e.createElement("canvas"),n=h.getContext("2d"),a=this.computeBBox(t.path);a&&(a.width||a.height)?(h.width=this.canvas.width,h.height=this.canvas.height,n.translate(a.x||0,a.y||0)):(h.width=i[0],h.height=i[1]),this.fillSketch(n,t,s),this.ctx.save(),this.ctx.fillStyle=this.ctx.createPattern(h,"repeat");const o=new Path2D(t.path);this.ctx.fill(o),this.ctx.restore()}else console.error("Cannot render path2Dpattern. No defs/document defined.");break}}}computeBBox(t){if(V)try{const e="http://www.w3.org/2000/svg",s=document.createElementNS(e,"svg");s.setAttribute("width","0"),s.setAttribute("height","0");const i=self.document.createElementNS(e,"path");i.setAttribute("d",t),s.appendChild(i),document.body.appendChild(s);const h=i.getBBox();return document.body.removeChild(s),h}catch(t){}return null}fillSketch(t,e,s){let i=s.fillWeight;i<0&&(i=s.strokeWidth/2),t.save(),t.strokeStyle=s.fill||"",t.lineWidth=i,this._drawToContext(t,e),t.restore()}_drawToContext(t,e){t.beginPath();for(const s of e.ops){const e=s.data;switch(s.op){case"move":t.moveTo(e[0],e[1]);break;case"bcurveTo":t.bezierCurveTo(e[0],e[1],e[2],e[3],e[4],e[5]);break;case"qcurveTo":t.quadraticCurveTo(e[0],e[1],e[2],e[3]);break;case"lineTo":t.lineTo(e[0],e[1])}}"fillPath"===e.type?t.fill():t.stroke()}}class F extends j{constructor(t,e){super(t),this.gen=new U(e||null,this.canvas)}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}line(t,e,s,i,h){const n=this.gen.line(t,e,s,i,h);return this.draw(n),n}rectangle(t,e,s,i,h){const n=this.gen.rectangle(t,e,s,i,h);return this.draw(n),n}ellipse(t,e,s,i,h){const n=this.gen.ellipse(t,e,s,i,h);return this.draw(n),n}circle(t,e,s,i){const h=this.gen.circle(t,e,s,i);return this.draw(h),h}linearPath(t,e){const s=this.gen.linearPath(t,e);return this.draw(s),s}polygon(t,e){const s=this.gen.polygon(t,e);return this.draw(s),s}arc(t,e,s,i,h,n,a=!1,o){const r=this.gen.arc(t,e,s,i,h,n,a,o);return this.draw(r),r}curve(t,e){const s=this.gen.curve(t,e);return this.draw(s),s}path(t,e){const s=this.gen.path(t,e);return this.draw(s),s}}const Q="undefined"!=typeof document;class Z{constructor(t){this.svg=t}get defs(){const t=this.svg.ownerDocument||Q&&document;if(t&&!this._defs){const e=t.createElementNS("http://www.w3.org/2000/svg","defs");this.svg.firstChild?this.svg.insertBefore(e,this.svg.firstChild):this.svg.appendChild(e),this._defs=e}return this._defs||null}draw(t){const e=t.sets||[],s=t.options||this.getDefaultOptions(),i=this.svg.ownerDocument||window.document,h=i.createElementNS("http://www.w3.org/2000/svg","g");for(const t of e){let e=null;switch(t.type){case"path":(e=i.createElementNS("http://www.w3.org/2000/svg","path")).setAttribute("d",this.opsToPath(t)),e.style.stroke=s.stroke,e.style.strokeWidth=s.strokeWidth+"",e.style.fill="none";break;case"fillPath":(e=i.createElementNS("http://www.w3.org/2000/svg","path")).setAttribute("d",this.opsToPath(t)),e.style.stroke="none",e.style.strokeWidth="0",e.style.fill=s.fill||null;break;case"fillSketch":e=this.fillSketch(i,t,s);break;case"path2Dfill":(e=i.createElementNS("http://www.w3.org/2000/svg","path")).setAttribute("d",t.path||""),e.style.stroke="none",e.style.strokeWidth="0",e.style.fill=s.fill||null;break;case"path2Dpattern":if(this.defs){const h=t.size,n=i.createElementNS("http://www.w3.org/2000/svg","pattern"),a=`rough-${Math.floor(Math.random()*(Number.MAX_SAFE_INTEGER||999999))}`;n.setAttribute("id",a),n.setAttribute("x","0"),n.setAttribute("y","0"),n.setAttribute("width","1"),n.setAttribute("height","1"),n.setAttribute("height","1"),n.setAttribute("viewBox",`0 0 ${Math.round(h[0])} ${Math.round(h[1])}`),n.setAttribute("patternUnits","objectBoundingBox");const o=this.fillSketch(i,t,s);n.appendChild(o),this.defs.appendChild(n),(e=i.createElementNS("http://www.w3.org/2000/svg","path")).setAttribute("d",t.path||""),e.style.stroke="none",e.style.strokeWidth="0",e.style.fill=`url(#${a})`}else console.error("Cannot render path2Dpattern. No defs/document defined.")}e&&h.appendChild(e)}return h}fillSketch(t,e,s){let i=s.fillWeight;i<0&&(i=s.strokeWidth/2);const h=t.createElementNS("http://www.w3.org/2000/svg","path");return h.setAttribute("d",this.opsToPath(e)),h.style.stroke=s.fill||null,h.style.strokeWidth=i+"",h.style.fill="none",h}}class H extends Z{constructor(t,e){super(t),this.gen=new U(e||null,this.svg)}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}opsToPath(t){return this.gen.opsToPath(t)}line(t,e,s,i,h){const n=this.gen.line(t,e,s,i,h);return this.draw(n)}rectangle(t,e,s,i,h){const n=this.gen.rectangle(t,e,s,i,h);return this.draw(n)}ellipse(t,e,s,i,h){const n=this.gen.ellipse(t,e,s,i,h);return this.draw(n)}circle(t,e,s,i){const h=this.gen.circle(t,e,s,i);return this.draw(h)}linearPath(t,e){const s=this.gen.linearPath(t,e);return this.draw(s)}polygon(t,e){const s=this.gen.polygon(t,e);return this.draw(s)}arc(t,e,s,i,h,n,a=!1,o){const r=this.gen.arc(t,e,s,i,h,n,a,o);return this.draw(r)}curve(t,e){const s=this.gen.curve(t,e);return this.draw(s)}path(t,e){const s=this.gen.path(t,e);return this.draw(s)}}return{canvas:(t,e)=>new F(t,e),svg:(t,e)=>new H(t,e),generator:(t,e)=>new U(t,e)}}();
3 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/RoughUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoughUIView.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | final class RoughUIView: UIView {
12 | var drawbles: [Drawable]?
13 | var options: Options?
14 | var previousBounds: CGRect = .zero
15 |
16 | override func layoutSubviews() {
17 | super.layoutSubviews()
18 |
19 | if previousBounds != bounds {
20 | previousBounds = bounds
21 |
22 | if let drawbles = drawbles, let options = options {
23 | update(drawables: drawbles, options: options)
24 | }
25 | }
26 | }
27 |
28 | private func update(drawables: [Drawable], options: Options) {
29 | layer.sublayers?.forEach {
30 | $0.removeFromSuperlayer()
31 | }
32 |
33 | let renderer = Renderer(layer: layer)
34 | let generator = Engine.shared.generator(size: bounds.size)
35 | for drawable in drawables {
36 | if let drawing = generator.generate(drawable: drawable, options: options) {
37 | renderer.render(drawing: drawing)
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/RoughSwift/RoughView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoughView.swift
3 | // RoughSwift
4 | //
5 | // Created by khoa on 26/03/2022.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | public struct RoughView: UIViewRepresentable {
12 | var options = Options()
13 | var drawables: [Drawable] = []
14 |
15 | public init() {}
16 |
17 | public func makeUIView(context: Context) -> UIView {
18 | let view = RoughUIView()
19 | return view
20 | }
21 |
22 | public func updateUIView(_ uiView: UIView, context: Context) {
23 | guard let view = uiView as? RoughUIView else { return }
24 | view.drawbles = drawables
25 | view.options = options
26 | view.setNeedsLayout()
27 | }
28 | }
29 |
30 | public extension RoughView {
31 | func maxRandomnessOffset(_ value: Float) -> Self {
32 | var v = self
33 | v.options.maxRandomnessOffset = value
34 | return v
35 | }
36 |
37 | func roughness(_ value: Float) -> Self {
38 | var v = self
39 | v.options.roughness = value
40 | return v
41 | }
42 |
43 | func bowing(_ value: Float) -> Self {
44 | var v = self
45 | v.options.bowing = value
46 | return v
47 | }
48 |
49 | func strokeWidth(_ value: Float) -> Self {
50 | var v = self
51 | v.options.strokeWidth = value
52 | return v
53 | }
54 |
55 | func fillWeight(_ value: Float) -> Self {
56 | var v = self
57 | v.options.fillWeight = value
58 | return v
59 | }
60 |
61 | func dashOffset(_ value: Float) -> Self {
62 | var v = self
63 | v.options.dashOffset = value
64 | return v
65 | }
66 |
67 | func zigzagOffset(_ value: Float) -> Self {
68 | var v = self
69 | v.options.zigzagOffset = value
70 | return v
71 | }
72 |
73 | func dashGap(_ value: Float) -> Self {
74 | var v = self
75 | v.options.dashGap = value
76 | return v
77 | }
78 |
79 | func hachureGap(_ value: Float) -> Self {
80 | var v = self
81 | v.options.hachureGap = value
82 | return v
83 | }
84 |
85 | func hachureAngle(_ value: Float) -> Self {
86 | var v = self
87 | v.options.hachureAngle = value
88 | return v
89 | }
90 |
91 | func curveTightness(_ value: Float) -> Self {
92 | var v = self
93 | v.options.curveTightness = value
94 | return v
95 | }
96 |
97 | func curveStepCount(_ value: Float) -> Self {
98 | var v = self
99 | v.options.curveStepCount = value
100 | return v
101 | }
102 |
103 | func stroke(_ value: UIColor) -> Self {
104 | var v = self
105 | v.options.stroke = value
106 | return v
107 | }
108 |
109 | func fill(_ value: UIColor) -> Self {
110 | var v = self
111 | v.options.fill = value
112 | return v
113 | }
114 |
115 | func fillStyle(_ value: FillStyle) -> Self {
116 | var v = self
117 | v.options.fillStyle = value
118 | return v
119 | }
120 |
121 | func draw(_ drawable: Drawable) -> Self {
122 | var v = self
123 | v.drawables.append(drawable)
124 | return v
125 | }
126 |
127 | func rectangle() -> Self {
128 | draw(FullRectangle())
129 | }
130 |
131 | func circle() -> Self {
132 | draw(FullCircle())
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Tests/RoughSwiftTests/RoughSwiftTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import RoughSwift
3 |
4 | final class RoughSwiftTests: XCTestCase {
5 | func testDrawing() throws {
6 | let engine = Engine()
7 | let generator = engine.generator(size: CGSize(width: 300, height: 300))
8 |
9 | let drawing = try XCTUnwrap(
10 | generator.generate(drawable: Rectangle(x: 10, y: 20, width: 100, height: 200))
11 | )
12 |
13 | XCTAssertEqual(drawing.shape, "rectangle")
14 | XCTAssertEqual(drawing.sets.count, 2)
15 |
16 | let set = drawing.sets[0]
17 | XCTAssertEqual(set.operations.count, 208)
18 | }
19 |
20 | func testDrawingWithOption() throws {
21 | let engine = Engine()
22 | let generator = engine.generator(size: CGSize(width: 300, height: 300))
23 |
24 | var options = Options()
25 | options.hachureAngle = 60
26 | options.hachureGap = 8
27 | options.fillStyle = .zigzag
28 | options.fill = UIColor.red
29 | let drawing = try XCTUnwrap(
30 | generator.generate(drawable: Circle(x: 50, y: 150, diameter: 80), options: options)
31 | )
32 |
33 | XCTAssertEqual(drawing.shape, "circle")
34 | XCTAssertEqual(drawing.sets.count, 2)
35 |
36 | let set = drawing.sets[0]
37 | XCTAssertTrue(set.operations.count == 68 || set.operations.count == 76)
38 |
39 | XCTAssertEqual(drawing.options.fillStyle, .zigzag)
40 | XCTAssertEqual(drawing.options.hachureAngle, 60)
41 | XCTAssertEqual(drawing.options.hachureGap, 8)
42 | }
43 |
44 | func testRenderer() throws {
45 | let size = CGSize(width: 300, height: 300)
46 | let engine = Engine()
47 | let generator = engine.generator(size: size)
48 |
49 | var options = Options()
50 | options.fill = UIColor.red
51 | options.stroke = UIColor.green
52 |
53 | let drawing = try XCTUnwrap(generator.generate(
54 | drawable: Rectangle(x: 10, y: 10, width: 50, height: 50),
55 | options: options
56 | ))
57 |
58 | let view = UIView(frame: CGRect(origin: .zero, size: size))
59 | let renderer = Renderer(layer: view.layer)
60 | renderer.render(drawing: drawing)
61 |
62 | XCTAssertEqual(view.layer.frame.size, size)
63 | }
64 |
65 | func testRectangle() {
66 | let size = CGSize(width: 300, height: 300)
67 | let engine = Engine()
68 | let generator = engine.generator(size: size)
69 |
70 | let drawing = generator.generate(
71 | drawable: Rectangle(x: 10, y: 20, width: 100, height: 200)
72 | )
73 | XCTAssertNotNil(drawing)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------