In an app I'm testing out, I'm using a gradient image as a background (590kb in size). I have the ability of breaking this image into 0.5 pixel vertical strips (19kb) and having this image repeat itself (1334 times) to create the same larger background image. I have 20 different background images total so the app will be 11.8mb vs 380kb in storage usage.
I don't like the idea of having 1334 sprites, but I do like the idea of using less storage. So, is there a way of flatting the 1334 images into one sprite? Not sure if this make sense, but any push in the right direction would be appreciated.
You don't have to use image at all if you want to have a gradient. Take a look at this:
let context = CIContext()
if let filter = CIFilter(name: "CILinearGradient")
{
filter.setDefaults()
let startColor = CIColor(color: .gray)
let endColor = CIColor(color: .purple)
let startVector = CIVector(x: frame.width, y: 0)
let endVector = CIVector(x: frame.width, y: frame.height)
filter.setValue(startVector, forKey: "inputPoint0")
filter.setValue(endVector, forKey: "inputPoint1")
filter.setValue(startColor, forKey: "inputColor0")
filter.setValue(endColor, forKey: "inputColor1")
if let outputImage = filter.outputImage {
if let croppedImage = context.createCGImage(outputImage, from: CGRect(origin: CGPoint.zero, size: frame.size)){
let background = SKSpriteNode(texture: SKTexture(cgImage: croppedImage))
addChild(background)
background.zPosition = -20
}
}
}
Repeating an image horizontally or vertically to achieve the same effect is possible. That would require creating texture from a container node using something like this. Still, IMO, you may want skip that and do gradient programatically. And certainly I would stay away of having many nodes which act as a background. That would be silly :)
Related
I've been trying to merge two images where one is on top and the other at the bottom. This code below doesn't seem to work. The x coordinator is correct but the y doesn't seem right and it crops the top image when I alter it. What am I doing wrong?
func combine(bottomImage: Data, topImage: Data) -> UIImage {
let bottomImage = UIImage(data: topImage)
let topImage = UIImage(data: bottomImage)
let size = CGSize(width: bottomImage!.size.width, height: bottomImage!.size.height + topImage!.size.height)
UIGraphicsBeginImageContext(size)
let areaSizeb = CGRect(x: 0, y: 0, width: bottomImage!.size.width, height: bottomImage!.size.height)
let areaSize = CGRect(x: 0, y: 0, width: topImage!.size.width, height: topImage!.size.height)
bottomImage!.draw(in: areaSizeb)
topImage!.draw(in: areaSize)
let newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
}
You are drawing both images into the same rect. You also should not use force-unwrapping. That causes your app to crash if anything goes wrong.
There are also various other small mistakes.
Change your function like this:
// Return an Optional so we can return nil if something goes wrong
func combine(bottomImage: Data, topImage: Data) -> UIImage? {
// Use a guard statement to make sure
// the data can be converted to images
guard
let bottomImage = UIImage(data: bottomImage),
let topImage = UIImage(data: topImage) else {
return nil
}
// Use a width wide enough for the widest image
let width = max(bottomImage.size.width, topImage.size.width)
// Make the height tall enough to stack the images on top of each other.
let size = CGSize(width: width, height: bottomImage.size.height + topImage.size.height)
UIGraphicsBeginImageContext(size)
let bottomRect = CGRect(
x: 0,
y: 0,
width: bottomImage.size.width,
height: bottomImage.size.height)
// Position the bottom image under the top image.
let topRect = CGRect(
x: 0,
y: bottomImage.size.height,
width: topImage.size.width,
height: topImage.size.height)
bottomImage.draw(in: bottomRect)
topImage!.draw(in: topRect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
(And you should really be using a UIGraphicsImageRenderer rather than than calling UIGraphicsBeginImageContext()/ UIGraphicsEndImageContext().)
Edit:
Note that if the 2 images are different widths the above code will leave a "dead space" on the right of the narrower image. You could also make the code center the narrower image, or scale it up to be the same width. (If you do scale it up I suggest scaling it up in both dimensions to preserve the original aspect ratio. Otherwise it will look stretched and unnatural.)
I'm trying to get the UIImage of the mask that I applied to a UIImageView.
I'm adding the mask using UIBezierPath and want the actual masked layer as UIImage, not the whole image. Think of it as a crop feature.
I'm cropping the image using:
func cropImage() {
shapeLayer.fillColor = UIColor.black.cgColor
viewSource.imageView.layer.mask = shapeLayer
viewSource.imageView.layer.masksToBounds = true
UIGraphicsBeginImageContextWithOptions(viewSource.imageView.bounds.size, false, 1)
viewSource.imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.completionObservable.onNext(newImage)
}
This eventually gives me the masked image on top of the old dimensions (the initial imageView width and height). But I want to have only the masked image, excluding the white background around them.
The screens are as shown:
I know what you mean now. Here is the answer, just update the size of imageContext.
UIGraphicsBeginImageContextWithOptions((shapeLayer.path?.boundingBoxOfPath)!.size, false, 1)
If it's not so simple, can try CIImage pipeline to achieve.
let context = CIContext()
let m1 = newImage?.cgImage
let m = CIImage.init(cgImage: m1!)
let bounds = imageView.layer.bounds
let cgImage = context.createCGImage(m, from: CGRect.init(x: 0, y: bounds.size.height, width: bounds.size.width, height: bounds.size.height))
let newUIImage = UIImage.init(cgImage: cgImage!)
You may need to adjust transform.
Is it possible give a circular mask/crop to an image node without jagged edges?
Following this example from Apple (https://developer.apple.com/reference/spritekit/skcropnode), the result is not ideal. You can click on the link to see.
let shapeNode = SKShapeNode()
shapeNode.physicsBody = SKPhysicsBody(circleOfRadius: radius)
shapeNode.physicsBody?.allowsRotation = false
shapeNode.strokeColor = SKColor.clearColor()
// Add a crop node to mask the profile image
// profile images (start off with place holder)
let scale = 1.0
let profileImageNode = SKSpriteNode(imageNamed: "PlaceholderUser")
profileImageNode.setScale(CGFloat(scale))
let circlePath = CGPathCreateWithEllipseInRect(CGRectMake(-radius, -radius, radius*2, radius*2), nil)
let circleMaskNode = SKShapeNode()
circleMaskNode.path = circlePath
circleMaskNode.zPosition = 12
circleMaskNode.name = "connection_node"
circleMaskNode.fillColor = SKColor.whiteColor()
circleMaskNode.strokeColor = SKColor.clearColor()
let zoom = SKAction.fadeInWithDuration(0.25)
circleMaskNode.runAction(zoom)
let cropNode = SKCropNode()
cropNode.maskNode = circleMaskNode
cropNode.addChild(profileImageNode)
cropNode.position = shapeNode.position
shapeNode.addChild(cropNode)
self.addChild(shapeNode)
UPDATE:
Ok, so here's one solution I came up. Not super ideal but it works perfectly. Essentially, I have a to size/scale, and cut the image exactly the way it would go on the SKSpriteNode so I would not have to use SKCropNode or some variation of SKShapeNode.
I used these UIImage extensions by Leo Dabus to resize/shape the image exactly as needed. Cut a UIImage into a circle Swift(iOS)
var circle: UIImage? {
let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
imageView.contentMode = .ScaleAspectFill
imageView.image = self
imageView.layer.cornerRadius = square.width/2
imageView.layer.masksToBounds = true
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.renderInContext(context)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
func resizedImageWithinRect(rectSize: CGSize) -> UIImage {
let widthFactor = size.width / rectSize.width
let heightFactor = size.height / rectSize.height
var resizeFactor = widthFactor
if size.height > size.width {
resizeFactor = heightFactor
}
let newSize = CGSizeMake(size.width/resizeFactor, size.height/resizeFactor)
let resized = resizedImage(newSize)
return resized
}
The final codes look like this:
//create/shape image
let image = UIImage(named: "TestImage")
let scaledImage = image?.resizedImageWithinRect(CGSize(width: 100, height: 100))
let circleImage = scaledImage?.circle
//create sprite
let sprite = SKSpriteNode(texture: SKTexture(image: circleImage!))
sprite.position = CGPoint(x: view.frame.width/2, y: view.frame.height/2)
//set texture/image
sprite.texture = SKTexture(image: circleImage!)
sprite.physicsBody = SKPhysicsBody(texture: SKTexture(image: circleImage!), size: CGSizeMake(100, 100))
if let physics = sprite.physicsBody {
//add the physic properties
}
//scale node
sprite.setScale(1.0)
addChild(sprite)
So if you have a perfectly scaled asset/image, then you probably dont need to do all this work, but I'm getting images from the backend that could come in any sizes.
There are two different techniques that can be combined to reduce the aliasing of edges created from cropping.
Create bigger images than you need, and then scale them down. Both the target (to be cropped) and the mask. Perform the cropping action, then scale down to required size.
Use very subtle blurring of the cropping shape, to soften its edges. This is best done in Photoshop or a similar editing program, to taste and need.
When these two techniques are combined, the results can be very good.
Let the stroke color to be displayed. Also, you can make line width a little thicker and the jagged edges will dissapear.
circleMaskNode.strokeColor = SKColor.whiteColor()
All you have to do is change the SKShapeNode's lineWidth property to be twice the radius of the circle:
func circularCropNode(radius: CGFloat, add: SKNode) {
let cropper = SKCropNode.init()
cropper.addChild(add)
addChild(cropper)
let circleMask = SKShapeNode.init(circleOfRadius: radius/2)
circleMask.lineWidth = radius
cropper.maskNode = circleMask
}
I develop a game and I'd like to have the background change its saturation over a certain amount of time, but have no idea how to do it... So here's my code:
var bg = SKSpriteNode()
bg.size = frame.size
bg.position = CGPoint(x: frame.midX, y: frame.midY)
bg.zPosition = -1000
bg.texture = SKTexture(imageNamed: "background")
bg.alpha = 0.5
addChild(bg)
Thanks a lot !
You can add you background as a child node of a SKEffectNode then set create a color controls filter as the effect node filter. Setting the kCIInputSaturationKey to values less than one will cause the background image to be desaturated, greater than one will cause the background to become more saturated.
var effectNode = SKEffectNode()
effectNode.addChild(bg)
effectNode.filter = CIFilter(name: "CIColorControls")
effectNode.filter?.setValue(0.1, forKey: kCIInputSaturationKey)
I'm trying to rotate an SKShapeNode with a texture, but it's not working. Basically, I have a circle with a texture and I'm trying to make it rotate using the same way I have done with an SKSpriteNode:
let spin = SKAction.rotateByAngle(CGFloat(M_PI/4.0), duration: 1)
The problem is that the circle is rotating, but not the texture. I can check this by using this code:
let wait = SKAction.waitForDuration(0.5)
let block = SKAction.runBlock({
print(self.circle.zRotation)
})
let sequence = SKAction.sequence([wait, block])
self.runAction(SKAction.repeatActionForever(sequence))
This is the code I have for creating the SKShapeNode:
let tex:SKTexture = SKTexture(image: image)
let circle = SKShapeNode(circleOfRadius: 100 )
circle.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 + 200)
circle.strokeColor = SKColor.clearColor()
circle.glowWidth = 1.0
circle.fillColor = SKColor.whiteColor()
circle.fillTexture = tex
self.addChild(circle)
// Runing the action
circle.runAction(spin)
Please help. Thanks in advance!
PS: I know that using an SKSpriteNode would be better but my goal is to place a square image in a circular frame and I figured that using an SKShapeNode would be perfect. If anyone know how to create a circular SKSpriteNode, feel free to post it in the answers section! :)
This is what I'm trying to achieve (with the capability of rotating it):
You can achieve what you want by using SKCropNode and setting its mask property to be a circle texture:
override func didMoveToView(view: SKView) {
let maskShapeTexture = SKTexture(imageNamed: "circle")
let texture = SKTexture(imageNamed: "pictureToMask")
let pictureToMask = SKSpriteNode(texture: texture, size: texture.size())
let mask = SKSpriteNode(texture: maskShapeTexture) //make a circular mask
let cropNode = SKCropNode()
cropNode.maskNode = mask
cropNode.addChild(pictureToMask)
cropNode.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(cropNode)
}
Let's say that picture to mask has a size of 300x300 pixels. The circle you are using as a mask, in that case, has to have the same size. Means the circle itself has to have a radius of 150 pixels (diameter of 300 pixels) when made in image editor.
Mask node determines the visible area of a picture. So don't make it too large. Mask node has to be a fully opaque circle with transparent background.