Mosaic light show CAReplicatorLayer animation - ios

I'm trying to achieve this mosaic light show effect for my background view with the CAReplicatorLayer object:
https://downloops.com/stock-footage/mosaic-light-show-blue-illuminated-pixel-grid-looping-background/
Each tile/CALayer is a single image that was replicated horizontally & vertically. That part I have done.
It seems to me this task is broken into at least 4 separate parts:
Pick a random tile
Select a random range of color offset for the selected tile
Apply that color offset over a specified duration in seconds
If the random color offset exceeds a specific threshold then apply a glow effect with the color offset animation.
But I'm not actually sure this would be the correct algorithm.
My current code was taken from this tutorial:
https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
Animations are not my strong suite & I don't actually know how to apply continuous/repeating animation on all tiles. Here is my current code:
#IBOutlet var animationView: UIView!
func cleanUpAnimationView() {
self.animationView.layer.removeAllAnimations()
self.animationView.layer.sublayers?.removeAll()
}
/// Start a background animation with a replicated pattern image in tiled formation.
func setupAnimationView(withPatternImage patternImage: UIImage, animate: Bool = true) {
// Tutorial: https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
let imageSize = patternImage.size.halve
self.cleanUpAnimationView()
// Animate pattern image
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame.size = self.animationView.frame.size
replicatorLayer.masksToBounds = true
self.animationView.layer.addSublayer(replicatorLayer)
// Give the replicator layer a sublayer to replicate
let imageLayer = CALayer()
imageLayer.contents = patternImage.cgImage
imageLayer.frame.size = imageSize
replicatorLayer.addSublayer(imageLayer)
// Tell the replicator layer how many copies (or instances) of the image needs to be rendered. But we won't see more than one since they are, per default, all rendered/stacked on top of each other.
let instanceCount = self.animationView.frame.width / imageSize.width
replicatorLayer.instanceCount = Int(ceil(instanceCount))
// Instance offsets & transforms is needed to move them
// 'CATransform3D' transform will be used on each instance: shifts them to the right & reduces the red & green color component of each instance's tint color.
// Shift each instance by the width of the image
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(imageSize.width, 0, 0)
// Reduce the red & green color component of each instance, effectively making each copy more & more blue while horizontally repeating the gradient pattern
let colorOffset = -1 / Float(replicatorLayer.instanceCount)
replicatorLayer.instanceRedOffset = colorOffset
replicatorLayer.instanceGreenOffset = colorOffset
//replicatorLayer.instanceBlueOffset = colorOffset
//replicatorLayer.instanceColor = UIColor.random.cgColor
// Extend the original pattern to also repeat vertically using another tint color gradient
let verticalReplicatorLayer = CAReplicatorLayer()
verticalReplicatorLayer.frame.size = self.animationView.frame.size
verticalReplicatorLayer.masksToBounds = true
verticalReplicatorLayer.instanceBlueOffset = colorOffset
self.animationView.layer.addSublayer(verticalReplicatorLayer)
let verticalInstanceCount = self.animationView.frame.height / imageSize.height
verticalReplicatorLayer.instanceCount = Int(ceil(verticalInstanceCount))
verticalReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, imageSize.height, 0)
verticalReplicatorLayer.addSublayer(replicatorLayer)
guard animate else { return }
// Set both the horizontal & vertical replicators to add a slight delay to all animations applied to the layer they're replicating
let delay = TimeInterval(0.1)
replicatorLayer.instanceDelay = delay
verticalReplicatorLayer.instanceDelay = delay
// This will make the image layer change color
let animColor = CABasicAnimation(keyPath: "instanceRedOffset")
animColor.duration = animationDuration
animColor.fromValue = verticalReplicatorLayer.instanceRedOffset
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor.autoreverses = true
animColor.repeatCount = .infinity
replicatorLayer.add(animColor, forKey: "colorshift")
let animColor1 = CABasicAnimation(keyPath: "instanceGreenOffset")
animColor1.duration = animationDuration
animColor1.fromValue = verticalReplicatorLayer.instanceGreenOffset
animColor1.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor1.autoreverses = true
animColor1.repeatCount = .infinity
replicatorLayer.add(animColor1, forKey: "colorshift1")
let animColor2 = CABasicAnimation(keyPath: "instanceBlueOffset")
animColor2.duration = animationDuration
animColor2.fromValue = verticalReplicatorLayer.instanceBlueOffset
animColor2.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor2.autoreverses = true
animColor2.repeatCount = .infinity
replicatorLayer.add(animColor2, forKey: "colorshift2")
}

