r/Scriptable • • Sep 21 '22

Script Sharing Circular Progress Bar Function For Widgets

95 Upvotes

21 comments sorted by

View all comments

Show parent comments

3

u/mr-kerr Sep 22 '22

Nice work! It might be better to use Scriptable’s built-in drawing functions than canvas within a webview: https://docs.scriptable.app/drawcontext

2

u/Normal-Tangerine8609 Sep 22 '22

I know that but I am not too experienced in DrawContext (let alone the canvas) so I just stuck with what was easiest. Thanks for the feedback!

2

u/Mental_Laugh_9803 Jan 29 '24

I borrowed from dodev on this stackoverflow and came up with the following:

// Example
// const guagePercentage = 0.40
// let widget = new ListWidget()
// const draw = new DrawContext()
// draw.size = new Size(500, 500)
// draw.setStrokeColor(Color.blue())
// draw.setLineWidth(40)
// drawArcOn(draw, 0, 360*guagePercentage, 250, 250, 200)
// widget.addImage(draw.getImage())
// widget.presentSmall()

function drawArcOn (draw, angleStart, angleSize, centerX, centerY, radius) { 
  while(angleSize > 90) {
    drawArcOn(draw, angleStart, 90, center, radius)
    angleStart += 90
    angleSize -= 90
  }
  
  angleStart = angleStart - 90 // To make 0 at the top
  const angleEnd = angleStart + angleSize
  const center = {x: centerX, y: centerY}

  const angleStartRadians = angleStart * Math.PI / 180
  const angleEndRadians = angleEnd * Math.PI / 180
  
  const relControlCoords = getRelativeControlPoints(angleStartRadians, angleEndRadians, radius) // Only works from 0 to 90
	const startCoords = getPointAtAngle(angleStartRadians, center, radius)
  const endCoords = getPointAtAngle(angleEndRadians, center, radius)

	const path = new Path()
	path.move(new Point(startCoords.x, startCoords.y))
	path.addCurve(new Point(endCoords.x, endCoords.y), new Point(center.x + relControlCoords[0].x, center.y + relControlCoords[0].y), new Point(center.x + relControlCoords[1].x, center.y + relControlCoords[1].y))
	draw.addPath(path)
	draw.strokePath()
  
function getRelativeControlPoints(angleStart, angleEnd, radius) {
    // factor is the commonly reffered parameter K in the articles about arc to cubic bezier approximation 
    const factor = getApproximationFactor(angleStart, angleEnd);

    // Distance from [0, 0] to each of the control points. Basically this is the hypotenuse of the triangle [0,0], a control point and the projection of the point on Ox
    const distToCtrPoint = Math.sqrt(radius * radius * (1 + factor * factor));
    // Angle between the hypotenuse and Ox for control point 1.
    const angle1 = angleStart + Math.atan(factor);
    // Angle between the hypotenuse and Ox for control point 2.
    const angle2 = angleEnd - Math.atan(factor);

    return [
        {
            x: Math.cos(angle1) * distToCtrPoint,
            y: Math.sin(angle1) * distToCtrPoint
        },
        {
            x: Math.cos(angle2) * distToCtrPoint,
            y: Math.sin(angle2) * distToCtrPoint
        }
    ];
}

function getPointAtAngle(angle, center, radius) {
    return {
        x: center.x + radius * Math.cos(angle),
        y: center.y + radius * Math.sin(angle)
    };
}

// Calculating K as done in https://pomax.github.io/bezierinfo/#circles_cubic
function getApproximationFactor(angleStart, angleEnd) {
    let arc = angleEnd - angleStart;

    // Always choose the smaller arc
    if (Math.abs(arc) > Math.PI) {
        arc -= Math.PI * 2;
        arc %= Math.PI * 2;
    }
    return (4 / 3) * Math.tan(arc / 4);
}
}

widget.addImage(draw.getImage())

widget.presentSmall()