function round _to _pow2 ( value , multiple ) {
return ( value + multiple - 1 ) & - multiple ;
}
function screen _to _canvas ( state , p ) {
// should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio
const xc = ( p . x - state . canvas . offset . x ) / state . canvas . zoom ;
const yc = ( p . y - state . canvas . offset . y ) / state . canvas . zoom ;
return { 'x' : xc , 'y' : yc } ;
}
function canvas _to _screen ( state , p ) {
const xs = ( p . x * state . canvas . zoom + state . canvas . offset . x ) / window . devicePixelRatio ;
const ys = ( p . y * state . canvas . zoom + state . canvas . offset . y ) / window . devicePixelRatio ;
return { 'x' : xs , 'y' : ys } ;
}
/ *
function rdp _find _max ( state , zoom , stroke , start , end ) {
// Finds a point from the range [start, end) with the maximum distance from the line (start--end) that is also further than EPS
const EPS = 1.0 / zoom ;
let result = - 1 ;
let max _dist = 0 ;
const ax = state . coordinates . data [ stroke . coords _from + start * 2 + 0 ] ;
const ay = state . coordinates . data [ stroke . coords _from + start * 2 + 1 ] ;
const bx = state . coordinates . data [ stroke . coords _from + end * 2 + 0 ] ;
const by = state . coordinates . data [ stroke . coords _from + end * 2 + 1 ] ;
const dx = bx - ax ;
const dy = by - ay ;
const dist _ab = Math . sqrt ( dx * dx + dy * dy ) ;
const dir _nx = dy / dist _ab ;
const dir _ny = - dx / dist _ab ;
for ( let i = start + 1 ; i < end ; ++ i ) {
const px = state . coordinates . data [ stroke . coords _from + i * 2 + 0 ] ;
const py = state . coordinates . data [ stroke . coords _from + i * 2 + 1 ] ;
const apx = px - ax ;
const apy = py - ay ;
const dist = Math . abs ( apx * dir _nx + apy * dir _ny ) ;
if ( dist > EPS && dist > max _dist ) {
result = i ;
max _dist = dist ;
}
}
state . stats . rdp _max _count ++ ;
state . stats . rdp _segments += end - start - 1 ;
return result ;
}
* /
function process _rdp _indices _r ( state , zoom , mask , stroke , start , end ) {
// Looks like the recursive implementation spends most of its time in the function call overhead
// Let's try to use an explicit stack instead to give the js engine more room to play with
// Update: it's not faster. But it gives more sensible source-line samples in chrome profiler, so I'll leave it
let result = 0 ;
const stack = [ ] ;
stack . push ( { 'start' : start , 'end' : end } ) ;
while ( stack . length > 0 ) {
const region = stack . pop ( ) ;
const max = rdp _find _max ( state , zoom , stroke , region . start , region . end ) ;
if ( max !== - 1 ) {
mask [ max ] = 1 ;
result += 1 ;
stack . push ( { 'start' : region . start , 'end' : max } ) ;
stack . push ( { 'start' : max , 'end' : region . end } ) ;
}
}
return result ;
}
function process _rdp _indices ( state , zoom , stroke ) {
const point _count = ( stroke . coords _to - stroke . coords _from ) / 2 ;
if ( state . rdp _mask . length < point _count ) {
state . rdp _mask = new Uint8Array ( point _count ) ;
}
state . rdp _mask . fill ( 0 , 0 , point _count ) ;
const mask = state . rdp _mask ;
const npoints = 2 + process _rdp _indices _r ( state , zoom , mask , stroke , 0 , point _count - 1 ) ; // 2 is for the first and last vertex, which do not get included by the recursive functions, but should always be there at any lod level
mask [ 0 ] = 1 ;
mask [ point _count - 1 ] = 1 ;
return npoints ;
}
function process _ewmv ( points , round = false ) {
const result = [ ] ;
const alpha = 0.5 ;
result . push ( points [ 0 ] ) ;
for ( let i = 1 ; i < points . length ; ++ i ) {
const p = points [ i ] ;
const x = Math . round ( alpha * p . x + ( 1 - alpha ) * result [ result . length - 1 ] . x ) ;
const y = Math . round ( alpha * p . y + ( 1 - alpha ) * result [ result . length - 1 ] . y ) ;
result . push ( { 'x' : x , 'y' : y } ) ;
}
return result ;
}
function process _stroke ( state , zoom , stroke ) {
// Try caching the highest zoom level that only returns the endpoints
if ( zoom <= stroke . turns _into _straight _line _zoom ) {
return 2 ;
}
const npoints = process _rdp _indices ( state , zoom , stroke , true ) ;
if ( npoints === 2 && zoom > stroke . turns _into _straight _line _zoom ) {
stroke . turns _into _straight _line _zoom = zoom ;
}
return npoints ;
}
function rdp _find _max2 ( points , start , end ) {
const EPS = 0.25 ;
let result = - 1 ;
let max _dist = 0 ;
const a = points [ start ] ;
const b = points [ end ] ;
const dx = b . x - a . x ;
const dy = b . y - a . y ;
const dist _ab = Math . sqrt ( dx * dx + dy * dy ) ;
const sin _theta = dy / dist _ab ;
const cos _theta = dx / dist _ab ;
for ( let i = start ; i < end ; ++ i ) {
const p = points [ i ] ;
const ox = p . x - a . x ;
const oy = p . y - a . y ;
const rx = cos _theta * ox + sin _theta * oy ;
const ry = - sin _theta * ox + cos _theta * oy ;
const x = rx + a . x ;
const y = ry + a . y ;
const dist = Math . abs ( y - a . y ) ;
if ( dist > EPS && dist > max _dist ) {
result = i ;
max _dist = dist ;
}
}
return result ;
}
function process _rdp _r2 ( points , start , end ) {
let result = [ ] ;
const max = rdp _find _max2 ( points , start , end ) ;
if ( max !== - 1 ) {
const before = process _rdp _r2 ( points , start , max ) ;
const after = process _rdp _r2 ( points , max , end ) ;
result = [ ... before , points [ max ] , ... after ] ;
}
return result ;
}
function process _rdp2 ( points ) {
const result = process _rdp _r2 ( points , 0 , points . length - 1 ) ;
result . unshift ( points [ 0 ] ) ;
result . push ( points [ points . length - 1 ] ) ;
return result ;
}
// TODO: unify with regular process stroke
function process _stroke2 ( points ) {
const result = process _rdp2 ( points ) ;
return result ;
}
function strokes _intersect _line ( state , a , b ) {
// TODO: handle stroke / eraser width
const result = [ ] ;
for ( let i = 0 ; i < state . events . length ; ++ i ) {
const event = state . events [ i ] ;
if ( event . type === EVENT . STROKE && ! event . deleted ) {
for ( let i = 0 ; i < event . points . length - 1 ; ++ i ) {
const c = event . points [ i + 0 ] ;
const d = event . points [ i + 1 ] ;
if ( segments _intersect ( a , b , c , d ) ) {
result . push ( i ) ;
break ;
}
}
}
}
return result ;
}
function color _to _u32 ( color _str ) {
const r = parseInt ( color _str . substring ( 0 , 2 ) , 16 ) ;
const g = parseInt ( color _str . substring ( 2 , 4 ) , 16 ) ;
const b = parseInt ( color _str . substring ( 4 , 6 ) , 16 ) ;
return ( r << 16 ) | ( g << 8 ) | b ;
}
function color _from _u32 ( color _u32 ) {
const r = ( color _u32 >> 16 ) & 0xFF ;
const g = ( color _u32 >> 8 ) & 0xFF ;
const b = color _u32 & 0xFF ;
let r _str = r . toString ( 16 ) ;
let g _str = g . toString ( 16 ) ;
let b _str = b . toString ( 16 ) ;
if ( r <= 0xF ) r _str = '0' + r _str ;
if ( g <= 0xF ) g _str = '0' + g _str ;
if ( b <= 0xF ) b _str = '0' + b _str ;
return '#' + r _str + g _str + b _str ;
}
function ccw ( A , B , C ) {
return ( C . y - A . y ) * ( B . x - A . x ) > ( B . y - A . y ) * ( C . x - A . x ) ;
}
// https://stackoverflow.com/a/9997374/11420590
function segments _intersect ( A , B , C , D ) {
return ccw ( A , C , D ) != ccw ( B , C , D ) && ccw ( A , B , C ) !== ccw ( A , B , D ) ;
}
function dist _v2 ( a , b ) {
const dx = a . x - b . x ;
const dy = a . y - b . y ;
return Math . sqrt ( dx * dx + dy * dy ) ;
}
function mid _v2 ( a , b ) {
return {
'x' : ( a . x + b . x ) / 2.0 ,
'y' : ( a . y + b . y ) / 2.0 ,
} ;
}
function point _in _quad ( p , quad _topleft , quad _bottomright ) {
if ( ( quad _topleft . x <= p . x && p . x < quad _bottomright . x ) && ( quad _topleft . y <= p . y && p . y < quad _bottomright . y ) ) {
return true ;
}
return false ;
}
function segment _interesects _quad ( a , b , quad _topleft , quad _bottomright , quad _topright , quad _bottomleft ) {
if ( point _in _quad ( a , quad _topleft , quad _bottomright ) ) {
return true ;
}
if ( point _in _quad ( b , quad _topleft , quad _bottomright ) ) {
return true ;
}
if ( segments _intersect ( a , b , quad _topleft , quad _topright ) ) return true ;
if ( segments _intersect ( a , b , quad _topright , quad _bottomright ) ) return true ;
if ( segments _intersect ( a , b , quad _bottomright , quad _bottomleft ) ) return true ;
if ( segments _intersect ( a , b , quad _bottomleft , quad _topleft ) ) return true ;
return false ;
}
function stroke _bbox ( state , stroke ) {
const radius = stroke . width / 2 ;
let min _x = state . coordinates . data [ stroke . coords _from + 0 ] - radius ;
let max _x = state . coordinates . data [ stroke . coords _from + 0 ] + radius ;
let min _y = state . coordinates . data [ stroke . coords _from + 1 ] - radius ;
let max _y = state . coordinates . data [ stroke . coords _from + 1 ] + radius ;
for ( let i = stroke . coords _from + 2 ; i < stroke . coords _to ; i += 2 ) {
const px = state . coordinates . data [ i + 0 ] ;
const py = state . coordinates . data [ i + 1 ] ;
min _x = Math . min ( min _x , px - radius ) ;
min _y = Math . min ( min _y , py - radius ) ;
max _x = Math . max ( max _x , px + radius ) ;
max _y = Math . max ( max _y , py + radius ) ;
}
return { 'x1' : min _x , 'y1' : min _y , 'x2' : max _x , 'y2' : max _y , 'cx' : ( max _x + min _x ) / 2 , 'cy' : ( max _y + min _y ) / 2 } ;
}
function quads _intersect ( a , b ) {
if ( a . x1 < b . x2 && a . x2 > b . x1 && a . y2 > b . y1 && a . y1 < b . y2 ) {
return true ;
}
return false ;
}
function quad _fully _onscreen ( screen , bbox ) {
if ( screen . x1 < bbox . x1 && screen . x2 > bbox . x2 && screen . y1 < bbox . y1 && screen . y2 > bbox . y2 ) {
return true ;
}
return false ;
}
function quad _union ( a , b ) {
return {
'x1' : Math . min ( a . x1 , b . x1 ) ,
'y1' : Math . min ( a . y1 , b . y1 ) ,
'x2' : Math . max ( a . x2 , b . x2 ) ,
'y2' : Math . max ( a . y2 , b . y2 ) ,
} ;
}
function box _area ( box ) {
return ( box . x2 - box . x1 ) * ( box . y2 - box . y1 ) ;
}
// https://stackoverflow.com/a/47593316
function mulberry32 ( seed ) {
let t = seed + 0x6D2B79F5 ;
t = Math . imul ( t ^ t >>> 15 , t | 1 ) ;
t ^= t + Math . imul ( t ^ t >>> 7 , t | 61 ) ;
return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296 ;
}
function random _bright _color _from _seed ( seed ) {
const h = Math . round ( mulberry32 ( seed ) * 360 ) ;
const s = 25 ;
const l = 50 ;
return ` hsl( ${ h } deg ${ s } % ${ l } %) ` ;
}