let imageSize = patternImage.size.halve
and
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
both generated errors.
I removed the halve and commented-out the animColor lines and the code runs and animates. I could not get ANY replicator layer to display or animate at all (not even the most basic apple or tutorial code) until I used your code. Thank you so much!

Related

SKEffectNode to an SKTexture?

SKEffectionNodes have a shouldRasterise "switch" that bakes them into a bitmap, and doesn't update them until such time as the underlying nodes that are impacted by the effect are changed.
However I can't find a way to create an SKTexture from this rasterised "image".
Is it possible to get a SKTexture from a SKEffectNode?
I think you could try a code like this (it's just an example):
if let effect = SKEffectNode.init(fileNamed: "myeffect") {
effect.shouldRasterize = true
self.addChild(effect)
...
let texture = SKView().texture(from: self)
}
Update:
After you answer, hope I understood better what do you want to achieve.
This is my point of view: if you want to make a shadow of a texture, you could simply create an SKSpriteNode with this texture:
let shadow = SKSpriteNode.init(texture: <yourTexture>)
shadow.blendMode = SKBlendMode.alpha
shadow.colorBlendFactor = 1
shadow.color = SKColor.black
shadow.alpha = 0.25
What I want to say is that you could proceed step by step:
get your texture
elaborate your texture (add filters, make some other effect..)
get shadow
This way of working produces a series of useful methods you could use in your project to build other kind of elements.
Maybe, by separating the tasks you don't need to use texture(from:)
I've figured this out, in a way that solves my problems, using a Factory.
Read more on how to make a factory, from BenMobile's patient and clear articulation, here: Factory creation and use for making Sprites and Shapes
There's an issue with blurring a SKTexture or SKSpriteNode in that it's going to run out of space. The blur/glow goes beyond the edges of the sprite. To solve this, in the below, you'll see I've created a "framer" object. This is simply an empty SKSpriteNode that's double the size of the texture to be blurred. The texture to be blurred is added as a child, to this "framer" object.
It works, regardless of how hacky this is ;)
Inside a static factory class file:
import SpriteKit
class Factory {
private static let view:SKView = SKView() // the magic. This is the rendering space
static func makeShadow(from source: SKTexture, rgb: SKColor, a: CGFloat) -> SKSpriteNode {
let shadowNode = SKSpriteNode(texture: source)
shadowNode.colorBlendFactor = 0.5 // near 1 makes following line more effective
shadowNode.color = SKColor.gray // makes for a darker shadow. White for "glow" shadow
let textureSize = source.size()
let doubleTextureSize = CGSize(width: textureSize.width * 2, height: textureSize.height * 2)
let framer = SKSpriteNode(color: UIColor.clear, size: doubleTextureSize)
framer.addChild(shadowNode)
let blurAmount = 10
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(blurAmount, forKey: kCIInputRadiusKey)
let fxNode = SKEffectNode()
fxNode.filter = filter
fxNode.blendMode = .alpha
fxNode.addChild(framer)
fxNode.shouldRasterize = true
let tex = view.texture(from: fxNode) // ‘view’ refers to the magic first line
let shadow = SKSpriteNode(texture: tex) //WHOOPEE!!! TEXTURE!!!
shadow.colorBlendFactor = 0.5
shadow.color = rgb
shadow.alpha = a
shadow.zPosition = -1
return shadow
}
}
Inside anywhere you can access the Sprite you want to make a shadow or glow texture for:
shadowSprite = Factory.makeShadow(from: button, rgb: myColor, a: 0.33)
shadowSprite.position = CGPoint(x: self.frame.midX, y: self.frame.midY - 5)
addChild(shadowSprite)
-
button is a texture of the button to be given a shadow. a: is an alpha setting (actually transparency level, 0.0 to 1.0, where 1.0 is fully opaque) the lower this is the lighter the shadow will be.
The positioning serves to drop the shadow slightly below the button so it looks like light is coming from the top, casting shadows down and onto the background.

Drawing a confidence ellipse on top of a scatter plot

