├── spriteKitShadersDemo.gif
├── SpriteKitShaders
├── Assets.xcassets
│ ├── Contents.json
│ ├── Particle Sprite Atlas.spriteatlas
│ │ ├── Contents.json
│ │ ├── bokeh.imageset
│ │ │ ├── bokeh.png
│ │ │ └── Contents.json
│ │ └── spark.imageset
│ │ │ ├── spark.png
│ │ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Images
│ ├── grid.png
│ ├── leaf.png
│ ├── water.png
│ ├── lightburst.png
│ ├── spritekitText.png
│ └── poolBackground.png
├── LightSparkle.sks
├── SpriteKitShaders.entitlements
├── Shaders
│ ├── algaeBloomShader.fsh
│ ├── simpleLiquidShader.fsh
│ └── poolWaterShader.fsh
├── UtilityExtensions.swift
├── Info.plist
├── AppDelegate.swift
├── PoolScene.swift
└── Base.lproj
│ └── MainMenu.xib
├── SpriteKitShaders.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── project.pbxproj
└── README.md
/spriteKitShadersDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/spriteKitShadersDemo.gif
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/grid.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/leaf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/leaf.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/water.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/water.png
--------------------------------------------------------------------------------
/SpriteKitShaders/LightSparkle.sks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/LightSparkle.sks
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/lightburst.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/lightburst.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/spritekitText.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/spritekitText.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Images/poolBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Images/poolBackground.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/bokeh.imageset/bokeh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/bokeh.imageset/bokeh.png
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/spark.imageset/spark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthewreagan/SpriteKitShaders/HEAD/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/spark.imageset/spark.png
--------------------------------------------------------------------------------
/SpriteKitShaders.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SpriteKitShaders/SpriteKitShaders.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/SpriteKitShaders/Shaders/algaeBloomShader.fsh:
--------------------------------------------------------------------------------
1 | //
2 | // Simple example shader, removes red and blue channels to leave
3 | // our texture green-tinted. Demo shader for related blog post:
4 | // http://sound-of-silence.com
5 | // For LICENSE information please see AppDelegate.swift.
6 | //
7 |
8 | void main() {
9 | vec4 color = SKDefaultShading();
10 | gl_FragColor = vec4(0.0, color.g * color.a, 0.0, color.a);
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/bokeh.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "bokeh.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/Particle Sprite Atlas.spriteatlas/spark.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "spark.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # SpriteKit Shaders & Geometry Demo
4 |
5 | This demo app shows how **SKShader** and **SKWarpable** can be used to create a liquid effect for a simple 2D pool simulation. Particles from **SKEmitterNode** and falling leaves animated with SKAction are thrown in for fun. The purpose of this code is to provide an introduction to how fragment shaders can be easily used with SpriteKit.
6 |
7 | ## Related Article
8 |
9 | Check out the short [YouTube video](https://www.youtube.com/watch?v=4VO97nosweA) or the related blog post here: [http://sound-of-silence.com/?article=20180806](http://sound-of-silence.com/?article=201808016)
10 |
11 | ## Author
12 |
13 | **Matt Reagan** - Website: [http://sound-of-silence.com/](http://sound-of-silence.com/) - Twitter: [@hmblebee](https://twitter.com/hmblebee)
14 |
15 | ## License
16 |
17 | Source code and related resources are Copyright (C) Matthew Reagan 2016. The source code is released under the [MIT License](https://opensource.org/licenses/MIT).
18 |
--------------------------------------------------------------------------------
/SpriteKitShaders/Shaders/simpleLiquidShader.fsh:
--------------------------------------------------------------------------------
1 | //
2 | // A simple 'liquid' shader which creates a ripple effect
3 | // by offsetting the actual pixel color we return with a nearby
4 | // sample, which is calculated with a sine wave.
5 | // For LICENSE information please see AppDelegate.swift.
6 | //
7 |
8 | void main() {
9 |
10 | // Set up some animation parameters for the waveform
11 |
12 | float speed = u_time * 0.35;
13 | float frequency = 14.0;
14 | float intensity = 0.006;
15 |
16 | // Get the coordinate for the target pixel
17 | vec2 coord = v_tex_coord;
18 |
19 | // Modify (offset slightly) using a sine wave
20 | coord.x += cos((coord.x + speed) * frequency) * intensity;
21 | coord.y += sin((coord.y + speed) * frequency) * intensity;
22 |
23 | // Rather than the original pixel color, using the offset target pixel
24 | vec4 targetPixelColor = texture2D(u_texture, coord);
25 |
26 | // Finish up by setting the actual color on gl_FragColor
27 | gl_FragColor = targetPixelColor;
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/SpriteKitShaders/UtilityExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UtilityExtensions.swift
3 | // SpriteKitShaders
4 | //
5 | // Created by Matthew Reagan on 8/3/18.
6 | // Copyright © 2018 Matt Reagan. All rights reserved.
7 | //
8 |
9 | import SpriteKit
10 |
11 | extension CGPoint {
12 | func distance(to otherPoint: CGPoint) -> CGFloat {
13 | let xDelta = x - otherPoint.x
14 | let yDelta = y - otherPoint.y
15 | return ((xDelta * xDelta) + (yDelta * yDelta)).squareRoot()
16 | }
17 | }
18 |
19 | extension SKAction {
20 | func byEasingIn() -> SKAction {
21 | self.timingMode = .easeIn
22 | return self
23 | }
24 | func byEasingInOut() -> SKAction {
25 | self.timingMode = .easeInEaseOut
26 | return self
27 | }
28 | func byEasingOut() -> SKAction {
29 | self.timingMode = .easeOut
30 | return self
31 | }
32 | }
33 |
34 | struct Random {
35 | static func seed() {
36 | srand48(Int(time(nil)))
37 | }
38 | static func between0And1() -> Float {
39 | return Float(drand48())
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/SpriteKitShaders/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/SpriteKitShaders/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2018 Matt Reagan. All rights reserved.
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 | PrefersOpenGL
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/SpriteKitShaders/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SpriteKitShaders
4 | //
5 | // Created by Matthew Reagan on 8/1/18.
6 | // Copyright © 2018 Matt Reagan. All rights reserved.
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
9 | // documentation files (the "Software"), to deal in the Software without restriction, including without limitation
10 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
11 | // and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
16 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | import Cocoa
21 | import SpriteKit
22 |
23 | @NSApplicationMain
24 | class AppDelegate: NSObject, NSApplicationDelegate {
25 |
26 | @IBOutlet weak var window: NSWindow!
27 | var skView: SKView!
28 |
29 | func applicationDidFinishLaunching(_ aNotification: Notification) {
30 | Random.seed()
31 | DispatchQueue.main.async {
32 | self.setUpView()
33 | }
34 | }
35 |
36 | func setUpView() {
37 | guard let contentView = window.contentView else { return }
38 |
39 | let scene = PoolScene.init(size: contentView.bounds.size)
40 | scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
41 | scene.backgroundColor = SKColor.black
42 | scene.scaleMode = .aspectFit
43 |
44 | skView = SKView.init(frame: contentView.bounds)
45 | skView.showsFPS = true
46 | skView.showsNodeCount = true
47 | skView.autoresizingMask = [.width, .height]
48 |
49 | contentView.addSubview(skView)
50 | skView.presentScene(scene)
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/SpriteKitShaders/Shaders/poolWaterShader.fsh:
--------------------------------------------------------------------------------
1 | //
2 | // 8/3/18 Demo shader for SpriteKit blog post by Matt Reagan.
3 | // More info: http://sound-of-silence.com | @hmblebee
4 | // For LICENSE information please see AppDelegate.swift.
5 | //
6 | // A shader which applies several different effects to
7 | // create a water-like effect on the texture:
8 | //
9 | // 1. Warps the texture with a moving sine wave form
10 | // 2. Slowly scrolls the texture horizontally (wrapping)
11 | // 3. Applies a blue darkening/subtraction color effect to the edges
12 | // of the texture (also with a moving sine wave)
13 | // 4. Applies a circular alpha masking by cropping any pixels outside
14 | // of a given radius
15 | //
16 | // +------------------------------------------------------------------------+
17 | // This shader should compile and run in both Metal and OpenGL environments.
18 | // (You can use PrefersOpenGL key in the app's Info.plist to toggle.)
19 | // It could most definitely be improved! This shader is entirely for
20 | // fun / demonstration purposes. - Matt
21 | // +------------------------------------------------------------------------+
22 |
23 | //
24 | // Ensure backwards compatibility here with GLSL on
25 | // non-Metal hardware, modulus operator may not be available.
26 | //
27 |
28 | int glMod(int a, int b) {
29 | float fa = a;
30 | float fb = b;
31 | return int(a - (b * floor(fa/fb)));
32 | }
33 |
34 | //
35 | // Our shader main() function which does all the work and should set the
36 | // resulting pixel color on gl_FragColor before completing.
37 | //
38 |
39 | void main() {
40 |
41 | // Define some variables for our principal wave form and drift
42 | float speed = u_time * 0.7;
43 | float frequency = 20.0;
44 | float intensity = 0.005;
45 | int driftSteps = 1200;
46 |
47 | // Get the current pixel coordinate we're operating on
48 | vec2 coord = v_tex_coord;
49 |
50 | // Check the distance of this coordinate from our texture center
51 | float distanceFromCenter = distance(coord, vec2(0.5,0.5));
52 |
53 | // If the distance is greater than 0.5 (a perfect circle within our
54 | // original square texture), we can finish immediately, we set a
55 | // clear color to effectively crop the pixel
56 | if (distanceFromCenter > 0.5) {
57 | gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
58 | } else {
59 |
60 | // Calculate the 'drift' (horizontal scrolling) based on u_time
61 | float driftAmount = glMod(int(u_time * 80), driftSteps);
62 | float adj = driftAmount / driftSteps;
63 |
64 | // Here the wave form is applied by offsetting the actual pixel
65 | // color we'll return based on cos/sin functions. We also apply
66 | // the 'drift' amount to the x coordinate to provide a slow
67 | // horizontal scrolling effect. Note that fract() is called on the
68 | // resulting coord.x as part of the calculation which allows us
69 | // to wrap the coordinate around if we exceed 1.0.
70 | coord.x += cos((coord.x + speed) * frequency) * intensity + adj;
71 | coord.x = fract(coord.x);
72 | coord.y += sin((coord.y + speed) * frequency) * intensity;
73 |
74 | // Get the color at the new coordinate we've calculated
75 | vec4 newColor = texture2D(u_texture, coord);
76 |
77 | // Finally, we apply a darkening effect here to the edges of the pool.
78 | // Set up a few values for the effect first
79 | float edgeShadowDistance = 0.16;
80 | float edgeShadowThreshold = (0.5 - edgeShadowDistance);
81 | float edgeShadowRippleIntensity = 0.16;
82 | float edgeShadowDarkeningValue = 50.0;
83 |
84 | // Check if this pixel is within the edge shadow threshold to be darkened
85 | if (distanceFromCenter > edgeShadowThreshold) {
86 |
87 | float darkenAmount = (distanceFromCenter - edgeShadowThreshold) / edgeShadowDistance;
88 | darkenAmount -= edgeShadowRippleIntensity;
89 |
90 | // Here we use cos() to give our edge darkening a nice moving wave shape
91 | // based on both the X,Y coordinates as well as the progressing simulation
92 | // time. (Try removing coord.x and coord.y from the calculation to see the
93 | // edge pulse instead.)
94 | float edgeCosineWaveValue = cos((coord.x + coord.y + glMod(int(u_time * 30), 2000)) * edgeShadowDarkeningValue);
95 |
96 | // Some final adjustments here to further adjust the output color
97 | darkenAmount += edgeCosineWaveValue * edgeShadowRippleIntensity;
98 | darkenAmount /= 2.3;
99 | darkenAmount = max(darkenAmount, 0.0);
100 |
101 | // For an additional subtlety we further adjust the edges of the rendered
102 | // texure by fading them slightly (reducing the alpha value)
103 | float edgeFadeDistance = edgeShadowDistance / 3.0;
104 | float edgeFadeThreshold = (0.5 - edgeFadeDistance);
105 | if (distanceFromCenter > edgeFadeThreshold) {
106 | float edgeFadeAmount = 1.0 - ((distanceFromCenter - edgeFadeThreshold) / edgeFadeDistance / 2.6);
107 | newColor.a = edgeFadeAmount;
108 | }
109 |
110 | // Here the final edge darkening adjustment is subtracted from our RGB values
111 | // (and the alpha value is multiplied). Try adding rather than subtracting for an edge glow
112 | gl_FragColor = vec4((newColor.rgb - darkenAmount) * newColor.a, newColor.a);
113 | } else {
114 | // If we're not applying any edge effects we can finish up immediately.
115 | gl_FragColor = newColor;
116 | }
117 | }
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/SpriteKitShaders/PoolScene.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoolScene.swift
3 | // SpriteKitShaders
4 | //
5 | // Created by Matthew Reagan on 8/3/18.
6 | // Copyright © 2018 Matt Reagan. All rights reserved.
7 | //
8 | // More info: http://sound-of-silence.com
9 | //
10 | // For LICENSE information please see AppDelegate.swift.
11 |
12 | import SpriteKit
13 |
14 | class PoolScene: SKScene {
15 |
16 | // MARK: - Properties
17 |
18 | let waterNode = SKSpriteNode(imageNamed: "water")
19 | let emitterNode = SKEmitterNode(fileNamed: "LightSparkle")!
20 | var waterWarpPositions: [float2] = []
21 | let waterWarpGridSize = 12
22 |
23 | // MARK: - Init
24 |
25 | override init(size: CGSize) {
26 | super.init(size: size)
27 | configureNodes()
28 | }
29 |
30 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
31 |
32 | // MARK: - Node Configuration
33 |
34 | func configureNodes() {
35 | waterNode.position = CGPoint(x: 0.0, y: 12.0);
36 | waterNode.shader = SKShader(fileNamed: "poolWaterShader")
37 | addChild(waterNode)
38 |
39 | emitterNode.position = waterNode.position
40 | addChild(emitterNode)
41 |
42 | let backgroundNode = SKSpriteNode(imageNamed: "poolBackground")
43 | backgroundNode.size = size
44 | backgroundNode.zPosition = -10.0
45 | addChild(backgroundNode)
46 |
47 | let spriteKitTextNode = SKSpriteNode(imageNamed: "spritekitText")
48 | spriteKitTextNode.zPosition = 50.0
49 | spriteKitTextNode.position = CGPoint(x: 0.0, y: 250.0)
50 | spriteKitTextNode.shader = SKShader(fileNamed: "simpleLiquidShader")
51 | addChild(spriteKitTextNode)
52 |
53 | waterWarpPositions = geometryGridPositions(byWarping: false)
54 | waterNode.warpGeometry = SKWarpGeometryGrid(columns: waterWarpGridSize, rows: waterWarpGridSize)
55 | randomizeWaterGeometry()
56 | }
57 |
58 | // MARK: - Animation Functions
59 |
60 | func geometryGridPositions(byWarping: Bool) -> [float2] {
61 | var points = [float2]()
62 | for y in 0...waterWarpGridSize {
63 | for x in 0...waterWarpGridSize {
64 | let shouldWarp = byWarping && x > 0 && y > 0 && x < waterWarpGridSize && y < waterWarpGridSize
65 | let warpAmount: Float = 1.0 / Float(waterWarpGridSize + 1) / 4.0
66 | func randomizedWarpAmount() -> Float { return (shouldWarp ? warpAmount * Random.between0And1() - (warpAmount / 2.0) : 0.0) }
67 | let nx = Float(x) * (1.0 / Float(waterWarpGridSize)) + randomizedWarpAmount()
68 | let ny = Float(y) * (1.0 / Float(waterWarpGridSize)) + randomizedWarpAmount()
69 | points.append(float2(nx,ny))
70 | }
71 | }
72 | return points
73 | }
74 |
75 | func randomizeWaterGeometry() {
76 | let sourcePositions = waterWarpPositions
77 | waterWarpPositions = geometryGridPositions(byWarping: true)
78 | let warpGeometryGrid = SKWarpGeometryGrid.init(columns: waterWarpGridSize, rows: waterWarpGridSize, sourcePositions: sourcePositions, destinationPositions: waterWarpPositions)
79 | let warpDuration = TimeInterval(0.36)
80 | if let warpAction = SKAction.warp(to: warpGeometryGrid, duration: warpDuration) {
81 | waterNode.run(.sequence([
82 | warpAction.byEasingInOut(),
83 | .run({[weak self] in self?.randomizeWaterGeometry()})
84 | ]))
85 | }
86 | }
87 |
88 | // MARK: - Event Handlers
89 |
90 | override func mouseDown(with event: NSEvent) {
91 | super.mouseDown(with: event)
92 |
93 | let pointInPool = event.location(in: waterNode)
94 |
95 | if pointInPool.distance(to: .zero) < 160.0 {
96 | dropLeaf(at: pointInPool)
97 | }
98 | }
99 |
100 | // MARK: - Leaf Animation Utilities
101 |
102 | func dropLeaf(at pointInPool: CGPoint) {
103 | let leaf = SKSpriteNode(imageNamed: "leaf")
104 | leaf.zPosition = 5.0
105 | leaf.position = pointInPool
106 | leaf.alpha = 0.4
107 | leaf.zRotation = CGFloat(Random.between0And1() * (Float.pi * 2.0))
108 | waterNode.addChild(leaf)
109 |
110 | let blueFadeColor = SKColor.init(red: 0.0, green: 0.0, blue: 0.7, alpha: 1.0)
111 |
112 | let randomRotationAngle = CGFloat(Random.between0And1() * (Float.pi * 2.0) - Float.pi)
113 | let randomDriftAngle = CGFloat(Random.between0And1() * (Float.pi * 2.0) - Float.pi)
114 | let randomDriftDistance = CGFloat(Random.between0And1() * 30.0) + 20.0
115 | let driftDestination = CGPoint(x: randomDriftDistance * cos(randomDriftAngle) * CGFloat(Random.between0And1()) + pointInPool.x,
116 | y: randomDriftDistance * sin(randomDriftAngle) * CGFloat(Random.between0And1()) + pointInPool.y)
117 |
118 | let totalAnimationDuration = TimeInterval(2.2)
119 | let leafFallDuration = TimeInterval(0.82)
120 | let leafDieDuration = TimeInterval(0.85)
121 | leaf.run(.sequence([
122 | .group([
123 | SKAction.scale(to: 0.5, duration: leafFallDuration).byEasingIn(),
124 | .fadeIn(withDuration: leafFallDuration)]),
125 | .run({ [weak self] in self?.createRipple(at: pointInPool) }),
126 | .group([
127 | .rotate(byAngle: randomRotationAngle, duration: totalAnimationDuration),
128 | .move(to: driftDestination, duration: totalAnimationDuration),
129 | .colorize(with: blueFadeColor, colorBlendFactor: 0.4, duration: totalAnimationDuration)]),
130 | .group([
131 | .scale(to: 0.25, duration: leafDieDuration),
132 | .colorize(with: blueFadeColor, colorBlendFactor: 0.6, duration: leafDieDuration),
133 | .fadeAlpha(to: 0.14, duration: leafDieDuration)]),
134 | .fadeOut(withDuration: 5.0),
135 | .removeFromParent(),
136 | ]))
137 | }
138 |
139 | func createRipple(at pointInPool: CGPoint) {
140 | let numRipples = 4
141 | for i in 0..
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
--------------------------------------------------------------------------------