Draw Line Animated

Draw line animated

You can animate the end of the stroke of a path on a CAShapeLayer, e.g.,

weak var shapeLayer: CAShapeLayer?

@IBAction func didTapButton(_ sender: Any) {
// remove old shape layer if any

self.shapeLayer?.removeFromSuperlayer()

// create whatever path you want

let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 50))
path.addLine(to: CGPoint(x: 200, y: 50))
path.addLine(to: CGPoint(x: 200, y: 240))

// create shape layer for that path

let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 4
shapeLayer.path = path.cgPath

// animate it

view.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 2
shapeLayer.add(animation, forKey: "MyAnimation")

// save shape layer

self.shapeLayer = shapeLayer
}

That yields:

stroked path

Clearly, you can change the UIBezierPath to whatever suits your interests. For example, you could have spaces in the path. Or you don't even need to have rectilinear paths:

let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 130))
path.addCurve(to: CGPoint(x: 210, y: 200), controlPoint1: CGPoint(x: 50, y: -100), controlPoint2: CGPoint(x: 100, y: 350))

You can also combine animation of both the start and end of the stroke in a CAAnimationGroup:

// create shape layer for that path (this defines what the path looks like when the animation is done)

let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 5
shapeLayer.path = path.cgPath
shapeLayer.strokeStart = 0.8

let startAnimation = CABasicAnimation(keyPath: "strokeStart")
startAnimation.fromValue = 0
startAnimation.toValue = 0.8

let endAnimation = CABasicAnimation(keyPath: "strokeEnd")
endAnimation.fromValue = 0.2
endAnimation.toValue = 1.0

let animation = CAAnimationGroup()
animation.animations = [startAnimation, endAnimation]
animation.duration = 2
shapeLayer.add(animation, forKey: "MyAnimation")

Yielding:

Sample Image

CoreAnimation gives you a lot of control over how the stroked path is rendered.

Animating drawLine using Jetpack Compose

I'm not sure what you want to achieve... But assuming that you want to animate just in case lives == 5, you just need to wrap the LaunchedEffect with your condition.
For instance:

@Composable
fun LineAnimation(lives: Int) {
val animVal = remember { Animatable(0f) }
if (lives > 5) {
LaunchedEffect(animVal) {
animVal.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
)
}
}
Canvas(modifier = Modifier.size(200.dp, 200.dp)) {
drawLine(
color = Color.Black,
start = Offset(0f, 0f),
end = Offset(animVal.value * size.width, animVal.value * size.height),
strokeWidth = 2f
)
}
}

and where you're calling this Composable, you can do the following:

@Composable
fun AnimationScreen() {
var count by remember {
mutableStateOf(0)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Button(onClick = { count++ }) {
Text("Count $count")
}
LineAnimation(count)
}
}

This is the result:

Sample Image

How do I animate a line from my draw() function?

Of course you can, but your question is a little broad. You do have a particular type of animation in mind? Endless possibilities ;)

The basic way to handle something like this in processing is to increase some animation-variables every frame (or use time management - though that is beyond the basics).

Because the animation-variables (for instance position or color) are changed every frame, the animation is different every frame. It's animated.

Below I give an example of a small green line traveling over a black 'connection' line. If you read through the code I think you'll figure it out. This should be incorporated in a nice 'connection' class for ease of use on a larger scale.

//set coordinates for connection-line
int fromX = 100;
int toX = 600;
int fromY = 100;
int toY = 200;
//copy start coordinate for animation
int animx = fromX;
int animy = fromY;
//determine animation stepsize
int stepX = (toX-fromX)/10;
int stepY = (toY-fromY)/10;

void setup() {
size(800, 300);
//set framerate so animation is not to fast
frameRate(5);
//draw thick lines
strokeWeight(10);
}

void draw() {
background(255);
// draw connection in black
stroke(0);
line(fromX, fromY, toX, toY);
//draw animation in green
stroke(0, 255, 0);
line(animx, animy, animx+stepX, animy+stepY);
// step animation for next frame
animx = animx+stepX;
animy = animy+stepY;
// check for reset (if the animation on the next frame is drawn outside the line)
if (animx+stepX > toX)
{
animx = fromX;
}
if (animy+stepY > toY)
{
animy = fromY;
}
}

How to show a line being drawn from one point to another?

You can use an AnimationController to control the animation duration.

