2020-12-04 06:38:08 +00:00
const fill _threshold = 1 ; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184
2014-05-04 13:32:02 +00:00
2018-07-27 18:04:40 +00:00
function get _brush _canvas _size ( brush _size , brush _shape ) {
// brush_shape optional, only matters if it's circle
2020-01-05 22:27:51 +00:00
// @TODO: does it actually still matter? the ellipse drawing code has changed
2018-07-27 18:04:40 +00:00
// round to nearest even number in order for the canvas to be drawn centered at a point reasonably
return Math . ceil ( brush _size * ( brush _shape === "circle" ? 2.1 : 1 ) / 2 ) * 2 ;
}
2014-12-11 21:36:48 +00:00
function render _brush ( ctx , shape , size ) {
2018-06-29 03:46:07 +00:00
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of draw_ellipse)
2018-06-29 00:18:47 +00:00
if ( shape . match ( /diagonal/ ) ) {
2014-12-11 21:36:48 +00:00
size -= 0.4 ;
}
2019-10-29 20:29:38 +00:00
const mid _x = Math . round ( ctx . canvas . width / 2 ) ;
const left = Math . round ( mid _x - size / 2 ) ;
const right = Math . round ( mid _x + size / 2 ) ;
const mid _y = Math . round ( ctx . canvas . height / 2 ) ;
const top = Math . round ( mid _y - size / 2 ) ;
const bottom = Math . round ( mid _y + size / 2 ) ;
2014-12-11 21:36:48 +00:00
if ( shape === "circle" ) {
2020-01-05 22:27:51 +00:00
// @TODO: ideally _without_pattern_support
2018-06-29 00:18:47 +00:00
draw _ellipse ( ctx , left , top , size , size , false , true ) ;
2018-07-27 18:04:40 +00:00
// was useful for testing:
// ctx.fillStyle = "red";
// ctx.fillRect(mid_x, mid_y, 1, 1);
2014-12-11 21:36:48 +00:00
} else if ( shape === "square" ) {
ctx . fillRect ( left , top , ~ ~ size , ~ ~ size ) ;
} else if ( shape === "diagonal" ) {
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( ctx , left , top , right , bottom ) ;
2014-12-11 21:36:48 +00:00
} else if ( shape === "reverse_diagonal" ) {
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( ctx , left , bottom , right , top ) ;
2014-12-11 21:36:48 +00:00
} else if ( shape === "horizontal" ) {
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( ctx , left , mid _y , size , mid _y ) ;
2014-12-11 21:36:48 +00:00
} else if ( shape === "vertical" ) {
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( ctx , mid _x , top , mid _x , size ) ;
2014-12-11 21:36:48 +00:00
}
2016-11-05 19:52:44 +00:00
}
2014-12-11 21:36:48 +00:00
2014-08-10 12:36:39 +00:00
function draw _ellipse ( ctx , x , y , w , h , stroke , fill ) {
2019-10-29 20:29:38 +00:00
const center _x = x + w / 2 ;
const center _y = y + h / 2 ;
2014-05-04 13:32:02 +00:00
if ( aliasing ) {
2019-10-29 20:29:38 +00:00
const points = [ ] ;
const step = 0.05 ;
for ( let theta = 0 ; theta < TAU ; theta += step ) {
2018-06-29 00:18:47 +00:00
points . push ( {
2018-06-29 01:00:51 +00:00
x : center _x + Math . cos ( theta ) * w / 2 ,
y : center _y + Math . sin ( theta ) * h / 2 ,
2018-06-29 00:18:47 +00:00
} ) ;
2014-05-04 13:32:02 +00:00
}
2018-06-29 00:18:47 +00:00
draw _polygon ( ctx , points , stroke , fill ) ;
2014-05-04 13:32:02 +00:00
} else {
ctx . beginPath ( ) ;
2021-02-03 01:25:24 +00:00
ctx . ellipse ( center _x , center _y , Math . abs ( w / 2 ) , Math . abs ( h / 2 ) , 0 , TAU , false ) ;
2014-05-04 13:32:02 +00:00
ctx . stroke ( ) ;
ctx . fill ( ) ;
}
}
2014-10-29 02:54:55 +00:00
2018-06-29 05:01:55 +00:00
function draw _rounded _rectangle ( ctx , x , y , width , height , radius _x , radius _y , stroke , fill ) {
2014-10-29 02:54:55 +00:00
2014-05-04 13:32:02 +00:00
if ( aliasing ) {
2019-10-29 20:29:38 +00:00
const points = [ ] ;
const lineTo = ( x , y ) => {
2018-06-28 23:14:32 +00:00
points . push ( { x , y } ) ;
} ;
2019-10-29 20:29:38 +00:00
const arc = ( x , y , radius _x , radius _y , startAngle , endAngle ) => {
const step = 0.05 ;
for ( let theta = startAngle ; theta < endAngle ; theta += step ) {
2018-06-28 23:14:32 +00:00
points . push ( {
2018-06-29 05:01:55 +00:00
x : x + Math . cos ( theta ) * radius _x ,
y : y + Math . sin ( theta ) * radius _y ,
2018-06-28 23:14:32 +00:00
} ) ;
}
// not just doing `theta <= endAngle` above because that doesn't account for floating point rounding errors
points . push ( {
2018-06-29 05:01:55 +00:00
x : x + Math . cos ( endAngle ) * radius _x ,
y : y + Math . sin ( endAngle ) * radius _y ,
2018-06-28 23:14:32 +00:00
} ) ;
} ;
2018-06-29 01:00:51 +00:00
2019-10-29 20:29:38 +00:00
const x2 = x + width ;
const y2 = y + height ;
2018-06-29 05:01:55 +00:00
arc ( x2 - radius _x , y + radius _y , radius _x , radius _y , TAU * 3 / 4 , TAU , false ) ;
lineTo ( x2 , y2 - radius _y ) ;
arc ( x2 - radius _x , y2 - radius _y , radius _x , radius _y , 0 , TAU * 1 / 4 , false ) ;
lineTo ( x + radius _x , y2 ) ;
arc ( x + radius _x , y2 - radius _y , radius _x , radius _y , TAU * 1 / 4 , TAU * 1 / 2 , false ) ;
lineTo ( x , y + radius _y ) ;
arc ( x + radius _x , y + radius _y , radius _x , radius _y , TAU / 2 , TAU * 3 / 4 , false ) ;
2018-06-28 23:14:32 +00:00
draw _polygon ( ctx , points , stroke , fill ) ;
2014-05-04 13:32:02 +00:00
} else {
ctx . beginPath ( ) ;
2018-06-29 05:01:55 +00:00
ctx . moveTo ( x + radius _x , y ) ;
ctx . lineTo ( x + width - radius _x , y ) ;
ctx . quadraticCurveTo ( x + width , y , x + width , y + radius _y ) ;
ctx . lineTo ( x + width , y + height - radius _y ) ;
ctx . quadraticCurveTo ( x + width , y + height , x + width - radius _x , y + height ) ;
ctx . lineTo ( x + radius _x , y + height ) ;
ctx . quadraticCurveTo ( x , y + height , x , y + height - radius _y ) ;
ctx . lineTo ( x , y + radius _y ) ;
ctx . quadraticCurveTo ( x , y , x + radius _x , y ) ;
2014-05-04 13:32:02 +00:00
ctx . closePath ( ) ;
2018-06-28 23:14:32 +00:00
if ( stroke ) {
ctx . stroke ( ) ;
}
if ( fill ) {
ctx . fill ( ) ;
}
2014-05-04 13:32:02 +00:00
}
}
2014-10-29 02:54:55 +00:00
2019-12-11 20:42:22 +00:00
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
2020-01-05 22:27:51 +00:00
// @TODO: protect against browser clearing canvases, invalidate cache
2019-12-18 07:49:58 +00:00
const get _brush _canvas = memoize _synchronous _function ( ( brush _shape , brush _size ) => {
2019-12-11 23:03:29 +00:00
const canvas _size = get _brush _canvas _size ( brush _size , brush _shape ) ;
const brush _canvas = make _canvas ( canvas _size , canvas _size ) ;
2019-12-11 20:42:22 +00:00
2019-12-11 23:03:29 +00:00
// brush_canvas.ctx.fillStyle = brush_canvas.ctx.strokeStyle = "black";
2019-12-11 20:42:22 +00:00
render _brush ( brush _canvas . ctx , brush _shape , brush _size ) ;
2019-12-11 23:03:29 +00:00
return brush _canvas ;
2019-12-18 03:08:52 +00:00
} , 20 ) ; // 12 brush tool options + current brush + current pencil + current eraser + current shape stroke + a few
2019-12-11 23:03:29 +00:00
2019-12-18 07:49:58 +00:00
$G . on ( "invalidate-brush-canvases" , ( ) => {
get _brush _canvas . clear _memo _cache ( ) ;
} ) ;
2019-12-11 23:03:29 +00:00
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
const stamp _brush _canvas = ( ctx , x , y , brush _shape , brush _size ) => {
const brush _canvas = get _brush _canvas ( brush _shape , brush _size ) ;
const offset _x = - Math . ceil ( brush _canvas . width / 2 ) ;
const offset _y = - Math . ceil ( brush _canvas . height / 2 ) ;
ctx . drawImage ( brush _canvas , x + offset _x , y + offset _y ) ;
} ;
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
const get _circumference _points _for _brush = memoize _synchronous _function ( ( brush _shape , brush _size ) => {
const brush _canvas = get _brush _canvas ( brush _shape , brush _size ) ;
2019-12-11 20:42:22 +00:00
const image _data = brush _canvas . ctx . getImageData ( 0 , 0 , brush _canvas . width , brush _canvas . height ) ;
2021-03-25 23:16:05 +00:00
const at = ( x , y ) => image _data . data [ ( y * image _data . width + x ) * 4 + 3 ] > 127 ;
2019-12-11 20:42:22 +00:00
2019-12-11 23:03:29 +00:00
const offset _x = - Math . ceil ( brush _canvas . width / 2 ) ;
const offset _y = - Math . ceil ( brush _canvas . height / 2 ) ;
2019-12-11 20:42:22 +00:00
const points = [ ] ;
for ( let x = 0 ; x < image _data . width ; x += 1 ) {
for ( let y = 0 ; y < image _data . height ; y += 1 ) {
if ( at ( x , y ) && (
! at ( x , y - 1 ) ||
! at ( x , y + 1 ) ||
! at ( x - 1 , y ) ||
! at ( x + 1 , y )
) ) {
points . push ( {
x : x + offset _x ,
y : y + offset _y ,
} ) ;
}
}
}
return points ;
} ) ;
2019-12-18 07:49:58 +00:00
$G . on ( "invalidate-brush-canvases" , ( ) => {
get _circumference _points _for _brush . clear _memo _cache ( ) ;
} ) ;
2019-10-29 20:29:38 +00:00
let line _brush _canvas ;
2019-12-12 02:57:02 +00:00
// USAGE NOTE: must be called outside of any other usage of op_canvas (because of render_brush)
2018-06-18 04:52:33 +00:00
function update _brush _for _drawing _lines ( stroke _size ) {
2019-12-12 02:57:02 +00:00
if ( aliasing && stroke _size > 1 ) {
line _brush _canvas = get _brush _canvas ( "circle" , stroke _size ) ;
}
2018-06-18 04:52:33 +00:00
}
2019-10-29 22:15:45 +00:00
function draw _line _without _pattern _support ( ctx , x1 , y1 , x2 , y2 , stroke _size = 1 ) {
2014-05-04 13:32:02 +00:00
if ( aliasing ) {
2016-11-06 00:13:54 +00:00
if ( stroke _size > 1 ) {
2019-10-29 18:46:29 +00:00
bresenham _line ( x1 , y1 , x2 , y2 , ( x , y ) => {
2018-06-18 04:52:33 +00:00
ctx . drawImage ( line _brush _canvas , ~ ~ ( x - line _brush _canvas . width / 2 ) , ~ ~ ( y - line _brush _canvas . height / 2 ) ) ;
2016-11-06 00:13:54 +00:00
} ) ;
} else {
2019-10-29 18:46:29 +00:00
bresenham _line ( x1 , y1 , x2 , y2 , ( x , y ) => {
2016-11-06 00:13:54 +00:00
ctx . fillRect ( x , y , 1 , 1 ) ;
} ) ;
}
2014-05-04 13:32:02 +00:00
} else {
ctx . beginPath ( ) ;
ctx . moveTo ( x1 , y1 ) ;
ctx . lineTo ( x2 , y2 ) ;
2014-08-10 15:09:56 +00:00
2016-11-06 00:13:54 +00:00
ctx . lineWidth = stroke _size ;
2014-08-10 15:09:56 +00:00
ctx . lineCap = "round" ;
2014-05-04 13:32:02 +00:00
ctx . stroke ( ) ;
2014-08-08 21:44:46 +00:00
ctx . lineCap = "butt" ;
2014-05-04 13:32:02 +00:00
}
}
2014-10-29 02:54:55 +00:00
2014-08-11 20:45:55 +00:00
function bresenham _line ( x1 , y1 , x2 , y2 , callback ) {
2014-05-04 13:32:02 +00:00
// Bresenham's line algorithm
2021-02-11 15:16:39 +00:00
x1 = ~ ~ x1 ; x2 = ~ ~ x2 ; y1 = ~ ~ y1 ; y2 = ~ ~ y2 ;
2014-05-04 13:32:02 +00:00
2019-10-29 20:29:38 +00:00
const dx = Math . abs ( x2 - x1 ) ;
const dy = Math . abs ( y2 - y1 ) ;
const sx = ( x1 < x2 ) ? 1 : - 1 ;
const sy = ( y1 < y2 ) ? 1 : - 1 ;
let err = dx - dy ;
2014-05-04 13:32:02 +00:00
2019-09-21 15:33:01 +00:00
// eslint-disable-next-line no-constant-condition
while ( true ) {
2014-05-04 13:32:02 +00:00
callback ( x1 , y1 ) ;
if ( x1 === x2 && y1 === y2 ) break ;
2019-10-29 20:29:38 +00:00
const e2 = err * 2 ;
2014-05-04 13:32:02 +00:00
if ( e2 > - dy ) { err -= dy ; x1 += sx ; }
if ( e2 < dx ) { err += dx ; y1 += sy ; }
}
}
2014-10-29 02:54:55 +00:00
2021-06-19 23:58:47 +00:00
function bresenham _dense _line ( x1 , y1 , x2 , y2 , callback ) {
// Bresenham's line algorithm with a callback between going horizontal and vertical
2021-02-11 15:16:39 +00:00
x1 = ~ ~ x1 ; x2 = ~ ~ x2 ; y1 = ~ ~ y1 ; y2 = ~ ~ y2 ;
2014-05-15 17:14:23 +00:00
2019-10-29 20:29:38 +00:00
const dx = Math . abs ( x2 - x1 ) ;
const dy = Math . abs ( y2 - y1 ) ;
const sx = ( x1 < x2 ) ? 1 : - 1 ;
const sy = ( y1 < y2 ) ? 1 : - 1 ;
let err = dx - dy ;
2014-05-15 17:14:23 +00:00
2019-09-21 15:33:01 +00:00
// eslint-disable-next-line no-constant-condition
while ( true ) {
2014-05-15 17:14:23 +00:00
callback ( x1 , y1 ) ;
if ( x1 === x2 && y1 === y2 ) break ;
2019-10-29 20:29:38 +00:00
const e2 = err * 2 ;
2014-05-15 17:14:23 +00:00
if ( e2 > - dy ) { err -= dy ; x1 += sx ; }
callback ( x1 , y1 ) ;
if ( e2 < dx ) { err += dx ; y1 += sy ; }
}
}
2014-10-29 02:54:55 +00:00
2019-12-23 20:03:24 +00:00
function draw _fill _without _pattern _support ( ctx , start _x , start _y , fill _r , fill _g , fill _b , fill _a ) {
2014-10-01 19:46:35 +00:00
2020-01-05 22:27:51 +00:00
// @TODO: split up processing in case it takes too long?
2018-01-18 03:18:52 +00:00
// progress bar and abort button (outside of image-manipulation.js)
// or at least just free up the main thread every once in a while
2020-01-05 22:27:51 +00:00
// @TODO: speed up with typed arrays? https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipulation-with-typed-arrays/
2018-01-18 03:18:52 +00:00
// could avoid endianness issues if only copying colors
// the jsperf only shows ~15% improvement
// maybe do something fancier like special-casing large chunks of single-color image
// (octree? or just have a higher level stack of chunks to fill and check at if a chunk is homogeneous)
2021-02-11 02:00:38 +00:00
const c _width = main _canvas . width ;
const c _height = main _canvas . height ;
2021-05-14 16:24:24 +00:00
start _x = Math . max ( 0 , Math . min ( Math . floor ( start _x ) , c _width ) ) ;
start _y = Math . max ( 0 , Math . min ( Math . floor ( start _y ) , c _height ) ) ;
const stack = [ [ start _x , start _y ] ] ;
2019-10-29 20:29:38 +00:00
const id = ctx . getImageData ( 0 , 0 , c _width , c _height ) ;
2019-11-03 15:36:34 +00:00
let pixel _pos = ( start _y * c _width + start _x ) * 4 ;
2019-10-29 20:29:38 +00:00
const start _r = id . data [ pixel _pos + 0 ] ;
const start _g = id . data [ pixel _pos + 1 ] ;
const start _b = id . data [ pixel _pos + 2 ] ;
const start _a = id . data [ pixel _pos + 3 ] ;
2014-10-01 19:46:35 +00:00
2021-03-25 23:16:05 +00:00
// @TODO: should this have a threshold? I feel like I had a reason it should not,
// but if there's is, I should document it.
2014-10-01 19:46:35 +00:00
if (
fill _r === start _r &&
fill _g === start _g &&
fill _b === start _b &&
fill _a === start _a
) {
return ;
}
while ( stack . length ) {
2019-10-29 21:00:31 +00:00
let new _pos ;
let x ;
let y ;
let reach _left ;
let reach _right ;
new _pos = stack . pop ( ) ;
x = new _pos [ 0 ] ;
y = new _pos [ 1 ] ;
pixel _pos = ( y * c _width + x ) * 4 ;
2020-01-05 17:06:44 +00:00
while ( should _fill _at ( pixel _pos ) ) {
2017-05-25 03:07:10 +00:00
y -- ;
pixel _pos = ( y * c _width + x ) * 4 ;
2014-10-01 19:46:35 +00:00
}
2019-10-29 21:00:31 +00:00
reach _left = false ;
reach _right = false ;
// eslint-disable-next-line no-constant-condition
while ( true ) {
2017-05-25 03:07:10 +00:00
y ++ ;
pixel _pos = ( y * c _width + x ) * 4 ;
2020-01-05 17:06:44 +00:00
if ( ! ( y < c _height && should _fill _at ( pixel _pos ) ) ) {
2017-05-25 03:07:10 +00:00
break ;
}
2020-01-05 17:06:44 +00:00
do _fill _at ( pixel _pos ) ;
2014-10-01 19:46:35 +00:00
if ( x > 0 ) {
2020-01-05 17:06:44 +00:00
if ( should _fill _at ( pixel _pos - 4 ) ) {
2014-10-01 19:46:35 +00:00
if ( ! reach _left ) {
stack . push ( [ x - 1 , y ] ) ;
reach _left = true ;
}
} else if ( reach _left ) {
reach _left = false ;
}
}
if ( x < c _width - 1 ) {
2020-01-05 17:06:44 +00:00
if ( should _fill _at ( pixel _pos + 4 ) ) {
2014-10-01 19:46:35 +00:00
if ( ! reach _right ) {
stack . push ( [ x + 1 , y ] ) ;
reach _right = true ;
}
} else if ( reach _right ) {
reach _right = false ;
}
}
pixel _pos += c _width * 4 ;
}
2019-10-29 21:00:31 +00:00
}
2014-10-01 19:46:35 +00:00
ctx . putImageData ( id , 0 , 0 ) ;
2020-01-05 17:06:44 +00:00
function should _fill _at ( pixel _pos ) {
2014-10-01 19:46:35 +00:00
return (
2020-01-05 17:06:44 +00:00
// matches start color (i.e. region to fill)
2020-12-04 06:38:08 +00:00
Math . abs ( id . data [ pixel _pos + 0 ] - start _r ) <= fill _threshold &&
Math . abs ( id . data [ pixel _pos + 1 ] - start _g ) <= fill _threshold &&
Math . abs ( id . data [ pixel _pos + 2 ] - start _b ) <= fill _threshold &&
Math . abs ( id . data [ pixel _pos + 3 ] - start _a ) <= fill _threshold
2014-10-01 19:46:35 +00:00
) ;
}
2020-01-05 17:06:44 +00:00
function do _fill _at ( pixel _pos ) {
2018-01-25 03:51:12 +00:00
id . data [ pixel _pos + 0 ] = fill _r ;
id . data [ pixel _pos + 1 ] = fill _g ;
id . data [ pixel _pos + 2 ] = fill _b ;
id . data [ pixel _pos + 3 ] = fill _a ;
}
}
2019-12-23 20:03:24 +00:00
function draw _fill ( ctx , start _x , start _y , swatch ) {
if ( typeof swatch === "string" ) {
const fill _rgba = get _rgba _from _color ( swatch ) ;
draw _fill _without _pattern _support ( ctx , start _x , start _y , fill _rgba [ 0 ] , fill _rgba [ 1 ] , fill _rgba [ 2 ] , fill _rgba [ 3 ] ) ;
} else {
const source _canvas = ctx . canvas ;
const fill _canvas = make _canvas ( source _canvas . width , source _canvas . height ) ;
draw _fill _separately ( source _canvas . ctx , fill _canvas . ctx , start _x , start _y , 255 , 255 , 255 , 255 ) ;
replace _colors _with _swatch ( fill _canvas . ctx , swatch , 0 , 0 ) ;
ctx . drawImage ( fill _canvas , 0 , 0 ) ;
}
2019-12-23 18:50:24 +00:00
}
function draw _fill _separately ( source _ctx , dest _ctx , start _x , start _y , fill _r , fill _g , fill _b , fill _a ) {
2021-05-14 16:24:24 +00:00
const c _width = main _canvas . width ;
const c _height = main _canvas . height ;
start _x = Math . max ( 0 , Math . min ( Math . floor ( start _x ) , c _width ) ) ;
start _y = Math . max ( 0 , Math . min ( Math . floor ( start _y ) , c _height ) ) ;
2019-12-23 18:50:24 +00:00
const stack = [ [ start _x , start _y ] ] ;
const source _id = source _ctx . getImageData ( 0 , 0 , c _width , c _height ) ;
const dest _id = dest _ctx . getImageData ( 0 , 0 , c _width , c _height ) ;
let pixel _pos = ( start _y * c _width + start _x ) * 4 ;
const start _r = source _id . data [ pixel _pos + 0 ] ;
const start _g = source _id . data [ pixel _pos + 1 ] ;
const start _b = source _id . data [ pixel _pos + 2 ] ;
const start _a = source _id . data [ pixel _pos + 3 ] ;
while ( stack . length ) {
let new _pos ;
let x ;
let y ;
let reach _left ;
let reach _right ;
new _pos = stack . pop ( ) ;
x = new _pos [ 0 ] ;
y = new _pos [ 1 ] ;
pixel _pos = ( y * c _width + x ) * 4 ;
2020-01-05 17:06:44 +00:00
while ( should _fill _at ( pixel _pos ) ) {
2019-12-23 18:50:24 +00:00
y -- ;
pixel _pos = ( y * c _width + x ) * 4 ;
}
reach _left = false ;
reach _right = false ;
// eslint-disable-next-line no-constant-condition
while ( true ) {
y ++ ;
pixel _pos = ( y * c _width + x ) * 4 ;
2020-01-05 17:06:44 +00:00
if ( ! ( y < c _height && should _fill _at ( pixel _pos ) ) ) {
2019-12-23 18:50:24 +00:00
break ;
}
2020-01-05 17:06:44 +00:00
do _fill _at ( pixel _pos ) ;
2019-12-23 18:50:24 +00:00
if ( x > 0 ) {
2020-01-05 17:06:44 +00:00
if ( should _fill _at ( pixel _pos - 4 ) ) {
2019-12-23 18:50:24 +00:00
if ( ! reach _left ) {
stack . push ( [ x - 1 , y ] ) ;
reach _left = true ;
}
} else if ( reach _left ) {
reach _left = false ;
}
}
if ( x < c _width - 1 ) {
2020-01-05 17:06:44 +00:00
if ( should _fill _at ( pixel _pos + 4 ) ) {
2019-12-23 18:50:24 +00:00
if ( ! reach _right ) {
stack . push ( [ x + 1 , y ] ) ;
reach _right = true ;
}
} else if ( reach _right ) {
reach _right = false ;
}
}
pixel _pos += c _width * 4 ;
}
}
dest _ctx . putImageData ( dest _id , 0 , 0 ) ;
2020-01-05 17:06:44 +00:00
function should _fill _at ( pixel _pos ) {
2019-12-23 18:50:24 +00:00
return (
2020-01-05 17:06:44 +00:00
// not reached yet
2019-12-23 18:50:24 +00:00
dest _id . data [ pixel _pos + 3 ] === 0 &&
2020-01-05 17:06:44 +00:00
// and matches start color (i.e. region to fill)
2019-12-23 18:50:24 +00:00
(
2020-12-04 06:38:08 +00:00
Math . abs ( source _id . data [ pixel _pos + 0 ] - start _r ) <= fill _threshold &&
Math . abs ( source _id . data [ pixel _pos + 1 ] - start _g ) <= fill _threshold &&
Math . abs ( source _id . data [ pixel _pos + 2 ] - start _b ) <= fill _threshold &&
Math . abs ( source _id . data [ pixel _pos + 3 ] - start _a ) <= fill _threshold
2019-12-23 18:50:24 +00:00
)
) ;
}
2020-01-05 17:06:44 +00:00
function do _fill _at ( pixel _pos ) {
2019-12-23 18:50:24 +00:00
dest _id . data [ pixel _pos + 0 ] = fill _r ;
dest _id . data [ pixel _pos + 1 ] = fill _g ;
dest _id . data [ pixel _pos + 2 ] = fill _b ;
dest _id . data [ pixel _pos + 3 ] = fill _a ;
}
}
2019-12-23 20:03:24 +00:00
function replace _color _globally ( image _data , from _r , from _g , from _b , from _a , to _r , to _g , to _b , to _a ) {
2018-01-25 03:51:12 +00:00
if (
2019-12-23 20:03:24 +00:00
from _r === to _r &&
from _g === to _g &&
from _b === to _b &&
from _a === to _a
2018-01-25 03:51:12 +00:00
) {
return ;
}
2019-12-23 20:03:24 +00:00
const { data } = image _data ;
for ( let i = 0 ; i < data . length ; i += 4 ) {
if (
2020-12-04 06:38:08 +00:00
Math . abs ( data [ i + 0 ] - from _r ) <= fill _threshold &&
Math . abs ( data [ i + 1 ] - from _g ) <= fill _threshold &&
Math . abs ( data [ i + 2 ] - from _b ) <= fill _threshold &&
Math . abs ( data [ i + 3 ] - from _a ) <= fill _threshold
2019-12-23 20:03:24 +00:00
) {
data [ i + 0 ] = to _r ;
data [ i + 1 ] = to _g ;
data [ i + 2 ] = to _b ;
data [ i + 3 ] = to _a ;
2018-01-25 03:51:12 +00:00
}
}
2019-12-23 20:03:24 +00:00
}
2018-01-25 03:51:12 +00:00
2019-12-23 20:03:24 +00:00
function find _color _globally ( source _image _data , dest _image _data , find _r , find _g , find _b , find _a ) {
const source _data = source _image _data . data ;
const dest _data = dest _image _data . data ;
for ( let i = 0 ; i < source _data . length ; i += 4 ) {
if (
2020-12-04 06:38:08 +00:00
Math . abs ( source _data [ i + 0 ] - find _r ) <= fill _threshold &&
Math . abs ( source _data [ i + 1 ] - find _g ) <= fill _threshold &&
Math . abs ( source _data [ i + 2 ] - find _b ) <= fill _threshold &&
Math . abs ( source _data [ i + 3 ] - find _a ) <= fill _threshold
2019-12-23 20:03:24 +00:00
) {
dest _data [ i + 0 ] = 255 ;
dest _data [ i + 1 ] = 255 ;
dest _data [ i + 2 ] = 255 ;
dest _data [ i + 3 ] = 255 ;
}
2018-01-25 03:51:12 +00:00
}
2019-12-23 20:03:24 +00:00
}
2018-01-25 03:51:12 +00:00
2021-05-14 16:24:24 +00:00
function draw _noncontiguous _fill _without _pattern _support ( ctx , x , y , fill _r , fill _g , fill _b , fill _a ) {
x = Math . max ( 0 , Math . min ( Math . floor ( x ) , ctx . canvas . width ) ) ;
y = Math . max ( 0 , Math . min ( Math . floor ( y ) , ctx . canvas . height ) ) ;
2019-12-23 20:03:24 +00:00
const image _data = ctx . getImageData ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
const start _index = ( y * image _data . width + x ) * 4 ;
const start _r = image _data . data [ start _index + 0 ] ;
const start _g = image _data . data [ start _index + 1 ] ;
const start _b = image _data . data [ start _index + 2 ] ;
const start _a = image _data . data [ start _index + 3 ] ;
replace _color _globally ( image _data , start _r , start _g , start _b , start _a , fill _r , fill _g , fill _b , fill _a ) ;
ctx . putImageData ( image _data , 0 , 0 ) ;
}
function draw _noncontiguous _fill ( ctx , x , y , swatch ) {
if ( typeof swatch === "string" ) {
const fill _rgba = get _rgba _from _color ( swatch ) ;
draw _noncontiguous _fill _without _pattern _support ( ctx , x , y , fill _rgba [ 0 ] , fill _rgba [ 1 ] , fill _rgba [ 2 ] , fill _rgba [ 3 ] ) ;
} else {
const source _canvas = ctx . canvas ;
const fill _canvas = make _canvas ( source _canvas . width , source _canvas . height ) ;
2020-04-18 23:02:10 +00:00
draw _noncontiguous _fill _separately ( source _canvas . ctx , fill _canvas . ctx , x , y ) ;
2019-12-23 20:03:24 +00:00
replace _colors _with _swatch ( fill _canvas . ctx , swatch , 0 , 0 ) ;
ctx . drawImage ( fill _canvas , 0 , 0 ) ;
2014-10-01 19:46:35 +00:00
}
}
2014-10-02 21:17:43 +00:00
2021-05-14 16:24:24 +00:00
function draw _noncontiguous _fill _separately ( source _ctx , dest _ctx , x , y ) {
x = Math . max ( 0 , Math . min ( Math . floor ( x ) , source _ctx . canvas . width ) ) ;
y = Math . max ( 0 , Math . min ( Math . floor ( y ) , source _ctx . canvas . height ) ) ;
2019-12-23 20:03:24 +00:00
const source _image _data = source _ctx . getImageData ( 0 , 0 , source _ctx . canvas . width , source _ctx . canvas . height ) ;
const dest _image _data = dest _ctx . getImageData ( 0 , 0 , dest _ctx . canvas . width , dest _ctx . canvas . height ) ;
const start _index = ( y * source _image _data . width + x ) * 4 ;
const start _r = source _image _data . data [ start _index + 0 ] ;
const start _g = source _image _data . data [ start _index + 1 ] ;
const start _b = source _image _data . data [ start _index + 2 ] ;
const start _a = source _image _data . data [ start _index + 3 ] ;
find _color _globally ( source _image _data , dest _image _data , start _r , start _g , start _b , start _a ) ;
dest _ctx . putImageData ( dest _image _data , 0 , 0 ) ;
}
2019-12-15 04:11:30 +00:00
function apply _image _transformation ( meta , fn ) {
2014-10-02 21:17:43 +00:00
// Apply an image transformation function to either the selection or the entire canvas
2021-02-11 02:00:38 +00:00
const original _canvas = selection ? selection . source _canvas : main _canvas ;
2014-10-02 21:17:43 +00:00
2019-10-29 21:43:46 +00:00
const new _canvas = make _canvas ( original _canvas . width , original _canvas . height ) ;
2018-01-18 01:13:02 +00:00
2019-10-29 20:29:38 +00:00
const original _ctx = original _canvas . getContext ( "2d" ) ;
const new _ctx = new _canvas . getContext ( "2d" ) ;
2018-01-18 01:13:02 +00:00
2014-10-02 21:17:43 +00:00
fn ( original _canvas , original _ctx , new _canvas , new _ctx ) ;
if ( selection ) {
2019-12-15 02:32:57 +00:00
undoable ( {
2021-02-18 12:48:37 +00:00
name : ` ${ meta . name } ( ${ localize ( "Selection" ) } ) ` ,
2019-12-15 04:11:30 +00:00
icon : meta . icon ,
2019-12-15 02:32:57 +00:00
soft : true ,
} , ( ) => {
2019-12-13 03:48:32 +00:00
selection . replace _source _canvas ( new _canvas ) ;
} ) ;
2014-10-02 21:17:43 +00:00
} else {
2019-12-16 01:54:03 +00:00
deselect ( ) ;
cancel ( ) ;
2019-12-15 04:11:30 +00:00
undoable ( {
name : meta . name ,
icon : meta . icon ,
} , ( ) => {
2019-12-08 14:07:31 +00:00
saved = false ;
2014-10-02 21:17:43 +00:00
2021-02-11 02:00:38 +00:00
main _ctx . copy ( new _canvas ) ;
2014-10-02 21:17:43 +00:00
$canvas . trigger ( "update" ) ; // update handles
} ) ;
}
}
2014-12-08 02:45:23 +00:00
function flip _horizontal ( ) {
2019-12-15 05:05:11 +00:00
apply _image _transformation ( {
2021-01-30 15:18:50 +00:00
name : localize ( "Flip horizontal" ) ,
2019-12-15 05:05:11 +00:00
icon : get _help _folder _icon ( "p_fliph.png" ) ,
} , ( original _canvas , original _ctx , new _canvas , new _ctx ) => {
2014-12-08 02:45:23 +00:00
new _ctx . translate ( new _canvas . width , 0 ) ;
new _ctx . scale ( - 1 , 1 ) ;
new _ctx . drawImage ( original _canvas , 0 , 0 ) ;
} ) ;
}
function flip _vertical ( ) {
2019-12-15 05:05:11 +00:00
apply _image _transformation ( {
2021-01-30 15:18:50 +00:00
name : localize ( "Flip vertical" ) ,
2019-12-15 05:05:11 +00:00
icon : get _help _folder _icon ( "p_flipv.png" ) ,
} , ( original _canvas , original _ctx , new _canvas , new _ctx ) => {
2014-12-08 02:45:23 +00:00
new _ctx . translate ( 0 , new _canvas . height ) ;
new _ctx . scale ( 1 , - 1 ) ;
new _ctx . drawImage ( original _canvas , 0 , 0 ) ;
} ) ;
}
function rotate ( angle ) {
2019-12-15 05:05:11 +00:00
apply _image _transformation ( {
2021-01-30 15:18:50 +00:00
name : ` ${ localize ( "Rotate by angle" ) } ${ angle / TAU * 360 } ${ localize ( "Degrees" ) } ` ,
2019-12-15 05:05:11 +00:00
icon : get _help _folder _icon ( ` p_rotate_ ${ angle >= 0 ? "cw" : "ccw" } .png ` ) ,
} , ( original _canvas , original _ctx , new _canvas , new _ctx ) => {
2014-12-08 13:56:20 +00:00
new _ctx . save ( ) ;
2014-12-08 02:45:23 +00:00
switch ( angle ) {
case TAU / 4 :
case TAU * - 3 / 4 :
new _canvas . width = original _canvas . height ;
new _canvas . height = original _canvas . width ;
2018-01-21 21:02:45 +00:00
new _ctx . disable _image _smoothing ( ) ;
2014-12-08 02:45:23 +00:00
new _ctx . translate ( new _canvas . width , 0 ) ;
new _ctx . rotate ( TAU / 4 ) ;
break ;
case TAU / 2 :
case TAU / - 2 :
new _ctx . translate ( new _canvas . width , new _canvas . height ) ;
new _ctx . rotate ( TAU / 2 ) ;
break ;
case TAU * 3 / 4 :
case TAU / - 4 :
new _canvas . width = original _canvas . height ;
new _canvas . height = original _canvas . width ;
2018-01-21 21:02:45 +00:00
new _ctx . disable _image _smoothing ( ) ;
2014-12-08 02:45:23 +00:00
new _ctx . translate ( 0 , new _canvas . height ) ;
new _ctx . rotate ( TAU / - 4 ) ;
break ;
2019-10-29 21:08:13 +00:00
default : {
2019-10-29 20:29:38 +00:00
const w = original _canvas . width ;
const h = original _canvas . height ;
2014-12-08 13:56:20 +00:00
2019-10-29 20:29:38 +00:00
let bb _min _x = + Infinity ;
let bb _max _x = - Infinity ;
let bb _min _y = + Infinity ;
let bb _max _y = - Infinity ;
const corner = ( x01 , y01 ) => {
const x = Math . sin ( - angle ) * h * x01 + Math . cos ( + angle ) * w * y01 ;
const y = Math . sin ( + angle ) * w * y01 + Math . cos ( - angle ) * h * x01 ;
2014-12-08 13:56:20 +00:00
bb _min _x = Math . min ( bb _min _x , x ) ;
bb _max _x = Math . max ( bb _max _x , x ) ;
bb _min _y = Math . min ( bb _min _y , y ) ;
bb _max _y = Math . max ( bb _max _y , y ) ;
} ;
corner ( 0 , 0 ) ;
corner ( 0 , 1 ) ;
corner ( 1 , 0 ) ;
corner ( 1 , 1 ) ;
2019-10-29 20:29:38 +00:00
const bb _x = bb _min _x ;
const bb _y = bb _min _y ;
const bb _w = bb _max _x - bb _min _x ;
const bb _h = bb _max _y - bb _min _y ;
2014-12-08 13:56:20 +00:00
new _canvas . width = bb _w ;
new _canvas . height = bb _h ;
2018-01-21 21:02:45 +00:00
new _ctx . disable _image _smoothing ( ) ;
2014-12-08 13:56:20 +00:00
if ( ! transparency ) {
2021-02-11 02:04:35 +00:00
new _ctx . fillStyle = selected _colors . background ;
2014-12-08 13:56:20 +00:00
new _ctx . fillRect ( 0 , 0 , new _canvas . width , new _canvas . height ) ;
}
new _ctx . translate ( - bb _x , - bb _y ) ;
new _ctx . rotate ( angle ) ;
new _ctx . drawImage ( original _canvas , 0 , 0 , w , h ) ;
2014-12-08 02:45:23 +00:00
break ;
2019-10-29 21:08:13 +00:00
}
2014-12-08 02:45:23 +00:00
}
new _ctx . drawImage ( original _canvas , 0 , 0 ) ;
2014-12-08 13:56:20 +00:00
new _ctx . restore ( ) ;
2014-12-08 02:45:23 +00:00
} ) ;
}
2021-06-19 23:58:47 +00:00
function stretch _and _skew ( x _scale , y _scale , h _skew , v _skew ) {
2019-12-15 18:01:47 +00:00
apply _image _transformation ( {
2019-12-15 18:15:31 +00:00
name :
2021-06-19 23:58:47 +00:00
( h _skew !== 0 || v _skew !== 0 ) ? (
( x _scale !== 1 || y _scale !== 1 ) ? localize ( "Stretch and Skew" ) : localize ( "Skew" )
2021-01-30 15:18:50 +00:00
) : localize ( "Stretch" ) ,
2019-12-15 18:01:47 +00:00
icon : get _help _folder _icon (
2021-06-19 23:58:47 +00:00
( h _skew !== 0 ) ? "p_skew_h.png" :
( v _skew !== 0 ) ? "p_skew_v.png" :
( y _scale !== 1 ) ? (
( x _scale !== 1 ) ? "p_stretch_both.png" : "p_stretch_v.png"
2019-12-15 18:15:31 +00:00
) : "p_stretch_h.png"
2019-12-15 18:01:47 +00:00
) ,
} , ( original _canvas , original _ctx , new _canvas , new _ctx ) => {
2021-06-19 23:58:47 +00:00
const w = original _canvas . width * x _scale ;
const h = original _canvas . height * y _scale ;
2014-11-29 19:20:59 +00:00
2019-10-29 20:29:38 +00:00
let bb _min _x = + Infinity ;
let bb _max _x = - Infinity ;
let bb _min _y = + Infinity ;
let bb _max _y = - Infinity ;
const corner = ( x01 , y01 ) => {
2021-06-19 23:58:47 +00:00
const x = Math . tan ( h _skew ) * h * x01 + w * y01 ;
const y = Math . tan ( v _skew ) * w * y01 + h * x01 ;
2014-11-29 19:20:59 +00:00
bb _min _x = Math . min ( bb _min _x , x ) ;
bb _max _x = Math . max ( bb _max _x , x ) ;
bb _min _y = Math . min ( bb _min _y , y ) ;
bb _max _y = Math . max ( bb _max _y , y ) ;
} ;
corner ( 0 , 0 ) ;
corner ( 0 , 1 ) ;
corner ( 1 , 0 ) ;
corner ( 1 , 1 ) ;
2019-10-29 20:29:38 +00:00
const bb _x = bb _min _x ;
const bb _y = bb _min _y ;
const bb _w = bb _max _x - bb _min _x ;
const bb _h = bb _max _y - bb _min _y ;
2014-11-29 19:20:59 +00:00
2019-12-15 18:23:10 +00:00
new _canvas . width = Math . max ( 1 , bb _w ) ;
new _canvas . height = Math . max ( 1 , bb _h ) ;
2018-01-21 21:02:45 +00:00
new _ctx . disable _image _smoothing ( ) ;
2014-11-29 19:20:59 +00:00
if ( ! transparency ) {
2021-02-11 02:04:35 +00:00
new _ctx . fillStyle = selected _colors . background ;
2014-11-29 19:20:59 +00:00
new _ctx . fillRect ( 0 , 0 , new _canvas . width , new _canvas . height ) ;
}
new _ctx . save ( ) ;
new _ctx . transform (
1 , // x scale
2021-06-19 23:58:47 +00:00
Math . tan ( v _skew ) , // vertical skew (skewY)
Math . tan ( h _skew ) , // horizontal skew (skewX)
2014-11-29 19:20:59 +00:00
1 , // y scale
2014-12-08 13:14:58 +00:00
- bb _x , // x translation
- bb _y // y translation
2014-11-29 19:20:59 +00:00
) ;
new _ctx . drawImage ( original _canvas , 0 , 0 , w , h ) ;
new _ctx . restore ( ) ;
} ) ;
}
2019-12-15 03:32:34 +00:00
function invert _rgb ( source _ctx , dest _ctx = source _ctx ) {
const image _data = source _ctx . getImageData ( 0 , 0 , source _ctx . canvas . width , source _ctx . canvas . height ) ;
for ( let i = 0 ; i < image _data . data . length ; i += 4 ) {
image _data . data [ i + 0 ] = 255 - image _data . data [ i + 0 ] ;
image _data . data [ i + 1 ] = 255 - image _data . data [ i + 1 ] ;
image _data . data [ i + 2 ] = 255 - image _data . data [ i + 2 ] ;
}
dest _ctx . putImageData ( image _data , 0 , 0 ) ;
}
2021-02-10 19:53:57 +00:00
function invert _monochrome ( source _ctx , dest _ctx = source _ctx , monochrome _info = detect _monochrome ( source _ctx ) ) {
2021-02-08 05:34:14 +00:00
const image _data = source _ctx . getImageData ( 0 , 0 , source _ctx . canvas . width , source _ctx . canvas . height ) ;
// Note: values in pixel_array may be different on big endian vs little endian machines.
// Only rely on equality of values within the array.
2021-02-10 17:26:15 +00:00
// pixel_array is a performance optimization, to access whole pixels at a time instead of individual color channels.
2021-02-08 05:34:14 +00:00
const pixel _array = new Uint32Array ( image _data . data . buffer ) ;
2021-02-10 19:53:57 +00:00
if ( monochrome _info . presentNonTransparentUint32s . length === 0 ) {
2021-02-10 17:54:59 +00:00
// Fully transparent.
// No change, and no need to copy the image to dest canvas to represent that lack of a change.
return ;
}
2021-02-10 19:53:57 +00:00
if ( monochrome _info . presentNonTransparentUint32s . length === 1 ) {
2021-02-10 17:54:59 +00:00
// Only one non-transparent color present in the image.
// Can't use just the information of what colors are in the canvas to invert, need to look at the palette.
2021-02-08 05:34:14 +00:00
// We could've done this in a unified way, but whatever!
// Personally, I think this is a CHARMINGLY poor solution.
2021-02-10 17:54:59 +00:00
// Maybe a little less so now that I added handling for transparency (i.e. Free-Form Select).
2021-02-08 05:34:14 +00:00
const color _1 = palette [ 0 ] ;
const color _2 = palette [ 14 ] || palette [ 1 ] ;
2021-02-10 17:54:59 +00:00
const color _1 _rgba = get _rgba _from _color ( color _1 ) ;
2021-02-10 19:53:57 +00:00
const present _rgba = monochrome _info . presentNonTransparentRGBAs [ 0 ] ;
2021-02-10 17:54:59 +00:00
if (
2021-02-10 19:53:57 +00:00
present _rgba [ 0 ] === color _1 _rgba [ 0 ] &&
present _rgba [ 1 ] === color _1 _rgba [ 1 ] &&
present _rgba [ 2 ] === color _1 _rgba [ 2 ] &&
present _rgba [ 3 ] === color _1 _rgba [ 3 ]
2021-02-10 17:54:59 +00:00
) {
2021-02-08 05:34:14 +00:00
dest _ctx . fillStyle = color _2 ;
} else {
dest _ctx . fillStyle = color _1 ;
}
2021-03-25 18:08:35 +00:00
if ( monochrome _info . monochromeWithTransparency ) {
2021-02-10 17:54:59 +00:00
dest _ctx . putImageData ( image _data , 0 , 0 ) ;
dest _ctx . globalCompositeOperation = "source-in" ;
}
2021-02-08 05:34:14 +00:00
dest _ctx . fillRect ( 0 , 0 , source _ctx . canvas . width , source _ctx . canvas . height ) ;
return ;
}
2021-02-10 19:53:57 +00:00
const [ uint32 _a , uint32 _b ] = monochrome _info . presentNonTransparentUint32s ;
2021-02-08 05:34:14 +00:00
for ( let i = 0 , len = pixel _array . length ; i < len ; i += 1 ) {
2021-02-10 19:53:57 +00:00
if ( pixel _array [ i ] === uint32 _a ) {
pixel _array [ i ] = uint32 _b ;
} else if ( pixel _array [ i ] === uint32 _b ) {
pixel _array [ i ] = uint32 _a ;
2021-02-08 05:34:14 +00:00
}
}
dest _ctx . putImageData ( image _data , 0 , 0 ) ;
}
2020-01-04 04:17:01 +00:00
function threshold _black _and _white ( ctx , threshold ) {
const image _data = ctx . getImageData ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
for ( let i = 0 ; i < image _data . data . length ; i += 4 ) {
const white = ( image _data . data [ i + 0 ] + image _data . data [ i + 1 ] + image _data . data [ i + 2 ] ) / 3 / 255 > threshold ;
image _data . data [ i + 0 ] = 255 * white ;
image _data . data [ i + 1 ] = 255 * white ;
image _data . data [ i + 2 ] = 255 * white ;
image _data . data [ i + 3 ] = 255 ;
}
ctx . putImageData ( image _data , 0 , 0 ) ;
}
2020-05-29 19:41:07 +00:00
function replace _colors _with _swatch ( ctx , swatch , x _offset _from _global _canvas = 0 , y _offset _from _global _canvas = 0 ) {
2018-06-28 23:14:32 +00:00
// USAGE NOTE: Context MUST be untranslated! (for the rectangle to cover the exact area of the canvas, and presumably for the pattern alignment as well)
2018-06-27 21:11:36 +00:00
// This function is mainly for patterns support (for black & white mode) but naturally handles solid colors as well.
2018-06-17 23:01:04 +00:00
ctx . globalCompositeOperation = "source-in" ;
ctx . fillStyle = swatch ;
ctx . beginPath ( ) ;
ctx . rect ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
ctx . save ( ) ;
2018-06-17 23:25:47 +00:00
ctx . translate ( - x _offset _from _global _canvas , - y _offset _from _global _canvas ) ;
2018-06-17 23:01:04 +00:00
ctx . fill ( ) ;
ctx . restore ( ) ;
}
2018-06-18 04:05:44 +00:00
// adapted from https://github.com/Pomax/bezierjs
function compute _bezier ( t , start _x , start _y , control _1 _x , control _1 _y , control _2 _x , control _2 _y , end _x , end _y ) {
2019-10-29 20:29:38 +00:00
const mt = 1 - t ;
const mt2 = mt * mt ;
const t2 = t * t ;
let a , b , c , d = 0 ;
2018-06-29 01:00:51 +00:00
2018-06-18 04:05:44 +00:00
a = mt2 * mt ;
b = mt2 * t * 3 ;
c = mt * t2 * 3 ;
d = t * t2 ;
2018-06-29 01:00:51 +00:00
2018-06-18 04:05:44 +00:00
return {
2018-06-29 01:00:51 +00:00
x : a * start _x + b * control _1 _x + c * control _2 _x + d * end _x ,
y : a * start _y + b * control _1 _y + c * control _2 _y + d * end _y
2018-06-18 04:05:44 +00:00
} ;
}
2018-06-27 21:11:36 +00:00
function draw _bezier _curve _without _pattern _support ( ctx , start _x , start _y , control _1 _x , control _1 _y , control _2 _x , control _2 _y , end _x , end _y , stroke _size ) {
2019-10-29 20:29:38 +00:00
const steps = 100 ;
let point _a = { x : start _x , y : start _y } ;
for ( let t = 0 ; t < 1 ; t += 1 / steps ) {
const point _b = compute _bezier ( t , start _x , start _y , control _1 _x , control _1 _y , control _2 _x , control _2 _y , end _x , end _y ) ;
2020-01-05 22:27:51 +00:00
// @TODO: carry "error" from Bresenham line algorithm between iterations? and/or get a proper Bezier drawing algorithm
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( ctx , point _a . x , point _a . y , point _b . x , point _b . y , stroke _size ) ;
2018-06-18 04:05:44 +00:00
point _a = point _b ;
}
2019-09-21 15:22:35 +00:00
}
2018-06-18 04:05:44 +00:00
function draw _quadratic _curve ( ctx , start _x , start _y , control _x , control _y , end _x , end _y , stroke _size ) {
draw _bezier _curve ( ctx , start _x , start _y , control _x , control _y , control _x , control _y , end _x , end _y , stroke _size ) ;
2019-09-21 15:22:35 +00:00
}
2018-06-18 04:05:44 +00:00
2018-06-27 21:11:36 +00:00
function draw _bezier _curve ( ctx , start _x , start _y , control _1 _x , control _1 _y , control _2 _x , control _2 _y , end _x , end _y , stroke _size ) {
// could calculate bounds of Bezier curve with something like bezier-js
// but just using the control points should be fine
2019-10-29 20:29:38 +00:00
const min _x = Math . min ( start _x , control _1 _x , control _2 _x , end _x ) ;
const min _y = Math . min ( start _y , control _1 _y , control _2 _y , end _y ) ;
const max _x = Math . max ( start _x , control _1 _x , control _2 _x , end _x ) ;
const max _y = Math . max ( start _y , control _1 _y , control _2 _y , end _y ) ;
2019-10-29 18:46:29 +00:00
draw _with _swatch ( ctx , min _x , min _y , max _x , max _y , stroke _color , op _ctx _2d => {
2018-06-27 21:11:36 +00:00
draw _bezier _curve _without _pattern _support ( op _ctx _2d , start _x , start _y , control _1 _x , control _1 _y , control _2 _x , control _2 _y , end _x , end _y , stroke _size ) ;
} ) ;
}
function draw _line ( ctx , x1 , y1 , x2 , y2 , stroke _size ) {
2019-10-29 20:29:38 +00:00
const min _x = Math . min ( x1 , x2 ) ;
const min _y = Math . min ( y1 , y2 ) ;
const max _x = Math . max ( x1 , x2 ) ;
const max _y = Math . max ( y1 , y2 ) ;
2019-10-29 18:46:29 +00:00
draw _with _swatch ( ctx , min _x , min _y , max _x , max _y , stroke _color , op _ctx _2d => {
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support ( op _ctx _2d , x1 , y1 , x2 , y2 , stroke _size ) ;
} ) ;
// also works:
2018-06-27 22:03:02 +00:00
// draw_line_strip(ctx, [{x: x1, y: y1}, {x: x2, y: y2}]);
2018-06-27 21:11:36 +00:00
}
2019-10-29 20:29:38 +00:00
let grid _pattern ;
2019-10-28 23:40:06 +00:00
function draw _grid ( ctx , scale ) {
2020-01-05 22:27:51 +00:00
const pattern _size = Math . floor ( scale ) ; // @TODO: try ceil too
2019-10-28 23:40:06 +00:00
if ( ! grid _pattern || grid _pattern . width !== pattern _size || grid _pattern . height !== pattern _size ) {
2019-10-29 21:43:46 +00:00
const grid _pattern _canvas = make _canvas ( pattern _size , pattern _size ) ;
2019-10-29 20:29:38 +00:00
const dark _gray = "#808080" ;
const light _gray = "#c0c0c0" ;
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillStyle = dark _gray ;
2019-10-28 23:40:06 +00:00
grid _pattern _canvas . ctx . fillRect ( 0 , 0 , 1 , pattern _size ) ;
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillStyle = dark _gray ;
2019-10-28 23:40:06 +00:00
grid _pattern _canvas . ctx . fillRect ( 0 , 0 , pattern _size , 1 ) ;
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillStyle = light _gray ;
2019-10-28 23:40:06 +00:00
for ( let i = 1 ; i < pattern _size ; i += 2 ) {
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillRect ( i , 0 , 1 , 1 ) ;
grid _pattern _canvas . ctx . fillRect ( 0 , i , 1 , 1 ) ;
}
grid _pattern = ctx . createPattern ( grid _pattern _canvas , "repeat" ) ;
}
2019-10-28 23:40:06 +00:00
ctx . save ( ) ;
ctx . rect ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
if ( scale !== pattern _size ) {
ctx . translate ( - 0.5 , - 0.75 ) ; // hand picked to look "good" at 110% in chrome
// might be better to just hide the grid in some more cases tho
2020-01-05 22:27:51 +00:00
// ...@TODO: if I can get helper layer to be pixel aligned, I can probably remove this
2019-10-28 23:40:06 +00:00
}
ctx . scale ( scale / pattern _size , scale / pattern _size ) ;
ctx . enable _image _smoothing ( ) ;
2019-09-30 14:40:47 +00:00
ctx . fillStyle = grid _pattern ;
2019-10-28 23:40:06 +00:00
ctx . fill ( ) ;
ctx . restore ( ) ;
2019-09-30 14:40:47 +00:00
}
2019-10-30 02:04:48 +00:00
( ( ) => {
// the dashes of the border are sized such that at 4x zoom,
// they're squares equal to one canvas pixel
// they're offset by a screen pixel tho from the canvas pixel cells
const svg _for _creating _matrices = document . createElementNS ( "http://www.w3.org/2000/svg" , "svg" ) ;
const horizontal _pattern _canvas = make _canvas ( 8 , 4 ) ;
const vertical _pattern _canvas = make _canvas ( 4 , 8 ) ;
let horizontal _pattern ;
let vertical _pattern ;
function draw _dashes ( ctx , x , y , go _x , go _y , scale , translate _x , translate _y ) {
if ( ! vertical _pattern ) {
horizontal _pattern _canvas . ctx . fillStyle = "white" ;
horizontal _pattern _canvas . ctx . fillRect ( 4 , 0 , 4 , 4 ) ;
vertical _pattern _canvas . ctx . fillStyle = "white" ;
vertical _pattern _canvas . ctx . fillRect ( 0 , 4 , 4 , 4 ) ;
horizontal _pattern = ctx . createPattern ( horizontal _pattern _canvas , "repeat" ) ;
vertical _pattern = ctx . createPattern ( vertical _pattern _canvas , "repeat" ) ;
}
const dash _width = 1 ;
const hairline _width = 1 / scale ; // size of a screen pixel
ctx . save ( ) ;
ctx . scale ( scale , scale ) ;
ctx . translate ( translate _x , translate _y ) ;
ctx . translate ( x , y ) ;
ctx . globalCompositeOperation = "difference" ;
if ( go _x > 0 ) {
const matrix = svg _for _creating _matrices . createSVGMatrix ( ) ;
2019-12-04 18:46:45 +00:00
if ( horizontal _pattern . setTransform ) { // not supported by Edge as of 2019-12-04
horizontal _pattern . setTransform ( matrix . translate ( - x , - y ) . translate ( hairline _width , 0 ) . scale ( 1 / scale ) ) ;
}
2019-10-30 02:04:48 +00:00
ctx . fillStyle = horizontal _pattern ;
ctx . fillRect ( 0 , 0 , go _x , dash _width ) ;
} else if ( go _y > 0 ) {
const matrix = svg _for _creating _matrices . createSVGMatrix ( ) ;
2019-12-04 18:46:45 +00:00
if ( vertical _pattern . setTransform ) { // not supported by Edge as of 2019-12-04
vertical _pattern . setTransform ( matrix . translate ( - x , - y ) . translate ( 0 , hairline _width ) . scale ( 1 / scale ) ) ;
}
2019-10-30 02:04:48 +00:00
ctx . fillStyle = vertical _pattern ;
ctx . fillRect ( 0 , 0 , dash _width , go _y ) ;
}
ctx . restore ( ) ;
}
window . draw _selection _box = ( ctx , rect _x , rect _y , rect _w , rect _h , scale , translate _x , translate _y ) => {
draw _dashes ( ctx , rect _x , rect _y , rect _w - 1 , 0 , scale , translate _x , translate _y ) ; // top
if ( rect _h === 1 ) {
draw _dashes ( ctx , rect _x , rect _y , 0 , 1 , scale , translate _x , translate _y ) ; // left
} else {
draw _dashes ( ctx , rect _x , rect _y + 1 , 0 , rect _h - 2 , scale , translate _x , translate _y ) ; // left
}
draw _dashes ( ctx , rect _x + rect _w - 1 , rect _y , 0 , rect _h , scale , translate _x , translate _y ) ; // right
draw _dashes ( ctx , rect _x , rect _y + rect _h - 1 , rect _w - 1 , 0 , scale , translate _x , translate _y ) ; // bottom
draw _dashes ( ctx , rect _x , rect _y + 1 , 0 , 1 , scale , translate _x , translate _y ) ; // top left dangling bit???
} ;
} ) ( ) ;
2019-10-29 18:46:29 +00:00
( ( ) => {
2018-06-16 21:49:00 +00:00
2019-10-29 20:29:38 +00:00
const tessy = ( function initTesselator ( ) {
2018-06-16 21:49:00 +00:00
// function called for each vertex of tesselator output
2021-06-19 23:58:47 +00:00
function vertex _callback ( data , poly _vert _array ) {
2019-12-14 22:49:23 +00:00
// window.console && console.log(data[0], data[1]);
2021-06-19 23:58:47 +00:00
poly _vert _array [ poly _vert _array . length ] = data [ 0 ] ;
poly _vert _array [ poly _vert _array . length ] = data [ 1 ] ;
2018-06-16 21:49:00 +00:00
}
2021-06-19 23:58:47 +00:00
function begin _callback ( type ) {
2018-06-16 21:49:00 +00:00
if ( type !== libtess . primitiveType . GL _TRIANGLES ) {
2019-12-14 22:49:23 +00:00
window . console && console . log ( ` Expected TRIANGLES but got type: ${ type } ` ) ;
2018-06-16 21:49:00 +00:00
}
}
2021-06-19 23:58:47 +00:00
function error _callback ( errno ) {
2019-12-14 22:49:23 +00:00
window . console && console . log ( 'error callback' ) ;
window . console && console . log ( ` error number: ${ errno } ` ) ;
2018-06-16 21:49:00 +00:00
}
// callback for when segments intersect and must be split
2021-06-19 23:58:47 +00:00
function combine _callback ( coords /*, data, weight*/ ) {
2019-12-14 22:49:23 +00:00
// window.console && console.log('combine callback');
2018-06-16 21:49:00 +00:00
return [ coords [ 0 ] , coords [ 1 ] , coords [ 2 ] ] ;
}
2021-06-19 23:58:47 +00:00
function edge _callback ( /*flag*/ ) {
2018-06-16 21:49:00 +00:00
// don't really care about the flag, but need no-strip/no-fan behavior
2019-12-14 22:49:23 +00:00
// window.console && console.log('edge flag: ' + flag);
2018-06-16 21:49:00 +00:00
}
2019-10-29 20:29:38 +00:00
const tessy = new libtess . GluTesselator ( ) ;
2018-06-16 21:49:00 +00:00
// tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE);
2021-06-19 23:58:47 +00:00
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _VERTEX _DATA , vertex _callback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _BEGIN , begin _callback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _ERROR , error _callback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _COMBINE , combine _callback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _EDGE _FLAG , edge _callback ) ;
2018-06-16 21:49:00 +00:00
return tessy ;
2021-02-11 15:16:39 +00:00
} ( ) ) ;
2018-06-16 21:49:00 +00:00
function triangulate ( contours ) {
// libtess will take 3d verts and flatten to a plane for tesselation
// since only doing 2d tesselation here, provide z=1 normal to skip
// iterating over verts only to get the same answer.
tessy . gluTessNormal ( 0 , 0 , 1 ) ;
2019-10-29 20:29:38 +00:00
const triangleVerts = [ ] ;
2018-06-16 21:49:00 +00:00
tessy . gluTessBeginPolygon ( triangleVerts ) ;
2019-10-29 20:29:38 +00:00
for ( let i = 0 ; i < contours . length ; i ++ ) {
2018-06-16 21:49:00 +00:00
tessy . gluTessBeginContour ( ) ;
2019-10-29 20:29:38 +00:00
const contour = contours [ i ] ;
for ( let j = 0 ; j < contour . length ; j += 2 ) {
const coords = [ contour [ j ] , contour [ j + 1 ] , 0 ] ;
2018-06-16 21:49:00 +00:00
tessy . gluTessVertex ( coords , coords ) ;
}
tessy . gluTessEndContour ( ) ;
}
tessy . gluTessEndPolygon ( ) ;
return triangleVerts ;
}
2019-10-29 20:29:38 +00:00
let gl ;
let positionLoc ;
2018-06-16 21:49:00 +00:00
function initWebGL ( canvas ) {
2020-12-10 20:20:46 +00:00
try {
gl = canvas . getContext ( 'webgl' , { antialias : false } ) ;
} catch ( error ) {
show _error _message ( "Failed to get WebGL context. You may need to refresh the web page, or restart your computer." , error ) ;
return ;
}
2018-06-16 21:49:00 +00:00
2020-12-04 15:33:43 +00:00
if ( ! gl ) {
show _error _message ( "Failed to get WebGL context. You may need to refresh the web page, or restart your computer." ) ;
return ;
}
2019-10-23 20:42:12 +00:00
window . WEBGL _lose _context = gl . getExtension ( "WEBGL_lose_context" ) ;
2019-10-29 20:29:38 +00:00
const program = createShaderProgram ( ) ;
2018-06-16 23:57:13 +00:00
positionLoc = gl . getAttribLocation ( program , 'position' ) ;
2018-06-16 21:49:00 +00:00
gl . enableVertexAttribArray ( positionLoc ) ;
}
2018-06-16 23:57:13 +00:00
function initArrayBuffer ( triangleVertexCoords ) {
2018-06-16 21:49:00 +00:00
// put triangle coordinates into a WebGL ArrayBuffer and bind to
// shader's 'position' attribute variable
2019-10-29 20:29:38 +00:00
const rawData = new Float32Array ( triangleVertexCoords ) ;
const polygonArrayBuffer = gl . createBuffer ( ) ;
2018-06-16 21:49:00 +00:00
gl . bindBuffer ( gl . ARRAY _BUFFER , polygonArrayBuffer ) ;
gl . bufferData ( gl . ARRAY _BUFFER , rawData , gl . STATIC _DRAW ) ;
gl . vertexAttribPointer ( positionLoc , 2 , gl . FLOAT , false , 0 , 0 ) ;
2018-06-16 23:57:13 +00:00
return triangleVertexCoords . length / 2 ;
}
2018-06-16 21:49:00 +00:00
function createShaderProgram ( ) {
// create vertex shader
2019-10-29 20:29:38 +00:00
const vertexSrc = [
2018-06-16 21:49:00 +00:00
'attribute vec4 position;' ,
'void main() {' ,
2019-10-29 21:00:31 +00:00
' /* already in normalized coordinates, so just pass through */' ,
' gl_Position = position;' ,
2018-06-16 21:49:00 +00:00
'}'
] . join ( '' ) ;
2019-10-29 20:29:38 +00:00
const vertexShader = gl . createShader ( gl . VERTEX _SHADER ) ;
2018-06-16 21:49:00 +00:00
gl . shaderSource ( vertexShader , vertexSrc ) ;
gl . compileShader ( vertexShader ) ;
if ( ! gl . getShaderParameter ( vertexShader , gl . COMPILE _STATUS ) ) {
2019-12-14 22:49:23 +00:00
window . console && console . log (
2019-10-29 21:16:33 +00:00
` Vertex shader failed to compile. Log: ${ gl . getShaderInfoLog ( vertexShader ) } `
2018-06-16 21:49:00 +00:00
) ;
}
// create fragment shader
2019-10-29 20:29:38 +00:00
const fragmentSrc = [
2018-06-16 21:49:00 +00:00
'precision mediump float;' ,
'void main() {' ,
2019-10-29 21:00:31 +00:00
' gl_FragColor = vec4(0, 0, 0, 1);' ,
2018-06-16 21:49:00 +00:00
'}'
] . join ( '' ) ;
2019-10-29 20:29:38 +00:00
const fragmentShader = gl . createShader ( gl . FRAGMENT _SHADER ) ;
2018-06-16 21:49:00 +00:00
gl . shaderSource ( fragmentShader , fragmentSrc ) ;
gl . compileShader ( fragmentShader ) ;
if ( ! gl . getShaderParameter ( fragmentShader , gl . COMPILE _STATUS ) ) {
2019-12-14 22:49:23 +00:00
window . console && console . log (
2019-10-29 21:16:33 +00:00
` Fragment shader failed to compile. Log: ${ gl . getShaderInfoLog ( fragmentShader ) } `
2018-06-16 21:49:00 +00:00
) ;
}
// link shaders to create our program
2019-10-29 20:29:38 +00:00
const program = gl . createProgram ( ) ;
2018-06-16 21:49:00 +00:00
gl . attachShader ( program , vertexShader ) ;
gl . attachShader ( program , fragmentShader ) ;
gl . linkProgram ( program ) ;
gl . useProgram ( program ) ;
return program ;
}
2019-10-29 20:29:38 +00:00
const op _canvas _webgl = document . createElement ( 'canvas' ) ;
const op _canvas _2d = document . createElement ( 'canvas' ) ;
const op _ctx _2d = op _canvas _2d . getContext ( "2d" ) ;
2018-06-16 21:49:00 +00:00
2018-06-27 21:11:36 +00:00
initWebGL ( op _canvas _webgl ) ;
2019-12-09 03:57:09 +00:00
let warning _tid ;
2019-10-22 03:43:16 +00:00
op _canvas _webgl . addEventListener ( "webglcontextlost" , ( e ) => {
e . preventDefault ( ) ;
2019-12-09 03:57:09 +00:00
window . console && console . warn ( "WebGL context lost" ) ;
2019-10-23 20:41:09 +00:00
clamp _brush _sizes ( ) ;
2019-12-09 03:57:09 +00:00
warning _tid = setTimeout ( ( ) => {
show _error _message ( "The WebGL context was lost. You may need to refresh the web page, or restart your computer." ) ;
} , 3000 ) ;
2019-10-22 03:43:16 +00:00
} , false ) ;
op _canvas _webgl . addEventListener ( "webglcontextrestored" , ( ) => {
initWebGL ( op _canvas _webgl ) ;
2019-10-23 20:42:12 +00:00
2019-12-09 03:57:09 +00:00
window . console && console . warn ( "WebGL context restored" ) ;
clearTimeout ( warning _tid ) ;
2019-10-23 20:41:09 +00:00
clamp _brush _sizes ( ) ;
2019-12-18 07:49:58 +00:00
// brushes rendered using WebGL may be invalid (i.e. invisible) since the context was lost
// invalidate the cache(s) so that brushes will be re-rendered now that WebGL is restored
$G . triggerHandler ( "invalidate-brush-canvases" ) ;
2019-12-09 03:57:09 +00:00
2020-05-29 19:41:07 +00:00
$G . triggerHandler ( "redraw-tool-options-because-webglcontextrestored" ) ;
2019-10-22 03:43:16 +00:00
} , false ) ;
2018-06-16 21:49:00 +00:00
2019-10-23 20:41:09 +00:00
function clamp _brush _sizes ( ) {
2019-10-29 20:29:38 +00:00
const max _size = 100 ;
2019-10-23 20:41:09 +00:00
if ( brush _size > max _size ) {
brush _size = max _size ;
show _error _message ( ` Brush size clamped to ${ max _size } ` ) ;
}
if ( pencil _size > max _size ) {
pencil _size = max _size ;
show _error _message ( ` Pencil size clamped to ${ max _size } ` ) ;
}
if ( stroke _size > max _size ) {
stroke _size = max _size ;
show _error _message ( ` Stroke size clamped to ${ max _size } ` ) ;
}
}
2019-10-29 18:46:29 +00:00
window . draw _line _strip = ( ctx , points ) => {
2018-06-17 16:47:20 +00:00
draw _polygon _or _line _strip ( ctx , points , true , false , false ) ;
2018-06-16 23:18:05 +00:00
} ;
2019-10-29 18:46:29 +00:00
window . draw _polygon = ( ctx , points , stroke , fill ) => {
2018-06-17 16:47:20 +00:00
draw _polygon _or _line _strip ( ctx , points , stroke , fill , true ) ;
2018-06-16 23:18:05 +00:00
} ;
2018-06-17 16:47:20 +00:00
function draw _polygon _or _line _strip ( ctx , points , stroke , fill , close _path ) {
2018-06-29 03:46:07 +00:00
// this must be before stuff is done with op_canvas
// otherwise update_brush_for_drawing_lines calls render_brush calls draw_ellipse calls draw_polygon calls draw_polygon_or_line_strip
// trying to use the same op_canvas
// (also, avoiding infinite recursion by checking for stroke; assuming brushes will never have outlines)
if ( stroke && stroke _size > 1 ) {
update _brush _for _drawing _lines ( stroke _size ) ;
}
2019-10-29 20:29:38 +00:00
const stroke _color = ctx . strokeStyle ;
const fill _color = ctx . fillStyle ;
2018-06-16 21:49:00 +00:00
2019-10-29 20:29:38 +00:00
const numPoints = points . length ;
const numCoords = numPoints * 2 ;
2018-06-16 21:49:00 +00:00
2018-06-18 19:53:59 +00:00
if ( numPoints === 0 ) {
return ;
}
2019-10-29 20:29:38 +00:00
let x _min = + Infinity ;
let x _max = - Infinity ;
let y _min = + Infinity ;
let y _max = - Infinity ;
2019-11-03 15:36:34 +00:00
for ( const { x , y } of points ) {
2018-06-16 21:49:00 +00:00
x _min = Math . min ( x , x _min ) ;
x _max = Math . max ( x , x _max ) ;
y _min = Math . min ( y , y _min ) ;
y _max = Math . max ( y , y _max ) ;
}
x _max += 1 ;
y _max += 1 ;
2018-06-28 23:14:32 +00:00
x _min -= 1 ;
y _min -= 1 ;
2018-06-16 21:49:00 +00:00
2018-06-27 21:11:36 +00:00
op _canvas _webgl . width = x _max - x _min ;
op _canvas _webgl . height = y _max - y _min ;
gl . viewport ( 0 , 0 , op _canvas _webgl . width , op _canvas _webgl . height ) ;
2018-06-16 21:49:00 +00:00
2019-10-29 20:29:38 +00:00
const coords = new Float32Array ( numCoords ) ;
2019-09-21 15:59:30 +00:00
for ( let i = 0 ; i < numPoints ; i ++ ) {
2018-06-27 21:11:36 +00:00
coords [ i * 2 + 0 ] = ( points [ i ] . x - x _min ) / op _canvas _webgl . width * 2 - 1 ;
coords [ i * 2 + 1 ] = 1 - ( points [ i ] . y - y _min ) / op _canvas _webgl . height * 2 ;
2020-01-05 22:27:51 +00:00
// @TODO: investigate: does this cause resolution/information loss? can we change the coordinate system?
2018-06-17 00:05:49 +00:00
}
2018-06-16 23:18:05 +00:00
2018-06-16 21:49:00 +00:00
if ( fill ) {
2019-10-29 20:29:38 +00:00
const contours = [ coords ] ;
const polyTriangles = triangulate ( contours ) ;
2019-09-21 15:59:30 +00:00
let numVertices = initArrayBuffer ( polyTriangles ) ;
2018-06-17 16:36:16 +00:00
gl . clear ( gl . COLOR _BUFFER _BIT ) ;
2018-06-16 23:57:13 +00:00
gl . drawArrays ( gl . TRIANGLES , 0 , numVertices ) ;
2018-06-17 05:27:14 +00:00
2018-06-27 21:11:36 +00:00
op _canvas _2d . width = op _canvas _webgl . width ;
op _canvas _2d . height = op _canvas _webgl . height ;
2018-06-17 16:47:20 +00:00
2018-06-27 21:11:36 +00:00
op _ctx _2d . drawImage ( op _canvas _webgl , 0 , 0 ) ;
replace _colors _with _swatch ( op _ctx _2d , fill _color , x _min , y _min ) ;
ctx . drawImage ( op _canvas _2d , x _min , y _min ) ;
2018-06-16 21:49:00 +00:00
}
if ( stroke ) {
2018-06-17 05:27:14 +00:00
if ( stroke _size > 1 ) {
2019-10-29 20:29:38 +00:00
const stroke _margin = ~ ~ ( stroke _size * 1.1 ) ;
2018-06-17 16:47:20 +00:00
2019-10-29 20:29:38 +00:00
const op _canvas _x = x _min - stroke _margin ;
const op _canvas _y = y _min - stroke _margin ;
2018-06-29 03:23:57 +00:00
2018-06-27 21:11:36 +00:00
op _canvas _2d . width = x _max - x _min + stroke _margin * 2 ;
op _canvas _2d . height = y _max - y _min + stroke _margin * 2 ;
2019-09-21 15:59:30 +00:00
for ( let i = 0 ; i < numPoints - ( close _path ? 0 : 1 ) ; i ++ ) {
2019-10-29 20:29:38 +00:00
const point _a = points [ i ] ;
const point _b = points [ ( i + 1 ) % numPoints ] ;
2018-06-29 03:46:07 +00:00
// Note: update_brush_for_drawing_lines way above
2018-06-27 21:11:36 +00:00
draw _line _without _pattern _support (
op _ctx _2d ,
2019-09-21 15:59:30 +00:00
point _a . x - op _canvas _x ,
point _a . y - op _canvas _y ,
point _b . x - op _canvas _x ,
point _b . y - op _canvas _y ,
2018-06-17 05:27:14 +00:00
stroke _size
2018-06-27 21:11:36 +00:00
) ;
2018-06-17 05:27:14 +00:00
}
2018-06-17 15:41:52 +00:00
2019-10-11 00:24:19 +00:00
replace _colors _with _swatch ( op _ctx _2d , stroke _color , op _canvas _x , op _canvas _y ) ;
ctx . drawImage ( op _canvas _2d , op _canvas _x , op _canvas _y ) ;
2018-06-17 05:27:14 +00:00
} else {
2019-09-21 15:59:30 +00:00
let numVertices = initArrayBuffer ( coords ) ;
2018-06-17 16:36:16 +00:00
gl . clear ( gl . COLOR _BUFFER _BIT ) ;
2018-06-17 16:47:20 +00:00
gl . drawArrays ( close _path ? gl . LINE _LOOP : gl . LINE _STRIP , 0 , numVertices ) ;
2018-06-16 21:49:00 +00:00
2018-06-27 21:11:36 +00:00
op _canvas _2d . width = op _canvas _webgl . width ;
op _canvas _2d . height = op _canvas _webgl . height ;
2018-06-17 16:47:20 +00:00
2018-06-27 21:11:36 +00:00
op _ctx _2d . drawImage ( op _canvas _webgl , 0 , 0 ) ;
replace _colors _with _swatch ( op _ctx _2d , stroke _color , x _min , y _min ) ;
ctx . drawImage ( op _canvas _2d , x _min , y _min ) ;
2018-06-17 05:27:14 +00:00
}
}
2019-09-21 15:22:35 +00:00
}
2018-06-16 21:49:00 +00:00
2019-10-29 18:46:29 +00:00
window . copy _contents _within _polygon = ( canvas , points , x _min , y _min , x _max , y _max ) => {
2018-06-18 19:28:27 +00:00
// Copy the contents of the given canvas within the polygon given by points bounded by x/y_min/max
2018-06-18 19:53:59 +00:00
x _max = Math . max ( x _max , x _min + 1 ) ;
y _max = Math . max ( y _max , y _min + 1 ) ;
2019-10-29 20:29:38 +00:00
const width = x _max - x _min ;
const height = y _max - y _min ;
2018-06-18 19:28:27 +00:00
2020-01-05 22:27:51 +00:00
// @TODO: maybe have the cutout only the width/height of the bounds
2019-11-03 15:36:34 +00:00
// const cutout = make_canvas(width, height);
2019-10-29 21:43:46 +00:00
const cutout = make _canvas ( canvas ) ;
2018-06-18 19:28:27 +00:00
cutout . ctx . save ( ) ;
cutout . ctx . globalCompositeOperation = "destination-in" ;
draw _polygon ( cutout . ctx , points , false , true ) ;
cutout . ctx . restore ( ) ;
2019-10-29 21:43:46 +00:00
const cutout _crop = make _canvas ( width , height ) ;
2018-06-18 19:28:27 +00:00
cutout _crop . ctx . drawImage ( cutout , x _min , y _min , width , height , 0 , 0 , width , height ) ;
return cutout _crop ;
}
2020-01-05 22:27:51 +00:00
// @TODO: maybe shouldn't be external...
2019-10-29 18:46:29 +00:00
window . draw _with _swatch = ( ctx , x _min , y _min , x _max , y _max , swatch , callback ) => {
2019-10-29 20:29:38 +00:00
const stroke _margin = ~ ~ ( stroke _size * 1.1 ) ;
2018-06-27 21:11:36 +00:00
x _max = Math . max ( x _max , x _min + 1 ) ;
y _max = Math . max ( y _max , y _min + 1 ) ;
op _canvas _2d . width = x _max - x _min + stroke _margin * 2 ;
op _canvas _2d . height = y _max - y _min + stroke _margin * 2 ;
2019-10-29 20:29:38 +00:00
const x = x _min - stroke _margin ;
const y = y _min - stroke _margin ;
2018-06-27 21:11:36 +00:00
op _ctx _2d . save ( ) ;
op _ctx _2d . translate ( - x , - y ) ;
callback ( op _ctx _2d ) ;
op _ctx _2d . restore ( ) ; // for replace_colors_with_swatch!
replace _colors _with _swatch ( op _ctx _2d , swatch , x , y ) ;
ctx . drawImage ( op _canvas _2d , x , y ) ;
// for debug:
// ctx.fillStyle = "rgba(255, 0, 255, 0.1)";
// ctx.fillRect(x, y, op_canvas_2d.width, op_canvas_2d.height);
}
2018-06-16 21:49:00 +00:00
} ) ( ) ;