Created
November 3, 2024 14:46
-
-
Save Nosgoroth/4751f995fa741155c4c4116268ec0de4 to your computer and use it in GitHub Desktop.
Draw concentric ring progress bars for Scriptable widgets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Draws concentric ring progress bars for Scriptable widgets | |
// | |
// Use by calling ProgressCircle.concentricImage() | |
// Parameters: | |
// ringsConfig (arr) An array of plain objects with these values: | |
// offset (int) The ring number starting from the outer edge. First is zero. | |
// value (int) Percentage from 0 to 100 | |
// bgcolor (str) Color for the background ring. This ring is always a circle. | |
// Set to falsy to omit the background ring. | |
// color (str) Color for the foreground ring, whose length will obey the value. | |
// Set to falsy to omit the foreground ring. | |
// lineWidth (int) Optionally override the line width for this single ring | |
// padding (int) Optionally override the ring padding for this single ring | |
// offsetWidth (int) Optionally set the ring width used to calculate the offset | |
// for this ring. Defaults to the lineWidth. | |
// lineWidth (int) Width of the ring. Default 20. | |
// padding (int) Padding between the ring and each edge of the image. Default 30. | |
// ringPadding (int) Padding between rings in each different offset. Default 0. | |
// resolution (int) Width and height of the generated image before padding. Default 300. | |
// retinaMult (int) Multiplier for all pixel sizes given. Default 3. | |
// | |
// Warning: this code contains war crimes. | |
// | |
// Adapted from: https://gist.github.com/Normal-Tangerine8609/0c7101942b3886bafdc08c357b8d3f18 | |
class ProgressCircle { | |
static prepareCanvas(elementId, resolution, padding) { | |
const canvas = document.getElementById(elementId); | |
const c = canvas.getContext('2d'); | |
canvas.width = resolution + 2 * padding; | |
canvas.height = resolution + 2 * padding; | |
c.lineCap = 'round'; | |
return { | |
elementId: elementId, | |
resolution: resolution, | |
padding: padding, | |
canvas: canvas, | |
canvasContext: c, | |
center: { | |
x: canvas.width / 2, | |
y: canvas.height / 2, | |
} | |
}; | |
} | |
static drawRingArc(color, value, ring, options) { | |
if (!color) { | |
return; | |
} | |
options.canvasContext.beginPath(); | |
options.canvasContext.strokeStyle = color; | |
options.canvasContext.lineWidth = ring.lineWidth; | |
options.canvasContext.arc( | |
options.center.x, | |
options.center.y, | |
( //Distance from center to middle of the line (radius) is | |
options.resolution // Total width of the circle | |
- ring.offsetWidth // Less its stroke width | |
- ( // Less as many double line widths as offsets this ring is on | |
2 * ring.offset * ( // Plus ring padding | |
ring.offsetWidth + ring.padding + 1 | |
) | |
) | |
- 1 | |
)/2, // The above gives diameter, so divide by two | |
(Math.PI/180) * 270, (Math.PI/180) * (270 + ((360 / 100) * value)) | |
); | |
options.canvasContext.stroke(); | |
} | |
static drawRing(ring, options) { | |
this.drawRingArc(ring.bgcolor, 100, ring, options); | |
this.drawRingArc(ring.color, ring.value, ring, options); | |
} | |
static async concentricImage( | |
ringsConfig, | |
lineWidth = 20, | |
padding = 30, | |
ringPadding = 0, | |
resolution = 300, | |
retinaMult = 3, | |
) { | |
for (let i=0; i<ringsConfig.length; i++) { | |
let value = ringsConfig[i].value; | |
if (value > 1) { value /= 100; } | |
if (value < 0) { value = 0; } | |
if (value > 1) { value = 1; } | |
ringsConfig[i].valueCorrected = value; | |
} | |
const w = new WebView(); | |
await w.loadHTML('<canvas id="c"></canvas>'); | |
const base64 = await w.evaluateJavaScript(` | |
// Converting the current class to string and evaluating it | |
// in the WebView context lmao | |
${ this.toString() } | |
// Assigning the class as a new variable so the code is | |
// independent of the original name of the class lmao | |
const ThisClass = ${this.name}; | |
const retinaMult = ${retinaMult}; | |
const resolution = ${resolution} * retinaMult; | |
const lineWidth = ${lineWidth} * retinaMult; | |
const padding = ${padding} * retinaMult; | |
const ringPadding = ${ringPadding} * retinaMult; | |
const ringsConfig = ${JSON.stringify(ringsConfig)}; | |
const options = ThisClass.prepareCanvas('c', resolution, padding) | |
for (let i=0; i<ringsConfig.length; i++) { | |
const ring = ringsConfig[i]; | |
ring.lineWidth = ring.lineWidth ? ring.lineWidth * retinaMult : lineWidth; | |
ring.padding = ring.padding ? ring.padding * retinaMult : ringPadding; | |
ring.offsetWidth = ring.offsetWidth ? ring.offsetWidth * retinaMult : ring.lineWidth; | |
ThisClass.drawRing(ring, options); | |
} | |
completion( | |
options.canvas.toDataURL().replace("data:image/png;base64,","") | |
);`, | |
true | |
); | |
return Image.fromData(Data.fromBase64String(base64)); | |
} | |
} | |
module.exports.ProgressCircle = ProgressCircle; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage example: