Skip to content

Commit

Permalink
Add image files
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinFincher committed May 13, 2020
1 parent 6fec818 commit 71126b2
Show file tree
Hide file tree
Showing 29 changed files with 419 additions and 156 deletions.
34 changes: 34 additions & 0 deletions Download/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# README

You can also download the source code project on [https://github.com/JustinFincher/WWDC20Playground](https://github.com/JustinFincher/WWDC20Playground)

# Contact

Haotian Zheng
[mailto:justzht@gmail.com](justzht@gmail.com)
+86 18556572637 / +1 4697512468

# Tell us about the features and technologies you used in your Swift playground.

Shader Node Editor is a node-based shader editor framework I wrote for easy graphics programming on iPad (and Mac via Catalyst). The core of Shader Node Editor uses UIKit & UIKit Dynamics for layout and SpriteKit for shader preview & compilation, while the peripheral includes certain usages of AVFoundation, Combine, and SwiftUI.

Features:
- Visual scripting with a node-based editor
- Capability to easily extend the existing set of nodes even in runtime with subclassing
- Automatic shader preview updates
- Rich uniform input including audio, timestamp, and UV.
- Node-based UI framework developed for generic purposes that can be converted for storyline designer or state machine editor.

Technologies:
- UIKit Dynamics: the whole canvas along with various nodes follows physical rules in interactions thanks to UIKit Dynamics. You can drag & drop and they will maintain momentum until hitting the boundary.
- Node Graph: as SpriteKit already handles shader compilation for me, my editor only needs to do the code generation part. To generate OpenGL ES code, I deployed a 2-pass-brutal-force-approach. It is far from optimal, but at least it works: The first pass is responsible to use graph searching algorithms to gather linkage information and thus build a dependency graph for each node, and the second pass would declare variables for each knot on the nodes, append equal expressions on linked knots, and finally generate the shader code following the order previously collected in the dependency graph.
- Uniforms: shader uniforms like audio loudness are provided with Singletons for their sole purposes. Currently, the uniform value would be updated in a timer callback, but it can also be adjusted to follow SpriteKit drawing callbacks.

# Apps on the App Store (optional)

Developer page (with all apps included): https://itunes.apple.com/cn/developer/haotian-zheng/id981803173?mt=8

- Contributions For GitHub (https://itunes.apple.com/cn/app/contributions-for-github/id1153432612?mt=8) A small app for viewing your GitHub contributions graph in 2D / 3D perspective. Available on iOS and watchOS.
- Epoch Core (https://itunes.apple.com/cn/app/epoch-core/id1177530091?mt=8) Tech demo for showing off my noise shader and procedurally generated planet terrain. It can generate near 1 million different planets.
- ArtWall (https://itunes.apple.com/cn/app/artwall/id1178151992?mt=12) If you are a digital artist or just a person who likes digital art, ArtWall is here for you to save ArtStation images as your desktop wallpaper.
- Golf GO (https://itunes.apple.com/cn/app/golf-go-scholarship-edition/id1380656648?mt=8) WWDC 18 winner project, a mini-golf game with infinite golf maps to play. Written in 1000 Swift lines.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ struct ContentView: View {
Text("Allow Audio Permission")
}.disabled(store.audioPermissionEnabled)
}
Section(header: Text("Tutorial: How to connect nodes")) {
VStack(alignment: .leading){
HStack()
{
Image(uiImage: UIImage(named:"NodeTutorial1.jpg")!).resizable()
.frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight)
Text("1. Drag the knot on any side of a node and a line would appear following your position").font(.footnote)
}
HStack()
{
Image(uiImage: UIImage(named:"NodeTutorial2.jpg")!).resizable()
.frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight)
Text("2. The line would display in red if it cannot connect (no knot or invalid data format)").font(.footnote)
}
HStack()
{
Image(uiImage: UIImage(named:"NodeTutorial3.jpg")!).resizable()
.frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight)
Text("3. The line would display in green if target knot is compatible").font(.footnote)
}
HStack()
{
Image(uiImage: UIImage(named:"NodeTutorial4.jpg")!).resizable()
.frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight)
Text("4. Lift your finger when the line is green and it would be added between two knots").font(.footnote)
}
HStack()
{
Image(uiImage: UIImage(named:"NodeTutorial5.jpg")!).resizable()
.frame(width: Constant.tutorialRowHeight, height: Constant.tutorialRowHeight)
Text("5. Drag an already connected knot would make the line disconnected").font(.footnote)
}
}

}
}
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
//:
//: You are probably using Shaders all the time but you didn't notice them when you were applying filters on Instagram or playing video games on your iPad. What's more, Shaders are now used in Machine Learning to accelerate the learning process (compute shaders, specifically), and all these are achieved with only pure mathematical expressions, what marvelous engineering!
//:
//: ---
//: However, shader might be hard to learn at first due to its parallel computing nature. The program you wrote would be executed thousands of times in one frame with different output values, which is an abstract concept and people sometimes don't get it. That's why Shader Node Editor comes to rescue. With a node-based user interface, you can compose shaders in both feedback-rich and visual-pleasing way, and it is always real-time so you don't need to wait for a debug build.
//:
//: Without further due, let's dive into Shader programming with Shader Node Editor, my node-based expression editor written in Swift (`UIKit Dynamics`, `SwiftUI`, `SpriteKit`)!
//:
//: > ➡️ Please switch to the next page after the reading (and **necessary steps in the playground**).
//: > ➡️ Please switch to the next page after your reading (and **necessary steps in the playground**). Also, for more technicial info, please refer to images below:
//:
//: ![](Tech1.jpg)
//: ![](Tech2.jpg)
//: ![](Tech3.jpg)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//#-end-hidden-code
//: # 🧑‍💻 Video Jockey
//: VJ is like a DJ, but with visuals. Shaders are especially well-fit for such scenarios, let's make an audio-reactive shader that changes with music loudness! 🎶
//:
//: ![](AudioShader.jpg)
//:
//: **Follow these steps:**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class Constant: NSObject
public static let nodeConnectionCurveControlOffset : CGFloat = 90
public static let fontObliqueName = "Avenir-Oblique"
public static let fontBoldName = "Avenir-Black"
public static let tutorialRowHeight : CGFloat = 128
public static let nodeKnotIndicatorColor = UIColor.tertiarySystemFill.withAlphaComponent(0.4)
public static let lineNormalColor = UIColor.systemYellow.withAlphaComponent(0.6)
public static let lineRejectColor = UIColor.systemRed.withAlphaComponent(0.6)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,146 @@
//

