384 lines
11 KiB
JavaScript
384 lines
11 KiB
JavaScript
const ChooserCanvas = (
|
|
url,
|
|
invert,
|
|
width,
|
|
height,
|
|
sourceX,
|
|
sourceY,
|
|
sourceWidth,
|
|
sourceHeight,
|
|
destX,
|
|
destY,
|
|
destWidth,
|
|
destHeight,
|
|
reuse_canvas,
|
|
) => {
|
|
const c = reuse_canvas(width, height);
|
|
let img = ChooserCanvas.cache[url];
|
|
if (!img) {
|
|
img = ChooserCanvas.cache[url] = E("img");
|
|
img.onerror = () => {
|
|
delete ChooserCanvas.cache[url];
|
|
};
|
|
img.src = url;
|
|
}
|
|
const render = () => {
|
|
try {
|
|
c.ctx.drawImage(
|
|
img,
|
|
sourceX, sourceY, sourceWidth, sourceHeight,
|
|
destX, destY, destWidth, destHeight
|
|
);
|
|
// eslint-disable-next-line no-empty
|
|
} catch (error) { }
|
|
// if (invert) {
|
|
// invert_rgb(c.ctx); // can fail due to tainted canvas if running from file: protocol
|
|
// }
|
|
c.style.filter = invert ? "invert()" : "";
|
|
};
|
|
$(img).on("load", render);
|
|
render();
|
|
return c;
|
|
};
|
|
ChooserCanvas.cache = {};
|
|
|
|
// @TODO: convert all options to use this themeable version (or more options? some are dynamically rendered...)
|
|
const ChooserDiv = (
|
|
class_name,
|
|
invert,
|
|
width,
|
|
height,
|
|
sourceX,
|
|
sourceY,
|
|
sourceWidth,
|
|
sourceHeight,
|
|
destX,
|
|
destY,
|
|
destWidth,
|
|
destHeight,
|
|
reuse_div,
|
|
) => {
|
|
const div = reuse_div(width, height);
|
|
div.classList.add(class_name);
|
|
div.style.width = sourceWidth + "px";
|
|
div.style.height = sourceHeight + "px";
|
|
|
|
// @TODO: single listener for all divs
|
|
const on_zoom_etc = () => {
|
|
const use_svg = (window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0);
|
|
div.classList.toggle("use-svg", use_svg);
|
|
};
|
|
if (div._on_zoom_etc) { // condition is needed, otherwise it will remove all listeners! (leading to only the last graphic being updated when zooming)
|
|
$G.off("theme-load resize", div._on_zoom_etc);
|
|
}
|
|
$G.on("theme-load resize", on_zoom_etc);
|
|
div._on_zoom_etc = on_zoom_etc;
|
|
on_zoom_etc();
|
|
|
|
div.style.backgroundPosition = `${-sourceX}px ${-sourceY}px`;
|
|
div.style.borderColor = "transparent";
|
|
div.style.borderStyle = "solid";
|
|
div.style.borderLeftWidth = destX + "px";
|
|
div.style.borderTopWidth = destY + "px";
|
|
div.style.borderRightWidth = (width - destX - destWidth) + "px";
|
|
div.style.borderBottomWidth = (height - destY - destHeight) + "px";
|
|
div.style.backgroundClip = "content-box";
|
|
div.style.filter = invert ? "invert()" : "";
|
|
return div;
|
|
};
|
|
|
|
|
|
|
|
const $Choose = (things, display, choose, is_chosen, gray_background_for_unselected) => {
|
|
const $chooser = $(E("div")).addClass("chooser").css("touch-action", "none");
|
|
const choose_thing = (thing) => {
|
|
if (is_chosen(thing)) {
|
|
return;
|
|
}
|
|
choose(thing);
|
|
$G.trigger("option-changed");
|
|
};
|
|
$chooser.on("update", () => {
|
|
if (!$chooser.is(":visible")) {
|
|
return;
|
|
}
|
|
$chooser.empty();
|
|
for (let i = 0; i < things.length; i++) {
|
|
(thing => {
|
|
const $option_container = $(E("div")).addClass("chooser-option").appendTo($chooser);
|
|
$option_container.data("thing", thing);
|
|
const reuse_canvas = (width, height) => {
|
|
let option_canvas = $option_container.find("canvas")[0];
|
|
if (option_canvas) {
|
|
if (option_canvas.width !== width) { option_canvas.width = width; }
|
|
if (option_canvas.height !== height) { option_canvas.height = height; }
|
|
} else {
|
|
option_canvas = make_canvas(width, height);
|
|
$option_container.append(option_canvas);
|
|
}
|
|
return option_canvas;
|
|
};
|
|
const reuse_div = (width, height) => {
|
|
let option_div = $option_container.find("div")[0];
|
|
if (option_div) {
|
|
if (option_div.style.width !== width + "px") { option_div.style.width = width + "px"; }
|
|
if (option_div.style.height !== height + "px") { option_div.style.height = height + "px"; }
|
|
} else {
|
|
option_div = E("div");
|
|
option_div.style.width = width + "px";
|
|
option_div.style.height = height + "px";
|
|
$option_container.append(option_div);
|
|
}
|
|
return option_div;
|
|
};
|
|
const update = () => {
|
|
const selected_color = getComputedStyle($chooser[0]).getPropertyValue("--Hilight");
|
|
const unselected_color = gray_background_for_unselected ? "rgb(192, 192, 192)" : "";
|
|
$option_container.css({
|
|
backgroundColor: is_chosen(thing) ? selected_color : unselected_color,
|
|
});
|
|
display(thing, is_chosen(thing), reuse_canvas, reuse_div);
|
|
};
|
|
update();
|
|
$G.on("option-changed theme-load redraw-tool-options-because-webglcontextrestored", update);
|
|
})(things[i]);
|
|
}
|
|
|
|
const onpointerover_while_pointer_down = (event) => {
|
|
const option_container = event.target.closest(".chooser-option");
|
|
if (option_container) {
|
|
choose_thing($(option_container).data("thing"));
|
|
}
|
|
};
|
|
const ontouchmove_while_pointer_down = (event) => {
|
|
const touch = event.originalEvent.changedTouches[0];
|
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
const option_container = target.closest(".chooser-option");
|
|
if (option_container) {
|
|
choose_thing($(option_container).data("thing"));
|
|
}
|
|
event.preventDefault();
|
|
};
|
|
$chooser.on("pointerdown click", (event) => {
|
|
const option_container = event.target.closest(".chooser-option");
|
|
if (option_container) {
|
|
choose_thing($(option_container).data("thing"));
|
|
}
|
|
if (event.type === "pointerdown") {
|
|
// glide thru tool options
|
|
$chooser.on("pointerover", onpointerover_while_pointer_down);
|
|
$chooser.on("touchmove", ontouchmove_while_pointer_down);
|
|
}
|
|
});
|
|
$G.on("pointerup pointercancel", () => {
|
|
$chooser.off("pointerover", onpointerover_while_pointer_down);
|
|
$chooser.off("touchmove", ontouchmove_while_pointer_down);
|
|
});
|
|
});
|
|
return $chooser;
|
|
};
|
|
const $ChooseShapeStyle = () => {
|
|
const $chooser = $Choose(
|
|
[
|
|
{ stroke: true, fill: false },
|
|
{ stroke: true, fill: true },
|
|
{ stroke: false, fill: true }
|
|
],
|
|
({ stroke, fill }, is_chosen, reuse_canvas) => {
|
|
const ss_canvas = reuse_canvas(39, 21);
|
|
const ss_ctx = ss_canvas.ctx;
|
|
|
|
// border px inwards amount
|
|
let b = 5;
|
|
|
|
const style = getComputedStyle(ss_canvas);
|
|
ss_ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
|
|
|
|
if (stroke) {
|
|
// just using a solid rectangle for the stroke
|
|
// so as not to have to deal with the pixel grid with strokes
|
|
ss_ctx.fillRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
|
|
}
|
|
|
|
// go inward a pixel for the fill
|
|
b += 1;
|
|
ss_ctx.fillStyle = style.getPropertyValue("--ButtonShadow");
|
|
|
|
if (fill) {
|
|
ss_ctx.fillRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
|
|
} else {
|
|
ss_ctx.clearRect(b, b, ss_canvas.width - b * 2, ss_canvas.height - b * 2);
|
|
}
|
|
|
|
return ss_canvas;
|
|
},
|
|
({ stroke, fill }) => {
|
|
$chooser.stroke = stroke;
|
|
$chooser.fill = fill;
|
|
},
|
|
({ stroke, fill }) => $chooser.stroke === stroke && $chooser.fill === fill
|
|
).addClass("choose-shape-style");
|
|
|
|
$chooser.fill = false;
|
|
$chooser.stroke = true;
|
|
|
|
return $chooser;
|
|
};
|
|
|
|
const $choose_brush = $Choose(
|
|
(() => {
|
|
const brush_shapes = ["circle", "square", "reverse_diagonal", "diagonal"];
|
|
const circular_brush_sizes = [7, 4, 1];
|
|
const brush_sizes = [8, 5, 2];
|
|
const things = [];
|
|
brush_shapes.forEach((brush_shape) => {
|
|
const sizes = brush_shape === "circle" ? circular_brush_sizes : brush_sizes;
|
|
sizes.forEach((brush_size) => {
|
|
things.push({
|
|
shape: brush_shape,
|
|
size: brush_size,
|
|
});
|
|
});
|
|
});
|
|
return things;
|
|
})(),
|
|
(o, is_chosen, reuse_canvas) => {
|
|
const cb_canvas = reuse_canvas(10, 10);
|
|
const style = getComputedStyle(cb_canvas);
|
|
|
|
const shape = o.shape;
|
|
const size = o.size;
|
|
const color = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
|
|
|
|
stamp_brush_canvas(cb_canvas.ctx, 5, 5, shape, size);
|
|
replace_colors_with_swatch(cb_canvas.ctx, color);
|
|
|
|
return cb_canvas;
|
|
}, ({ shape, size }) => {
|
|
brush_shape = shape;
|
|
brush_size = size;
|
|
}, ({ shape, size }) => brush_shape === shape && brush_size === size
|
|
).addClass("choose-brush");
|
|
|
|
const $choose_eraser_size = $Choose(
|
|
[4, 6, 8, 10],
|
|
(size, is_chosen, reuse_canvas) => {
|
|
const ce_canvas = reuse_canvas(39, 16);
|
|
|
|
const style = getComputedStyle(ce_canvas);
|
|
ce_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
|
|
render_brush(ce_canvas.ctx, "square", size);
|
|
|
|
return ce_canvas;
|
|
},
|
|
size => {
|
|
eraser_size = size;
|
|
},
|
|
size => eraser_size === size
|
|
).addClass("choose-eraser");
|
|
|
|
const $choose_stroke_size = $Choose(
|
|
[1, 2, 3, 4, 5],
|
|
(size, is_chosen, reuse_canvas) => {
|
|
const w = 39, h = 12, b = 5;
|
|
const cs_canvas = reuse_canvas(w, h);
|
|
const center_y = (h - size) / 2;
|
|
const style = getComputedStyle(cs_canvas);
|
|
cs_canvas.ctx.fillStyle = is_chosen ? style.getPropertyValue("--HilightText") : style.getPropertyValue("--WindowText");
|
|
cs_canvas.ctx.fillRect(b, ~~center_y, w - b * 2, size);
|
|
return cs_canvas;
|
|
},
|
|
size => {
|
|
stroke_size = size;
|
|
},
|
|
size => stroke_size === size
|
|
).addClass("choose-stroke-size");
|
|
|
|
const magnifications = [1, 2, 6, 8, 10];
|
|
const $choose_magnification = $Choose(
|
|
magnifications,
|
|
(scale, is_chosen, reuse_canvas, reuse_div) => {
|
|
const i = magnifications.indexOf(scale);
|
|
const secret = scale === 10; // 10x is secret
|
|
const chooser_el = ChooserDiv(
|
|
"magnification-option",
|
|
is_chosen, // invert if chosen
|
|
39, (secret ? 2 : 13), // width, height of destination canvas
|
|
i * 23, 0, 23, 9, // x, y, width, height from source image
|
|
8, 2, 23, 9, // x, y, width, height on destination
|
|
reuse_div,
|
|
);
|
|
if (secret) {
|
|
$(chooser_el).addClass("secret-option");
|
|
}
|
|
return chooser_el;
|
|
},
|
|
scale => {
|
|
set_magnification(scale);
|
|
},
|
|
scale => scale === magnification,
|
|
true,
|
|
).addClass("choose-magnification")
|
|
.css({ position: "relative" }); // positioning context for .secret-option `position: "absolute"` canvas
|
|
|
|
$choose_magnification.on("update", () => {
|
|
$choose_magnification
|
|
.find(".secret-option")
|
|
.parent()
|
|
.css({ position: "absolute", bottom: "-2px", left: 0, opacity: 0, height: 2, overflow: "hidden" });
|
|
});
|
|
|
|
const airbrush_sizes = [9, 16, 24];
|
|
const $choose_airbrush_size = $Choose(
|
|
airbrush_sizes,
|
|
(size, is_chosen, reuse_canvas) => {
|
|
|
|
const image_width = 72; // width of source image
|
|
const i = airbrush_sizes.indexOf(size); // 0 or 1 or 2
|
|
const l = airbrush_sizes.length; // 3
|
|
const is_bottom = (i === 2);
|
|
|
|
const shrink = 4 * !is_bottom;
|
|
const w = image_width / l - shrink * 2;
|
|
const h = 23;
|
|
const source_x = image_width / l * i + shrink;
|
|
|
|
return ChooserCanvas(
|
|
"images/options-airbrush-size.png",
|
|
is_chosen, // invert if chosen
|
|
w, h, // width, height of created destination canvas
|
|
source_x, 0, w, h, // x, y, width, height from source image
|
|
0, 0, w, h, // x, y, width, height on created destination canvas
|
|
reuse_canvas,
|
|
);
|
|
},
|
|
size => {
|
|
airbrush_size = size;
|
|
},
|
|
size => size === airbrush_size,
|
|
true,
|
|
).addClass("choose-airbrush-size");
|
|
|
|
const $choose_transparent_mode = $Choose(
|
|
[false, true],
|
|
(option, _is_chosen, reuse_canvas, reuse_div) => {
|
|
const sw = 35, sh = 23; // width, height from source image
|
|
const b = 2; // margin by which the source image is inset on the destination
|
|
const theme_folder = `images/${get_theme().replace(/\.css/i, "")}`;
|
|
return ChooserDiv(
|
|
"transparent-mode-option",
|
|
false, // never invert it
|
|
b + sw + b, b + sh + b, // width, height of created destination canvas
|
|
0, option ? 22 : 0, sw, sh, // x, y, width, height from source image
|
|
b, b, sw, sh, // x, y, width, height on created destination canvas
|
|
reuse_div,
|
|
);
|
|
},
|
|
option => {
|
|
tool_transparent_mode = option;
|
|
},
|
|
option => option === tool_transparent_mode,
|
|
true,
|
|
).addClass("choose-transparent-mode");
|
|
|