TL;DR: I'm using a Path
to specify the hit area for an Image
. But I don't know how to adjust the Path
coordinates to match the layout that SwiftUI decides on... or if I even need to do so.
Runtime
My (test) app looks like this, Image
borders (not frames) colored for clarity:
What I want is for taps in the opaque orange to be handled by that Image
. Taps outside the opaque orange — even if within the bounds of the orange image — should "fall through" either to the green image or the gray background. But no. (The purple outline shows where the path is, according to itself; see code below).
Details
Here's the intrinsic (in pixels) layout of the images:
The path around the opaque part is trivially seen to be
[(200, 200), (600, 200), (600, 600), (200, 600)]
How do these coordinates and the Image
coordinates relate?
Code
extension CGPoint {
typealias Tuple = (x:CGFloat, y:CGFloat)
init(tuple t: Tuple) {
self.init(x: t.x, y: t.y)
}
}
struct ContentView: View {
var points = [(200, 200), (600, 200), (600, 600), (200, 600)]
.map(CGPoint.init(tuple:))
// .map { p in p.shifted(dx:-64, dy:-10)} // Didn't seem to help.
var path : Path {
var result = Path()
result.move(to: points.first!)
result.addLines(points)
result.closeSubpath()
return result
}
var body: some View {
ZStack{
Image("gray") // Just so we record all touches.
.resizable()
.frame(maxWidth : .infinity,maxHeight: .infinity)
.onTapGesture {
print("background")
}
Image("square_green")
.resizable()
.scaledToFit()
.border(Color.green, width: 4)
.offset(x: 64, y:10) // So the two Images don't overlap completely.
.onTapGesture {
print("green")
}
Image("square_orange")
.resizable()
.scaledToFit()
.contentShape(path) // Magic should happen here.
.border(Color.orange, width: 4)
.offset(x: -64, y:-10)
// .contentShape(path) // Didn't work here either.
.onTapGesture {
print("orange")
}
path.stroke(Color.purple) // Origin at absolute (200,200) as expected.
}
}
}
Asperi was correct, and it dawned on me when I read Paul Hudson and grasped the (single) requirement of Shape
— a path(in rect: CGRect) -> Path method. The rect
parameter tells you all you need to know about the local coordinate system: namely, its size.
My working code now looks like this.
Helpers
extension CGPoint {
func scaled(xFactor:CGFloat, yFactor:CGFloat) -> CGPoint {
return CGPoint(x: x * xFactor, y: y * yFactor)
}
typealias SelfMap = (CGPoint) -> CGPoint
static func scale(_ designSize: CGSize, into displaySize: CGSize) -> SelfMap {{
$0.scaled(
xFactor: displaySize.width / designSize.width,
yFactor: displaySize.height / designSize.height
)
}}
typealias Tuple = (x:CGFloat, y:CGFloat)
init(tuple t: Tuple) {
self.init(x: t.x, y: t.y)
}
}
Drawing the Path in proper context
// This is just the ad-hoc solution.
// You will want to parameterize the designSize and points.
let designSize = CGSize(width:800, height:800)
let opaqueBorder = [(200, 200), (600, 200), (600, 600), (200, 600)]
// To find boundary of real-life images, see Python code below.
struct Mask : Shape {
func path(in rect: CGRect) -> Path {
let points = opaqueBorder
.map(CGPoint.init(tuple:))
// *** Here we use the context *** (rect.size)
.map(CGPoint.scale(designSize, into:rect.size))
var result = Path()
result.move(to: points.first!)
result.addLines(points)
result.closeSubpath()
return result
}
}
Using the Mask
struct ContentView: View {
var body: some View {
ZStack{
Image("gray") // Just so we record all touches.
.resizable()
.frame(
maxWidth : .infinity,
maxHeight: .infinity
)
.onTapGesture {
print("background")
}
// Adding mask here left as exercise.
Image("square_green")
.resizable()
.scaledToFit()
.border(Color.green, width: 4)
.offset(x: 64, y:10) // So the two Images don't overlap completely.
.onTapGesture {
print("green")
}
Image("square_orange")
.resizable()
.scaledToFit()
.border(Color.orange, width: 4)
// Sanity check shows the Mask outline.
.overlay(Mask().stroke(Color.purple))
// *** Actual working Mask ***
.contentShape(Mask())
.offset(x: -64, y:-10)
.onTapGesture {
print("orange")
}
}
}
}
Getting the Outline
#!/usr/bin/python3
# From https://www.reddit.com/r/Python/comments/f2kv1/question_on_tracing_an_image_in_python_with_pil
import sys
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
# Find simple border.
fname = sys.argv[1]
image = pygame.image.load(fname)
bitmask = pygame.mask.from_surface(image)
comp = bitmask.connected_component()
outline = comp.outline(48)
print("name: ", fname)
print("size: ", image.get_rect().size)
print("points:", outline)
# Sanity check.
# From https://www.geeksforgeeks.org/python-pil-imagedraw-draw-polygon-method
from PIL import Image, ImageDraw, ImagePath
import math
box = ImagePath.Path(outline).getbbox()
bsize = list(map(int, map(math.ceil, box[2:])))
im = Image.new("RGB", bsize, "white")
draw = ImageDraw.Draw(im)
draw.polygon(outline, fill="#e0c0ff", outline="purple")
im.show() # That this works is amazing.