288 lines
8.6 KiB
JavaScript
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";
|
|
}
|
|
}
|