diff --git a/extensions/LincolnX/colors.js b/extensions/LincolnX/colors.js
new file mode 100644
index 0000000000..c5b5600fe6
--- /dev/null
+++ b/extensions/LincolnX/colors.js
@@ -0,0 +1,1073 @@
+// Name: Colors
+// ID: lxColors
+// Description: Adds a variety of new blocks that manipulates and utilizes colors.
+// By: LincolnX
+// License: MPL-2.0
+
+(function (Scratch) {
+ "use strict";
+
+ const { abs, round, floor, sqrt } = Math;
+
+ const toDec = (hex) => parseInt(hex, 16);
+ const toHex = (dec) => dec.toString(16);
+ const limitHex = (hex, mi, ma) =>
+ toHex(Math.min(Math.max(toDec(hex), mi), ma));
+ const clamp = (n, mi, ma) => Math.min(Math.max(n, mi), ma);
+ const fixHex = (hex) => limitHex(hex, 0, 255).padStart(2, "0");
+ const toFixHex = (dec) => fixHex(dec.toString(16));
+ const lerp = (a, b, t) => a + (b - a) * t;
+ function interpolateHexColorsHsv(hex1, hex2, t) {
+ const hsv1 = hexToHsv(hex1);
+ const hsv2 = hexToHsv(hex2);
+ t = clamp(t, 0, 1);
+ // Handle hue interpolation for the shortest path around the color wheel
+ let h1 = hsv1.h;
+ let h2 = hsv2.h;
+ let hueDiff = h2 - h1;
+ if (hueDiff > 180) {
+ h1 += 360;
+ } else if (hueDiff < -180) {
+ h2 += 360;
+ }
+
+ const h = h1 + t * (h2 - h1);
+ const s = hsv1.s + t * (hsv2.s - hsv1.s);
+ const v = hsv1.v + t * (hsv2.v - hsv1.v);
+
+ return hsvToHex({ h, s, v });
+ }
+
+ function hexToRgb(hex) {
+ let r = 0,
+ g = 0,
+ b = 0;
+ // Handle 3-digit shorthand
+ if (hex.length === 4) {
+ r = parseInt(hex[1] + hex[1], 16);
+ g = parseInt(hex[2] + hex[2], 16);
+ b = parseInt(hex[3] + hex[3], 16);
+ } else if (hex.length === 7) {
+ r = parseInt(hex.substring(1, 3), 16);
+ g = parseInt(hex.substring(3, 5), 16);
+ b = parseInt(hex.substring(5, 7), 16);
+ }
+ return { r, g, b };
+ }
+ function rgbToHex(rgb) {
+ const makeHex = (c) => Math.round(c).toString(16).padStart(2, "0");
+ return `#${makeHex(rgb.r)}${makeHex(rgb.g)}${makeHex(rgb.b)}`;
+ }
+ function hsvToRgb(h, s, v) {
+ var r, g, b, i, f, p, q, t;
+ if (arguments.length === 1) {
+ ((s = h.s), (v = h.v), (h = h.h));
+ }
+ h /= 360;
+ s /= 100;
+ v /= 100;
+ i = floor(h * 6);
+ f = h * 6 - i;
+ p = v * (1 - s);
+ q = v * (1 - f * s);
+ t = v * (1 - (1 - f) * s);
+ switch (i % 6) {
+ case 0:
+ ((r = v), (g = t), (b = p));
+ break;
+ case 1:
+ ((r = q), (g = v), (b = p));
+ break;
+ case 2:
+ ((r = p), (g = v), (b = t));
+ break;
+ case 3:
+ ((r = p), (g = q), (b = v));
+ break;
+ case 4:
+ ((r = t), (g = p), (b = v));
+ break;
+ case 5:
+ ((r = v), (g = p), (b = q));
+ break;
+ }
+ return {
+ r: round(r * 255),
+ g: round(g * 255),
+ b: round(b * 255),
+ };
+ }
+ function rgbToHsv(r, g, b) {
+ if (arguments.length === 1) {
+ ((g = r.g), (b = r.b), (r = r.r));
+ }
+ var max = Math.max(r, g, b),
+ min = Math.min(r, g, b),
+ d = max - min,
+ h,
+ s = max === 0 ? 0 : d / max,
+ v = max / 255;
+ switch (max) {
+ case min:
+ h = 0;
+ break;
+ case r:
+ h = g - b + d * (g < b ? 6 : 0);
+ h /= 6 * d;
+ break;
+ case g:
+ h = b - r + d * 2;
+ h /= 6 * d;
+ break;
+ case b:
+ h = r - g + d * 4;
+ h /= 6 * d;
+ break;
+ }
+ return {
+ h: h * 360,
+ s: s * 100,
+ v: v * 100,
+ };
+ }
+ const hexToHsv = (hex) => rgbToHsv(hexToRgb(hex));
+ function hsvToHex(h, s, v) {
+ if (arguments.length === 1) {
+ ((s = h.s), (v = h.v), (h = h.h));
+ }
+ return rgbToHex(hsvToRgb(h, s, v));
+ }
+
+ function hslToRgb(h, s, l) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ let r, g, b;
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = hueToRgb(p, q, h + 1 / 3);
+ g = hueToRgb(p, q, h);
+ b = hueToRgb(p, q, h - 1 / 3);
+ }
+ return { r: round(r * 255), g: round(g * 255), b: round(b * 255) };
+ }
+
+ function hueToRgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ }
+
+ function channelToLinear(c) {
+ c /= 255;
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
+ }
+
+ function relativeLuminance(hex) {
+ const { r, g, b } = hexToRgb(hex);
+ const R = channelToLinear(r);
+ const G = channelToLinear(g);
+ const B = channelToLinear(b);
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
+ }
+
+ function contrastRatio(hex1, hex2) {
+ const L1 = relativeLuminance(hex1);
+ const L2 = relativeLuminance(hex2);
+ const light = Math.max(L1, L2);
+ const dark = Math.min(L1, L2);
+ return (light + 0.05) / (dark + 0.05);
+ }
+
+ function distanceBetweenHexColorsDeltaE2000(hex1, hex2) {
+ // convert Hex to RGB
+ const rgb1 = hexToRgb(hex1);
+ const rgb2 = hexToRgb(hex2);
+
+ // convert RGB to XYZ
+ const xyz1 = rgbToXyz(rgb1.r, rgb1.g, rgb1.b);
+ const xyz2 = rgbToXyz(rgb2.r, rgb2.g, rgb2.b);
+
+ // convert XYZ to Lab
+ const lab1 = xyzToLab(xyz1.x, xyz1.y, xyz1.z);
+ const lab2 = xyzToLab(xyz2.x, xyz2.y, xyz2.z);
+
+ // calculate Delta E 2000
+ return deltaE2000(lab1, lab2);
+ }
+
+ function rgbToXyz(r, g, b) {
+ r /= 255;
+ g /= 255;
+ b /= 255;
+
+ r = r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
+ g = g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
+ b = b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;
+
+ r *= 100;
+ g *= 100;
+ b *= 100;
+
+ // D65 white point reference primaries
+ const x = r * 0.4124 + g * 0.3576 + b * 0.1805;
+ const y = r * 0.2126 + g * 0.7152 + b * 0.0722;
+ const z = r * 0.0193 + g * 0.1192 + b * 0.9505;
+
+ return { x, y, z };
+ }
+
+ // converts XYZ values to CIELAB color space.
+ function xyzToLab(x, y, z) {
+ // D65 white point values
+ const refX = 95.047;
+ const refY = 100.0;
+ const refZ = 108.883;
+
+ x /= refX;
+ y /= refY;
+ z /= refZ;
+
+ x = x > 0.008856 ? x ** (1 / 3) : 7.787 * x + 16 / 116;
+ y = y > 0.008856 ? y ** (1 / 3) : 7.787 * y + 16 / 116;
+ z = z > 0.008856 ? z ** (1 / 3) : 7.787 * z + 16 / 116;
+
+ const L = 116 * y - 16;
+ const a = 500 * (x - y);
+ const b = 200 * (y - z);
+
+ return { L, a, b };
+ }
+
+ // calculates the Delta E 2000 difference between two Lab colors.
+ // based on the implementation notes from Sharma et al..
+ function deltaE2000(lab1, lab2) {
+ const kL = 1.0,
+ kC = 1.0,
+ kH = 1.0; // parametric factors often set to 1.0
+ const deg2rad = Math.PI / 180;
+ const rad2deg = 180 / Math.PI;
+
+ // extract Lab values
+ const L1 = lab1.L,
+ a1 = lab1.a,
+ b1 = lab1.b;
+ const L2 = lab2.L,
+ a2 = lab2.a,
+ b2 = lab2.b;
+
+ // calculate C*ab values (Chroma)
+ const C1 = sqrt(a1 * a1 + b1 * b1);
+ const C2 = sqrt(a2 * a2 + b2 * b2);
+ const CBar = (C1 + C2) / 2.0;
+
+ // calculate G (chroma correction factor)
+ const CBarPow7 = CBar ** 7;
+ const G = 0.5 * (1 - sqrt(CBarPow7 / (CBarPow7 + 6103515625.0))); // 6103515625 = 25^7
+
+ // calculate a' and C' (lightness corrected a* and new chroma)
+ const a1Prime = a1 * (1 + G);
+ const a2Prime = a2 * (1 + G);
+ const C1Prime = sqrt(a1Prime * a1Prime + b1 * b1);
+ const C2Prime = sqrt(a2Prime * a2Prime + b2 * b2);
+ const CBarPrime = (C1Prime + C2Prime) / 2.0;
+ const DeltaCPrime = C2Prime - C1Prime;
+
+ // calculate h' (hue angle)
+ const h1Prime = Math.atan2(b1, a1Prime) * rad2deg;
+ const h2Prime = Math.atan2(b2, a2Prime) * rad2deg;
+
+ // normalize hue angles to 0-360 range
+ const normalizedH1 = h1Prime >= 0 ? h1Prime : h1Prime + 360;
+ const normalizedH2 = h2Prime >= 0 ? h2Prime : h2Prime + 360;
+
+ // calculate Delta h' (hue difference) and Delta H' (weighted hue difference)
+ let DeltaHPrime;
+ if (C1Prime * C2Prime === 0) {
+ DeltaHPrime = 0; // if one chroma is zero, hue difference is meaningless.
+ } else if (abs(normalizedH1 - normalizedH2) <= 180) {
+ DeltaHPrime = normalizedH2 - normalizedH1;
+ } else if (normalizedH2 - normalizedH1 > 180) {
+ DeltaHPrime = normalizedH2 - normalizedH1 - 360;
+ } else {
+ // (h2 - h1) < -180
+ DeltaHPrime = normalizedH2 - normalizedH1 + 360;
+ }
+
+ // convert Delta H' to a metric difference (ΔH')
+ const DeltaSmallHPrime =
+ 2 * sqrt(C1Prime * C2Prime) * Math.sin((DeltaHPrime * deg2rad) / 2.0);
+
+ // calculate Delta L'
+ const DeltaLPrime = L2 - L1;
+
+ // calculate Average H' (HBarPrime)
+ let HBarPrime;
+ if (C1Prime * C2Prime === 0) {
+ HBarPrime = normalizedH1 + normalizedH2; // use sum as average if one is indeterminate
+ } else if (abs(normalizedH1 - normalizedH2) > 180) {
+ if (normalizedH1 + normalizedH2 < 360) {
+ HBarPrime = (normalizedH1 + normalizedH2 + 360) / 2.0;
+ } else {
+ HBarPrime = (normalizedH1 + normalizedH2 - 360) / 2.0;
+ }
+ } else {
+ HBarPrime = (normalizedH1 + normalizedH2) / 2.0;
+ }
+
+ // calculate T (hue weighting function)
+ const T =
+ 1.0 -
+ 0.17 * Math.cos((HBarPrime - 30.0) * deg2rad) +
+ 0.24 * Math.cos(2.0 * HBarPrime * deg2rad) +
+ 0.32 * Math.cos((3.0 * HBarPrime + 6.0) * deg2rad) -
+ 0.2 * Math.cos((4.0 * HBarPrime - 63.0) * deg2rad);
+
+ // calculate SL, SC, SH (weighting functions)
+ const SL =
+ 1.0 +
+ (0.015 * (HBarPrime - 27.5) ** 2) / (20.0 + (HBarPrime - 27.5) ** 2);
+ const SC = 1.0 + 0.045 * CBarPrime;
+ const SH = 1.0 + 0.015 * CBarPrime * T;
+
+ // calculate RT (rotation term)
+ const CBarPrimePow7 = CBarPrime ** 7;
+ const RT =
+ -2.0 *
+ Math.sin(HBarPrime * deg2rad) *
+ sqrt(CBarPrimePow7 / (CBarPrimePow7 + 6103515625.0));
+
+ // calculate the final Delta E 2000 value
+ const deltaE = sqrt(
+ (DeltaLPrime / (kL * SL)) ** 2 +
+ (DeltaCPrime / (kC * SC)) ** 2 +
+ (DeltaSmallHPrime / (kH * SH)) ** 2 +
+ RT * (DeltaCPrime / (kC * SC)) * (DeltaSmallHPrime / (kH * SH))
+ );
+
+ return deltaE;
+ }
+
+ const _spectrumIcon =
+ "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1NS42NTIwNCIgaGVpZ2h0PSI1NS42NTIwNCIgdmlld0JveD0iMCwwLDU1LjY1MjA0LDU1LjY1MjA0Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgeDE9IjI0Mi4wNjU4NSIgeTE9IjE1Mi4zNjgwMSIgeDI9IjIzNy45MzQxOSIgeTI9IjIwNy42MzE5OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0xIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMzMzNhZmYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMzNhZmYiIHN0b3Atb3BhY2l0eT0iMCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHgxPSIyMTIuMzY4MDIiIHkxPSIxNzcuOTM0MTciIHgyPSIyNjcuNjMxOTkiIHkyPSIxODIuMDY1ODMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iY29sb3ItMiI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzZmZjMzIiBzdG9wLW9wYWNpdHk9IjAuNzAxOTYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNmZmMzMiIHN0b3Atb3BhY2l0eT0iMCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMTIuMTc0LC0xNTIuMTczOTgpIj48ZyBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiPjxwYXRoIGQ9Ik0yMTIuMjkwOTEsMTgwYzAsLTE1LjMwMzMyIDEyLjQwNTc5LC0yNy43MDkxMSAyNy43MDkxMSwtMjcuNzA5MTFjMTUuMzAzMzIsMCAyNy43MDkxMSwxMi40MDU3OSAyNy43MDkxMSwyNy43MDkxMWMwLDE1LjMwMzMyIC0xMi40MDU3OSwyNy43MDkxMSAtMjcuNzA5MTEsMjcuNzA5MTFjLTE1LjMwMzMyLDAgLTI3LjcwOTExLC0xMi40MDU3OSAtMjcuNzA5MTEsLTI3LjcwOTExeiIgZmlsbD0iI2ZmMzMzMyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9Ik5hTiIvPjxwYXRoIGQ9Ik0yMTIuMzY4MDMsMTc3LjkzNDE3YzEuMTQwOTMsLTE1LjI2MDczIDE0LjQzNzEsLTI2LjcwNzA5IDI5LjY5NzgyLC0yNS41NjYxNmMxNS4yNjA3MywxLjE0MDkzIDI2LjcwNzA5LDE0LjQzNzEgMjUuNTY2MTYsMjkuNjk3ODJjLTEuMTQwOTMsMTUuMjYwNzMgLTE0LjQzNzEsMjYuNzA3MDkgLTI5LjY5NzgyLDI1LjU2NjE2Yy0xNS4yNjA3MywtMS4xNDA5MyAtMjYuNzA3MDksLTE0LjQzNzEgLTI1LjU2NjE2LC0yOS42OTc4MnoiIGZpbGw9InVybCgjY29sb3ItMSkiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSJOYU4iLz48cGF0aCBkPSJNMjM3LjkzNDE5LDIwNy42MzJjLTE1LjI2MDcyLC0xLjE0MDkzIC0yNi43MDcwOSwtMTQuNDM3MSAtMjUuNTY2MTYsLTI5LjY5NzgzYzEuMTQwOTMsLTE1LjI2MDczIDE0LjQzNzEsLTI2LjcwNzA5IDI5LjY5NzgzLC0yNS41NjYxN2MxNS4yNjA3MywxLjE0MDkzIDI2LjcwNzA5LDE0LjQzNzEgMjUuNTY2MTYsMjkuNjk3ODNjLTEuMTQwOTMsMTUuMjYwNzMgLTE0LjQzNzEsMjYuNzA3MDkgLTI5LjY5NzgzLDI1LjU2NjE3eiIgZmlsbD0idXJsKCNjb2xvci0yKSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9Ik5hTiIvPjxwYXRoIGQ9Ik0yMTMuNDI0LDE4MGMwLC0xNC42Nzc1MyAxMS44OTg0OSwtMjYuNTc2MDIgMjYuNTc2MDIsLTI2LjU3NjAyYzE0LjY3NzUzLDAgMjYuNTc2MDIsMTEuODk4NDkgMjYuNTc2MDIsMjYuNTc2MDJjMCwxNC42Nzc1MyAtMTEuODk4NDksMjYuNTc2MDIgLTI2LjU3NjAyLDI2LjU3NjAyYy0xNC42Nzc1MywwIC0yNi41NzYwMiwtMTEuODk4NDkgLTI2LjU3NjAyLC0yNi41NzYwMnoiIGZpbGw9Im5vbmUiIHN0cm9rZS1vcGFjaXR5PSIwLjQ0MzE0IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMi41Ii8+PC9nPjwvZz48L3N2Zz4=";
+ const blockIcon =
+ "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNi41ODk4NSIgaGVpZ2h0PSIxNi41ODk4NSIgdmlld0JveD0iMCwwLDE2LjU4OTg1LDE2LjU4OTg1Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjMxLjcwNTA4LC0xNzEuNzA1MDYpIj48ZyBzdHJva2Utd2lkdGg9IjAuNjI1IiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiPjxwYXRoIGQ9Ik0yMzIuMDQ1LDE4M2MwLC0yLjQ4NTI4IDIuMDE0NzIsLTQuNSA0LjUsLTQuNWMyLjQ4NTI4LDAgNC41LDIuMDE0NzIgNC41LDQuNWMwLDIuNDg1MjggLTIuMDE0NzIsNC41IC00LjUsNC41Yy0yLjQ4NTI4LDAgLTQuNSwtMi4wMTQ3MiAtNC41LC00LjV6IiBmaWxsPSIjNGM5N2ZmIiBzdHJva2U9IiMzMzczY2MiLz48cGF0aCBkPSJNMjQwLjA0NSwxNzIuNWMyLjQ4NTI4LDAgNC41LDIuMDE0NzIgNC41LDQuNWMwLDIuNDg1MjggLTIuMDE0NzIsNC41IC00LjUsNC41Yy0yLjQ4NTI4LDAgLTQuNSwtMi4wMTQ3MiAtNC41LC00LjVjMCwtMi40ODUyOCAyLjAxNDcyLC00LjUgNC41LC00LjV6IiBmaWxsPSIjZmY2NjgwIiBzdHJva2U9IiNmZjMzNTUiLz48cGF0aCBkPSJNMjQzLjQ1NSwxNzguNWMyLjQ4NTI4LDAgNC41LDIuMDE0NzIgNC41LDQuNWMwLDIuNDg1MjggLTIuMDE0NzIsNC41IC00LjUsNC41Yy0yLjQ4NTI4LDAgLTQuNSwtMi4wMTQ3MiAtNC41LC00LjVjMCwtMi40ODUyOCAyLjAxNDcyLC00LjUgNC41LC00LjV6IiBmaWxsPSIjZmZiZjAwIiBzdHJva2U9IiNjYzk5MDAiLz48cGF0aCBkPSJNMjMxLjcwNTA5LDE4OC4yOTQ5MnYtMTYuNTg5ODVoMTYuNTg5ODV2MTYuNTg5ODV6IiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48cGF0aCBkPSJNMjQzLjQ1NSwxNzguNWMyLjQ4NTI4LDAgNC41LDIuMDE0NzIgNC41LDQuNWMwLDIuNDg1MjggLTIuMDE0NzIsNC41IC00LjUsNC41Yy0yLjQ4NTI4LDAgLTQuNSwtMi4wMTQ3MiAtNC41LC00LjVjMCwtMi40ODUyOCAyLjAxNDcyLC00LjUgNC41LC00LjV6IiBmaWxsLW9wYWNpdHk9IjAuNTAxOTYiIGZpbGw9IiNmZmJmMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjUwMTk2IiBzdHJva2U9IiNjYzk5MDAiLz48cGF0aCBkPSJNMjMyLjA0NSwxODNjMCwtMi40ODUyOCAyLjAxNDcyLC00LjUgNC41LC00LjVjMi40ODUyOCwwIDQuNSwyLjAxNDcyIDQuNSw0LjVjMCwyLjQ4NTI4IC0yLjAxNDcyLDQuNSAtNC41LDQuNWMtMi40ODUyOCwwIC00LjUsLTIuMDE0NzIgLTQuNSwtNC41eiIgZmlsbC1vcGFjaXR5PSIwLjUwMTk2IiBmaWxsPSIjNGQ5N2ZmIiBzdHJva2Utb3BhY2l0eT0iMC41MDE5NiIgc3Ryb2tlPSIjMzM3M2NjIi8+PHBhdGggZD0iTTI0MC4wNDUsMTcyLjVjMi40ODUyOCwwIDQuNSwyLjAxNDcyIDQuNSw0LjVjMCwyLjQ4NTI4IC0yLjAxNDcyLDQuNSAtNC41LDQuNWMtMi40ODUyOCwwIC00LjUsLTIuMDE0NzIgLTQuNSwtNC41YzAsLTIuNDg1MjggMi4wMTQ3MiwtNC41IDQuNSwtNC41eiIgZmlsbC1vcGFjaXR5PSIwLjUwMTk2IiBmaWxsPSIjZmY2NjgwIiBzdHJva2Utb3BhY2l0eT0iMC41MDE5NiIgc3Ryb2tlPSIjZmYzMzU1Ii8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6OC4yOTQ5MTUwMDAwMDAwMDM6OC4yOTQ5MzUwMDAwMDAwMS0tPg==";
+ class Colors {
+ getInfo() {
+ return {
+ id: "lxColors",
+ name: Scratch.translate("Colors"),
+ color1: "#f94c97",
+ menuIconURI: blockIcon,
+ blocks: [
+ {
+ opcode: "newColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("new color [COL]"),
+ arguments: {
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ },
+ },
+ {
+ opcode: "newColorRGB",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("from RGB [R] [G] [B]"),
+ arguments: {
+ R: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "164",
+ },
+ G: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "94",
+ },
+ B: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "255",
+ },
+ },
+ },
+ {
+ opcode: "newColorHSV",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("from HSV [H] [S] [V]"),
+ arguments: {
+ H: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "266",
+ },
+ S: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "63",
+ },
+ V: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "100",
+ },
+ },
+ },
+ {
+ opcode: "newColorHSL",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("from HSL [H] [S] [L]"),
+ arguments: {
+ H: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "266",
+ },
+ S: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "100",
+ },
+ L: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "68",
+ },
+ },
+ },
+ {
+ opcode: "newColorDecimal",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate({
+ default: "from decimal [DEC]",
+ description: "From decimal - as in the base system",
+ }),
+ arguments: {
+ DEC: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "10772223",
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "randomColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("random color"),
+ disableMonitor: true,
+ },
+ "---",
+ {
+ opcode: "additiveBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("[COL1] + [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "subtractiveBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("[COL1] - [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "multiplicativeBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("[COL1] * [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "divisingBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("[COL1] / [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "differenceBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("difference of [COL1] - [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "screenBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("screen [COL1] * [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "overlayBlend",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("overlay [COL1] * [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "invertColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("invert [COL]"),
+ arguments: {
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ },
+ },
+ {
+ opcode: "contrastColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate({
+ default: "contrast [COL] by [NUM]",
+ description:
+ "Contrast - as a verb, comparing to highlight differences",
+ }),
+ arguments: {
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ NUM: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "0.5",
+ },
+ },
+ },
+ {
+ opcode: "grayscaleColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("grayscale [COL]"),
+ arguments: {
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ },
+ },
+ {
+ opcode: "percentWhite",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("[NUM] % white"),
+ arguments: {
+ NUM: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "75",
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "distanceBetweenColors",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("distance between [COL1] and [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "contrastRatioOfColors",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("contrast ratio of [COL1] and [COL2]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ },
+ },
+ {
+ opcode: "nearEqualColors",
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: Scratch.translate("[COL1] ≈ [COL2] threshold [THR]"),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ THR: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "25",
+ },
+ },
+ },
+ {
+ opcode: "colorFollowsWCAG",
+ blockType: Scratch.BlockType.BOOLEAN,
+ text: Scratch.translate(
+ "does [COL1] and [COL2] follow [AAA] for [TXT] text"
+ ),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ AAA: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "WCAG_MENU",
+ },
+ TXT: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "TEXTWCAG_SIZE_MENU",
+ },
+ },
+ hideFromPalette: true,
+ },
+ "---",
+ {
+ opcode: "interpolateColors",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate(
+ "interpolate [COL1] to [COL2] ratio [RATIO] using [SPACE]"
+ ),
+ arguments: {
+ COL1: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ COL2: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#eb57ab",
+ },
+ RATIO: {
+ type: Scratch.ArgumentType.NUMBER,
+ defaultValue: "0.5",
+ },
+ SPACE: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "SPACE_MENU",
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "getChannelFromColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("get [CHN] of [COL]"),
+ arguments: {
+ CHN: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "CHANNEL_MENU",
+ },
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ },
+ },
+ {
+ opcode: "setChannelOfColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("set [CHN] of [COL] to [SET]"),
+ arguments: {
+ CHN: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "CHANNEL_MENU",
+ },
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ SET: {
+ type: Scratch.ArgumentType.NUMBER,
+ },
+ },
+ },
+ {
+ opcode: "changeChannelOfColor",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate("change [CHN] of [COL] by [SET]"),
+ arguments: {
+ CHN: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "CHANNEL_MENU",
+ },
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ SET: {
+ type: Scratch.ArgumentType.NUMBER,
+ },
+ },
+ },
+ "---",
+ {
+ opcode: "colorToDecimal",
+ blockType: Scratch.BlockType.REPORTER,
+ text: Scratch.translate({
+ default: "[COL] to decimal",
+ description: "To decimal - as in the base system",
+ }),
+ arguments: {
+ COL: {
+ type: Scratch.ArgumentType.COLOR,
+ defaultValue: "#a45eff",
+ },
+ },
+ },
+ ],
+ menus: {
+ CHANNEL_MENU: {
+ acceptReporters: true,
+ items: [
+ { text: Scratch.translate("red"), value: "red" },
+ { text: Scratch.translate("green"), value: "green" },
+ { text: Scratch.translate("blue"), value: "blue" },
+ { text: Scratch.translate("hue"), value: "hue" },
+ { text: Scratch.translate("saturation"), value: "saturation" },
+ { text: Scratch.translate("value"), value: "value" },
+ ],
+ },
+ SPACE_MENU: {
+ acceptReporters: true,
+ items: [
+ { text: Scratch.translate("RGB"), value: "RGB" },
+ { text: Scratch.translate("HSV"), value: "HSV" },
+ ],
+ },
+ WCAG_MENU: {
+ acceptReporters: true,
+ items: ["A", "AA", "AAA"],
+ },
+ TEXTWCAG_SIZE_MENU: {
+ acceptReporters: true,
+ items: [
+ { text: Scratch.translate("normal"), value: "normal" },
+ { text: Scratch.translate("large"), value: "large" },
+ ],
+ },
+ },
+ };
+ }
+ newColor(args) {
+ return args.COL;
+ }
+ newColorRGB(args) {
+ return "#" + toFixHex(args.R) + toFixHex(args.G) + toFixHex(args.B);
+ }
+ newColorHSV(args) {
+ return hsvToHex(args.H, args.S, args.V);
+ }
+ newColorHSL(args) {
+ let convRGB = hslToRgb(args.H, args.S, args.L);
+ return (
+ "#" + toFixHex(convRGB.r) + toFixHex(convRGB.g) + toFixHex(convRGB.b)
+ );
+ }
+ newColorDecimal(args) {
+ return "#" + toHex(args.DEC);
+ }
+ randomColor() {
+ return (
+ "#" +
+ Math.floor(Math.random() * 0xffffff)
+ .toString(16)
+ .padStart(6, "0")
+ );
+ }
+ additiveBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const add = (c1, c2) => clamp(c1 + c2, 0, 255);
+ return rgbToHex({
+ r: add(a.r, b.r),
+ g: add(a.g, b.g),
+ b: add(a.b, b.b),
+ });
+ }
+ subtractiveBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const sub = (c1, c2) => clamp(c1 - c2, 0, 255);
+ return rgbToHex({
+ r: sub(a.r, b.r),
+ g: sub(a.g, b.g),
+ b: sub(a.b, b.b),
+ });
+ }
+ multiplicativeBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const mul = (c1, c2) => clamp((c1 * c2) / 255, 0, 255);
+ return rgbToHex({
+ r: mul(a.r, b.r),
+ g: mul(a.g, b.g),
+ b: mul(a.b, b.b),
+ });
+ }
+ divisingBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const div = (c1, c2) => clamp((c1 / Math.max(c2, 1)) * 255, 0, 255);
+ return rgbToHex({
+ r: div(a.r, b.r),
+ g: div(a.g, b.g),
+ b: div(a.b, b.b),
+ });
+ }
+ differenceBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const sub = (c1, c2) => clamp(abs(c1 - c2), 0, 255);
+ return rgbToHex({
+ r: sub(a.r, b.r),
+ g: sub(a.g, b.g),
+ b: sub(a.b, b.b),
+ });
+ }
+ screenBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const scr = (c1, c2) => clamp(c1 + c2 - (c1 * c2) / 255, 0, 255);
+ return rgbToHex({
+ r: scr(a.r, b.r),
+ g: scr(a.g, b.g),
+ b: scr(a.b, b.b),
+ });
+ }
+ overlayBlend(args) {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ const ove = (c1, c2) =>
+ clamp(
+ c1 < 128
+ ? (2 * c1 * c2) / 255
+ : 255 - (2 * (255 - c1) * (255 - c2)) / 255,
+ 0,
+ 255
+ );
+ return rgbToHex({
+ r: ove(a.r, b.r),
+ g: ove(a.g, b.g),
+ b: ove(a.b, b.b),
+ });
+ }
+ invertColor(args) {
+ let col = hexToRgb(args.COL);
+ const inv = (c) => 255 - c;
+ return rgbToHex({
+ r: inv(col.r),
+ g: inv(col.g),
+ b: inv(col.b),
+ });
+ }
+ contrastColor(args) {
+ let col = hexToRgb(args.COL);
+ const cnt = (c) => (c - 128) * (1 - args.NUM) + 128;
+ return rgbToHex({
+ r: cnt(col.r),
+ g: cnt(col.g),
+ b: cnt(col.b),
+ });
+ }
+ grayscaleColor(args) {
+ let rgb = hexToRgb(args.COL);
+ let gray = Math.round(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
+ return "#" + toFixHex(gray) + toFixHex(gray) + toFixHex(gray);
+ }
+ percentWhite(args) {
+ let white = round(args.NUM * 2.55);
+ return "#" + toFixHex(white) + toFixHex(white) + toFixHex(white);
+ }
+ distanceBetweenColors(args) {
+ return distanceBetweenHexColorsDeltaE2000(args.COL1, args.COL2);
+ }
+ nearEqualColors(args) {
+ return (
+ distanceBetweenHexColorsDeltaE2000(args.COL1, args.COL2) <= args.THR
+ );
+ }
+ contrastRatioOfColors(args) {
+ return round(contrastRatio(args.COL1, args.COL2) * 100) / 100;
+ }
+ colorFollowsWCAG(args) {
+ let ratio = round(contrastRatio(args.COL1, args.COL2) * 100) / 100;
+ let req = 0;
+ let level = args.AAA;
+ let size = args.TXT;
+ if (level === "AA") {
+ req = size === "large" ? 3.0 : 4.5;
+ }
+ if (level === "AAA") {
+ req = size === "large" ? 4.5 : 7.0;
+ }
+ return ratio >= req;
+ }
+ interpolateColors(args) {
+ if (args.SPACE == "RGB") {
+ let a = hexToRgb(args.COL1);
+ let b = hexToRgb(args.COL2);
+ let t = args.RATIO;
+ return rgbToHex({
+ r: lerp(a.r, b.r, t),
+ g: lerp(a.g, b.g, t),
+ b: lerp(a.b, b.b, t),
+ });
+ } else {
+ return interpolateHexColorsHsv(args.COL1, args.COL2, args.RATIO);
+ }
+ }
+ getChannelFromColor(args) {
+ if (["red", "green", "blue"].includes(args.CHN)) {
+ let channel = ["red", "green", "blue"].indexOf(args.CHN);
+ let letter = ["red", "green", "blue"][channel][0];
+ return hexToRgb(args.COL)[letter];
+ } else if (["hue", "saturation", "value"].includes(args.CHN)) {
+ let hsv = hexToHsv(args.COL);
+ switch (args.CHN) {
+ case "hue":
+ return round(hsv.h);
+ case "saturation":
+ return round(hsv.s);
+ case "value":
+ return round(hsv.v);
+ }
+ }
+ }
+ setChannelOfColor(args) {
+ if (["red", "green", "blue"].includes(args.CHN)) {
+ let rgb = hexToRgb(args.COL);
+ switch (args.CHN) {
+ case "red":
+ rgb.r = args.SET;
+ break;
+ case "green":
+ rgb.g = args.SET;
+ break;
+ case "blue":
+ rgb.b = args.SET;
+ break;
+ }
+ return "#" + toFixHex(rgb.r) + toFixHex(rgb.g) + toFixHex(rgb.b);
+ } else if (["hue", "saturation", "value"].includes(args.CHN)) {
+ let hsv = hexToHsv(args.COL);
+ switch (args.CHN) {
+ case "hue":
+ hsv.h = args.SET;
+ break;
+ case "saturation":
+ hsv.s = args.SET;
+ break;
+ case "value":
+ hsv.v = args.SET;
+ break;
+ }
+ return hsvToHex(hsv.h, hsv.s, hsv.v);
+ }
+ }
+ changeChannelOfColor(args) {
+ if (["red", "green", "blue"].includes(args.CHN)) {
+ let rgb = hexToRgb(args.COL);
+ switch (args.CHN) {
+ case "red":
+ rgb.r = clamp(rgb.r + args.SET, 0, 255);
+ break;
+ case "green":
+ rgb.g = clamp(rgb.g + args.SET, 0, 255);
+ break;
+ case "blue":
+ rgb.b = clamp(rgb.b + args.SET, 0, 255);
+ break;
+ }
+ return "#" + toFixHex(rgb.r) + toFixHex(rgb.g) + toFixHex(rgb.b);
+ } else if (["hue", "saturation", "value"].includes(args.CHN)) {
+ let hsv = hexToHsv(args.COL);
+ switch (args.CHN) {
+ case "hue":
+ hsv.h = (hsv.h + args.SET) % 360;
+ break;
+ case "saturation":
+ hsv.s = clamp(hsv.s + args.SET, 0, 100);
+ break;
+ case "value":
+ hsv.v = clamp(hsv.v + args.SET, 0, 100);
+ break;
+ }
+ return hsvToHex(hsv.h, hsv.s, hsv.v);
+ }
+ }
+ colorToDecimal(args) {
+ return toDec(args.COL.slice(1));
+ }
+ }
+ Scratch.extensions.register(new Colors());
+})(Scratch);
diff --git a/extensions/extensions.json b/extensions/extensions.json
index 442f300d59..e27bdf8722 100644
--- a/extensions/extensions.json
+++ b/extensions/extensions.json
@@ -21,6 +21,7 @@
"iframe",
"Clay/htmlEncode",
"Xeltalliv/clippingblending",
+ "LincolnX/colors",
"clipboard",
"obviousAlexC/penPlus",
"penplus",
diff --git a/images/LincolnX/colors.svg b/images/LincolnX/colors.svg
new file mode 100644
index 0000000000..5258c4da87
--- /dev/null
+++ b/images/LincolnX/colors.svg
@@ -0,0 +1 @@
+