266 lines
6.9 KiB
JavaScript
266 lines
6.9 KiB
JavaScript
|
|
const TAU = //////|//////
|
|
///// | /////
|
|
/// tau ///
|
|
/// ...--> | <--... ///
|
|
/// -' one | turn '- ///
|
|
// .' | '. //
|
|
// / | \ //
|
|
// | | <-.. | //
|
|
// | .->| \ | //
|
|
// | / | | | //
|
|
- - - - - - Math.PI + Math.PI - - - - - 0;
|
|
// | \ | | | //
|
|
// | '->| / | //
|
|
// | | <-'' | //
|
|
// \ | / //
|
|
// '. | .' //
|
|
/// -. | .- ///
|
|
/// '''----|----''' ///
|
|
/// | ///
|
|
////// | /////
|
|
//////|////// C/r;
|
|
|
|
const is_pride_month = new Date().getMonth() === 5; // June (0-based, 0 is January)
|
|
|
|
const $G = $(window);
|
|
|
|
function make_css_cursor(name, coords, fallback) {
|
|
return `url(images/cursors/${name}.png) ${coords.join(" ")}, ${fallback}`;
|
|
}
|
|
|
|
function E(t) {
|
|
return document.createElement(t);
|
|
}
|
|
|
|
/** Returns a function, that, as long as it continues to be invoked, will not
|
|
be triggered. The function will be called after it stops being called for
|
|
N milliseconds. If `immediate` is passed, trigger the function on the
|
|
leading edge, instead of the trailing. */
|
|
function debounce(func, wait_ms, immediate) {
|
|
let timeout;
|
|
const debounced_func = function () {
|
|
const context = this;
|
|
const args = arguments;
|
|
|
|
const later = () => {
|
|
timeout = null;
|
|
if (!immediate) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
|
|
const callNow = immediate && !timeout;
|
|
|
|
clearTimeout(timeout);
|
|
|
|
timeout = setTimeout(later, wait_ms);
|
|
|
|
if (callNow) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
debounced_func.cancel = () => {
|
|
clearTimeout(timeout);
|
|
};
|
|
return debounced_func;
|
|
}
|
|
|
|
function memoize_synchronous_function(func, max_entries = 50000) {
|
|
const cache = {};
|
|
const keys = [];
|
|
const memoized_func = (...args) => {
|
|
if (args.some((arg) => arg instanceof CanvasPattern)) {
|
|
return func.apply(null, args);
|
|
}
|
|
const key = JSON.stringify(args);
|
|
if (cache[key]) {
|
|
return cache[key];
|
|
} else {
|
|
const val = func.apply(null, args);
|
|
cache[key] = val;
|
|
keys.push(key);
|
|
if (keys.length > max_entries) {
|
|
const oldest_key = keys.shift();
|
|
delete cache[oldest_key];
|
|
}
|
|
return val;
|
|
}
|
|
}
|
|
memoized_func.clear_memo_cache = () => {
|
|
for (const key of keys) {
|
|
delete cache[key];
|
|
}
|
|
keys.length = 0;
|
|
};
|
|
return memoized_func;
|
|
}
|
|
|
|
window.get_rgba_from_color = memoize_synchronous_function((color) => {
|
|
const single_pixel_canvas = make_canvas(1, 1);
|
|
|
|
single_pixel_canvas.ctx.fillStyle = color;
|
|
single_pixel_canvas.ctx.fillRect(0, 0, 1, 1);
|
|
|
|
const image_data = single_pixel_canvas.ctx.getImageData(0, 0, 1, 1);
|
|
|
|
// We could just return image_data.data, but let's return an Array instead
|
|
// I'm not totally sure image_data.data wouldn't keep the ImageData object around in memory
|
|
return Array.from(image_data.data);
|
|
});
|
|
|
|
/**
|
|
* Compare two ImageData.
|
|
* Note: putImageData is lossy, due to premultiplied alpha.
|
|
* @returns {boolean} whether all pixels match within the specified threshold
|
|
*/
|
|
function image_data_match(a, b, threshold) {
|
|
const a_data = a.data;
|
|
const b_data = b.data;
|
|
if (a_data.length !== b_data.length) {
|
|
return false;
|
|
}
|
|
for (let len = a_data.length, i = 0; i < len; i++) {
|
|
if (a_data[i] !== b_data[i]) {
|
|
if (Math.abs(a_data[i] - b_data[i]) > threshold) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function make_canvas(width, height) {
|
|
const image = width;
|
|
|
|
const new_canvas = E("canvas");
|
|
const new_ctx = new_canvas.getContext("2d");
|
|
|
|
new_canvas.ctx = new_ctx;
|
|
|
|
new_ctx.disable_image_smoothing = () => {
|
|
new_ctx.imageSmoothingEnabled = false;
|
|
// condition is to avoid a deprecation warning in Firefox
|
|
if (new_ctx.imageSmoothingEnabled !== false) {
|
|
new_ctx.mozImageSmoothingEnabled = false;
|
|
new_ctx.webkitImageSmoothingEnabled = false;
|
|
new_ctx.msImageSmoothingEnabled = false;
|
|
}
|
|
};
|
|
new_ctx.enable_image_smoothing = () => {
|
|
new_ctx.imageSmoothingEnabled = true;
|
|
if (new_ctx.imageSmoothingEnabled !== true) {
|
|
new_ctx.mozImageSmoothingEnabled = true;
|
|
new_ctx.webkitImageSmoothingEnabled = true;
|
|
new_ctx.msImageSmoothingEnabled = true;
|
|
}
|
|
};
|
|
|
|
// @TODO: simplify the abstraction by defining setters for width/height
|
|
// that reset the image smoothing to disabled
|
|
// and make image smoothing a parameter to make_canvas
|
|
|
|
new_ctx.copy = image => {
|
|
new_canvas.width = image.naturalWidth || image.width;
|
|
new_canvas.height = image.naturalHeight || image.height;
|
|
|
|
// setting width/height resets image smoothing (along with everything)
|
|
new_ctx.disable_image_smoothing();
|
|
|
|
if (image instanceof ImageData) {
|
|
new_ctx.putImageData(image, 0, 0);
|
|
} else {
|
|
new_ctx.drawImage(image, 0, 0);
|
|
}
|
|
};
|
|
|
|
if (width && height) {
|
|
// make_canvas(width, height)
|
|
new_canvas.width = width;
|
|
new_canvas.height = height;
|
|
// setting width/height resets image smoothing (along with everything)
|
|
new_ctx.disable_image_smoothing();
|
|
} else if (image) {
|
|
// make_canvas(image)
|
|
new_ctx.copy(image);
|
|
}
|
|
|
|
return new_canvas;
|
|
}
|
|
|
|
function get_help_folder_icon(file_name) {
|
|
const icon_img = new Image();
|
|
icon_img.src = `help/${file_name}`;
|
|
return icon_img;
|
|
}
|
|
|
|
function get_icon_for_tool(tool) {
|
|
return get_help_folder_icon(tool.help_icon);
|
|
}
|
|
|
|
// not to be confused with load_image_from_uri
|
|
function load_image_simple(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
|
|
img.onload = () => { resolve(img); };
|
|
img.onerror = () => { reject(new Error(`failed to load image from ${src}`)); };
|
|
|
|
img.src = src;
|
|
});
|
|
}
|
|
|
|
function get_icon_for_tools(tools) {
|
|
if (tools.length === 1) {
|
|
return get_icon_for_tool(tools[0]);
|
|
}
|
|
const icon_canvas = make_canvas(16, 16);
|
|
|
|
Promise.all(tools.map((tool) => load_image_simple(`help/${tool.help_icon}`)))
|
|
.then((icons) => {
|
|
icons.forEach((icon, i) => {
|
|
const w = icon_canvas.width / icons.length;
|
|
const x = i * w;
|
|
const h = icon_canvas.height;
|
|
const y = 0;
|
|
icon_canvas.ctx.drawImage(icon, x, y, w, h, x, y, w, h);
|
|
});
|
|
})
|
|
return icon_canvas;
|
|
}
|
|
|
|
/**
|
|
* Converts an RGB color value to HSL. Conversion formula
|
|
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
|
* Assumes r, g, and b are contained in the set [0, 255] and
|
|
* returns h, s, and l in the set [0, 1].
|
|
*
|
|
* @param Number r The red color value
|
|
* @param Number g The green color value
|
|
* @param Number b The blue color value
|
|
* @return Array The HSL representation
|
|
*/
|
|
function rgb_to_hsl(r, g, b) {
|
|
r /= 255; g /= 255; b /= 255;
|
|
|
|
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
var h, s, l = (max + min) / 2;
|
|
|
|
if (max == min) {
|
|
h = s = 0; // achromatic
|
|
} else {
|
|
var d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
|
|
switch (max) {
|
|
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
case g: h = (b - r) / d + 2; break;
|
|
case b: h = (r - g) / d + 4; break;
|
|
}
|
|
|
|
h /= 6;
|
|
}
|
|
|
|
return [h, s, l];
|
|
}
|