-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1b75d93
Showing
8 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
8 changes: 8 additions & 0 deletions
8
.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>IDEDidComputeMac32BitWarning</key> | ||
<true/> | ||
</dict> | ||
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// swift-tools-version: 5.9 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Confetti", | ||
platforms: [ | ||
.iOS(.v15) | ||
], | ||
products: [ | ||
.library( | ||
name: "Confetti", | ||
targets: ["Confetti"]), | ||
], | ||
targets: [ | ||
.target( | ||
name: "Confetti"), | ||
.testTarget( | ||
name: "ConfettiTests", | ||
dependencies: ["Confetti"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
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 | ||
// color: new year confetti https://www.schemecolor.com/new-year-confetti.php | ||
private let colors = [SKColor(hex: "dc4353")!, SKColor(hex: "e8d4b4")!, SKColor(hex: "de45ba")!, SKColor(hex: "3a378b")!, SKColor(hex: "66d3e1")!] | ||
// 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.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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import SwiftUI | ||
import SpriteKit | ||
|
||
public struct ConfettiView: View { | ||
// Confetti emission duration in seconds. | ||
private var emissionDuration: Double | ||
|
||
/// Creates a new confetti view. | ||
/// - Parameters: | ||
/// - emissionDuration: Confetti emission duration in seconds. Duration for all confetto to fall isn't controllable. It's depends on confetto falling speed that are random. | ||
public init(emissionDuration: Double = 2.0) { | ||
self.emissionDuration = emissionDuration | ||
} | ||
|
||
public var body: some View { | ||
GeometryReader { | ||
SpriteView(scene: ConfettiScene(size: $0.size, emissionDuration: emissionDuration), options: [.allowsTransparency]) | ||
.background(.clear) | ||
.ignoresSafeArea() | ||
.allowsHitTesting(false) | ||
} | ||
} | ||
} | ||
|
||
struct ContentView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
ConfettiView(emissionDuration: 120.0) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import UIKit | ||
|
||
extension UIColor { | ||
/// create new object with hex string | ||
convenience init?(hex: String, alpha: CGFloat = 1.0) { | ||
// delete "#" prefix | ||
let hexNorm = hex.hasPrefix("#") ? String(hex.dropFirst(1)) : hex | ||
|
||
// scan each byte of RGB respectively | ||
let scanner = Scanner(string: hexNorm) | ||
var color: UInt64 = 0 | ||
if scanner.scanHexInt64(&color) { | ||
let red = CGFloat((color & 0xFF0000) >> 16) / 255.0 | ||
let green = CGFloat((color & 0x00FF00) >> 8) / 255.0 | ||
let blue = CGFloat(color & 0x0000FF) / 255.0 | ||
self.init(red: red, green: green, blue: blue, alpha: alpha) | ||
} else { | ||
// invalid format | ||
return nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// import XCTest | ||
// @testable import Confetti | ||
// | ||
// final class ConfettiTests: XCTestCase { | ||
// func testExample() throws { | ||
// } | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import XCTest | ||
@testable import Confetti | ||
|
||
final class UIColorHexCodeTests: XCTestCase { | ||
var red, green, blue, alpha: CGFloat! | ||
|
||
override func setUp() { | ||
red = 0 | ||
green = 0 | ||
blue = 0 | ||
alpha = 0 | ||
} | ||
|
||
func testWhite() throws { | ||
let color = UIColor(hex: "FFFFFF")! | ||
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) | ||
XCTAssertEqual(red, 1.0) | ||
XCTAssertEqual(green, 1.0) | ||
XCTAssertEqual(blue, 1.0) | ||
XCTAssertEqual(alpha, 1.0) | ||
} | ||
|
||
func testBlack() throws { | ||
let color = UIColor(hex: "000000")! | ||
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) | ||
XCTAssertEqual(red, 0.0) | ||
XCTAssertEqual(green, 0.0) | ||
XCTAssertEqual(blue, 0.0) | ||
XCTAssertEqual(alpha, 1.0) | ||
} | ||
|
||
func testSkyBlue() throws { | ||
let color = UIColor(hex: "#3366cc", alpha: 0.9)! | ||
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) | ||
XCTAssertEqual(red, 0.2) | ||
XCTAssertEqual(green, 0.4) | ||
XCTAssertEqual(blue, 0.8) | ||
XCTAssertEqual(alpha, 0.9) | ||
} | ||
|
||
func testInvalidFormat() throws { | ||
XCTAssertNil(UIColor(hex: "white")) | ||
} | ||
} |