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 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)
const EPS = 1.0 / zoom ;
let cache _key = null ;
if ( end - start > config . rdp _cache _threshold ) {
cache _key = stroke . index + '-' + zoom + '-' + start + '-' + end ;
if ( cache _key in state . rdp _cache ) {
return state . rdp _cache [ cache _key ] ;
}
}
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 ;
if ( end - start > config . rdp _cache _threshold ) {
state . rdp _cache [ cache _key ] = result ;
}
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 result0 = process_ewmv(points);
const result1 = process _rdp _indices ( state , zoom , stroke , true ) ;
if ( result1 === 2 && zoom > stroke . turns _into _straight _line _zoom ) {
stroke . turns _into _straight _line _zoom = zoom ;
}
return result1 ;
}
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 ) ;
}
function segments _onscreen ( state , context , do _clip ) {
// TODO: handle stroke width
if ( state . onscreen _segments === null ) {
let total _points = 0 ;
for ( const event of state . events ) {
if ( event . type === EVENT . STROKE && ! event . deleted && event . points . length > 0 ) {
total _points += event . points . length - 1 ;
}
}
if ( total _points > 0 ) {
state . onscreen _segments = new Uint32Array ( total _points * 6 ) ;
}
}
let at = 0 ;
const screen _topleft = screen _to _canvas ( state , { 'x' : 0 , 'y' : 0 } ) ;
const screen _bottomright = screen _to _canvas ( state , { 'x' : context . canvas . width , 'y' : context . canvas . height } ) ;
/ *
screen _topleft . x += 300 ;
screen _topleft . y += 300 ;
screen _bottomright . x -= 300 ;
screen _bottomright . y -= 300 ;
* /
const screen _topright = { 'x' : screen _bottomright . x , 'y' : screen _topleft . y } ;
const screen _bottomleft = { 'x' : screen _topleft . x , 'y' : screen _bottomright . y } ;
const screen = { 'x1' : screen _topleft . x , 'y1' : screen _topleft . y , 'x2' : screen _bottomright . x , 'y2' : screen _bottomright . y } ;
let head = 0 ;
for ( let i = 0 ; i < state . events . length ; ++ i ) {
if ( state . debug . limit _to && i >= state . debug . render _to ) break ;
const event = state . events [ i ] ;
if ( ! ( state . debug . limit _from && i < state . debug . render _from ) ) {
if ( event . type === EVENT . STROKE && ! event . deleted && event . points . length > 0 ) {
if ( ! do _clip || quads _intersect ( screen , event . bbox ) ) {
for ( let j = 0 ; j < event . points . length - 1 ; ++ j ) {
let base = head + j * 4 ;
// We draw quads as [1, 2, 3, 4, 3, 2]
state . onscreen _segments [ at + 0 ] = base + 0 ;
state . onscreen _segments [ at + 1 ] = base + 1 ;
state . onscreen _segments [ at + 2 ] = base + 2 ;
state . onscreen _segments [ at + 3 ] = base + 3 ;
state . onscreen _segments [ at + 4 ] = base + 2 ;
state . onscreen _segments [ at + 5 ] = base + 1 ;
at += 6 ;
}
}
}
}
head += ( event . points . length - 1 ) * 4 ;
}
return at ;
}