Skip to content

Commit

Permalink
Add sand simulation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ines333 committed Apr 22, 2024
1 parent 04f4d2d commit b53c157
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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: "main"),
.package(url: "https://github.com/madmachineio/MadBoards.git", branch: "main"),
.package(url: "https://github.com/madmachineio/MadDrivers.git", branch: "main"),
.package(url: "https://github.com/madmachineio/MadGraphics.git", branch: "main"),
],
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"
]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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
let canvas: Canvas
var particles: [Particle] = []

let colors: [Color] = [.red, .orange, .yellow, .lime, .cyan, .blue, .purple, .magenta]
var grid: [Bool]

let column: Int
let row: Int

init(canvas: Canvas, _ acceleration: (x: Float, y: Float, z: Float)) {
self.canvas = canvas
column = canvas.width / size
row = canvas.height / size
grid = [Bool](repeating: false, count: column * row)

// Draw the particles on the top of the canvas.
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()!))
canvas.fillRectangle(at: Point(x * size, y * size), width: size, height: size, color: particles[y * column + x].color)
grid[y * column + x] = true

index += 1
}
}

mutating func update(_ 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
canvas.fillRectangle(at: Point(lastX * size, lastY * size), width: size, height: size, color: Color.black)
canvas.fillRectangle(at: Point(newX * size, newY * size), width: size, height: size, color: particles[i].color)
}
}

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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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)

let canvas = Canvas(width: screen.width, height: screen.height)
var frameBuffer = [UInt16](repeating: 0, count: canvas.width * canvas.height)

// Initialize the accelerometer.
let i2c = I2C(Id.I2C0)
let accelerometer = LIS3DH(i2c)

// Draw the sand particle.
var sand = Sand(canvas: canvas, accelerometer.readXYZ())
updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen)

// Update sand particle positions based on movement.
while true {
sand.update(accelerometer.readXYZ())
updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen)
sleep(ms: 1)
}
}
}

// Get the region that needs to be updated and send data to the screen.
func updateDisplay(canvas: Canvas, frameBuffer: inout [UInt16], screen: ST7789) {
guard let dirty = canvas.getDirtyRect() else {
return
}

var index = 0
let stride = canvas.width
let canvasBuffer = canvas.buffer
for y in dirty.y0..<dirty.y1 {
for x in dirty.x0..<dirty.x1 {
frameBuffer[index] = Color.getRGB565LE(canvasBuffer[y * stride + x])
index += 1
}
}

frameBuffer.withUnsafeBytes { ptr in
screen.writeBitmap(x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height, data: ptr)
}

canvas.finishRefresh()
}

0 comments on commit b53c157

Please sign in to comment.