Album Cover Practice

//
//  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)
    }
}

Tetrominoes Practice

//  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)
        
        
        
        /////////////////////////////////////////////////////////////////////////
    }
}

Animated Sculpture Practice

//
//  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])
    }
}

Untitled_Artwork.jpg

    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

IMG_5310.PNG

IMG_5309.PNG

IMG_5312.PNG