├── .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 | ![](Screenshots/s.png) 2 | 3 | Checkout https://indiegoodies.com/ 4 | 5 | ![](Screenshots/s1.png) 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 | ![](Screenshots/green_rectangle.png) 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 | ![](Screenshots/circles.png) 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 | ![](Screenshots/svg.png) 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 | ![](Screenshots/chart.png) 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 | --------------------------------------------------------------------------------