Skip to content

Instantly share code, notes, and snippets.

@Nosgoroth
Created November 3, 2024 14:46
Show Gist options
  • Save Nosgoroth/4751f995fa741155c4c4116268ec0de4 to your computer and use it in GitHub Desktop.
Save Nosgoroth/4751f995fa741155c4c4116268ec0de4 to your computer and use it in GitHub Desktop.
Draw concentric ring progress bars for Scriptable widgets
// 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;
@Nosgoroth
Copy link
Author

Usage example:

const image = await ProgressCircle.concentricImage([
	{ offset:0, value: 85, color: rgb(255,0,0), bgcolor:  "#440000" },
	{ offset:1, value: 59, color: rgb(0,255,0), bgcolor:  "#004400" },
	{ offset:2, value: 20, color: rgb(0,0,255), bgcolor:  "#000044" },
]);
listWidget.backgroundImage = image;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment