2022-12-01 11:31:09 +01:00
var editorControlsLeft = document . getElementById ( "image-editor-controls-left" )
const IMAGE _EDITOR _MAX _SIZE = 800
const IMAGE _EDITOR _BUTTONS = [
2023-04-27 19:56:56 +02:00
{
name : "Cancel" ,
icon : "fa-regular fa-circle-xmark" ,
handler : ( editor ) => {
editor . hide ( )
2023-04-28 12:20:44 +02:00
} ,
2023-04-27 19:56:56 +02:00
} ,
{
name : "Save" ,
icon : "fa-solid fa-floppy-disk" ,
handler : ( editor ) => {
editor . saveImage ( )
2023-04-28 12:20:44 +02:00
} ,
} ,
2022-12-01 11:31:09 +01:00
]
2022-12-06 09:26:51 +01:00
const defaultToolBegin = ( editor , ctx , x , y , is _overlay = false ) => {
2023-04-27 19:56:56 +02:00
ctx . beginPath ( )
ctx . moveTo ( x , y )
2022-12-06 09:26:51 +01:00
}
const defaultToolMove = ( editor , ctx , x , y , is _overlay = false ) => {
2023-04-27 19:56:56 +02:00
ctx . lineTo ( x , y )
if ( is _overlay ) {
ctx . clearRect ( 0 , 0 , editor . width , editor . height )
ctx . stroke ( )
}
2022-12-06 09:26:51 +01:00
}
const defaultToolEnd = ( editor , ctx , x , y , is _overlay = false ) => {
2023-04-27 19:56:56 +02:00
ctx . stroke ( )
if ( is _overlay ) {
ctx . clearRect ( 0 , 0 , editor . width , editor . height )
}
2022-12-06 09:26:51 +01:00
}
2022-12-30 10:07:46 +01:00
const toolDoNothing = ( editor , ctx , x , y , is _overlay = false ) => { }
2022-12-06 09:26:51 +01:00
2022-12-01 11:31:09 +01:00
const IMAGE _EDITOR _TOOLS = [
2023-04-27 19:56:56 +02:00
{
id : "draw" ,
name : "Draw" ,
icon : "fa-solid fa-pencil" ,
cursor : "url(/media/images/fa-pencil.svg) 0 24, pointer" ,
begin : defaultToolBegin ,
move : defaultToolMove ,
2023-04-28 12:20:44 +02:00
end : defaultToolEnd ,
2023-07-02 18:38:16 +02:00
hotkey : "d" ,
2023-04-27 19:56:56 +02:00
} ,
{
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"
2023-04-28 12:20:44 +02:00
} ,
2023-07-02 18:38:16 +02:00
hotkey : "e" ,
2023-04-27 19:56:56 +02:00
} ,
{
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 ,
2023-04-28 12:20:44 +02:00
end : toolDoNothing ,
2023-07-02 18:38:16 +02:00
hotkey : "f" ,
2023-04-27 19:56:56 +02:00
} ,
{
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 ) ,
2023-04-28 12:20:44 +02:00
b : drawn _rgb [ 2 ] * drawn _opacity + img _rgb [ 2 ] * ( 1 - drawn _opacity ) ,
2023-04-27 19:56:56 +02:00
} )
editor . custom _color _input . dispatchEvent ( new Event ( "change" ) )
}
} ,
move : toolDoNothing ,
2023-04-28 12:20:44 +02:00
end : toolDoNothing ,
2023-07-02 18:38:16 +02:00
hotkey : "p" ,
2023-04-28 12:20:44 +02:00
} ,
2022-12-06 09:26:51 +01:00
]
const IMAGE _EDITOR _ACTIONS = [
2023-04-27 19:56:56 +02:00
{
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 ( )
} ,
2023-04-28 12:20:44 +02:00
trackHistory : true ,
2023-04-27 19:56:56 +02:00
} ,
{
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 ( )
} ,
2023-04-28 12:20:44 +02:00
trackHistory : true ,
2023-04-27 19:56:56 +02:00
} ,
{
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
} ,
2023-04-28 12:20:44 +02:00
trackHistory : true ,
2023-04-27 19:56:56 +02:00
} ,
{
id : "undo" ,
name : "Undo" ,
icon : "fa-solid fa-rotate-left" ,
handler : ( editor ) => {
editor . history . undo ( )
} ,
2023-04-28 12:20:44 +02:00
trackHistory : false ,
2023-04-27 19:56:56 +02:00
} ,
{
id : "redo" ,
name : "Redo" ,
icon : "fa-solid fa-rotate-right" ,
handler : ( editor ) => {
editor . history . redo ( )
} ,
2023-04-28 12:20:44 +02:00
trackHistory : false ,
} ,
2022-12-01 11:31:09 +01:00
]
var IMAGE _EDITOR _SECTIONS = [
2023-04-27 19:56:56 +02:00
{
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 )
2023-07-02 18:38:16 +02:00
var label _element = document . createElement ( "span" )
label _element . classList . add ( "image-editor-button-label" )
label _element . textContent = tool _info . name
sub _element . appendChild ( label _element )
2023-04-27 19:56:56 +02:00
element . appendChild ( sub _element )
2023-04-28 12:20:44 +02:00
} ,
2023-04-27 19:56:56 +02:00
} ,
{
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" ,
2023-04-28 12:20:44 +02:00
"#000000" ,
2023-04-27 19:56:56 +02:00
] ,
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
2023-04-28 12:20:44 +02:00
} ,
2023-04-27 19:56:56 +02:00
} ,
{
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"
2023-04-28 12:20:44 +02:00
} ,
2023-04-27 19:56:56 +02:00
} ,
{
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 `
2023-04-28 12:20:44 +02:00
} ,
2023-04-27 19:56:56 +02:00
} ,
{
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 )
2023-04-28 12:20:44 +02:00
} ,
} ,
2022-12-01 11:31:09 +01:00
]
2022-12-06 09:26:51 +01:00
class EditorHistory {
2023-04-27 19:56:56 +02:00
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" ,
2023-04-28 12:20:44 +02:00
id : action ,
2023-04-27 19:56:56 +02:00
} )
}
editBegin ( x , y ) {
this . current _edit = {
type : "edit" ,
id : this . editor . getOptionValue ( "tool" ) ,
options : Object . assign ( { } , this . editor . options ) ,
2023-04-28 12:20:44 +02:00
points : [ { x : x , y : y } ] ,
2023-04-27 19:56:56 +02:00
}
}
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
}
2022-12-06 09:26:51 +01:00
}
2022-12-01 11:31:09 +01:00
class ImageEditor {
2023-04-27 19:56:56 +02:00
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 ,
2023-04-28 12:20:44 +02:00
ctx : canvas . getContext ( "2d" ) ,
2023-04-27 19:56:56 +02:00
}
} )
// 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
}
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 ( )
}
2023-07-02 18:38:16 +02:00
else if ( event . key == "y" && event . ctrlKey ) {
2023-04-27 19:56:56 +02:00
this . history . redo ( )
event . stopPropagation ( )
event . preventDefault ( )
}
2023-07-02 18:38:16 +02:00
else if ( event . key === "Escape" ) {
2023-04-27 19:56:56 +02:00
this . hide ( )
event . stopPropagation ( )
event . preventDefault ( )
2023-07-02 18:38:16 +02:00
} else {
let toolIndex = IMAGE _EDITOR _TOOLS . findIndex ( t => t . hotkey == event . key )
if ( toolIndex != - 1 ) {
this . selectOption ( "tool" , toolIndex )
}
2023-04-27 19:56:56 +02:00
}
}
// 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" ,
2023-04-28 12:20:44 +02:00
touchcancel : "mouseup" ,
2023-04-27 19:56:56 +02:00
}
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 ) {
2023-07-02 18:38:16 +02:00
console . log ( "SELECT" , section _name , option _index )
2023-04-27 19:56:56 +02:00
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 ( )
}
}
2022-12-01 11:31:09 +01:00
}
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" , ( ) => {
2023-04-27 19:56:56 +02:00
imageEditor . show ( )
2022-12-01 11:31:09 +01:00
} )
document . getElementById ( "init_image_button_inpaint" ) . addEventListener ( "click" , ( ) => {
2023-04-27 19:56:56 +02:00
imageInpainter . show ( )
2022-12-02 10:10:21 +01:00
} )
2022-12-07 08:41:49 +01:00
img2imgUnload ( ) // no init image when the app starts
2022-12-30 10:07:46 +01:00
function rgbToHex ( rgb ) {
2023-04-27 19:56:56 +02:00
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 )
2022-12-30 10:07:46 +01:00
}
function hexToRgb ( hex ) {
2023-04-27 19:56:56 +02:00
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 ) ,
2023-04-28 12:20:44 +02:00
b : parseInt ( result [ 3 ] , 16 ) ,
2023-04-27 19:56:56 +02:00
}
: null
2022-12-30 10:07:46 +01:00
}
function pixelCompare ( int1 , int2 ) {
2023-04-27 19:56:56 +02:00
return Math . abs ( int1 - int2 ) < 4
2022-12-30 10:07:46 +01:00
}
// adapted from https://ben.akrin.com/canvas_fill/fill_04.html
2023-05-24 06:38:00 +02:00
// May 2023 - look at using a library instead of custom code: https://github.com/shaneosullivan/example-canvas-fill
2022-12-30 10:07:46 +01:00
function flood _fill ( editor , the _canvas _context , x , y , color ) {
2023-04-27 19:56:56 +02:00
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 ] ,
2023-04-28 12:20:44 +02:00
a : pixels . data [ linear _cords + 3 ] ,
2023-04-27 19:56:56 +02:00
}
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 ) ) ,
2023-04-28 12:20:44 +02:00
b : parseInt ( color . b * opacity + original _color . b * ( 1 - opacity ) ) ,
2023-04-27 19:56:56 +02:00
}
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 ) &&
2023-04-28 12:20:44 +02:00
pixelCompare ( pixels . data [ linear _cords + 1 ] , original _color . g ) &&
pixelCompare ( pixels . data [ linear _cords + 2 ] , original _color . b )
2023-04-27 19:56:56 +02:00
) {
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 ) &&
2023-04-28 12:20:44 +02:00
pixelCompare ( pixels . data [ linear _cords + 1 ] , original _color . g ) &&
pixelCompare ( pixels . data [ linear _cords + 2 ] , original _color . b )
2023-04-27 19:56:56 +02:00
) {
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 )
2022-12-30 10:07:46 +01:00
}