HuggingChat-Mac/Views/Confetti/ConfettiScene.swift (101 lines of code) (raw):

// // ConfettiScene.swift // HuggingChat-Mac // // Created by Cyril Zakka on 8/26/24. // import Foundation import SpriteKit class ConfettiScene: SKScene { /// Confetti emission duration in seconds. /// Duration for all confetto to fall isn't controllable. It's depends on confetto falling speed that are random. var emissionDuration: Double! // emission rate per seconds private let emissionRate = 120.0 // max angle from straight down (270 degree) in radians private let maxDirectionAngle = Double.pi / 4 private let colors = [SKColor(.red), SKColor(.purple), SKColor(.blue), SKColor(.yellow), SKColor(.green)] // debug mode private let debug = false // label for debug private var nodeCountLabel: SKLabelNode! // timer for emission private var emissionTimer: Timer? convenience init(size: CGSize, emissionDuration: Double) { self.init(size: size) self.emissionDuration = emissionDuration } // generate random confetti size private func randomSize() -> CGSize { let longSide = [18.0, 22.0, 25.0].randomElement()! let aspectRatio = [0.5, 0.4, 0.3].randomElement()! return CGSize(width: longSide, height: longSide * aspectRatio) } // generate random confetti direction private func randomDirection() -> Double { Double.random(in: (Double.pi * 1.5 - maxDirectionAngle) ... (Double.pi * 1.5 + maxDirectionAngle)) } // generate random confetti rotation speed private func randomRotationSpeed() -> Double { Double.random(in: 0.3...4.0) * [-1, 1].randomElement()! } // generate random confetti scale speed private func randomScaleSpeed() -> Double { Double.random(in: 0.8...1.3) } // generate random confetti color private func randomColor() -> SKColor { colors.randomElement()! } // generage random confetti initial position private func randomInitialPosition(viewSize: CGSize) -> CGPoint { let maxXMovement = viewSize.height * sin(maxDirectionAngle) let x = Double.random(in: -maxXMovement ... (viewSize.width + maxXMovement)) // FIXME: 固定値(10)ではなくノードの回転を考慮した取りうる最大高さを計算して足す let y = Double.random(in: (viewSize.height + 10) ... viewSize.height * 1.2) return CGPoint(x: x, y: y) } override func update(_ currentTime: TimeInterval) { // remove fallen confetto for node in children { if node.position.y < -50 { node.removeAllActions() node.removeFromParent() } } // update label for debug if debug { nodeCountLabel.text = "confetti count: \(children.count - 1)" } } override func didMove(to view: SKView) { // make background transparent backgroundColor = .clear view.allowsTransparency = true view.scene?.backgroundColor = .clear // set confetti emission timer emissionTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / emissionRate, repeats: true) { timer in // create random confetti and add it to scene let confettiNode = self.createConfettiNode( color: self.randomColor(), size: self.randomSize(), direction: self.randomDirection(), rotationSpeedX: self.randomRotationSpeed(), rotationSpeedY: self.randomRotationSpeed(), rotationSpeedZ: self.randomRotationSpeed(), scaleSpeed: self.randomScaleSpeed()) confettiNode.position = self.randomInitialPosition(viewSize: view.frame.size) self.addChild(confettiNode) } // finish emisison after `emissionDuration` sec Timer.scheduledTimer(withTimeInterval: emissionDuration, repeats: false) { timer in self.emissionTimer?.invalidate() } // put label for debug if debug { nodeCountLabel = SKLabelNode(text: "") nodeCountLabel.position = CGPoint(x: 50, y: 50) nodeCountLabel.fontColor = .blue addChild(nodeCountLabel) } } // create confetti node private func createConfettiNode(color: SKColor, size: CGSize, direction: Double, rotationSpeedX: Double, rotationSpeedY: Double, rotationSpeedZ: Double, scaleSpeed: Double) -> SKNode { let node = SKShapeNode(path: .init(rect: CGRect(origin: .zero, size: size), transform: nil), centered: true) node.fillColor = color node.strokeColor = .clear // wrapping node for x-rotation and y-rotation let transformNode = SKTransformNode() transformNode.addChild(node) // x-rotation action let rotationActionX = SKAction.customAction(withDuration: abs(rotationSpeedX)) { (node: SKNode, time: CGFloat) -> Void in (node as! SKTransformNode).xRotation = (time / rotationSpeedX) * 2 * CGFloat(Double.pi) } // y-rotation action let rotationActionY = SKAction.customAction(withDuration: abs(rotationSpeedY)) { (node: SKNode, time: CGFloat) -> Void in (node as! SKTransformNode).yRotation = (time / rotationSpeedY) * 2 * CGFloat(Double.pi) } // z-rotation action let rotationActionZ = SKAction.customAction(withDuration: abs(rotationSpeedZ)) { (node: SKNode, time: CGFloat) -> Void in (node as! SKTransformNode).zRotation = (time / rotationSpeedZ) * 2 * CGFloat(Double.pi) } // move action // biger(near) faster, smaller(far) slower let moveSpeed = pow(size.width, 1.2) * 8 let moveAction = SKAction.move(by: CGVector(dx: cos(direction) * moveSpeed, dy: sin(direction) * moveSpeed), duration: 1.0) // scale action let scaleAction = SKAction.scale(by: scaleSpeed, duration: 1.0) // add actions to node transformNode.run(SKAction.repeatForever(rotationActionX)) transformNode.run(SKAction.repeatForever(rotationActionY)) transformNode.run(SKAction.repeatForever(rotationActionZ)) transformNode.run(SKAction.repeatForever(moveAction)) transformNode.run(SKAction.repeatForever(scaleAction)) return transformNode } }