Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import Foundation
- import UIKit
- class CurveText: UIView, UIGestureRecognizerDelegate, UITextViewDelegate {
- var tf = UITextView()
- private let shadowLayer = CALayer()
- private let textLayer = CALayer()
- private let defaultTextViewLeftInset = CGFloat(5)
- private let defaultTextViewRightInset = CGFloat(5)
- var curvePattern = (CGPoint(x: 0, y: 0), CGPoint(x: 90, y: 50), CGPoint(x: 180, y: 0)) {
- didSet {
- curve = curvePattern
- setNeedsLayout()
- }
- }
- var curve = (CGPoint(x: 0, y: 0), CGPoint(x: 90, y: 50), CGPoint(x: 180, y: 0))
- var min = CGFloat(20) { didSet { setNeedsLayout() } }
- var max = CGFloat(40) { didSet { setNeedsLayout() } }
- var maxPosition: CGFloat = 0 { didSet { setNeedsLayout() } } // 0...1
- let minCharsForStartCalculating = 3
- var textColor = UIColor.white { didSet { setNeedsDisplay() } }
- var shadowOffset = CGPoint(x: 1, y: 0) { didSet { setNeedsDisplay() } }
- var shadowBlur = CGFloat(0) { didSet { setNeedsDisplay() } }
- var shadowWeight = CGFloat(1) { didSet { setNeedsDisplay() } }
- var shadowColor = UIColor.black { didSet { setNeedsDisplay() } }
- var text: String = "Psssh" { didSet { setNeedsLayout() } }
- var font: UIFont = UIFont.systemFont(ofSize: 1) { didSet { setNeedsLayout() } }
- func update() {
- let newString = self.text
- let attrStr = NSMutableAttributedString(string: newString)
- attrStr.addAttributes([
- NSForegroundColorAttributeName: UIColor.red.withAlphaComponent(0),
- NSKernAttributeName: 0
- ], range: NSRange(location: 0, length: attrStr.length))
- for (index, _) in newString.characters.enumerated() {
- let base = CGFloat([newString.characters.count, minCharsForStartCalculating].max()!) - 1
- var radius: CGFloat
- radius = [1 - maxPosition, maxPosition][base * maxPosition - CGFloat(index) < 0 ? 0 : 1]
- //radius = [1 - maxPosition, maxPosition].max()!
- radius = [1, radius * base].max()!
- let koef = abs(base * maxPosition - CGFloat(index)) / radius
- let fontSize = CGFloat(Int(CGFloat(max - min) * (1 - koef) + CGFloat(min) * 5) / 5)
- //let fontSize = CGFloat((1 - CGFloat(index) / CGFloat(newValue.characters.count)) * (CGFloat(max) - CGFloat(min)) + CGFloat(min))
- let font = self.font.withSize(fontSize)
- attrStr.addAttribute(NSFontAttributeName, value: font, range: NSRange(location: index, length: 1))
- }
- let l = length(CGPoint(x: curve.0.x, y: 0), CGPoint(x: curve.2.x, y: 0))
- if newString.count >= minCharsForStartCalculating || l <= 10 {
- curve.2 = interpolate(curve.0, curve.2, attrStr.size().width / l)
- curve.1 = interpolate(curve.0, curve.1, attrStr.size().width / l)
- }
- var baselineGlobalOffset: CGFloat = 0
- var currentXPosition = CGFloat(0)
- for (index, char) in newString.characters.enumerated() {
- let charAttrs = attrStr.attributes(at: index, effectiveRange: nil)
- let font = charAttrs[NSFontAttributeName] as! UIFont
- let charSize = NSString(string: String(char)).size(attributes: charAttrs)
- let baselineOffset = -(quadraticBezier(curve, rootsQuadraticBezier(curve, currentXPosition + charSize.width / 2).0).y)
- baselineGlobalOffset = [font.ascender + [baselineOffset, 0].max()!, baselineGlobalOffset].max()!
- attrStr.addAttribute(NSBaselineOffsetAttributeName, value: baselineOffset, range: NSRange(location: index, length: 1))
- currentXPosition += charSize.width
- var unichars = [UniChar](String(char).utf16)
- var glyphs = [CGGlyph](repeating: 0, count: unichars.count)
- var boundingRects = [CGRect](repeating: .zero, count: unichars.count)
- var advances = [CGSize](repeating: .zero, count: unichars.count)
- let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
- let _ = CTFontGetBoundingRectsForGlyphs(font as CTFont, .horizontal, glyphs, &boundingRects, unichars.count)
- let _ = CTFontGetAdvancesForGlyphs(font as CTFont, .horizontal, glyphs, &advances, unichars.count)
- attrStr.addAttribute("advance", value: advances.first!, range: NSRange(location: index, length: 1))
- attrStr.addAttribute("boundingRect", value: boundingRects.first!, range: NSRange(location: index, length: 1))
- attrStr.addAttribute("glyph", value: glyphs.first!, range: NSRange(location: index, length: 1))
- if gotGlyphs {
- for glyph in glyphs {
- var matrix = CGAffineTransform.identity
- if let path = CTFontCreatePathForGlyph(font as CTFont, glyph, &matrix) {
- attrStr.addAttribute("path", value: path, range: NSRange(location: index, length: 1))
- }
- }
- }
- }
- let selection = tf.selectedTextRange
- tf.attributedText = attrStr
- tf.selectedTextRange = selection
- tf.bounds.size = attrStr.size()
- tf.frame.origin.y = -baselineGlobalOffset
- tf.frame.origin.x = 0
- tf.frame = tf.frame.insetBy(dx: -(defaultTextViewLeftInset + defaultTextViewRightInset) / 2, dy: 0)
- }
- private func updateVector(_ attrStr: NSMutableAttributedString) {
- let glyphsPath = CGMutablePath()
- var advancesSum = CGFloat(0)
- for (index, _) in self.text.enumerated() {
- // let font = attrStr.attribute(NSFontAttributeName, at: index, effectiveRange: nil) as! UIFont
- let baselineOffset = attrStr.attribute(NSBaselineOffsetAttributeName, at: index, effectiveRange: nil) as! CGFloat
- let kern = attrStr.attribute(NSKernAttributeName, at: index, effectiveRange: nil) as? CGFloat ?? 0
- let boundingRect = attrStr.attribute("boundingRect", at: index, effectiveRange: nil) as! CGRect
- let advance = attrStr.attribute("advance", at: index, effectiveRange: nil) as! CGSize
- //let glyph = attrStr.attribute("glyph", at: index, effectiveRange: nil) as! CGGlyph
- let path = attrStr.attribute("path", at: index, effectiveRange: nil) as! CGPath?
- var prevBaseline: CGFloat? = nil
- var prevAdvance: CGSize? = nil
- if 0..<self.text.count ~= (index - 1) {
- prevBaseline = attrStr.attribute(NSBaselineOffsetAttributeName, at: index - 1, effectiveRange: nil) as? CGFloat
- prevAdvance = attrStr.attribute("advance", at: index - 1, effectiveRange: nil) as? CGSize
- }
- var nextBaseline: CGFloat? = nil
- if 0..<self.text.count ~= (index + 1) {
- prevBaseline = attrStr.attribute(NSBaselineOffsetAttributeName, at: index + 1, effectiveRange: nil) as? CGFloat
- }
- var angle = CGFloat(0)
- let point1: CGPoint
- let point2: CGPoint
- if let prevBaseline = prevBaseline, let prevAdvance = prevAdvance {
- point1 = CGPoint(x: advancesSum - prevAdvance.width / 2, y: prevBaseline)
- point2 = CGPoint(x: advancesSum + advance.width / 2, y: -baselineOffset)
- } else {
- point1 = CGPoint(x: curve.0.x, y: curve.0.y)
- point2 = CGPoint(x: advancesSum + advance.width / 2, y: -baselineOffset)
- }
- angle = CGFloat(atan2(Double(point2.y - point1.y), Double(point2.x - point1.x))) + CGFloat.pi / 2
- if let path = path {
- let matrix = CGAffineTransform.identity.translatedBy(x: advancesSum, y: -baselineOffset).scaledBy(x: 1, y: -1).rotated(by: angle)
- glyphsPath.addPath(path, transform: matrix)
- let rect = CGMutablePath()
- var copyMatrix = matrix.rotated(by: angle * 2)
- let copyPath = path.copy(using: ©Matrix)!
- rect.addRect(copyPath.boundingBoxOfPath)
- glyphsPath.addPath(rect)
- }
- advancesSum += advance.width + kern
- }
- shadowLayer.sublayers?.forEach{ $0.removeFromSuperlayer() }
- if shadowWeight > 0 {
- let shadowSprite = CALayer()
- let strokeLayer = CAShapeLayer()
- strokeLayer.strokeColor = UIColor.black.cgColor
- strokeLayer.lineWidth = shadowWeight * 2
- strokeLayer.lineCap = kCALineCapRound
- strokeLayer.lineJoin = kCALineJoinRound
- strokeLayer.path = glyphsPath
- strokeLayer.lineDashPattern = [NSNumber(floatLiteral: Double.infinity), 0]
- shadowSprite.addSublayer(strokeLayer)
- let fillLayer = CAShapeLayer()
- fillLayer.fillColor = UIColor.black.cgColor
- fillLayer.path = glyphsPath
- shadowSprite.addSublayer(fillLayer)
- shadowSprite.shadowColor = shadowColor.cgColor
- let dropCoord = CGFloat(1000)
- shadowSprite.shadowOffset = CGSize(width: self.shadowOffset.x + dropCoord, height: self.shadowOffset.y)
- shadowSprite.shadowRadius = self.shadowBlur
- shadowSprite.shadowOpacity = 1
- shadowSprite.frame.origin.x -= dropCoord
- shadowLayer.addSublayer(shadowSprite)
- }
- let textShape = CAShapeLayer()
- textShape.fillColor = textColor.cgColor
- textShape.path = glyphsPath
- textLayer.sublayers?.forEach{ $0.removeFromSuperlayer() }
- textLayer.addSublayer(textShape)
- let lineShape = CAShapeLayer()
- lineShape.strokeColor = UIColor.black.cgColor
- lineShape.lineWidth = 1
- lineShape.fillColor = nil
- let linePath = CGMutablePath()
- linePath.move(to: curve.0)
- linePath.addQuadCurve(to: curve.2, control: curve.1)
- lineShape.path = linePath
- textLayer.addSublayer(lineShape)
- self.textLayer.removeAllAnimations()
- self.shadowLayer.removeAllAnimations()
- }
- override init(frame: CGRect) {
- super.init(frame: frame)
- }
- override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
- return self.frame.contains(point) || self.tf.frame.contains(point)
- }
- convenience init() {
- self.init(frame: .zero)
- self.backgroundColor = .white
- tf.delegate = self
- tf.backgroundColor = nil
- tf.textContainerInset = .zero
- tf.isScrollEnabled = false
- tf.scrollIndicatorInsets = .zero
- tf.showsVerticalScrollIndicator = false
- tf.showsHorizontalScrollIndicator = false
- tf.clipsToBounds = false
- self.addSubview(tf)
- self.layer.insertSublayer(shadowLayer, at: 0)
- self.layer.insertSublayer(textLayer, at: 1)
- // let shape = CAShapeLayer()
- //
- // shape.lineWidth = 1
- // shape.lineCap = kCALineCapRound
- // shape.fillColor = nil
- // shape.strokeColor = UIColor.black.cgColor
- // self.layer.insertSublayer(shape, at: 2)
- //
- // var angle = CGFloat(1);
- // Timer.scheduledTimer(withTimeInterval: 1 / 60, repeats: true, block: {
- // _ in
- //
- // angle -= 0.04
- // if angle < -0.5 {
- // //return
- // }
- // let path = CGMutablePath()
- // path.move(to: self.curve.0)
- // path.addQuadCurve(to: self.curve.2, control: self.curve.1)
- // self.curve.1.y += sin(angle) * 3
- // //self.curve.2.y -= sin(angle) / 4
- // self.curve.0.y -= sin(angle) / 4
- // self.text = (self.text + "")
- // self.setNeedsDisplay()
- //
- // shape.path = path
- // })
- }
- override func draw(_ rect: CGRect) {
- updateVector(NSMutableAttributedString(attributedString: tf.attributedText))
- }
- override func layoutSubviews() {
- update()
- setNeedsDisplay()
- }
- func textViewDidChange(_ textView: UITextView) {
- self.text = textView.text
- }
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- func pointAtBrokenLine(_ points: [CGPoint], position: CGFloat) -> (CGPoint, CGPoint)? {
- var sumLength: CGFloat = 0
- guard points.count > 1 else {
- return nil
- }
- guard position > 0 else {
- return (points[0], halfVec(points[1], points[0], points[1]))
- }
- var prevPoint: CGPoint = points.first!
- for i in 1..<points.count {
- let point = points[i]
- let len = length(prevPoint, point)
- if sumLength + len == position || i == points.count - 1 && (position - sumLength - len < CGFloat.ulpOfOne * CGFloat(points.count)) {
- let nextPoint = i < points.count - 1 ? points[i + 1] : prevPoint
- if (nextPoint != prevPoint) {
- return (point, CGPoint(x: (point.x - prevPoint.x) / len, y: (point.y - prevPoint.y) / len))
- }
- return (point, halfVec(prevPoint, point, nextPoint))
- }
- if sumLength + len > position {
- return (interpolate(prevPoint, point, (position - sumLength) / len), CGPoint(x: -(prevPoint.y - point.y) / len, y: (prevPoint.x - point.x) / len ))
- }
- sumLength += len
- prevPoint = point
- }
- return nil
- }
- func halfVec(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint) -> CGPoint {
- var halfPoint = CGPoint(
- x: (p1.x - p2.x) + (p1.x - p3.x),
- y: (p1.y - p2.y) + (p1.y - p3.y)
- )
- if skewProduct(sub(p1, p2), sub(p2, p3)) >= 0 {
- halfPoint.x *= -1
- halfPoint.y *= -1
- }
- let len = length(halfPoint, p2)
- halfPoint.x /= len
- halfPoint.y /= len
- return halfPoint
- }
- func quadraticBezierToBrokenLine(_ points: (CGPoint, CGPoint, CGPoint), quality: CGFloat = 1) -> [CGPoint] {
- guard CGFloat.ulpOfOne...1 ~= quality else {
- return [points.0, points.1, points.2]
- }
- let step = (1 / quality) / (length(points.0, points.1) + length(points.1, points.2))
- return stride(from: 0, to: 1, by: step).map{
- return quadraticBezier(points, $0)
- } + [points.2]
- }
- func rootsQuadraticBezier(_ points: (CGPoint, CGPoint, CGPoint), _ x: CGFloat) -> (CGFloat, CGFloat) {
- let a = [points.2.x - points.1.x * 2 + points.0.x, 1].max()!
- let b = 2 * (points.1.x - points.0.x)
- let c = points.0.x - x
- let d = b * b - 4 * a * c
- return (
- (-b + sqrt(d)) / (2 * a),
- (-b - sqrt(d)) / (2 * a)
- )
- }
- func rootLine(_ points: (CGPoint, CGPoint), _ x: CGFloat) -> CGFloat {
- return (x - points.0.x) / (points.1.x - points.0.x)
- }
- func quadraticBezier(_ points: (CGPoint, CGPoint, CGPoint), _ t: CGFloat) -> CGPoint {
- return CGPoint(x: quadraticBezier((points.0.x, points.1.x, points.2.x), t), y: quadraticBezier((points.0.y, points.1.y, points.2.y), t))
- }
- func quadraticBezier(_ points: (CGFloat, CGFloat, CGFloat), _ t: CGFloat) -> CGFloat {
- return points.0 + t * (t * (points.2 - points.1 * 2 + points.0) + 2 * (points.1 - points.0));
- }
- func skewProduct(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
- return p1.x * p2.y - p2.x * p1.y
- }
- func sub(_ p1: CGPoint, _ p2: CGPoint) -> CGPoint {
- return CGPoint(x: p1.x - p2.x, y: p1.y - p2.y)
- }
- func add(_ p1: CGPoint, _ p2: CGPoint) -> CGPoint {
- return CGPoint(x: p1.x + p2.x, y: p1.y + p2.y)
- }
- func length(_ points: [CGPoint]) -> CGFloat? {
- guard points.count > 1 else { return nil }
- return points.reduce((points.first!, CGFloat(0)), {
- return ($1, $0.1 + length($0.0, $1))
- }).1
- }
- func length(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
- return length(CGPoint(x: p2.x - p1.x, y: p2.y - p1.y))
- }
- func length(_ p: CGPoint) -> CGFloat {
- return (p.x * p.x + p.y * p.y).squareRoot()
- }
- func interpolate(_ p1: CGPoint, _ p2: CGPoint, _ t: CGFloat) -> CGPoint {
- return CGPoint(x: (p2.x - p1.x) * t + p1.x, y: (p2.y - p1.y) * t + p1.y)
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement