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 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 . coords _from , 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 ;
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 exponential _smoothing ( points , last , up _to ) {
const alpha = 0.5 ;
let pr = 0 ;
let start = points . length - up _to ;
if ( start < 0 ) {
start = 0 ;
}
for ( let i = start ; i < points . length ; ++ i ) {
const p = points [ i ] ;
pr = alpha * p . pressure + ( 1 - alpha ) * pr ;
}
pr = alpha * last . pressure + ( 1 - alpha ) * pr ;
return pr ;
}
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 ( zoom , points , start , end ) {
const EPS = 0.125 / zoom ;
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 + 1 ; 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 ) + Math . abs ( a . pressure - p . pressure ) / 255 + Math . abs ( b . pressure - p . pressure ) / 255 ;
if ( dist > EPS && dist > max _dist ) {
result = i ;
max _dist = dist ;
}
}
return result ;
}
function process _rdp _r2 ( zoom , points , start , end ) {
let result = [ ] ;
const max = rdp _find _max2 ( zoom , points , start , end ) ;
if ( max !== - 1 ) {
const before = process _rdp _r2 ( zoom , points , start , max ) ;
const after = process _rdp _r2 ( zoom , points , max , end ) ;
result = [ ... before , points [ max ] , ... after ] ;
}
return result ;
}
function process _rdp2 ( zoom , points ) {
const result = [ ] ;
const stack = [ ] ;
stack . push ( {
'type' : 0 ,
'start' : 0 ,
'end' : points . length - 1 ,
} ) ;
result . push ( points [ 0 ] ) ;
while ( stack . length > 0 ) {
const entry = stack . pop ( ) ;
if ( entry . type === 0 ) {
const max = rdp _find _max2 ( zoom , points , entry . start , entry . end ) ;
if ( max !== - 1 ) {
stack . push ( {
'type' : 0 ,
'start' : max ,
'end' : entry . end
} ) ;
stack . push ( {
'type' : 1 ,
'index' : max ,
} ) ;
stack . push ( {
'type' : 0 ,
'start' : entry . start ,
'end' : max ,
} ) ;
}
} else {
result . push ( points [ entry . index ] ) ;
}
}
result . push ( points [ points . length - 1 ] ) ;
return result ;
}
// TODO: unify with regular process stroke
function process _stroke2 ( zoom , points ) {
const result = process _rdp2 ( zoom , 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 color _from _rgbdict ( color _dict ) {
const r = Math . floor ( color _dict . r * 255 ) ;
const g = Math . floor ( color _dict . g * 255 ) ;
const b = Math . floor ( color _dict . b * 255 ) ;
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 point _in _bbox ( p , bbox ) {
if ( bbox . x1 <= p . x && p . x < bbox . x2 && bbox . y1 <= p . y && p . y < bbox . y2 ) {
return true ;
}
return false ;
}
function clamp ( v , a , b ) {
return ( v < a ? a : ( v > b ? b : v ) ) ;
}
function dot ( a , b ) {
return a . x * b . x + a . y * b . y ;
}
function mix ( a , b , t ) {
return a * t + b * ( 1 - t ) ;
}
function point _in _stroke ( p , xs , ys , pressures , width ) {
for ( let i = 0 ; i < xs . length - 1 ; ++ i ) {
const ax = xs [ i + 0 ] ;
const bx = xs [ i + 1 ] ;
const ay = ys [ i + 0 ] ;
const by = ys [ i + 1 ] ;
const at = pressures [ i + 0 ] / 255 * width ;
const bt = pressures [ i + 1 ] / 255 * width ;
const pa = {
'x' : p . x - ax ,
'y' : p . y - ay ,
} ;
const ba = {
'x' : bx - ax ,
'y' : by - ay ,
} ;
const h = clamp ( dot ( pa , ba ) / dot ( ba , ba ) , 0.0 , 1.0 ) ;
const thickness = mix ( at , bt , h ) ;
const v = {
'x' : p . x - ( ax + ba . x * h ) ,
'y' : p . y - ( ay + ba . y * h ) ,
} ;
const dist = Math . sqrt ( dot ( v , v ) ) - thickness ;
if ( dist <= 0 ) {
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 ; // do not divide by 2 to account for max possible pressure
const xs = state . wasm . buffers [ 'xs' ] . tv . data ;
const ys = state . wasm . buffers [ 'ys' ] . tv . data ;
let min _x = xs [ stroke . coords _from ] - radius ;
let max _x = xs [ stroke . coords _from ] + radius ;
let min _y = ys [ stroke . coords _from ] - radius ;
let max _y = ys [ stroke . coords _from ] + radius ;
for ( let i = stroke . coords _from + 1 ; i < stroke . coords _to ; ++ i ) {
const px = xs [ i ] ;
const py = ys [ i ] ;
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 _inside ( outer , inner ) {
if ( outer . x1 < inner . x1 && outer . x2 > inner . x2 && outer . y1 < inner . y1 && outer . y2 > inner . 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 } %) ` ;
}