mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2024-11-29 19:53:40 +01:00
944 lines
34 KiB
JavaScript
944 lines
34 KiB
JavaScript
var editorControlsLeft = document.getElementById("image-editor-controls-left")
|
|
|
|
const IMAGE_EDITOR_MAX_SIZE = 800
|
|
|
|
const IMAGE_EDITOR_BUTTONS = [
|
|
{
|
|
name: "Cancel",
|
|
icon: "fa-regular fa-circle-xmark",
|
|
handler: (editor) => {
|
|
editor.hide()
|
|
},
|
|
},
|
|
{
|
|
name: "Save",
|
|
icon: "fa-solid fa-floppy-disk",
|
|
handler: (editor) => {
|
|
editor.saveImage()
|
|
},
|
|
},
|
|
]
|
|
|
|
const defaultToolBegin = (editor, ctx, x, y, is_overlay = false) => {
|
|
ctx.beginPath()
|
|
ctx.moveTo(x, y)
|
|
}
|
|
const defaultToolMove = (editor, ctx, x, y, is_overlay = false) => {
|
|
ctx.lineTo(x, y)
|
|
if (is_overlay) {
|
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
|
ctx.stroke()
|
|
}
|
|
}
|
|
const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => {
|
|
ctx.stroke()
|
|
if (is_overlay) {
|
|
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.svg) 0 24, pointer",
|
|
begin: defaultToolBegin,
|
|
move: defaultToolMove,
|
|
end: defaultToolEnd,
|
|
hotkey: "d",
|
|
},
|
|
{
|
|
id: "erase",
|
|
name: "Erase",
|
|
icon: "fa-solid fa-eraser",
|
|
cursor: "url(/media/images/fa-eraser.svg) 0 14, pointer",
|
|
begin: defaultToolBegin,
|
|
move: (editor, ctx, x, y, is_overlay = false) => {
|
|
ctx.lineTo(x, y)
|
|
if (is_overlay) {
|
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
|
ctx.globalCompositeOperation = "source-over"
|
|
ctx.globalAlpha = 1
|
|
ctx.filter = "none"
|
|
ctx.drawImage(editor.canvas_current, 0, 0)
|
|
editor.setBrush(editor.layers.overlay)
|
|
ctx.stroke()
|
|
editor.canvas_current.style.opacity = 0
|
|
}
|
|
},
|
|
end: (editor, ctx, x, y, is_overlay = false) => {
|
|
ctx.stroke()
|
|
if (is_overlay) {
|
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
|
editor.canvas_current.style.opacity = ""
|
|
}
|
|
},
|
|
setBrush: (editor, layer) => {
|
|
layer.ctx.globalCompositeOperation = "destination-out"
|
|
},
|
|
hotkey: "e",
|
|
},
|
|
{
|
|
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) => {
|
|
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: toolDoNothing,
|
|
end: toolDoNothing,
|
|
hotkey: "f",
|
|
},
|
|
{
|
|
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,
|
|
hotkey: "p",
|
|
},
|
|
]
|
|
|
|
const IMAGE_EDITOR_ACTIONS = [
|
|
{
|
|
id: "load_mask",
|
|
name: "Load mask from file",
|
|
className: "load_mask",
|
|
icon: "fa-regular fa-folder-open",
|
|
handler: (editor) => {
|
|
let el = document.createElement("input")
|
|
el.setAttribute("type", "file")
|
|
el.addEventListener("change", function() {
|
|
if (this.files.length === 0) {
|
|
return
|
|
}
|
|
|
|
let reader = new FileReader()
|
|
let file = this.files[0]
|
|
|
|
reader.addEventListener("load", function(event) {
|
|
let maskData = reader.result
|
|
|
|
editor.layers.drawing.ctx.clearRect(0, 0, editor.width, editor.height)
|
|
var image = new Image()
|
|
image.onload = () => {
|
|
editor.layers.drawing.ctx.drawImage(image, 0, 0, editor.width, editor.height)
|
|
}
|
|
image.src = maskData
|
|
})
|
|
|
|
if (file) {
|
|
reader.readAsDataURL(file)
|
|
}
|
|
})
|
|
|
|
el.click()
|
|
},
|
|
trackHistory: true,
|
|
},
|
|
{
|
|
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",
|
|
icon: "fa-solid fa-xmark",
|
|
handler: (editor) => {
|
|
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
|
|
imageEditor.setImage(null, editor.width, editor.height) // properly reset the drawing canvas
|
|
},
|
|
trackHistory: true,
|
|
},
|
|
{
|
|
id: "undo",
|
|
name: "Undo",
|
|
icon: "fa-solid fa-rotate-left",
|
|
handler: (editor) => {
|
|
editor.history.undo()
|
|
},
|
|
trackHistory: false,
|
|
},
|
|
{
|
|
id: "redo",
|
|
name: "Redo",
|
|
icon: "fa-solid fa-rotate-right",
|
|
handler: (editor) => {
|
|
editor.history.redo()
|
|
},
|
|
trackHistory: false,
|
|
},
|
|
]
|
|
|
|
var IMAGE_EDITOR_SECTIONS = [
|
|
{
|
|
name: "tool",
|
|
title: "Tool",
|
|
default: "draw",
|
|
options: Array.from(IMAGE_EDITOR_TOOLS.map((t) => t.id)),
|
|
initElement: (element, option) => {
|
|
var tool_info = IMAGE_EDITOR_TOOLS.find((t) => t.id == option)
|
|
element.className = "image-editor-button button"
|
|
var sub_element = document.createElement("div")
|
|
var icon = document.createElement("i")
|
|
tool_info.icon.split(" ").forEach((c) => icon.classList.add(c))
|
|
sub_element.appendChild(icon)
|
|
var label_element = document.createElement("div")
|
|
label_element.classList.add("image-editor-button-label")
|
|
label_element.textContent=tool_info.name
|
|
sub_element.appendChild(label_element)
|
|
element.appendChild(sub_element)
|
|
},
|
|
},
|
|
{
|
|
name: "color",
|
|
title: "Color",
|
|
default: "#f1c232",
|
|
options: [
|
|
"custom",
|
|
"#ea9999",
|
|
"#e06666",
|
|
"#cc0000",
|
|
"#990000",
|
|
"#660000",
|
|
"#f9cb9c",
|
|
"#f6b26b",
|
|
"#e69138",
|
|
"#b45f06",
|
|
"#783f04",
|
|
"#ffe599",
|
|
"#ffd966",
|
|
"#f1c232",
|
|
"#bf9000",
|
|
"#7f6000",
|
|
"#b6d7a8",
|
|
"#93c47d",
|
|
"#6aa84f",
|
|
"#38761d",
|
|
"#274e13",
|
|
"#a4c2f4",
|
|
"#6d9eeb",
|
|
"#3c78d8",
|
|
"#1155cc",
|
|
"#1c4587",
|
|
"#b4a7d6",
|
|
"#8e7cc3",
|
|
"#674ea7",
|
|
"#351c75",
|
|
"#20124d",
|
|
"#d5a6bd",
|
|
"#c27ba0",
|
|
"#a64d79",
|
|
"#741b47",
|
|
"#4c1130",
|
|
"#ffffff",
|
|
"#c0c0c0",
|
|
"#838383",
|
|
"#525252",
|
|
"#000000",
|
|
],
|
|
initElement: (element, option) => {
|
|
if (option == "custom") {
|
|
var input = document.createElement("input")
|
|
input.type = "color"
|
|
element.appendChild(input)
|
|
var span = document.createElement("span")
|
|
span.textContent = "Custom"
|
|
span.onclick = function(e) {
|
|
input.click()
|
|
}
|
|
element.appendChild(span)
|
|
} else {
|
|
element.style.background = option
|
|
}
|
|
},
|
|
getCustom: (editor) => {
|
|
var input = editor.popup.querySelector(".image_editor_color input")
|
|
return input.value
|
|
},
|
|
},
|
|
{
|
|
name: "brush_size",
|
|
title: "Brush Size",
|
|
default: 48,
|
|
options: [6, 12, 16, 24, 30, 40, 48, 64],
|
|
initElement: (element, option) => {
|
|
element.parentElement.style.flex = option
|
|
element.style.width = option + "px"
|
|
element.style.height = option + "px"
|
|
element.style["margin-right"] = "2px"
|
|
element.style["border-radius"] = (option / 2).toFixed() + "px"
|
|
},
|
|
},
|
|
{
|
|
name: "opacity",
|
|
title: "Opacity",
|
|
default: 0,
|
|
options: [0, 0.2, 0.4, 0.6, 0.8],
|
|
initElement: (element, option) => {
|
|
element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px`
|
|
},
|
|
},
|
|
{
|
|
name: "sharpness",
|
|
title: "Sharpness",
|
|
default: 0,
|
|
options: [0, 0.05, 0.1, 0.2, 0.3],
|
|
initElement: (element, option) => {
|
|
var size = 32
|
|
var blur_amount = parseInt(option * size)
|
|
var sub_element = document.createElement("div")
|
|
sub_element.style.background = `var(--background-color3)`
|
|
sub_element.style.filter = `blur(${blur_amount}px)`
|
|
sub_element.style.width = `${size - 2}px`
|
|
sub_element.style.height = `${size - 2}px`
|
|
sub_element.style["border-radius"] = `${size}px`
|
|
element.style.background = "none"
|
|
element.appendChild(sub_element)
|
|
},
|
|
},
|
|
]
|
|
|
|
class EditorHistory {
|
|
constructor(editor) {
|
|
this.editor = editor
|
|
this.events = [] // stack of all events (actions/edits)
|
|
this.current_edit = null
|
|
this.rewind_index = 0 // how many events back into the history we've rewound to. (current state is just after event at index 'length - this.rewind_index - 1')
|
|
}
|
|
push(event) {
|
|
// probably add something here eventually to save state every x events
|
|
if (this.rewind_index != 0) {
|
|
this.events = this.events.slice(0, 0 - this.rewind_index)
|
|
this.rewind_index = 0
|
|
}
|
|
var snapshot_frequency = 20 // (every x edits, take a snapshot of the current drawing state, for faster rewinding)
|
|
if (this.events.length > 0 && this.events.length % snapshot_frequency == 0) {
|
|
event.snapshot = this.editor.layers.drawing.ctx.getImageData(0, 0, this.editor.width, this.editor.height)
|
|
}
|
|
this.events.push(event)
|
|
}
|
|
pushAction(action) {
|
|
this.push({
|
|
type: "action",
|
|
id: action,
|
|
})
|
|
}
|
|
editBegin(x, y) {
|
|
this.current_edit = {
|
|
type: "edit",
|
|
id: this.editor.getOptionValue("tool"),
|
|
options: Object.assign({}, this.editor.options),
|
|
points: [{ x: x, y: y }],
|
|
}
|
|
}
|
|
editMove(x, y) {
|
|
if (this.current_edit) {
|
|
this.current_edit.points.push({ x: x, y: y })
|
|
}
|
|
}
|
|
editEnd(x, y) {
|
|
if (this.current_edit) {
|
|
this.push(this.current_edit)
|
|
this.current_edit = null
|
|
}
|
|
}
|
|
clear() {
|
|
this.events = []
|
|
}
|
|
undo() {
|
|
this.rewindTo(this.rewind_index + 1)
|
|
}
|
|
redo() {
|
|
this.rewindTo(this.rewind_index - 1)
|
|
}
|
|
rewindTo(new_rewind_index) {
|
|
if (new_rewind_index < 0 || new_rewind_index > this.events.length) {
|
|
return // do nothing if target index is out of bounds
|
|
}
|
|
|
|
var ctx = this.editor.layers.drawing.ctx
|
|
ctx.clearRect(0, 0, this.editor.width, this.editor.height)
|
|
|
|
var target_index = this.events.length - 1 - new_rewind_index
|
|
var snapshot_index = target_index
|
|
while (snapshot_index > -1) {
|
|
if (this.events[snapshot_index].snapshot) {
|
|
break
|
|
}
|
|
snapshot_index--
|
|
}
|
|
|
|
if (snapshot_index != -1) {
|
|
ctx.putImageData(this.events[snapshot_index].snapshot, 0, 0)
|
|
}
|
|
|
|
for (var i = snapshot_index + 1; i <= target_index; i++) {
|
|
var event = this.events[i]
|
|
if (event.type == "action") {
|
|
var action = IMAGE_EDITOR_ACTIONS.find((a) => a.id == event.id)
|
|
action.handler(this.editor)
|
|
} else if (event.type == "edit") {
|
|
var tool = IMAGE_EDITOR_TOOLS.find((t) => t.id == event.id)
|
|
this.editor.setBrush(this.editor.layers.drawing, event.options)
|
|
|
|
var first_point = event.points[0]
|
|
tool.begin(this.editor, ctx, first_point.x, first_point.y)
|
|
for (var point_i = 1; point_i < event.points.length; point_i++) {
|
|
tool.move(this.editor, ctx, event.points[point_i].x, event.points[point_i].y)
|
|
}
|
|
var last_point = event.points[event.points.length - 1]
|
|
tool.end(this.editor, ctx, last_point.x, last_point.y)
|
|
}
|
|
}
|
|
|
|
// re-set brush to current settings
|
|
this.editor.setBrush(this.editor.layers.drawing)
|
|
|
|
this.rewind_index = new_rewind_index
|
|
}
|
|
}
|
|
|
|
class ImageEditor {
|
|
constructor(popup, inpainter = false) {
|
|
this.inpainter = inpainter
|
|
this.popup = popup
|
|
this.history = new EditorHistory(this)
|
|
if (inpainter) {
|
|
this.popup.classList.add("inpainter")
|
|
}
|
|
this.drawing = false
|
|
this.temp_previous_tool = null // used for the ctrl-colorpicker functionality
|
|
this.container = popup.querySelector(".editor-controls-center > div")
|
|
this.layers = {}
|
|
var layer_names = ["background", "drawing", "overlay"]
|
|
layer_names.forEach((name) => {
|
|
let canvas = document.createElement("canvas")
|
|
canvas.className = `editor-canvas-${name}`
|
|
this.container.appendChild(canvas)
|
|
this.layers[name] = {
|
|
name: name,
|
|
canvas: canvas,
|
|
ctx: canvas.getContext("2d"),
|
|
}
|
|
})
|
|
|
|
// add mouse handlers
|
|
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("mousemove", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("mouseout", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("mouseenter", this.mouseHandler.bind(this))
|
|
|
|
this.container.addEventListener("touchstart", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("touchcancel", this.mouseHandler.bind(this))
|
|
this.container.addEventListener("touchend", this.mouseHandler.bind(this))
|
|
|
|
// initialize editor controls
|
|
this.options = {}
|
|
this.optionElements = {}
|
|
IMAGE_EDITOR_SECTIONS.forEach((section) => {
|
|
section.id = `image_editor_${section.name}`
|
|
var sectionElement = document.createElement("div")
|
|
sectionElement.className = section.id
|
|
|
|
var title = document.createElement("h4")
|
|
title.innerText = section.title
|
|
sectionElement.appendChild(title)
|
|
|
|
var optionsContainer = document.createElement("div")
|
|
optionsContainer.classList.add("editor-options-container")
|
|
|
|
this.optionElements[section.name] = []
|
|
section.options.forEach((option, index) => {
|
|
var optionHolder = document.createElement("div")
|
|
var optionElement = document.createElement("div")
|
|
optionHolder.appendChild(optionElement)
|
|
section.initElement(optionElement, option)
|
|
optionElement.addEventListener("click", (target) => this.selectOption(section.name, index))
|
|
optionsContainer.appendChild(optionHolder)
|
|
this.optionElements[section.name].push(optionElement)
|
|
})
|
|
this.selectOption(section.name, section.options.indexOf(section.default))
|
|
|
|
sectionElement.appendChild(optionsContainer)
|
|
|
|
this.popup.querySelector(".editor-controls-left").appendChild(sectionElement)
|
|
})
|
|
|
|
this.custom_color_input = this.popup.querySelector(`input[type="color"]`)
|
|
this.custom_color_input.addEventListener("change", () => {
|
|
this.custom_color_input.parentElement.style.background = this.custom_color_input.value
|
|
this.selectOption("color", 0)
|
|
})
|
|
|
|
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
|
|
var buttonContainer = document.createElement("div")
|
|
IMAGE_EDITOR_BUTTONS.forEach((button) => {
|
|
var element = document.createElement("div")
|
|
var icon = document.createElement("i")
|
|
element.className = "image-editor-button button"
|
|
icon.className = button.icon
|
|
element.appendChild(icon)
|
|
element.append(button.name)
|
|
buttonContainer.appendChild(element)
|
|
element.addEventListener("click", (event) => button.handler(this))
|
|
})
|
|
var actionsContainer = document.createElement("div")
|
|
var actionsTitle = document.createElement("h4")
|
|
actionsTitle.textContent = "Actions"
|
|
actionsContainer.appendChild(actionsTitle)
|
|
IMAGE_EDITOR_ACTIONS.forEach((action) => {
|
|
var element = document.createElement("div")
|
|
var icon = document.createElement("i")
|
|
element.className = "image-editor-button button"
|
|
if (action.className) {
|
|
element.className += " " + action.className
|
|
}
|
|
icon.className = action.icon
|
|
element.appendChild(icon)
|
|
element.append(action.name)
|
|
actionsContainer.appendChild(element)
|
|
element.addEventListener("click", (event) => this.runAction(action.id))
|
|
})
|
|
this.popup.querySelector(".editor-controls-right").appendChild(actionsContainer)
|
|
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
|
|
|
|
this.keyHandlerBound = this.keyHandler.bind(this)
|
|
|
|
this.setSize(512, 512)
|
|
}
|
|
show() {
|
|
this.popup.classList.add("active")
|
|
document.addEventListener("keydown", this.keyHandlerBound, true)
|
|
document.addEventListener("keyup", this.keyHandlerBound, true)
|
|
}
|
|
hide() {
|
|
this.popup.classList.remove("active")
|
|
document.removeEventListener("keydown", this.keyHandlerBound, true)
|
|
document.removeEventListener("keyup", this.keyHandlerBound, true)
|
|
}
|
|
setSize(width, height) {
|
|
if (width == this.width && height == this.height) {
|
|
return
|
|
}
|
|
|
|
if (width > height) {
|
|
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
|
|
var multiplier = max_size / width
|
|
width = (multiplier * width).toFixed()
|
|
height = (multiplier * height).toFixed()
|
|
} else {
|
|
var max_size = Math.min(parseInt(window.innerHeight * 0.9), height, 768)
|
|
var multiplier = max_size / height
|
|
width = (multiplier * width).toFixed()
|
|
height = (multiplier * height).toFixed()
|
|
}
|
|
this.width = parseInt(width)
|
|
this.height = parseInt(height)
|
|
|
|
this.container.style.width = width + "px"
|
|
this.container.style.height = height + "px"
|
|
|
|
Object.values(this.layers).forEach((layer) => {
|
|
layer.canvas.width = width
|
|
layer.canvas.height = height
|
|
})
|
|
|
|
if (this.inpainter) {
|
|
this.saveImage() // We've reset the size of the image so inpainting is different
|
|
}
|
|
this.setBrush()
|
|
this.history.clear()
|
|
}
|
|
get tool() {
|
|
var tool_id = this.getOptionValue("tool")
|
|
return IMAGE_EDITOR_TOOLS.find((t) => t.id == tool_id)
|
|
}
|
|
loadTool() {
|
|
this.drawing = false
|
|
this.container.style.cursor = this.tool.cursor
|
|
}
|
|
setImage(url, width, height) {
|
|
this.setSize(width, 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 = () => {
|
|
this.layers.background.ctx.drawImage(image, 0, 0, this.width, this.height)
|
|
}
|
|
image.src = url
|
|
} else {
|
|
this.layers.background.ctx.fillStyle = "#ffffff"
|
|
this.layers.background.ctx.beginPath()
|
|
this.layers.background.ctx.rect(0, 0, this.width, this.height)
|
|
this.layers.background.ctx.fill()
|
|
}
|
|
this.history.clear()
|
|
}
|
|
saveImage() {
|
|
if (!this.inpainter) {
|
|
// This is not an inpainter, so save the image as the new img2img input
|
|
this.layers.background.ctx.drawImage(this.layers.drawing.canvas, 0, 0, this.width, this.height)
|
|
var base64 = this.layers.background.canvas.toDataURL()
|
|
initImagePreview.src = base64 // this will trigger the rest of the app to use it
|
|
} else {
|
|
// This is an inpainter, so make sure the toggle is set accordingly
|
|
var is_blank = !this.layers.drawing.ctx
|
|
.getImageData(0, 0, this.width, this.height)
|
|
.data.some((channel) => channel !== 0)
|
|
maskSetting.checked = !is_blank
|
|
maskSetting.dispatchEvent(new Event("change"))
|
|
}
|
|
this.hide()
|
|
}
|
|
getImg() {
|
|
// a drop-in replacement of the drawingboard version
|
|
return this.layers.drawing.canvas.toDataURL()
|
|
}
|
|
setImg(dataUrl) {
|
|
// a drop-in replacement of the drawingboard version
|
|
var image = new Image()
|
|
image.onload = () => {
|
|
var ctx = this.layers.drawing.ctx
|
|
ctx.clearRect(0, 0, this.width, this.height)
|
|
ctx.globalCompositeOperation = "source-over"
|
|
ctx.globalAlpha = 1
|
|
ctx.filter = "none"
|
|
ctx.drawImage(image, 0, 0, this.width, this.height)
|
|
this.setBrush(this.layers.drawing)
|
|
}
|
|
image.src = dataUrl
|
|
}
|
|
runAction(action_id) {
|
|
var action = IMAGE_EDITOR_ACTIONS.find((a) => a.id == action_id)
|
|
if (action.trackHistory) {
|
|
this.history.pushAction(action_id)
|
|
}
|
|
action.handler(this)
|
|
}
|
|
setBrush(layer = null, options = null) {
|
|
if (options == null) {
|
|
options = this.options
|
|
}
|
|
if (layer) {
|
|
layer.ctx.lineCap = "round"
|
|
layer.ctx.lineJoin = "round"
|
|
layer.ctx.lineWidth = options.brush_size
|
|
layer.ctx.fillStyle = options.color
|
|
layer.ctx.strokeStyle = options.color
|
|
var sharpness = parseInt(options.sharpness * options.brush_size)
|
|
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
|
|
layer.ctx.globalAlpha = 1 - options.opacity
|
|
layer.ctx.globalCompositeOperation = "source-over"
|
|
var tool = IMAGE_EDITOR_TOOLS.find((t) => t.id == options.tool)
|
|
if (tool && tool.setBrush) {
|
|
tool.setBrush(editor, layer)
|
|
}
|
|
} else {
|
|
Object.values(["drawing", "overlay"])
|
|
.map((name) => this.layers[name])
|
|
.forEach((l) => {
|
|
this.setBrush(l)
|
|
})
|
|
}
|
|
}
|
|
get ctx_overlay() {
|
|
return this.layers.overlay.ctx
|
|
}
|
|
get ctx_current() {
|
|
// the idea is this will help support having custom layers and editing each one
|
|
return this.layers.drawing.ctx
|
|
}
|
|
get canvas_current() {
|
|
return this.layers.drawing.canvas
|
|
}
|
|
keyHandler(event) {
|
|
// handles keybinds like ctrl+z, ctrl+y
|
|
if (!this.popup.classList.contains("active")) {
|
|
document.removeEventListener("keydown", this.keyHandlerBound)
|
|
document.removeEventListener("keyup", this.keyHandlerBound)
|
|
return // this catches if something else closes the window but doesnt properly unbind the key handler
|
|
}
|
|
|
|
// keybindings
|
|
if (event.type == "keydown") {
|
|
if ((event.key == "z" || event.key == "Z") && event.ctrlKey) {
|
|
if (!event.shiftKey) {
|
|
this.history.undo()
|
|
} else {
|
|
this.history.redo()
|
|
}
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
else if (event.key == "y" && event.ctrlKey) {
|
|
this.history.redo()
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
else if (event.key === "Escape") {
|
|
this.hide()
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
} else {
|
|
let toolIndex = IMAGE_EDITOR_TOOLS.findIndex( t => t.hotkey ==event.key )
|
|
if (toolIndex != -1) {
|
|
this.selectOption("tool", toolIndex)
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
}
|
|
|
|
// dropper ctrl holding handler stuff
|
|
var dropper_active = this.temp_previous_tool != null
|
|
if (dropper_active && !event.ctrlKey) {
|
|
this.selectOption(
|
|
"tool",
|
|
IMAGE_EDITOR_TOOLS.findIndex((t) => t.id == this.temp_previous_tool)
|
|
)
|
|
this.temp_previous_tool = null
|
|
} else if (!dropper_active && event.ctrlKey) {
|
|
this.temp_previous_tool = this.getOptionValue("tool")
|
|
this.selectOption(
|
|
"tool",
|
|
IMAGE_EDITOR_TOOLS.findIndex((t) => t.id == "colorpicker")
|
|
)
|
|
}
|
|
}
|
|
mouseHandler(event) {
|
|
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
|
|
var x = (event.clientX || 0) - bbox.left
|
|
var y = (event.clientY || 0) - bbox.top
|
|
var type = event.type
|
|
var touchmap = {
|
|
touchstart: "mousedown",
|
|
touchmove: "mousemove",
|
|
touchend: "mouseup",
|
|
touchcancel: "mouseup",
|
|
}
|
|
if (type in touchmap) {
|
|
type = touchmap[type]
|
|
if (event.touches && event.touches[0]) {
|
|
var touch = event.touches[0]
|
|
var x = (touch.clientX || 0) - bbox.left
|
|
var y = (touch.clientY || 0) - bbox.top
|
|
}
|
|
}
|
|
event.preventDefault()
|
|
// do drawing-related stuff
|
|
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
|
|
this.drawing = true
|
|
this.tool.begin(this, this.ctx_current, x, y)
|
|
this.tool.begin(this, this.ctx_overlay, x, y, true)
|
|
this.history.editBegin(x, y)
|
|
}
|
|
if (type == "mouseup" || type == "mousemove") {
|
|
if (this.drawing) {
|
|
if (x > 0 && y > 0) {
|
|
this.tool.move(this, this.ctx_current, x, y)
|
|
this.tool.move(this, this.ctx_overlay, x, y, true)
|
|
this.history.editMove(x, y)
|
|
}
|
|
}
|
|
}
|
|
if (type == "mouseup" || type == "mouseout") {
|
|
if (this.drawing) {
|
|
this.drawing = false
|
|
this.tool.end(this, this.ctx_current, x, y)
|
|
this.tool.end(this, this.ctx_overlay, x, y, true)
|
|
this.history.editEnd(x, y)
|
|
}
|
|
}
|
|
}
|
|
getOptionValue(section_name) {
|
|
var section = IMAGE_EDITOR_SECTIONS.find((s) => s.name == section_name)
|
|
return this.options && section_name in this.options ? this.options[section_name] : section.default
|
|
}
|
|
selectOption(section_name, option_index) {
|
|
var section = IMAGE_EDITOR_SECTIONS.find((s) => s.name == section_name)
|
|
var value = section.options[option_index]
|
|
this.options[section_name] = value == "custom" ? section.getCustom(this) : value
|
|
|
|
this.optionElements[section_name].forEach((element) => element.classList.remove("active"))
|
|
this.optionElements[section_name][option_index].classList.add("active")
|
|
|
|
// change the editor
|
|
this.setBrush()
|
|
if (section.name == "tool") {
|
|
this.loadTool()
|
|
}
|
|
}
|
|
}
|
|
|
|
const imageEditor = new ImageEditor(document.getElementById("image-editor"))
|
|
const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true)
|
|
|
|
imageEditor.setImage(null, 512, 512)
|
|
imageInpainter.setImage(null, 512, 512)
|
|
|
|
document.getElementById("init_image_button_draw").addEventListener("click", () => {
|
|
imageEditor.show()
|
|
})
|
|
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
|
|
imageInpainter.show()
|
|
})
|
|
|
|
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
|
|
// May 2023 - look at using a library instead of custom code: https://github.com/shaneosullivan/example-canvas-fill
|
|
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++ < editor.height &&
|
|
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)
|
|
) {
|
|
pixels.data[linear_cords] = new_color.r
|
|
pixels.data[linear_cords + 1] = new_color.g
|
|
pixels.data[linear_cords + 2] = new_color.b
|
|
pixels.data[linear_cords + 3] = 255
|
|
|
|
if (x > 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 < editor.width - 1) {
|
|
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_right) {
|
|
pixel_stack.push({ x: x + 1, y: y })
|
|
reached_right = true
|
|
}
|
|
} else if (reached_right) {
|
|
reached_right = false
|
|
}
|
|
}
|
|
|
|
linear_cords += editor.width * 4
|
|
}
|
|
}
|
|
the_canvas_context.putImageData(pixels, 0, 0)
|
|
}
|