Swift: Make translucent overlapping lines of the same color not change color when intersecting - swift

Currently I have drawn 2 lines on the screen which are the same color but both have an alpha value less than 1. When these lines intersect, the intersection is a different color than the rest of the lines. There was a previous post addressing the same issue: swift drawing translucent lines, how to make overlapping parts not getting darker? However, this post wasn't sufficiently answered. I draw the lines currently like this:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
FirstLine.lineWidth = 30
addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
SecondLine.lineWidth = 30
addChild(SecondLine)
Here is what it looks like:
I understand why this occurs, but is there any way to make the intersection look the same so it looks much better?
Edit: I have decided to implement #Confused's answer. However, the problem now is that the texture always centres to the middle of the screen. This is an example:
The red cross is in its correct position as it is connecting the specified points together. However, once I make the red cross a texture, it always centres to the middle of the screen (the green cross is the red cross as a texture). Can I use any code that could re-position the texture to its correct position. Note: this code can't just work for this example, I need one that works all the time regardless of the red cross' position.
FINAL EDIT FOR PEOPLE WITH THE SAME PROBLEM
First, setup everything like this:
var points = [CGPoint]()
let crossParent = SKNode()
addChild(crossParent)
Please note THAT YOU MUST create a parent SKNode for the texture otherwise everything on the screen will become the texture and not just the node that you want. Then, add that parent node to the scene.
After, create the lines that you want (in this case the green cross):
//The first line of the green cross
points = [CGPoint(x: -300, y: 300), CGPoint(x: -100, y: 100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
FirstLine.lineWidth = 30
crossParent.addChild(FirstLine)
Remember to NOT add the first line that you create to the scene, but rather to the parent SKNode that you made at the beginning. Also, set the alpha value to 1.0 for every line that you draw. Then add your other lines:
//The second line of the green cross
points = [CGPoint(x: -100, y: 300), CGPoint(x: -300, y: 100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
Note that you MUST add that line to the first line like this and not to the scene. If you are adding more than one line after adding the first line, then add it to the first line like I have done here too for each consecutive
line that you add.
Now, create the texture like this:
let tex = view?.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
addChild(cross)
After doing this, whatever you call cross will be your texture, and you can change the alpha value like I have done here to anything you like and the image will not have varying colors. Remember to then add that texture to the scene.
Finally, you may notice that the texture is not in the same position as where you originally put the points. You can put it back into the same position like this:
cross.position = CGPoint(x: (FirstLine.frame.midX), y: (FirstLine.frame.midY))
Hope this helps :) Thank you to #Confused for the texture part of the program :D

This is a technique. It does not solve your problem, nor directly answer your question. Instead it offers a way to have the desired result, but without the flexibility and inherent nature of lines you actually want.
You can create textures from anything you draw with any number of nodes, with any number of techniques.
You do this (easiest way) by attaching all the drawing elements to a single node, within an SKView space that you have access to, and then render the "parent" node of your drawn objects to a texture.
How does this help?
I'm glad you asked:
You can draw everything at an opacity level of 100%, and render it to a texture, then take that texture of your drawings, put it where you like, and reduce its opacity to any percentage you like, and get an even result. No bright spots where things overlay each other.
Here's code that does all the above:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
// ^^ Note the FirstLine is not added to the Scene
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
// ^^ Note SecondLine being added to FirstLine, and that they both have alpha of 1
// Now the magic: use the view of the SKScene to render FirstLine and its child (SecondLine)
// They are rendered into a texture, named, imaginatively, "tex"
let tex = view.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
// ^^ The alpha of the above sprite is set to your original desire of 0.5
// And then added to the scene, with the desired result.
addChild(cross)
and here's the result:

NO! I think is, unfortunately, the answer.
There are the following blend modes, none of which provide the result you're looking for:
case alpha // Blends the source and destination colors by multiplying the source alpha value.
case add // Blends the source and destination colors by adding them up.
case subtract // Blends the source and destination colors by subtracting the source from the destination.
case multiply // Blends the source and destination colors by multiplying them.
case multiplyX2 // Blends the source and destination colors by multiplying them and doubling the result.
case screen // Blends the source and destination colors by multiplying one minus the source with the destination and adding the source.
case replace // Replaces the destination with the source (ignores alpha).
The only option that might work is a custom shader that figures out if it's overlapping itself, somehow. But I have NO CLUE where to even start making something like that, or if it's even possible.

Make the stroke color fully opaque.
Add your path nodes to an SKEffectNode.
Set the alpha of the effect node to the desired value.
Put the effect node into your main scene.
While doing this you can also put all lines in one shape node:
let path = CGMutablePath()
path.move( to: CGPoint(x: -100, y: 100))
path.addLine(to: CGPoint(x: 100, y: -100))
path.move( to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: -100, y: -100))
let shape = SKShapeNode(path: path)
shape.strokeColor = UIColor(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
shape.lineWidth = 30
let effect = SKEffectNode()
effect.addChild(shape)
effect.alpha = 0.5
addChild(effect)

The problem is you are doing the alpha blend at the color level, do the alpha blending at the node level
var points = [CGPoint]()
var cross = SKEffectNode()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
cross.addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
cross.addChild(SecondLine)
cross.alpha = 0.5
addChild(cross)

Related

How to draw gradient in SKShapeNode?

I am trying to draw gradient in SKShapeNode object.
I draw a triangle with a color using alpha component and it works fine. And I would like to add gradient so one edge of the triangle disappears slowly to the background. What I want to achieve is to simulate sight range of a character.
I have found one answer regarding this challenge: How to apply a gradient to SKShapeNode created from a path, but the answer doesn't work in XCode 13.1.
The reason is that proposed solution of adding SKTexture can't be compiled:
var testTexture = SKTexture(size: CGSize(width: 200, height: 1), color1: CIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 1.0), color2: CIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.0), direction: GradientDirection.Left)
It results in the error "Cannot find 'GradientDirection' in scope...". And I cannot find any other method to add gradient.
I have found another answer which says that it isn't possible, but it is from 2013, so maybe something changed (How to create a Gradient in Spritekit?).
I also tried this solution: https://augmentedcode.io/2017/11/12/drawing-gradients-in-spritekit/, but it doesn't produce any effect and decreases performance of the application.
Some sources from the internet directed my to OpenGL Shading Language: https://www.khronos.org/opengl/wiki/Core_Language_(GLSL).
Here is partly solution:
let sight = SKShapeNode()
let gradientShader = SKShader(source: "void main() {" +
"float distanceFromCenter = distance(v_tex_coord, vec2(0.5,0.5));" +
"gl_FragColor = vec4(0.6, 0.3, 0.0, distanceFromCenter*6.0);" +
"}")
sight.fillColor = .clear
sight.fillShader = gradientShader
Here are helpful links: Drawing a circle with a shader in SpriteKit, https://thebookofshaders.com/02/, Add shader for SKShapeNode, https://developer.apple.com/documentation/spritekit/skshapenode/controlling_shape_drawing_with_shaders.
The above solution was half way, but after another couple of hours I managed to achieve what I wanted to.
let pos = n.sprite.position
let frame = n.sight.frame
let x = (pos.x - frame.minX) / frame.width
let y = (pos.y - frame.minY) / frame.height
let gradientShader = SKShader(source: "void main() {" +
"float distanceFromCenter = distance(v_tex_coord, npcLocation);" +
"float alpha = 0.8 - distanceFromCenter;" +
"if (alpha < 0) { alpha = 0.0;}" +
"gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);" +
"}")
gradientShader.uniforms = [SKUniform(name: "npcLocation", vectorFloat2: vector_float2(Float(x), Float(y)))]
n.sight.fillColor = .clear
n.sight.fillShader = gradientShader

How to use alpha component in gradient colors with GKNoise and SKTexture?

I'm using GKNoise with a gradient map to generate color noise, getting a CGImage via SKTexture, on Mac OS. In particular I'm using a GKPerlinNoiseSource and setting two gradient colors, at values -1.0 and 1.0.
It works as expected if the two colors are opaque. If one or both colors has an alpha component less than 1.0, I expect the output to have transparency. However, it looks to me like the alpha is completely ignored in GKNoise's gradient color input (treated as if it's always 1.0) - or, if not ignored there, ignored in the SKTexture rendering of the image output.
I have found a couple of SO questions which reference limitations with SKTexture in other uses, including a possible workaround with SKView rendering, however that doesn't apply here because I'm only using an SKTexture instance to get a CGImage (not using SKView or otherwise using anything from SpriteKit) - and FWIW those questions focus on iOS. For reference:
Export SKTexture to a UIImage with alpha channel
SKSpriteNode created from SKTexture(data:size:) issues with Alpha (Opacity)
I'm looking for ideas on how to make alpha components in the gradient colors work using GKNoise/SKTexture.
Below is the test output image, and the code that reproduces it. In the view, both CGImages draw identically; I expect the one drawn with the red alpha=0.5 to be darker in the red parts when the background is black, lighter when it's white, etc.
import Foundation
import GameplayKit
class GKNoiseGradientIssue {
var redColor_opaque = NSColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
var redColor_halfAlpha = NSColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5)
var blueColor_opaque = NSColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0)
var opaqueImage: CGImage
var halfAlphaImage: CGImage
init() {
let noiseSource = GKPerlinNoiseSource(frequency: 0.15,
octaveCount: 7,
persistence: 1.25,
lacunarity: 0.5,
seed: 12345)
let opaqueGradient: [NSNumber: NSColor] = [-1.0: redColor_opaque, 1.0: blueColor_opaque]
let opaqueNoise = GKNoise(noiseSource, gradientColors: opaqueGradient)
let opaqueNoiseMap = GKNoiseMap(opaqueNoise,
size: [200.0, 200.0],
origin: [0.0, 0.0],
sampleCount: [200, 200],
seamless: true)
let opaqueTexture = SKTexture(noiseMap: opaqueNoiseMap)
self.opaqueImage = opaqueTexture.cgImage()
let halfAlphaGradient: [NSNumber: NSColor] = [-1.0: redColor_halfAlpha, 1.0: blueColor_opaque]
let halfAlphaNoise = GKNoise(noiseSource, gradientColors: halfAlphaGradient)
let halfAlphaNoiseMap = GKNoiseMap(halfAlphaNoise,
size: [200.0, 200.0],
origin: [0.0, 0.0],
sampleCount: [200, 200],
seamless: true)
let halfAlphaTexture = SKTexture(noiseMap: halfAlphaNoiseMap)
self.halfAlphaImage = halfAlphaTexture.cgImage()
}
}
class GradientIssueView: NSView {
var issue: GKNoiseGradientIssue?
override func awakeFromNib() {
self.issue = GKNoiseGradientIssue()
}
override func draw(_ dirtyRect: NSRect) {
NSColor.black.setFill()
self.bounds.fill()
if let cgc = NSGraphicsContext.current?.cgContext {
cgc.draw(self.issue!.opaqueImage,
in: CGRect(origin: CGPoint(x: 10.0, y: 10.0),
size: CGSize(width: 200.0, height: 200.0)))
cgc.draw(self.issue!.halfAlphaImage,
in: CGRect(origin: CGPoint(x: 10.0, y: 220.0),
size: CGSize(width: 200.0, height: 200.0)))
}
}
}
Just following up here, for anyone else who might encounter this... While I never got a definitive reply from Apple Developer forums I did come to the conclusion that SKTexture + GKNoise only supports opaque colors, so the question as asked has no answer: one can't do this with SKTexture. Instead, I updated my approach to just use GKNoiseMap directly and calculate my own gradients.
An advantage of this approach is that I can also now create gradients from a noise source using more than two colors. I also have more options for creatively skewing or clamping the noise values before they're converted to colors.
To use GKNoiseMap directly instead of with the two-color gradients and SKTexture approach, create the noise with no reference to gradient colors, and create the GKNoiseMap the same way:
let myNoise = GKNoise(noiseSource)
let noiseMap = GKNoiseMap(myNoise,
size: [myModelWidth, myModelHeight],
origin: [myX, myY],
sampleCount: [myPointWidth, myPointHeight],
seamless: true)
Then when rendering, just call the noiseMap value() method to get the noise value for each point in your view or image output, and then calculate your own color gradient for that point including the alpha (if desired). Note that voronoi noise will be between 0.0 and 1.0 while other perlin-based noise will be between -1.0 and 1.0; to simplify here I'll assume one just wants a gradient between two colors at 0.0 and 1.0.
Given two colors in .deviceRGB color space (rgbColor1 and rgbColor2), and the noise value unitNoiseValue as a CGFloat,
let r_range = rgbColor2.redComponent - rgbColor1.redComponent
let g_range = rgbColor2.greenComponent - rgbColor1.greenComponent
let b_range = rgbColor2.blueComponent - rgbColor1.blueComponent
let a_range = rgbColor2.alphaComponent - rgbColor1.alphaComponent
let r = noiseUnitValue * r_range + rgbColor1.redComponent
let g = noiseUnitValue * g_range + rgbColor1.greenComponent
let b = noiseUnitValue * b_range + rgbColor1.blueComponent
let a = noiseUnitValue * a_range + rgbColor2.alphaComponent
return NSColor(red: r, green: g, blue: b, alpha: a)
In order to make this performant it may be helpful to limit the discrete levels of each gradient (e.g. 256 levels), and to cache calculated colors for large images and store in a hash that can be looked up quickly, and similar techniques. Also, because the conversion here just maps a unit value, you can have other skewing of your noise value if desired before calculation of the gradient color, such as additional fade or normalization or other 'shaping' of the input value.

