diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/.gitignore b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.mmp b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.mmp new file mode 100644 index 0000000..102d0e2 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.mmp @@ -0,0 +1,17 @@ +# This is a MadMachine project file in TOML format +# This file holds those parameters that could not be managed by SwiftPM +# Edit this file would change the behavior of the building/downloading procedure +# Those project files in the dependent libraries would be IGNORED + +# Specify the board name below +# There are "SwiftIOBoard" and "SwiftIOMicro" now +board = "SwiftIOMicro" + +# Specifiy the target triple below +# There are "thumbv7em-unknown-none-eabi" and "thumbv7em-unknown-none-eabihf" now +# If your code use significant floating-point calculation, +# plz set it to "thumbv7em-unknown-none-eabihf" +triple = "thumbv7em-unknown-none-eabi" + +# Reserved for future use +version = 1 diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.swift new file mode 100644 index 0000000..e399495 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MazeGame", + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/madmachineio/SwiftIO.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadBoards.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadDrivers.git", branch: "develop"), + .package(path: "/Users/andy/Documents/MadGraphics") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "MazeGame", + dependencies: [ + "SwiftIO", + "MadBoards", + // Use specific library name rather than "MadDrivers" would speed up the build procedure. + .product(name: "ST7789", package: "MadDrivers"), + .product(name: "LIS3DH", package: "MadDrivers"), + "MadGraphics" + ]), + ] +) diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf differ diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Ball.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Ball.swift new file mode 100644 index 0000000..8f31d63 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Ball.swift @@ -0,0 +1,20 @@ +import MadGraphics + +struct Ball { + var x1: Int + var y1: Int + let size: Int + + var x2: Int { + x1 + size + } + var y2: Int { + y1 + size + } + + init(at point: Point, size: Int) { + x1 = point.x + y1 = point.y + self.size = size + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Game.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Game.swift new file mode 100644 index 0000000..8a561e5 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Game.swift @@ -0,0 +1,241 @@ +import MadGraphics +import ST7789 +import SwiftIO + +// Place a ball at the upper left corner of the maze and move it based on acceleration. +// If the ball reaches the destination (bottom right), the game ends. +// Press the D1 button to restart the game. +class Game { + var maze: Maze + var ball: Ball + let ballColor = Color(UInt32(0xEFE891)) + + let width = 20 + var speed = 2 + + let screen: ST7789 + let layer: Layer + var frameBuffer: [UInt32] + var screenBuffer: [UInt16] + + init(screen: ST7789, layer: Layer) { + ball = Ball(at: Point(x: 1, y: 1), size: 7) + maze = Maze(width: width, layer: layer) + frameBuffer = [UInt32](repeating: 0, count: layer.bounds.width * layer.bounds.height) + screenBuffer = [UInt16](repeating: 0, count: layer.bounds.width * layer.bounds.height) + self.screen = screen + self.layer = layer + + maze.generate(layer) + + layer.draw() { canvas in + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, data: ballColor.rawValue) + } + + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + } + + // Create a new maze and place the ball at the starting point. + func reset() { + maze.reset(layer) + maze.generate(layer) + + layer.draw() { canvas in + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, data: maze.bgColor.rawValue) + } + ball = Ball(at: Point(x: 1, y: 1), size: 7) + + layer.draw() { canvas in + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, data: ballColor.rawValue) + } + + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + } + + // Update the display to show that the game has finished. + func finishGame() { + layer.draw() { canvas in + canvas.fillRectangle(at: Point(0, 0), width: canvas.width, height: canvas.height, data: Color.red.rawValue) + } + + let font = Font(path: "/lfs/Resources/Fonts/Roboto-Regular.ttf", pointSize: 10, dpi: 220) + + let text1 = TextLayer(at: Point(x: layer.bounds.size.halfWidth, y: 60), anchorPoint: UnitPoint.center, string: "Good Job!", font: font, foregroundColor: Color.white) + + font.setSize(pointSize: 6) + let text2 = TextLayer(at: Point(x: layer.bounds.size.halfWidth, y: 140), anchorPoint: UnitPoint.center, string: "Press D1 to continue", font: font, foregroundColor: Color.white) + + layer.append(text1) + layer.append(text2) + + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + + layer.removeAll() + } + + // Verify if the ball has reached the bottom right corner of the maze. + func finished() -> Bool { + return ball.x1 / width == maze.column - 1 && ball.y1 / width == maze.row - 1 + } + + // Update the ball's position based on the acceleration. + func update(_ acceleration: (x: Float, y: Float, z: Float)) { + guard !finished() else { + finishGame() + return + } + + let lastBallPos = Point(ball.x1, ball.y1) + + // Move to the left. + if acceleration.x > 0.25 { + ball.x1 -= speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + // Check if the ball collides with any walls. + // If it does, reposition it close to the wall. + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top || result.bottom || result.right { + ball.x1 = (x + 1) * width + 1 + } + + if result.left { + ball.x1 = x * width + 1 + } + } + } + } + + // Move to the right + if acceleration.x < -0.25 { + ball.x1 += speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top || result.bottom || result.left { + ball.x1 = x * width - ball.size - 1 + } + + if result.right { + ball.x1 = (x + 1) * width - ball.size - 1 + } + } + } + } + + // Move downwards. + if acceleration.y > 0.25 { + ball.y1 += speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.bottom { + ball.y1 = (y + 1) * width - 1 - ball.size + } + + if result.top || result.right || result.left { + ball.y1 = y * width - 1 - ball.size + } + } + } + } + + // Move upwards. + if acceleration.y < -0.25 { + ball.y1 -= speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top { + ball.y1 = y * width + 1 + } + + if result.bottom || result.right || result.left { + ball.y1 = (y + 1) * width + 1 + } + } + } + } + + // If the ball's position has changed, update the display. + if lastBallPos.x != ball.x1 || lastBallPos.y != ball.y1 { + layer.draw() { canvas in + canvas.fillRectangle(at: lastBallPos, width: ball.size, height: ball.size, data: maze.bgColor.rawValue) + } + layer.draw() { canvas in + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, data: ballColor.rawValue) + } + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + } + } + + // Check if the ball collides with a wall. + func checkCollision(ballPos: Point, wallP1: Point, wallP2: Point) -> Bool { + return ball.x1 <= wallP2.x && ball.x2 >= wallP1.x && ball.y1 <= wallP2.y && ball.y2 >= wallP1.y + } + + // Check if the ball collides with any wall of a cell in the maze grid. + func checkGridWalls(ballPos: Point, gridPos: Point) -> Wall { + let walls = maze.grids[maze.getIndex(gridPos)].walls + var result = Wall(top: false, right: false, bottom: false, left: false) + + if walls.top && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, gridPos.y * width), wallP2: Point((gridPos.x + 1) * width, gridPos.y * width)) { + result.top = true + } + + if walls.right && + checkCollision(ballPos: ballPos, wallP1: Point((gridPos.x + 1) * width, gridPos.y * width), wallP2: Point((gridPos.x + 1) * width, (gridPos.y + 1) * width)) { + result.right = true + + } + + if walls.bottom && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, (gridPos.y + 1) * width), wallP2: Point((gridPos.x + 1) * width, (gridPos.y + 1) * width)) { + result.bottom = true + } + + if walls.left && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, gridPos.y * width), wallP2: Point(gridPos.x * width, (gridPos.y + 1) * width)) { + result.left = true + } + + return result + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Grid.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Grid.swift new file mode 100644 index 0000000..9ccabe5 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Grid.swift @@ -0,0 +1,13 @@ +struct Wall { + var top: Bool + var right: Bool + var bottom: Bool + var left: Bool +} + +struct Grid { + let x: Int + let y: Int + var walls = Wall(top: true, right: true, bottom: true, left: true) + var visited = false +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Maze.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Maze.swift new file mode 100644 index 0000000..815134b --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/Maze.swift @@ -0,0 +1,186 @@ +import MadGraphics +import ST7789 +import SwiftIO + +// Generate a random maze. +struct Maze { + let width: Int + let column: Int + let row: Int + + let bgColor = Color(UInt32(0x3F52E3)) + let wallColor = Color.white + let canvasSize: Size + + var current = Point(0, 0) + var grids: [Grid] = [] + var stack: [Point] = [] + + init(width: Int, layer: Layer) { + self.canvasSize = layer.bounds.size + self.width = width + column = canvasSize.width / width + row = canvasSize.height / width + reset(layer) + } + + // Reset all walls to their default state. + mutating func reset(_ layer: Layer) { + grids = [] + stack = [] + current = Point(0, 0) + + // Initially, all walls are present. + for y in 0.. Bool { + return grids.filter { !$0.visited }.count == 0 + } + + // Calculate the index of a cell in the array. + func getIndex(_ point: Point) -> Int { + return point.x + point.y * column + } + + // Remove the wall between two cells. + mutating func removeWall(_ current: Point, _ next: Point) { + let x = current.x - next.x + + if x == 1 { + grids[getIndex(current)].walls.left = false + grids[getIndex(next)].walls.right = false + } else if x == -1 { + grids[getIndex(current)].walls.right = false + grids[getIndex(next)].walls.left = false + } + + let y = current.y - next.y + + if y == 1 { + grids[getIndex(current)].walls.top = false + grids[getIndex(next)].walls.bottom = false + } else if y == -1 { + grids[getIndex(current)].walls.bottom = false + grids[getIndex(next)].walls.top = false + } + } + + // Find nearby cells that haven't been visited yet, and select one randomly. + func getNext() -> Point? { + var neighbors: [Point] = [] + + if current.y > 0 { + let top = Point(current.x, current.y - 1) + + if !grids[getIndex(top)].visited { + neighbors.append(top) + } + } + if current.x < column - 1 { + let right = Point(current.x + 1, current.y) + + if !grids[getIndex(right)].visited { + neighbors.append(right) + } + } + + if current.y < row - 1 { + let bottom = Point(current.x, current.y + 1) + if !grids[getIndex(bottom)].visited { + neighbors.append(bottom) + } + } + + if current.x > 0 { + let left = Point(current.x - 1, current.y) + if !grids[getIndex(left)].visited { + neighbors.append(left) + } + } + + return neighbors.randomElement() + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/MazeGame.swift b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/MazeGame.swift new file mode 100644 index 0000000..2b2300d --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/MazeGame/Sources/MazeGame/MazeGame.swift @@ -0,0 +1,53 @@ +import SwiftIO +import MadBoard +import ST7789 +import MadGraphics +import LIS3DH + +@main +public struct MazeGame { + public static func main() { + // Initialize the SPI pin and the digital pins for the LCD. + let bl = DigitalOut(Id.D2) + let rst = DigitalOut(Id.D12) + let dc = DigitalOut(Id.D13) + let cs = DigitalOut(Id.D5) + let spi = SPI(Id.SPI0, speed: 30_000_000) + + // Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left. + let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90) + + let i2c = I2C(Id.I2C0) + let accelerometer = LIS3DH(i2c) + + let layer = Layer(at: Point.zero, anchorPoint: UnitPoint.zero, width: screen.width, height: screen.height) + let mazeGame = Game(screen: screen, layer: layer) + + let resetButton = DigitalIn(Id.D1) + + var reset = false + resetButton.setInterrupt(.falling) { + reset = true + } + + var sleepTime: Float = 0 + let maxTime: Float = 20 + let minTime: Float = 5 + + while true { + // If the reset button is pressed, restart the game. + if reset { + mazeGame.reset() + reset = false + } + + // Update ball's position based on the acceleration. + let acceleration = accelerometer.readXYZ() + mazeGame.update(acceleration) + + // Map the acceleration into a sleep time in order to control the speed of the ball. + sleepTime = min(max(abs(acceleration.x), abs(acceleration.y)), 1) * (minTime - maxTime) + maxTime + sleep(ms: Int(sleepTime)) + } + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/.gitignore b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.mmp b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.mmp new file mode 100644 index 0000000..102d0e2 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.mmp @@ -0,0 +1,17 @@ +# This is a MadMachine project file in TOML format +# This file holds those parameters that could not be managed by SwiftPM +# Edit this file would change the behavior of the building/downloading procedure +# Those project files in the dependent libraries would be IGNORED + +# Specify the board name below +# There are "SwiftIOBoard" and "SwiftIOMicro" now +board = "SwiftIOMicro" + +# Specifiy the target triple below +# There are "thumbv7em-unknown-none-eabi" and "thumbv7em-unknown-none-eabihf" now +# If your code use significant floating-point calculation, +# plz set it to "thumbv7em-unknown-none-eabihf" +triple = "thumbv7em-unknown-none-eabi" + +# Reserved for future use +version = 1 diff --git a/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.swift b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.swift new file mode 100644 index 0000000..6b268db --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SandSimulation", + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/madmachineio/SwiftIO.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadBoards.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadDrivers.git", branch: "develop"), + .package(path: "/Users/andy/Documents/MadGraphics"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "SandSimulation", + dependencies: [ + "SwiftIO", + "MadBoards", + // Use specific library name rather than "MadDrivers" would speed up the build procedure. + .product(name: "ST7789", package: "MadDrivers"), + .product(name: "LIS3DH", package: "MadDrivers"), + "MadGraphics" + ]), + ] +) diff --git a/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/Sand.swift b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/Sand.swift new file mode 100644 index 0000000..cc97551 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/Sand.swift @@ -0,0 +1,180 @@ +import MadGraphics + +struct Particle { + var x: Int + var y: Int + var xSpeed: Float + var ySpeed: Float + let color: Color +} + +struct Sand { + let count = 1000 + let size = 3 + var particles: [Particle] = [] + + let colors: [Color] = [.red, .orange, .yellow, .lime, .cyan, .blue, .purple, .magenta] + var grid: [Bool] + + let column: Int + let row: Int + + init(layer: Layer, _ acceleration: (x: Float, y: Float, z: Float)) { + column = layer.bounds.width / size + row = layer.bounds.height / size + grid = [Bool](repeating: false, count: column * row) + + // Draw the particles on the top of the layer. + var index = 0 + + while index < count { + let x = index % column + let y = index / column + + particles.append(Particle(x: x, y: y, xSpeed: 0, ySpeed: 0, color: colors.randomElement()!)) + layer.draw() { canvas in + canvas.fillRectangle(at: Point(x * size, y * size), width: size, height: size, data: particles[y * column + x].color.rawValue) + } + grid[y * column + x] = true + + index += 1 + } + } + + mutating func update(layer: Layer, _ acceleration: (x: Float, y: Float, z: Float)) { + updateSpeed(acceleration) + + for i in particles.indices { + var xSpeed = particles[i].xSpeed + var ySpeed = particles[i].ySpeed + + let lastX = particles[i].x + let lastY = particles[i].y + + var newX = particles[i].x + Int(xSpeed) + var newY = particles[i].y + Int(ySpeed) + + // If the particle collides with a wall, bounce it. + if newX > column - 1 { + newX = column - 1 + xSpeed /= -2 + } else if newX < 0 { + newX = 0 + xSpeed /= -2 + } + + if newY > row - 1 { + newY = row - 1 + ySpeed /= -2 + } else if newY < 0 { + newY = 0 + ySpeed /= -2 + } + + let lastIndex = lastY * column + lastX + let newIndex = newY * column + newX + + // The particle moves and collide with other particle. + if lastIndex != newIndex && grid[newIndex] { + if abs(lastIndex - newIndex) == 1 { + // The particle moves horizontally. + xSpeed /= -2 + newX = lastX + } else if abs(lastIndex - newIndex) == column { + // The particle moves vertically. + ySpeed /= -2 + newY = lastY + } else { + // If the particle moves diagonally, find an available position adjacent to the particle to place it. + if abs(xSpeed) >= abs(ySpeed) { + // The speed on the x-axis is greater than the speed on the y-axis. + if !grid[lastY * column + newX] { + // Suppress movement along the y-axis. + newY = lastY + ySpeed /= -2 + } else if !grid[newY * column + lastX] { + // Suppress movement along the x-axis. + newX = lastX + xSpeed /= -2 + } else { + // Remain still. + newX = lastX + newY = lastY + xSpeed /= -2 + ySpeed /= -2 + } + } else { + // The speed on the x-axis is less than the speed on the y-axis. + if !grid[newY * column + lastX] { + // Suppress movement along the x-axis. + newX = lastX + xSpeed /= -2 + } else if !grid[lastY * column + newX] { + // Suppress movement along the y-axis. + newY = lastY + ySpeed /= -2 + } else { + // Remain still. + newX = lastX + newY = lastY + xSpeed /= -2 + ySpeed /= -2 + } + } + } + } + + particles[i].x = newX + particles[i].y = newY + particles[i].xSpeed = xSpeed + particles[i].ySpeed = ySpeed + grid[lastY * column + lastX] = false + grid[newY * column + newX] = true + layer.draw() { canvas in + canvas.fillRectangle(at: Point(lastX * size, lastY * size), width: size, height: size, data: Color.black.rawValue) + } + layer.draw() { canvas in + canvas.fillRectangle(at: Point(newX * size, newY * size), width: size, height: size, data: particles[i].color.rawValue) + } + } + } + + mutating func updateSpeed(_ acceleration: (x: Float, y: Float, z: Float)) { + var xAccel = -acceleration.x + var yAccel = acceleration.y + var zAccel = min(abs(acceleration.z), 1) + + if abs(xAccel) <= 0.1 && abs(yAccel) <= 0.1 { + // Prevent the particles from moving when the board is lying on the table not completely level. + for i in particles.indices { + particles[i].xSpeed = 0 + particles[i].ySpeed = 0 + } + } else { + // Acceleration on z-axis simulates the effect of gravitational on the motion of the particles. + // When z-axis acceleration is close to 1, sensor is flat, and gravity barely affects xy movement. + // Lower z-axis acceleration means more gravity influence on xy motion. + zAccel = 0.5 - zAccel / 2 + xAccel -= zAccel + yAccel -= zAccel + + //A slight random motion is added to each particle according to the z-axis acceleration. + // Their speed stays below 1 to avoid overlap. + // However, the rapid iteration speed creates the illusion of smooth particle movement. + for i in particles.indices { + var xSpeed = particles[i].xSpeed + xAccel + Float.random(in: 0...zAccel) + if abs(xSpeed) > 1 { + xSpeed /= abs(xSpeed) + } + + var ySpeed = particles[i].ySpeed + yAccel + Float.random(in: 0...zAccel) + if abs(ySpeed) > 1 { + ySpeed /= abs(ySpeed) + } + + particles[i].xSpeed = xSpeed + particles[i].ySpeed = ySpeed + } + } + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/SandSimulation.swift b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/SandSimulation.swift new file mode 100644 index 0000000..42c2e61 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/SandSimulation/Sources/SandSimulation/SandSimulation.swift @@ -0,0 +1,43 @@ +import SwiftIO +import MadBoard +import ST7789 +import MadGraphics +import LIS3DH + +@main +public struct SandSimulation { + public static func main() { + // Initialize the SPI pin and the digital pins for the LCD. + let bl = DigitalOut(Id.D2) + let rst = DigitalOut(Id.D12) + let dc = DigitalOut(Id.D13) + let cs = DigitalOut(Id.D5) + let spi = SPI(Id.SPI0, speed: 30_000_000) + + // Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left. + let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90) + var screenBuffer = [UInt16](repeating: 0, count: screen.width * screen.width) + var frameBuffer = [UInt32](repeating: 0, count: screen.width * screen.width) + + let layer = Layer(at: Point.zero, anchorPoint: UnitPoint.zero, width: screen.width, height: screen.height) + + // Initialize the accelerometer. + let i2c = I2C(Id.I2C0) + let accelerometer = LIS3DH(i2c) + + // Draw the sand particle. + var sand = Sand(layer: layer, accelerometer.readXYZ()) + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + + // Update sand particle positions based on movement. + while true { + sand.update(layer: layer, accelerometer.readXYZ()) + layer.render(into: &frameBuffer, output: &screenBuffer, transform: Color.getRGB565LE) { dirty, data in + screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: data) + } + sleep(ms: 1) + } + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/13MoreProjects/WordClock/.gitignore b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.mmp b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.mmp new file mode 100644 index 0000000..102d0e2 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.mmp @@ -0,0 +1,17 @@ +# This is a MadMachine project file in TOML format +# This file holds those parameters that could not be managed by SwiftPM +# Edit this file would change the behavior of the building/downloading procedure +# Those project files in the dependent libraries would be IGNORED + +# Specify the board name below +# There are "SwiftIOBoard" and "SwiftIOMicro" now +board = "SwiftIOMicro" + +# Specifiy the target triple below +# There are "thumbv7em-unknown-none-eabi" and "thumbv7em-unknown-none-eabihf" now +# If your code use significant floating-point calculation, +# plz set it to "thumbv7em-unknown-none-eabihf" +triple = "thumbv7em-unknown-none-eabi" + +# Reserved for future use +version = 1 diff --git a/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.swift b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.swift new file mode 100644 index 0000000..110e613 --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "WordClock", + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/madmachineio/SwiftIO.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadBoards.git", branch: "develop"), + .package(url: "https://github.com/madmachineio/MadDrivers.git", branch: "develop"), + .package(path: "/Users/andy/Documents/MadGraphics") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "WordClock", + dependencies: [ + "SwiftIO", + "MadBoards", + // Use specific library name rather than "MadDrivers" would speed up the build procedure. + .product(name: "ST7789", package: "MadDrivers"), + .product(name: "PCF8563", package: "MadDrivers"), + "MadGraphics" + ]), + ] +) diff --git a/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Resources/Fonts/Graduate-Regular.ttf b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Resources/Fonts/Graduate-Regular.ttf new file mode 100644 index 0000000..29061d9 Binary files /dev/null and b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Resources/Fonts/Graduate-Regular.ttf differ diff --git a/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Sources/WordClock/WordClock.swift b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Sources/WordClock/WordClock.swift new file mode 100644 index 0000000..574e78a --- /dev/null +++ b/Examples/SwiftIOPlayground/13MoreProjects/WordClock/Sources/WordClock/WordClock.swift @@ -0,0 +1,107 @@ +import SwiftIO +import MadBoard +import ST7789 +import MadGraphics +import PCF8563 + +@main +public struct WordClock { + public static func main() { + // Initialize the SPI pin and the digital pins for the LCD. + let bl = DigitalOut(Id.D2) + let rst = DigitalOut(Id.D12) + let dc = DigitalOut(Id.D13) + let cs = DigitalOut(Id.D5) + let spi = SPI(Id.SPI0, speed: 30_000_000) + + // Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left. + let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90) + var screenBuffer = [UInt16](repeating: 0, count: screen.width * screen.width) + var frameBuffer = [UInt32](repeating: 0, count: screen.width * screen.width) + + // Initialize the rtc. + let i2c = I2C(Id.I2C0) + let rtc = PCF8563(i2c) + + // Update the RTC with your current time. + let currentTime = PCF8563.Time( + year: 2024, month: 12, day: 5, hour: 18, + minute: 29, second: 0, dayOfWeek: 4 + ) + + sleep(ms: 500) + rtc.setTime(currentTime) + + let layer = Layer(at: Point.zero, anchorPoint: UnitPoint.zero, width: screen.width, height: screen.height) + + // Calculate the point size for each character. + // Get masks from the font file for all characters of the word clock. + let dpi = 220 + let pointSize = min(screen.width / Words.column, screen.height / Words.row) * 72 / dpi * 4 / 5 + let characterMasks = getCharacterMasks(path: "/lfs/Resources/Fonts/Graduate-Regular.ttf", pointSize: pointSize, dpi: dpi) + + let clock = WordView(layer: layer, characterMasks: characterMasks) + + // Define the colors to be used for displaying the words. + let colors: [Color] = [.red, .orange, .yellow, .lime, .blue, .magenta, .cyan, Color(UInt32(0xFE679A))] + + // Highlight each column with different colors in turn. + var index = 0 + for x in 0.. [[Mask]] { + var characterMasks = [[Mask]](repeating: [Mask](), count: Words.row) + + let font = Font(path: path, pointSize: pointSize, dpi: dpi) + for y in 0.. (minute: [Minute], prep: Preposition?, hour: Hour) { + static func getWords(hourNumber: Int, minuteNumber: Int) -> Words { + var minuteWord: [MinuteWord] = [] + var hourWord = HourWord(rawValue: hourNumber < 12 ? hourNumber : hourNumber % 12)! + var prepWord: PrepositionWord? = nil + + switch minuteNumber { + case 5..<10: + minuteWord.append(.five) + prepWord = .past + case 10..<15: + minuteWord.append(.ten) + prepWord = .past + case 15..<20: + minuteWord.append(.quarter) + prepWord = .past + case 20..<25: + minuteWord.append(.twenty) + prepWord = .past + case 25..<30: + minuteWord.append(.twenty) + minuteWord.append(.five) + prepWord = .past + case 30..<35: + minuteWord.append(.half) + prepWord = .past + case 35..<40: + //minuteWord.append(contentsOf: [.twenty, .five]) + minuteWord.append(.twenty) + minuteWord.append(.five) + prepWord = .to + hourWord = HourWord(rawValue: hourNumber + 1 < 12 ? (hourNumber + 1) : (hourNumber + 1) % 12)! + case 40..<45: + minuteWord.append(.twenty) + prepWord = .to + hourWord = HourWord(rawValue: hourNumber + 1 < 12 ? (hourNumber + 1) : (hourNumber + 1) % 12)! + case 45..<50: + minuteWord.append(.quarter) + prepWord = .to + hourWord = HourWord(rawValue: hourNumber + 1 < 12 ? (hourNumber + 1) : (hourNumber + 1) % 12)! + case 50..<55: + minuteWord.append(.ten) + prepWord = .to + hourWord = HourWord(rawValue: hourNumber + 1 < 12 ? (hourNumber + 1) : (hourNumber + 1) % 12)! + case 55..<60: + minuteWord.append(.five) + prepWord = .to + hourWord = HourWord(rawValue: hourNumber + 1 < 12 ? (hourNumber + 1) : (hourNumber + 1) % 12)! + default: break + } + + return Words(hourWord: hourWord, prepWord: prepWord, minuteWord: minuteWord) + } +} \ No newline at end of file