To draw the line "step by step" you can use a Tween (linear interpolation between a beginning and ending value).

Then you just need to pass the current progress to your line painter and calculate the new width/height on each paint() when you call canvas.drawLine.

Working example :

import 'package:flutter/material.dart';

class Line extends StatefulWidget {
@override
State<StatefulWidget> createState() => _LineState();
}

class _LineState extends State<Line> with SingleTickerProviderStateMixin {
double _progress = 0.0;
Animation<double> animation;

@override
void initState() {
super.initState();
var controller = AnimationController(duration: Duration(milliseconds: 3000), vsync: this);

animation = Tween(begin: 1.0, end: 0.0).animate(controller)
..addListener(() {
setState(() {
_progress = animation.value;
});
});

controller.forward();
}

@override
Widget build(BuildContext context) {
return CustomPaint(painter: LinePainter(_progress));
}
}

class LinePainter extends CustomPainter {
Paint _paint;
double _progress;

LinePainter(this._progress) {
_paint = Paint()
..color = Colors.green
..strokeWidth = 8.0;
}

@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0.0, 0.0), Offset(size.width - size.width * _progress, size.height - size.height * _progress), _paint);
}

@override
bool shouldRepaint(LinePainter oldDelegate) {
return oldDelegate._progress != _progress;
}
}

Then use it like this :

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _HomeState();
}
}

class _HomeState extends State<Home> {
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('Line animation'),
leading: new Icon(Icons.insert_emoticon),
),
backgroundColor: Colors.white,
body: SizedBox(height: 200, width: 200, child: Line()),
);
}
}

The line will be drawn in the sized box from 0,0 to 200,200 in 3 seconds.

Result :

Sample Image

Canvas line drawing animation

Here is what I think you are describing...

window.onload = function() {  var canvas = document.getElementById("canvas"),    context = canvas.getContext("2d"),    width = canvas.width = 400,    height = canvas.height = 220,    xcenter = 200,    ycenter = 110,    radius = 0,    radiusmax = 100,    start_angle1 = 0,    start_angle2 = 0;
function toRadians(angle) { return angle * (Math.PI / 180); }
function draw(x1, y1, x2, y2) { context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); context.stroke(); }
function drawWheel(xc, yc, start_angle, count, rad) { var inc = 360 / count; for (var angle = start_angle; angle < start_angle + 180; angle += inc) { var x = Math.cos(toRadians(angle)) * rad; var y = Math.sin(toRadians(angle)) * rad; draw(xc - x, yc - y, xc + x, yc + y); } }
function update() { start_angle1 += 0.1; start_angle2 -= 0.1; if(radius<radiusmax) radius++; context.clearRect(0, 0, width, height); drawWheel(xcenter, ycenter, start_angle1, 40, radius); drawWheel(xcenter, ycenter, start_angle2, 40, radius); requestAnimationFrame(update); }

update();};
html,body {  margin: 0px;}
canvas { display: block;}
<canvas id="canvas"></canvas>

how to animate drawing lines on canvas

I understand that your want the lines to extend incrementally along the points in your path using animation.

A Demo: http://jsfiddle.net/m1erickson/7faRQ/

You can use this function to calculate waypoints along the path:

// define the path to plot
var vertices=[];
vertices.push({x:0,y:0});
vertices.push({x:300,y:100});
vertices.push({x:80,y:200});
vertices.push({x:10,y:100});
vertices.push({x:0,y:0});

// calc waypoints traveling along vertices
function calcWaypoints(vertices){
var waypoints=[];
for(var i=1;i<vertices.length;i++){
var pt0=vertices[i-1];
var pt1=vertices[i];
var dx=pt1.x-pt0.x;
var dy=pt1.y-pt0.y;
for(var j=0;j<100;j++){
var x=pt0.x+dx*j/100;
var y=pt0.y+dy*j/100;
waypoints.push({x:x,y:y});
}
}
return(waypoints);
}

Then you can use requestAnimationFrame to animate each incremental line segment:

// calculate incremental points along the path
var points=calcWaypoints(vertices);


// variable to hold how many frames have elapsed in the animation
// t represents each waypoint along the path and is incremented in the animation loop
var t=1;


// start the animation
animate();