Button gradient not working after ios 12 (Swift 4.2)

With the coming of ios 12 and swift 4.2 my code is not working anymore. It used to be a gradient from left-to-right, dark purple-to-light-purple button. How can I fix this?
// Gives the button gradient values
func setGradientButton(colorOne: UIColor, colorTwo: UIColor, x1: Double, y1: Double, x2: Double, y2: Double) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 0.0]
gradientLayer.startPoint = CGPoint(x: x1, y: y1)
gradientLayer.endPoint = CGPoint(x: x2, y: y2)
layer.insertSublayer(gradientLayer, at: 0)
}
// Sets UI elements
func setUI(_ label : UILabel, _ button : UIButton) {
let colorOne = UIColor(red: 119.0 / 255.0, green: 85.0 / 255.0, blue: 254.0 / 255.0, alpha: 100.0)
let colorTwo = UIColor(red: 177.0 / 255.0, green: 166.0 / 255.0, blue: 251.0 / 255.0, alpha: 100.0)
button.setGradientButton(colorOne: colorOne, colorTwo: colorTwo, x1: 0.0, y1: 50, x2: 150, y2: 50)
button.layer.cornerRadius = 4
button.clipsToBounds = true
}
I can't assume why your code has worked before, but I see that you got a few problems with your CAGradientLayer setup.
At first, the locations array. According to Apple Documentation this value "defines the location of each gradient stop". So if you want your gradient to start with one color and end with another, you need to set your locations like that
gradientLayer.locations = [0.0, 1.0]
Another problem is startPoint and endPoint. Again, from documentation:
The point is defined in the unit coordinate space and is then mapped to the layer’s bounds rectangle when drawn.
So your points values should be between 0.0 and 1.0. In unit coordinate space (0.0, 0.0) is the top-left corner of your view and (1.0, 1.0) is the bottom-right. If you want to get a horizontal gradient, you need to set points like that
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
Hope it helps you.

