Skip to content

Instantly share code, notes, and snippets.

@jtvargas
Created September 9, 2024 01:24
Show Gist options
  • Save jtvargas/9d046ab3e267d2d55fbb235a7fcb7c2b to your computer and use it in GitHub Desktop.
Save jtvargas/9d046ab3e267d2d55fbb235a7fcb7c2b to your computer and use it in GitHub Desktop.
Stress Fiddle App in SwiftUI
//
// MatrixEffect.swift
//
// Created by J.T on 9/8/24.
//
import SwiftUI
import Combine
class MatrixEffectModel: ObservableObject {
@Published var dots: [Dot]
@Published var touchLocation: CGPoint = .zero
@Published var dotSize: CGFloat = 3
@Published var dotSpacing: CGFloat = 20
@Published var touchBoundingSize: CGFloat = 100
@Published var dotInertia: CGFloat = 0.40
private var cancellables: Set<AnyCancellable> = []
private let updateSubject = PassthroughSubject<Void, Never>()
init() {
dots = []
setupUpdateTimer()
}
private func setupUpdateTimer() {
Timer.publish(every: 1/120, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateSubject.send()
}
.store(in: &cancellables)
updateSubject
.collect(.byTime(DispatchQueue.main, .milliseconds(16)))
.sink { [weak self] _ in
self?.updateDots()
}
.store(in: &cancellables)
}
func initializeDots(in size: CGSize) {
let rows = Int(size.height / dotSpacing)
let columns = Int(size.width / dotSpacing)
dots = (0..<rows).flatMap { row in
(0..<columns).map { column in
let x = CGFloat(column) * dotSpacing
let y = CGFloat(row) * dotSpacing
return Dot(x: x, y: y, originX: x, originY: y)
}
}
}
func updateDots() {
let touchBoundingSizeSquared = touchBoundingSize * touchBoundingSize
dots = dots.map { dot in
var updatedDot = dot
let dx = touchLocation.x - updatedDot.x
let dy = touchLocation.y - updatedDot.y
let distanceSquared = dx*dx + dy*dy
if distanceSquared < touchBoundingSizeSquared {
let distance = sqrt(distanceSquared)
let force = (touchBoundingSize - distance) / touchBoundingSize
let angle = atan2(dy, dx)
let targetX = updatedDot.x - cos(angle) * force * 20
let targetY = updatedDot.y - sin(angle) * force * 20
updatedDot.vx += (targetX - updatedDot.x) * dotInertia
updatedDot.vy += (targetY - updatedDot.y) * dotInertia
}
updatedDot.vx *= 0.9
updatedDot.vy *= 0.9
updatedDot.x += updatedDot.vx
updatedDot.y += updatedDot.vy
let dx2 = updatedDot.originX - updatedDot.x
let dy2 = updatedDot.originY - updatedDot.y
let distance2Squared = dx2*dx2 + dy2*dy2
if distance2Squared > 1 {
updatedDot.x += dx2 * 0.03
updatedDot.y += dy2 * 0.03
}
return updatedDot
}
}
}
struct MatrixEffect: View {
@StateObject private var model = MatrixEffectModel()
var body: some View {
ZStack {
GeometryReader { geometry in
DotCanvas(dots: model.dots, dotSize: model.dotSize)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
model.touchLocation = value.location
}
.onEnded { _ in
model.touchLocation = CGPoint(x: -1000, y: -1000)
}
)
.onAppear {
model.initializeDots(in: geometry.size)
}
.onChange(of: geometry.size) { newSize in
model.initializeDots(in: newSize)
}
}
.background(Color.black)
.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
controlPanel
}
}
}
private var controlPanel: some View {
VStack(spacing: 10) {
SliderView(value: $model.dotSize, range: 1...10, title: "Dot Size")
SliderView(value: $model.touchBoundingSize, range: 50...200, title: "Touch Bounding")
SliderView(value: $model.dotInertia, range: 0.01...0.5, title: "Dot Inertia")
}
.padding()
.background(Color.black.opacity(0.5))
.cornerRadius(10)
.padding()
}
}
struct DotCanvas: View {
let dots: [Dot]
let dotSize: CGFloat
var body: some View {
Canvas { context, size in
for dot in dots {
context.fill(
Path(ellipseIn: CGRect(
x: dot.x - dotSize/2,
y: dot.y - dotSize/2,
width: dotSize,
height: dotSize
)),
with: .color(.white)
)
}
}
}
}
struct Dot: Equatable {
var x: CGFloat
var y: CGFloat
let originX: CGFloat
let originY: CGFloat
var vx: CGFloat = 0
var vy: CGFloat = 0
}
struct SliderView: View {
@Binding var value: CGFloat
let range: ClosedRange<CGFloat>
let title: String
var body: some View {
VStack(alignment: .leading) {
Text("\(title): \(value, specifier: "%.2f")")
.foregroundColor(.white)
Slider(value: $value, in: range)
}
}
}
#Preview {
MatrixEffect()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment