Skip to content

Commit

Permalink
Added simple inner-shadow drawing support
Browse files Browse the repository at this point in the history
  • Loading branch information
dagronf committed Aug 6, 2024
1 parent f071ca7 commit 60ca1d9
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Bitmap.podspec
Original file line number Diff line number Diff line change
@@ -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' }
Expand Down
42 changes: 42 additions & 0 deletions Sources/Bitmap/drawing/Bitmap+Shadow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
7 changes: 7 additions & 0 deletions Sources/Bitmap/utils/CGContext+extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
60 changes: 60 additions & 0 deletions Sources/Bitmap/utils/CGContext+innerShadow.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
35 changes: 25 additions & 10 deletions Tests/BitmapTests/BitmapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down

0 comments on commit 60ca1d9

Please sign in to comment.