SwiftUI 路径作为 contentShape,与图像不一致
SwiftUI Path as contentShape, doesn't line up with Image
TL;DR:我正在使用 Path
来指定 Image
的命中区域。但我不知道如何调整 Path
坐标以匹配 SwiftUI 决定的布局......或者我是否需要这样做。
运行时间
我的(测试)应用程序看起来像这样,Image
边框(不是 框架)为清晰起见着色:
我 想要 的是 Image
处理不透明橙色中的点击。在不透明的橙色之外点击——即使在橙色图像的边界内——应该 "fall through" 到绿色图像或灰色背景。但不是。 (紫色轮廓根据自身显示路径的位置;请参阅下面的代码)。
详情
这是图像的固有(以像素为单位)布局:
不透明部分周围的路径通常被视为
[(200, 200), (600, 200), (600, 600), (200, 600)]
这些坐标和 Image
坐标有什么关系?
代码
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.
}
}
}
1) .offset
只是移动了内容,但是视图在视图层级布局中的位置没有改变,所以改用.position
;
2) contentShape
的形状应该在视图坐标 space
中构建
Asperi 是正确的,当我阅读 Paul Hudson 并掌握了 Shape
的(单一)要求时,我恍然大悟——一条路径(in rect: CGRect) -> 路径方法。 rect
参数告诉你所有你需要知道的关于局部坐标系的信息:即它的大小。
我的工作代码现在看起来像这样。
帮手
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 {{
[=10=].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)
}
}
在适当的上下文中绘制路径
// 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
}
}
使用面具
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")
}
}
}
}
获取大纲
#!/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.
TL;DR:我正在使用 Path
来指定 Image
的命中区域。但我不知道如何调整 Path
坐标以匹配 SwiftUI 决定的布局......或者我是否需要这样做。
运行时间
我的(测试)应用程序看起来像这样,Image
边框(不是 框架)为清晰起见着色:
我 想要 的是 Image
处理不透明橙色中的点击。在不透明的橙色之外点击——即使在橙色图像的边界内——应该 "fall through" 到绿色图像或灰色背景。但不是。 (紫色轮廓根据自身显示路径的位置;请参阅下面的代码)。
详情
这是图像的固有(以像素为单位)布局:
不透明部分周围的路径通常被视为
[(200, 200), (600, 200), (600, 600), (200, 600)]
这些坐标和 Image
坐标有什么关系?
代码
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.
}
}
}
1) .offset
只是移动了内容,但是视图在视图层级布局中的位置没有改变,所以改用.position
;
2) contentShape
的形状应该在视图坐标 space
Asperi 是正确的,当我阅读 Paul Hudson 并掌握了 Shape
的(单一)要求时,我恍然大悟——一条路径(in rect: CGRect) -> 路径方法。 rect
参数告诉你所有你需要知道的关于局部坐标系的信息:即它的大小。
我的工作代码现在看起来像这样。
帮手
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 {{
[=10=].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)
}
}
在适当的上下文中绘制路径
// 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
}
}
使用面具
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")
}
}
}
}
获取大纲
#!/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.