import Foundation
import UIKit
import PlaygroundSupport

open class LiveViewController : NodeEditorViewController, PlaygroundLiveViewMessageHandler, PlaygroundLiveViewSafeAreaContainer
{
public override func viewDidLoad() {
super.viewDidLoad()

self.navigationItem.prompt = "Long press on canvas to add nodes. Use Cheat Sheet if you need help"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "🎲 Cheat Sheet", style: .plain, target: self, action: #selector(showCheatSheet))
}

@objc func showCheatSheet() -> Void {
let alertController = UIAlertController(title: "Cheat Sheet", message: "Select pre-made node graph if you cannot complete corresponding tutorials but want to see the final result instantly", preferredStyle: .actionSheet)

alertController.addAction(UIAlertAction(title: "Clear > Reset Graph", style: .default, handler: { (action) in
self.nodeEditorData.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute:
{
self.nodeEditorData.forceUpdate()
})
}))

alertController.addAction(UIAlertAction(title: "Complete > Getting Started", style: .default, handler: { (action) in
self.nodeEditorData.removeAll()
let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData()
floatGeneratorNode1.frame = CGRect.init(x: 16, y: 16, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height)
floatGeneratorNode1.value.value = 0.5
self.nodeEditorData.addNode(node: floatGeneratorNode1)

let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData()
floatGeneratorNode2.frame = CGRect.init(x: 16, y: 300, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height)
floatGeneratorNode2.value.value = 0.5
self.nodeEditorData.addNode(node: floatGeneratorNode2)

let floatAddNode : FloatAddNodeData = FloatAddNodeData()
floatAddNode.frame = CGRect.init(x: 300, y: 30, width: floatAddNode.frame.size.width, height: floatAddNode.frame.size.height)
self.nodeEditorData.addNode(node: floatAddNode)

self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: floatAddNode.inPorts[0])
self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: floatAddNode.inPorts[1])
self.nodeEditorData.forceUpdate()
}))

alertController.addAction(UIAlertAction(title: "Complete > Advanced Usages", style: .default, handler: { (action) in
self.nodeEditorData.removeAll()
let uvNode : Vec2TexCoordNodeData = Vec2TexCoordNodeData()
uvNode.frame = CGRect.init(x: 16, y: 16, width: uvNode.frame.size.width, height: uvNode.frame.size.height)
self.nodeEditorData.addNode(node: uvNode)

let uvSplitNode : Vec2ChannelSplitNodeData = Vec2ChannelSplitNodeData()
uvSplitNode.frame = CGRect.init(x: 250, y: 16, width: uvSplitNode.frame.size.width, height: uvSplitNode.frame.size.height)
self.nodeEditorData.addNode(node: uvSplitNode)
self.nodeEditorData.connectNode(outPort: uvNode.outPorts[0], inPort: uvSplitNode.inPorts[0])

let timeNode : FloatTimeNodeData = FloatTimeNodeData()
timeNode.frame = CGRect.init(x: 16, y: 300, width: timeNode.frame.size.width, height: timeNode.frame.size.height)
self.nodeEditorData.addNode(node: timeNode)

let sinNode : FloatSinNodeData = FloatSinNodeData()
sinNode.frame = CGRect.init(x: 250, y: 200, width: sinNode.frame.size.width, height: sinNode.frame.size.height)
self.nodeEditorData.addNode(node: sinNode)
self.nodeEditorData.connectNode(outPort: timeNode.outPorts[0], inPort: sinNode.inPorts[0])

let vec4CombineNode : Vec4ChannelCombineNodeData = Vec4ChannelCombineNodeData()
vec4CombineNode.frame = CGRect.init(x: 550, y: 70, width: vec4CombineNode.frame.size.width, height: vec4CombineNode.frame.size.height)
self.nodeEditorData.addNode(node: vec4CombineNode)
self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[0], inPort: vec4CombineNode.inPorts[0])
self.nodeEditorData.connectNode(outPort: uvSplitNode.outPorts[1], inPort: vec4CombineNode.inPorts[1])
self.nodeEditorData.connectNode(outPort: sinNode.outPorts[0], inPort: vec4CombineNode.inPorts[2])
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute:
{
self.nodeEditorData.forceUpdate()
})
}))

