From 60ca1d90a3a168e7e250e81d75b3fbb5ea0c60ac Mon Sep 17 00:00:00 2001 From: Darren Ford Date: Tue, 6 Aug 2024 10:11:40 +1000 Subject: [PATCH] Added simple inner-shadow drawing support --- Bitmap.podspec | 2 +- Sources/Bitmap/drawing/Bitmap+Shadow.swift | 42 +++++++++++++ .../Bitmap/utils/CGContext+extensions.swift | 7 +++ .../Bitmap/utils/CGContext+innerShadow.swift | 60 +++++++++++++++++++ Tests/BitmapTests/BitmapTests.swift | 35 +++++++---- 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 Sources/Bitmap/utils/CGContext+innerShadow.swift diff --git a/Bitmap.podspec b/Bitmap.podspec index 9335cc1..379898a 100644 --- a/Bitmap.podspec +++ b/Bitmap.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'Bitmap' -s.version = '1.1.0' +s.version = '1.2.0' s.summary = 'A Swift-y convenience for loading, saving and manipulating bitmap images.' s.homepage = 'https://github.com/dagronf/Bitmap' s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/Sources/Bitmap/drawing/Bitmap+Shadow.swift b/Sources/Bitmap/drawing/Bitmap+Shadow.swift index 137833d..0d64134 100644 --- a/Sources/Bitmap/drawing/Bitmap+Shadow.swift +++ b/Sources/Bitmap/drawing/Bitmap+Shadow.swift @@ -36,7 +36,11 @@ public extension Bitmap { self.color = color } } +} + +// MARK: - Shadows +public extension Bitmap { /// Apply a shadow to a drawing block /// - Parameters: /// - shadow: The shadow style @@ -48,3 +52,41 @@ public extension Bitmap { } } } + +// MARK: - Inner shadows + +public extension Bitmap { + /// Draw a path using an inner shadow + /// - Parameters: + /// - path: The path + /// - fillColor: The color to fill the path, or nil for no color + /// - shadow: The shadow definition + func drawInnerShadow(_ path: CGPath, fillColor: CGColor? = nil, shadow: Bitmap.Shadow) { + self.draw { ctx in + if let fillColor = fillColor { + ctx.setFillColor(fillColor) + ctx.addPath(path) + ctx.fillPath() + } + + ctx.drawInnerShadow( + in: path, + shadowColor: shadow.color, + offset: shadow.offset, + blurRadius: shadow.blur + ) + } + } + + /// Create a new bitmap by drawing a path using an inner shadow + /// - Parameters: + /// - path: The path + /// - fillColor: The color to fill the path, or nil for no color + /// - shadow: The shadow definition + /// - Returns: A new bitmap + func drawingInnerShadow(_ path: CGPath, fillColor: CGColor? = nil, shadow: Bitmap.Shadow) throws -> Bitmap { + let copy = try self.copy() + copy.drawInnerShadow(path, fillColor: fillColor, shadow: shadow) + return copy + } +} diff --git a/Sources/Bitmap/utils/CGContext+extensions.swift b/Sources/Bitmap/utils/CGContext+extensions.swift index 9642307..9998b3b 100644 --- a/Sources/Bitmap/utils/CGContext+extensions.swift +++ b/Sources/Bitmap/utils/CGContext+extensions.swift @@ -27,4 +27,11 @@ extension CGContext { defer { self.restoreGState() } try drawBlock(self) } + + /// Wrap the drawing commands in `block` within a transparency layer + @inlinable func usingTransparencyLayer(auxiliaryInfo: CFDictionary? = nil, _ block: () -> Void) { + self.beginTransparencyLayer(auxiliaryInfo: auxiliaryInfo) + defer { self.endTransparencyLayer() } + block() + } } diff --git a/Sources/Bitmap/utils/CGContext+innerShadow.swift b/Sources/Bitmap/utils/CGContext+innerShadow.swift new file mode 100644 index 0000000..a4e756d --- /dev/null +++ b/Sources/Bitmap/utils/CGContext+innerShadow.swift @@ -0,0 +1,60 @@ +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import CoreGraphics +import Foundation + +public extension CGContext { + /// Draw an inner shadow in the path + /// - Parameters: + /// - path: The path to apply the inner shadow to + /// - shadowColor: Specifies the color of the shadow, which may contain a non-opaque alpha value. If NULL, then shadowing is disabled. + /// - offset: Specifies a translation in base-space. + /// - blurRadius: A non-negative number specifying the amount of blur. + /// + /// **Inner Shadows in Quartz: Helftone** + /// [Blog Article](https://blog.helftone.com/demystifying-inner-shadows-in-quartz/) + /// [(Archived)](https://web.archive.org/web/20221206132428/https://blog.helftone.com/demystifying-inner-shadows-in-quartz/) + func drawInnerShadow(in path: CGPath, shadowColor: CGColor?, offset: CGSize, blurRadius: CGFloat) { + guard + let shadowColor = shadowColor, + let opaqueShadowColor = shadowColor.copy(alpha: 1.0) + else { + // No shadow color specified, therefore no shadow. + return + } + + self.savingGState { ctx in + ctx.addPath(path) + ctx.clip() + ctx.setAlpha(shadowColor.alpha) + ctx.usingTransparencyLayer { + ctx.setShadow(offset: offset, blur: blurRadius, color: opaqueShadowColor) + ctx.setBlendMode(.sourceOut) + ctx.setFillColor(opaqueShadowColor) + ctx.addPath(path) + ctx.fillPath() + } + } + } +} diff --git a/Tests/BitmapTests/BitmapTests.swift b/Tests/BitmapTests/BitmapTests.swift index 9b07cb9..8a02c4a 100644 --- a/Tests/BitmapTests/BitmapTests.swift +++ b/Tests/BitmapTests/BitmapTests.swift @@ -255,20 +255,35 @@ final class BitmapTests: XCTestCase { func testShadow() throws { markdown.h2("Shadow drawing") - let bitmap = try Bitmap(width: 255, height: 255) - bitmap.applyingShadow(Bitmap.Shadow()) { bitmap in - bitmap.fill(CGRect(x: 10, y: 10, width: 100, height: 100).path, .init(gray: 0.5, alpha: 1)) + do { + let bitmap = try Bitmap(width: 255, height: 255) + bitmap.applyingShadow(Bitmap.Shadow()) { bitmap in + bitmap.fill(CGRect(x: 10, y: 10, width: 100, height: 100).path, .init(gray: 0.5, alpha: 1)) + } + + bitmap.applyingShadow(Bitmap.Shadow(offset: CGSize(width: -3, height: 3), color: CGColor.blue)) { bitmap in + bitmap.stroke( + CGRect(x: 110, y: 110, width: 100, height: 100).path, + Bitmap.Stroke(color: CGColor.red, lineWidth: 2) + ) + } + + let image = try XCTUnwrap(bitmap.cgImage) + try markdown.image(image, linked: true) } - bitmap.applyingShadow(Bitmap.Shadow(offset: CGSize(width: -3, height: 3), color: CGColor.blue)) { bitmap in - bitmap.stroke( - CGRect(x: 110, y: 110, width: 100, height: 100).path, - Bitmap.Stroke(color: CGColor.red, lineWidth: 2) - ) + markdown.br() + + do { + let bitmap = try Bitmap(width: 300, height: 300, backgroundColor: CGColor.white) + let path = CGPath(roundedRect: CGRect(x: 20, y: 20, width: 260, height: 260), cornerWidth: 10, cornerHeight: 10, transform: nil) + let shadow = Bitmap.Shadow(offset: .init(width: 4, height: -4), blur: 20, color: .blue) + let b = try bitmap.drawingInnerShadow(path, fillColor: CGColor(red: 1, green: 1, blue: 0.4, alpha: 1), shadow: shadow) + + let image = try XCTUnwrap(b.cgImage) + try markdown.image(image, width: 150, linked: true) } - let image = try XCTUnwrap(bitmap.cgImage) - try markdown.image(image, linked: true) markdown.br() }