// incrementally draw additional line segments along the path
function animate(){
if(t<points.length-1){ requestAnimationFrame(animate); }
// draw a line segment from the last waypoint
// to the current waypoint
ctx.beginPath();
ctx.moveTo(points[t-1].x,points[t-1].y);
ctx.lineTo(points[t].x,points[t].y);
ctx.stroke();
// increment "t" to get the next waypoint
t++;
}

Animate a line between two points - swift

The things is, I do not see how the path was actually created using your points p1 and p2

Anyways, I am assuming your end goal is to do a drawing line path animation in a UICollectionViewCell and that is what I tried to achieve based on the given the description in your question.

First the drawing class:

class DrawFunction: NSObject
{
weak var shapeLayer: CAShapeLayer?

// Change as you wish
let duration = 2.0

// Responsible for drawing the lines from any number of points
func drawFooting(points: [CGPoint])
{
guard !points.isEmpty else { return }

// Remove old drawings
shapeLayer?.removeFromSuperlayer()

let path = UIBezierPath()

// This is actual drawing path using the points
// I don't see this in your code
for (index, point) in points.enumerated()
{
// first pair of points
if index == 0
{
// Move to the starting point
path.move(to: point)
continue
}

// Draw a line from the previous point to the current
path.addLine(to: point)
}

// Create a shape layer to visualize the path
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = randomColor().cgColor
shapeLayer.lineWidth = 5
shapeLayer.path = path.cgPath

self.shapeLayer = shapeLayer
}

// Animate function to be called after shape has been drawn
// by specifying the view to show this animation in
func animateShape(in view: UIView)
{
if let shapeLayer = shapeLayer
{
view.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = duration
shapeLayer.add(animation, forKey: "MyAnimation")
}
}

// You can ignore this function, just for convenience
private func randomColor() -> UIColor
{
let red = CGFloat(arc4random_uniform(256)) / 255.0
let blue = CGFloat(arc4random_uniform(256)) / 255.0
let green = CGFloat(arc4random_uniform(256)) / 255.0

return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}
}

Then basic custom cell set up, nothing fancy, just added for completeness

// Basic Cell, nothing unique here
class CategoryCell: UICollectionViewCell
{
static let identifier = "cell"

override init(frame: CGRect)
{
super.init(frame: frame)
configure()
}

required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}

private func configure()
{
contentView.backgroundColor = .lightGray
contentView.layer.cornerRadius = 5.0
contentView.clipsToBounds = true
}
}

The view controller set up where the most interesting parts are in the willDisplay cell function

class LineAnimateVC: UICollectionViewController
{
// Random points to draw lines
let points = [[CGPoint(x: 0.0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 50)],

[CGPoint(x: 0.0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 50),
CGPoint(x: UIScreen.main.bounds.maxX, y: 50)],

[CGPoint(x: 0.0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 50),
CGPoint(x: UIScreen.main.bounds.midX + 40, y: 50),
CGPoint(x: UIScreen.main.bounds.midX + 40, y: UIScreen.main.bounds.maxY),
CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)],

[CGPoint(x: 0.0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 50)],

[CGPoint(x: 0.0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 50),
CGPoint(x: UIScreen.main.bounds.midX + 40, y: 50),
CGPoint(x: UIScreen.main.bounds.midX + 40, y: UIScreen.main.bounds.maxY),
CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)]
]

override func viewDidLoad()
{
super.viewDidLoad()

view.backgroundColor = .white
title = "Line animate"

collectionView.register(CategoryCell.self,
forCellWithReuseIdentifier: CategoryCell.identifier)

collectionView.backgroundColor = .white
}

// Number of cells equals to points we have
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int
{
return points.count
}

override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier,
for: indexPath) as! CategoryCell

return cell
}

// Add animation when cell is about to be displayed
override func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
let cell = cell as! CategoryCell

// Draw the path and perform the animation
let drawingFunction = DrawFunction()
drawingFunction.drawFooting(points: points[indexPath.row])
drawingFunction.animateShape(in: cell.contentView)
}
}

Just for completeness, my flow layout set up

// Basic set up stuff
extension LineAnimateVC: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.frame.width, height: 300)
}

func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
return 20
}
}

This gives me an animated path in the collectionview cell

Draw Path Line Animation UICollectionView UICollectionViewCell CAShapeLayer UIBezierPath CABasicAnimation Animate line between two points CGPoint

Hope this gives you some ideas to achieve your task

