mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
11 KiB
Swift
227 lines
11 KiB
Swift
import UIKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import GlassBackgroundComponent
|
|
|
|
public final class WalletSendCommentBackgroundView: UIView {
|
|
private struct Parameters: Equatable {
|
|
let size: CGSize
|
|
let scale: CGFloat
|
|
let maxCornerRadius: CGFloat
|
|
let minCornerRadius: CGFloat
|
|
let incoming: Bool
|
|
let hasTail: Bool
|
|
let backgroundColor: UIColor
|
|
let primaryTextColor: UIColor
|
|
let isDark: Bool
|
|
}
|
|
|
|
private let imageView = UIImageView()
|
|
private let glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil)
|
|
private var parameters: Parameters?
|
|
|
|
override public var isUserInteractionEnabled: Bool {
|
|
didSet {
|
|
self.glassHighlightRecognizer.isEnabled = self.isUserInteractionEnabled
|
|
if !self.isUserInteractionEnabled {
|
|
self.layer.removeAnimation(forKey: "sublayerTransform")
|
|
self.layer.sublayerTransform = CATransform3DIdentity
|
|
}
|
|
}
|
|
}
|
|
|
|
override public init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.imageView.contentMode = .scaleToFill
|
|
self.addSubview(self.imageView)
|
|
self.addGestureRecognizer(self.glassHighlightRecognizer)
|
|
self.isUserInteractionEnabled = false
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
public func update(size: CGSize, maxCornerRadius: CGFloat, minCornerRadius: CGFloat, theme: PresentationTheme, incoming: Bool = false, hasTail: Bool = true) {
|
|
guard size.width > 0.0, size.height > 0.0 else { return }
|
|
self.imageView.frame = CGRect(origin: .zero, size: size)
|
|
// Keep the rounded ends and their glow fixed while the center stretches.
|
|
let glowPadding = ceil(min(10.0, size.height * 0.24) * 2.0)
|
|
let imageSize = CGSize(width: hasTail ? size.width : min(size.width, maxCornerRadius * 2.0 + glowPadding * 2.0 + 1.0), height: size.height)
|
|
let parameters = Parameters(
|
|
size: imageSize,
|
|
scale: self.window?.screen.scale ?? UIScreen.main.scale,
|
|
maxCornerRadius: maxCornerRadius,
|
|
minCornerRadius: minCornerRadius,
|
|
incoming: incoming,
|
|
hasTail: hasTail,
|
|
backgroundColor: theme.list.modalPlainBackgroundColor,
|
|
primaryTextColor: theme.list.itemPrimaryTextColor,
|
|
isDark: theme.overallDarkAppearance
|
|
)
|
|
guard self.parameters != parameters else { return }
|
|
if let image = Self.generateImage(parameters: parameters) {
|
|
self.parameters = parameters
|
|
if hasTail {
|
|
self.imageView.image = image
|
|
} else {
|
|
let capWidth = max(0.0, (imageSize.width - 1.0) * 0.5)
|
|
self.imageView.image = image.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: capWidth, bottom: 0.0, right: capWidth), resizingMode: .stretch)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func generateImage(parameters: Parameters) -> UIImage? {
|
|
let size = parameters.size
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
func roundedRectMask(inset: CGFloat) -> UIImage? {
|
|
return Display.generateImage(size, scale: parameters.scale, rotatedContext: { _, context in
|
|
context.clear(bounds)
|
|
let cornerRadius = max(0.0, min(parameters.maxCornerRadius, min(size.width, size.height) * 0.5) - inset)
|
|
context.addPath(CGPath(roundedRect: bounds.insetBy(dx: inset, dy: inset), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.fillPath()
|
|
})
|
|
}
|
|
let shapeMask: UIImage?
|
|
if parameters.hasTail {
|
|
let bubble = messageBubbleImage(
|
|
maxCornerRadius: parameters.maxCornerRadius,
|
|
minCornerRadius: parameters.minCornerRadius,
|
|
incoming: parameters.incoming,
|
|
fillColor: .white,
|
|
strokeColor: .clear,
|
|
neighbors: .none,
|
|
shadow: nil,
|
|
wallpaper: .color(0),
|
|
knockout: false,
|
|
mask: true
|
|
)
|
|
shapeMask = Display.generateImage(size, scale: parameters.scale, rotatedContext: { _, context in
|
|
context.clear(bounds)
|
|
UIGraphicsPushContext(context)
|
|
bubble.draw(in: bounds)
|
|
UIGraphicsPopContext()
|
|
})
|
|
} else {
|
|
shapeMask = roundedRectMask(inset: 0.0)
|
|
}
|
|
guard let mask = shapeMask else { return nil }
|
|
|
|
func insetMask(by inset: CGFloat) -> UIImage? {
|
|
if !parameters.hasTail {
|
|
return roundedRectMask(inset: inset)
|
|
}
|
|
return Display.generateImage(size, scale: parameters.scale, rotatedContext: { _, context in
|
|
context.clear(bounds)
|
|
UIGraphicsPushContext(context)
|
|
mask.draw(in: bounds)
|
|
for index in 0 ..< 8 {
|
|
let angle = CGFloat(index) * .pi / 4.0
|
|
mask.draw(
|
|
in: bounds.offsetBy(dx: cos(angle) * inset, dy: sin(angle) * inset),
|
|
blendMode: .destinationIn,
|
|
alpha: 1.0
|
|
)
|
|
}
|
|
UIGraphicsPopContext()
|
|
})
|
|
}
|
|
let contourScale: CGFloat = parameters.isDark ? 0.5 : 1.0
|
|
guard let borderMask = insetMask(by: contourScale / parameters.scale),
|
|
let rimMask = insetMask(by: 0.65 * contourScale),
|
|
let innerMask = insetMask(by: 1.4 * contourScale) else { return nil }
|
|
|
|
let glowRadius = min(10.0, size.height * 0.24)
|
|
let padding = ceil(glowRadius * 2.0)
|
|
let outsideSize = CGSize(width: size.width + padding * 2.0, height: size.height + padding * 2.0)
|
|
guard let outsideMask = Display.generateImage(outsideSize, scale: parameters.scale, rotatedContext: { _, context in
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.fill(CGRect(origin: .zero, size: outsideSize))
|
|
UIGraphicsPushContext(context)
|
|
mask.draw(in: bounds.offsetBy(dx: padding, dy: padding), blendMode: .destinationOut, alpha: 1.0)
|
|
UIGraphicsPopContext()
|
|
}) else { return nil }
|
|
|
|
let fillLocations: [CGFloat] = [0.0, 0.10, 0.23, 0.40, 0.54, 0.70, 0.82, 1.0]
|
|
let fillColors: [CGColor]
|
|
if parameters.isDark {
|
|
let fillAmounts: [CGFloat] = [0.048, 0.026, 0.013, 0.013, 0.018, 0.018, 0.022, 0.048]
|
|
fillColors = fillAmounts.map { amount in
|
|
parameters.backgroundColor.mixedWith(parameters.primaryTextColor, alpha: amount).cgColor
|
|
}
|
|
} else {
|
|
let fillShades: [UInt32] = [0xfafafb, 0xf9f9fa, 0xf6f6f7, 0xf4f4f5, 0xf6f6f7, 0xf8f8f9, 0xf9f9fa, 0xfafafb]
|
|
fillColors = fillShades.map { UIColor(rgb: $0).cgColor }
|
|
}
|
|
guard let fillGradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: fillColors as CFArray, locations: fillLocations) else { return nil }
|
|
let highlightColor: UIColor
|
|
if parameters.isDark {
|
|
var hue: CGFloat = 0.0
|
|
var saturation: CGFloat = 0.0
|
|
parameters.backgroundColor.getHue(&hue, saturation: &saturation, brightness: nil, alpha: nil)
|
|
highlightColor = UIColor(hue: hue, saturation: saturation * 0.65, brightness: parameters.primaryTextColor.brightness, alpha: 1.0)
|
|
} else {
|
|
highlightColor = .white
|
|
}
|
|
|
|
let borderLocations: [CGFloat] = [0.0, 0.20, 0.50, 0.80, 1.0]
|
|
let borderAmounts: [CGFloat] = parameters.isDark ? [0.26, 0.075, 0.013, 0.075, 0.26] : [0.10, 0.24, 0.33, 0.24, 0.11]
|
|
let borderColors = borderAmounts.map { amount in
|
|
parameters.backgroundColor.mixedWith(parameters.isDark ? highlightColor : parameters.primaryTextColor, alpha: amount).cgColor
|
|
}
|
|
guard let borderGradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: borderColors as CFArray, locations: borderLocations) else { return nil }
|
|
let glowColor = highlightColor.withAlphaComponent(parameters.isDark ? 0.025 : 0.9)
|
|
let highlightAmounts: [CGFloat] = parameters.isDark ? [0.14, 0.03, 0.0, 0.03, 0.14] : [0.94, 0.94, 0.94, 0.94, 0.94]
|
|
let highlightColors = highlightAmounts.map { highlightColor.withAlphaComponent($0).cgColor }
|
|
guard let highlightGradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: highlightColors as CFArray, locations: borderLocations) else { return nil }
|
|
|
|
return Display.generateImage(size, scale: parameters.scale, rotatedContext: { _, context in
|
|
let fillInset = parameters.hasTail ? min(2.0, size.height * 0.1) : 0.0
|
|
context.drawLinearGradient(
|
|
fillGradient,
|
|
start: CGPoint(x: 0.0, y: fillInset),
|
|
end: CGPoint(x: 0.0, y: size.height - fillInset),
|
|
options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]
|
|
)
|
|
UIGraphicsPushContext(context)
|
|
|
|
context.saveGState()
|
|
context.setShadow(offset: .zero, blur: glowRadius, color: glowColor.cgColor)
|
|
outsideMask.draw(in: CGRect(x: -padding, y: -padding, width: outsideSize.width, height: outsideSize.height))
|
|
context.restoreGState()
|
|
mask.draw(in: bounds, blendMode: .destinationIn, alpha: 1.0)
|
|
|
|
func drawRing(outer: UIImage, inner: UIImage, fill: () -> Void) {
|
|
context.saveGState()
|
|
context.beginTransparencyLayer(auxiliaryInfo: nil)
|
|
outer.draw(in: bounds)
|
|
inner.draw(in: bounds, blendMode: .destinationOut, alpha: 1.0)
|
|
context.setBlendMode(.sourceIn)
|
|
fill()
|
|
context.setBlendMode(.normal)
|
|
context.endTransparencyLayer()
|
|
context.restoreGState()
|
|
}
|
|
drawRing(outer: mask, inner: borderMask) {
|
|
context.drawLinearGradient(
|
|
borderGradient,
|
|
start: CGPoint(x: 0.0, y: fillInset),
|
|
end: CGPoint(x: 0.0, y: size.height - fillInset),
|
|
options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]
|
|
)
|
|
}
|
|
drawRing(outer: rimMask, inner: innerMask) {
|
|
context.drawLinearGradient(
|
|
highlightGradient,
|
|
start: CGPoint(x: 0.0, y: fillInset),
|
|
end: CGPoint(x: 0.0, y: size.height - fillInset),
|
|
options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]
|
|
)
|
|
}
|
|
UIGraphicsPopContext()
|
|
})
|
|
}
|
|
}
|