I am trying to use a SKCropNode with a width of at least 5000 pixels, yet if the maskNode property width is greater than 4100 something strange happens -- the first 100-200 or so pixels are not cropped, and the cropping spans approx only 4100 pixels.
I'm on a Mac Mini M1, Ventura 13.0.1 and Xcode 14.1 (14B47b)
Here is the working code:
import SwiftUI
import SpriteKit
struct ContentView: View {
#State var spriteView : SpriteView!
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
ZStack {
if spriteView != nil {
spriteView
}
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
.padding()
.onAppear {
spriteView = .init(scene: myScene)
}
.frame(minWidth: 1000, minHeight: 500)
}
}
var myScene : SKScene {
let r = SKScene.init(size: .init(width: 2000, height: 500))
r.backgroundColor = .yellow
let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 5000, height: 300))
// let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096+2, height: 300))
maskRect.fillColor = .white
maskRect.lineWidth = 0
let redRect = SKShapeNode.init(rect: .init(origin: .zero, size: .init(width: 5000, height: 500)))
redRect.fillColor = .red
redRect.lineWidth = 0
let path = CGMutablePath.init()
path.move(to: .zero)
path.addLine(to: .init(x: 1000, y: 500))
let blueLine = SKShapeNode.init(path: path)
blueLine.strokeColor = .blue
blueLine.lineWidth = 12
redRect.addChild(blueLine)
let cropNode = SKCropNode.init()
cropNode.maskNode = SKNode.init()
cropNode.maskNode?.addChild(maskRect)
cropNode.addChild(redRect)
let blackRect = SKShapeNode.init(rect: .init(origin: .init(x: 0, y: 100), size: .init(width: 5000, height: 300)))
blackRect.fillColor = .black
blackRect.lineWidth = 0
r.addChild(blackRect)
r.addChild(cropNode)
return r
}
}
The result I get is
If I change the width to 4096 I get the correct result without the black rectangle on the left hand side, ie let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096, height: 300))
EDIT:
Using SKSpriteNode as suggested produced the same results for me:
let maskRect = SKSpriteNode.init(color: .black, size: .init(width: 5000, height: 300))
maskRect.position.x = 0 + maskRect.size.width/2
maskRect.position.y = 100 + maskRect.size.height/2
this appears to be an issue with SKShapeNode.
solution: use SKSpriteNode instead -- both for your mask and for any other large elements in the scene. here is an illustration (swap the red and green nodes to see the glitch):
import SwiftUI
import SpriteKit
struct GameSceneCrop: View {
#State private var scene = CropScene()
var body: some View {
SpriteView(scene: scene)
}
}
class CropScene: SKScene {
let skCameraNode = SKCameraNode()
var WIDTH:CGFloat = 5000
var HEIGHT:CGFloat = 1000
let container = SKNode()
override func didMove(to view: SKView) {
scaleMode = .aspectFill // Set the scale mode to scale to fit the window
backgroundColor = .yellow
anchorPoint = CGPoint(x: 0.5, y: 0.5)
//set up camera node
camera = skCameraNode
addChild(skCameraNode)
skCameraNode.setScale( 10000 )
addChild(container)
rebuild()
}
func rebuild() {
container.removeAllChildren()
//a green SKSpriteNode
let green = SKSpriteNode(color: .green, size: CGSize(width: WIDTH, height: HEIGHT))
//a red SKShapeNode
let red = SKShapeNode(rectOf: CGSize(width: WIDTH, height: HEIGHT))
red.fillColor = .red
//crop node
let mask = SKSpriteNode(color: .black, size: red.frame.size) //using SKSpriteNode here instead of SKShapeNode
let crop_node = SKCropNode()
crop_node.maskNode = mask
crop_node.addChild(green) //<--- replace green SKSpriteNode with red SKShapeNode to see clipping glitch
container.addChild(crop_node)
//draw outline around red block
let reference_frame = SKShapeNode(rect: red.frame.insetBy(dx: -50, dy: -50))
reference_frame.strokeColor = .black
reference_frame.lineWidth = 50
container.addChild(reference_frame)
}
override func update(_ currentTime: TimeInterval) {
let x = 0.65 + (sin(currentTime*2) + 1) / 4
WIDTH = 5000 * x
rebuild()
}
}
Related
I'm trying to draw labels above lines using a Canvas in SwiftUI. The lines draw at the correction position, however the labels are never drawn above the lines (see screenshot which shows both the "1" and "2" label drawn atop each other).
I use a for loop to get the position data from an array:
Canvas {context, size in
let font = Font.custom("Arial Rounded MT Bold", size: 16)
for line in TLDataModel.lines {
let resolved = context.resolve(Text(line.tickLabel).font(font))
let midPoint = CGPoint(x: line.points.startIndex, y: line.points.endIndex)
context.draw(resolved, at: midPoint, anchor: .center)
print("Line Label: \(line.tickLabel)")
print("..for points: \(line.points)")
var path = Path()
path.addLines(line.points)
context.stroke(path, with: .color(.blue), lineWidth: 1.0)
}
}.frame(width: (400), height: 100, alignment: .leading)
And my class:
class testDataModel: ObservableObject {
#Published var lines: [line] = []
init(){
var nl = line(points: [CGPoint(x: 20.0, y: 0.0), CGPoint(x: 20.0, y: 100.0)], tickLabel: "1")
addLines(newLine: nl)
nl = line(points: [CGPoint(x: 50.0, y: 0.0), CGPoint(x: 50, y: 100.0)], tickLabel: "2")
addLines(newLine: nl)
}
func addLines (newLine: line){
lines.append(newLine)
}
}
public struct line: Identifiable {
public let id = UUID()
let points: [CGPoint]
let tickLabel: String
}
I have modified Line graph of Minh Nguyen to some extend to show two lines one for systolic and othere for diastolic.
The first image show the how the graph should look like and second image is what I have achieved.
struct PointEntry {
let systolic: Int
let diastolic: Int
let label: String
}
extension PointEntry: Comparable {
static func <(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic < rhs.systolic || lhs.systolic < rhs.systolic
}
static func ==(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic == rhs.systolic && lhs.diastolic == rhs.diastolic
}
}
class LineChart: UIView {
/// gap between each point
let lineGap: CGFloat = 30.0
/// preseved space at top of the chart
let topSpace: CGFloat = 20.0
/// preserved space at bottom of the chart to show labels along the Y axis
let bottomSpace: CGFloat = 40.0
/// The top most horizontal line in the chart will be 10% higher than the highest value in the chart
let topHorizontalLine: CGFloat = 110.0 / 100.0
/// Dot inner Radius
var innerRadius: CGFloat = 8
/// Dot outer Radius
var outerRadius: CGFloat = 12
var dataEntries: [PointEntry]? {
didSet {
self.setNeedsLayout()
}
}
/// Contains the main line which represents the data
private let dataLayer: CALayer = CALayer()
/// Contains dataLayer and gradientLayer
private let mainLayer: CALayer = CALayer()
/// Contains mainLayer and label for each data entry
private let scrollView: UIScrollView = {
let view = UIScrollView()
view.showsVerticalScrollIndicator = false
view.showsHorizontalScrollIndicator = false
return view
}()
/// Contains horizontal lines
private let gridLayer: CALayer = CALayer()
/// An array of CGPoint on dataLayer coordinate system that the main line will go through. These points will be calculated from dataEntries array
private var systolicDataPoint: [CGPoint]?
private var daistolicDataPoint: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
mainLayer.addSublayer(dataLayer)
mainLayer.addSublayer(gridLayer)
scrollView.layer.addSublayer(mainLayer)
self.addSubview(scrollView)
}
override func layoutSubviews() {
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
if let dataEntries = dataEntries {
scrollView.contentSize = CGSize(width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
dataLayer.frame = CGRect(x: 0, y: topSpace, width: mainLayer.frame.width, height: mainLayer.frame.height - topSpace - bottomSpace)
systolicGradientLayer.frame = dataLayer.frame
diastolicGradientLayer.frame = dataLayer.frame
systolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: true)
daistolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: false)
gridLayer.frame = CGRect(x: 0, y: topSpace, width: CGFloat(dataEntries.count) * lineGap + 30, height: mainLayer.frame.height - topSpace - bottomSpace)
clean()
drawHorizontalLines()
drawVerticleLine()
drawChart(for: systolicDataPoint, color: .blue)
drawChart(for: daistolicDataPoint, color: .green)
drawLables()
}
}
/// Convert an array of PointEntry to an array of CGPoint on dataLayer coordinate system
/// - Parameter entries: Arrays of PointEntry
private func convertDataEntriesToPoints(entries: [PointEntry], isSystolic: Bool) -> [CGPoint] {
var result: [CGPoint] = []
// let gridValues: [CGFloat] = [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.05]
for (index, value) in entries.enumerated() {
let difference: CGFloat = 0.125 / 30
let userValue: CGFloat = isSystolic ? CGFloat(value.systolic) : CGFloat(value.diastolic)
var height = (userValue - 30.0) * difference
height = (1.0 - height) * gridLayer.frame.size.height
let point = CGPoint(x: CGFloat(index)*lineGap + 40, y: height)
result.append(point)
}
return result
}
/// Draw a zigzag line connecting all points in dataPoints
private func drawChart(for points: [CGPoint]?, color: UIColor) {
if let dataPoints = points, dataPoints.count > 0 {
guard let path = createPath(for: points) else { return }
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.strokeColor = color.cgColor
lineLayer.fillColor = UIColor.clear.cgColor
dataLayer.addSublayer(lineLayer)
}
}
/// Create a zigzag bezier path that connects all points in dataPoints
private func createPath(for points: [CGPoint]?) -> UIBezierPath? {
guard let dataPoints = points, dataPoints.count > 0 else {
return nil
}
let path = UIBezierPath()
path.move(to: dataPoints[0])
for i in 1..<dataPoints.count {
path.addLine(to: dataPoints[i])
}
return path
}
/// Create titles at the bottom for all entries showed in the chart
private func drawLables() {
if let dataEntries = dataEntries,
dataEntries.count > 0 {
for i in 0..<dataEntries.count {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: lineGap*CGFloat(i) - lineGap/2 + 40, y: mainLayer.frame.size.height - bottomSpace/2 - 8, width: lineGap, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 11
textLayer.string = dataEntries[i].label
mainLayer.addSublayer(textLayer)
}
}
}
/// Create horizontal lines (grid lines) and show the value of each line
private func drawHorizontalLines() {
let gridValues: [CGFloat] = [1.05, 1.0, 0.875, 0.75, 0.625, 0.5, 0.375, 0.25, 0.125]
let gridText = ["", "30", "60", "90", "120", "150", "180", "210", "240"]
for (index, value) in gridValues.enumerated() {
let height = value * gridLayer.frame.size.height
let path = UIBezierPath()
if value == gridValues.first! {
path.move(to: CGPoint(x: 30, y: height))
} else {
path.move(to: CGPoint(x: 28, y: height))
}
path.addLine(to: CGPoint(x: gridLayer.frame.size.width, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
if value != gridValues.first! {
lineLayer.lineDashPattern = [4, 4]
}
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 4, y: height-8, width: 50, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 12
textLayer.string = gridText[index]
gridLayer.addSublayer(textLayer)
}
}
private func drawVerticleLine() {
let height = gridLayer.frame.size.height * 1.05
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 0))
path.addLine(to: CGPoint(x: 30, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
}
private func clean() {
mainLayer.sublayers?.forEach({
if $0 is CATextLayer {
$0.removeFromSuperlayer()
}
})
dataLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
gridLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
}
}
How can I add shadow to lines like shown in the first image and add simple line drawing animation to the Graph?
I created a subclass of SKNode:
class SquareDrop: SKNode {
init(image: SKSpriteNode) {
super.init()
self.setScale(0.3
//Set the starting position of the node
self.position = CGPoint(x: 0.5, y: UIScreen.main.bounds.height/2)
//Apply a physics body to the node
self.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "square"), size: CGSize(width: image.size.width * 0.3, height: image.size.height * 0.3))
self.addChild(image)
}
I created an instance in GameScene class and I set up it:
func spawnRain()
squareDrop = SquareDrop(image: SKSpriteNode(imageNamed: "square"))
squareDrop?.physicsBody?.linearDamping = 5
squareDrop?.physicsBody?.restitution = 0
self.addChild(squareDrop!)
}
It is the result:
It works like aspected, but instead of using an image I wanted to draw a square:
func spawnRain() {
//Here I did not use the class that I had created previously
let shape = SKShapeNode()
shape.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 100, height: 100), cornerRadius: 5).cgPath
shape.position = CGPoint(x: frame.midX - shape.frame.width/2 , y: UIScreen.main.bounds.height/2)
shape.fillColor = UIColor.red
shape.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 100))
addChild(shape)
}
It doesn't seem like I aspected (the squares that I created have a strange behaviour). Any hints?
I am sorry for the question, but I am a noob of SpriteKit.
I am trying to change the square crop tool to circular one for the images selected from Photo Library in Swift 4. I tried many old codes available in here with no luck. Can somebody please help me.
There are mainly 2 issues in this code.
It doesn't hide the already existing square cropping area.
It shows the choose and cancel buttons under the old overlay, so cant tap on it.
Any help would be appreciated.
My code:
private func hideDefaultEditOverlay(view: UIView)
{
for subview in view.subviews
{
if let cropOverlay = NSClassFromString("PLCropOverlayCropView")
{
if subview.isKind(of: cropOverlay) {
subview.isHidden = true
break
}
else {
hideDefaultEditOverlay(view: subview)
}
}
}
}
private func addCircleOverlayToImageViewer(viewController: UIViewController) {
let circleColor = UIColor.clear
let maskColor = UIColor.black.withAlphaComponent(0.8)
let screenHeight = UIScreen.main.bounds.size.height
let screenWidth = UIScreen.main.bounds.size.width
hideDefaultEditOverlay(view: viewController.view)
let circleLayer = CAShapeLayer()
let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: screenHeight - screenWidth, width: screenWidth, height: screenWidth))
circlePath.usesEvenOddFillRule = true
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = circleColor.cgColor
let maskPath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight), cornerRadius: 0)
maskPath.append(circlePath)
maskPath.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.fillColor = maskColor.cgColor
viewController.view.layer.addSublayer(maskLayer)
// Move and Scale label
let label = UILabel(frame: CGRect(x: 0, y: 20, width: view.frame.width, height: 50))
label.text = "Move and Scale"
label.textAlignment = .center
label.textColor = UIColor.white
viewController.view.addSubview(label)
}
I am having some odd behavior in SpriteKit when creating a texture. The function below shows you what I am doing. In short, I'm in SceneKit and making a SCNNode out of an Array of colors (think pixel/voxels). It works like a charm. However, after exactly 104 calls the texture returned is nil. Afterwards, it is hit or miss whether the texture will be nil or not. I am also providing the exact color information. Thoughts?
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}
This is where autoreleasepool comes in handy, it allows you to release the memory when the autoreleasepool is finished so that you do not run out of space before using it again.
Of course this is not going to solve your main problem, where you are creating too many textures and running out of memory space, but it will allow you to at least make some more because it will release the temporary memory that view.texture(from:node) is holding on to.
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
autoreleasepool{
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
}
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}