Update

Based on OP, Xin Lok's comment:

However still did not get what I want, lets say I have path1 =
[p1,p2,p3,p4,p5] and path2 = [m1,m2,m3], if I run drawFooting(points:
path1) and drawFooting(path2), both of the 2 paths will be animated in
the same time , and this what I don't want, I need to complete
animation for Path1, and then after finish to proceed with animation
of Path2. I tried to insert sleep, but it did not work

Based on that comment, One way I can think of achieving that is to I think the key is to reuse and persist with the shape layer and the path.

Here are some updates I made based on that conclusion

First I just made a simple struct so we can create lines easily

struct Line
{
var points: [CGPoint] = []

init(_ points: [CGPoint])
{
self.points = points
}
}

Then I create some random lines and grouped them in an array

// Random lines
// Random lines
let line1 = Line([CGPoint(x: 0, y: 10),
CGPoint(x: UIScreen.main.bounds.midX, y: 10)])

let line2 = Line([CGPoint(x: 0, y: 70),
CGPoint(x: UIScreen.main.bounds.midX, y: 70),
CGPoint(x: UIScreen.main.bounds.midX, y: 100)])

let line3 = Line([CGPoint(x: 0, y: 150),
CGPoint(x: UIScreen.main.bounds.midX, y: 110),
CGPoint(x: UIScreen.main.bounds.maxX, y: 190)])

let line4 = Line([CGPoint(x: 0, y: 210),
CGPoint(x: UIScreen.main.bounds.maxX / 4, y: 235),
CGPoint(x: UIScreen.main.bounds.maxX * 0.75, y: 220),
CGPoint(x: UIScreen.main.bounds.maxX,
y: UIScreen.main.bounds.maxY)])

var lineGroups: [[Line]] = []

private func setLines()
{
// First cell, it should draw lines in the order 3 -> 1 -> 2
// Second cell, in the order 4 -> 3 -> 2 -> 1
lineGroups = [[line3, line1, line2],
[line4, line3, line2, line1]]
}

Importantly note the line order in each array, because this is the order they will be drawn

In the drawing class, I made some changes to persist the CAShapeLayer and path

A special mention to jrturton in the comments for suggesting CGMutablePath and simplifying the path creation.

class DrawFunction: NSObject
{
weak var shapeLayer: CAShapeLayer?
var path: CGMutablePath?

// Change as you wish
let duration = 5.0

// Responsible for drawing the lines from any number of points
func drawFooting(line: Line)
{
var shapeLayer = CAShapeLayer()

if self.shapeLayer != nil
{
shapeLayer = self.shapeLayer!
}

if path == nil
{
path = CGMutablePath()
}

// Thank you @jrturton for this
path?.addLines(between: line.points)

shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = randomColor().cgColor
shapeLayer.lineWidth = 5
shapeLayer.path = path

self.shapeLayer = shapeLayer
}

// Animate function to be called after shape has been drawn
// by specifying the view to show this animation in
func animateShape(in view: UIView)
{
if let shapeLayer = shapeLayer
{
view.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = duration
shapeLayer.add(animation, forKey: "MyAnimation")
}
}

// You can ignore this function, just for convenience
private func randomColor() -> UIColor
{
let red = CGFloat(arc4random_uniform(256)) / 255.0
let blue = CGFloat(arc4random_uniform(256)) / 255.0
let green = CGFloat(arc4random_uniform(256)) / 255.0

return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}
}

Then some minor changes in the collectionview cell configuration

// Number of cells equals to lines we have
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int
{
return lineGroups.count
}

override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell
= collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier,
for: indexPath) as! CategoryCell

return cell
}

// Add animation when cell is about to be displayed
override func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
let cell = cell as! CategoryCell

let lines = lineGroups[indexPath.item]

// Draw the path and perform the animation
let drawingFunction = DrawFunction()

for line in lines
{
drawingFunction.drawFooting(line: line)
}

drawingFunction.animateShape(in: cell.contentView)
}

Now again, for convenience, remember the order in which they should be drawn:

First cell, it should draw lines in the order 3 -> 1 -> 2
Second cell, in the order 4 -> 3 -> 2 -> 1

The end result:

Animate a line between two CGPoints swift iOS CAAnimation chaining CAShapeLayer CGMutablePath



Related Topics



Leave a reply



Submit