CAShapeLayer background-color fading to sides in Swift

I have created a clickable diagram with CAShapeLayers and UIBezierPaths. These shapes have a background-color that I want to fade out to transparent on the sides/edges. All CAShapeLayers have different shapes. Is there a way to achieve this in Swift?
I attached a simple example image of what I'm trying to do:
Right now, I'm creating the CAShapeLayers as follows:
var path = UIBezierPath()
path.moveToPoint(CGPointMake(20, 30))
path.addLineToPoint(CGPointMake(40, 30))
path.addLineToPoint(CGPointMake(50, 60))
path.addLineToPoint(CGPointMake(120, 180))
path.closePath()
var layer = CAShapeLayer()
layer.path = path.CGPath
layer.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.5).CGColor
layer.hidden = true
baseImg.layer.addSublayer(layer)
Is there a way to add a CIGaussianBlur or a UIBlurEffect to these shapes?

SpriteKit: How to spawn object in exact place?

I'm in the middle of making a simple game in XCode 7, using sprite kit. I currently have a square in the middle of the scene and I have set up swipe gestures which make smaller squares "shoot" out of the left, right, top of bottom of the square, depending on the way of the swipe gesture. They go in a straight direction and I would like other random "Enemy" squares to come in the opposite way, again in the same line of path as the squares that are shot out so they can collide... Any ideas on how to do this?
Here's the basic idea, same for the right, up and down:
func swipedLeft(){
print("SwipedLeft")
let SmallSquare1 = SKSpriteNode(imageNamed: "Square")
SmallSquare1.position = MainSquare.position
SmallSquare1.size = CGSize(width: 30, height: 30)
SmallSquare1.physicsBody = SKPhysicsBody(circleOfRadius: SmallSquare1.size.width / 2)
SmallSquare1.physicsBody?.affectedByGravity = false
self.addChild(SmallSquare1)
let moveLeft = SKAction.moveByX(-frame.size.width, y: 0, duration: 2)
SmallSquare1.runAction(moveLeft)
Here's the Enemy function I'm stuck on...
func Enemies(){
let Enemy = SKSpriteNode(imageNamed: "Square")
Enemy.size = CGSize(width: 20, height: 20)
Enemy.color = UIColor(red: 0.9, green: 0.1, blue: 0.1, alpha: 1.0)
Enemy.colorBlendFactor = 1.0
}
For the one on the left:
Enemy.position = CGPoint(x: (any X position), y: MainSquare.position.y)
And do the same for the right side.
And for above and below just do the MainSquare.position.x as the x position