如何逐像素缓慢渲染完整图像?
How to render full image slowly pixel by pixel?
我想慢慢渲染图像的各个部分,如附图所示。如何使用 Swift 在 iOS 中实现此目的?
您可以使用 CAShapeLayer
作为 遮罩。
如果我们从这张图片开始:
并添加如下所示的遮罩层(灰色实际上将完全透明):
像这样叠加在图像上:
只会显示蒙版的黑色部分(同样,灰色部分将完全透明):
因此,如果我们计算图像的矩形网格:
然后我们可以用黑色“随机填充网格”,循环填充越来越多的网格点,图像将像您的 gif 中一样“显露出来”。
这是一个简单的示例,使用 0.75 秒的重复计时器:
class RevealImageView: UIView {
public var image: UIImage? {
didSet {
imgView.image = image
}
}
private let imgView = UIImageView()
private let maskLayer = CAShapeLayer()
// array of rectangles that make up our grid
private var gridOfRects: [CGRect] = []
// we're going to use a 20x20 grid
private var numRows: Int = 20
private var numCols: Int = 20
// counter for how many rectangles to reveal
private var gridCounter: Int = 0
// how many more rects each time
private var gridIncrement: Int = 40
// how long between steps (in seconds)
private var stepTime: CFTimeInterval = 0.75
// timer for updating the grid
private var timer: Timer?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// add the image view and constrain all 4 sides
addSubview(imgView)
imgView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: topAnchor),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
// set the layer mask for the image view
imgView.layer.mask = maskLayer
// black rects will show that part of the image
maskLayer.fillColor = UIColor.black.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// this is when we know auto-layout has set the frame / bounds
maskLayer.frame = bounds
}
@objc private func updateMask() -> Void {
// increment number of rectangles to reveal
gridCounter += gridIncrement
let pth = UIBezierPath()
for i in 0..<gridCounter {
// don't go past the end of the array
if i < gridOfRects.count {
pth.append(UIBezierPath(rect: gridOfRects[i]))
}
}
maskLayer.path = pth.cgPath
// if image is fully revealed
if gridCounter >= gridOfRects.count {
// stop the repeating timer
timer?.invalidate()
}
}
// call this from the controller to start the reveal
func startReveal() -> Void {
// stop the timer if it's running
timer?.invalidate()
// empty grid array if it's being called again
gridOfRects.removeAll()
let rowHeight = bounds.height / CGFloat(numRows)
let colWidth = bounds.width / CGFloat(numCols)
var r: CGRect = CGRect(x: 0, y: 0, width: colWidth, height: rowHeight)
// build array of grid rectangles
for _ in 1...numRows {
for _ in 1...numCols {
gridOfRects.append(r)
r.origin.x += colWidth
}
r.origin.x = 0
r.origin.y += rowHeight
}
// shuffle the array so the rectangles are randomized
gridOfRects.shuffle()
// reset grid counter
gridCounter = 0
// do the first update
updateMask()
// set a repeating timer to continue the reveal
timer = Timer.scheduledTimer(timeInterval: stepTime, target: self, selector: #selector(self.updateMask), userInfo: nil, repeats: true)
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
// stop the timer if it's running
timer?.invalidate()
}
}
}
这是一个示例视图控制器 class 以查看它的实际效果:
class RevealImageViewController: UIViewController {
var revealImageView: RevealImageView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
// make sure we can load an image
guard let img = UIImage(named: "testing") else {
print("Could not load image!!!")
return
}
// create the view
revealImageView = RevealImageView()
// set the image
revealImageView.image = img
revealImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(revealImageView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
revealImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
revealImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
// use image size to keep proportions
revealImageView.heightAnchor.constraint(equalTo:revealImageView.widthAnchor, multiplier: img.size.height / img.size.width),
revealImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// tap anywhere in the view to begin the reveal
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.didTap))
view.addGestureRecognizer(tapGesture)
}
@objc func didTap() -> Void {
revealImageView.startReveal()
}
}
发布时的样子:
点击任意位置,几秒钟后看起来像这样:
当它完全“显示”时,计时器停止,看起来像这样:
我想慢慢渲染图像的各个部分,如附图所示。如何使用 Swift 在 iOS 中实现此目的?
您可以使用 CAShapeLayer
作为 遮罩。
如果我们从这张图片开始:
并添加如下所示的遮罩层(灰色实际上将完全透明):
像这样叠加在图像上:
只会显示蒙版的黑色部分(同样,灰色部分将完全透明):
因此,如果我们计算图像的矩形网格:
然后我们可以用黑色“随机填充网格”,循环填充越来越多的网格点,图像将像您的 gif 中一样“显露出来”。
这是一个简单的示例,使用 0.75 秒的重复计时器:
class RevealImageView: UIView {
public var image: UIImage? {
didSet {
imgView.image = image
}
}
private let imgView = UIImageView()
private let maskLayer = CAShapeLayer()
// array of rectangles that make up our grid
private var gridOfRects: [CGRect] = []
// we're going to use a 20x20 grid
private var numRows: Int = 20
private var numCols: Int = 20
// counter for how many rectangles to reveal
private var gridCounter: Int = 0
// how many more rects each time
private var gridIncrement: Int = 40
// how long between steps (in seconds)
private var stepTime: CFTimeInterval = 0.75
// timer for updating the grid
private var timer: Timer?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// add the image view and constrain all 4 sides
addSubview(imgView)
imgView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: topAnchor),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
// set the layer mask for the image view
imgView.layer.mask = maskLayer
// black rects will show that part of the image
maskLayer.fillColor = UIColor.black.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// this is when we know auto-layout has set the frame / bounds
maskLayer.frame = bounds
}
@objc private func updateMask() -> Void {
// increment number of rectangles to reveal
gridCounter += gridIncrement
let pth = UIBezierPath()
for i in 0..<gridCounter {
// don't go past the end of the array
if i < gridOfRects.count {
pth.append(UIBezierPath(rect: gridOfRects[i]))
}
}
maskLayer.path = pth.cgPath
// if image is fully revealed
if gridCounter >= gridOfRects.count {
// stop the repeating timer
timer?.invalidate()
}
}
// call this from the controller to start the reveal
func startReveal() -> Void {
// stop the timer if it's running
timer?.invalidate()
// empty grid array if it's being called again
gridOfRects.removeAll()
let rowHeight = bounds.height / CGFloat(numRows)
let colWidth = bounds.width / CGFloat(numCols)
var r: CGRect = CGRect(x: 0, y: 0, width: colWidth, height: rowHeight)
// build array of grid rectangles
for _ in 1...numRows {
for _ in 1...numCols {
gridOfRects.append(r)
r.origin.x += colWidth
}
r.origin.x = 0
r.origin.y += rowHeight
}
// shuffle the array so the rectangles are randomized
gridOfRects.shuffle()
// reset grid counter
gridCounter = 0
// do the first update
updateMask()
// set a repeating timer to continue the reveal
timer = Timer.scheduledTimer(timeInterval: stepTime, target: self, selector: #selector(self.updateMask), userInfo: nil, repeats: true)
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
// stop the timer if it's running
timer?.invalidate()
}
}
}
这是一个示例视图控制器 class 以查看它的实际效果:
class RevealImageViewController: UIViewController {
var revealImageView: RevealImageView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
// make sure we can load an image
guard let img = UIImage(named: "testing") else {
print("Could not load image!!!")
return
}
// create the view
revealImageView = RevealImageView()
// set the image
revealImageView.image = img
revealImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(revealImageView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
revealImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
revealImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
// use image size to keep proportions
revealImageView.heightAnchor.constraint(equalTo:revealImageView.widthAnchor, multiplier: img.size.height / img.size.width),
revealImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// tap anywhere in the view to begin the reveal
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.didTap))
view.addGestureRecognizer(tapGesture)
}
@objc func didTap() -> Void {
revealImageView.startReveal()
}
}
发布时的样子:
点击任意位置,几秒钟后看起来像这样:
当它完全“显示”时,计时器停止,看起来像这样: