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 = [
{
name : "Cancel" ,
icon : "fa-regular fa-circle-xmark" ,
handler : editor => {
2022-12-06 09:26:51 +01:00
editor . hide ( )
2022-12-01 11:31:09 +01:00
}
} ,
{
name : "Save" ,
icon : "fa-solid fa-floppy-disk" ,
handler : editor => {
editor . saveImage ( )
}
}
]
2022-12-06 09:26:51 +01:00
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 )
}
}
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 = [
{
id : "draw" ,
name : "Draw" ,
2022-12-06 09:26:51 +01:00
icon : "fa-solid fa-pencil" ,
2022-12-30 10:07:46 +01:00
cursor : "url(/media/images/fa-pencil.svg) 0 24, pointer" ,
2022-12-06 09:26:51 +01:00
begin : defaultToolBegin ,
move : defaultToolMove ,
end : defaultToolEnd
2022-12-01 11:31:09 +01:00
} ,
{
id : "erase" ,
name : "Erase" ,
2022-12-06 09:26:51 +01:00
icon : "fa-solid fa-eraser" ,
2022-12-30 10:07:46 +01:00
cursor : "url(/media/images/fa-eraser.svg) 0 14, pointer" ,
2022-12-06 09:26:51 +01:00
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"
}
} ,
2022-12-30 10:07:46 +01: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 ,
end : toolDoNothing
} ,
2022-12-06 09:26:51 +01:00
{
id : "colorpicker" ,
2022-12-30 10:07:46 +01:00
name : "Picker" ,
2022-12-06 09:26:51 +01:00
icon : "fa-solid fa-eye-dropper" ,
2022-12-30 10:07:46 +01:00
cursor : "url(/media/images/fa-eye-dropper.svg) 0 24, pointer" ,
2022-12-06 09:26:51 +01:00
begin : ( editor , ctx , x , y , is _overlay = false ) => {
2022-12-30 10:07:46 +01:00
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" ) )
}
2022-12-06 09:26:51 +01:00
} ,
2022-12-30 10:07:46 +01:00
move : toolDoNothing ,
end : toolDoNothing
2022-12-06 09:26:51 +01:00
}
]
const IMAGE _EDITOR _ACTIONS = [
2023-03-11 08:07:51 +01: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 ( )
} ,
trackHistory : true
} ,
2022-12-22 01:20:07 +01:00
{
2022-12-30 10:07:46 +01:00
id : "fill_all" ,
name : "Fill all" ,
icon : "fa-solid fa-paint-roller" ,
2022-12-22 01:20:07 +01:00
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
} ,
2022-12-06 09:26:51 +01:00
{
id : "clear" ,
name : "Clear" ,
icon : "fa-solid fa-xmark" ,
handler : ( editor ) => {
editor . ctx _current . clearRect ( 0 , 0 , editor . width , editor . height )
2023-03-24 08:08:51 +01:00
imageEditor . setImage ( null , editor . width , editor . height ) // properly reset the drawing canvas
2022-12-17 00:29:54 +01:00
} ,
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
2022-12-01 11:31:09 +01:00
}
]
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 )
sub _element . append ( tool _info . name )
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"
2022-12-06 11:47:45 +01:00
span . onclick = function ( e ) {
input . click ( )
}
2022-12-01 11:31:09 +01:00
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 ,
2022-12-07 07:57:40 +01:00
options : [ 6 , 12 , 16 , 24 , 30 , 40 , 48 , 64 ] ,
2022-12-01 11:31:09 +01:00
initElement : ( element , option ) => {
element . parentElement . style . flex = option
element . style . width = option + "px"
element . style . height = option + "px"
2022-12-07 07:57:40 +01:00
element . style [ 'margin-right' ] = '2px'
2022-12-01 11:31:09 +01:00
element . style [ "border-radius" ] = ( option / 2 ) . toFixed ( ) + "px"
}
} ,
{
name : "opacity" ,
title : "Opacity" ,
default : 0 ,
2022-12-06 09:26:51 +01:00
options : [ 0 , 0.2 , 0.4 , 0.6 , 0.8 ] ,
2022-12-01 11:31:09 +01:00
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) `
2023-02-19 21:08:06 +01:00
sub _element . style . width = ` ${ size - 2 } px `
sub _element . style . height = ` ${ size - 2 } px `
2022-12-01 11:31:09 +01:00
sub _element . style [ 'border-radius' ] = ` ${ size } px `
element . style . background = "none"
element . appendChild ( sub _element )
}
}
]
2022-12-06 09:26:51 +01:00
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
}
}
2022-12-01 11:31:09 +01:00
class ImageEditor {
constructor ( popup , inpainter = false ) {
this . inpainter = inpainter
this . popup = popup
2022-12-06 09:26:51 +01:00
this . history = new EditorHistory ( this )
2022-12-01 11:31:09 +01:00
if ( inpainter ) {
this . popup . classList . add ( "inpainter" )
}
this . drawing = false
2022-12-06 09:26:51 +01:00
this . temp _previous _tool = null // used for the ctrl-colorpicker functionality
2022-12-01 11:31:09 +01:00
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" ) )
2023-01-10 17:26:26 +01:00
this . selectOption ( "opacity" , IMAGE _EDITOR _SECTIONS . find ( s => s . name == "opacity" ) . options . indexOf ( 0.4 ) )
2022-12-01 11:31:09 +01:00
}
// 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 ) )
} )
2022-12-06 09:26:51 +01:00
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"
2023-03-11 08:07:51 +01:00
if ( action . className ) {
element . className += " " + action . className
}
2022-12-06 09:26:51 +01:00
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 )
2022-12-01 11:31:09 +01:00
this . popup . querySelector ( ".editor-controls-right" ) . appendChild ( buttonContainer )
2022-12-06 09:26:51 +01:00
this . keyHandlerBound = this . keyHandler . bind ( this )
this . setSize ( 512 , 512 )
}
show ( ) {
this . popup . classList . add ( "active" )
2023-04-01 07:50:36 +02:00
document . addEventListener ( "keydown" , this . keyHandlerBound , true )
document . addEventListener ( "keyup" , this . keyHandlerBound , true )
2022-12-06 09:26:51 +01:00
}
hide ( ) {
this . popup . classList . remove ( "active" )
2023-04-01 07:50:36 +02:00
document . removeEventListener ( "keydown" , this . keyHandlerBound , true )
document . removeEventListener ( "keyup" , this . keyHandlerBound , true )
2022-12-01 11:31:09 +01:00
}
setSize ( width , height ) {
if ( width == this . width && height == this . height ) {
return
}
if ( width > height ) {
2022-12-17 00:29:54 +01:00
var max _size = Math . min ( parseInt ( window . innerWidth * 0.9 ) , width , 768 )
2022-12-01 11:31:09 +01:00
var multiplier = max _size / width
width = ( multiplier * width ) . toFixed ( )
height = ( multiplier * height ) . toFixed ( )
}
else {
2022-12-17 00:29:54 +01:00
var max _size = Math . min ( parseInt ( window . innerHeight * 0.9 ) , height , 768 )
2022-12-01 11:31:09 +01:00
var multiplier = max _size / height
width = ( multiplier * width ) . toFixed ( )
height = ( multiplier * height ) . toFixed ( )
}
2022-12-30 10:07:46 +01:00
this . width = parseInt ( width )
this . height = parseInt ( height )
2022-12-01 11:31:09 +01:00
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 ( )
2022-12-06 09:26:51 +01:00
this . history . clear ( )
2022-12-01 11:31:09 +01:00
}
2022-12-06 09:26:51 +01:00
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 ;
2022-12-01 11:31:09 +01:00
}
setImage ( url , width , height ) {
this . setSize ( width , height )
this . layers . background . ctx . clearRect ( 0 , 0 , this . width , this . height )
2022-12-22 01:35:03 +01:00
if ( ! ( url && this . inpainter ) ) {
this . layers . drawing . ctx . clearRect ( 0 , 0 , this . width , this . height )
}
2022-12-01 11:31:09 +01:00
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 ( )
}
2022-12-06 09:26:51 +01:00
this . history . clear ( )
2022-12-01 11:31:09 +01:00
}
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
}
2022-12-06 09:26:51 +01:00
this . hide ( )
2022-12-01 11:31:09 +01:00
}
getImg ( ) { // a drop-in replacement of the drawingboard version
return this . layers . drawing . canvas . toDataURL ( )
}
2022-12-06 09:26:51 +01:00
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
2022-12-01 11:31:09 +01:00
}
2022-12-06 09:26:51 +01:00
runAction ( action _id ) {
var action = IMAGE _EDITOR _ACTIONS . find ( a => a . id == action _id )
2022-12-17 00:29:54 +01:00
if ( action . trackHistory ) {
this . history . pushAction ( action _id )
}
2022-12-06 09:26:51 +01:00
action . handler ( this )
2022-12-01 11:31:09 +01:00
}
2022-12-06 09:26:51 +01:00
setBrush ( layer = null , options = null ) {
if ( options == null ) {
options = this . options
}
2022-12-01 11:31:09 +01:00
if ( layer ) {
layer . ctx . lineCap = "round"
layer . ctx . lineJoin = "round"
2022-12-06 09:26:51 +01:00
layer . ctx . lineWidth = options . brush _size
layer . ctx . fillStyle = options . color
layer . ctx . strokeStyle = options . color
var sharpness = parseInt ( options . sharpness * options . brush _size )
2022-12-01 11:31:09 +01:00
layer . ctx . filter = sharpness == 0 ? ` none ` : ` blur( ${ sharpness } px) `
2022-12-06 09:26:51 +01:00
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 )
}
2022-12-01 11:31:09 +01:00
}
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
}
2022-12-06 09:26:51 +01:00
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 ( )
}
2023-04-01 09:00:19 +02:00
event . stopPropagation ( ) ;
2023-03-31 09:02:44 +02:00
event . preventDefault ( ) ;
2022-12-06 09:26:51 +01:00
}
if ( event . key == "y" && event . ctrlKey ) {
this . history . redo ( )
2023-04-01 09:00:19 +02:00
event . stopPropagation ( ) ;
2023-03-31 09:02:44 +02:00
event . preventDefault ( ) ;
2022-12-06 09:26:51 +01:00
}
2022-12-29 08:50:56 +01:00
if ( event . key === "Escape" ) {
this . hide ( )
2023-04-01 09:00:19 +02:00
event . stopPropagation ( ) ;
2023-03-31 09:02:44 +02:00
event . preventDefault ( ) ;
2022-12-29 08:50:56 +01:00
}
2022-12-06 09:26:51 +01: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" ) )
}
}
2022-12-01 11:31:09 +01:00
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
}
}
2022-12-02 10:10:21 +01:00
event . preventDefault ( )
2022-12-01 11:31:09 +01:00
// do drawing-related stuff
if ( type == "mousedown" || ( type == "mouseenter" && event . buttons == 1 ) ) {
2022-12-06 09:26:51 +01:00
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 )
2022-12-01 11:31:09 +01:00
}
if ( type == "mouseup" || type == "mousemove" ) {
if ( this . drawing ) {
if ( x > 0 && y > 0 ) {
2022-12-06 09:26:51 +01:00
this . tool . move ( this , this . ctx _current , x , y )
this . tool . move ( this , this . ctx _overlay , x , y , true )
this . history . editMove ( x , y )
2022-12-01 11:31:09 +01:00
}
}
}
if ( type == "mouseup" || type == "mouseout" ) {
if ( this . drawing ) {
this . drawing = false
2022-12-06 09:26:51 +01:00
this . tool . end ( this , this . ctx _current , x , y )
this . tool . end ( this , this . ctx _overlay , x , y , true )
this . history . editEnd ( x , y )
2022-12-01 11:31:09 +01:00
}
}
}
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" ) {
2022-12-06 09:26:51 +01:00
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" , ( ) => {
2022-12-06 09:26:51 +01:00
imageEditor . show ( )
2022-12-01 11:31:09 +01:00
} )
document . getElementById ( "init_image_button_inpaint" ) . addEventListener ( "click" , ( ) => {
2022-12-06 09:26:51 +01: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 ) {
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 ++ < 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 ) ;
}