import { Size , Point2D , Vector2D } from "./types/sol" ; import { hsla , ColorSpec } from "./colors" ; import { Traceable , TextConfig , Text , Rect } from "./paths" ; import { RNG } from "./rng" ; export interface Gradientable { gradient ( ctx ) : CanvasGradient ; } /**
 * Utility type if want to expose an API where have access to Solandra non-drawing related things
 *
 * (want to use in thing I'm calling sol-game-r)
 */ export type SCanvasNonDrawing = Pick < SCanvas , "random" | "randomPoint" | "randomPolarity" | "aspectRatio" | "build" | "doProportion" | "downFrom" | "forGrid" | "gaussian" | "meta" | "perturb" | "poisson" | "proportionately" | "range" | "resetRandomNumberGenerator" | "sample" | "samples" | "shuffle" | "times" | "uniformGridPoint" | "uniformRandomInt" | "withRandomOrder" > ; export default class SCanvas { readonly aspectRatio : number ; readonly originalScale : number ; private rng : RNG ; readonly t : number ; constructor ( private ctx , { width , height } , rngSeed ? , time ? ) { ctx . resetTransform ( ) ; this . aspectRatio = width / height ; this . originalScale = width ; ctx . scale ( width , width ) ; ctx . lineWidth = 0.01 ; ctx . lineJoin = "round" ; ctx . strokeStyle = "black" ; ctx . fillStyle = "gray" ; this . lineStyle = { cap : "round" } ; this . rng = new RNG ( rngSeed ) ; this . t = time || 0 ; } /**
     * Allow for re-use of a single SCanvas
     *
     * In examples and most early use just recreated but this is somewhat wasteful
     */ updateTime ( time ) { this . t = time ; } /**
     * Allow for re-use of a single SCanvas
     *
     * In examples and most early use just recreated but this is somewhat wasteful
     */ updateSize ( { width , height } ) { this . ctx . resetTransform ( ) ; this . aspectRatio = width / height ; this . originalScale = width ; this . ctx . scale ( width , width ) ; } resetRandomNumberGenerator ( seed ? ) { this . rng = new RNG ( seed ) ; } get meta ( ) { return { top : 0 , bottom : 1 / this . aspectRatio , right : 1 , left : 0 , aspectRatio : this . aspectRatio , center : [ 0.5 , 0.5 / this . aspectRatio ] as [ number , number ] , } ; } set lineWidth ( width ) { this . ctx . lineWidth = width ; } set lineStyle ( { cap = "round" , join = "round" , } ) { this . ctx . lineCap = cap ; this . ctx . lineJoin = join ; } /**
     * The supplied pattern is at scale of canvas
     */ set dash ( { pattern = [ 0.05 , 0.05 ] , offset = 0 , } ) { this . ctx . setLineDash ( pattern ) ; this . ctx . lineDashOffset = offset ; } clearShadow ( ) { this . ctx . shadowBlur = 0 ; this . ctx . shadowOffsetX = 0 ; this . ctx . shadowOffsetY = 0 ; } /**
     * Shadow scale is at size of Canvas. (This is not how HTML 5 Canvas works.)
     */ set shadow ( { size = 0.01 , color = { h : 0 , s : 0 , l : 0 , a : 0.5 } , dX = 0 , dY = 0.01 , } ) { const { h , s , l , a } = color ; this . ctx . shadowBlur = size * this . originalScale ; this . ctx . shadowColor = hsla ( h , s , l , a ) ; this . ctx . shadowOffsetX = dX * this . originalScale ; this . ctx . shadowOffsetY = dY * this . originalScale ; } background ( h , s , l , a = 1 ) { this . pushState ( ) ; this . ctx . fillStyle = hsla ( h , s , l , a ) ; const { right , bottom } = this . meta ; this . fill ( new Rect ( { at : [ 0 , 0 ] , w : right , h : bottom } ) ) ; this . popState ( ) ; } backgroundFromSpec ( { h , s , l , a } ) { this . background ( h , s , l , a ) ; } backgroundGradient ( gradient ) { this . pushState ( ) ; this . ctx . fillStyle = gradient . gradient ( this . ctx ) ; const { right , bottom } = this . meta ; this . fill ( new Rect ( { at : [ 0 , 0 ] , w : right , h : bottom } ) ) ; this . popState ( ) ; } setStrokeColor ( h , s , l , a = 1 ) { this . ctx . strokeStyle = hsla ( h , s , l , a ) ; } setStrokeColorFromSpec ( { h , s , l , a } ) { this . setStrokeColor ( h , s , l , a ) ; } setFillColor ( h , s , l , a = 1 ) { this . ctx . fillStyle = hsla ( h , s , l , a ) ; } setFillColorFromSpec ( { h , s , l , a } ) { this . setFillColor ( h , s , l , a ) ; } setStrokeGradient ( gradient ) { this . ctx . strokeStyle = gradient . gradient ( this . ctx ) ; } setFillGradient ( gradient ) { this . ctx . fillStyle = gradient . gradient ( this . ctx ) ; } draw ( traceable ) { this . ctx . beginPath ( ) ; traceable . traceIn ( this . ctx ) ; this . ctx . stroke ( ) ; } fill ( traceable ) { this . ctx . beginPath ( ) ; traceable . traceIn ( this . ctx ) ; this . ctx . fill ( ) ; } drawText ( config , text ) { new Text ( { ... config , kind : "stroke" } , text ) . textIn ( this . ctx ) ; } fillText ( config , text ) { new Text ( { ... config , kind : "fill" } , text ) . textIn ( this . ctx ) ; } forMargin = ( margin , callback ) => this . forTiling ( { n : 1 , margin } , callback ) ; forTiling = ( config , callback ) => { let k = 0 ; const { n , type = "proportionate" , margin = 0 , order = "columnFirst" , } = config ; const nY = type === "square" ? Math . floor ( n * ( 1 / this . aspectRatio ) ) : n ; const deltaX = ( 1 - margin * 2 ) / n ; const hY = type === "square" ? deltaX * nY : 1 / this . aspectRatio - 2 * margin ; const deltaY = hY / nY ; const sX = margin ; const sY = ( 1 / this . aspectRatio - hY ) / 2 ; if ( order === "columnFirst" ) { for ( let i = 0 ; i < n ; i ++ ) { for ( let j = 0 ; j < nY ; j ++ ) { callback ( [ sX + i * deltaX , sY + j * deltaY ] , [ deltaX , deltaY ] , [ sX + i * deltaX + deltaX / 2 , sY + j * deltaY + deltaY / 2 ] , k ) ; k ++ ; } } } else { for ( let j = 0 ; j < nY ; j ++ ) { for ( let i = 0 ; i < n ; i ++ ) { callback ( [ sX + i * deltaX , sY + j * deltaY ] , [ deltaX , deltaY ] , [ sX + i * deltaX + deltaX / 2 , sY + j * deltaY + deltaY / 2 ] , k ) ; k ++ ; } } } } ; forHorizontal = ( config , callback ) => { const { n , margin = 0 } = config ; const sX = margin ; const eX = 1 - margin ; const sY = margin ; const dY = 1 / this . aspectRatio - 2 * margin ; const dX = ( eX - sX ) / n ; for ( let i = 0 ; i < n ; i ++ ) { callback ( [ sX + i * dX , sY ] , [ dX , dY ] , [ sX + i * dX + dX / 2 , sY + dY / 2 ] , i ) ; } } ; forVertical = ( config , callback ) => { const { n , margin = 0 } = config ; const sX = margin ; const eY = 1 / this . aspectRatio - margin ; const sY = margin ; const dX = 1 - 2 * margin ; const dY = ( eY - sY ) / n ; for ( let i = 0 ; i < n ; i ++ ) { callback ( [ sX , sY + i * dY ] , [ dX , dY ] , [ sX + dX / 2 , sY + i * dY + dY / 2 ] , i ) ; } } ; forGrid = ( config , callback ) => { let k = 0 ; const { minX , maxX , minY , maxY , order = "columnFirst" } = config ; if ( order === "columnFirst" ) { for ( let i = minX ; i <= maxX ; i ++ ) { for ( let j = minY ; j <= maxY ; j ++ ) { callback ( [ i , j ] , k ) ; k ++ ; } } } else { for ( let j = minY ; j <= maxY ; j ++ ) { for ( let i = minX ; i <= maxX ; i ++ ) { callback ( [ i , j ] , k ) ; k ++ ; } } } } ; build = < C , T extends any [ ] , U > ( iterFn , config , cb ) : U [ ] => { const res = [ ] ; iterFn ( config , ( ... as ) => { res . push ( cb ( ... as ) ) ; } ) ; return res ; } ; withRandomOrder < C , T extends any [ ] > ( iterFn , config , cb ) { const args = [ ] ; iterFn ( config , ( ... as ) => { args . push ( as ) ; } ) ; this . shuffle ( args ) ; for ( let a of args ) { cb ( ... a ) ; } } doProportion ( p , callback ) { if ( this . rng . number ( ) < p ) { callback ( ) ; } } times ( n , callback ) { for ( let i = 0 ; i < n ; i ++ ) { callback ( i ) ; } } downFrom ( n , callback ) { for ( let i = n ; i > 0 ; i -- ) { callback ( i ) ; } } aroundCircle = ( config , callback ) => { const { n , at : [ cX , cY ] = [ 0.5 , 0.5 / this . aspectRatio ] , r = 0.25 } = config ; const da = ( Math . PI * 2 ) / n ; let a = - Math . PI * 0.5 ; for ( let i = 0 ; i < n ; i ++ ) { callback ( [ cX + r * Math . cos ( a + da ) , cY + r * Math . sin ( a + da ) ] , i ) ; a += da ; } } ; proportionately < T > ( cases ) { const total = cases . map ( c => c [ 0 ] ) . reduce ( ( a , b ) => a + b , 0 ) ; if ( total <= 0 ) throw new Error ( "Must be positive total" ) ; let r = this . rng . number ( ) * total ; for ( let i = 0 ; i < cases . length ; i ++ ) { if ( cases [ i ] [ 0 ] > r ) { return cases [ i ] [ 1 ] ( ) ; } else { r -= cases [ i ] [ 0 ] ; } } return cases [ 0 ] [ 1 ] ( ) ; } randomPoint ( ) { return [ this . rng . number ( ) , this . rng . number ( ) / this . aspectRatio ] ; } range ( config , callback ) { const { from = 0 , to = 1 , n , inclusive = true } = config ; const di = ( to - from ) / n ; const max = inclusive ? n : n - 1 ; for ( let i = 0 ; i <= max ; i ++ ) { callback ( i * di + from ) ; } } inDrawing = ( point ) : boolean => { const { left , right , top , bottom } = this . meta ; return ( point [ 0 ] > left && point [ 0 ] < right && point [ 1 ] > top && point [ 1 ] < bottom ) ; } ; private pushState ( ) { this . ctx . save ( ) ; } private popState ( ) { this . ctx . restore ( ) ; } withClipping = ( clipArea , callback ) => { this . pushState ( ) ; this . ctx . beginPath ( ) ; clipArea . traceIn ( this . ctx ) ; this . ctx . clip ( ) ; callback ( ) ; this . popState ( ) ; } ; /**
     * Within a context all style/color changes are local.
     */ withContext = ( callback ) => { this . pushState ( ) ; callback ( ) ; this . popState ( ) ; } ; withRotation = ( angle , callback ) => { this . pushState ( ) ; this . ctx . rotate ( angle ) ; callback ( ) ; this . popState ( ) ; } ; withScale = ( scale , callback ) => { this . pushState ( ) ; this . ctx . scale ( scale [ 0 ] , scale [ 1 ] ) ; callback ( ) ; this . popState ( ) ; } ; withTranslation = ( translation , callback ) => { this . pushState ( ) ; this . ctx . translate ( translation [ 0 ] , translation [ 1 ] ) ; callback ( ) ; this . popState ( ) ; } ; withTransform = ( config , callback ) => { this . pushState ( ) ; const { hScale , hskew , vSkew , vScaling , dX , dY } = config ; this . ctx . transform ( hScale , hskew , vSkew , vScaling , dX , dY ) ; callback ( ) ; this . popState ( ) ; } ; /**
     * A uniform random number betweeon 0 and 1
     */ random = ( ) : number => { return this . rng . number ( ) ; } ; /**
     * A uniform random integer. Default lower bound is 0.
     * Upper bound can be inclusive (default) or exclusive
     */ uniformRandomInt = ( config ) => { const { to , from = 0 , inclusive = true } = config ; const d = to - from + ( inclusive ? 1 : 0 ) ; return from + Math . floor ( this . random ( ) * d ) ; } ; /**
     * A random Point2D on a grid
     */ uniformGridPoint = ( { minX , maxX , minY , maxY , } ) : Point2D => { return [ this . uniformRandomInt ( { from : minX , to : maxX } ) , this . uniformRandomInt ( { from : minY , to : maxY } ) , ] ; } ; /**
     * A coin toss with result either -1 or 1
     */ randomPolarity = ( ) : 1 | - 1 => { return this . rng . number ( ) > 0.5 ? 1 : - 1 ; } ; /**
     * Sample uniformly from an array
     */ sample = < T > ( from ) : T => { return from [ Math . floor ( this . rng . number ( ) * from . length ) ] ; } ; /**
     * n uniform samples from an array
     */ samples = < T > ( n , from ) : T [ ] => { let res = [ ] ; for ( let i = 0 ; i < n ; i ++ ) { res . push ( this . sample ( from ) ) ; } return res ; } ; /**
     * Shuffle an array
     */ shuffle = < T > ( items ) : T [ ] => { let currentIndex = items . length ; let temporaryValue ; let randomIndex = 0 ; while ( 0 !== currentIndex ) { randomIndex = Math . floor ( this . rng . number ( ) * currentIndex ) ; currentIndex -= 1 ; temporaryValue = items [ currentIndex ] ; items [ currentIndex ] = items [ randomIndex ] ; items [ randomIndex ] = temporaryValue ; } return items ; } ; /**
     * Perturb a point by a random amount (by default uniform random changes in
     * -0.05 to 0.05, optional magnitude scales this e.g. magnitude 1 is perturbations
     * of -0.5 to 0.5)
     */ perturb = ( config ) : Point2D => { const { at : [ x , y ] , magnitude = 0.1 , } = config ; return [ x + magnitude * ( this . rng . number ( ) - 0.5 ) , y + magnitude * ( this . rng . number ( ) - 0.5 ) , ] ; } ; /**
     * Gaussian random number, default mean 0, default standard deviation 1
     */ gaussian = ( config ? ) : number => { const { mean = 0 , sd = 1 } = config || { } ; const a = this . rng . number ( ) ; const b = this . rng . number ( ) ; const n = Math . sqrt ( - 2.0 * Math . log ( a ) ) * Math . cos ( 2.0 * Math . PI * b ) ; return mean + n * sd ; } ; /**
     * Poisson random number, lambda (the mean and variance) is only parameter
     */ poisson = ( lambda ) : number => { const limit = Math . exp ( - lambda ) ; let prod = this . rng . number ( ) ; let n = 0 ; while ( prod >= limit ) { n ++ ; prod *= this . rng . number ( ) ; } return n ; } ; oscillate = ( config ? ) : number => { const { from = 0 , to = 1 , rate = 1 } = config || { } ; return from + ( ( to - from ) * ( 1 + Math . cos ( this . t * rate ) ) ) / 2 ; } ; }