PCBGG/script.js
Alexander Bass de9b6979a7 init
2023-09-19 09:14:34 -04:00

288 lines
8.6 KiB
JavaScript

"use strict";
var canvas, ctx;
// Not jQuery
function $(id) {
return document.getElementById(id);
}
var downloadCount = 0;
function downloadImage() {
const link = document.createElement("a");
link.download = `image${downloadCount}.png`;
link.href = $("viewport").toDataURL();
link.click();
downloadCount++;
}
function getDrawingParameters() {
let angle = Number($("angle").value);
const bandCount = Number($("band-count").value);
// Convert to radians
angle = angle * (Math.PI / 180);
const start = $("starting-color");
const end = $("ending-color");
const dynamicWidth = $("dynamic-width").checked;
const startingColor = parseHexColor(start.value);
const endingColor = parseHexColor(end.value);
return { startingColor, endingColor, dynamicWidth, bandCount, angle };
}
// Setup, and register events
document.addEventListener("DOMContentLoaded", () => {
// Check URL for parameters
parseURLParams();
canvas = $("viewport");
ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.imageSmoothingEnabled = false;
const watchIds = [
"starting-color",
"ending-color",
"band-count",
"band-count-number",
"angle",
"angle-number",
"dynamic-width",
];
const LinkedInputs = [
["angle", "angle-number"],
["band-count", "band-count-number"],
];
// Link inputs
LinkedInputs.forEach((ids) => {
const [inputA, inputB] = ids.map((id) => $(id));
inputB.value = inputA.value;
inputA.addEventListener("input", () => {
inputB.value = inputA.value;
});
inputB.addEventListener("input", () => {
inputA.value = inputB.value;
});
});
// Watch inputs
watchIds.forEach((id) => {
$(id).addEventListener("input", () => {
drawParamUpdated();
});
});
//Watch random color button
$("randomize-colors").addEventListener("click", () => {
$("starting-color").value = randomHexColor();
$("ending-color").value = randomHexColor();
drawParamUpdated();
});
// Watch download button
$("download-image").addEventListener("click", () => {
downloadImage();
});
// Watch copy link button
$("copy-link").addEventListener("click", () => {
const url = encodeShareURL();
const linkCopyIndicator = $("link-copy-indicator");
linkCopyIndicator.textContent = url;
linkCopyIndicator.href = url;
$("link-copy-infobox").style.display = "";
navigator.clipboard.writeText(url);
});
drawImage(getDrawingParameters());
});
function drawParamUpdated() {
$("link-copy-infobox").style.display = "none";
drawImage(getDrawingParameters());
}
function drawImage({
startingColor,
endingColor,
dynamicWidth,
bandCount,
angle,
}) {
const canvasWidth = canvas.width;
let drawWidth;
// Rotating the bands will leave unfilled space in the corners.
// Optionally, all bands can be scaled such that the entire space is filled with bands evenly.
if (!dynamicWidth) {
drawWidth = canvasWidth;
} else if (angle < 0) {
drawWidth = canvasWidth * Math.cos(angle + Math.PI / 4) * 2 * Math.SQRT1_2;
} else {
drawWidth = canvasWidth * Math.sin(angle + Math.PI / 4) * 2 * Math.SQRT1_2;
}
const stepIncrementWidth = drawWidth / bandCount;
const incrementColor = {
red: (endingColor.red - startingColor.red) / (bandCount - 1),
green: (endingColor.green - startingColor.green) / (bandCount - 1),
blue: (endingColor.blue - startingColor.blue) / (bandCount - 1),
};
const h_w = drawWidth / 2;
const rotate = newRotator(angle);
// Draw a bunch of rectangular bands.
// Each rectangle is defined to be vertical, and further to the right than the last.
// Rotation of the rectangles is taken care of separately.
for (let bandNo = 0; bandNo < bandCount; bandNo++) {
const red = startingColor.red + incrementColor.red * bandNo;
const green = startingColor.green + incrementColor.green * bandNo;
const blue = startingColor.blue + incrementColor.blue * bandNo;
ctx.fillStyle = `rgb(${red},${green},${blue})`;
// `magic` is added to the points to make the bands continue on out when dynamicWidth is disabled
// magic was found by trial and error.
// I reckon there's a more mathematical method to find it, but 4.75 seems to fill the screen for all cases.
const magic = 4.75 * canvasWidth;
const point1 = [bandNo * stepIncrementWidth - h_w - 1, -h_w - magic];
const point2 = [(bandNo + 1) * stepIncrementWidth - h_w, -h_w - magic];
const point3 = [(bandNo + 1) * stepIncrementWidth - h_w, h_w + magic];
const point4 = [bandNo * stepIncrementWidth - h_w - 1, h_w + magic];
// If dynamic width is disabled, make the first and last bands wider such that no black space is left
if (!dynamicWidth) {
// magic2 is again a magic number found by trial and error. I wasn't in a mood for trigonometry on the day I wrote this.
const magic2 = canvasWidth * 4.85;
if (bandNo === 0) {
point1[0] -= magic2;
point4[0] -= magic2;
} else if (bandNo === bandCount - 1) {
point2[0] += magic2;
point2[0] += magic2;
}
}
// Draw polygon with above points, rotated
ctx.beginPath();
ctx.moveTo(...rotate(point1));
ctx.lineTo(...rotate(point2));
ctx.lineTo(...rotate(point3));
ctx.lineTo(...rotate(point4));
ctx.closePath();
ctx.fill();
}
}
// Vector rotation but with cached sine and cosine values to greatly improve performance
function newRotator(angleRad) {
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
// Rotation using matrix math
// https://en.wikipedia.org/wiki/Rotation_matrix
return function rotate([x, y]) {
return [x * cos - y * sin, x * sin + y * cos];
};
}
// Take a hexadecimal color input like "#abcd61" and convert it into Red, Green, and Blue color values
function parseHexColor(hexColorStr) {
if (hexColorStr.length !== 7) return false;
if (hexColorStr.substr(0, 1) !== "#") return false;
// Remove hash from beginning of string
const hexDigits = hexColorStr.substr(1, 6);
// Isolate colors into substrings
const redDigits = hexDigits.substr(0, 2);
const greenDigits = hexDigits.substr(2, 2);
const blueDigits = hexDigits.substr(4, 2);
// Convert colors to numbers, from hexadecimal
const red = parseInt(redDigits, 16);
const green = parseInt(greenDigits, 16);
const blue = parseInt(blueDigits, 16);
return { red, green, blue };
}
function randomHexColor() {
const randomHex = () =>
Math.round(Math.random() * 255)
.toString(16)
.padStart(2, "0");
return `#${randomHex()}${randomHex()}${randomHex()}`;
}
function RGBtoHexColor({ red, green, blue }) {
return `#${red.toString(16).padStart(2)}${green
.toString(16)
.padStart(2)}${blue.toString(16).padStart(2)}`;
}
function encodeShareURL() {
const dParams = getDrawingParameters();
// Convert drawing parameters into efficient, URL encodable form
// Substring to remove the initial '#'
dParams.startingColor = RGBtoHexColor(dParams.startingColor).substring(1, 7);
dParams.endingColor = RGBtoHexColor(dParams.endingColor).substring(1, 7);
dParams.dynamicWidth = dParams.dynamicWidth ? 1 : 0;
dParams.angle *= 180 / Math.PI;
dParams.angle = Math.round(dParams.angle * 100) / 100;
const URLparameters = `c1=${dParams.startingColor}&c2=${dParams.endingColor}&a=${dParams.angle}&b=${dParams.bandCount}&d=${dParams.dynamicWidth}`;
const fullURL = new URL(window.origin);
fullURL.pathname = location.pathname;
fullURL.search = URLparameters;
return fullURL;
}
function parseURLParams() {
const urlSearchSlug = window.location.search;
const fullParameterString = urlSearchSlug.split("?")[1];
if (fullParameterString === undefined) {
return;
}
const parameterMap = {};
fullParameterString
.split("&")
.map((p) => p.split("="))
.forEach((p) => {
if (p.length == 2) {
parameterMap[p[0]] = p[1];
}
});
const parameterCount = Object.keys(parameterMap).length;
if (parameterCount === 0) {
return;
}
console.log(`Parsed ${parameterCount}, URL parameter(s)`, parameterMap);
if ("a" in parameterMap) {
const value = parseInt(parameterMap.a, 10);
if (!isNaN(value)) {
$("angle").value = value;
}
}
if ("b" in parameterMap) {
const value = parseInt(parameterMap.b, 10);
if (!isNaN(value)) {
$("band-count").value = value;
}
}
if ("c1" in parameterMap) {
$("starting-color").value = `#${parameterMap.c1}`;
}
if ("c2" in parameterMap) {
$("ending-color").value = `#${parameterMap.c2}`;
}
if ("d" in parameterMap) {
const dynamicWidthCheckbox = $("dynamic-width");
dynamicWidthCheckbox.checked = parameterMap.d === "1";
}
}