{"id":13694319,"url":"https://github.com/FlexMonkey/ForceSketch","last_synced_at":"2025-05-03T01:32:30.648Z","repository":{"id":45575863,"uuid":"43816266","full_name":"FlexMonkey/ForceSketch","owner":"FlexMonkey","description":"Demonstration of a Sketching App Using 3D Touch","archived":false,"fork":false,"pushed_at":"2019-10-28T07:32:06.000Z","size":1284,"stargazers_count":109,"open_issues_count":3,"forks_count":22,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-07T07:13:29.331Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FlexMonkey.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-10-07T13:06:41.000Z","updated_at":"2023-08-12T17:49:39.000Z","dependencies_parsed_at":"2022-09-21T21:21:48.588Z","dependency_job_id":null,"html_url":"https://github.com/FlexMonkey/ForceSketch","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FlexMonkey%2FForceSketch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FlexMonkey%2FForceSketch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FlexMonkey%2FForceSketch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FlexMonkey%2FForceSketch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FlexMonkey","download_url":"https://codeload.github.com/FlexMonkey/ForceSketch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224346493,"owners_count":17296223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T17:01:29.266Z","updated_at":"2024-11-12T20:32:02.339Z","avatar_url":"https://github.com/FlexMonkey.png","language":"Swift","readme":"# ForceSketch\n####Demonstration of a Sketching App Using 3D Touch\n\n![screenshot](ForceSketch/ForceSketch.gif)\n\n#####Companion project to this blog post: http://flexmonkey.blogspot.co.uk/2015/10/forcesketch-3d-touch-drawing-app-using.html\n\nFollowing on from my recent posts on 3D Touch and touch coalescing, combining the two things together in a simple drawing application seemed like an obvious next step. This also gives me the chance to tinker with `CIImageAccumulator` which was newly introduced in iOS 9.\n\nMy little demo app, ForceSketch, allows the user to draw on their iPhone 6 screen. Both the line weight and the line colour are linked to the touch pressure. Much like my ChromaTouch demo, the pressure controls the hue, so the very lightest touch is red, turning to green at a third of maximum pressure, to blue at two thirds and back to red at maximum pressure. \n\nOnce the user lifts their finger, two Core Image filters, `CIColorControls` and `CIGaussianBlur` kick in and fade the drawing out.\n\n##Drawing Mechanics of ForceSketch\n\nThe drawing code is all called from my view controller's `touchesMoved` method. It's in here that I create a `UIImage` instance based on the coalesced touches and composite that image overthe existing image accumulator. In a production application, I'd probably do the image filtering in a background thread to improve the performance of the user interface but, for this demo, I think this approach is OK.\n\nThe opening guard statement ensures I have non-optional constants for the most important items:\n\n```swift\n    guard let touch = touches.first,\n        event = event,\n        coalescedTouches = event.coalescedTouchesForTouch(touch) else\n    {\n        return\n    }\n```\n\nThe next step is to prepare for creating the image object. To do this, I need to begin an image context and create a reference to the current context:\n\n```swift\n    UIGraphicsBeginImageContext(view.frame.size)\n\n    let cgContext = UIGraphicsGetCurrentContext()\n```\n\nTo ensure I get maximum fidelity of the user's gesture, I loop over the coalesced touches - this gives me all the intermediate touches that may have happened between invocations of `touchesMoved()`.\n\n```swift\n    for coalescedTouch in coalescedTouches {\n```\n\nUsing the force property of each touch, I create constants for the line segments colour and weight. To ensure users of non-3D Touch devices call still use the app, I check `forceTouchCapability` and give those users a fixed weight and colour:\n\n```swift\n    let lineWidth = (traitCollection.forceTouchCapability == UIForceTouchCapability.Available) ?\n        (coalescedTouch.force / coalescedTouch.maximumPossibleForce) * 20 :\n        10\n    \n    let lineColor = (traitCollection.forceTouchCapability == UIForceTouchCapability.Available) ?\n        UIColor(hue: coalescedTouch.force / coalescedTouch.maximumPossibleForce, saturation: 1, brightness: 1, alpha: 1).CGColor :\n        UIColor.grayColor().CGColor\n```\n\nWith these constants I can set the line width and stroke colour in the graphics context:\n\n```swift\n    CGContextSetLineWidth(cgContext, lineWidth)\n    CGContextSetStrokeColorWithColor(cgContext, lineColor)\n```\n\n...and I'm now ready to define the beginning and end of my line segment for this coalesced touch:\n\n```swift\n    CGContextMoveToPoint(cgContext,\n        previousTouchLocation!.x,\n        previousTouchLocation!.y)\n\n    CGContextAddLineToPoint(cgContext,\n        coalescedTouch.locationInView(view).x,\n        coalescedTouch.locationInView(view).y)\n```\n\nThe final steps inside the coalesced touches loop is to stroke the path and update `previousTouchLocation`:\n\n```swift\n    CGContextStrokePath(cgContext)\n\n    previousTouchLocation = coalescedTouch.locationInView(view)\n```\n\nOnce all of the strokes have been added to the graphics context, it's one line of code to create a `UIImage` instance and then end the context:\n\n```swift\n    let drawnImage = UIGraphicsGetImageFromCurrentImageContext()\n\n    UIGraphicsEndImageContext()\n```\n\n##Displaying the Drawn Lines\n\nTo display the newly drawn lines held in drawnImage, I use a `CISourceOverCompositing` filter with `drawnImage` as the foreground image and the image accumulator's current image as the background: \n\n```swift\n    compositeFilter.setValue(CIImage(image: drawnImage),\n        forKey: kCIInputImageKey)\n        \n    compositeFilter.setValue(imageAccumulator.image(),\n        forKey: kCIInputBackgroundImageKey)\n```\n\nThen take the output of the source over compositor, pass that back into the accumulator and populate my `UIImageView` with the accumulator's image:\n\n```swift\n    imageAccumulator.setImage(compositeFilter.valueForKey(kCIOutputImageKey) as! CIImage)\n\n    imageView.image = UIImage(CIImage: imageAccumulator.image())\n```\n    \n##Blurry Fade Out\n\nOnce the user lifts their finger, I do a \"blurry fade out\" of the drawn image. This effect uses two Core Image filters which are defined as constants: \n\n```swift\n    let hsb = CIFilter(name: \"CIColorControls\",\n        withInputParameters: [kCIInputBrightnessKey: 0.05])!\n    let gaussianBlur = CIFilter(name: \"CIGaussianBlur\",\n        withInputParameters: [kCIInputRadiusKey: 1])!\n```\n\nThe first part of the effect is to use a CADisplayLink which will invoke step() with each screen refresh:\n\n```swift\n    let displayLink = CADisplayLink(target: self, selector: Selector(\"step\"))\n    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)\n```\n\nI rely on previousTouchLocation being nil to infer the user has finished their touch. If that's the case, I simply pass the accumulator's current image into the HSB / colour control filter, pass that filter's output into the Gaussian Blur and finally the blur's output back into the accumulator:\n\n```swift\n    hsb.setValue(imageAccumulator.image(), forKey: kCIInputImageKey)\n    gaussianBlur.setValue(hsb.valueForKey(kCIOutputImageKey) as! CIImage, forKey: kCIInputImageKey)\n    \n    imageAccumulator.setImage(gaussianBlur.valueForKey(kCIOutputImageKey) as! CIImage)\n\n    imageView.image = UIImage(CIImage: imageAccumulator.image())\n```\n    \n##Source Code\n\nAs always, the source code for this project is available in my GitHub repository here. Enjoy!\n","funding_links":[],"categories":["Misc"],"sub_categories":["Notes"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFlexMonkey%2FForceSketch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FFlexMonkey%2FForceSketch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFlexMonkey%2FForceSketch/lists"}