Skip to content

Commit

Permalink
CATTY-502 Fix incorrect placement of Say/Think bubbles(#1720)
Browse files Browse the repository at this point in the history
  • Loading branch information
max-rosenblattl authored Apr 1, 2022
1 parent e231ff1 commit d565258
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 106 deletions.
92 changes: 73 additions & 19 deletions src/Catty/Helpers/BrickHelpers/BubbleBrickHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ let kSentenceLength = 10
let fullTextLength = fullTextNode.frame.size.width
var sentenceSubStringLength = Int(fullTextLength)

if fullTextLength > CGFloat(kMaxBubbleWidth) {
while sentencePosition + kSentenceLength < text.count {
if fullTextLength > CGFloat(kMaxBubbleWidth) {
while sentencePosition + kSentenceLength < text.count {

let sentencePosIndex = text.index(text.startIndex, offsetBy: sentencePosition)
let sentenceLengthIndex = text.index(text.startIndex, offsetBy: sentencePosition + kSentenceLength)
Expand All @@ -56,24 +56,24 @@ let kSentenceLength = 10
let charRange = lowerCharIndex..<UpperCharIndex
let nextCharInLine = String(text[charRange])

if nextCharInLine == " " {
if nextCharInLine == " " {
sentenceSubString += "-"
}
}

addSentence(toLabel: labelNode, withSentence: sentenceSubString, andPosition: (CGFloat)(verticalPosition))
sentencePosition += kSentenceLength
addSentence(toLabel: labelNode, withSentence: sentenceSubString, andPosition: (CGFloat)(verticalPosition))
sentencePosition += kSentenceLength
verticalPosition += SpriteKitDefines.defaultLabelFontSize + 5
bubbleHeight += SpriteKitDefines.defaultLabelFontSize + 5
sentenceSubStringLength = Int(SKLabelNode(text: sentenceSubString).frame.size.width)
}
sentenceSubStringLength = Int(SKLabelNode(text: sentenceSubString).frame.size.width)
}

let lowerSubStringIndex = text.index(text.startIndex, offsetBy: sentencePosition)
let UpperSubStringIndex = text.index(text.startIndex, offsetBy: text.count)
let subStringRange = lowerSubStringIndex..<UpperSubStringIndex
let sentenceSubString = String(text[subStringRange])

addSentence(toLabel: labelNode, withSentence: sentenceSubString, andPosition: (CGFloat)(verticalPosition))
} else {
} else {
addSentence(toLabel: labelNode, withSentence: text, andPosition: (CGFloat)(verticalPosition))
}

Expand All @@ -93,24 +93,78 @@ let kSentenceLength = 10
bubble.addChild(labelNode)
sprite.addChild(bubble)
}
private static func createBubble(with sprite: CBSpriteNode, width: CGFloat, height: CGFloat, type: CBBubbleType)
-> SKShapeNode {

private static func getTopLeftAndRightMostPixel(_ image: UIImage?) -> (CGPoint, CGPoint) {
guard let image = image, let cgImage = image.cgImage, let dataProvider = cgImage.dataProvider else {
return (.zero, .zero)
}

let pixelData = dataProvider.data
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
let imageWidth = Int(image.size.width)
let imageHeight = Int(image.size.height)
let alphaIndexOffset = 3
let numberOfValuesPerPixelIndex = 4

for y in 0..<imageHeight {
for x in 0..<imageWidth {
let pixelIndex = ((imageWidth * y) + x) * numberOfValuesPerPixelIndex
let alpha = data[pixelIndex + alphaIndexOffset]

if alpha != 0 {
let topLeftMostPixel = CGPoint(x: x, y: y)
var topRightMostPixel = topLeftMostPixel

for reverseX in 0..<imageWidth {
let pixelIndex = ((imageWidth * y) + imageWidth - reverseX) * numberOfValuesPerPixelIndex
let alpha = data[pixelIndex + alphaIndexOffset]
if alpha != 0 {
topRightMostPixel = CGPoint(x: imageWidth - reverseX, y: y)
break
}
}

return (topLeftMostPixel, topRightMostPixel)
}
}
}

return (.zero, .zero)
}

private static func calculateBubbleOffset(pixel: CGPoint, sprite: CBSpriteNode) -> CGPoint {
let offsetX = abs(sprite.size.width / 2 - pixel.x)
let offsetY = abs(sprite.size.height / (2 * sprite.yScale) - pixel.y)

return CGPoint(x: pixel.x > sprite.size.width / 2 ? offsetX : -offsetX, y: offsetY)
}

private static func createBubble(with sprite: CBSpriteNode, width: CGFloat, height: CGFloat, type: CBBubbleType) -> SKShapeNode {

let bubbleTailHeight = CGFloat(48.0)
let bubble = SKShapeNode(path: bubblePathWith(width: width,
height: height,
bubbleTailHeight: bubbleTailHeight,
type: type))

let bubbleInitialPosition = CGPoint(x: sprite.frame.size.width / 2,
y: sprite.frame.size.height / (2 * sprite.yScale))
let topMostPixels = getTopLeftAndRightMostPixel(sprite.currentUIImageLook)
let leftMostPixel = topMostPixels.0
let rightMostPixel = topMostPixels.1

let leftPixelOffset = calculateBubbleOffset(pixel: leftMostPixel, sprite: sprite)
let rightPixelOffset = calculateBubbleOffset(pixel: rightMostPixel, sprite: sprite)

let bubbleInitialPosition = CGPoint(x: rightPixelOffset.x, y: rightPixelOffset.y)
let bubbleInvertedInitialPosition = CGPoint(x: leftPixelOffset.x, y: leftPixelOffset.y)

bubble.constraints = [SpriteBubbleConstraint(bubble: bubble,
parent: sprite,
width: width,
height: height,
position: bubbleInitialPosition,
invertedPosition: bubbleInvertedInitialPosition,
bubbleTailHeight: bubbleTailHeight)]

bubble.constraints = (NSArray(object: SpriteBubbleConstraint(bubble: bubble,
parent: sprite,
width: width,
height: height,
position: bubbleInitialPosition,
bubbleTailHeight: bubbleTailHeight)) as! [SKConstraint])
bubble.name = SpriteKitDefines.bubbleBrickNodeName
bubble.fillColor = UIColor.white
bubble.strokeColor = UIColor.black
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ final class SpriteBubbleConstraint: SKConstraint {
private let bubble: SKNode
private let bubbleWidth: CGFloat
private let bubbleHeight: CGFloat
private let bubbleInitialePosition: CGPoint
private let bubbleInitialPosition: CGPoint
private let bubbleInvertedInitialPosition: CGPoint

@objc init(bubble: SKNode, parent: SKNode, width: CGFloat, height: CGFloat, position: CGPoint, bubbleTailHeight: CGFloat) {
@objc init(bubble: SKNode, parent: SKNode, width: CGFloat, height: CGFloat, position: CGPoint, invertedPosition: CGPoint, bubbleTailHeight: CGFloat) {
self.bubble = bubble
self.parent = parent
self.bubbleWidth = width
self.bubbleHeight = height + bubbleTailHeight
self.bubbleInitialePosition = position
self.bubbleInitialPosition = position
self.bubbleInvertedInitialPosition = invertedPosition

super.init()
self.enabled = true
Expand Down Expand Up @@ -61,50 +63,29 @@ final class SpriteBubbleConstraint: SKConstraint {
return
}

let leftBubbleBorder = parent.position.x - bubbleInitialePosition.x - bubbleWidth
let rightBubbleBorder = parent.position.x + bubbleInitialePosition.x + bubbleWidth
let isBubbleInverted = bubble.xScale < 0
var leftBubbleBorder = parent.position.x
var rightBubbleBorder = parent.position.x

if isBubbleInverted {
leftBubbleBorder += bubbleInvertedInitialPosition.x - bubbleWidth
rightBubbleBorder += bubbleInvertedInitialPosition.x
bubble.position.x = bubbleInvertedInitialPosition.x
} else {
leftBubbleBorder += bubbleInitialPosition.x
rightBubbleBorder += bubbleInitialPosition.x + bubbleWidth
bubble.position.x = bubbleInitialPosition.x
}

let rightSceneEdge = scene.size.width
let leftSceneEdge = CGFloat(0)

if rightBubbleBorder > rightSceneEdge && bubble.xScale > 0 && leftBubbleBorder > leftSceneEdge {
bubble.position.x = -parent.convert(bubbleInitialePosition, from: parent).x
bubble.xScale *= -1
bubbleLabel.xScale *= -1
if (rightBubbleBorder > rightSceneEdge && !isBubbleInverted && leftBubbleBorder > leftSceneEdge) ||
(leftBubbleBorder < leftSceneEdge && isBubbleInverted && rightBubbleBorder < rightSceneEdge) {

} else if leftBubbleBorder < leftSceneEdge && bubble.xScale < 0 && rightBubbleBorder < rightSceneEdge {
bubble.position.x = parent.convert(bubbleInitialePosition, from: parent).x
bubble.xScale *= -1
bubbleLabel.xScale *= -1
}

}

private func handleYCollision() {
guard let scene = parent.scene else {
return
}

let topEdge = scene.size.height
let bottomEdge = CGFloat(0)

let topBubblePosition = CGFloat(parent.position.y + parent.yScale * bubbleInitialePosition.y + bubbleHeight)
let botBubblePosition = CGFloat(parent.position.y + parent.yScale * bubbleInitialePosition.y)

let xCollisionPosition = scene.convert(bubble.position, from: parent).x
let yTopCollisionPosition = CGFloat(topEdge - bubbleHeight)

if scene.intersects(parent) {
bubble.position.y = parent.convert(bubbleInitialePosition, from: parent).y
bubble.position.x = copysign(1.0, bubble.xScale) * parent.convert(bubbleInitialePosition, from: parent).x
}

if topBubblePosition >= topEdge {
bubble.position = scene.convert(CGPoint(x: xCollisionPosition, y: yTopCollisionPosition), to: parent)
} else if botBubblePosition <= bottomEdge {
bubble.position = scene.convert(CGPoint(x: xCollisionPosition, y: CGFloat(0)), to: parent)
}

}

private func calcRelativeSizeToParent() {
Expand All @@ -118,7 +99,6 @@ final class SpriteBubbleConstraint: SKConstraint {
public func apply() {
calcRelativeSizeToParent()
calcRelativeRotationToParent()
handleYCollision()
handleXCollision()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ final class SpriteBubbleConstraintsTests: XCTestCase {
var parent: CBSpriteNodeMock!
var child: SKNode!
var bubbleConstraint: SpriteBubbleConstraint!
var bubbleInitialPosition: CGPoint!
var bubbleInvertedInitialPosition: CGPoint!
var bubbleSize: CGSize!

override func setUp() {
let object = SpriteObject()
Expand All @@ -40,11 +43,16 @@ final class SpriteBubbleConstraintsTests: XCTestCase {
BubbleBrickHelper.addBubble(to: parent, withText: "Hello", andType: CBBubbleType.thought)
child = parent.children.first!

bubbleInitialPosition = CGPoint(x: 300, y: 400)
bubbleInvertedInitialPosition = CGPoint(x: -300, y: 400)
bubbleSize = CGSize(width: 200, height: 200)

bubbleConstraint = SpriteBubbleConstraint(bubble: child,
parent: parent,
width: 200,
height: 200,
position: CGPoint(x: 300, y: 400),
width: bubbleSize.width,
height: bubbleSize.height,
position: bubbleInitialPosition,
invertedPosition: bubbleInvertedInitialPosition,
bubbleTailHeight: 48)

}
Expand All @@ -62,58 +70,34 @@ final class SpriteBubbleConstraintsTests: XCTestCase {
XCTAssertEqual(-parent.zRotation, child.zRotation, accuracy: 0.000001)
}

func testRightXCollision() {
XCTAssertEqual(child.xScale, 1)
parent.position.x = 1000
bubbleConstraint.apply()
XCTAssertEqual(child.xScale, -1)
}
func testBubbleCollisionX() {
let bubbleOffsetX = bubbleInitialPosition.x
let bubbleWidth = bubbleSize.width
let sceneWidth = parent.scene.frame.width
let delta: CGFloat = 10

func testLeftXCollision() {
parent.position.x = 1000
bubbleConstraint.apply()
parent.position.x = -1000
let bubbleIntersectsRightSceneEdge = sceneWidth - bubbleWidth - bubbleOffsetX

parent.position.x = bubbleIntersectsRightSceneEdge - delta
bubbleConstraint.apply()
XCTAssertEqual(child.xScale, 1)
}
XCTAssertEqual(child.position.x, bubbleOffsetX, accuracy: delta)

func testTopYCollision() {
bubbleConstraint.apply()
XCTAssertEqual(0, CGFloat(child.position.y), accuracy: 1)
parent.position.y = 100000
parent.position.x = bubbleIntersectsRightSceneEdge + delta
bubbleConstraint.apply()
let topEdge = parent.scene.size.height - parent.yScale * child.frame.size.height
guard let yCollision = parent.mockedStage?.convert(child.position, from: parent).y else {
XCTAssert(false)
return
}
XCTAssertEqual(topEdge, CGFloat(yCollision), accuracy: 200)
}
XCTAssertEqual(child.xScale, -1)
XCTAssertEqual(child.position.x, bubbleOffsetX, accuracy: delta)

func testBottomYCollision() {
parent.position.y = 100000
bubbleConstraint.apply()
parent.position.y = -100000
bubbleConstraint.apply()
guard let yCollision = parent.mockedStage?.convert(child.position, from: parent).y else {
XCTAssert(false)
return
}
XCTAssertEqual(0, yCollision, accuracy: 1)
}
let bubbleIntersectsLeftSceneEdge = bubbleWidth + bubbleOffsetX

func testRotationCollision() {
bubbleConstraint.apply()
XCTAssertEqual(0, CGFloat(child.position.y), accuracy: 1)
parent.zRotation = .pi
parent.position.y = 10000
parent.position.x = bubbleIntersectsLeftSceneEdge + delta
bubbleConstraint.apply()
XCTAssertEqual(child.xScale, -1)
XCTAssertEqual(child.position.x, -bubbleOffsetX, accuracy: delta)

guard let yCollision = parent.mockedStage?.convert(child.position, from: parent).y else {
XCTAssert(false)
return
}
let topEdge = parent.scene.size.height - parent.yScale * child.frame.size.height
XCTAssertEqual(topEdge, CGFloat(yCollision), accuracy: 200)
parent.position.x = bubbleIntersectsLeftSceneEdge - delta
bubbleConstraint.apply()
XCTAssertEqual(child.xScale, 1)
XCTAssertEqual(child.position.x, -bubbleOffsetX, accuracy: delta)
}
}

0 comments on commit d565258

Please sign in to comment.