"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"; } }