alertController.addAction(UIAlertAction(title: "Complete > VJ Machine", style: .default, handler: { (action) in
self.nodeEditorData.removeAll()

let atanNode : FloatUVCenterAtanNodeData = FloatUVCenterAtanNodeData()
atanNode.frame = CGRect.init(x: 16, y: 16, width: atanNode.frame.size.width, height: atanNode.frame.size.height)
self.nodeEditorData.addNode(node: atanNode)

let audioNode : FloatAudioDBNodeData = FloatAudioDBNodeData()
audioNode.frame = CGRect.init(x: 16, y: 300, width: audioNode.frame.size.width, height: audioNode.frame.size.height)
self.nodeEditorData.addNode(node: audioNode)

let floatGeneratorNode1 : FloatGeneratorNodeData = FloatGeneratorNodeData()
floatGeneratorNode1.frame = CGRect.init(x: 16, y: 400, width: floatGeneratorNode1.frame.size.width, height: floatGeneratorNode1.frame.size.height)
floatGeneratorNode1.value.value = 5.0
self.nodeEditorData.addNode(node: floatGeneratorNode1)

let floatGeneratorNode2 : FloatGeneratorNodeData = FloatGeneratorNodeData()
floatGeneratorNode2.frame = CGRect.init(x: 16, y: 640, width: floatGeneratorNode2.frame.size.width, height: floatGeneratorNode2.frame.size.height)
floatGeneratorNode2.value.value = 8.0
self.nodeEditorData.addNode(node: floatGeneratorNode2)

let discRayNode : FloatRayDiscNodeData = FloatRayDiscNodeData()
discRayNode.frame = CGRect.init(x: 300, y: 60, width: discRayNode.frame.size.width, height: discRayNode.frame.size.height)
self.nodeEditorData.addNode(node: discRayNode)
self.nodeEditorData.connectNode(outPort: atanNode.outPorts[0], inPort: discRayNode.inPorts[0])
self.nodeEditorData.connectNode(outPort: audioNode.outPorts[0], inPort: discRayNode.inPorts[1])
self.nodeEditorData.connectNode(outPort: floatGeneratorNode1.outPorts[0], inPort: discRayNode.inPorts[2])
self.nodeEditorData.connectNode(outPort: floatGeneratorNode2.outPorts[0], inPort: discRayNode.inPorts[3])

let floatGeneratorNode3 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData()
floatGeneratorNode3.frame = CGRect.init(x: 320, y: 470, width: floatGeneratorNode3.frame.size.width, height: floatGeneratorNode3.frame.size.height)
floatGeneratorNode3.value.value = 0.0
self.nodeEditorData.addNode(node: floatGeneratorNode3)

let floatGeneratorNode4 : FloatSliderGeneratorNodeData = FloatSliderGeneratorNodeData()
floatGeneratorNode4.frame = CGRect.init(x: 370, y: 650, width: floatGeneratorNode4.frame.size.width, height: floatGeneratorNode4.frame.size.height)
floatGeneratorNode4.value.value = 0.3
self.nodeEditorData.addNode(node: floatGeneratorNode4)

let circleOutlineNode : FloatCircleOutlineNodeData = FloatCircleOutlineNodeData()
circleOutlineNode.frame = CGRect.init(x: 550, y: 70, width: circleOutlineNode.frame.size.width, height: circleOutlineNode.frame.size.height)
self.nodeEditorData.addNode(node: circleOutlineNode)

self.nodeEditorData.connectNode(outPort: discRayNode.outPorts[0], inPort: circleOutlineNode.inPorts[0])
self.nodeEditorData.connectNode(outPort: floatGeneratorNode3.outPorts[0], inPort: circleOutlineNode.inPorts[1])
self.nodeEditorData.connectNode(outPort: floatGeneratorNode4.outPorts[0], inPort: circleOutlineNode.inPorts[2])

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200), execute:
{
self.nodeEditorData.forceUpdate()
})
}))

alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) in

}))
alertController.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem
present(alertController, animated: true, completion: {})
}

//MARK: PlaygroundLiveViewMessageHandler

public func receive(_ message: PlaygroundValue) {
// guard case let .string(messageString) = message else { return }
}

public func send(_ message: PlaygroundValue) {
Expand Down
Loading

0 comments on commit 71126b2

Please sign in to comment.