{"version":3,"sources":["../src/Canvas.ts","../src/Space.ts","../src/Pt.ts","../src/Util.ts","../src/Num.ts","../src/Op.ts","../src/LinearAlgebra.ts","../src/uheprng.ts","../src/UI.ts","../src/Form.ts","../src/Typography.ts","../src/Image.ts","../src/Create.ts","../src/Color.ts","../src/Dom.ts","../src/Svg.ts","../src/Physics.ts","../src/Play.ts","../src/_script.ts"],"sourcesContent":["/*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */\r\n\r\nimport {MultiTouchSpace} from './Space';\r\nimport {VisualForm, Font} from \"./Form\";\r\nimport {Pt, Group, Bound} from \"./Pt\";\r\nimport {Const, Util} from \"./Util\";\r\nimport {Typography as Typo} from \"./Typography\";\r\nimport { Rectangle } from './Op';\r\nimport {Img} from './Image';\r\nimport {PtLike, GroupLike, PtsCanvasRenderingContext2D, DefaultFormStyle, PtLikeIterable, PtIterable, CanvasSpaceOptions} from \"./Types\";\r\n\r\n\r\n\r\n\r\n/**\r\n* CanvasSpace is an implementation of the abstract class [`Space`](#link). It represents a space for HTML Canvas.\r\n* Learn more about the concept of Space in [this guide](../guide/Space-0500.html).\r\n*/\r\nexport class CanvasSpace extends MultiTouchSpace {\r\n \r\n protected _canvas:HTMLCanvasElement;\r\n protected _container:Element;\r\n\r\n protected _pixelScale = 1;\r\n protected _autoResize = true;\r\n protected _bgcolor = \"#e1e9f0\";\r\n protected _ctx:PtsCanvasRenderingContext2D;\r\n \r\n protected _offscreen = false;\r\n protected _offCanvas:HTMLCanvasElement;\r\n protected _offCtx:PtsCanvasRenderingContext2D;\r\n\r\n protected _initialResize = false;\r\n \r\n\r\n \r\n /**\r\n * Create a CanvasSpace which represents a HTML Canvas Space\r\n * @param elem Specify an element by its \"id\" attribute as string, or by the element object itself. An element can be an existing ``, or a `
` container in which a new `` will be created. If left empty, a `
` will be added to DOM. Use css to customize its appearance if needed.\r\n * @param callback an optional callback `function(boundingBox, spaceElement)` to be called when canvas is appended and ready. Alternatively, a \"ready\" event will also be fired from the `` element when it's appended, which can be traced with `spaceInstance.canvas.addEventListener(\"ready\")`\r\n * @example `new CanvasSpace( \"#myElementID\" )`\r\n */\r\n constructor( elem:string|Element, callback?:Function) {\r\n super();\r\n \r\n var _selector:Element = null;\r\n var _existed = false;\r\n this.id = \"pt\";\r\n \r\n // check element or element id string\r\n if ( elem instanceof Element ) {\r\n _selector = elem;\r\n this.id = \"pts_existing_space\";\r\n } else {\r\n let id = elem;\r\n id = (elem[0] === \"#\" || elem[0] === \".\") ? elem : \"#\"+elem;\r\n _selector = document.querySelector( id );\r\n _existed = true;\r\n this.id = id.substr(1);\r\n }\r\n \r\n // if selector is not defined, create a canvas\r\n if (!_selector) { \r\n this._container = this._createElement( \"div\", this.id+\"_container\" );\r\n this._canvas = this._createElement( \"canvas\", this.id ) as HTMLCanvasElement;\r\n this._container.appendChild( this._canvas );\r\n document.body.appendChild( this._container );\r\n _existed = false;\r\n \r\n // if selector is element but not canvas, create a canvas inside it\r\n } else if (_selector.nodeName.toLowerCase() != \"canvas\") { \r\n this._container = _selector;\r\n this._canvas = this._createElement( \"canvas\", this.id+\"_canvas\" ) as HTMLCanvasElement;\r\n this._container.appendChild( this._canvas );\r\n this._initialResize = true;\r\n \r\n // if selector is an existing canvas\r\n } else {\r\n this._canvas = _selector as HTMLCanvasElement;\r\n this._container = _selector.parentElement;\r\n this._autoResize = false;\r\n }\r\n \r\n // if size is known then set it immediately\r\n // if (_existed) {\r\n // let b = this._container.getBoundingClientRect();\r\n // this.resize( Bound.fromBoundingRect(b) );\r\n // }\r\n \r\n // no mutation observer, so we set a timeout for ready event\r\n setTimeout( this._ready.bind( this, callback ), 100 );\r\n \r\n // store canvas 2d rendering context\r\n this._ctx = this._canvas.getContext('2d');\r\n \r\n }\r\n \r\n \r\n /**\r\n * Helper function to create a DOM element\r\n * @param elem element tag name\r\n * @param id element id attribute\r\n */\r\n protected _createElement( elem=\"div\", id ) {\r\n let d = document.createElement( elem );\r\n d.setAttribute(\"id\", id);\r\n return d;\r\n }\r\n \r\n \r\n /**\r\n * Handle callbacks after element is mounted in DOM\r\n * @param callback \r\n */\r\n private _ready( callback:Function ) {\r\n if (!this._container) throw new Error(`Cannot initiate #${this.id} element`);\r\n \r\n this._isReady = true;\r\n \r\n this._resizeHandler( null );\r\n\r\n this.clear( this._bgcolor );\r\n this._canvas.dispatchEvent( new Event(\"ready\") );\r\n \r\n for (let k in this.players) {\r\n if (this.players.hasOwnProperty(k)) {\r\n if (this.players[k].start) this.players[k].start( this.bound.clone(), this );\r\n }\r\n }\r\n \r\n this._pointer = this.center;\r\n this._initialResize = false; // unset\r\n \r\n if (callback) callback( this.bound, this._canvas );\r\n }\r\n \r\n \r\n /**\r\n * Set up various options for CanvasSpace. The `opt` parameter is an object with the following fields. This is usually set during instantiation, eg `new CanvasSpace(...).setup( { opt } )`\r\n * @param opt a [`CanvasSpaceOptions`](#link) object with optional settings, ie `{ bgcolor:string, resize:boolean, retina:boolean, offscreen:boolean, pixelDensity:number }`. \r\n * @example `space.setup({ bgcolor: \"#f00\", retina: true, resize: true })`\r\n */\r\n setup( opt:CanvasSpaceOptions ):this {\r\n this._bgcolor = opt.bgcolor ? opt.bgcolor : \"transparent\";\r\n \r\n this.autoResize = (opt.resize != undefined) ? opt.resize : false;\r\n \r\n if (opt.retina !== false) {\r\n let r1 = window ? window.devicePixelRatio || 1 : 1;\r\n let r2 = this._ctx.webkitBackingStorePixelRatio || this._ctx.mozBackingStorePixelRatio || this._ctx.msBackingStorePixelRatio || this._ctx.oBackingStorePixelRatio || this._ctx.backingStorePixelRatio || 1; \r\n this._pixelScale = Math.max(1, r1/r2);\r\n }\r\n \r\n if (opt.offscreen) {\r\n this._offscreen = true;\r\n this._offCanvas = this._createElement( \"canvas\", this.id+\"_offscreen\" ) as HTMLCanvasElement;\r\n this._offCtx = this._offCanvas.getContext('2d');\r\n } else {\r\n this._offscreen = false;\r\n }\r\n\r\n if (opt.pixelDensity) {\r\n this._pixelScale = opt.pixelDensity;\r\n }\r\n \r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Set whether the canvas element should resize when its container is resized. \r\n * @param auto a boolean value indicating if auto size is set\r\n */\r\n set autoResize( auto ) {\r\n if (!window) return;\r\n this._autoResize = auto;\r\n if (auto) {\r\n window.addEventListener( 'resize', this._resizeHandler.bind(this) );\r\n } else {\r\n window.removeEventListener( 'resize', this._resizeHandler.bind(this) );\r\n }\r\n }\r\n get autoResize(): boolean { return this._autoResize; }\r\n \r\n \r\n /**\r\n * This overrides Space's `resize` function. It's used as a callback function for window's resize event and not usually called directly. You can keep track of resize events with `resize: (bound ,evt)` callback in your player objects. \r\n * @param b a Bound object to resize to\r\n * @param evt Optionally pass a resize event\r\n * @see Space.add\r\n */\r\n resize( b:Bound, evt?:Event):this {\r\n \r\n this.bound = b;\r\n\r\n this._canvas.width = Math.ceil(this.bound.size.x) * this._pixelScale;\r\n this._canvas.height = Math.ceil(this.bound.size.y) * this._pixelScale;\r\n this._canvas.style.width = Math.ceil(this.bound.size.x) + \"px\";\r\n this._canvas.style.height = Math.ceil(this.bound.size.y) + \"px\";\r\n \r\n if (this._offscreen) {\r\n this._offCanvas.width = Math.ceil(this.bound.size.x) * this._pixelScale;\r\n this._offCanvas.height = Math.ceil(this.bound.size.y) * this._pixelScale;\r\n // this._offCanvas.style.width = Math.floor(this.bound.size.x) + \"px\";\r\n // this._offCanvas.style.height = Math.floor(this.bound.size.y) + \"px\";\r\n }\r\n \r\n if (this._pixelScale != 1) {\r\n this._ctx.scale( this._pixelScale, this._pixelScale );\r\n \r\n if (this._offscreen) {\r\n this._offCtx.scale( this._pixelScale, this._pixelScale );\r\n }\r\n }\r\n \r\n for (let k in this.players) {\r\n if (this.players.hasOwnProperty(k)) {\r\n let p = this.players[k];\r\n if (p.resize) p.resize( this.bound, evt);\r\n }\r\n }\r\n \r\n this.render( this._ctx );\r\n\r\n // if it's a valid resize event and space is not playing, repaint the canvas once\r\n if (evt && !this.isPlaying) this.playOnce( 0 ); \r\n \r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Window resize handling\r\n * @param evt \r\n */\r\n protected _resizeHandler( evt:Event ) {\r\n if (!window) return;\r\n let b = (this._autoResize || this._initialResize) ? this._container.getBoundingClientRect() : this._canvas.getBoundingClientRect();\r\n\r\n if (b) {\r\n let box = Bound.fromBoundingRect(b);\r\n \r\n // Need to compute offset from window scroll. See outerBound calculation in Space's _mouseAction \r\n box.center = box.center.add( window.pageXOffset, window.pageYOffset ); \r\n this.resize( box, evt );\r\n }\r\n }\r\n \r\n\r\n /**\r\n * Set a background color for this canvas. Alternatively, you may use `clear()` function.\r\n @param bg background color as hex or rgba string\r\n */\r\n set background( bg:string ) { this._bgcolor = bg; }\r\n get background():string { return this._bgcolor; }\r\n\r\n \r\n /**\r\n * `pixelScale` property returns a number that let you determine if the screen is \"retina\" (when value >= 2)\r\n */\r\n public get pixelScale():number {\r\n return this._pixelScale;\r\n }\r\n \r\n \r\n /**\r\n * Check if an offscreen canvas is created\r\n */\r\n public get hasOffscreen():boolean {\r\n return this._offscreen;\r\n }\r\n \r\n \r\n /**\r\n * Get the rendering context of offscreen canvas (if created via `setup()`)\r\n */\r\n public get offscreenCtx():PtsCanvasRenderingContext2D { return this._offCtx; }\r\n \r\n \r\n /**\r\n * Get the offscreen canvas element\r\n */\r\n public get offscreenCanvas():HTMLCanvasElement { return this._offCanvas; }\r\n \r\n \r\n\r\n \r\n /**\r\n * Get a new `CanvasForm` for drawing\r\n * @see `CanvasForm`\r\n */\r\n public getForm():CanvasForm { return new CanvasForm(this); }\r\n \r\n \r\n /**\r\n * Get the html canvas element\r\n */\r\n get element():HTMLCanvasElement {\r\n return this._canvas;\r\n }\r\n \r\n \r\n /**\r\n * Get the parent element that contains the canvas element\r\n */\r\n get parent():Element {\r\n return this._container;\r\n }\r\n\r\n\r\n /**\r\n * A property to indicate if the Space is ready\r\n */\r\n get ready():boolean { \r\n return this._isReady; \r\n }\r\n \r\n \r\n /**\r\n * Get the rendering context of canvas\r\n * @example `form.ctx.clip()`\r\n */\r\n public get ctx():PtsCanvasRenderingContext2D { return this._ctx; }\r\n \r\n \r\n \r\n /**\r\n * Clear the canvas with its background color. Overrides Space's `clear` function.\r\n * @param bg Optionally specify a custom background color in hex or rgba string, or \"transparent\". If not defined, it will use its `bgcolor` property as background color to clear the canvas.\r\n */\r\n clear( bg?:string ):this {\r\n \r\n if (bg) this._bgcolor = bg;\r\n const lastColor = this._ctx.fillStyle;\r\n const px = Math.ceil(this.pixelScale);\r\n \r\n if (!this._bgcolor || this._bgcolor === \"transparent\") {\r\n this._ctx.clearRect( -px, -px, this._canvas.width+px, this._canvas.height+px );\r\n } else { \r\n // semi-transparent bg needs to be cleared first\r\n if (this._bgcolor.indexOf(\"rgba\") === 0 || (this._bgcolor.length === 9 && this._bgcolor.indexOf(\"#\") === 0) ) { \r\n this._ctx.clearRect( -px, -px, this._canvas.width+px, this._canvas.height+px );\r\n }\r\n this._ctx.fillStyle = this._bgcolor;\r\n this._ctx.fillRect( -px, -px, this._canvas.width+px, this._canvas.height+px );\r\n }\r\n \r\n this._ctx.fillStyle = lastColor;\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Similiar to `clear()` but clear the offscreen canvas instead\r\n * @param bg Optionally specify a custom background color in hex or rgba string, or \"transparent\". If not defined, it will use its `bgcolor` property as background color to clear the canvas.\r\n */\r\n clearOffscreen( bg?:string ):this {\r\n if (this._offscreen) {\r\n const px = Math.ceil(this.pixelScale);\r\n if (bg) {\r\n this._offCtx.fillStyle = bg;\r\n this._offCtx.fillRect( -px, -px, this._canvas.width+px, this._canvas.height+px );\r\n } else {\r\n this._offCtx.clearRect( -px, -px, this._offCanvas.width+px, this._offCanvas.height+px );\r\n }\r\n }\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Main animation function.\r\n * @param time current time\r\n */\r\n protected playItems( time: number ) {\r\n if (this._isReady) {\r\n this._ctx.save();\r\n if (this._offscreen) this._offCtx.save();\r\n super.playItems( time );\r\n this._ctx.restore();\r\n if (this._offscreen) this._offCtx.restore();\r\n this.render( this._ctx );\r\n }\r\n }\r\n \r\n \r\n /**\r\n * Dispose of browser resources held by this space and remove all players. Call this before unmounting the canvas.\r\n */\r\n dispose():this {\r\n if (!window) return;\r\n // remove event listeners\r\n window.removeEventListener( 'resize', this._resizeHandler.bind(this) );\r\n // stop animation loop\r\n this.stop();\r\n // remove players from space\r\n this.removeAll();\r\n\r\n return this;\r\n }\r\n \r\n\r\n /**\r\n * Get a [`MediaRecorder`](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) to record the current CanvasSpace. You can then call its `start()` function to start recording, and `stop()` to either download the video file or handle the blob data in the callback function you provided.\r\n * @param downloadOrCallback Either `true` to download the video, or provide a callback function to handle the Blob data, when recording is completed.\r\n * @param filetype video format. Default is \"webm\".\r\n * @param bitrate bitrate per second\r\n * @example `let rec = space.recorder(true); rec.start(); setTimeout( () => rec.stop(), 5000); // record 5s of video and download the file`\r\n */\r\n recorder( downloadOrCallback: boolean | ((blobURL:string) => {}), filetype:string = \"webm\", bitrate:number = 15000000 ): MediaRecorder {\r\n // @ts-ignore\r\n let stream = this._canvas.captureStream();\r\n const recorder = new MediaRecorder(stream, { mimeType: `video/${filetype}`, bitsPerSecond: bitrate } );\r\n \r\n recorder.ondataavailable = function(d) {\r\n let url = URL.createObjectURL( new Blob( [d.data], { type: `video/${filetype}` } ) );\r\n\r\n if (typeof downloadOrCallback === \"function\") {\r\n downloadOrCallback( url );\r\n\r\n } else if (downloadOrCallback) {\r\n let a = document.createElement(\"a\");\r\n a.href = url;\r\n a.download = `canvas_video.${filetype}`;\r\n a.click();\r\n a.remove();\r\n }\r\n };\r\n\r\n return recorder;\r\n }\r\n\r\n}\r\n\r\n\r\n\r\n\r\n\r\n/**\r\n* CanvasForm is an implementation of abstract class [`VisualForm`](#link). It provide methods to express Pts on [`CanvasSpace`](#link). \r\n* You may extend CanvasForm to implement your own expressions for CanvasSpace.\r\n*/\r\nexport class CanvasForm extends VisualForm {\r\n \r\n protected _space:CanvasSpace;\r\n protected _ctx:CanvasRenderingContext2D; \r\n protected _estimateTextWidth:(string) => number;\r\n\r\n /** \r\n * store common styles so that they can be restored to canvas context when using multiple forms. See `reset()`.\r\n */\r\n protected _style:DefaultFormStyle = {\r\n fillStyle: \"#f03\", strokeStyle:\"#fff\", \r\n lineWidth: 1, lineJoin: \"bevel\", lineCap: \"butt\",\r\n globalAlpha: 1\r\n };\r\n \r\n \r\n /**\r\n * Create a new CanvasForm. You may also use [`CanvasSpace.getForm()`](#link) to get the default form.\r\n * @param space an instance of CanvasSpace\r\n */\r\n constructor( space?:CanvasSpace|CanvasRenderingContext2D ) {\r\n super();\r\n\r\n // allow for undefined context to support custom contexts via subclassing. \r\n if (!space) return this; \r\n \r\n const _setup = (ctx) => {\r\n this._ctx = ctx;\r\n this._ctx.fillStyle = this._style.fillStyle;\r\n this._ctx.strokeStyle = this._style.strokeStyle; \r\n this._ctx.lineJoin = \"bevel\";\r\n this._ctx.font = this._font.value;\r\n this._ready = true;\r\n };\r\n\r\n if (space instanceof CanvasRenderingContext2D) {\r\n _setup( space );\r\n } else {\r\n this._space = space;\r\n this._space.add( { start: () => {\r\n _setup( this._space.ctx );\r\n }} );\r\n }\r\n }\r\n \r\n \r\n /**\r\n * get the CanvasSpace instance that this form is associated with\r\n */\r\n get space():CanvasSpace { return this._space; }\r\n \r\n\r\n /**\r\n * Get the rendering context of canvas to perform other canvas functions.\r\n * @example `form.ctx.clip()`\r\n */\r\n get ctx():PtsCanvasRenderingContext2D { return this._ctx; }\r\n\r\n\r\n /**\r\n * Toggle whether to draw on offscreen canvas (if offscreen is set in CanvasSpace)\r\n * @param off if `true`, draw on offscreen canvas instead of the visible canvas. Default is `true`\r\n * @param clear optionally provide a valid color string to fill a bg color. see CanvasSpace's `clearOffscreen` function.\r\n */\r\n useOffscreen( off:boolean=true, clear:boolean|string=false ) {\r\n if (clear) this._space.clearOffscreen( (typeof clear == \"string\") ? clear : null );\r\n this._ctx = (this._space.hasOffscreen && off) ? this._space.offscreenCtx : this._space.ctx;\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Render the offscreen canvas's content on the visible canvas\r\n * @param offset Optional offset on the top-left position when drawing on the visible canvas\r\n */\r\n renderOffscreen( offset:PtLike=[0,0] ) {\r\n if (this._space.hasOffscreen) {\r\n this._space.ctx.drawImage( \r\n this._space.offscreenCanvas, offset[0], offset[1], this._space.width, this._space.height );\r\n }\r\n }\r\n \r\n \r\n /**\r\n * Set current alpha value.\r\n * @example `form.alpha(0.6)`\r\n * @param a alpha value between 0 and 1\r\n */\r\n alpha( a:number ):this {\r\n this._ctx.globalAlpha = a;\r\n this._style.globalAlpha = a;\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * Set current fill style. Provide a valid color string such as `\"#FFF\"` or `\"rgba(255,0,100,0.5)\"` or `false` to specify no fill color.\r\n * @example `form.fill(\"#F90\")`, `form.fill(\"rgba(0,0,0,.5\")`, `form.fill(false)`\r\n * @param c fill color which can be as color, gradient, or pattern. (See [canvas documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle))\r\n */\r\n fill( c:string|boolean|CanvasGradient|CanvasPattern ):this {\r\n if (typeof c == \"boolean\") {\r\n this.filled = c;\r\n } else {\r\n this.filled = true;\r\n this._style.fillStyle = c;\r\n this._ctx.fillStyle = c;\r\n }\r\n return this;\r\n }\r\n \r\n /**\r\n * Set current fill style and remove stroke style.\r\n * @param c fill color which can be as color, gradient, or pattern. (See [canvas documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle))\r\n */\r\n fillOnly( c:string|boolean|CanvasGradient|CanvasPattern ):this {\r\n this.stroke( false );\r\n return this.fill( c );\r\n }\r\n \r\n \r\n /**\r\n * Set current stroke style. Provide a valid color string or `false` to specify no stroke color.\r\n * @example `form.stroke(\"#F90\")`, `form.stroke(\"rgba(0,0,0,.5\")`, `form.stroke(false)`, `form.stroke(\"#000\", 0.5, 'round', 'square')`\r\n * @param c stroke color which can be as color, gradient, or pattern. (See [canvas documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle))\r\n * @param width Optional value (can be floating point) to set line width\r\n * @param linejoin Optional string to set line joint style. Can be \"miter\", \"bevel\", or \"round\".\r\n * @param linecap Optional string to set line cap style. Can be \"butt\", \"round\", or \"square\".\r\n */\r\n stroke( c:string|boolean|CanvasGradient|CanvasPattern, width?:number, linejoin?:CanvasLineJoin, linecap?:CanvasLineCap ):this {\r\n if (typeof c == \"boolean\") {\r\n this.stroked = c;\r\n } else {\r\n this.stroked = true;\r\n this._style.strokeStyle = c;\r\n this._ctx.strokeStyle = c;\r\n if (width) {\r\n this._ctx.lineWidth = width;\r\n this._style.lineWidth = width;\r\n }\r\n if (linejoin) {\r\n this._ctx.lineJoin = linejoin;\r\n this._style.lineJoin = linejoin;\r\n }\r\n if (linecap) {\r\n this._ctx.lineCap = linecap;\r\n this._style.lineCap = linecap;\r\n }\r\n }\r\n return this;\r\n }\r\n\r\n /**\r\n * Set stroke style and remove fill style.\r\n * @param c stroke color which can be as color, gradient, or pattern. (See [canvas documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle))\r\n * @param width Optional value (can be floating point) to set line width\r\n * @param linejoin Optional string to set line joint style. Can be \"miter\", \"bevel\", or \"round\".\r\n * @param linecap Optional string to set line cap style. Can be \"butt\", \"round\", or \"square\".\r\n */\r\n strokeOnly( c:string|boolean|CanvasGradient|CanvasPattern, width?:number, linejoin?:CanvasLineJoin, linecap?:CanvasLineCap ):this {\r\n this.fill( false );\r\n return this.stroke( c, width, linejoin, linecap );\r\n }\r\n\r\n /**\r\n * A convenient function to apply fill and/or stroke after custom drawings using canvas context (eg, `form.ctx.ellipse(...)`). \r\n * You don't need to call this function if you're using Pts' drawing functions like `form.point` or `form.rect`\r\n * @param filled apply fill when set to `true`\r\n * @param stroked apply stroke when set to `true`\r\n * @param strokeWidth optionally set a stroke width\r\n * @example `form.ctx.beginPath(); form.ctx.ellipse(...); form.applyFillStroke();`\r\n */\r\n applyFillStroke( filled:boolean|string = true, stroked:boolean|string = true, strokeWidth:number = 1 ) {\r\n if (filled) {\r\n if (typeof filled === 'string') this.fill( filled );\r\n this._ctx.fill();\r\n }\r\n\r\n if (stroked) {\r\n if (typeof stroked === 'string') this.stroke( stroked, strokeWidth );\r\n this._ctx.stroke();\r\n }\r\n\r\n return this;\r\n }\r\n \r\n\r\n /**\r\n * This function takes an array of gradient colors, and returns a function to define the areas of the gradient fill. See demo code in [CanvasForm.gradient](https://ptsjs.org/demo/?name=canvasform.textBox).\r\n * @param stops an array of gradient stops. This can be an array of colors `[\"#f00\", \"#0f0\", ...]` for evenly distributed gradient, or an array of [stop, color] like `[[0.1, \"#f00\"], [0.7, \"#0f0\"]]`\r\n * @returns a function that takes 1 or 2 `Group` as parameters. Use a single `Group` to specify a rectangular area for linear gradient, or use 2 `Groups` to specify 2 `Circles` for radial gradient.\r\n * @example `c1 = Circle.fromCenter(...); grad = form.gradient([\"#f00\", \"#00f\"]); form.fill( grad( c1, c2 ) ).circle( c1 )`\r\n */\r\n gradient( stops:[number, string][]|string[] ):((area1:GroupLike, area2?:GroupLike) => CanvasGradient) {\r\n let vals:[number, string][] = [];\r\n if (stops.length < 2) (stops as [number, string][]).push( [0.99, \"#000\"], [1,\"#000\"] );\r\n\r\n for (let i=0, len=stops.length; i {\r\n let grad = area2 \r\n ? this._ctx.createRadialGradient( area1[0][0], area1[0][1], Math.abs(area1[1][0]), area2[0][0], area2[0][1], Math.abs(area2[1][0]) )\r\n : this._ctx.createLinearGradient( area1[0][0], area1[0][1], area1[1][0], area1[1][1] );\r\n\r\n for (let i=0, len=vals.length; i this._ctx.measureText(c).width) ) : undefined;\r\n return this;\r\n }\r\n\r\n\r\n /**\r\n * Get the width of this text. It will return an actual measurement or an estimate based on [`fontWidthEstimate`](#link) setting. Default is an actual measurement using canvas context's measureText.\r\n * @param c a string of text contents\r\n */\r\n getTextWidth( c:string ):number {\r\n return (!this._estimateTextWidth) ? this._ctx.measureText(c+\" .\").width : this._estimateTextWidth( c );\r\n }\r\n\r\n\r\n /**\r\n * Truncate text to fit width.\r\n * @param str text to truncate\r\n * @param width width to fit\r\n * @param tail text to indicate overflow such as \"...\". Default is empty \"\".\r\n */\r\n protected _textTruncate( str:string, width:number, tail:string=\"\" ):[string, number] {\r\n return Typo.truncate( this.getTextWidth.bind(this), str, width, tail );\r\n }\r\n\r\n\r\n /**\r\n * Align text within a rectangle box.\r\n * @param box a Group or an Iterable that defines a rectangular box\r\n * @param vertical a string that specifies the vertical alignment in the box: \"top\", \"bottom\", \"middle\", \"start\", \"end\"\r\n * @param offset Optional offset from the edge (like padding)\r\n * @param center Optional center position \r\n */\r\n protected _textAlign( box:PtLikeIterable, vertical:string, offset?:PtLike, center?:Pt ):Pt {\r\n let _box = Util.iterToArray( box );\r\n if ( !Util.arrayCheck(_box) ) return;\r\n\r\n if (!center) center = Rectangle.center( _box );\r\n\r\n var px = _box[0][0];\r\n if (this._ctx.textAlign == \"end\" || this._ctx.textAlign == \"right\") {\r\n px = _box[1][0];\r\n // @ts-ignore\r\n } else if (this._ctx.textAlign == \"center\" || this._ctx.textAlign == \"middle\") {\r\n px = center[0];\r\n }\r\n\r\n var py = center[1];\r\n if (vertical == \"top\" || vertical == \"start\") {\r\n py = _box[0][1];\r\n } else if (vertical == \"end\" || vertical == \"bottom\") {\r\n py = _box[1][1];\r\n }\r\n\r\n return (offset) ? new Pt( px+offset[0], py+offset[1] ) : new Pt(px, py);\r\n }\r\n \r\n \r\n /**\r\n * Reset the rendering context's common styles to this form's styles. This supports using multiple forms on the same canvas context.\r\n */\r\n reset():this {\r\n for (let k in this._style) {\r\n if (this._style.hasOwnProperty(k)) {\r\n this._ctx[k] = this._style[k];\r\n }\r\n }\r\n this._font = new Font();\r\n this._ctx.font = this._font.value;\r\n return this;\r\n }\r\n \r\n \r\n protected _paint() {\r\n if (this._filled) this._ctx.fill();\r\n if (this._stroked) this._ctx.stroke();\r\n }\r\n \r\n\r\n /**\r\n * A static function to draw a point.\r\n * @param ctx canvas rendering context\r\n * @param p a Pt object\r\n * @param radius radius of the point. Default is 5.\r\n * @param shape The shape of the point. Defaults to \"square\", but it can be \"circle\" or a custom shape function in your own implementation.\r\n * @example `form.point( p )`, `form.point( p, 10, \"circle\" )`\r\n */\r\n static point( ctx:CanvasRenderingContext2D, p:PtLike, radius:number=5, shape:string=\"square\" ) {\r\n if (!p) return;\r\n if (!CanvasForm[shape]) throw new Error(`${shape} is not a static function of CanvasForm`);\r\n CanvasForm[shape]( ctx, p, radius );\r\n }\r\n \r\n\r\n /**\r\n * Draws a point.\r\n * @param p a Pt object\r\n * @param radius radius of the point. Default is 5.\r\n * @param shape The shape of the point. Defaults to \"square\", but it can be \"circle\" or a custom shape function in your own implementation.\r\n * @example `form.point( p )`, `form.point( p, 10, \"circle\" )`\r\n */\r\n point( p:PtLike, radius:number=5, shape:string=\"square\" ):this {\r\n CanvasForm.point( this._ctx, p, radius, shape );\r\n this._paint();\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * A static function to draw a circle.\r\n * @param ctx canvas rendering context\r\n * @param pt center position of the circle\r\n * @param radius radius of the circle\r\n */\r\n static circle( ctx:CanvasRenderingContext2D, pt:PtLike, radius:number=10 ) {\r\n if (!pt) return;\r\n ctx.beginPath();\r\n ctx.arc( pt[0], pt[1], radius, 0, Const.two_pi, false );\r\n ctx.closePath();\r\n }\r\n \r\n \r\n /**\r\n * Draw a circle. See also [`Circle.fromCenter`](#link)\r\n * @param pts usually a Group or an Iterable with 2 Pt, but it can also take an array of two numeric arrays [ [position], [size] ]\r\n */\r\n circle( pts:PtLikeIterable ):this {\r\n let p = Util.iterToArray( pts );\r\n CanvasForm.circle( this._ctx, p[0], p[1][0] );\r\n this._paint();\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * A static function to draw an ellipse.\r\n * @param ctx canvas rendering context\r\n * @param pt center position \r\n * @param radius radius [x, y] of the ellipse\r\n * @param rotation rotation of the ellipse in radian. Default is 0.\r\n * @param startAngle start angle of the ellipse. Default is 0.\r\n * @param endAngle end angle of the ellipse. Default is 2 PI.\r\n * @param cc an optional boolean value to specify if it should be drawn clockwise (`false`) or counter-clockwise (`true`). Default is clockwise.\r\n */\r\n static ellipse( ctx:CanvasRenderingContext2D, pt:PtLike, radius:PtLike, rotation:number=0, startAngle:number=0, endAngle:number=Const.two_pi, cc:boolean=false ) {\r\n if (!pt || !radius) return;\r\n ctx.beginPath();\r\n ctx.ellipse( pt[0], pt[1], radius[0], radius[1], rotation, startAngle, endAngle, cc );\r\n }\r\n\r\n\r\n /**\r\n * Draw an ellipse.\r\n * @param pt center position \r\n * @param radius radius [x, y] of the ellipse\r\n * @param rotation rotation of the ellipse in radian. Default is 0.\r\n * @param startAngle start angle of the ellipse. Default is 0.\r\n * @param endAngle end angle of the ellipse. Default is 2 PI.\r\n * @param cc an optional boolean value to specify if it should be drawn clockwise (`false`) or counter-clockwise (`true`). Default is clockwise.\r\n */\r\n ellipse( pt:PtLike, radius:PtLike, rotation:number=0, startAngle:number=0, endAngle:number=Const.two_pi, cc:boolean=false ) {\r\n CanvasForm.ellipse( this._ctx, pt, radius, rotation, startAngle, endAngle, cc );\r\n this._paint();\r\n return this;\r\n }\r\n\r\n\r\n /**\r\n * A static function to draw an arc.\r\n * @param ctx canvas rendering context\r\n * @param pt center position \r\n * @param radius radius of the arc circle\r\n * @param startAngle start angle of the arc\r\n * @param endAngle end angle of the arc\r\n * @param cc an optional boolean value to specify if it should be drawn clockwise (`false`) or counter-clockwise (`true`). Default is clockwise.\r\n */\r\n static arc( ctx:CanvasRenderingContext2D, pt:PtLike, radius:number, startAngle:number, endAngle:number, cc?:boolean ) {\r\n if (!pt) return;\r\n ctx.beginPath();\r\n ctx.arc( pt[0], pt[1], radius, startAngle, endAngle, cc );\r\n }\r\n\r\n\r\n /**\r\n * Draw an arc.\r\n * @param pt center position\r\n * @param radius radius of the arc circle\r\n * @param startAngle start angle of the arc\r\n * @param endAngle end angle of the arc\r\n * @param cc an optional boolean value to specify if it should be drawn clockwise (`false`) or counter-clockwise (`true`). Default is clockwise.\r\n */\r\n arc( pt:PtLike, radius:number, startAngle:number, endAngle:number, cc?:boolean ):this {\r\n CanvasForm.arc( this._ctx, pt, radius, startAngle, endAngle, cc );\r\n this._paint();\r\n return this;\r\n }\r\n\r\n \r\n /**\r\n * A static function to draw a square.\r\n * @param ctx canvas rendering context\r\n * @param pt center position of the square\r\n * @param halfsize half size of the square\r\n */\r\n static square( ctx:CanvasRenderingContext2D, pt:PtLike, halfsize:number ) {\r\n if (!pt) return;\r\n let x1 = pt[0]-halfsize;\r\n let y1 = pt[1]-halfsize;\r\n let x2 = pt[0]+halfsize;\r\n let y2 = pt[1]+halfsize;\r\n \r\n // faster than using `rect`\r\n ctx.beginPath();\r\n ctx.moveTo( x1, y1 );\r\n ctx.lineTo( x1, y2 );\r\n ctx.lineTo( x2, y2 );\r\n ctx.lineTo( x2, y1 );\r\n ctx.closePath();\r\n }\r\n \r\n \r\n /**\r\n * Draw a square, given a center and its half-size.\r\n * @param pt center Pt\r\n * @param halfsize half-size\r\n */\r\n square( pt:PtLike, halfsize:number ) {\r\n CanvasForm.square( this._ctx, pt, halfsize );\r\n this._paint();\r\n return this;\r\n }\r\n\r\n \r\n /**\r\n * A static function to draw a line or polyline.\r\n * @param ctx canvas rendering context\r\n * @param pts a Group or an Iterable representing a line\r\n */\r\n static line( ctx:CanvasRenderingContext2D, pts:PtLikeIterable ) {\r\n if ( !Util.arrayCheck(pts) ) return;\r\n let i = 0;\r\n ctx.beginPath();\r\n for (let it of pts) {\r\n if (it) {\r\n if (i++ > 0) {\r\n ctx.lineTo(it[0], it[1]);\r\n } else {\r\n ctx.moveTo(it[0], it[1]);\r\n }\r\n }\r\n }\r\n }\r\n \r\n \r\n /**\r\n * Draw a line or polyline.\r\n * @param pts a Group or an Iterable representing a line\r\n */\r\n line( pts:PtLikeIterable ):this {\r\n CanvasForm.line( this._ctx, pts );\r\n this._paint();\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * A static function to draw a polygon.\r\n * @param ctx canvas rendering context\r\n * @param pts a Group or an Iterable representing a polygon\r\n */\r\n static polygon( ctx:CanvasRenderingContext2D, pts:PtLikeIterable ) {\r\n if ( !Util.arrayCheck(pts) ) return;\r\n CanvasForm.line( ctx, pts );\r\n ctx.closePath();\r\n }\r\n \r\n \r\n /**\r\n * Draw a polygon.\r\n * @param pts a Group or an Iterable representingg a polygon\r\n */\r\n polygon( pts:PtLikeIterable ):this {\r\n CanvasForm.polygon( this._ctx, pts );\r\n this._paint();\r\n return this;\r\n }\r\n \r\n \r\n /**\r\n * A static function to draw a rectangle.\r\n * @param ctx canvas rendering context\r\n * @param pts a Group or an Iterable with 2 Pt specifying the top-left and bottom-right positions.\r\n */\r\n static rect( ctx:CanvasRenderingContext2D, pts:PtLikeIterable ) {\r\n let p = Util.iterToArray( pts );\r\n if ( !Util.arrayCheck(p) ) return;\r\n ctx.beginPath();\r\n ctx.moveTo( p[0][0], p[0][1] );\r\n ctx.lineTo( p[0][0], p[1][1] );\r\n ctx.lineTo( p[1][0], p[1][1] );\r\n ctx.lineTo( p[1][0], p[0][1] );\r\n ctx.closePath();\r\n \r\n }\r\n \r\n \r\n /**\r\n * Draw a rectangle.\r\n * @param pts a Group or an Iterable with 2 Pt specifying the top-left and bottom-right positions.\r\n */\r\n rect( pts:PtLikeIterable ):this {\r\n CanvasForm.rect( this._ctx, pts );\r\n this._paint();\r\n return this;\r\n }\r\n\r\n\r\n /**\r\n * A static function to draw an image.\r\n * @param ctx canvas rendering context\r\n * @param img either an [Img](#link) instance or an [`CanvasImageSource`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasImageSource) instance (eg the image from ``, `