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
// TODO: does it actually still matter? the ellipse drawing code has changed
// 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 ;
}
2018-07-27 18:04:40 +00:00
var mid _x = Math . round ( ctx . canvas . width / 2 ) ;
var left = Math . round ( mid _x - size / 2 ) ;
var right = Math . round ( mid _x + size / 2 ) ;
var mid _y = Math . round ( ctx . canvas . height / 2 ) ;
var top = Math . round ( mid _y - size / 2 ) ;
var bottom = Math . round ( mid _y + size / 2 ) ;
2014-12-11 21:36:48 +00:00
if ( shape === "circle" ) {
2018-06-29 00:18:47 +00:00
// TODO: ideally _without_pattern_support
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 ) {
2018-06-29 01:00:51 +00:00
var center _x = x + w / 2 ;
var center _y = y + h / 2 ;
2014-05-04 13:32:02 +00:00
if ( aliasing ) {
2018-06-29 00:18:47 +00:00
var points = [ ] ;
var step = 0.05 ;
for ( var theta = 0 ; theta < TAU ; theta += step ) {
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 {
2014-10-29 02:54:55 +00:00
if ( w < 0 ) { x += w ; w = - w ; }
if ( h < 0 ) { y += h ; h = - h ; }
2014-05-04 13:32:02 +00:00
ctx . beginPath ( ) ;
2018-06-29 01:00:51 +00:00
ctx . ellipse ( center _x , center _y , w / 2 , 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 ) {
2018-06-28 23:14:32 +00:00
var points = [ ] ;
var lineTo = ( x , y ) => {
points . push ( { x , y } ) ;
} ;
2018-06-29 05:01:55 +00:00
var arc = ( x , y , radius _x , radius _y , startAngle , endAngle ) => {
2018-06-28 23:14:32 +00:00
var step = 0.05 ;
for ( var theta = startAngle ; theta < endAngle ; theta += step ) {
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
var x2 = x + width ;
var 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
2018-06-18 04:52:33 +00:00
var line _brush _canvas ;
2018-06-18 05:12:08 +00:00
var line _brush _canvas _rendered _shape ;
var line _brush _canvas _rendered _color ;
var line _brush _canvas _rendered _size ;
2018-06-18 04:52:33 +00:00
function update _brush _for _drawing _lines ( stroke _size ) {
2018-06-29 03:46:07 +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
if ( aliasing && stroke _size > 1 ) {
2018-06-18 05:12:08 +00:00
// TODO: DRY brush caching code
if (
line _brush _canvas _rendered _shape !== "circle" ||
line _brush _canvas _rendered _color !== stroke _color ||
line _brush _canvas _rendered _size !== stroke _size
) {
// don't need to do brush_ctx.disable_image_smoothing() currently because images aren't drawn to the brush
2018-07-27 18:04:40 +00:00
var csz = get _brush _canvas _size ( stroke _size , "circle" ) ;
2018-06-18 05:12:08 +00:00
line _brush _canvas = new Canvas ( csz , csz ) ;
line _brush _canvas . width = csz ;
line _brush _canvas . height = csz ;
line _brush _canvas . ctx . fillStyle = line _brush _canvas . ctx . strokeStyle = stroke _color ;
render _brush ( line _brush _canvas . ctx , "circle" , stroke _size ) ;
line _brush _canvas _rendered _shape = "circle" ;
line _brush _canvas _rendered _color = stroke _color ;
line _brush _canvas _rendered _size = stroke _size ;
}
2018-06-18 04:52:33 +00:00
}
}
2018-06-27 21:11:36 +00:00
function draw _line _without _pattern _support ( ctx , x1 , y1 , x2 , y2 , stroke _size ) {
2016-11-06 00:13:54 +00:00
stroke _size = 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 ) {
bresenham _line ( x1 , y1 , x2 , y2 , function ( 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 {
bresenham _line ( x1 , y1 , x2 , y2 , function ( x , y ) {
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
x1 = ~ ~ x1 , x2 = ~ ~ x2 , y1 = ~ ~ y1 , y2 = ~ ~ y2 ;
var dx = Math . abs ( x2 - x1 ) ;
var dy = Math . abs ( y2 - y1 ) ;
var sx = ( x1 < x2 ) ? 1 : - 1 ;
var sy = ( y1 < y2 ) ? 1 : - 1 ;
var err = dx - dy ;
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 ;
var e2 = err * 2 ;
if ( e2 > - dy ) { err -= dy ; x1 += sx ; }
if ( e2 < dx ) { err += dx ; y1 += sy ; }
}
}
2014-10-29 02:54:55 +00:00
2014-08-11 20:45:55 +00:00
function brosandham _line ( x1 , y1 , x2 , y2 , callback ) {
2016-11-05 19:52:44 +00:00
// Bresenham's line argorithm with a callback between going horizontal and vertical
2014-05-15 17:14:23 +00:00
x1 = ~ ~ x1 , x2 = ~ ~ x2 , y1 = ~ ~ y1 , y2 = ~ ~ y2 ;
var dx = Math . abs ( x2 - x1 ) ;
var dy = Math . abs ( y2 - y1 ) ;
var sx = ( x1 < x2 ) ? 1 : - 1 ;
var sy = ( y1 < y2 ) ? 1 : - 1 ;
var err = dx - dy ;
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 ;
var e2 = err * 2 ;
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-09-21 15:59:30 +00:00
function draw _fill ( ctx , start _x , start _y , fill _r , fill _g , fill _b , fill _a ) {
2014-10-01 19:46:35 +00:00
2018-01-18 03:18:52 +00:00
// TODO: split up processing in case it takes too long?
// progress bar and abort button (outside of image-manipulation.js)
// or at least just free up the main thread every once in a while
// TODO: speed up with typed arrays? https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipulation-with-typed-arrays/
// 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)
2019-09-21 15:59:30 +00:00
var stack = [ [ start _x , start _y ] ] ;
2014-10-01 19:46:35 +00:00
var c _width = canvas . width ;
var c _height = canvas . height ;
var id = ctx . getImageData ( 0 , 0 , c _width , c _height ) ;
2019-09-24 20:09:13 +00:00
pixel _pos = ( start _y * c _width + start _x ) * 4 ;
2014-10-01 19:46:35 +00:00
var start _r = id . data [ pixel _pos + 0 ] ;
var start _g = id . data [ pixel _pos + 1 ] ;
var start _b = id . data [ pixel _pos + 2 ] ;
var start _a = id . data [ pixel _pos + 3 ] ;
if (
fill _r === start _r &&
fill _g === start _g &&
fill _b === start _b &&
fill _a === start _a
) {
return ;
}
while ( stack . length ) {
var new _pos , x , y , pixel _pos , reach _left , reach _right ;
new _pos = stack . pop ( ) ;
x = new _pos [ 0 ] ;
y = new _pos [ 1 ] ;
pixel _pos = ( y * c _width + x ) * 4 ;
2017-05-25 03:07:10 +00:00
while ( matches _start _color ( pixel _pos ) ) {
y -- ;
pixel _pos = ( y * c _width + x ) * 4 ;
2014-10-01 19:46:35 +00:00
}
reach _left = false ;
reach _right = false ;
2019-09-21 15:33:01 +00:00
// eslint-disable-next-line no-constant-condition
2017-05-25 03:07:10 +00:00
while ( true ) {
y ++ ;
pixel _pos = ( y * c _width + x ) * 4 ;
if ( ! ( y < c _height && matches _start _color ( pixel _pos ) ) ) {
break ;
}
2014-10-01 19:46:35 +00:00
color _pixel ( pixel _pos ) ;
if ( x > 0 ) {
2017-05-25 03:07:10 +00:00
if ( matches _start _color ( 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 ) {
2017-05-25 03:07:10 +00:00
if ( matches _start _color ( 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 ;
}
}
ctx . putImageData ( id , 0 , 0 ) ;
2017-05-25 03:07:10 +00:00
function matches _start _color ( pixel _pos ) {
2014-10-01 19:46:35 +00:00
return (
id . data [ pixel _pos + 0 ] === start _r &&
id . data [ pixel _pos + 1 ] === start _g &&
id . data [ pixel _pos + 2 ] === start _b &&
id . data [ pixel _pos + 3 ] === start _a
) ;
}
2018-01-25 03:51:12 +00:00
function color _pixel ( pixel _pos ) {
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 ;
}
}
function draw _noncontiguous _fill ( ctx , x , y , fill _r , fill _g , fill _b , fill _a ) {
var c _width = canvas . width ;
var c _height = canvas . height ;
var id = ctx . getImageData ( 0 , 0 , c _width , c _height ) ;
pixel _pos = ( y * c _width + x ) * 4 ;
var start _r = id . data [ pixel _pos + 0 ] ;
var start _g = id . data [ pixel _pos + 1 ] ;
var start _b = id . data [ pixel _pos + 2 ] ;
var start _a = id . data [ pixel _pos + 3 ] ;
if (
fill _r === start _r &&
fill _g === start _g &&
fill _b === start _b &&
fill _a === start _a
) {
return ;
}
for ( var i = 0 ; i < id . data . length ; i += 4 ) {
if ( matches _start _color ( i ) ) {
color _pixel ( i ) ;
}
}
ctx . putImageData ( id , 0 , 0 ) ;
function matches _start _color ( pixel _pos ) {
return (
id . data [ pixel _pos + 0 ] === start _r &&
id . data [ pixel _pos + 1 ] === start _g &&
id . data [ pixel _pos + 2 ] === start _b &&
id . data [ pixel _pos + 3 ] === start _a
) ;
}
2014-10-01 19:46:35 +00:00
function color _pixel ( pixel _pos ) {
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 ;
}
}
2014-10-02 21:17:43 +00:00
function apply _image _transformation ( fn ) {
// Apply an image transformation function to either the selection or the entire canvas
2018-01-18 01:13:02 +00:00
var original _canvas = selection ? selection . source _canvas : canvas ;
2014-10-02 21:17:43 +00:00
2018-01-18 01:13:02 +00:00
var new _canvas = new Canvas ( original _canvas . width , original _canvas . height ) ;
2014-10-02 21:17:43 +00:00
var original _ctx = original _canvas . getContext ( "2d" ) ;
2018-01-18 01:13:02 +00:00
var new _ctx = new _canvas . getContext ( "2d" ) ;
2014-10-02 21:17:43 +00:00
fn ( original _canvas , original _ctx , new _canvas , new _ctx ) ;
if ( selection ) {
2018-01-18 01:13:02 +00:00
selection . replace _source _canvas ( new _canvas ) ;
2014-10-02 21:17:43 +00:00
} else {
undoable ( 0 , function ( ) {
this _ones _a _frame _changer ( ) ;
2014-12-07 22:19:56 +00:00
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 ( ) {
apply _image _transformation ( function ( original _canvas , original _ctx , new _canvas , new _ctx ) {
new _ctx . translate ( new _canvas . width , 0 ) ;
new _ctx . scale ( - 1 , 1 ) ;
new _ctx . drawImage ( original _canvas , 0 , 0 ) ;
} ) ;
}
function flip _vertical ( ) {
apply _image _transformation ( function ( original _canvas , original _ctx , new _canvas , new _ctx ) {
new _ctx . translate ( 0 , new _canvas . height ) ;
new _ctx . scale ( 1 , - 1 ) ;
new _ctx . drawImage ( original _canvas , 0 , 0 ) ;
} ) ;
}
function rotate ( angle ) {
apply _image _transformation ( function ( 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 ;
default :
2014-12-08 13:56:20 +00:00
var w = original _canvas . width ;
var h = original _canvas . height ;
var bb _min _x = + Infinity ;
var bb _max _x = - Infinity ;
var bb _min _y = + Infinity ;
var bb _max _y = - Infinity ;
var corner = function ( x01 , y01 ) {
var x = Math . sin ( - angle ) * h * x01 + Math . cos ( + angle ) * w * y01 ;
var y = Math . sin ( + angle ) * w * y01 + Math . cos ( - angle ) * h * x01 ;
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 ) ;
var bb _x = bb _min _x ;
var bb _y = bb _min _y ;
var bb _w = bb _max _x - bb _min _x ;
var bb _h = bb _max _y - bb _min _y ;
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 ) {
2015-02-24 00:18:07 +00:00
new _ctx . fillStyle = 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 ;
}
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
} ) ;
}
2014-11-29 19:20:59 +00:00
function stretch _and _skew ( xscale , yscale , hsa , vsa ) {
apply _image _transformation ( function ( original _canvas , original _ctx , new _canvas , new _ctx ) {
var w = original _canvas . width * xscale ;
var h = original _canvas . height * yscale ;
var bb _min _x = + Infinity ;
var bb _max _x = - Infinity ;
var bb _min _y = + Infinity ;
var bb _max _y = - Infinity ;
var corner = function ( x01 , y01 ) {
var x = Math . tan ( hsa ) * h * x01 + w * y01 ;
var y = Math . tan ( vsa ) * w * y01 + h * x01 ;
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 ) ;
var bb _x = bb _min _x ;
var bb _y = bb _min _y ;
var bb _w = bb _max _x - bb _min _x ;
var bb _h = bb _max _y - bb _min _y ;
new _canvas . width = bb _w ;
new _canvas . height = 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 ) {
2015-02-24 00:18:07 +00:00
new _ctx . fillStyle = 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
Math . tan ( vsa ) , // vertical skew (skewY)
Math . tan ( hsa ) , // horizontal skew (skewX)
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 ( ) ;
} ) ;
}
2018-06-17 23:01:04 +00:00
function replace _colors _with _swatch ( ctx , swatch , x _offset _from _global _canvas , y _offset _from _global _canvas ) {
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 ) {
var mt = 1 - t ;
var mt2 = mt * mt ;
var t2 = t * t ;
var 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 ) {
2018-06-18 04:05:44 +00:00
var steps = 100 ;
var point _a = { x : start _x , y : start _y } ;
for ( var t = 0 ; t < 1 ; t += 1 / steps ) {
var 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 ) ;
2018-06-27 21:11:36 +00:00
// TODO: carry "error" from Bresenham line algorithm between iterations? and/or get a proper Bezier drawing algorithm
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
var min _x = Math . min ( start _x , control _1 _x , control _2 _x , end _x ) ;
var min _y = Math . min ( start _y , control _1 _y , control _2 _y , end _y ) ;
var max _x = Math . max ( start _x , control _1 _x , control _2 _x , end _x ) ;
var max _y = Math . max ( start _y , control _1 _y , control _2 _y , end _y ) ;
draw _with _swatch ( ctx , min _x , min _y , max _x , max _y , stroke _color , function ( op _ctx _2d ) {
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 ) {
var min _x = Math . min ( x1 , x2 ) ;
var min _y = Math . min ( y1 , y2 ) ;
var max _x = Math . max ( x1 , x2 ) ;
var max _y = Math . max ( y1 , y2 ) ;
draw _with _swatch ( ctx , min _x , min _y , max _x , max _y , stroke _color , function ( op _ctx _2d ) {
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-09-30 14:40:47 +00:00
var grid _pattern ;
2019-09-30 22:43:28 +00:00
function draw _grid ( ctx , wanted _size ) {
if ( ! grid _pattern || grid _pattern . width !== wanted _size || grid _pattern . height !== wanted _size ) {
var grid _pattern _canvas = new Canvas ( wanted _size , wanted _size ) ;
2019-09-30 14:40:47 +00:00
var dark _gray = "#808080" ;
var light _gray = "#c0c0c0" ;
grid _pattern _canvas . ctx . fillStyle = dark _gray ;
2019-09-30 22:43:28 +00:00
grid _pattern _canvas . ctx . fillRect ( 0 , 0 , 1 , wanted _size ) ;
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillStyle = dark _gray ;
2019-09-30 22:43:28 +00:00
grid _pattern _canvas . ctx . fillRect ( 0 , 0 , wanted _size , 1 ) ;
2019-09-30 14:40:47 +00:00
grid _pattern _canvas . ctx . fillStyle = light _gray ;
2019-09-30 22:43:28 +00:00
for ( let i = 1 ; i < wanted _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" ) ;
}
ctx . fillStyle = grid _pattern ;
ctx . fillRect ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
}
2018-06-16 21:49:00 +00:00
( function ( ) {
var tessy = ( function initTesselator ( ) {
// function called for each vertex of tesselator output
function vertexCallback ( data , polyVertArray ) {
// console.log(data[0], data[1]);
polyVertArray [ polyVertArray . length ] = data [ 0 ] ;
polyVertArray [ polyVertArray . length ] = data [ 1 ] ;
}
function begincallback ( type ) {
if ( type !== libtess . primitiveType . GL _TRIANGLES ) {
console . log ( 'expected TRIANGLES but got type: ' + type ) ;
}
}
function errorcallback ( errno ) {
console . log ( 'error callback' ) ;
console . log ( 'error number: ' + errno ) ;
}
// callback for when segments intersect and must be split
function combinecallback ( coords , data , weight ) {
// console.log('combine callback');
return [ coords [ 0 ] , coords [ 1 ] , coords [ 2 ] ] ;
}
function edgeCallback ( flag ) {
// don't really care about the flag, but need no-strip/no-fan behavior
// console.log('edge flag: ' + flag);
}
var tessy = new libtess . GluTesselator ( ) ;
// tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE);
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _VERTEX _DATA , vertexCallback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _BEGIN , begincallback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _ERROR , errorcallback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _COMBINE , combinecallback ) ;
tessy . gluTessCallback ( libtess . gluEnum . GLU _TESS _EDGE _FLAG , edgeCallback ) ;
return tessy ;
} ) ( ) ;
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 ) ;
var triangleVerts = [ ] ;
tessy . gluTessBeginPolygon ( triangleVerts ) ;
for ( var i = 0 ; i < contours . length ; i ++ ) {
tessy . gluTessBeginContour ( ) ;
var contour = contours [ i ] ;
for ( var j = 0 ; j < contour . length ; j += 2 ) {
var coords = [ contour [ j ] , contour [ j + 1 ] , 0 ] ;
tessy . gluTessVertex ( coords , coords ) ;
}
tessy . gluTessEndContour ( ) ;
}
tessy . gluTessEndPolygon ( ) ;
return triangleVerts ;
}
var gl ;
var positionLoc ;
function initWebGL ( canvas ) {
gl = canvas . getContext ( 'webgl' , { antialias : false } ) ;
2018-06-16 23:57:13 +00:00
var program = createShaderProgram ( ) ;
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
2018-06-16 23:57:13 +00:00
var rawData = new Float32Array ( triangleVertexCoords ) ;
var 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
var vertexSrc = [
'attribute vec4 position;' ,
'void main() {' ,
' /* already in normalized coordinates, so just pass through */' ,
' gl_Position = position;' ,
'}'
] . join ( '' ) ;
var vertexShader = gl . createShader ( gl . VERTEX _SHADER ) ;
gl . shaderSource ( vertexShader , vertexSrc ) ;
gl . compileShader ( vertexShader ) ;
if ( ! gl . getShaderParameter ( vertexShader , gl . COMPILE _STATUS ) ) {
console . log (
'Vertex shader failed to compile. Log: ' +
gl . getShaderInfoLog ( vertexShader )
) ;
}
// create fragment shader
var fragmentSrc = [
'precision mediump float;' ,
'void main() {' ,
2018-06-17 16:45:47 +00:00
' gl_FragColor = vec4(0, 0, 0, 1);' ,
2018-06-16 21:49:00 +00:00
'}'
] . join ( '' ) ;
var fragmentShader = gl . createShader ( gl . FRAGMENT _SHADER ) ;
gl . shaderSource ( fragmentShader , fragmentSrc ) ;
gl . compileShader ( fragmentShader ) ;
if ( ! gl . getShaderParameter ( fragmentShader , gl . COMPILE _STATUS ) ) {
console . log (
'Fragment shader failed to compile. Log: ' +
gl . getShaderInfoLog ( fragmentShader )
) ;
}
// link shaders to create our program
var program = gl . createProgram ( ) ;
gl . attachShader ( program , vertexShader ) ;
gl . attachShader ( program , fragmentShader ) ;
gl . linkProgram ( program ) ;
gl . useProgram ( program ) ;
return program ;
}
2018-06-27 21:11:36 +00:00
var op _canvas _webgl = document . createElement ( 'canvas' ) ;
var op _canvas _2d = document . createElement ( 'canvas' ) ;
var 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 ) ;
2018-06-16 21:49:00 +00:00
2018-06-16 23:18:05 +00:00
window . draw _line _strip = function ( 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
} ;
2018-06-16 21:49:00 +00:00
window . draw _polygon = function ( 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 ) ;
}
2018-06-16 21:49:00 +00:00
var stroke _color = ctx . strokeStyle ;
var fill _color = ctx . fillStyle ;
var numPoints = points . length ;
var numCoords = numPoints * 2
2018-06-18 19:53:59 +00:00
if ( numPoints === 0 ) {
return ;
}
2018-06-16 21:49:00 +00:00
var x _min = + Infinity ;
var x _max = - Infinity ;
var y _min = + Infinity ;
var y _max = - Infinity ;
2018-06-17 00:05:49 +00:00
for ( var i = 0 ; i < numPoints ; i ++ ) {
2018-06-16 21:49:00 +00:00
var { x , y } = points [ i ] ;
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
var 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 ;
2018-06-16 21:49:00 +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 ) {
2018-06-16 23:57:13 +00:00
var contours = [ coords ] ;
var 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 ) {
2018-06-27 21:11:36 +00:00
var stroke _margin = ~ ~ ( stroke _size * 1.1 ) ;
2018-06-17 16:47:20 +00:00
2019-09-21 15:59:30 +00:00
var op _canvas _x = x _min - stroke _margin ;
var 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 ++ ) {
2018-06-17 05:27:14 +00:00
var point _a = points [ i ] ;
var 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
2018-06-18 19:48:21 +00:00
window . copy _contents _within _polygon = function ( 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 ) ;
2018-06-18 19:28:27 +00:00
var width = x _max - x _min ;
var height = y _max - y _min ;
// TODO: maybe have the cutout only the width/height of the bounds
// var cutout = new Canvas(width, height);
2018-06-18 19:48:21 +00:00
var cutout = new 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 ( ) ;
var cutout _crop = new Canvas ( width , height ) ;
cutout _crop . ctx . drawImage ( cutout , x _min , y _min , width , height , 0 , 0 , width , height ) ;
return cutout _crop ;
}
2018-06-27 21:11:36 +00:00
// TODO: maybe shouldn't be external...
window . draw _with _swatch = function ( ctx , x _min , y _min , x _max , y _max , swatch , callback ) {
var stroke _margin = ~ ~ ( stroke _size * 1.1 ) ;
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 ;
var x = x _min - stroke _margin ;
var y = y _min - stroke _margin ;
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
} ) ( ) ;