2014-02-24 05:57:52 +00:00
2019-10-30 17:38:16 +00:00
const default _magnification = 1 ;
2020-12-10 22:59:27 +00:00
const default _tool = get _tool _by _id ( TOOL _PENCIL ) ;
2019-10-30 17:38:16 +00:00
2019-10-30 00:48:51 +00:00
const default _canvas _width = 683 ;
const default _canvas _height = 384 ;
let my _canvas _width = default _canvas _width ;
let my _canvas _height = default _canvas _height ;
2019-10-29 20:37:53 +00:00
let aliasing = true ;
let transparency = false ;
let monochrome = false ;
2014-02-24 05:57:52 +00:00
2019-10-30 17:38:16 +00:00
let magnification = default _magnification ;
2019-10-29 20:37:53 +00:00
let return _to _magnification = 4 ;
2014-11-29 02:27:51 +00:00
2021-02-11 02:00:38 +00:00
const main _canvas = make _canvas ( ) ;
main _canvas . classList . add ( "main-canvas" ) ;
const main _ctx = main _canvas . ctx ;
2017-06-24 18:28:23 +00:00
2020-05-31 14:53:09 +00:00
const default _palette = [
"rgb(0,0,0)" , // Black
"rgb(128,128,128)" , // Dark Gray
"rgb(128,0,0)" , // Dark Red
"rgb(128,128,0)" , // Pea Green
"rgb(0,128,0)" , // Dark Green
"rgb(0,128,128)" , // Slate
"rgb(0,0,128)" , // Dark Blue
"rgb(128,0,128)" , // Lavender
"rgb(128,128,64)" , //
"rgb(0,64,64)" , //
"rgb(0,128,255)" , //
"rgb(0,64,128)" , //
"rgb(64,0,255)" , //
"rgb(128,64,0)" , //
"rgb(255,255,255)" , // White
"rgb(192,192,192)" , // Light Gray
"rgb(255,0,0)" , // Bright Red
"rgb(255,255,0)" , // Yellow
"rgb(0,255,0)" , // Bright Green
"rgb(0,255,255)" , // Cyan
"rgb(0,0,255)" , // Bright Blue
"rgb(255,0,255)" , // Magenta
"rgb(255,255,128)" , //
"rgb(0,255,128)" , //
"rgb(128,255,255)" , //
"rgb(128,128,255)" , //
"rgb(255,0,128)" , //
"rgb(255,128,64)" , //
] ;
const monochrome _palette _as _colors = [
"rgb(0,0,0)" ,
"rgb(9,9,9)" ,
"rgb(18,18,18)" ,
"rgb(27,27,27)" ,
"rgb(37,37,37)" ,
"rgb(46,46,46)" ,
"rgb(55,55,55)" ,
"rgb(63,63,63)" ,
"rgb(73,73,73)" ,
"rgb(82,82,82)" ,
"rgb(92,92,92)" ,
"rgb(101,101,101)" ,
"rgb(110,110,110)" ,
"rgb(119,119,119)" ,
"rgb(255,255,255)" ,
"rgb(250,250,250)" ,
"rgb(242,242,242)" ,
"rgb(212,212,212)" ,
"rgb(201,201,201)" ,
"rgb(191,191,191)" ,
"rgb(182,182,182)" ,
"rgb(159,159,159)" ,
"rgb(128,128,128)" ,
"rgb(173,173,173)" ,
"rgb(164,164,164)" ,
"rgb(155,155,155)" ,
"rgb(146,146,146)" ,
"rgb(137,137,137)" ,
] ;
2019-12-19 21:33:48 +00:00
let palette = default _palette ;
2019-10-29 20:37:53 +00:00
let polychrome _palette = palette ;
let monochrome _palette = make _monochrome _palette ( ) ;
2017-06-24 18:28:23 +00:00
2020-05-30 13:35:46 +00:00
// https://github.com/kouzhudong/win2k/blob/ce6323f76d5cd7d136b74427dad8f94ee4c389d2/trunk/private/shell/win16/comdlg/color.c#L38-L43
2021-06-19 23:58:47 +00:00
// These are a fallback in case colors are not received from some driver.
2020-05-30 13:35:46 +00:00
// const default_basic_colors = [
// "#8080FF", "#80FFFF", "#80FF80", "#80FF00", "#FFFF80", "#FF8000", "#C080FF", "#FF80FF",
// "#0000FF", "#00FFFF", "#00FF80", "#40FF00", "#FFFF00", "#C08000", "#C08080", "#FF00FF",
// "#404080", "#4080FF", "#00FF00", "#808000", "#804000", "#FF8080", "#400080", "#8000FF",
// "#000080", "#0080FF", "#008000", "#408000", "#FF0000", "#A00000", "#800080", "#FF0080",
// "#000040", "#004080", "#004000", "#404000", "#800000", "#400000", "#400040", "#800040",
// "#000000", "#008080", "#408080", "#808080", "#808040", "#C0C0C0", "#400040", "#FFFFFF",
// ];
// Grabbed with Color Cop from the screen with Windows 98 SE running in VMWare
const basic _colors = [
"#FF8080" , "#FFFF80" , "#80FF80" , "#00FF80" , "#80FFFF" , "#0080FF" , "#FF80C0" , "#FF80FF" ,
"#FF0000" , "#FFFF00" , "#80FF00" , "#00FF40" , "#00FFFF" , "#0080C0" , "#8080C0" , "#FF00FF" ,
"#804040" , "#FF8040" , "#00FF00" , "#008080" , "#004080" , "#8080FF" , "#800040" , "#FF0080" ,
"#800000" , "#FF8000" , "#008000" , "#008040" , "#0000FF" , "#0000A0" , "#800080" , "#8000FF" ,
"#400000" , "#804000" , "#004000" , "#004040" , "#000080" , "#000040" , "#400040" , "#400080" ,
"#000000" , "#808000" , "#808040" , "#808080" , "#408080" , "#C0C0C0" , "#400040" , "#FFFFFF" ,
] ;
let custom _colors = [
"#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" ,
"#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" , "#FFFFFF" ,
] ;
2021-03-23 01:23:45 +00:00
// The methods in systemHooks can be overridden by a containing page like 98.js.org which hosts jspaint in a same-origin iframe.
// This allows integrations like setting the wallpaper as the background of the host page, or saving files to a server.
// This API may be removed at any time (and perhaps replaced by something based around postMessage)
window . systemHooks = window . systemHooks || { } ;
window . systemHookDefaults = {
2021-08-03 10:28:19 +00:00
// named to be distinct from various platform APIs (showSaveFilePicker, saveAs, electron's showSaveDialog; and saveFile is too ambiguous)
// could call it saveFileAs maybe but then it'd be weird that you don't pass in the file directly
showSaveFileDialog : async ( { formats , defaultFileName , defaultPath , defaultFileFormatID , getBlob , savedCallbackUnreliable , dialogTitle } ) => {
2021-03-23 01:23:45 +00:00
// Note: showSaveFilePicker currently doesn't support suggesting a filename,
// or retrieving which file type was selected in the dialog (you have to get it (guess it) from the file name)
// In particular, some formats are ambiguous with the file name, e.g. different bit depths of BMP files.
// So, it's a tradeoff with the benefit of overwriting on Save.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker
2021-08-29 15:54:57 +00:00
// Also, if you're using accessibility options Speech Recognition or Eye Gaze Mode,
// `showSaveFilePicker` fails based on a notion of it not being a "user gesture".
// `saveAs` will likely also fail on the same basis,
// but at least in chrome, there's a "Downloads Blocked" icon with a popup where you can say Always Allow.
// I can't detect when it's allowed or blocked, but `saveAs` has a better chance of working,
// so in Speech Recognition and Eye Gaze Mode, I set a global flag temporarily to disable File System Access API (window.untrusted_gesture).
if ( window . showSaveFilePicker && ! window . untrusted _gesture ) {
2021-03-23 01:23:45 +00:00
// We can't get the selected file type, not even from newHandle.getFile()
// so limit formats shown to a set that can all be used by their unique file extensions
2021-04-01 04:45:07 +00:00
// formats = formats_unique_per_file_extension(formats);
// OR, show two dialogs, one for the format and then one for the save location.
const { newFileFormatID } = await save _as _prompt ( { dialogTitle , defaultFileName , defaultFileFormatID , formats , promptForName : false } ) ;
const new _format = formats . find ( ( format ) => format . formatID === newFileFormatID ) ;
const blob = await getBlob ( new _format && new _format . formatID ) ;
formats = [ new _format ] ;
2021-05-14 02:11:27 +00:00
let newHandle ;
let newFileName ;
try {
newHandle = await showSaveFilePicker ( {
types : formats . map ( ( format ) => {
return {
description : format . name ,
accept : {
[ format . mimeType ] : format . extensions . map ( ( extension ) => "." + extension )
}
2021-03-23 01:23:45 +00:00
}
2021-05-14 02:11:27 +00:00
} )
} ) ;
newFileName = newHandle . name ;
2021-08-17 21:31:47 +00:00
const newFileExtension = get _file _extension ( newFileName ) ;
if ( ! newFileExtension ) {
2021-11-26 23:37:58 +00:00
show _error _message ( ` Missing file extension. \n \n Try adding . ${ new _format . extensions [ 0 ] } to the name. ` ) ;
2021-08-17 21:31:47 +00:00
return ;
}
if ( ! new _format . extensions . includes ( newFileExtension ) ) {
// Closest translation: "Paint cannot save to the same filename with a different file type."
2021-11-26 23:37:58 +00:00
show _error _message ( ` Wrong file extension for selected file type. \n \n Try adding . ${ new _format . extensions [ 0 ] } to the name. ` ) ;
2021-08-17 21:31:47 +00:00
return ;
}
2021-05-14 02:11:27 +00:00
// const new_format =
// get_format_from_extension(formats, newHandle.name) ||
// formats.find((format)=> format.formatID === defaultFileFormatID);
// const blob = await getBlob(new_format && new_format.formatID);
const writableStream = await newHandle . createWritable ( ) ;
await writableStream . write ( blob ) ;
await writableStream . close ( ) ;
} catch ( error ) {
if ( error . name === "AbortError" ) {
// user canceled save
return ;
}
2021-08-03 10:28:19 +00:00
// console.warn("Error during showSaveFileDialog (for showSaveFilePicker; now falling back to saveAs)", error);
2021-05-14 02:11:27 +00:00
// newFileName = (newFileName || file_name || localize("untitled"))
// .replace(/\.(bmp|dib|a?png|gif|jpe?g|jpe|jfif|tiff?|webp|raw)$/i, "") +
// "." + new_format.extensions[0];
// saveAs(blob, newFileName);
2021-08-29 15:54:57 +00:00
if ( error . message . match ( /gesture|activation/ ) ) {
2021-08-17 21:31:47 +00:00
// show_error_message("Your browser blocked the file from being saved, because you didn't use the mouse or keyboard directly to save. Try looking for a Downloads Blocked icon and say Always Allow, or save again with the keyboard or mouse.", error);
show _error _message ( "Sorry, due to browser security measures, you must use the keyboard or mouse directly to save." ) ;
return ;
}
show _error _message ( localize ( "Failed to save document." ) , error ) ;
2021-05-14 02:11:27 +00:00
return ;
}
2021-03-23 01:23:45 +00:00
savedCallbackUnreliable && savedCallbackUnreliable ( {
2021-05-14 02:11:27 +00:00
newFileName : newFileName ,
2021-04-01 04:17:11 +00:00
newFileFormatID : new _format && new _format . formatID ,
2021-03-23 01:23:45 +00:00
newFileHandle : newHandle ,
newBlob : blob ,
} ) ;
} else {
2021-04-01 04:45:07 +00:00
const { newFileName , newFileFormatID } = await save _as _prompt ( { dialogTitle , defaultFileName , defaultFileFormatID , formats } ) ;
2021-04-01 04:17:11 +00:00
const blob = await getBlob ( newFileFormatID ) ;
saveAs ( blob , newFileName ) ;
savedCallbackUnreliable && savedCallbackUnreliable ( {
newFileName ,
newFileFormatID ,
newFileHandle : null ,
newBlob : blob ,
2021-03-23 01:23:45 +00:00
} ) ;
}
} ,
2021-08-29 15:54:57 +00:00
showOpenFileDialog : async ( { formats } ) => {
if ( window . untrusted _gesture ) {
// We can't show a file picker RELIABLY.
show _error _message ( "Sorry, a file picker cannot be shown when using Speech Recognition or Eye Gaze Mode. You must click File > Open directly with the mouse, or press Ctrl+O on the keyboard." ) ;
throw new Error ( "can't show file picker reliably" ) ;
}
2021-08-03 10:28:52 +00:00
if ( window . showOpenFilePicker ) {
const [ fileHandle ] = await window . showOpenFilePicker ( {
types : formats . map ( ( format ) => {
return {
description : format . name ,
accept : {
[ format . mimeType ] : format . extensions . map ( ( extension ) => "." + extension )
}
}
} )
} ) ;
const file = await fileHandle . getFile ( ) ;
return { file , fileHandle } ;
} else {
// @TODO: specify mime types?
return new Promise ( ( resolve ) => {
const $input = $ ( "<input type='file'>" )
. on ( "change" , ( ) => {
resolve ( { file : $input [ 0 ] . files [ 0 ] } ) ;
$input . remove ( ) ;
} )
. appendTo ( $app )
. hide ( )
. trigger ( "click" ) ;
} ) ;
}
} ,
2021-08-02 20:20:08 +00:00
writeBlobToHandle : async ( save _file _handle , blob ) => {
if ( save _file _handle && save _file _handle . createWritable ) {
await confirm _overwrite ( ) ;
try {
const writableStream = await save _file _handle . createWritable ( ) ;
await writableStream . write ( blob ) ;
await writableStream . close ( ) ;
} catch ( error ) {
if ( error . name === "AbortError" ) {
2021-08-29 15:54:57 +00:00
// user canceled save (this might not be a real error code that can occur here)
2021-08-02 20:20:08 +00:00
return ;
}
if ( error . name === "NotAllowedError" ) {
// use didn't give permission to save
// is this too much of a warning?
show _error _message ( localize ( "Save was interrupted, so your file has not been saved." ) , error ) ;
return ;
}
2021-08-29 15:54:57 +00:00
if ( error . name === "SecurityError" ) {
// not in a user gesture ("User activation is required to request permissions.")
saveAs ( blob , file _name ) ;
return ;
}
2021-08-02 20:20:08 +00:00
}
} else {
saveAs ( blob , file _name ) ;
// hopefully if the page reloads/closes the save dialog/download will persist and succeed?
}
} ,
readBlobFromHandle : async ( file _handle ) => {
if ( file _handle && file _handle . getFile ) {
const file = await file _handle . getFile ( ) ;
return file ;
} else {
throw new Error ( ` Unknown file handle ( ${ file _handle } ) ` ) ;
// show_error_message(`${localize("Failed to open document.")}\n${localize("An unsupported operation was attempted.")}`, error);
}
} ,
2021-03-23 01:23:45 +00:00
setWallpaperTiled : ( canvas ) => {
const wallpaperCanvas = make _canvas ( screen . width , screen . height ) ;
const pattern = wallpaperCanvas . ctx . createPattern ( canvas , "repeat" ) ;
wallpaperCanvas . ctx . fillStyle = pattern ;
wallpaperCanvas . ctx . fillRect ( 0 , 0 , wallpaperCanvas . width , wallpaperCanvas . height ) ;
systemHooks . setWallpaperCentered ( wallpaperCanvas ) ;
} ,
setWallpaperCentered : ( canvas ) => {
2021-08-03 10:28:19 +00:00
systemHooks . showSaveFileDialog ( {
2021-03-23 01:23:45 +00:00
dialogTitle : localize ( "Save As" ) ,
defaultName : ` ${ file _name . replace ( /\.(bmp|dib|a?png|gif|jpe?g|jpe|jfif|tiff?|webp|raw)$/i , "" ) } wallpaper.png ` ,
defaultFileFormatID : "image/png" ,
formats : image _formats ,
getBlob : ( new _file _type ) => {
return new Promise ( ( resolve ) => {
write _image _file ( canvas , new _file _type , ( blob ) => {
resolve ( blob ) ;
} ) ;
} ) ;
} ,
} ) ;
} ,
} ;
for ( const [ key , defaultValue ] of Object . entries ( window . systemHookDefaults ) ) {
window . systemHooks [ key ] = window . systemHooks [ key ] || defaultValue ;
}
2021-08-17 21:31:47 +00:00
function get _file _extension ( file _path _or _name ) {
// does NOT accept a file extension itself as input - if input does not have a dot, returns empty string
return file _path _or _name . match ( /\.([^./]+)$/ ) ? . [ 1 ] || "" ;
}
2021-03-23 01:23:45 +00:00
function get _format _from _extension ( formats , file _path _or _name _or _ext ) {
2021-08-17 21:31:47 +00:00
// accepts a file extension as input, or a file name, or path
2021-03-23 01:23:45 +00:00
const ext _match = file _path _or _name _or _ext . match ( /\.([^.]+)$/ ) ;
const ext = ext _match ? ext _match [ 1 ] . toLowerCase ( ) : file _path _or _name _or _ext ; // excluding dot
for ( const format of formats ) {
if ( format . extensions . includes ( ext ) ) {
return format ;
}
}
}
2021-02-04 06:28:13 +00:00
const image _formats = [ ] ;
// const ext_to_image_formats = {}; // there can be multiple with the same extension, e.g. different bit depth BMP files
// const mime_type_to_image_formats = {};
const add _image _format = ( mime _type , name _and _exts ) => {
// Note: some localizations have commas instead of semicolons to separate file extensions
// Assumption: file extensions are never localized
const format = {
formatID : mime _type ,
mimeType : mime _type ,
name : localize ( name _and _exts ) . replace ( /\s+\([^(]+$/ , "" ) ,
nameWithExtensions : localize ( name _and _exts ) ,
extensions : [ ] ,
} ;
const ext _regexp = /\*\.([^);,]+)/g ;
if ( get _direction ( ) === "rtl" ) {
const rlm = "\u200F" ;
const lrm = "\u200E" ;
format . nameWithExtensions = format . nameWithExtensions . replace ( ext _regexp , ` ${ rlm } *. ${ lrm } $ 1 ${ rlm } ` ) ;
}
let match ;
// eslint-disable-next-line no-cond-assign
while ( match = ext _regexp . exec ( name _and _exts ) ) {
const ext = match [ 1 ] ;
// ext_to_image_formats[ext] = ext_to_image_formats[ext] || [];
// ext_to_image_formats[ext].push(format);
// mime_type_to_image_formats[mime_type] = mime_type_to_image_formats[mime_type] || [];
// mime_type_to_image_formats[mime_type].push(format);
format . extensions . push ( ext ) ;
}
image _formats . push ( format ) ;
} ;
2021-02-07 01:49:16 +00:00
// First file extension in a parenthetical defines default for the format.
2021-02-04 06:28:13 +00:00
// Strings are localized in add_image_format, don't need localize() here.
add _image _format ( "image/png" , "PNG (*.png)" ) ;
add _image _format ( "image/webp" , "WebP (*.webp)" ) ;
add _image _format ( "image/gif" , "GIF (*.gif)" ) ;
add _image _format ( "image/tiff" , "TIFF (*.tif;*.tiff)" ) ;
add _image _format ( "image/jpeg" , "JPEG (*.jpg;*.jpeg;*.jpe;*.jfif)" ) ;
2021-03-23 01:23:45 +00:00
add _image _format ( "image/x-bmp-1bpp" , "Monochrome Bitmap (*.bmp;*.dib)" ) ;
add _image _format ( "image/x-bmp-4bpp" , "16 Color Bitmap (*.bmp;*.dib)" ) ;
add _image _format ( "image/x-bmp-8bpp" , "256 Color Bitmap (*.bmp;*.dib)" ) ;
add _image _format ( "image/bmp" , "24-bit Bitmap (*.bmp;*.dib)" ) ;
// add_image_format("image/x-bmp-32bpp", "32-bit Transparent Bitmap (*.bmp;*.dib)");
// Only support 24bpp BMP files for File System Access API and Electron save dialog,
// as these APIs don't allow you to access the selected file type.
// You can only guess it from the file extension the user types.
const formats _unique _per _file _extension = ( formats ) => {
// first handle BMP format specifically to make sure the 24-bpp is the selected BMP format
formats = formats . filter ( ( format ) =>
format . extensions . includes ( "bmp" ) ? format . mimeType === "image/bmp" : true
)
// then generally uniquify on extensions
// (this could be overzealous in case of partial overlap in extensions of different formats,
// but in general it needs special care anyways, to decide which format should win)
// This can't be simply chained with the above because it needs to use the intermediate, partially filtered formats array.
return formats . filter ( ( format , format _index ) =>
! format . extensions . some ( ( extension ) =>
formats . some ( ( other _format , other _format _index ) =>
other _format _index < format _index &&
other _format . extensions . includes ( extension )
)
)
) ;
} ;
2021-02-04 06:28:13 +00:00
const palette _formats = [ ] ;
for ( const [ format _id , format ] of Object . entries ( AnyPalette . formats ) ) {
if ( format . write ) {
palette _formats . push ( {
formatID : format _id ,
name : format . name ,
nameWithExtensions : ` ${ format . name } ( ${
format . fileExtensions . map ( ( extension ) => ` *. ${ extension } ` ) . join ( ";" )
} ) ` ,
extensions : format . fileExtensions ,
} ) ;
}
}
palette _formats . sort ( ( a , b ) =>
// Order important formats first, starting with RIFF PAL format:
( b . formatID === "RIFF_PALETTE" ) - ( a . formatID === "RIFF_PALETTE" ) ||
( b . formatID === "GIMP_PALETTE" ) - ( a . formatID === "GIMP_PALETTE" ) ||
0
) ;
2020-05-29 03:27:29 +00:00
// declared like this for Cypress tests
window . default _brush _shape = "circle" ;
window . default _brush _size = 4 ;
window . default _eraser _size = 8 ;
window . default _airbrush _size = 9 ;
window . default _pencil _size = 1 ;
window . default _stroke _size = 1 ; // applies to lines, curves, shape outlines
// declared like this for Cypress tests
window . brush _shape = default _brush _shape ;
window . brush _size = default _brush _size
window . eraser _size = default _eraser _size ;
window . airbrush _size = default _airbrush _size ;
window . pencil _size = default _pencil _size ;
window . stroke _size = default _stroke _size ; // applies to lines, curves, shape outlines
2019-12-18 04:41:37 +00:00
let tool _transparent _mode = false ;
2014-03-03 22:32:52 +00:00
2019-10-29 20:29:38 +00:00
let stroke _color ;
let fill _color ;
let stroke _color _k = "foreground" ; // enum of "foreground", "background", "ternary"
let fill _color _k = "background" ; // enum of "foreground", "background", "ternary"
2014-03-03 22:32:52 +00:00
2019-10-30 17:38:16 +00:00
let selected _tool = default _tool ;
2019-10-29 20:37:53 +00:00
let selected _tools = [ selected _tool ] ;
let return _to _tools = [ selected _tool ] ;
2021-02-11 02:04:35 +00:00
window . selected _colors = { // declared like this for Cypress tests
2015-02-24 00:18:07 +00:00
foreground : "" ,
background : "" ,
ternary : "" ,
} ;
2014-03-03 22:32:52 +00:00
2019-10-29 20:29:38 +00:00
let selection ; //the one and only OnCanvasSelection
let textbox ; //the one and only OnCanvasTextBox
let helper _layer ; //the OnCanvasHelperLayer for the grid and tool previews
2019-10-29 20:37:53 +00:00
let show _grid = false ;
let text _tool _font = {
2019-10-21 16:14:25 +00:00
family : '"Arial"' , // should be an exact value detected by Font Detective
2014-08-16 20:46:30 +00:00
size : 12 ,
2019-10-21 16:14:25 +00:00
line _scale : 20 / 12 ,
bold : false ,
italic : false ,
underline : false ,
vertical : false ,
color : "" ,
background : "" ,
2014-08-16 20:46:30 +00:00
} ;
2019-12-14 21:18:31 +00:00
let root _history _node = make _history _node ( { name : "App Not Loaded Properly - Please send a bug report." } ) ; // will be replaced
2019-12-08 22:42:52 +00:00
let current _history _node = root _history _node ;
2019-12-16 01:54:03 +00:00
let history _node _to _cancel _to = null ;
2019-12-08 20:12:12 +00:00
/** array of history nodes */
let undos = [ ] ;
/** array of history nodes */
let redos = [ ] ;
2014-03-03 22:32:52 +00:00
2019-10-29 20:29:38 +00:00
let file _name ;
2021-08-02 20:20:08 +00:00
let system _file _handle ; // For saving over opened file on Save. Can be different type for File System Access API vs Electron.
2019-10-29 20:37:53 +00:00
let saved = true ;
2014-05-04 13:32:02 +00:00
2020-05-29 03:45:48 +00:00
/** works in canvas coordinates */
let pointer ;
/** works in canvas coordinates */
let pointer _start ;
/** works in canvas coordinates */
let pointer _previous ;
2019-12-10 04:10:33 +00:00
let pointer _active = false ;
let pointer _type , pointer _buttons ;
2019-10-30 00:48:51 +00:00
let reverse ;
let ctrl ;
let button ;
2019-11-03 15:36:34 +00:00
let pointer _over _canvas = false ;
let update _helper _layer _on _pointermove _active = false ;
2014-05-04 13:32:02 +00:00
2020-05-29 03:45:48 +00:00
/** works in client coordinates */
2019-12-10 04:10:33 +00:00
let pointers = [ ] ;
2020-05-10 04:12:52 +00:00
const update _from _url _params = ( ) => {
2020-04-23 06:20:32 +00:00
if ( location . hash . match ( /eye-gaze-mode/i ) ) {
if ( ! $ ( "body" ) . hasClass ( "eye-gaze-mode" ) ) {
$ ( "body" ) . addClass ( "eye-gaze-mode" ) ;
$G . triggerHandler ( "eye-gaze-mode-toggled" ) ;
$G . triggerHandler ( "theme-load" ) ; // signal layout change
}
} else {
if ( $ ( "body" ) . hasClass ( "eye-gaze-mode" ) ) {
$ ( "body" ) . removeClass ( "eye-gaze-mode" ) ;
$G . triggerHandler ( "eye-gaze-mode-toggled" ) ;
$G . triggerHandler ( "theme-load" ) ; // signal layout change
}
}
if ( location . hash . match ( /vertical-color-box-mode|eye-gaze-mode/i ) ) {
if ( ! $ ( "body" ) . hasClass ( "vertical-color-box-mode" ) ) {
$ ( "body" ) . addClass ( "vertical-color-box-mode" ) ;
$G . triggerHandler ( "vertical-color-box-mode-toggled" ) ;
$G . triggerHandler ( "theme-load" ) ; // signal layout change
}
} else {
if ( $ ( "body" ) . hasClass ( "vertical-color-box-mode" ) ) {
$ ( "body" ) . removeClass ( "vertical-color-box-mode" ) ;
$G . triggerHandler ( "vertical-color-box-mode-toggled" ) ;
$G . triggerHandler ( "theme-load" ) ; // signal layout change
}
}
2020-05-10 04:12:52 +00:00
if ( location . hash . match ( /speech-recognition-mode/i ) ) {
window . enable _speech _recognition && enable _speech _recognition ( ) ;
} else {
window . disable _speech _recognition && disable _speech _recognition ( ) ;
}
2021-11-26 04:34:35 +00:00
$ ( "body" ) . toggleClass ( "compare-reference" , ! ! location . hash . match ( /compare-reference/i ) ) ;
2021-11-26 05:54:09 +00:00
$ ( "body" ) . toggleClass ( "compare-reference-tool-windows" , ! ! location . hash . match ( /compare-reference-tool-windows/i ) ) ;
setTimeout ( ( ) => {
if ( location . hash . match ( /compare-reference/i ) ) { // including compare-reference-tool-windows
select _tool ( get _tool _by _id ( TOOL _SELECT ) ) ;
const test _canvas _width = 576 ;
const test _canvas _height = 432 ;
if ( main _canvas . width !== test _canvas _width || main _canvas . height !== test _canvas _height ) {
// Unfortunately, right now this can cause a reverse "Save changes?" dialog,
// where Discard will restore your drawing, Cancel will discard it, and Save will save a blank canvas,
// because the load from storage happens after this resize.
// But this is just a helper for development, so it's not a big deal.
// are_you_sure here doesn't help, either.
// are_you_sure(() => {
resize _canvas _without _saving _dimensions ( test _canvas _width , test _canvas _height ) ;
// });
}
if ( ! location . hash . match ( /compare-reference-tool-windows/i ) ) {
$toolbox . dock ( $left ) ;
$colorbox . dock ( $bottom ) ;
window . debugKeepMenusOpen = false ;
}
}
if ( location . hash . match ( /compare-reference-tool-windows/i ) ) {
$toolbox . undock _to ( 84 , 35 ) ;
$colorbox . undock _to ( 239 , 195 ) ;
window . debugKeepMenusOpen = true ;
// $(".help-menu-button").click(); // have to trigger pointerdown/up, it doesn't respond to click
// $(".help-menu-button").trigger("pointerdown").trigger("pointerup"); // and it doesn't use jQuery
$ ( ".help-menu-button" ) [ 0 ] . dispatchEvent ( new Event ( "pointerdown" ) ) ;
$ ( ".help-menu-button" ) [ 0 ] . dispatchEvent ( new Event ( "pointerup" ) ) ;
$ ( '[aria-label="About Paint"]' ) [ 0 ] . dispatchEvent ( new Event ( "pointerenter" ) ) ;
}
} , 500 ) ;
2020-04-23 06:20:32 +00:00
} ;
2020-05-10 04:12:52 +00:00
update _from _url _params ( ) ;
$G . on ( "hashchange popstate change-url-params" , update _from _url _params ) ;
2020-04-23 06:20:32 +00:00
// handle backwards compatibility URLs
2020-04-18 19:25:55 +00:00
if ( location . search . match ( /eye-gaze-mode/ ) ) {
2020-05-10 02:40:05 +00:00
change _url _param ( "eye-gaze-mode" , true , { replace _history _state : true } ) ;
2020-05-10 04:12:52 +00:00
update _from _url _params ( ) ;
2020-04-23 06:20:32 +00:00
}
if ( location . search . match ( /vertical-colors?-box/ ) ) {
2020-05-10 02:40:05 +00:00
change _url _param ( "vertical-color-box" , true , { replace _history _state : true } ) ;
2020-05-10 04:12:52 +00:00
update _from _url _params ( ) ;
2020-04-18 19:25:55 +00:00
}
2019-10-29 20:29:38 +00:00
const $app = $ ( E ( "div" ) ) . addClass ( "jspaint" ) . appendTo ( "body" ) ;
2014-05-04 13:32:02 +00:00
2019-10-29 20:29:38 +00:00
const $V = $ ( E ( "div" ) ) . addClass ( "vertical" ) . appendTo ( $app ) ;
const $H = $ ( E ( "div" ) ) . addClass ( "horizontal" ) . appendTo ( $V ) ;
2014-05-04 13:32:02 +00:00
2021-11-26 04:42:57 +00:00
const $canvas _area = $ ( E ( "div" ) ) . addClass ( "canvas-area inset-deep" ) . appendTo ( $H ) ;
2014-05-06 02:09:52 +00:00
2021-02-11 02:00:38 +00:00
const $canvas = $ ( main _canvas ) . appendTo ( $canvas _area ) ;
2021-07-30 07:36:45 +00:00
$canvas . css ( "touch-action" , "none" ) ;
2021-02-11 02:00:38 +00:00
let canvas _bounding _client _rect = main _canvas . getBoundingClientRect ( ) ; // cached for performance, updated later
2021-02-09 01:11:06 +00:00
const $canvas _handles = $Handles ( $canvas _area , $canvas _area , {
2021-02-11 02:00:38 +00:00
get _rect : ( ) => ( { x : 0 , y : 0 , width : main _canvas . width , height : main _canvas . height } ) ,
2021-02-08 23:58:10 +00:00
set _rect : ( { width , height } ) => resize _canvas _and _save _dimensions ( width , height ) ,
2018-01-10 23:18:26 +00:00
outset : 4 ,
2021-02-10 16:26:34 +00:00
get _handles _offset _left : ( ) => parseFloat ( $canvas _area . css ( "padding-left" ) ) + 1 ,
get _handles _offset _top : ( ) => parseFloat ( $canvas _area . css ( "padding-top" ) ) + 1 ,
get _ghost _offset _left : ( ) => parseFloat ( $canvas _area . css ( "padding-left" ) ) + 1 ,
get _ghost _offset _top : ( ) => parseFloat ( $canvas _area . css ( "padding-top" ) ) + 1 ,
2019-10-29 19:51:23 +00:00
size _only : true ,
2018-01-10 23:18:26 +00:00
} ) ;
2019-12-05 03:44:07 +00:00
// hack: fix canvas handles causing document to scroll when selecting/deselecting
// by overriding these methods
$canvas _handles . hide = ( ) => { $canvas _handles . css ( { opacity : 0 , pointerEvents : "none" } ) ; } ;
$canvas _handles . show = ( ) => { $canvas _handles . css ( { opacity : "" , pointerEvents : "" } ) ; } ;
2014-05-06 02:09:52 +00:00
2021-11-26 04:42:57 +00:00
const $top = $ ( E ( "div" ) ) . addClass ( "component-area top" ) . prependTo ( $V ) ;
const $bottom = $ ( E ( "div" ) ) . addClass ( "component-area bottom" ) . appendTo ( $V ) ;
const $left = $ ( E ( "div" ) ) . addClass ( "component-area left" ) . prependTo ( $H ) ;
const $right = $ ( E ( "div" ) ) . addClass ( "component-area right" ) . appendTo ( $H ) ;
2020-12-16 21:40:29 +00:00
// there's also probably a CSS solution alternative to this
if ( get _direction ( ) === "rtl" ) {
$left . appendTo ( $H ) ;
$right . prependTo ( $H ) ;
}
2014-05-04 13:32:02 +00:00
2019-10-29 20:29:38 +00:00
const $status _area = $ ( E ( "div" ) ) . addClass ( "status-area" ) . appendTo ( $V ) ;
2021-11-26 04:34:43 +00:00
const $status _text = $ ( E ( "div" ) ) . addClass ( "status-text status-field inset-shallow" ) . appendTo ( $status _area ) ;
const $status _position = $ ( E ( "div" ) ) . addClass ( "status-coordinates status-field inset-shallow" ) . appendTo ( $status _area ) ;
const $status _size = $ ( E ( "div" ) ) . addClass ( "status-coordinates status-field inset-shallow" ) . appendTo ( $status _area ) ;
2014-05-04 13:32:02 +00:00
2019-12-22 04:24:12 +00:00
const $news _indicator = $ ( `
< a class = 'news-indicator' href = '#project-news' >
< img src = 'images/winter/present.png' width = '24' height = '22' alt = '' / >
2020-12-19 22:49:10 +00:00
< span class = 'marquee' dir = 'ltr' style = '--text-width: 52ch; --animation-duration: 5s;' >
< span >
< strong > New ! < / s t r o n g > & n b s p ; L o c a l i z a t i o n , E y e G a z e M o d e , a n d S p e e c h R e c o g n i t i o n !
< / s p a n >
2019-12-22 05:20:42 +00:00
< / s p a n >
2019-12-22 04:24:12 +00:00
< / a >
` );
$news _indicator . on ( "click auxclick" , ( event ) => {
event . preventDefault ( ) ;
show _news ( ) ;
} ) ;
2021-06-19 23:58:47 +00:00
// @TODO: use localStorage to show until clicked, if available
2019-12-22 04:24:12 +00:00
// and show for a longer period of time after the update, if available
2020-12-19 22:49:10 +00:00
if ( Date . now ( ) < Date . parse ( "Jan 5 2021 23:42:42 GMT-0500" ) ) {
2019-12-22 04:24:12 +00:00
$status _area . append ( $news _indicator ) ;
}
2019-10-29 18:46:29 +00:00
$status _text . default = ( ) => {
2020-12-05 22:33:21 +00:00
$status _text . text ( localize ( "For Help, click Help Topics on the Help Menu." ) ) ;
2017-05-26 20:31:26 +00:00
} ;
$status _text . default ( ) ;
2015-06-16 01:29:30 +00:00
2019-10-26 16:29:09 +00:00
// menu bar
2019-10-29 20:29:38 +00:00
let menu _bar _outside _frame = false ;
2019-10-26 16:29:09 +00:00
if ( frameElement ) {
try {
2021-11-20 02:00:22 +00:00
if ( parent . MenuBar ) {
MenuBar = parent . MenuBar ;
2019-10-26 16:29:09 +00:00
menu _bar _outside _frame = true ;
}
// eslint-disable-next-line no-empty
} catch ( e ) { }
}
2021-11-20 02:00:22 +00:00
const menu _bar = MenuBar ( menus ) ;
2019-10-26 16:29:09 +00:00
if ( menu _bar _outside _frame ) {
2021-11-20 02:00:22 +00:00
$ ( menu _bar . element ) . insertBefore ( frameElement ) ;
2019-10-26 16:29:09 +00:00
} else {
2021-11-20 02:00:22 +00:00
$ ( menu _bar . element ) . prependTo ( $V ) ;
2019-10-26 16:29:09 +00:00
}
2021-11-20 02:00:22 +00:00
$ ( menu _bar . element ) . on ( "info" , ( event ) => {
$status _text . text ( event . detail ? . description ? ? "" ) ;
2019-10-26 16:29:09 +00:00
} ) ;
2021-11-20 02:00:22 +00:00
$ ( menu _bar . element ) . on ( "default-info" , ( ) => {
2019-10-26 16:29:09 +00:00
$status _text . default ( ) ;
} ) ;
// </menu bar>
2020-04-23 06:20:32 +00:00
let $toolbox = $ToolBox ( tools ) ;
2020-04-23 23:14:36 +00:00
// let $toolbox2 = $ToolBox(extra_tools, true);//.hide();
2018-02-17 06:45:06 +00:00
// Note: a second $ToolBox doesn't work because they use the same tool options (which could be remedied)
// If there's to be extra tools, they should probably get a window, with different UI
// so it can display names of the tools, and maybe authors and previews (and not necessarily icons)
2020-04-23 06:20:32 +00:00
let $colorbox = $ColorBox ( $ ( "body" ) . hasClass ( "vertical-color-box-mode" ) ) ;
$G . on ( "vertical-color-box-mode-toggled" , ( ) => {
$colorbox . destroy ( ) ;
$colorbox = $ColorBox ( $ ( "body" ) . hasClass ( "vertical-color-box-mode" ) ) ;
prevent _selection ( $colorbox ) ;
} ) ;
$G . on ( "eye-gaze-mode-toggled" , ( ) => {
$colorbox . destroy ( ) ;
$colorbox = $ColorBox ( $ ( "body" ) . hasClass ( "vertical-color-box-mode" ) ) ;
prevent _selection ( $colorbox ) ;
$toolbox . destroy ( ) ;
$toolbox = $ToolBox ( tools ) ;
prevent _selection ( $toolbox ) ;
2020-04-23 23:14:36 +00:00
// $toolbox2.destroy();
// $toolbox2 = $ToolBox(extra_tools, true);
// prevent_selection($toolbox2);
2020-04-23 06:20:32 +00:00
} ) ;
2014-05-04 13:32:02 +00:00
2019-10-29 18:46:29 +00:00
$G . on ( "resize" , ( ) => { // for browser zoom, and in-app zoom of the canvas
2019-10-26 22:00:29 +00:00
update _canvas _rect ( ) ;
2019-09-30 18:23:20 +00:00
update _disable _aa ( ) ;
} ) ;
2019-10-29 18:46:29 +00:00
$canvas _area . on ( "scroll" , ( ) => {
2019-10-26 22:00:29 +00:00
update _canvas _rect ( ) ;
2019-10-21 18:16:36 +00:00
} ) ;
2019-10-29 18:46:29 +00:00
$canvas _area . on ( "resize" , ( ) => {
2019-10-21 18:16:36 +00:00
update _magnified _canvas _size ( ) ;
2019-10-01 16:19:25 +00:00
} ) ;
2018-01-27 21:50:47 +00:00
2021-01-31 16:29:56 +00:00
// Despite overflow:hidden on html and body,
// focusing elements that are partially offscreen can still scroll the page.
// For example, with Edit Colors dialog partially offscreen, navigating the color grid.
// We need to prevent (reset) scroll on focus, and also avoid scrollIntoView().
// Listening for scroll here is mainly in case a case is forgotten, like scrollIntoView,
// in which case it will flash sometimes but at least not end up with part of
// the application scrolled off the screen with no scrollbar to get it back.
2021-03-23 01:23:45 +00:00
$G . on ( "scroll focusin" , ( ) => {
2021-01-31 16:29:56 +00:00
window . scrollTo ( 0 , 0 ) ;
} ) ;
2021-03-23 01:23:45 +00:00
$ ( "body" ) . on ( "dragover dragenter" , ( event ) => {
const dt = event . originalEvent . dataTransfer ;
const has _files = dt && Array . from ( dt . types ) . includes ( "Files" ) ;
if ( has _files ) {
event . preventDefault ( ) ;
2015-06-22 01:05:48 +00:00
}
2021-03-23 01:23:45 +00:00
} ) . on ( "drop" , async ( event ) => {
if ( event . isDefaultPrevented ( ) ) {
2015-06-22 01:05:48 +00:00
return ;
}
2021-03-23 01:23:45 +00:00
const dt = event . originalEvent . dataTransfer ;
const has _files = dt && Array . from ( dt . types ) . includes ( "Files" ) ;
if ( has _files ) {
event . preventDefault ( ) ;
// @TODO: sort files/items in priority of image, theme, palette
// and then try loading them in series, with async await to avoid race conditions?
// or maybe support opening multiple documents in tabs
// Note: don't use FS Access API in Electron app because:
// 1. it's faulty (permissions problems, 0 byte files maybe due to the perms problems)
// 2. we want to save the file.path, which the dt.files code path takes care of
if ( window . FileSystemHandle && ! window . is _electron _app ) {
for ( const item of dt . items ) {
// kind will be 'file' for file/directory entries.
if ( item . kind === 'file' ) {
2021-08-02 21:33:11 +00:00
let handle ;
try {
handle = await item . getAsFileSystemHandle ( ) ;
} catch ( error ) {
// I'm not sure when this happens.
// should this use "An invalid file handle was associated with %1." message?
show _error _message ( localize ( "File not found." ) , error ) ;
return ;
}
2021-03-23 01:23:45 +00:00
if ( handle . kind === 'file' ) {
2021-08-02 21:33:11 +00:00
let file ;
try {
file = await handle . getFile ( ) ;
} catch ( error ) {
// NotFoundError can happen when the file was moved or deleted,
// then dragged and dropped via the browser's downloads bar, or some other outdated file listing.
show _error _message ( localize ( "File not found." ) , error ) ;
return ;
}
2021-08-02 20:20:08 +00:00
open _from _file ( file , handle ) ;
2021-07-10 16:25:23 +00:00
if ( window . _open _images _serially ) {
// For testing a suite of files:
await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
} else {
// Normal behavior: only open one file.
return ;
}
2021-03-23 01:23:45 +00:00
}
// else if (handle.kind === 'directory') {}
}
}
} else if ( dt . files && dt . files . length ) {
2021-07-10 16:25:23 +00:00
if ( window . _open _images _serially ) {
// For testing a suite of files, such as http://www.schaik.com/pngsuite/
let i = 0 ;
const iid = setInterval ( ( ) => {
console . log ( "opening" , dt . files [ i ] . name ) ;
open _from _file ( dt . files [ i ] ) ;
i ++ ;
if ( i >= dt . files . length ) {
clearInterval ( iid ) ;
}
} , 1500 ) ;
} else {
// Normal behavior: only open one file.
open _from _file ( dt . files [ 0 ] ) ;
}
2018-01-27 21:50:47 +00:00
}
2014-05-04 13:32:02 +00:00
}
} ) ;
2019-10-29 18:46:29 +00:00
$G . on ( "keydown" , e => {
2015-06-22 01:05:48 +00:00
if ( e . isDefaultPrevented ( ) ) {
return ;
}
2021-11-26 17:05:07 +00:00
if ( e . key === "Escape" ) { // Note: Escape handled below too! (after input/textarea return condition)
2019-12-03 20:02:27 +00:00
if ( textbox && textbox . $editor . is ( e . target ) ) {
deselect ( ) ;
}
}
2019-12-21 22:40:39 +00:00
if (
// Ctrl+Shift+Y
( e . ctrlKey || e . metaKey ) && e . shiftKey && ! e . altKey &&
2021-11-26 17:05:07 +00:00
e . key . toUpperCase ( ) === "Y"
2019-12-21 22:40:39 +00:00
) {
show _document _history ( ) ;
e . preventDefault ( ) ;
return ;
}
2020-01-05 22:27:51 +00:00
// @TODO: return if menus/menubar focused or focus in dialog window
2018-01-31 05:59:01 +00:00
// or maybe there's a better way to do this that works more generally
// maybe it should only handle the event if document.activeElement is the body or html element?
// (or $app could have a tabIndex and no focus style and be focused under various conditions,
// if that turned out to make more sense for some reason)
2015-06-22 01:05:48 +00:00
if (
e . target instanceof HTMLInputElement ||
e . target instanceof HTMLTextAreaElement
) {
return ;
}
2018-01-31 05:59:01 +00:00
2020-01-05 22:27:51 +00:00
// @TODO: preventDefault in all cases where the event is handled
2018-01-31 05:59:01 +00:00
// also, ideally check that modifiers *aren't* pressed
// probably best to use a library at this point!
if ( selection ) {
2019-10-29 20:29:38 +00:00
const nudge _selection = ( delta _x , delta _y ) => {
2018-01-31 05:59:01 +00:00
selection . x += delta _x ;
selection . y += delta _y ;
selection . position ( ) ;
} ;
2021-11-26 17:05:07 +00:00
switch ( e . key ) {
case "ArrowLeft" :
2018-01-31 05:59:01 +00:00
nudge _selection ( - 1 , 0 ) ;
e . preventDefault ( ) ;
break ;
2021-11-26 17:05:07 +00:00
case "ArrowRight" :
2018-01-31 05:59:01 +00:00
nudge _selection ( + 1 , 0 ) ;
e . preventDefault ( ) ;
break ;
2021-11-26 17:05:07 +00:00
case "ArrowDown" :
2018-01-31 05:59:01 +00:00
nudge _selection ( 0 , + 1 ) ;
e . preventDefault ( ) ;
break ;
2021-11-26 17:05:07 +00:00
case "ArrowUp" :
2018-01-31 05:59:01 +00:00
nudge _selection ( 0 , - 1 ) ;
e . preventDefault ( ) ;
break ;
}
}
2021-11-26 17:05:07 +00:00
if ( e . key === "Escape" ) { // Note: Escape handled above too!
if ( selection ) {
2014-05-04 13:32:02 +00:00
deselect ( ) ;
2021-11-26 17:05:07 +00:00
} else {
2014-05-04 13:32:02 +00:00
cancel ( ) ;
}
2020-05-11 20:08:28 +00:00
window . stopSimulatingGestures && window . stopSimulatingGestures ( ) ;
window . trace _and _sketch _stop && window . trace _and _sketch _stop ( ) ;
2021-11-26 17:05:07 +00:00
} else if ( e . key === "Enter" ) {
2014-09-23 02:10:11 +00:00
if ( selection ) {
deselect ( ) ;
}
2021-11-26 17:05:07 +00:00
} else if ( e . key === "F4" ) {
2014-05-04 13:32:02 +00:00
redo ( ) ;
2021-11-26 17:05:07 +00:00
} else if ( e . key === "Delete" ) {
2014-05-04 13:32:02 +00:00
delete _selection ( ) ;
2021-11-26 17:05:07 +00:00
} else if ( e . code === "NumpadAdd" || e . code === "NumpadSubtract" ) {
const plus = e . code === "NumpadAdd" ;
const minus = e . code === "NumpadSubtract" ;
2019-11-03 15:36:34 +00:00
const delta = plus - minus ; // const delta = +plus++ -minus--; // Δ = ±±±±
2018-01-21 15:03:59 +00:00
2014-05-11 00:03:13 +00:00
if ( selection ) {
2019-10-29 22:19:38 +00:00
selection . scale ( 2 * * delta ) ;
2014-05-11 00:03:13 +00:00
} else {
2020-12-10 22:59:27 +00:00
if ( selected _tool . id === TOOL _BRUSH ) {
2014-05-11 00:03:13 +00:00
brush _size = Math . max ( 1 , Math . min ( brush _size + delta , 500 ) ) ;
2020-12-10 22:59:27 +00:00
} else if ( selected _tool . id === TOOL _ERASER ) {
2014-05-11 00:03:13 +00:00
eraser _size = Math . max ( 1 , Math . min ( eraser _size + delta , 500 ) ) ;
2020-12-10 22:59:27 +00:00
} else if ( selected _tool . id === TOOL _AIRBRUSH ) {
2014-05-11 00:32:23 +00:00
airbrush _size = Math . max ( 1 , Math . min ( airbrush _size + delta , 500 ) ) ;
2020-12-10 22:59:27 +00:00
} else if ( selected _tool . id === TOOL _PENCIL ) {
2014-05-11 00:03:13 +00:00
pencil _size = Math . max ( 1 , Math . min ( pencil _size + delta , 50 ) ) ;
2020-12-10 22:59:27 +00:00
} else if (
selected _tool . id === TOOL _LINE ||
selected _tool . id === TOOL _CURVE ||
selected _tool . id === TOOL _RECTANGLE ||
2020-12-25 05:32:15 +00:00
selected _tool . id === TOOL _ROUNDED _RECTANGLE ||
2020-12-10 22:59:27 +00:00
selected _tool . id === TOOL _ELLIPSE ||
selected _tool . id === TOOL _POLYGON
) {
2014-08-08 21:44:46 +00:00
stroke _size = Math . max ( 1 , Math . min ( stroke _size + delta , 500 ) ) ;
2014-05-11 00:03:13 +00:00
}
2018-01-21 15:03:59 +00:00
2014-08-10 14:01:26 +00:00
$G . trigger ( "option-changed" ) ;
2020-01-06 04:00:11 +00:00
if ( button !== undefined && pointer ) { // pointer may only be needed for tests
2019-09-30 03:57:12 +00:00
selected _tools . forEach ( ( selected _tool ) => {
tool _go ( selected _tool ) ;
} ) ;
2018-06-29 03:46:07 +00:00
}
2019-12-12 03:55:59 +00:00
update _helper _layer ( ) ;
2014-05-11 00:03:13 +00:00
}
e . preventDefault ( ) ;
2015-06-22 01:05:48 +00:00
return ;
2019-10-28 16:57:56 +00:00
} else if ( e . ctrlKey || e . metaKey ) {
2014-08-10 05:23:28 +00:00
if ( textbox ) {
2021-11-26 17:05:07 +00:00
switch ( e . key . toUpperCase ( ) ) {
2014-08-10 05:23:28 +00:00
case "A" :
case "Z" :
case "Y" :
case "I" :
case "B" :
case "U" :
// Don't prevent the default. Allow text editing commands.
2015-06-22 01:05:48 +00:00
return ;
2014-08-10 05:23:28 +00:00
}
}
2021-11-26 17:05:07 +00:00
switch ( e . key . toUpperCase ( ) ) {
case "," : // '<' without Shift
case "<" :
case "[" :
case "{" :
2014-12-08 02:45:23 +00:00
rotate ( - TAU / 4 ) ;
2017-10-30 21:02:45 +00:00
$canvas _area . trigger ( "resize" ) ;
2014-12-08 02:45:23 +00:00
break ;
2021-11-26 17:05:07 +00:00
case "." : // '>' without Shift
case ">" :
case "]" :
case "}" :
2014-12-08 02:45:23 +00:00
rotate ( + TAU / 4 ) ;
2017-10-30 21:02:45 +00:00
$canvas _area . trigger ( "resize" ) ;
2014-12-08 02:45:23 +00:00
break ;
2014-05-04 13:32:02 +00:00
case "Z" :
e . shiftKey ? redo ( ) : undo ( ) ;
break ;
case "Y" :
2019-12-21 22:40:39 +00:00
// Ctrl+Shift+Y handled above
redo ( ) ;
2014-05-04 13:32:02 +00:00
break ;
case "G" :
2014-05-16 04:30:03 +00:00
e . shiftKey ? render _history _as _gif ( ) : toggle _grid ( ) ;
2014-05-04 13:32:02 +00:00
break ;
case "F" :
2014-05-23 21:49:55 +00:00
view _bitmap ( ) ;
2014-05-04 13:32:02 +00:00
break ;
case "O" :
file _open ( ) ;
break ;
case "N" :
2014-08-18 17:34:47 +00:00
e . shiftKey ? clear ( ) : file _new ( ) ;
2014-05-04 13:32:02 +00:00
break ;
case "S" :
e . shiftKey ? file _save _as ( ) : file _save ( ) ;
break ;
case "A" :
select _all ( ) ;
break ;
case "I" :
2020-01-04 16:30:16 +00:00
image _invert _colors ( ) ;
2014-05-04 13:32:02 +00:00
break ;
2014-09-21 01:27:56 +00:00
case "E" :
image _attributes ( ) ;
break ;
2014-08-08 21:44:46 +00:00
default :
2015-06-22 01:05:48 +00:00
return ; // don't preventDefault
2014-05-04 00:33:01 +00:00
}
2014-05-04 13:32:02 +00:00
e . preventDefault ( ) ;
2014-03-20 15:57:42 +00:00
}
2014-05-04 13:32:02 +00:00
} ) ;
2021-11-26 23:01:16 +00:00
// $G.on("wheel", (e) => {
addEventListener ( "wheel" , ( e ) => {
if ( e . ctrlKey || e . metaKey ) {
return ;
}
// for reference screenshot mode (development helper):
if ( location . hash . match ( /compare-reference/i ) ) { // including compare-reference-tool-windows
// const delta_opacity = Math.sign(e.originalEvent.deltaY) * -0.1; // since attr() is not supported other than for content, this increment must match CSS
const delta _opacity = Math . sign ( e . deltaY ) * - 0.2 ; // since attr() is not supported other than for content, this increment must match CSS
let old _opacity = parseFloat ( $ ( "body" ) . attr ( "data-reference-opacity" ) ) ;
if ( ! isFinite ( old _opacity ) ) {
old _opacity = 0.5 ;
}
const new _opacity = Math . max ( 0 , Math . min ( 1 , old _opacity + delta _opacity ) ) ;
$ ( "body" ) . attr ( "data-reference-opacity" , new _opacity ) ;
// prevent scrolling, keeping the screenshot lined up
// e.preventDefault(); // doesn't work
// $canvas_area.scrollTop(0); // doesn't work with smooth scrolling
// $canvas_area.scrollLeft(0);
}
} ) ;
2019-10-29 18:46:29 +00:00
$G . on ( "cut copy paste" , e => {
2018-01-27 21:50:47 +00:00
if ( e . isDefaultPrevented ( ) ) {
return ;
}
2014-11-20 15:11:20 +00:00
if (
document . activeElement instanceof HTMLInputElement ||
2018-01-27 21:50:47 +00:00
document . activeElement instanceof HTMLTextAreaElement ||
! window . getSelection ( ) . isCollapsed
2014-11-20 15:11:20 +00:00
) {
2018-01-27 21:50:47 +00:00
// Don't prevent cutting/copying/pasting within inputs or textareas, or if there's a selection
2014-11-20 15:11:20 +00:00
return ;
}
2018-01-21 15:03:59 +00:00
2014-05-04 13:32:02 +00:00
e . preventDefault ( ) ;
2019-10-29 20:29:38 +00:00
const cd = e . originalEvent . clipboardData || window . clipboardData ;
2014-06-09 05:04:32 +00:00
if ( ! cd ) { return ; }
2018-01-11 07:45:26 +00:00
2014-05-04 13:32:02 +00:00
if ( e . type === "copy" || e . type === "cut" ) {
if ( selection && selection . canvas ) {
2019-10-29 20:29:38 +00:00
const do _sync _clipboard _copy _or _cut = ( ) => {
2019-09-21 16:35:03 +00:00
// works only for pasting within a jspaint instance
2019-10-29 20:29:38 +00:00
const data _url = selection . canvas . toDataURL ( ) ;
2019-09-21 16:35:03 +00:00
cd . setData ( "text/x-data-uri; type=image/png" , data _url ) ;
cd . setData ( "text/uri-list" , data _url ) ;
cd . setData ( "URL" , data _url ) ;
2019-09-13 20:50:46 +00:00
if ( e . type === "cut" ) {
2019-12-16 03:21:47 +00:00
delete _selection ( {
2021-01-30 15:18:50 +00:00
name : localize ( "Cut" ) ,
2019-12-16 03:21:47 +00:00
icon : get _help _folder _icon ( "p_cut.png" ) ,
} ) ;
2019-09-13 20:50:46 +00:00
}
2019-09-17 21:28:30 +00:00
} ;
2019-09-17 21:34:58 +00:00
if ( ! navigator . clipboard || ! navigator . clipboard . write ) {
2019-09-17 21:28:30 +00:00
return do _sync _clipboard _copy _or _cut ( ) ;
}
try {
if ( e . type === "cut" ) {
edit _cut ( ) ;
} else {
edit _copy ( ) ;
}
} catch ( e ) {
do _sync _clipboard _copy _or _cut ( ) ;
2014-05-04 13:32:02 +00:00
}
}
} else if ( e . type === "paste" ) {
2019-10-30 01:38:14 +00:00
for ( const item of cd . items ) {
2018-01-11 07:45:26 +00:00
if ( item . type . match ( /^text\/(?:x-data-uri|uri-list|plain)|URL$/ ) ) {
2019-10-29 18:46:29 +00:00
item . getAsString ( text => {
2021-02-12 16:12:42 +00:00
const uris = get _uris ( text ) ;
2019-09-17 20:31:49 +00:00
if ( uris . length > 0 ) {
2021-02-17 13:45:41 +00:00
load _image _from _uri ( uris [ 0 ] ) . then ( ( info ) => {
2021-02-11 22:26:49 +00:00
paste ( info . image || make _canvas ( info . image _data ) ) ;
2021-02-17 13:45:41 +00:00
} , ( error ) => {
show _resource _load _error _message ( error ) ;
2019-09-17 20:31:49 +00:00
} ) ;
} else {
show _error _message ( "The information on the Clipboard can't be inserted into Paint." ) ;
}
2014-10-02 20:41:43 +00:00
} ) ;
2019-10-30 01:38:14 +00:00
break ;
2018-01-11 07:45:26 +00:00
} else if ( item . type . match ( /^image\// ) ) {
paste _image _from _file ( item . getAsFile ( ) ) ;
2019-10-30 01:38:14 +00:00
break ;
2014-03-23 23:28:55 +00:00
}
2019-10-30 01:38:14 +00:00
}
2014-03-20 15:57:42 +00:00
}
2014-05-04 13:32:02 +00:00
} ) ;
2019-10-25 19:43:45 +00:00
reset _file ( ) ;
2021-02-12 02:07:41 +00:00
reset _selected _colors ( ) ;
2019-10-29 20:37:53 +00:00
reset _canvas _and _history ( ) ; // (with newly reset colors)
2019-10-30 17:38:16 +00:00
set _magnification ( default _magnification ) ;
2019-10-25 19:43:45 +00:00
2020-01-05 22:27:51 +00:00
// this is synchronous for now, but @TODO: handle possibility of loading a document before callback
2019-12-14 21:18:31 +00:00
// when switching to asynchronous storage, e.g. with localforage
2019-10-25 19:49:44 +00:00
storage . get ( {
width : default _canvas _width ,
height : default _canvas _height ,
2019-11-03 18:06:07 +00:00
} , ( err , stored _values ) => {
2019-10-25 19:49:44 +00:00
if ( err ) { return ; }
2019-11-03 18:06:07 +00:00
my _canvas _width = stored _values . width ;
my _canvas _height = stored _values . height ;
2019-12-14 21:18:31 +00:00
make _or _update _undoable ( {
2021-01-30 15:18:50 +00:00
match : ( history _node ) => history _node . name === localize ( "New" ) ,
name : "Resize Canvas For New Document" ,
2019-12-16 06:11:48 +00:00
icon : get _help _folder _icon ( "p_stretch_both.png" ) ,
2019-12-14 21:18:31 +00:00
} , ( ) => {
2021-02-11 02:00:38 +00:00
main _canvas . width = Math . max ( 1 , my _canvas _width ) ;
main _canvas . height = Math . max ( 1 , my _canvas _height ) ;
main _ctx . disable _image _smoothing ( ) ;
2019-12-14 21:18:31 +00:00
if ( ! transparency ) {
2021-02-11 02:04:35 +00:00
main _ctx . fillStyle = selected _colors . background ;
2021-02-11 02:00:38 +00:00
main _ctx . fillRect ( 0 , 0 , main _canvas . width , main _canvas . height ) ;
2019-12-14 21:18:31 +00:00
}
$canvas _area . trigger ( "resize" ) ;
} ) ;
2019-10-25 19:49:44 +00:00
} ) ;
2021-08-17 21:26:21 +00:00
if ( window . initial _system _file _handle ) {
systemHooks . readBlobFromHandle ( window . initial _system _file _handle ) . then ( file => {
if ( file ) {
open _from _file ( file , window . initial _system _file _handle ) ;
2019-10-25 19:43:45 +00:00
}
2021-08-17 21:26:21 +00:00
} , ( error ) => {
// this handler is not always called, sometimes error message is shown from readBlobFromHandle
show _error _message ( ` Failed to open file ${ window . initial _system _file _handle } ` , error ) ;
2019-10-25 19:43:45 +00:00
} ) ;
}
2019-12-19 21:33:48 +00:00
const lerp = ( a , b , b _ness ) => a + ( b - a ) * b _ness ;
const color _ramp = ( num _colors , start _hsla , end _hsla ) =>
Array ( num _colors ) . fill ( ) . map ( ( _undefined , index , array ) =>
` hsla( ${
lerp ( start _hsla [ 0 ] , end _hsla [ 0 ] , index / array . length )
} deg , $ {
lerp ( start _hsla [ 1 ] , end _hsla [ 1 ] , index / array . length )
} % , $ {
lerp ( start _hsla [ 2 ] , end _hsla [ 2 ] , index / array . length )
} % , $ {
lerp ( start _hsla [ 3 ] , end _hsla [ 3 ] , index / array . length )
} % ) `
) ;
const update _palette _from _theme = ( ) => {
if ( get _theme ( ) === "winter.css" ) {
2019-12-20 16:32:58 +00:00
const make _stripe _patterns = ( reverse ) => [
make _stripe _pattern ( reverse , [
"hsl(166, 93%, 38%)" ,
"white" ,
] ) ,
make _stripe _pattern ( reverse , [
"white" ,
"hsl(355, 78%, 46%)" ,
] ) ,
make _stripe _pattern ( reverse , [
"hsl(355, 78%, 46%)" ,
"white" ,
"white" ,
"hsl(355, 78%, 46%)" ,
"hsl(355, 78%, 46%)" ,
"hsl(355, 78%, 46%)" ,
"white" ,
"white" ,
"hsl(355, 78%, 46%)" ,
"white" ,
] , 2 ) ,
make _stripe _pattern ( reverse , [
"hsl(166, 93%, 38%)" ,
"white" ,
"white" ,
"hsl(166, 93%, 38%)" ,
"hsl(166, 93%, 38%)" ,
"hsl(166, 93%, 38%)" ,
"white" ,
"white" ,
"hsl(166, 93%, 38%)" ,
"white" ,
] , 2 ) ,
make _stripe _pattern ( reverse , [
"hsl(166, 93%, 38%)" ,
"white" ,
"hsl(355, 78%, 46%)" ,
"white" ,
] , 2 ) ,
] ;
2019-12-19 21:33:48 +00:00
palette = [
"black" ,
// green
"hsl(91, 55%, 81%)" ,
"hsl(142, 57%, 64%)" ,
"hsl(166, 93%, 38%)" ,
2019-12-20 21:22:38 +00:00
"#04ce1f" , // elf green
2019-12-19 21:33:48 +00:00
"hsl(159, 93%, 16%)" ,
// red
"hsl(2, 77%, 27%)" ,
2019-12-22 04:54:22 +00:00
"hsl(350, 100%, 50%)" ,
2019-12-19 21:33:48 +00:00
"hsl(356, 97%, 64%)" ,
2019-12-20 16:31:01 +00:00
// brown
"#ad4632" ,
"#5b3b1d" ,
2019-12-20 21:22:38 +00:00
// stripes
2019-12-20 16:32:58 +00:00
... make _stripe _patterns ( false ) ,
// white to blue
... color _ramp (
6 ,
[ 200 , 100 , 100 , 100 ] ,
[ 200 , 100 , 10 , 100 ] ,
) ,
2019-12-20 21:22:38 +00:00
// pink
"#fcbaf8" ,
2019-12-19 21:33:48 +00:00
// silver
"hsl(0, 0%, 90%)" ,
"hsl(22, 5%, 71%)" ,
// gold
"hsl(48, 82%, 54%)" ,
"hsl(49, 82%, 72%)" ,
2019-12-20 21:22:38 +00:00
// stripes
2019-12-20 16:32:58 +00:00
... make _stripe _patterns ( true ) ,
2019-12-19 21:33:48 +00:00
] ;
$colorbox . rebuild _palette ( ) ;
} else {
palette = default _palette ;
$colorbox . rebuild _palette ( ) ;
}
} ;
$G . on ( "theme-load" , update _palette _from _theme ) ;
update _palette _from _theme ( ) ;
2019-10-30 00:48:51 +00:00
function to _canvas _coords ( { clientX , clientY } ) {
2019-10-29 20:29:38 +00:00
const rect = canvas _bounding _client _rect ;
2019-10-29 22:45:27 +00:00
const cx = clientX - rect . left ;
const cy = clientY - rect . top ;
2014-05-04 13:32:02 +00:00
return {
2021-02-11 02:00:38 +00:00
x : ~ ~ ( cx / rect . width * main _canvas . width ) ,
y : ~ ~ ( cy / rect . height * main _canvas . height ) ,
2014-05-01 23:32:27 +00:00
} ;
2014-05-04 13:32:02 +00:00
}
2019-10-02 14:34:43 +00:00
function update _fill _and _stroke _colors _and _lineWidth ( selected _tool ) {
2021-02-11 02:00:38 +00:00
main _ctx . lineWidth = stroke _size ;
2018-01-21 15:03:59 +00:00
2019-10-29 20:29:38 +00:00
const reverse _because _fill _only = selected _tool . $options && selected _tool . $options . fill && ! selected _tool . $options . stroke ;
2021-02-11 02:00:38 +00:00
main _ctx . fillStyle = fill _color =
main _ctx . strokeStyle = stroke _color =
2021-02-11 02:04:35 +00:00
selected _colors [
( ctrl && selected _colors . ternary && pointer _active ) ? "ternary" :
2018-06-29 04:42:20 +00:00
( ( reverse ^ reverse _because _fill _only ) ? "background" : "foreground" )
2014-05-04 13:32:02 +00:00
] ;
2019-10-01 19:06:50 +00:00
2015-02-24 00:18:07 +00:00
fill _color _k =
stroke _color _k =
2018-06-29 04:42:20 +00:00
ctrl ? "ternary" : ( ( reverse ^ reverse _because _fill _only ) ? "background" : "foreground" ) ;
2019-10-02 14:34:43 +00:00
2014-11-26 18:27:00 +00:00
if ( selected _tool . shape || selected _tool . shape _colors ) {
2014-05-04 13:32:02 +00:00
if ( ! selected _tool . stroke _only ) {
2018-06-29 04:42:20 +00:00
if ( ( reverse ^ reverse _because _fill _only ) ) {
2016-11-06 00:13:54 +00:00
fill _color _k = "foreground" ;
stroke _color _k = "background" ;
2014-04-04 08:23:36 +00:00
} else {
2016-11-06 00:13:54 +00:00
fill _color _k = "background" ;
stroke _color _k = "foreground" ;
2014-04-04 08:23:36 +00:00
}
}
2021-02-11 02:04:35 +00:00
main _ctx . fillStyle = fill _color = selected _colors [ fill _color _k ] ;
main _ctx . strokeStyle = stroke _color = selected _colors [ stroke _color _k ] ;
2014-11-26 18:27:00 +00:00
}
2019-10-02 14:34:43 +00:00
}
function tool _go ( selected _tool , event _name ) {
update _fill _and _stroke _colors _and _lineWidth ( selected _tool ) ;
2014-05-04 13:32:02 +00:00
if ( selected _tool [ event _name ] ) {
2021-02-11 02:00:38 +00:00
selected _tool [ event _name ] ( main _ctx , pointer . x , pointer . y ) ;
2014-03-03 03:36:22 +00:00
}
2014-05-04 13:32:02 +00:00
if ( selected _tool . paint ) {
2021-02-11 02:00:38 +00:00
selected _tool . paint ( main _ctx , pointer . x , pointer . y ) ;
2014-05-04 13:32:02 +00:00
}
}
2015-06-22 00:01:12 +00:00
function canvas _pointer _move ( e ) {
2014-05-04 13:32:02 +00:00
ctrl = e . ctrlKey ;
2018-01-25 03:51:12 +00:00
shift = e . shiftKey ;
2019-10-30 00:48:51 +00:00
pointer = to _canvas _coords ( e ) ;
2019-09-29 03:16:51 +00:00
// Quick Undo
// (Note: pointermove also occurs when the set of buttons pressed changes,
// except when another event would fire like pointerdown)
2019-12-10 04:10:33 +00:00
if ( pointers . length && e . button != - 1 ) {
2019-10-02 00:33:54 +00:00
// compare buttons other than middle mouse button by using bitwise OR to make that bit of the number the same
const MMB = 4 ;
if ( e . pointerType != pointer _type || ( e . buttons | MMB ) != ( pointer _buttons | MMB ) ) {
2019-02-10 17:52:28 +00:00
cancel ( ) ;
2019-12-07 17:54:52 +00:00
pointer _active = false ; // NOTE: pointer_active used in cancel()
2019-02-10 17:52:28 +00:00
return ;
}
}
2019-09-29 03:16:51 +00:00
2014-05-04 13:32:02 +00:00
if ( e . shiftKey ) {
2020-12-10 22:59:27 +00:00
if (
selected _tool . id === TOOL _LINE ||
selected _tool . id === TOOL _CURVE
) {
2017-05-23 03:25:40 +00:00
// snap to eight directions
2019-10-29 20:29:38 +00:00
const dist = Math . sqrt (
2015-06-22 00:01:12 +00:00
( pointer . y - pointer _start . y ) * ( pointer . y - pointer _start . y ) +
( pointer . x - pointer _start . x ) * ( pointer . x - pointer _start . x )
2014-04-04 08:32:49 +00:00
) ;
2019-10-29 20:29:38 +00:00
const eighth _turn = TAU / 8 ;
const angle _0 _to _8 = Math . atan2 ( pointer . y - pointer _start . y , pointer . x - pointer _start . x ) / eighth _turn ;
const angle = Math . round ( angle _0 _to _8 ) * eighth _turn ;
2017-05-23 03:25:40 +00:00
pointer . x = Math . round ( pointer _start . x + Math . cos ( angle ) * dist ) ;
pointer . y = Math . round ( pointer _start . y + Math . sin ( angle ) * dist ) ;
2014-05-04 13:32:02 +00:00
} else if ( selected _tool . shape ) {
2017-05-23 03:25:40 +00:00
// snap to four diagonals
2019-10-29 20:29:38 +00:00
const w = Math . abs ( pointer . x - pointer _start . x ) ;
const h = Math . abs ( pointer . y - pointer _start . y ) ;
2014-05-04 13:32:02 +00:00
if ( w < h ) {
2015-06-22 00:01:12 +00:00
if ( pointer . y > pointer _start . y ) {
pointer . y = pointer _start . y + w ;
2014-05-04 00:33:01 +00:00
} else {
2015-06-22 00:01:12 +00:00
pointer . y = pointer _start . y - w ;
2014-05-04 00:33:01 +00:00
}
2014-02-27 07:29:26 +00:00
} else {
2015-06-22 00:01:12 +00:00
if ( pointer . x > pointer _start . x ) {
pointer . x = pointer _start . x + h ;
2014-03-03 05:47:17 +00:00
} else {
2015-06-22 00:01:12 +00:00
pointer . x = pointer _start . x - h ;
2014-03-03 05:47:17 +00:00
}
}
}
2014-03-13 19:12:30 +00:00
}
2019-09-30 03:57:12 +00:00
selected _tools . forEach ( ( selected _tool ) => {
tool _go ( selected _tool ) ;
} ) ;
2015-06-22 00:01:12 +00:00
pointer _previous = pointer ;
2014-05-04 13:32:02 +00:00
}
2019-10-29 18:46:29 +00:00
$canvas . on ( "pointermove" , e => {
2019-10-30 00:48:51 +00:00
pointer = to _canvas _coords ( e ) ;
2019-10-29 21:16:33 +00:00
$status _position . text ( ` ${ pointer . x } , ${ pointer . y } ` ) ;
2019-10-01 22:41:33 +00:00
} ) ;
2019-12-18 05:42:19 +00:00
$canvas . on ( "pointerenter" , ( ) => {
2019-10-01 22:41:33 +00:00
pointer _over _canvas = true ;
2019-10-01 00:34:02 +00:00
update _helper _layer ( ) ;
2019-10-01 22:41:33 +00:00
if ( ! update _helper _layer _on _pointermove _active ) {
$G . on ( "pointermove" , update _helper _layer ) ;
update _helper _layer _on _pointermove _active = true ;
}
2014-05-04 13:32:02 +00:00
} ) ;
2019-12-18 05:42:19 +00:00
$canvas . on ( "pointerleave" , ( ) => {
2019-10-01 22:41:33 +00:00
pointer _over _canvas = false ;
2014-05-04 13:32:02 +00:00
$status _position . text ( "" ) ;
2019-10-01 22:41:33 +00:00
update _helper _layer ( ) ;
if ( ! pointer _active && update _helper _layer _on _pointermove _active ) {
$G . off ( "pointermove" , update _helper _layer ) ;
update _helper _layer _on _pointermove _active = false ;
}
2014-05-04 13:32:02 +00:00
} ) ;
2014-06-08 21:31:26 +00:00
2020-04-23 06:20:32 +00:00
let clean _up _eye _gaze _mode = ( ) => { } ;
$G . on ( "eye-gaze-mode-toggled" , ( ) => {
if ( $ ( "body" ) . hasClass ( "eye-gaze-mode" ) ) {
init _eye _gaze _mode ( ) ;
} else {
clean _up _eye _gaze _mode ( ) ;
}
} ) ;
2020-04-18 19:25:55 +00:00
if ( $ ( "body" ) . hasClass ( "eye-gaze-mode" ) ) {
2020-04-23 06:20:32 +00:00
init _eye _gaze _mode ( ) ;
}
2021-05-18 18:45:19 +00:00
const eye _gaze _mode _config = {
targets : `
button : not ( [ disabled ] ) ,
input ,
textarea ,
label ,
a ,
. flip - and - rotate . sub - options . radio - wrapper ,
. current - colors ,
. color - button ,
. edit - colors - window . swatch ,
. edit - colors - window . rainbow - canvas ,
. edit - colors - window . luminosity - canvas ,
. tool : not ( . selected ) ,
. chooser - option ,
. menu - button : not ( . active ) ,
. menu - item ,
. main - canvas ,
. selection canvas ,
. handle ,
. grab - region ,
. window : not ( . maximized ) . window - titlebar ,
2021-05-18 19:23:10 +00:00
. history - entry
2021-05-18 18:45:19 +00:00
` ,
noCenter : ( target ) => (
2021-05-22 17:48:44 +00:00
target . matches ( `
. main - canvas ,
. selection canvas ,
. window - titlebar ,
. rainbow - canvas ,
. luminosity - canvas ,
input [ type = "range" ]
` )
2021-05-18 18:45:19 +00:00
) ,
2021-05-18 19:12:39 +00:00
retarget : [
2021-05-18 19:43:30 +00:00
// Nudge hovers near the edges of the canvas onto the canvas
{ from : ".canvas-area" , to : ".main-canvas" , withinMargin : 50 } ,
// Top level menus are just immediately switched between for now.
// Prevent awkward hover clicks on top level menu buttons while menus are open.
{
from : ( target ) => (
( target . closest ( ".menu-button" ) || target . matches ( ".menu-container" ) ) &&
document . querySelector ( ".menu-button.active" ) != null
) ,
to : null ,
2021-05-18 22:16:22 +00:00
} ,
// Can we make it easier to click on help topics with short names?
// { from: ".help-window li", to: (target) => target.querySelector(".item")},
2021-05-18 19:12:39 +00:00
] ,
2021-05-18 18:51:21 +00:00
isEquivalentTarget : ( apparent _hover _target , hover _target ) => (
2021-05-18 22:41:06 +00:00
apparent _hover _target . closest ( "label" ) === hover _target ||
apparent _hover _target . closest ( ".radio-wrapper" ) === hover _target
2021-05-18 18:45:19 +00:00
) ,
2021-05-18 18:51:21 +00:00
dwellClickEvenIfPaused : ( target ) => (
target . matches ( ".toggle-dwell-clicking" )
) ,
2021-05-18 18:45:19 +00:00
shouldDrag : ( target ) => (
target . matches ( ".window-titlebar, .window-titlebar *:not(button)" ) ||
target . matches ( ".selection, .selection *, .handle, .grab-region" ) ||
(
target === main _canvas &&
selected _tool . id !== TOOL _PICK _COLOR &&
selected _tool . id !== TOOL _FILL &&
selected _tool . id !== TOOL _MAGNIFIER &&
selected _tool . id !== TOOL _POLYGON &&
selected _tool . id !== TOOL _CURVE
)
) ,
2021-05-22 17:48:44 +00:00
click : ( { target , x , y } ) => {
2021-05-18 19:01:38 +00:00
if ( target . matches ( "button:not(.toggle)" ) ) {
target . style . borderImage = "var(--inset-deep-border-image)" ;
2021-05-22 17:48:44 +00:00
setTimeout ( ( ) => {
2021-05-18 19:01:38 +00:00
target . style . borderImage = "" ;
2021-08-29 15:54:57 +00:00
// delay the button.click() as well, so the pressed state is
2021-05-18 19:01:38 +00:00
// visible even if the button closes a dialog
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 19:01:38 +00:00
target . click ( ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-05-18 19:01:38 +00:00
} , 100 ) ;
2021-05-22 17:48:44 +00:00
} else if ( target . matches ( "input[type='range']" ) ) {
const rect = target . getBoundingClientRect ( ) ;
const vertical =
target . getAttribute ( "orient" ) === "vertical" ||
( getCurrentRotation ( target ) !== 0 ) ||
rect . height > rect . width ;
const min = Number ( target . min ) ;
const max = Number ( target . max ) ;
const v = (
vertical ?
( y - rect . top ) / rect . height :
( x - rect . left ) / rect . width
) * ( max - min ) + min ;
target . value = v ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-22 17:48:44 +00:00
target . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
target . dispatchEvent ( new Event ( "change" , { bubbles : true } ) ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-05-18 19:01:38 +00:00
} else {
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 19:01:38 +00:00
target . click ( ) ;
if ( target . matches ( "input, textarea" ) ) {
target . focus ( ) ;
}
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-05-18 19:01:38 +00:00
}
2021-05-22 17:48:44 +00:00
// Source: https://stackoverflow.com/a/54492696/2624876
function getCurrentRotation ( el ) {
const st = window . getComputedStyle ( el , null ) ;
const tm = st . getPropertyValue ( "-webkit-transform" ) ||
st . getPropertyValue ( "-moz-transform" ) ||
st . getPropertyValue ( "-ms-transform" ) ||
st . getPropertyValue ( "-o-transform" ) ||
st . getPropertyValue ( "transform" ) ||
"none" ;
if ( tm !== "none" ) {
const [ a , b ] = tm . split ( '(' ) [ 1 ] . split ( ')' ) [ 0 ] . split ( ',' ) ;
return Math . round ( Math . atan2 ( a , b ) * ( 180 / Math . PI ) ) ;
}
return 0 ;
}
2021-05-18 19:01:38 +00:00
} ,
2021-05-18 18:45:19 +00:00
} ;
2021-06-19 23:58:57 +00:00
var enable _tracky _mouse = false ;
2021-05-19 07:33:34 +00:00
var tracky _mouse _deps _promise ;
async function init _eye _gaze _mode ( ) {
2021-06-19 23:58:57 +00:00
if ( enable _tracky _mouse ) {
if ( ! tracky _mouse _deps _promise ) {
TrackyMouse . dependenciesRoot = "lib/tracky-mouse" ;
tracky _mouse _deps _promise = TrackyMouse . loadDependencies ( ) ;
}
await tracky _mouse _deps _promise ;
2021-05-19 07:33:34 +00:00
2021-06-19 23:58:57 +00:00
const $tracky _mouse _window = $Window ( {
title : "Tracky Mouse" ,
icon : "tracky-mouse" ,
} ) ;
$tracky _mouse _window . addClass ( "tracky-mouse-window" ) ;
const tracky _mouse _container = $tracky _mouse _window . $content [ 0 ] ;
TrackyMouse . init ( tracky _mouse _container ) ;
TrackyMouse . useCamera ( ) ;
$tracky _mouse _window . center ( ) ;
let last _el _over ;
TrackyMouse . onPointerMove = ( x , y ) => {
const target = document . elementFromPoint ( x , y ) || document . body ;
if ( target !== last _el _over ) {
if ( last _el _over ) {
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-06-19 23:58:57 +00:00
const event = new /*PointerEvent*/ $ . Event ( "pointerleave" , Object . assign ( get _event _options ( { x , y } ) , {
button : 0 ,
buttons : 1 ,
bubbles : false ,
cancelable : false ,
} ) ) ;
// last_el_over.dispatchEvent(event);
$ ( last _el _over ) . trigger ( event ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-06-19 23:58:57 +00:00
}
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-06-19 23:58:57 +00:00
const event = new /*PointerEvent*/ $ . Event ( "pointerenter" , Object . assign ( get _event _options ( { x , y } ) , {
2021-05-19 07:33:34 +00:00
button : 0 ,
buttons : 1 ,
bubbles : false ,
cancelable : false ,
} ) ) ;
2021-06-19 23:58:57 +00:00
// target.dispatchEvent(event);
$ ( target ) . trigger ( event ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-06-19 23:58:57 +00:00
last _el _over = target ;
2021-05-19 07:33:34 +00:00
}
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-06-19 23:58:57 +00:00
const event = new PointerEvent /*$.Event*/ ( "pointermove" , Object . assign ( get _event _options ( { x , y } ) , {
2021-05-19 07:33:34 +00:00
button : 0 ,
buttons : 1 ,
} ) ) ;
2021-06-19 23:58:57 +00:00
target . dispatchEvent ( event ) ;
// $(target).trigger(event);
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-06-19 23:58:57 +00:00
} ;
2021-05-19 07:33:34 +00:00
2021-06-19 23:58:57 +00:00
// tracky_mouse_container.querySelector(".tracky-mouse-canvas").classList.add("inset-deep");
2021-05-19 07:33:34 +00:00
2021-06-19 23:58:57 +00:00
}
2020-04-23 22:32:54 +00:00
const circle _radius _max = 50 ; // dwell indicator size in pixels
const hover _timespan = 500 ; // how long between the dwell indicator appearing and triggering a click
2020-04-10 21:45:18 +00:00
const averaging _window _timespan = 500 ;
2020-04-23 22:32:54 +00:00
const inactive _at _startup _timespan = 1500 ; // (should be at least averaging_window_timespan, but more importantly enough to make it not awkward when enabling eye gaze mode)
const inactive _after _release _timespan = 1000 ; // after click or drag release (from dwell or otherwise)
const inactive _after _hovered _timespan = 1000 ; // after dwell click indicator appears; does not control the time to finish that dwell click, only to click on something else after this is canceled (but it doesn't control that directly)
const inactive _after _invalid _timespan = 1000 ; // after a dwell click is canceled due to an element popping up in front, or existing in front at the center of the other element
const inactive _after _focused _timespan = 1000 ; // after page becomes focused after being unfocused
2020-04-10 21:45:18 +00:00
let recent _points = [ ] ;
2020-04-23 22:32:54 +00:00
let inactive _until _time = Date . now ( ) ;
2020-04-22 17:07:23 +00:00
let paused = false ;
2020-04-10 21:45:18 +00:00
let hover _candidate ;
2020-04-16 05:07:41 +00:00
let gaze _dragging = null ;
2020-04-23 06:20:32 +00:00
2020-04-23 22:32:54 +00:00
const deactivate _for _at _least = ( timespan ) => {
inactive _until _time = Math . max ( inactive _until _time , Date . now ( ) + timespan ) ;
} ;
deactivate _for _at _least ( inactive _at _startup _timespan ) ;
2021-05-18 04:13:21 +00:00
const halo = document . createElement ( "div" ) ;
halo . className = "hover-halo" ;
halo . style . display = "none" ;
document . body . appendChild ( halo ) ;
const dwell _indicator = document . createElement ( "div" ) ;
dwell _indicator . className = "dwell-indicator" ;
dwell _indicator . style . width = ` ${ circle _radius _max } px ` ;
dwell _indicator . style . height = ` ${ circle _radius _max } px ` ;
dwell _indicator . style . display = "none" ;
document . body . appendChild ( dwell _indicator ) ;
2020-04-23 06:20:32 +00:00
const on _pointer _move = ( e ) => {
2020-04-10 21:45:18 +00:00
recent _points . push ( { x : e . clientX , y : e . clientY , time : Date . now ( ) } ) ;
2020-04-23 06:20:32 +00:00
} ;
const on _pointer _up _or _cancel = ( e ) => {
2020-04-23 22:32:54 +00:00
deactivate _for _at _least ( inactive _after _release _timespan ) ;
2020-04-16 05:07:41 +00:00
gaze _dragging = null ;
2020-04-23 06:20:32 +00:00
} ;
2020-04-11 18:40:56 +00:00
2020-05-10 17:53:14 +00:00
let page _focused = document . visibilityState === "visible" ; // guess/assumption
let mouse _inside _page = true ; // assumption
2020-04-23 06:20:32 +00:00
const on _focus = ( ) => {
2020-04-19 01:18:06 +00:00
page _focused = true ;
2020-04-23 22:32:54 +00:00
deactivate _for _at _least ( inactive _after _focused _timespan ) ;
2020-04-23 06:20:32 +00:00
} ;
const on _blur = ( ) => {
page _focused = false ;
} ;
const on _mouse _leave _page = ( ) => {
2020-05-10 17:53:14 +00:00
mouse _inside _page = false ;
2020-04-23 06:20:32 +00:00
} ;
const on _mouse _enter _page = ( ) => {
2020-05-10 17:53:14 +00:00
mouse _inside _page = true ;
2020-04-23 06:20:32 +00:00
} ;
2021-05-18 04:13:21 +00:00
window . addEventListener ( "pointermove" , on _pointer _move ) ;
window . addEventListener ( "pointerup" , on _pointer _up _or _cancel ) ;
window . addEventListener ( "pointercancel" , on _pointer _up _or _cancel ) ;
window . addEventListener ( "focus" , on _focus ) ;
window . addEventListener ( "blur" , on _blur ) ;
document . addEventListener ( "mouseleave" , on _mouse _leave _page ) ;
document . addEventListener ( "mouseenter" , on _mouse _enter _page ) ;
2020-04-18 23:47:12 +00:00
2020-04-16 04:06:38 +00:00
const get _hover _candidate = ( clientX , clientY ) => {
2020-04-18 23:47:12 +00:00
2020-05-10 17:53:14 +00:00
if ( ! page _focused || ! mouse _inside _page ) return null ;
2020-04-18 23:47:12 +00:00
2020-04-23 01:59:44 +00:00
let target = document . elementFromPoint ( clientX , clientY ) ;
2020-04-23 01:14:51 +00:00
if ( ! target ) {
return null ;
}
2020-04-16 04:06:38 +00:00
let hover _candidate = {
x : clientX ,
y : clientY ,
time : Date . now ( ) ,
} ;
2021-05-18 19:23:10 +00:00
let retargeted = false ;
for ( const { from , to , withinMargin = Infinity } of eye _gaze _mode _config . retarget ) {
2021-05-18 19:43:30 +00:00
if (
from instanceof Element ? from === target :
typeof from === "function" ? from ( target ) :
target . matches ( from )
) {
2021-05-18 19:23:10 +00:00
const to _element =
2021-05-18 19:43:30 +00:00
( to instanceof Element || to === null ) ? to :
2021-05-18 19:23:10 +00:00
typeof to === "function" ? to ( target ) :
( target . closest ( to ) || target . querySelector ( to ) ) ;
2021-05-18 19:43:30 +00:00
if ( to _element === null ) {
return null ;
} else if ( to _element ) {
const to _rect = to _element . getBoundingClientRect ( ) ;
if (
hover _candidate . x > to _rect . left - withinMargin &&
hover _candidate . y > to _rect . top - withinMargin &&
hover _candidate . x < to _rect . right + withinMargin &&
hover _candidate . y < to _rect . bottom + withinMargin
) {
target = to _element ;
hover _candidate . x = Math . min (
to _rect . right - 1 ,
Math . max (
to _rect . left ,
hover _candidate . x ,
) ,
) ;
hover _candidate . y = Math . min (
to _rect . bottom - 1 ,
Math . max (
to _rect . top ,
hover _candidate . y ,
) ,
) ;
retargeted = true ;
}
2021-05-18 19:12:39 +00:00
}
2020-04-16 04:06:38 +00:00
}
2021-05-18 19:12:39 +00:00
}
2021-05-18 19:23:10 +00:00
if ( ! retargeted ) {
target = target . closest ( eye _gaze _mode _config . targets ) ;
if ( ! target ) {
return null ;
}
}
2021-05-18 19:12:39 +00:00
if ( ! eye _gaze _mode _config . noCenter ( target ) ) {
2020-04-23 01:14:51 +00:00
// Nudge hover previews to the center of buttons and things
2020-04-23 01:59:44 +00:00
const rect = target . getBoundingClientRect ( ) ;
2020-04-16 04:06:38 +00:00
hover _candidate . x = rect . left + rect . width / 2 ;
hover _candidate . y = rect . top + rect . height / 2 ;
}
2020-04-23 01:59:44 +00:00
hover _candidate . target = target ;
2020-04-16 04:06:38 +00:00
return hover _candidate ;
} ;
2020-04-11 18:40:56 +00:00
2021-05-18 15:06:11 +00:00
const get _event _options = ( { x , y } ) => {
2020-12-21 02:15:23 +00:00
return {
2021-05-18 15:06:11 +00:00
view : window , // needed for offsetX/Y calculation
2020-12-21 02:15:23 +00:00
clientX : x ,
clientY : y ,
pointerId : 1234567890 ,
pointerType : "mouse" ,
isPrimary : true ,
2021-05-18 04:13:21 +00:00
bubbles : true ,
2021-05-18 09:52:05 +00:00
cancelable : true ,
2020-12-21 02:15:23 +00:00
} ;
} ;
2020-04-19 17:04:09 +00:00
const update = ( ) => {
2020-04-10 21:45:18 +00:00
const time = Date . now ( ) ;
recent _points = recent _points . filter ( ( point _record ) => time < point _record . time + averaging _window _timespan ) ;
if ( recent _points . length ) {
const latest _point = recent _points [ recent _points . length - 1 ] ;
recent _points . push ( { x : latest _point . x , y : latest _point . y , time } ) ;
const average _point = average _points ( recent _points ) ;
// debug
// const canvas_point = to_canvas_coords({clientX: average_point.x, clientY: average_point.y});
// ctx.fillStyle = "red";
// ctx.fillRect(canvas_point.x, canvas_point.y, 10, 10);
const recent _movement _amount = Math . hypot ( latest _point . x - average _point . x , latest _point . y - average _point . y ) ;
2020-04-11 18:40:56 +00:00
// Invalidate in case an element pops up in front of the element you're hovering over, e.g. a submenu
2021-05-18 22:16:22 +00:00
// (that use case doesn't actually work because the menu pops up before the hover_candidate exists)
// (TODO: disable hovering to open submenus in eye gaze mode)
// or an element occludes the center of an element you're hovering over, in which case it
// could be confusing if it showed a dwell click indicator over a different element than it would click
// (but TODO: just move the indicator off center in that case)
2020-04-19 03:00:15 +00:00
if ( hover _candidate && ! gaze _dragging ) {
2020-04-16 04:06:38 +00:00
const apparent _hover _candidate = get _hover _candidate ( hover _candidate . x , hover _candidate . y ) ;
2021-05-19 02:16:52 +00:00
const show _occluder _indicator = ( occluder ) => {
const occluder _indicator = document . createElement ( "div" ) ;
const occluder _rect = occluder . getBoundingClientRect ( ) ;
const outline _width = 4 ;
occluder _indicator . style . pointerEvents = "none" ;
occluder _indicator . style . zIndex = 1000001 ;
occluder _indicator . style . display = "block" ;
occluder _indicator . style . position = "fixed" ;
occluder _indicator . style . left = ` ${ occluder _rect . left + outline _width } px ` ;
occluder _indicator . style . top = ` ${ occluder _rect . top + outline _width } px ` ;
occluder _indicator . style . width = ` ${ occluder _rect . width - outline _width * 2 } px ` ;
occluder _indicator . style . height = ` ${ occluder _rect . height - outline _width * 2 } px ` ;
occluder _indicator . style . outline = ` ${ outline _width } px dashed red ` ;
occluder _indicator . style . boxShadow = ` 0 0 ${ outline _width } px ${ outline _width } px maroon ` ;
document . body . appendChild ( occluder _indicator ) ;
setTimeout ( ( ) => {
occluder _indicator . remove ( ) ;
} , inactive _after _invalid _timespan * 0.5 ) ;
} ;
2020-04-16 04:06:38 +00:00
if ( apparent _hover _candidate ) {
2020-04-23 01:36:30 +00:00
if (
apparent _hover _candidate . target !== hover _candidate . target &&
2021-05-18 22:16:22 +00:00
// !retargeted &&
2021-05-18 18:45:19 +00:00
! eye _gaze _mode _config . isEquivalentTarget (
apparent _hover _candidate . target , hover _candidate . target
)
2020-04-23 01:36:30 +00:00
) {
2020-04-11 18:40:56 +00:00
hover _candidate = null ;
2020-04-23 22:32:54 +00:00
deactivate _for _at _least ( inactive _after _invalid _timespan ) ;
2021-05-19 02:16:52 +00:00
show _occluder _indicator ( apparent _hover _candidate . target ) ;
2020-04-11 18:40:56 +00:00
}
} else {
2021-05-19 02:16:52 +00:00
let occluder = document . elementFromPoint ( hover _candidate . x , hover _candidate . y ) ;
2020-04-11 18:40:56 +00:00
hover _candidate = null ;
2020-04-23 22:32:54 +00:00
deactivate _for _at _least ( inactive _after _invalid _timespan ) ;
2021-05-19 02:16:52 +00:00
show _occluder _indicator ( occluder || document . body ) ;
2020-04-11 18:40:56 +00:00
}
}
2020-04-10 21:45:18 +00:00
let circle _position = latest _point ;
2020-04-19 18:10:00 +00:00
let circle _opacity = 0 ;
let circle _radius = 0 ;
2020-04-10 21:45:18 +00:00
if ( hover _candidate ) {
circle _position = hover _candidate ;
2020-04-19 23:02:46 +00:00
circle _opacity = 0.4 ;
2020-04-19 18:10:00 +00:00
circle _radius =
2020-04-10 21:45:18 +00:00
( hover _candidate . time - time + hover _timespan ) / hover _timespan
2020-04-19 18:10:00 +00:00
* circle _radius _max ;
2020-04-18 23:29:12 +00:00
if ( time > hover _candidate . time + hover _timespan ) {
if ( pointer _active || gaze _dragging ) {
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 04:13:21 +00:00
hover _candidate . target . dispatchEvent ( new PointerEvent ( "pointerup" ,
Object . assign ( get _event _options ( hover _candidate ) , {
button : 0 ,
buttons : 0 ,
} )
) ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2020-04-18 23:29:12 +00:00
} else {
pointers = [ ] ; // prevent multi-touch panning
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 04:13:21 +00:00
hover _candidate . target . dispatchEvent ( new PointerEvent ( "pointerdown" ,
Object . assign ( get _event _options ( hover _candidate ) , {
button : 0 ,
buttons : 1 ,
} )
) ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2021-05-18 18:45:19 +00:00
if ( eye _gaze _mode _config . shouldDrag ( hover _candidate . target ) ) {
2020-04-18 23:29:12 +00:00
gaze _dragging = hover _candidate . target ;
} else {
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 04:13:21 +00:00
hover _candidate . target . dispatchEvent ( new PointerEvent ( "pointerup" ,
Object . assign ( get _event _options ( hover _candidate ) , {
button : 0 ,
buttons : 0 ,
} )
) ) ;
2021-05-22 17:48:44 +00:00
eye _gaze _mode _config . click ( hover _candidate ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2020-04-18 23:29:12 +00:00
}
2020-04-11 01:25:05 +00:00
}
2020-04-10 21:45:18 +00:00
hover _candidate = null ;
2020-04-23 22:32:54 +00:00
deactivate _for _at _least ( inactive _after _hovered _timespan ) ;
2020-04-10 21:45:18 +00:00
}
}
2020-04-19 23:02:46 +00:00
if ( gaze _dragging ) {
2021-05-18 04:13:21 +00:00
dwell _indicator . classList . add ( "for-release" ) ;
2020-04-19 23:02:46 +00:00
} else {
2021-05-18 04:13:21 +00:00
dwell _indicator . classList . remove ( "for-release" ) ;
2020-04-19 23:02:46 +00:00
}
2021-05-18 04:13:21 +00:00
dwell _indicator . style . display = "" ;
dwell _indicator . style . opacity = circle _opacity ;
dwell _indicator . style . transform = ` scale( ${ circle _radius / circle _radius _max } ) ` ;
dwell _indicator . style . left = ` ${ circle _position . x - circle _radius _max / 2 } px ` ;
dwell _indicator . style . top = ` ${ circle _position . y - circle _radius _max / 2 } px ` ;
2020-04-10 21:45:18 +00:00
2020-04-16 05:07:41 +00:00
let halo _target =
gaze _dragging ||
( hover _candidate || get _hover _candidate ( latest _point . x , latest _point . y ) || { } ) . target ;
2020-04-22 17:07:23 +00:00
2021-05-18 18:51:21 +00:00
if ( halo _target && ( ! paused || eye _gaze _mode _config . dwellClickEvenIfPaused ( halo _target ) ) ) {
2020-04-19 03:21:36 +00:00
let rect = halo _target . getBoundingClientRect ( ) ;
2020-04-29 22:47:55 +00:00
const computed _style = getComputedStyle ( halo _target ) ;
2021-05-18 17:46:00 +00:00
let ancestor = halo _target ;
2021-05-18 18:35:29 +00:00
let border _radius _scale = 1 ; // for border radius mimicry, given parents with transform: scale()
2021-05-18 17:46:00 +00:00
while ( ancestor instanceof HTMLElement ) {
const ancestor _computed _style = getComputedStyle ( ancestor ) ;
if ( ancestor _computed _style . transform ) {
2021-05-18 18:35:29 +00:00
// Collect scale transforms
2021-05-18 17:46:00 +00:00
const match = ancestor _computed _style . transform . match ( /(?:scale|matrix)\((\d+(?:\.\d+)?)/ ) ;
if ( match ) {
border _radius _scale *= Number ( match [ 1 ] ) ;
}
}
2021-05-18 18:35:29 +00:00
if ( ancestor _computed _style . overflow !== "visible" ) {
// Clamp to visible region if in scrollable area
// This lets you see the hover halo when scrolled to the middle of a large canvas
const scroll _area _rect = ancestor . getBoundingClientRect ( ) ;
rect = {
left : Math . max ( rect . left , scroll _area _rect . left ) ,
top : Math . max ( rect . top , scroll _area _rect . top ) ,
right : Math . min ( rect . right , scroll _area _rect . right ) ,
bottom : Math . min ( rect . bottom , scroll _area _rect . bottom ) ,
} ;
rect . width = rect . right - rect . left ;
rect . height = rect . bottom - rect . top ;
}
2021-05-18 17:46:00 +00:00
ancestor = ancestor . parentNode ;
}
2021-05-18 04:13:21 +00:00
halo . style . display = "block" ;
halo . style . position = "fixed" ;
halo . style . left = ` ${ rect . left } px ` ;
halo . style . top = ` ${ rect . top } px ` ;
halo . style . width = ` ${ rect . width } px ` ;
halo . style . height = ` ${ rect . height } px ` ;
// shorthand properties might not work in all browsers (not tested)
// this is so overkill...
halo . style . borderTopRightRadius = ` ${ parseFloat ( computed _style . borderTopRightRadius ) * border _radius _scale } px ` ;
halo . style . borderTopLeftRadius = ` ${ parseFloat ( computed _style . borderTopLeftRadius ) * border _radius _scale } px ` ;
halo . style . borderBottomRightRadius = ` ${ parseFloat ( computed _style . borderBottomRightRadius ) * border _radius _scale } px ` ;
halo . style . borderBottomLeftRadius = ` ${ parseFloat ( computed _style . borderBottomLeftRadius ) * border _radius _scale } px ` ;
2020-04-16 04:30:45 +00:00
} else {
2021-05-18 04:13:21 +00:00
halo . style . display = "none" ;
2020-04-16 04:06:38 +00:00
}
2020-04-10 21:45:18 +00:00
if ( time < inactive _until _time ) {
return ;
}
if ( recent _movement _amount < 5 ) {
2020-04-18 23:29:12 +00:00
if ( ! hover _candidate ) {
2020-04-10 21:45:18 +00:00
hover _candidate = {
x : average _point . x ,
y : average _point . y ,
time : Date . now ( ) ,
2020-04-19 03:00:15 +00:00
target : gaze _dragging || null ,
2020-04-10 21:45:18 +00:00
} ;
2020-04-19 03:00:15 +00:00
if ( ! gaze _dragging ) {
hover _candidate = get _hover _candidate ( hover _candidate . x , hover _candidate . y ) ;
}
2021-05-18 18:51:21 +00:00
if ( hover _candidate && ( paused && ! eye _gaze _mode _config . dwellClickEvenIfPaused ( hover _candidate . target ) ) ) {
2020-04-22 17:07:23 +00:00
hover _candidate = null ;
}
2020-04-10 21:45:18 +00:00
}
}
2020-04-11 01:25:05 +00:00
if ( recent _movement _amount > 100 ) {
2020-04-16 05:07:41 +00:00
if ( gaze _dragging ) {
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = true ;
2021-05-18 04:13:21 +00:00
window . dispatchEvent ( new PointerEvent ( "pointerup" ,
Object . assign ( get _event _options ( average _point ) , {
button : 0 ,
buttons : 0 ,
} )
) ) ;
2021-08-29 15:54:57 +00:00
window . untrusted _gesture = false ;
2020-04-11 21:03:45 +00:00
pointers = [ ] ; // prevent multi-touch panning
2020-04-11 01:25:05 +00:00
}
2020-04-11 05:23:30 +00:00
}
if ( recent _movement _amount > 60 ) {
2020-04-10 21:45:18 +00:00
hover _candidate = null ;
}
}
2020-04-19 17:04:09 +00:00
} ;
2020-04-23 06:20:32 +00:00
let raf _id ;
2020-04-19 17:04:09 +00:00
const animate = ( ) => {
2020-04-23 06:20:32 +00:00
raf _id = requestAnimationFrame ( animate ) ;
2020-04-19 17:04:09 +00:00
update ( ) ;
} ;
2020-04-23 06:20:32 +00:00
raf _id = requestAnimationFrame ( animate ) ;
2020-04-18 19:33:55 +00:00
2020-04-22 17:07:23 +00:00
const $floating _buttons =
$ ( "<div/>" )
. appendTo ( "body" )
. css ( {
position : "fixed" ,
bottom : 0 ,
left : 0 ,
transformOrigin : "bottom left" ,
transform : "scale(3)" ,
} ) ;
2020-04-18 19:33:55 +00:00
$ ( "<button title='Undo'/>" )
2020-04-19 03:26:00 +00:00
. on ( "click" , undo )
2020-04-22 17:07:23 +00:00
. appendTo ( $floating _buttons )
2020-04-18 19:33:55 +00:00
. css ( {
width : 28 ,
height : 28 ,
2020-04-22 17:07:23 +00:00
verticalAlign : "bottom" ,
2020-04-29 16:15:42 +00:00
position : "relative" , // to make the icon's "absolute" relative to here
2020-04-18 19:33:55 +00:00
} )
2020-04-19 03:26:00 +00:00
. append (
$ ( "<div>" )
. css ( {
position : "absolute" ,
left : 0 ,
top : 0 ,
2020-04-22 17:07:23 +00:00
width : 24 ,
height : 24 ,
2020-04-19 03:26:00 +00:00
backgroundImage : "url(images/classic/undo.svg)" ,
} )
) ;
2020-04-22 17:07:23 +00:00
2021-06-19 23:58:47 +00:00
// These are matched on exactly, for code that provides speech command synonyms
2020-05-12 16:21:53 +00:00
const pause _button _text = "Pause Dwell Clicking" ;
const resume _button _text = "Resume Dwell Clicking" ;
2021-05-18 18:51:21 +00:00
const $pause _button = $ ( ` <button title=" ${ pause _button _text } " class="toggle-dwell-clicking"/> ` )
2020-04-22 17:07:23 +00:00
. on ( "click" , ( ) => {
paused = ! paused ;
2020-05-12 16:21:53 +00:00
$pause _button
. attr ( "title" , paused ? resume _button _text : pause _button _text )
. find ( "div" ) . css ( {
2020-04-22 17:07:23 +00:00
backgroundImage :
paused ?
"url(images/classic/eye-gaze-unpause.svg)" :
"url(images/classic/eye-gaze-pause.svg)" ,
2020-05-12 16:21:53 +00:00
} ) ;
2020-04-22 17:07:23 +00:00
} )
. appendTo ( $floating _buttons )
. css ( {
width : 28 ,
height : 28 ,
verticalAlign : "bottom" ,
2020-04-29 16:15:42 +00:00
position : "relative" , // to make the icon's "absolute" relative to here
2020-04-22 17:07:23 +00:00
} )
. append (
$ ( "<div>" )
. css ( {
position : "absolute" ,
left : 0 ,
top : 0 ,
width : 24 ,
height : 24 ,
backgroundImage : "url(images/classic/eye-gaze-pause.svg)" ,
} )
) ;
2020-04-23 06:20:32 +00:00
clean _up _eye _gaze _mode = ( ) => {
console . log ( "Cleaning up / disabling eye gaze mode" ) ;
cancelAnimationFrame ( raf _id ) ;
2021-05-18 04:13:21 +00:00
halo . remove ( ) ;
dwell _indicator . remove ( ) ;
2020-04-23 06:20:32 +00:00
$floating _buttons . remove ( ) ;
2021-05-18 04:13:21 +00:00
window . removeEventListener ( "pointermove" , on _pointer _move ) ;
window . removeEventListener ( "pointerup" , on _pointer _up _or _cancel ) ;
window . removeEventListener ( "pointercancel" , on _pointer _up _or _cancel ) ;
window . removeEventListener ( "focus" , on _focus ) ;
window . removeEventListener ( "blur" , on _blur ) ;
document . removeEventListener ( "mouseleave" , on _mouse _leave _page ) ;
document . removeEventListener ( "mouseenter" , on _mouse _enter _page ) ;
2020-04-23 06:20:32 +00:00
clean _up _eye _gaze _mode = ( ) => { } ;
} ;
2020-04-10 21:45:18 +00:00
}
2019-12-10 04:32:28 +00:00
let pan _start _pos ;
let pan _start _scroll _top ;
let pan _start _scroll _left ;
2019-12-10 05:25:56 +00:00
function average _points ( points ) {
const average = { x : 0 , y : 0 } ;
for ( const pointer of points ) {
average . x += pointer . x ;
average . y += pointer . y ;
}
average . x /= points . length ;
average . y /= points . length ;
return average ;
}
2019-12-10 04:10:33 +00:00
$canvas _area . on ( "pointerdown" , ( event ) => {
2021-01-30 06:32:03 +00:00
if ( document . activeElement && document . activeElement !== document . body && document . activeElement !== document . documentElement ) {
// Allow unfocusing dialogs etc. in order to use keyboard shortcuts
document . activeElement . blur ( ) ;
}
2020-04-11 21:03:45 +00:00
if ( pointers . every ( ( pointer ) =>
// prevent multitouch panning in case of synthetic events from eye gaze mode
pointer . pointerId !== 1234567890 &&
// prevent multitouch panning in case of dragging across iframe boundary with a mouse/pen
// Note: there can be multiple active primary pointers, one per pointer type
! ( pointer . isPrimary && ( pointer . pointerType === "mouse" || pointer . pointerType === "pen" ) )
2020-01-05 22:27:51 +00:00
// @TODO: handle case of dragging across iframe boundary with touch
2020-04-11 21:03:45 +00:00
) ) {
pointers . push ( {
pointerId : event . pointerId ,
pointerType : event . pointerType ,
// isPrimary not available on jQuery.Event, and originalEvent not available in synthetic case
isPrimary : event . originalEvent && event . originalEvent . isPrimary || event . isPrimary ,
x : event . clientX ,
y : event . clientY ,
} ) ;
}
2019-12-10 05:25:56 +00:00
if ( pointers . length == 2 ) {
pan _start _pos = average _points ( pointers ) ;
2019-12-10 04:32:28 +00:00
pan _start _scroll _top = $canvas _area . scrollTop ( ) ;
pan _start _scroll _left = $canvas _area . scrollLeft ( ) ;
2019-12-10 04:10:33 +00:00
}
2019-12-10 05:19:08 +00:00
// Quick Undo when there are multiple pointers (i.e. for touch)
// see pointermove for other pointer types
2019-12-10 05:25:56 +00:00
if ( pointers . length >= 2 ) {
2019-12-10 05:19:08 +00:00
cancel ( ) ;
pointer _active = false ; // NOTE: pointer_active used in cancel()
return ;
}
2019-12-10 04:10:33 +00:00
} ) ;
2019-12-10 04:32:28 +00:00
$G . on ( "pointerup pointercancel" , ( event ) => {
2020-04-11 17:22:18 +00:00
pointers = pointers . filter ( ( pointer ) =>
pointer . pointerId !== event . pointerId
) ;
2019-12-10 04:10:33 +00:00
} ) ;
2019-12-10 04:32:28 +00:00
$G . on ( "pointermove" , ( event ) => {
for ( const pointer of pointers ) {
if ( pointer . pointerId === event . pointerId ) {
pointer . x = event . clientX ;
pointer . y = event . clientY ;
}
}
if ( pointers . length >= 2 ) {
2019-12-10 05:25:56 +00:00
const current _pos = average _points ( pointers ) ;
2019-12-10 04:32:28 +00:00
const difference _in _x = current _pos . x - pan _start _pos . x ;
const difference _in _y = current _pos . y - pan _start _pos . y ;
$canvas _area . scrollLeft ( pan _start _scroll _left - difference _in _x ) ;
$canvas _area . scrollTop ( pan _start _scroll _top - difference _in _y ) ;
}
} ) ;
// window.onerror = show_error_message;
2019-12-10 04:10:33 +00:00
2019-10-29 18:46:29 +00:00
$canvas . on ( "pointerdown" , e => {
2019-10-26 22:00:29 +00:00
update _canvas _rect ( ) ;
2019-10-21 18:16:36 +00:00
2019-09-29 03:16:51 +00:00
// Quick Undo when there are multiple pointers (i.e. for touch)
// see pointermove for other pointer types
2019-12-10 05:19:08 +00:00
// NOTE: this relies on event handler order for pointerdown
// pointer is not added to pointers yet
if ( pointers . length >= 1 ) {
2015-06-22 01:05:48 +00:00
cancel ( ) ;
2019-12-07 17:54:52 +00:00
pointer _active = false ; // NOTE: pointer_active used in cancel()
2020-04-11 21:03:45 +00:00
// in eye gaze mode, allow drawing with mouse after canceling gaze gesture with mouse
pointers = pointers . filter ( ( pointer ) =>
pointer . pointerId !== 1234567890
) ;
2015-06-22 01:05:48 +00:00
return ;
2014-06-03 01:38:06 +00:00
}
2019-12-15 01:51:50 +00:00
history _node _to _cancel _to = current _history _node ;
2019-10-01 19:06:50 +00:00
2019-12-10 04:10:33 +00:00
pointer _active = ! ! ( e . buttons & ( 1 | 2 ) ) ; // as far as tools are concerned
2019-02-10 17:52:28 +00:00
pointer _type = e . pointerType ;
2019-09-30 00:16:44 +00:00
pointer _buttons = e . buttons ;
2019-12-18 05:42:19 +00:00
$G . one ( "pointerup" , ( ) => {
2019-09-29 03:16:51 +00:00
pointer _active = false ;
2019-10-01 19:06:50 +00:00
update _helper _layer ( ) ;
2019-10-01 22:41:33 +00:00
if ( ! pointer _over _canvas && update _helper _layer _on _pointermove _active ) {
$G . off ( "pointermove" , update _helper _layer ) ;
update _helper _layer _on _pointermove _active = false ;
}
2014-06-08 21:31:26 +00:00
} ) ;
2019-10-01 19:06:50 +00:00
2014-05-04 13:32:02 +00:00
if ( e . button === 0 ) {
reverse = false ;
} else if ( e . button === 2 ) {
reverse = true ;
} else {
2015-06-22 01:05:48 +00:00
return ;
2014-02-24 05:57:52 +00:00
}
2019-10-01 19:06:50 +00:00
2014-05-04 13:32:02 +00:00
button = e . button ;
ctrl = e . ctrlKey ;
2018-01-25 03:51:12 +00:00
shift = e . shiftKey ;
2019-10-30 00:48:51 +00:00
pointer _start = pointer _previous = pointer = to _canvas _coords ( e ) ;
2018-01-21 15:03:59 +00:00
2019-10-29 20:29:38 +00:00
const pointerdown _action = ( ) => {
2019-12-07 18:23:13 +00:00
let interval _ids = [ ] ;
selected _tools . forEach ( ( selected _tool ) => {
if ( selected _tool . paint || selected _tool . pointerdown ) {
tool _go ( selected _tool , "pointerdown" ) ;
}
2019-12-12 03:18:47 +00:00
if ( selected _tool . paint _on _time _interval != null ) {
interval _ids . push ( setInterval ( ( ) => {
tool _go ( selected _tool ) ;
} , selected _tool . paint _on _time _interval ) ) ;
2019-12-07 18:23:13 +00:00
}
} ) ;
2018-01-21 15:03:59 +00:00
2015-06-22 00:01:12 +00:00
$G . on ( "pointermove" , canvas _pointer _move ) ;
2019-12-07 18:23:13 +00:00
2019-12-16 01:54:03 +00:00
$G . one ( "pointerup" , ( e , canceling ) => {
2014-06-07 15:39:22 +00:00
button = undefined ;
2019-10-09 01:52:03 +00:00
reverse = false ;
2019-12-12 04:58:19 +00:00
pointer = to _canvas _coords ( e ) ;
selected _tools . forEach ( ( selected _tool ) => {
2021-02-11 02:00:38 +00:00
selected _tool . pointerup && selected _tool . pointerup ( main _ctx , pointer . x , pointer . y ) ;
2019-12-12 04:58:19 +00:00
} ) ;
2019-09-21 05:50:02 +00:00
if ( selected _tools . length === 1 ) {
2019-09-21 05:59:56 +00:00
if ( selected _tool . deselect ) {
select _tools ( return _to _tools ) ;
}
2019-09-20 15:04:42 +00:00
}
2015-06-22 00:01:12 +00:00
$G . off ( "pointermove" , canvas _pointer _move ) ;
2019-12-07 18:23:13 +00:00
for ( const interval _id of interval _ids ) {
clearInterval ( interval _id ) ;
2014-06-07 15:39:22 +00:00
}
2019-12-16 01:54:03 +00:00
if ( ! canceling ) {
history _node _to _cancel _to = null ;
}
2014-06-07 15:39:22 +00:00
} ) ;
} ;
2018-01-21 15:03:59 +00:00
2019-12-11 01:31:53 +00:00
pointerdown _action ( ) ;
2019-10-01 19:06:50 +00:00
update _helper _layer ( ) ;
2014-05-04 13:32:02 +00:00
} ) ;
2014-02-24 05:57:52 +00:00
2019-10-29 18:46:29 +00:00
$canvas _area . on ( "pointerdown" , e => {
2014-09-23 02:10:11 +00:00
if ( e . button === 0 ) {
if ( $canvas _area . is ( e . target ) ) {
if ( selection ) {
deselect ( ) ;
}
}
}
2014-05-04 13:32:02 +00:00
} ) ;
2014-09-23 02:10:11 +00:00
2020-04-23 06:20:32 +00:00
function prevent _selection ( $el ) {
$el . on ( "mousedown selectstart contextmenu" , ( e ) => {
if ( e . isDefaultPrevented ( ) ) {
return ;
}
if (
e . target instanceof HTMLSelectElement ||
e . target instanceof HTMLTextAreaElement ||
( e . target instanceof HTMLLabelElement && e . type !== "contextmenu" ) ||
( e . target instanceof HTMLInputElement && e . target . type !== "color" )
) {
return ;
}
if ( e . button === 1 ) {
return ; // allow middle-click scrolling
}
e . preventDefault ( ) ;
// we're just trying to prevent selection
// but part of the default for mousedown is *deselection*
// so we have to do that ourselves explicitly
window . getSelection ( ) . removeAllRanges ( ) ;
} ) ;
}
prevent _selection ( $app ) ;
prevent _selection ( $toolbox ) ;
// prevent_selection($toolbox2);
prevent _selection ( $colorbox ) ;
2014-11-20 05:52:08 +00:00
2021-06-19 23:58:47 +00:00
// Stop drawing (or dragging or whatever) if you Alt+Tab or whatever
2019-12-18 05:42:19 +00:00
$G . on ( "blur" , ( ) => {
2015-06-22 00:01:12 +00:00
$G . triggerHandler ( "pointerup" ) ;
2014-11-20 05:52:08 +00:00
} ) ;