//
// ContentView.swift
// AlbumCovers
//
// Created by Nien Lam on 9/15/21.
// Copyright © 2021 Line Break, LLC. All rights reserved.
//
import SwiftUI
class ViewModel: ObservableObject {
// Intialize with placeholder information.
@Published var coverImage: String = "abbey-road.jpg"
@Published var albumName: String = "Abbey Road"
@Published var artist: String = "Beatles"
@Published var currentTrack: String = "1. Come Together"
// Indices for current album and track to display.
var albumIdx: Int = 0
var trackIdx: Int = 0
// TODO: Create a stucture for holding album data.
// coverImage, albumName, artist, tracks
struct album {
var coverImage: String
var albumName: String
var artist: String
var tracks: [String]
}
// TODO: Create an empty array to store multiple albums.
var albums: [album] = []
init() {
// TODO: Initialize 3 or more albums with data.
let abbeyRoad = album(coverImage: "abbey-road.jpg", albumName: "Abbey Road", artist: "Beatles", tracks: ["1. Come Together", "2. Something", "3. Maxwell's Silver Hammer", "4. Oh! Darling", "5. Octopus's Garden"])
let letItBe = album(coverImage: "letItBe.jpeg", albumName: "Let It Be", artist: "Beatles", tracks: ["1. Two of Us", "2. Dig A Pony", "3. Let It Be", "4. I Me Mine", "5. Accross the Universe"])
let beatlesForSale = album(coverImage: "beatlesForSale.jpeg", albumName: "Beatles For Sale", artist: "Beatles", tracks: ["1. I'm a Loser", "2. No Reply", "3. Baby's in Black", "4. Rock and Roll Music", "5. I'll Follow the Sun"])
// TODO: Append albums to array.
albums.append(abbeyRoad)
albums.append(letItBe)
albums.append(beatlesForSale)
// TODO: Intialize screen variables with first album.
// coverImage, albumName, artist, currentTrack
coverImage = albums[0].coverImage
albumName = albums[0].albumName
artist = albums[0].artist
currentTrack = albums[0].tracks[0]
}
// TODO: Update variables: albumIdx, trackIdx, coverImage, albumName, artist, currentTrack
func nextAlbumButtonPressed() {
print("🔺 Did press Next Album")
if albumIdx < 2 {
albumIdx += 1
} else {
albumIdx = 0
}
trackIdx = 0
coverImage = albums[albumIdx].coverImage
albumName = albums[albumIdx].albumName
artist = albums[albumIdx].artist
currentTrack = albums[albumIdx].tracks[0]
}
// TODO: Update variables: trackIdx and currentTrack
func nextTrackButtonPressed() {
print("🔺 Did press Next Track")
if trackIdx < 4 {
trackIdx += 1
} else {
trackIdx = 0
}
currentTrack = albums[albumIdx].tracks[trackIdx]
}
}
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Image(uiImage: UIImage(named: viewModel.coverImage)!)
.resizable()
.frame(width: 300, height: 300)
.padding(.bottom, 10)
Text(viewModel.albumName)
.font(.system(.title))
Text(viewModel.artist)
.font(.system(.title2))
.padding(.bottom, 10)
Text(viewModel.currentTrack)
.font(.system(.subheadline))
.padding(.bottom, 30)
Button {
viewModel.nextAlbumButtonPressed()
} label: {
actionLabel(text: "Next Album", color: .green)
}
.padding(.bottom, 15)
Button {
viewModel.nextTrackButtonPressed()
} label: {
actionLabel(text: "Next Track", color: .orange)
}
}
}
// Helper method for rendering button label.
func actionLabel(text: String, color: Color) -> some View {
Label(text, systemImage: "chevron.forward.square")
.font(.system(.body))
.foregroundColor(.white)
.frame(width: 200, height: 44)
.background(color)
.cornerRadius(4)
}
}
// ContentView.swift
// Tetrominoes
//
// Created by Nien Lam on 9/15/21.
// Copyright © 2021 Line Break, LLC. All rights reserved.
//
import SwiftUI
import ARKit
import RealityKit
import Combine
// MARK: - View model for handling communication between the UI and ARView.
class ViewModel: ObservableObject {
let uiSignal = PassthroughSubject<UISignal, Never>()
enum UISignal {
case straightSelected
case squareSelected
case tSelected
case lSelected
case skewSelected
}
}
// MARK: - UI Layer.
struct ContentView : View {
@StateObject var viewModel = ViewModel()
var body: some View {
ZStack {
// AR View.
ARViewContainer(viewModel: viewModel)
// Bottom buttons.
HStack {
Button {
viewModel.uiSignal.send(.straightSelected)
} label: {
tetrominoIcon("straight", color: Color(red: 0, green: 1, blue: 1))
}
Button {
viewModel.uiSignal.send(.squareSelected)
} label: {
tetrominoIcon("square", color: .yellow)
}
Button {
viewModel.uiSignal.send(.tSelected)
} label: {
tetrominoIcon("t", color: .purple)
}
Button {
viewModel.uiSignal.send(.lSelected)
} label: {
tetrominoIcon("l", color: .orange)
}
Button {
viewModel.uiSignal.send(.skewSelected)
} label: {
tetrominoIcon("skew", color: .green)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 30)
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
// Helper method for rendering icon.
func tetrominoIcon(_ image: String, color: Color) -> some View {
Image(image)
.resizable()
.padding(3)
.frame(width: 44, height: 44)
.background(color)
.cornerRadius(5)
}
}
// MARK: - AR View.
struct ARViewContainer: UIViewRepresentable {
let viewModel: ViewModel
func makeUIView(context: Context) -> ARView {
SimpleARView(frame: .zero, viewModel: viewModel)
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
class SimpleARView: ARView {
var viewModel: ViewModel
var arView: ARView { return self }
var originAnchor: AnchorEntity!
var subscriptions = Set<AnyCancellable>()
// Empty entity for cursor.
var cursor: Entity!
// Root entities for pieces.
var straightEntity: Entity!
var squareEntity: Entity!
var tEntity: Entity!
var lEntity: Entity!
var skewEntity: Entity!
init(frame: CGRect, viewModel: ViewModel) {
self.viewModel = viewModel
super.init(frame: frame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(frame frameRect: CGRect) {
fatalError("init(frame:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
UIApplication.shared.isIdleTimerDisabled = true
setupScene()
setupEntities()
// Enable straight piece on startup.
straightEntity.isEnabled = true
squareEntity.isEnabled = false
tEntity.isEnabled = false
lEntity.isEnabled = false
skewEntity.isEnabled = false
}
func setupScene() {
// Setup world tracking and plane detection.
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
configuration.environmentTexturing = .automatic
arView.renderOptions = [.disableDepthOfField, .disableMotionBlur]
arView.session.run(configuration)
// Called every frame.
scene.subscribe(to: SceneEvents.Update.self) { event in
self.updateCursor()
}.store(in: &subscriptions)
// Process UI signals.
viewModel.uiSignal.sink { [weak self] in
self?.processUISignal($0)
}.store(in: &subscriptions)
}
// Hide/Show active tetromino.
func processUISignal(_ signal: ViewModel.UISignal) {
straightEntity.isEnabled = false
squareEntity.isEnabled = false
tEntity.isEnabled = false
lEntity.isEnabled = false
skewEntity.isEnabled = false
switch signal {
case .straightSelected:
straightEntity.isEnabled = true
case .squareSelected:
squareEntity.isEnabled = true
case .tSelected:
tEntity.isEnabled = true
case .lSelected:
lEntity.isEnabled = true
case .skewSelected:
skewEntity.isEnabled = true
}
}
// Move cursor to plane detected.
func updateCursor() {
// Raycast to get cursor position.
let results = raycast(from: center,
allowing: .existingPlaneGeometry,
alignment: .any)
// Move cursor to position if hitting plane.
if let result = results.first {
cursor.isEnabled = true
cursor.move(to: result.worldTransform, relativeTo: originAnchor)
} else {
cursor.isEnabled = false
}
}
func setupEntities() {
// Create an anchor at scene origin.
originAnchor = AnchorEntity(world: .zero)
arView.scene.addAnchor(originAnchor)
// Create and add empty cursor entity to origin anchor.
cursor = Entity()
originAnchor.addChild(cursor)
// Create root tetrominoes. ////
// Box mesh.
let boxSize: Float = 0.03
let cornerRadius: Float = 0.002
let boxMesh = MeshResource.generateBox(size: boxSize, cornerRadius: cornerRadius)
// Colored materials.
let cyanMaterial = SimpleMaterial(color: .cyan, isMetallic: false)
let yellowMaterial = SimpleMaterial(color: .yellow, isMetallic: false)
let purpleMaterial = SimpleMaterial(color: .purple, isMetallic: false)
let orangeMaterial = SimpleMaterial(color: .orange, isMetallic: false)
let greenMaterial = SimpleMaterial(color: .green, isMetallic: false)
// Create an relative origin entity for centering the root box of each tetromino.
let relativeOrigin = Entity()
relativeOrigin.position.y = boxSize / 2
cursor.addChild(relativeOrigin)
// Straight piece.
straightEntity = ModelEntity(mesh: boxMesh, materials: [cyanMaterial])
relativeOrigin.addChild(straightEntity)
// Square piece.
squareEntity = ModelEntity(mesh: boxMesh, materials: [yellowMaterial])
relativeOrigin.addChild(squareEntity)
// T piece.
tEntity = ModelEntity(mesh: boxMesh, materials: [purpleMaterial])
relativeOrigin.addChild(tEntity)
// L piece.
lEntity = ModelEntity(mesh: boxMesh, materials: [orangeMaterial])
relativeOrigin.addChild(lEntity)
// Skew piece.
skewEntity = ModelEntity(mesh: boxMesh, materials: [greenMaterial])
relativeOrigin.addChild(skewEntity)
// TODO: Create tetrominoes //////////////////////////////////////
// ... create straight piece.
// 1. Clone new cube.
let newCube = straightEntity.clone(recursive: false)
// 2. Set position based on root tetromino entity.
newCube.position.y = boxSize
// 3. Add child to root tetromino entity.
straightEntity.addChild(newCube)
let newCube1 = straightEntity.clone(recursive: false)
newCube1.position.y = boxSize * 2
straightEntity.addChild(newCube1)
let newCube2 = straightEntity.clone(recursive: false)
newCube2.position.y = boxSize * 3
straightEntity.addChild(newCube2)
// ... create square piece.
let squareCube1 = squareEntity.clone(recursive: false)
squareCube1.position.y = boxSize
squareEntity.addChild(squareCube1)
let squareCube2 = squareEntity.clone(recursive: false)
squareCube2.position.x = boxSize
squareEntity.addChild(squareCube2)
let squareCube3 = squareEntity.clone(recursive: false)
squareCube3.position.x = boxSize
squareCube3.position.y = boxSize
squareEntity.addChild(squareCube3)
// ... create t piece.
let tCube1 = tEntity.clone(recursive: false)
tCube1.position.y = boxSize
tEntity.addChild(tCube1)
let tCube2 = tEntity.clone(recursive: false)
tCube2.position.x = boxSize
tEntity.addChild(tCube2)
let tCube3 = tEntity.clone(recursive: false)
tCube3.position.x = boxSize * -1
tEntity.addChild(tCube3)
// ... create l piece.
let lCube1 = lEntity.clone(recursive: false)
lCube1.position.y = boxSize
lEntity.addChild(lCube1)
let lCube2 = lEntity.clone(recursive: false)
lCube2.position.y = boxSize * 2
lEntity.addChild(lCube2)
let lCube3 = lEntity.clone(recursive: false)
lCube3.position.x = boxSize
lEntity.addChild(lCube3)
// ... create skew piece.
let skewCube1 = skewEntity.clone(recursive: false)
skewCube1.position.y = boxSize
skewEntity.addChild(skewCube1)
let skewCube2 = skewEntity.clone(recursive: false)
skewCube2.position.x = boxSize * -1
skewEntity.addChild(skewCube2)
let skewCube3 = skewEntity.clone(recursive: false)
skewCube3.position.x = boxSize
skewCube3.position.y = boxSize
skewEntity.addChild(skewCube3)
/////////////////////////////////////////////////////////////////////////
}
}
//
// ContentView.swift
// AnimatedSculpture
//
// Created by Nien Lam on 9/15/21.
// Copyright © 2021 Line Break, LLC. All rights reserved.
//
import SwiftUI
import ARKit
import RealityKit
import Combine
// MARK: - View model for handling communication between the UI and ARView.
class ViewModel: ObservableObject {
let uiSignal = PassthroughSubject<UISignal, Never>()
@Published var positionLocked = false
@Published var sliderValue: Double = 0
enum UISignal {
case lockPosition
}
}
// MARK: - UI Layer.
struct ContentView : View {
@StateObject var viewModel = ViewModel()
var body: some View {
ZStack {
ARViewContainer(viewModel: viewModel)
HStack {
Button {
viewModel.uiSignal.send(.lockPosition)
} label: {
Label("Lock Position", systemImage: "target")
.font(.system(.title))
.foregroundColor(.white)
.labelStyle(IconOnlyLabelStyle())
.frame(width: 44, height: 44)
.opacity(viewModel.positionLocked ? 0.25 : 1.0)
}
Slider(value: $viewModel.sliderValue, in: 0...1)
.accentColor(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.horizontal, 10)
.padding(.bottom, 30)
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
// MARK: - AR View.
struct ARViewContainer: UIViewRepresentable {
let viewModel: ViewModel
func makeUIView(context: Context) -> ARView {
SimpleARView(frame: .zero, viewModel: viewModel)
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
class SimpleARView: ARView {
var viewModel: ViewModel
var arView: ARView { return self }
var originAnchor: AnchorEntity!
var subscriptions = Set<AnyCancellable>()
// Empty entity for cursor.
var cursor: Entity!
// TODO: Add any local variables here. //////////////////////////////////////
var boxEntity: Entity!
var sphereEntity: Entity!
var upDnToggle = false
init(frame: CGRect, viewModel: ViewModel) {
self.viewModel = viewModel
super.init(frame: frame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(frame frameRect: CGRect) {
fatalError("init(frame:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
UIApplication.shared.isIdleTimerDisabled = true
setupScene()
setupEntities()
}
func setupScene() {
// Setup world tracking and plane detection.
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
configuration.environmentTexturing = .automatic
arView.renderOptions = [.disableDepthOfField, .disableMotionBlur]
arView.session.run(configuration)
// Called every frame.
scene.subscribe(to: SceneEvents.Update.self) { event in
// Update cursor position when position is not locked.
if !self.viewModel.positionLocked {
self.updateCursor()
}
// Call renderLoop method on every frame.
self.renderLoop()
}.store(in: &subscriptions)
// Process UI signals.
viewModel.uiSignal.sink { [weak self] in
self?.processUISignal($0)
}.store(in: &subscriptions)
}
// Process UI signals.
func processUISignal(_ signal: ViewModel.UISignal) {
switch signal {
case .lockPosition:
viewModel.positionLocked.toggle()
}
}
// Move cursor to plane detected.
func updateCursor() {
// Raycast to get cursor position.
let results = raycast(from: center,
allowing: .existingPlaneGeometry,
alignment: .any)
// Move cursor to position if hitting plane.
if let result = results.first {
cursor.isEnabled = true
cursor.move(to: result.worldTransform, relativeTo: originAnchor)
} else {
cursor.isEnabled = false
}
}
// TODO: Setup entities. //////////////////////////////////////
func setupEntities() {
// Create an anchor at scene origin.
originAnchor = AnchorEntity(world: .zero)
arView.scene.addAnchor(originAnchor)
// Create and add empty cursor entity to origin anchor.
cursor = Entity()
originAnchor.addChild(cursor)
// Checker material.
var checkerMaterial = SimpleMaterial()
let texture = try! TextureResource.load(named: "neonGreen.png")
checkerMaterial.baseColor = .texture(texture)
// Setup example box entity.
let boxMesh = MeshResource.generateSphere(radius: 0.05)//(size: [0.03, 0.03, 0.03], cornerRadius: 0.0)
boxEntity = ModelEntity(mesh: boxMesh, materials: [checkerMaterial])
boxEntity.position.y = 0.015
cursor.addChild(boxEntity)
// Example: Stair pattern.
for idx in 1..<100 {
// Create and position new entity.
let newEntity = boxEntity.clone(recursive: false)
newEntity.position.x = Float(idx) * Float.random(in: 0.01..<0.5)
newEntity.position.y = Float(idx) * Float.random(in: 0.01..<0.5)
newEntity.position.z = Float(idx) * Float.random(in: 0.01..<0.5)
// Add to starting entity.
boxEntity.addChild(newEntity)
}
/*
// Example: Spiral stair pattern.
// Remember last entity in tree.
var lastBoxEntity = boxEntity
for _ in 0..<10 {
// Create and position new entity.
let newEntity = boxEntity.clone(recursive: false)
newEntity.position.x = 0.03
newEntity.position.y = 0.03
// Rotate on y-axis by 45 degrees.
newEntity.orientation = simd_quatf(angle: .pi / 4, axis: [0, 1, 0])
// Add to last entity in tree.
lastBoxEntity?.addChild(newEntity)
// Set last entity used.
lastBoxEntity = newEntity
}
*/
// // Setup example sphere entity.
// let sphereMesh = MeshResource.generateSphere(radius: 0.015)
// sphereEntity = ModelEntity(mesh: sphereMesh, materials: [checkerMaterial])
// sphereEntity.position.x = 0.075
// sphereEntity.position.y = 0.015
// cursor.addChild(sphereEntity)
}
// TODO: Animate entities. //////////////////////////////////////
func renderLoop() {
// Slider value from UI.
let sliderValue = Float(viewModel.sliderValue)
// Scale sphere entity based on slider value.
boxEntity.scale = [1 + sliderValue * 10, 1 + sliderValue * 10, 1 + sliderValue * 10]
// Increment or decrement z position of sphere.
if upDnToggle {
boxEntity.position.z += 0.002
} else {
boxEntity.position.z -= 0.002
}
// Put limits on movement.
if boxEntity.position.z > 0.1 {
upDnToggle = false
} else if boxEntity.position.z < -0.1 {
upDnToggle = true
}
// Spin box entity on y axis.
boxEntity.orientation *= simd_quatf(angle: 0.001, axis: [1, 1, 0])
}
}
For the animated sculpture project, my initial idea was to create a bunch of virtual fireflies and add them to our physical world. When I was a kid, I always see fireflies glowing everywhere in my city in the summer, but as time goes by and our environments deteriorate, they gradually disappeared. However, when testing, I had a hard time trying to figure out how to add the emission/glowing effect to my texture in swift. Without the emission effect, the sphere with the matte green texture actually reminds me of the virus. And I changed my mind that I wanted to visualized the coronavirus in our space through augmented reality. I randomized the size and position of each sphere in order to fill the physical environment with green dots, which represent the virus. By doing so, I hope to remind people that we are still going through Covid-19 pandemic The virus is everywhere and we should never lower our guard protecting ourselves and fighting against it.
https://www.youtube.com/watch?v=ocbC4leltTA
https://www.youtube.com/watch?v=9KLYqclgs90