diff --git a/ui/media/images/fa-eraser.svg b/ui/media/images/fa-eraser.svg
new file mode 100644
index 00000000..6acb1d5a
--- /dev/null
+++ b/ui/media/images/fa-eraser.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/media/images/fa-eye-dropper.svg b/ui/media/images/fa-eye-dropper.svg
new file mode 100644
index 00000000..25894e63
--- /dev/null
+++ b/ui/media/images/fa-eye-dropper.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/media/images/fa-fill.svg b/ui/media/images/fa-fill.svg
new file mode 100644
index 00000000..af41281a
--- /dev/null
+++ b/ui/media/images/fa-fill.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/media/images/fa-pencil.svg b/ui/media/images/fa-pencil.svg
new file mode 100644
index 00000000..ec76b9fe
--- /dev/null
+++ b/ui/media/images/fa-pencil.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/media/js/image-editor.js b/ui/media/js/image-editor.js
index 78f41b84..317c40e0 100644
--- a/ui/media/js/image-editor.js
+++ b/ui/media/js/image-editor.js
@@ -36,13 +36,14 @@ const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => {
ctx.clearRect(0, 0, editor.width, editor.height)
}
}
+const toolDoNothing = (editor, ctx, x, y, is_overlay = false) => {}
const IMAGE_EDITOR_TOOLS = [
{
id: "draw",
name: "Draw",
icon: "fa-solid fa-pencil",
- cursor: "url(/media/images/fa-pencil.png) 0 24, pointer",
+ cursor: "url(/media/images/fa-pencil.svg) 0 24, pointer",
begin: defaultToolBegin,
move: defaultToolMove,
end: defaultToolEnd
@@ -51,7 +52,7 @@ const IMAGE_EDITOR_TOOLS = [
id: "erase",
name: "Erase",
icon: "fa-solid fa-eraser",
- cursor: "url(/media/images/fa-eraser.png) 0 18, pointer",
+ cursor: "url(/media/images/fa-eraser.svg) 0 14, pointer",
begin: defaultToolBegin,
move: (editor, ctx, x, y, is_overlay = false) => {
ctx.lineTo(x, y)
@@ -78,27 +79,56 @@ const IMAGE_EDITOR_TOOLS = [
}
},
{
- id: "colorpicker",
- name: "Color Picker",
- icon: "fa-solid fa-eye-dropper",
- cursor: "url(/media/images/fa-eye-dropper.png) 0 24, pointer",
+ id: "fill",
+ name: "Fill",
+ icon: "fa-solid fa-fill",
+ cursor: "url(/media/images/fa-fill.svg) 20 6, pointer",
begin: (editor, ctx, x, y, is_overlay = false) => {
- var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data
- var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data
- var drawn_opacity = drawn_rgb[3] / 255
- editor.custom_color_input.value = rgbToHex({
- r: (drawn_rgb[0] * drawn_opacity) + (img_rgb[0] * (1 - drawn_opacity)),
- g: (drawn_rgb[1] * drawn_opacity) + (img_rgb[1] * (1 - drawn_opacity)),
- b: (drawn_rgb[2] * drawn_opacity) + (img_rgb[2] * (1 - drawn_opacity)),
- })
- editor.custom_color_input.dispatchEvent(new Event("change"))
+ if (!is_overlay) {
+ var color = hexToRgb(ctx.fillStyle)
+ color.a = parseInt(ctx.globalAlpha * 255) // layer.ctx.globalAlpha
+ flood_fill(editor, ctx, parseInt(x), parseInt(y), color)
+ }
},
- move: (editor, ctx, x, y, is_overlay = false) => {},
- end: (editor, ctx, x, y, is_overlay = false) => {}
+ move: toolDoNothing,
+ end: toolDoNothing
+ },
+ {
+ id: "colorpicker",
+ name: "Picker",
+ icon: "fa-solid fa-eye-dropper",
+ cursor: "url(/media/images/fa-eye-dropper.svg) 0 24, pointer",
+ begin: (editor, ctx, x, y, is_overlay = false) => {
+ if (!is_overlay) {
+ var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data
+ var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data
+ var drawn_opacity = drawn_rgb[3] / 255
+ editor.custom_color_input.value = rgbToHex({
+ r: (drawn_rgb[0] * drawn_opacity) + (img_rgb[0] * (1 - drawn_opacity)),
+ g: (drawn_rgb[1] * drawn_opacity) + (img_rgb[1] * (1 - drawn_opacity)),
+ b: (drawn_rgb[2] * drawn_opacity) + (img_rgb[2] * (1 - drawn_opacity)),
+ })
+ editor.custom_color_input.dispatchEvent(new Event("change"))
+ }
+ },
+ move: toolDoNothing,
+ end: toolDoNothing
}
]
const IMAGE_EDITOR_ACTIONS = [
+ {
+ id: "fill_all",
+ name: "Fill all",
+ icon: "fa-solid fa-paint-roller",
+ handler: (editor) => {
+ editor.ctx_current.globalCompositeOperation = "source-over"
+ editor.ctx_current.rect(0, 0, editor.width, editor.height)
+ editor.ctx_current.fill()
+ editor.setBrush()
+ },
+ trackHistory: true
+ },
{
id: "clear",
name: "Clear",
@@ -404,7 +434,6 @@ class ImageEditor {
if (this.inpainter) {
this.selectOption("color", IMAGE_EDITOR_SECTIONS.find(s => s.name == "color").options.indexOf("#ffffff"))
- this.selectOption("opacity", IMAGE_EDITOR_SECTIONS.find(s => s.name == "opacity").options.indexOf(0.4))
}
// initialize the right-side controls
@@ -467,8 +496,8 @@ class ImageEditor {
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
- this.width = width
- this.height = height
+ this.width = parseInt(width)
+ this.height = parseInt(height)
this.container.style.width = width + "px"
this.container.style.height = height + "px"
@@ -494,8 +523,10 @@ class ImageEditor {
}
setImage(url, width, height) {
this.setSize(width, height)
- this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height)
this.layers.background.ctx.clearRect(0, 0, this.width, this.height)
+ if (!(url && this.inpainter)) {
+ this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height)
+ }
if (url) {
var image = new Image()
image.onload = () => {
@@ -685,14 +716,6 @@ class ImageEditor {
}
}
-function rgbToHex(rgb) {
- function componentToHex(c) {
- var hex = parseInt(c).toString(16)
- return hex.length == 1 ? "0" + hex : hex
- }
- return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)
-}
-
const imageEditor = new ImageEditor(document.getElementById("image-editor"))
const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true)
@@ -707,3 +730,107 @@ document.getElementById("init_image_button_inpaint").addEventListener("click", (
})
img2imgUnload() // no init image when the app starts
+
+
+function rgbToHex(rgb) {
+ function componentToHex(c) {
+ var hex = parseInt(c).toString(16)
+ return hex.length == 1 ? "0" + hex : hex
+ }
+ return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)
+}
+
+function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+}
+
+function pixelCompare(int1, int2) {
+ return Math.abs(int1 - int2) < 4
+}
+
+// adapted from https://ben.akrin.com/canvas_fill/fill_04.html
+function flood_fill(editor, the_canvas_context, x, y, color) {
+ pixel_stack = [{x:x, y:y}] ;
+ pixels = the_canvas_context.getImageData( 0, 0, editor.width, editor.height ) ;
+ var linear_cords = ( y * editor.width + x ) * 4 ;
+ var original_color = {r:pixels.data[linear_cords],
+ g:pixels.data[linear_cords+1],
+ b:pixels.data[linear_cords+2],
+ a:pixels.data[linear_cords+3]} ;
+
+ var opacity = color.a / 255;
+ var new_color = {
+ r: parseInt((color.r * opacity) + (original_color.r * (1 - opacity))),
+ g: parseInt((color.g * opacity) + (original_color.g * (1 - opacity))),
+ b: parseInt((color.b * opacity) + (original_color.b * (1 - opacity)))
+ }
+
+ if ((pixelCompare(new_color.r, original_color.r) &&
+ pixelCompare(new_color.g, original_color.g) &&
+ pixelCompare(new_color.b, original_color.b)))
+ {
+ return; // This color is already the color we want, so do nothing
+ }
+ var max_stack_size = editor.width * editor.height;
+ while( pixel_stack.length > 0 && pixel_stack.length < max_stack_size ) {
+ new_pixel = pixel_stack.shift() ;
+ x = new_pixel.x ;
+ y = new_pixel.y ;
+
+ linear_cords = ( y * editor.width + x ) * 4 ;
+ while( y-->=0 &&
+ (pixelCompare(pixels.data[linear_cords], original_color.r) &&
+ pixelCompare(pixels.data[linear_cords+1], original_color.g) &&
+ pixelCompare(pixels.data[linear_cords+2], original_color.b))) {
+ linear_cords -= editor.width * 4 ;
+ }
+ linear_cords += editor.width * 4 ;
+ y++ ;
+
+ var reached_left = false ;
+ var reached_right = false ;
+ while( y++0 ) {
+ if( pixelCompare(pixels.data[linear_cords-4], original_color.r) &&
+ pixelCompare(pixels.data[linear_cords-4+1], original_color.g) &&
+ pixelCompare(pixels.data[linear_cords-4+2], original_color.b)) {
+ if( !reached_left ) {
+ pixel_stack.push( {x:x-1, y:y} ) ;
+ reached_left = true ;
+ }
+ } else if( reached_left ) {
+ reached_left = false ;
+ }
+ }
+
+ if( x sheet.href?.startsWith(window.location.origin))
.flatMap(sheet => Array.from(sheet.cssRules))
.forEach(rule => {
- var selector = rule.selectorText; // TODO: also do selector == ":root", re-run un-set props
+ var selector = rule.selectorText;
if (selector && selector.startsWith(".theme-") && !selector.includes(" ")) {
+ if (DEFAULT_THEME) { // re-add props that dont change (css needs this so they update correctly)
+ Array.from(DEFAULT_THEME.rule.style)
+ .filter(cssVariable => !Array.from(rule.style).includes(cssVariable))
+ .forEach(cssVariable => {
+ rule.style.setProperty(cssVariable, DEFAULT_THEME.rule.style.getPropertyValue(cssVariable));
+ });
+ }
var theme_key = selector.substring(1);
THEMES.push({
key: theme_key,
@@ -62,12 +69,6 @@ function themeFieldChanged() {
var theme = THEMES.find(t => t.key == theme_key);
let borderColor = undefined
if (theme) {
- // refresh variables incase they are back referencing
- Array.from(DEFAULT_THEME.rule.style)
- .filter(cssVariable => !Array.from(theme.rule.style).includes(cssVariable))
- .forEach(cssVariable => {
- body.style.setProperty(cssVariable, DEFAULT_THEME.rule.style.getPropertyValue(cssVariable));
- });
borderColor = theme.rule.style.getPropertyValue('--input-border-color').trim()
if (!borderColor.startsWith('#')) {
borderColor = theme.rule.style.getPropertyValue('--theme-color-fallback')