I'm currently working on an iOS app where I'm using the CorePlot library (Version 2.1) to draw a scatter plot. My scatter plot draws fine, and in the next step I'd like to draw an translucent confidence ellipse on top of the plot. I've written a class computing the main and minor axis and the required rotation angle of my ellipse. My ConfidenceEllipse class implements a getPath() method which returns a CGPath representing the ellipse to draw.
func getPath() -> CGPath
{
var ellipse: CGPath
var transform = CGAffineTransformIdentity
transform = CGAffineTransformTranslate (transform, CGFloat(-self.meanX), CGFloat(-self.meanY))
transform = CGAffineTransformRotate (transform, CGFloat(self.angle))
transform = CGAffineTransformTranslate (transform, CGFloat(self.meanX), CGFloat(self.meanY))
ellipse = CGPathCreateWithEllipseInRect(CGRectMake (CGFloat(-self.mainHalfAxis), CGFloat(-self.minorHalfAxis), CGFloat(2 * self.mainHalfAxis), CGFloat(2 * self.minorHalfAxis)),&transform)
return ellipse
}
After searching the web for a while, it appears that Annotations are the way to go, so I tried this:
let graph = hostView.hostedGraph!
let space = graph.defaultPlotSpace
let ellipse = ConfidenceEllipse(chiSquare: 5.991)
ellipse.analyze(self.samples)
let annotation = CPTPlotSpaceAnnotation (plotSpace: space!, anchorPlotPoint: [0,0])
let overlay = CPTBorderedLayer (frame: graph.frame)
overlay.outerBorderPath = ellipse.getPath()
let fillColor = CPTColor.yellowColor()
overlay.fill = CPTFill (color: fillColor)
annotation.contentLayer = overlay
annotation.contentLayer?.opacity = 0.5
graph.addAnnotation(annotation)
Doing this, will give me the following
Screenshot
As you can see, the overlay takes up the full size of the frame, which seems logical given the fact that I passed the frames dimensions when creating the CPTBorderedLayer object. I also tried leaving the constructor empty, but then the overlay doesn't show at all. So I'm wondering, is there anything I'm missing here ?
You need to scale the ellipse to match the plot. Use the plot area bounds for the frame of the annotation layer and attach the annotation to the plot area. Scale the ellipse in the x- and y-directions to match the transform used by the plot space to fit plots in the plot area.
Edit:
After looking into how bordered layers work, I realized my suggestion above won't work. CPTBorderedLayer sets the outerBorderPath automatically whenever the layer bounds change. Instead of trying to affect the layer border, draw the ellipse into an image and use that as the fill for the bordered layer. You should size the layer so the ellipse just fits inside.
After failing to get the Annotations to work properly, I decided to take a different road. My final solution consists in overlaying my original scatter plot with a second one, which only contains one datapoint, namely the center of my confidence ellipse. Here's the code
func drawConfidenceEllipse () {
let graph = hostView.hostedGraph!
let plotSpace = graph.defaultPlotSpace as! CPTXYPlotSpace
let scaleX = (graph.bounds.size.width - graph.paddingLeft - graph.paddingRight) / CGFloat(plotSpace.xRange.lengthDouble)
let scaleY = (graph.bounds.size.height - graph.paddingTop - graph.paddingBottom) / CGFloat(plotSpace.yRange.lengthDouble)
let analysis = ConfidenceEllipse(chiSquare: 5.991)
analysis.analyze(self.samples)
let unscaledPath = analysis.getPath()
let bounds = CGPathGetBoundingBox(unscaledPath)
var scaler = CGAffineTransformIdentity
scaler = CGAffineTransformScale (scaler, scaleX, scaleY)
scaler = CGAffineTransformTranslate (scaler, CGFloat (-bounds.origin.x), CGFloat (-bounds.origin.y))
let scaledPath = CGPathCreateCopyByTransformingPath (unscaledPath, &scaler)
let scaledBounds = CGPathGetPathBoundingBox(scaledPath)
let symbol = CPTPlotSymbol ()
symbol.symbolType = CPTPlotSymbolType.Custom
symbol.customSymbolPath = scaledPath
symbol.fill = CPTFill (color: CPTColor.yellowColor().colorWithAlphaComponent(0.25))
symbol.size = CGSize (width: scaledBounds.size.width, height: scaledBounds.size.height)
let lineStyle = CPTMutableLineStyle()
lineStyle.lineWidth = 1
lineStyle.lineColor = CPTColor.yellowColor()
symbol.lineStyle = lineStyle
let ellipse = CPTScatterPlot (frame: hostView.frame)
ellipse.title = "Confidence Ellipse"
ellipse.delegate = self
ellipse.dataSource = self
ellipse.plotSymbol = symbol
ellipse.dataLineStyle = nil
graph.addPlot(ellipse)
}
Here's a screenshot of the final result:
95% Confidence Ellipse on top of scatter plot
Hope this helps

How do I make this animation go counterclockwise?

I have already got part of my animation completed. What I am trying to do is create the line border animation like this, where the line travels across the top and right side of the rectangle. What I need in addition to this is creating another line that mirrors the line shown in the video (traveling across the left side then bottom of the rectangle),counterclockwise. I tried doing this using the -bezierPathByReversingPath(_:) function, but it made the origin for the line begin in the bottom left of the rectangle. I am not sure what to do, any help would be appreciated!
Here’s my code:
// Create layer
rightLine = CAShapeLayer()
rightLine.bounds = centerButton.bounds
rightLine.position = view.center
rightLine.path = UIBezierPath(rect: rightLine.bounds).CGPath
rightLine.lineWidth = 3
rightLine.strokeColor = UIColor(hex: "3D424E").CGColor
rightLine.fillColor = UIColor.clearColor().CGColor
rightLine.strokeStart = 0
rightLine.strokeEnd = 0
// Create animation
let rightStart = CABasicAnimation(keyPath: "strokeStart")
rightStart.toValue = 0
let rightEnd = CABasicAnimation(keyPath: "strokeEnd")
rightEnd.toValue = 0.5
rightLineGroup = CAAnimationGroup()
rightLineGroup.animations = [rightStart, rightEnd]
rightLineGroup.duration = 1.5
rightLineGroup.autoreverses = true
rightLineGroup.repeatCount = HUGE // repeat forever
self.view.layer.addSublayer(rightLine)
rightLine.addAnimation(rightLineGroup, forKey: nil)
Animating strokeStart from zero to zero does nothing.
You probably want to leave strokeEnd at 1.0 (the default) and animate strokeStart from 1.0 to 0.5. That should have the desired effect.

Keeping a background image in the center on animation

I have animation setup to resize the image to about 1.3 times it's original size. The animations and everything are working without a problem but the image is moving towards the top left. Which means that the position of the image is not centering upon resize. How do i solve this problem
These are the animations I setup
var borderWidth:CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidth.fromValue = 0
borderWidth.toValue = 5
borderWidth.repeatCount = Float.infinity
sender.layer.borderWidth = 0
var increaseButtonHeight:CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.height")
increaseButtonHeight.fromValue = sender.frame.size.height
increaseButtonHeight.toValue = sender.frame.size.height * 1.3
var increaseButtonWidth: CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.width")
increaseButtonWidth.fromValue = sender.frame.size.width
increaseButtonWidth.toValue = sender.frame.size.width * 1.3
var boom:CAAnimationGroup = CAAnimationGroup()
boom.animations = [borderWidth,increaseButtonWidth, increaseButtonHeight]
boom.repeatCount = Float.infinity
boom.duration = 0.5
boom.autoreverses = true
sender.layer.addAnimation(boom, forKey: "boom")
Do I need to setup a new animation for centering the button continuously as the animation happens?
Please help
Nikhil
Set the property contentsGravity of the layer to kCAGravityCenter

Adding image to particle emitter and stopping after a duration in swift/ios

I've been trying to learn and understand the emitter functions of CAEmitter, but I'm currently a little bit stuck. I want to add an image for the emitter and make it stop after a duration.
I've got a view that I'm using to emit some particles, and I want them to only appear emit when the view appears for around 10 seconds, then stop. I also am unsure how to attach a UI image with a png, instead of using CGrect.
Thanks for any help and advice!
import UIKit
class ParticleView: UIView {
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(15,8), false, 1)
let con = UIGraphicsGetCurrentContext()
CGContextAddRect(con, CGRectMake(0, 0, 15, 8))
CGContextSetFillColorWithColor(con, UIColor.whiteColor().CGColor)
CGContextFillPath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// make a cell with that image
var cell = CAEmitterCell()
cell.birthRate = 10
cell.color = UIColor(red:0.5, green:0.5, blue:0.5, alpha:1.0).CGColor
cell.redRange = 1
cell.blueRange = 1
cell.greenRange = 1
cell.lifetime = 5
cell.alphaSpeed = -1/cell.lifetime
cell.velocity = -100
cell.spinRange = 10.0
cell.scale = 1.0;
cell.scaleRange = 0.2;
cell.emissionRange = CGFloat(M_PI)/5.0
cell.contents = im.CGImage
var emit = CAEmitterLayer()
emit.emitterSize = CGSize(width: 100, height: 0)
emit.emitterPosition = CGPointMake(30,100)
emit.emitterShape = kCAEmitterLayerLine
emit.emitterMode = kCAEmitterLayerLine
emit.emitterCells = [cell]
self.layer.addSublayer(emit)
}
}
I have found a nice workaround to stop CAEmitter:
Create 2 identical view controllers with the same layout
Implement a Start and Stop button on both (to begin and end the CAEmitter)
Connect the Stop button of each view controller to each other with a “Show Detail (e.g. Replace) Segue and deselect "Animates"
When you hit “Stop” button it will make a seamless transition to the identical VC without emitter particles!! But what is really happening is that you are just switching to a replica view controller. This is not an elegant solution but it is the only reliable way that I have found to stop a CAEmitter (segue to a different VC) all of the other solutions are "buggy"
visual of how the VCs are set up

Resources