/// module BABYLON.GUI { export class Control { private _alpha = 1; private _alphaSet = false; private _zIndex = 0; public _root: Container; public _host: AdvancedDynamicTexture; public _currentMeasure = Measure.Empty(); private _fontFamily = "Arial"; private _fontStyle = ""; private _fontSize = new ValueAndUnit(18, ValueAndUnit.UNITMODE_PIXEL, false); private _font: string; public _width = new ValueAndUnit(1, ValueAndUnit.UNITMODE_PERCENTAGE, false); public _height = new ValueAndUnit(1, ValueAndUnit.UNITMODE_PERCENTAGE, false); private _lastMeasuredFont: string; protected _fontOffset: {ascent: number, height: number, descent: number}; private _color = ""; protected _horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; protected _verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; private _isDirty = true; protected _cachedParentMeasure = Measure.Empty(); private _paddingLeft = new ValueAndUnit(0); private _paddingRight = new ValueAndUnit(0); private _paddingTop = new ValueAndUnit(0); private _paddingBottom = new ValueAndUnit(0); public _left = new ValueAndUnit(0); public _top = new ValueAndUnit(0); private _scaleX = 1.0; private _scaleY = 1.0; private _rotation = 0; private _transformCenterX = 0.5; private _transformCenterY = 0.5; private _transformMatrix = Matrix2D.Identity(); private _invertTransformMatrix = Matrix2D.Identity(); private _transformedPosition = Vector2.Zero(); private _isMatrixDirty = true; private _cachedOffsetX: number; private _cachedOffsetY: number; private _isVisible = true; public _linkedMesh: AbstractMesh; private _fontSet = false; private _dummyVector2 = Vector2.Zero(); private _downCount = 0; private _enterCount = 0; private _doNotRender = false; public isHitTestVisible = true; public isPointerBlocker = false; public isFocusInvisible = false; protected _linkOffsetX = new ValueAndUnit(0); protected _linkOffsetY = new ValueAndUnit(0); // Properties public get typeName(): string { return this._getTypeName(); } /** * An event triggered when the pointer move over the control. * @type {BABYLON.Observable} */ public onPointerMoveObservable = new Observable(); /** * An event triggered when the pointer move out of the control. * @type {BABYLON.Observable} */ public onPointerOutObservable = new Observable(); /** * An event triggered when the pointer taps the control * @type {BABYLON.Observable} */ public onPointerDownObservable = new Observable(); /** * An event triggered when pointer up * @type {BABYLON.Observable} */ public onPointerUpObservable = new Observable(); /** * An event triggered when pointer enters the control * @type {BABYLON.Observable} */ public onPointerEnterObservable = new Observable(); /** * An event triggered when the control is marked as dirty * @type {BABYLON.Observable} */ public onDirtyObservable = new Observable(); /** * An event triggered after the control is drawn * @type {BABYLON.Observable} */ public onAfterDrawObservable = new Observable(); public get alpha(): number { return this._alpha; } public set alpha(value: number) { if (this._alpha === value) { return; } this._alphaSet = true; this._alpha = value; this._markAsDirty(); } public get scaleX(): number { return this._scaleX; } public set scaleX(value: number) { if (this._scaleX === value) { return; } this._scaleX = value; this._markAsDirty(); this._markMatrixAsDirty(); } public get scaleY(): number { return this._scaleY; } public set scaleY(value: number) { if (this._scaleY === value) { return; } this._scaleY = value; this._markAsDirty(); this._markMatrixAsDirty(); } public get rotation(): number { return this._rotation; } public set rotation(value: number) { if (this._rotation === value) { return; } this._rotation = value; this._markAsDirty(); this._markMatrixAsDirty(); } public get transformCenterY(): number { return this._transformCenterY; } public set transformCenterY(value: number) { if (this._transformCenterY === value) { return; } this._transformCenterY = value; this._markAsDirty(); this._markMatrixAsDirty(); } public get transformCenterX(): number { return this._transformCenterX; } public set transformCenterX(value: number) { if (this._transformCenterX === value) { return; } this._transformCenterX = value; this._markAsDirty(); this._markMatrixAsDirty(); } public get horizontalAlignment(): number { return this._horizontalAlignment; } public set horizontalAlignment(value: number) { if (this._horizontalAlignment === value) { return; } this._horizontalAlignment = value; this._markAsDirty(); } public get verticalAlignment(): number { return this._verticalAlignment; } public set verticalAlignment(value: number) { if (this._verticalAlignment === value) { return; } this._verticalAlignment = value; this._markAsDirty(); } public get width(): string | number { return this._width.toString(this._host); } public get widthInPixels(): number { return this._width.getValueInPixel(this._host, this._cachedParentMeasure.width); } public set width(value: string | number ) { if (this._width.toString(this._host) === value) { return; } if (this._width.fromString(value)) { this._markAsDirty(); } } public get height(): string | number { return this._height.toString(this._host); } public get heightInPixels(): number { return this._height.getValueInPixel(this._host, this._cachedParentMeasure.height); } public set height(value: string | number ) { if (this._height.toString(this._host) === value) { return; } if (this._height.fromString(value)) { this._markAsDirty(); } } public get fontFamily(): string { return this._fontFamily; } public set fontFamily(value: string) { if (this._fontFamily === value) { return; } this._fontFamily = value; this._fontSet = true; } public get fontStyle(): string { return this._fontStyle; } public set fontStyle(value: string) { if (this._fontStyle === value) { return; } this._fontStyle = value; this._fontSet = true; } public get fontSizeInPixels(): number { return this._fontSize.getValueInPixel(this._host, 100); } public get fontSize(): string | number { return this._fontSize.toString(this._host); } public set fontSize(value: string | number ) { if (this._fontSize.toString(this._host) === value) { return; } if (this._fontSize.fromString(value)) { this._markAsDirty(); this._fontSet = true; } } public get color(): string { return this._color; } public set color(value: string) { if (this._color === value) { return; } this._color = value; this._markAsDirty(); } public get zIndex(): number { return this._zIndex; } public set zIndex(value: number) { if (this.zIndex === value) { return; } this._zIndex = value; if (this._root) { this._root._reOrderControl(this); } } public get notRenderable(): boolean { return this._doNotRender; } public set notRenderable(value: boolean) { if (this._doNotRender === value) { return; } this._doNotRender = value; this._markAsDirty(); } public get isVisible(): boolean { return this._isVisible; } public set isVisible(value: boolean) { if (this._isVisible === value) { return; } this._isVisible = value; this._markAsDirty(); } public get isDirty(): boolean { return this._isDirty; } public get paddingLeft(): string | number { return this._paddingLeft.toString(this._host); } public get paddingLeftInPixels(): number { return this._paddingLeft.getValueInPixel(this._host, this._cachedParentMeasure.width); } public set paddingLeft(value: string | number ) { if (this._paddingLeft.fromString(value)) { this._markAsDirty(); } } public get paddingRight(): string | number { return this._paddingRight.toString(this._host); } public get paddingRightInPixels(): number { return this._paddingRight.getValueInPixel(this._host, this._cachedParentMeasure.width); } public set paddingRight(value: string | number ) { if (this._paddingRight.fromString(value)) { this._markAsDirty(); } } public get paddingTop(): string | number { return this._paddingTop.toString(this._host); } public get paddingTopInPixels(): number { return this._paddingTop.getValueInPixel(this._host, this._cachedParentMeasure.height); } public set paddingTop(value: string | number ) { if (this._paddingTop.fromString(value)) { this._markAsDirty(); } } public get paddingBottom(): string | number { return this._paddingBottom.toString(this._host); } public get paddingBottomInPixels(): number { return this._paddingBottom.getValueInPixel(this._host, this._cachedParentMeasure.height); } public set paddingBottom(value: string | number ) { if (this._paddingBottom.fromString(value)) { this._markAsDirty(); } } public get left(): string | number { return this._left.toString(this._host); } public get leftInPixels(): number { return this._left.getValueInPixel(this._host, this._cachedParentMeasure.width); } public set left(value: string | number ) { if (this._left.fromString(value)) { this._markAsDirty(); } } public get top(): string | number { return this._top.toString(this._host); } public get topInPixels(): number { return this._top.getValueInPixel(this._host, this._cachedParentMeasure.height); } public set top(value: string | number ) { if (this._top.fromString(value)) { this._markAsDirty(); } } public get linkOffsetX(): string | number { return this._linkOffsetX.toString(this._host); } public get linkOffsetXInPixels(): number { return this._linkOffsetX.getValueInPixel(this._host, this._cachedParentMeasure.width); } public set linkOffsetX(value: string | number ) { if (this._linkOffsetX.fromString(value)) { this._markAsDirty(); } } public get linkOffsetY(): string | number { return this._linkOffsetY.toString(this._host); } public get linkOffsetYInPixels(): number { return this._linkOffsetY.getValueInPixel(this._host, this._cachedParentMeasure.height); } public set linkOffsetY(value: string | number ) { if (this._linkOffsetY.fromString(value)) { this._markAsDirty(); } } public get centerX(): number { return this._currentMeasure.left + this._currentMeasure.width / 2; } public get centerY(): number { return this._currentMeasure.top + this._currentMeasure.height / 2; } // Functions constructor(public name?: string) { } protected _getTypeName(): string { return "Control"; } public getLocalCoordinates(globalCoordinates: Vector2): Vector2 { var result = Vector2.Zero(); this.getLocalCoordinatesToRef(globalCoordinates, result); return result; } public getLocalCoordinatesToRef(globalCoordinates: Vector2, result: Vector2): Control { result.x = globalCoordinates.x - this._currentMeasure.left; result.y = globalCoordinates.y - this._currentMeasure.top; return this; } public getParentLocalCoordinates(globalCoordinates: Vector2): Vector2 { var result = Vector2.Zero(); result.x = globalCoordinates.x - this._cachedParentMeasure.left; result.y = globalCoordinates.y - this._cachedParentMeasure.top; return result; } public moveToVector3(position: Vector3, scene: Scene): void { if (!this._host || this._root !== this._host._rootContainer) { Tools.Error("Cannot move a control to a vector3 if the control is not at root level"); return; } this.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; this.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP; var engine = scene.getEngine(); var globalViewport = this._host._getGlobalViewport(scene); var projectedPosition = Vector3.Project(position, Matrix.Identity(), scene.getTransformMatrix(), globalViewport); this._moveToProjectedPosition(projectedPosition); if (projectedPosition.z < 0 || projectedPosition.z > 1) { this.notRenderable = true; return; } this.notRenderable = false; } public linkWithMesh(mesh: AbstractMesh): void { if (!this._host || this._root !== this._host._rootContainer) { Tools.Error("Cannot link a control to a mesh if the control is not at root level"); return; } var index = this._host._linkedControls.indexOf(this); if (index !== -1) { this._linkedMesh = mesh; if (!mesh) { this._host._linkedControls.splice(index, 1); } return; } this.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; this.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP; this._linkedMesh = mesh; this._host._linkedControls.push(this); } public _moveToProjectedPosition(projectedPosition: Vector3): void { this.left = ((projectedPosition.x + this._linkOffsetX.getValue(this._host)) - this._currentMeasure.width / 2) + "px"; this.top = ((projectedPosition.y + this._linkOffsetY.getValue(this._host)) - this._currentMeasure.height / 2) + "px"; this._left.ignoreAdaptiveScaling = true; this._top.ignoreAdaptiveScaling = true; } public _markMatrixAsDirty(): void { this._isMatrixDirty = true; this._markAsDirty(); } public _markAsDirty(): void { this._isDirty = true; if (!this._host) { return; // Not yet connected } this._host.markAsDirty(); } public _markAllAsDirty(): void { this._markAsDirty(); if (this._font) { this._prepareFont(); } } public _link(root: Container, host: AdvancedDynamicTexture): void { this._root = root; this._host = host; } protected _transform(context: CanvasRenderingContext2D): void { if (!this._isMatrixDirty && this._scaleX === 1 && this._scaleY ===1 && this._rotation === 0) { return; } // postTranslate var offsetX = this._currentMeasure.width * this._transformCenterX + this._currentMeasure.left; var offsetY = this._currentMeasure.height * this._transformCenterY + this._currentMeasure.top; context.translate(offsetX, offsetY); // rotate context.rotate(this._rotation); // scale context.scale(this._scaleX, this._scaleY); // preTranslate context.translate(-offsetX, -offsetY); // Need to update matrices? if (this._isMatrixDirty || this._cachedOffsetX !== offsetX || this._cachedOffsetY !== offsetY) { this._cachedOffsetX = offsetX; this._cachedOffsetY = offsetY; this._isMatrixDirty = false; Matrix2D.ComposeToRef(-offsetX, -offsetY, this._rotation, this._scaleX, this._scaleY, this._root ? this._root._transformMatrix : null, this._transformMatrix); this._transformMatrix.invertToRef(this._invertTransformMatrix); } } protected _applyStates(context: CanvasRenderingContext2D): void { if (this._fontSet) { this._prepareFont(); this._fontSet = false; } if (this._font) { context.font = this._font; } if (this._color) { context.fillStyle = this._color; } if (this._alphaSet) { context.globalAlpha = this._alpha; } } protected _processMeasures(parentMeasure: Measure, context: CanvasRenderingContext2D): boolean { if (this._isDirty || !this._cachedParentMeasure.isEqualsTo(parentMeasure)) { this._isDirty = false; this._currentMeasure.copyFrom(parentMeasure); // Let children take some pre-measurement actions this._preMeasure(parentMeasure, context); this._measure(); this._computeAlignment(parentMeasure, context); // Convert to int values this._currentMeasure.left = this._currentMeasure.left | 0; this._currentMeasure.top = this._currentMeasure.top | 0; this._currentMeasure.width = this._currentMeasure.width | 0; this._currentMeasure.height = this._currentMeasure.height | 0; // Let children add more features this._additionalProcessing(parentMeasure, context); this._cachedParentMeasure.copyFrom(parentMeasure); if (this.onDirtyObservable.hasObservers()) { this.onDirtyObservable.notifyObservers(this); } } if (this._currentMeasure.left > parentMeasure.left + parentMeasure.width) { return false; } if (this._currentMeasure.left + this._currentMeasure.width < parentMeasure.left) { return false; } if (this._currentMeasure.top > parentMeasure.top + parentMeasure.height) { return false; } if (this._currentMeasure.top + this._currentMeasure.height < parentMeasure.top) { return false; } // Transform this._transform(context); // Clip this._clip(context); context.clip(); return true; } protected _clip( context: CanvasRenderingContext2D) { context.beginPath(); context.rect(this._currentMeasure.left ,this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height); } public _measure(): void { // Width / Height if (this._width.isPixel) { this._currentMeasure.width = this._width.getValue(this._host); } else { this._currentMeasure.width *= this._width.getValue(this._host); } if (this._height.isPixel) { this._currentMeasure.height = this._height.getValue(this._host); } else { this._currentMeasure.height *= this._height.getValue(this._host); } } protected _computeAlignment(parentMeasure: Measure, context: CanvasRenderingContext2D): void { var width = this._currentMeasure.width; var height = this._currentMeasure.height; var parentWidth = parentMeasure.width; var parentHeight = parentMeasure.height; // Left / top var x = 0; var y = 0; switch (this.horizontalAlignment) { case Control.HORIZONTAL_ALIGNMENT_LEFT: x = 0 break; case Control.HORIZONTAL_ALIGNMENT_RIGHT: x = parentWidth - width; break; case Control.HORIZONTAL_ALIGNMENT_CENTER: x = (parentWidth - width) / 2; break; } switch (this.verticalAlignment) { case Control.VERTICAL_ALIGNMENT_TOP: y = 0; break; case Control.VERTICAL_ALIGNMENT_BOTTOM: y = parentHeight - height; break; case Control.VERTICAL_ALIGNMENT_CENTER: y = (parentHeight - height) / 2; break; } if (this._paddingLeft.isPixel) { this._currentMeasure.left += this._paddingLeft.getValue(this._host); this._currentMeasure.width -= this._paddingLeft.getValue(this._host); } else { this._currentMeasure.left += parentWidth * this._paddingLeft.getValue(this._host); this._currentMeasure.width -= parentWidth * this._paddingLeft.getValue(this._host); } if (this._paddingRight.isPixel) { this._currentMeasure.width -= this._paddingRight.getValue(this._host); } else { this._currentMeasure.width -= parentWidth * this._paddingRight.getValue(this._host); } if (this._paddingTop.isPixel) { this._currentMeasure.top += this._paddingTop.getValue(this._host); this._currentMeasure.height -= this._paddingTop.getValue(this._host); } else { this._currentMeasure.top += parentHeight * this._paddingTop.getValue(this._host); this._currentMeasure.height -= parentHeight * this._paddingTop.getValue(this._host); } if (this._paddingBottom.isPixel) { this._currentMeasure.height -= this._paddingBottom.getValue(this._host); } else { this._currentMeasure.height -= parentHeight * this._paddingBottom.getValue(this._host); } if (this._left.isPixel) { this._currentMeasure.left += this._left.getValue(this._host); } else { this._currentMeasure.left += parentWidth * this._left.getValue(this._host); } if (this._top.isPixel) { this._currentMeasure.top += this._top.getValue(this._host); } else { this._currentMeasure.top += parentHeight * this._top.getValue(this._host); } this._currentMeasure.left += x; this._currentMeasure.top += y; } protected _preMeasure(parentMeasure: Measure, context: CanvasRenderingContext2D): void { // Do nothing } protected _additionalProcessing(parentMeasure: Measure, context: CanvasRenderingContext2D): void { // Do nothing } public _draw(parentMeasure: Measure, context: CanvasRenderingContext2D): void { // Do nothing } public contains(x: number, y: number) : boolean { // Invert transform this._invertTransformMatrix.transformCoordinates(x, y, this._transformedPosition); x = this._transformedPosition.x; y = this._transformedPosition.y; // Check if (x < this._currentMeasure.left) { return false; } if (x > this._currentMeasure.left + this._currentMeasure.width) { return false; } if (y < this._currentMeasure.top) { return false; } if (y > this._currentMeasure.top + this._currentMeasure.height) { return false; } if (this.isPointerBlocker) { this._host._shouldBlockPointer = true; } return true; } public _processPicking(x: number, y: number, type: number, buttonIndex: number): boolean { if (!this.isHitTestVisible || !this.isVisible || this._doNotRender) { return false; } if (!this.contains(x, y)) { return false; } this._processObservables(type, x, y, buttonIndex); return true; } protected _onPointerMove(coordinates: Vector2): void { if (this.onPointerMoveObservable.hasObservers()) { this.onPointerMoveObservable.notifyObservers(coordinates); } } protected _onPointerEnter(): boolean { if (this._enterCount !== 0) { return false; } this._enterCount++; if (this.onPointerEnterObservable.hasObservers()) { this.onPointerEnterObservable.notifyObservers(this); } return true; } public _onPointerOut(): void { this._enterCount = 0; if (this.onPointerOutObservable.hasObservers()) { this.onPointerOutObservable.notifyObservers(this); } } protected _onPointerDown(coordinates: Vector2, buttonIndex: number): boolean { if (this._downCount !== 0) { return false; } this._downCount++; if (this.onPointerDownObservable.hasObservers()) { this.onPointerDownObservable.notifyObservers(new Vector2WithInfo(coordinates, buttonIndex)); } return true; } protected _onPointerUp(coordinates: Vector2, buttonIndex: number): void { this._downCount = 0; if (this.onPointerUpObservable.hasObservers()) { this.onPointerUpObservable.notifyObservers(new Vector2WithInfo(coordinates, buttonIndex)); } } public forcePointerUp() { this._onPointerUp(Vector2.Zero(), 0); } public _processObservables(type: number, x: number, y: number, buttonIndex: number): boolean { this._dummyVector2.copyFromFloats(x, y); if (type === BABYLON.PointerEventTypes.POINTERMOVE) { this._onPointerMove(this._dummyVector2); var previousControlOver = this._host._lastControlOver; if (previousControlOver && previousControlOver !== this) { previousControlOver._onPointerOut(); } if (previousControlOver !== this) { this._onPointerEnter(); } this._host._lastControlOver = this; return true; } if (type === BABYLON.PointerEventTypes.POINTERDOWN) { this._onPointerDown(this._dummyVector2, buttonIndex); this._host._lastControlDown = this; this._host._lastPickedControl = this; return true; } if (type === BABYLON.PointerEventTypes.POINTERUP) { if (this._host._lastControlDown) { this._host._lastControlDown._onPointerUp(this._dummyVector2, buttonIndex); } this._host._lastControlDown = null; return true; } return false; } private _prepareFont() { if (!this._font && !this._fontSet) { return; } this._font = this._fontStyle + " " + this._fontSize.getValue(this._host) + "px " + this._fontFamily; this._fontOffset = Control._GetFontOffset(this._font); } public dispose() { this.onDirtyObservable.clear(); this.onAfterDrawObservable.clear(); this.onPointerDownObservable.clear(); this.onPointerEnterObservable.clear(); this.onPointerMoveObservable.clear(); this.onPointerOutObservable.clear(); this.onPointerUpObservable.clear(); if (this._root) { this._root.removeControl(this); this._root = null; } var index = this._host._linkedControls.indexOf(this); if (index > -1) { this.linkWithMesh(null); } } // Statics private static _HORIZONTAL_ALIGNMENT_LEFT = 0; private static _HORIZONTAL_ALIGNMENT_RIGHT = 1; private static _HORIZONTAL_ALIGNMENT_CENTER = 2; private static _VERTICAL_ALIGNMENT_TOP = 0; private static _VERTICAL_ALIGNMENT_BOTTOM = 1; private static _VERTICAL_ALIGNMENT_CENTER = 2; public static get HORIZONTAL_ALIGNMENT_LEFT(): number { return Control._HORIZONTAL_ALIGNMENT_LEFT; } public static get HORIZONTAL_ALIGNMENT_RIGHT(): number { return Control._HORIZONTAL_ALIGNMENT_RIGHT; } public static get HORIZONTAL_ALIGNMENT_CENTER(): number { return Control._HORIZONTAL_ALIGNMENT_CENTER; } public static get VERTICAL_ALIGNMENT_TOP(): number { return Control._VERTICAL_ALIGNMENT_TOP; } public static get VERTICAL_ALIGNMENT_BOTTOM(): number { return Control._VERTICAL_ALIGNMENT_BOTTOM; } public static get VERTICAL_ALIGNMENT_CENTER(): number { return Control._VERTICAL_ALIGNMENT_CENTER; } private static _FontHeightSizes = {}; public static _GetFontOffset(font: string): {ascent: number, height: number, descent: number} { if (Control._FontHeightSizes[font]) { return Control._FontHeightSizes[font]; } var text = document.createElement("span"); text.innerHTML = "Hg"; text.style.font = font; var block = document.createElement("div"); block.style.display = "inline-block"; block.style.width = "1px"; block.style.height = "0px"; block.style.verticalAlign = "bottom"; var div = document.createElement("div"); div.appendChild(text); div.appendChild(block); document.body.appendChild(div); var fontAscent = 0; var fontHeight = 0; try { fontHeight = block.getBoundingClientRect().top - text.getBoundingClientRect().top; block.style.verticalAlign = "baseline"; fontAscent = block.getBoundingClientRect().top - text.getBoundingClientRect().top; } finally { document.body.removeChild(div); } var result = { ascent: fontAscent, height: fontHeight, descent: fontHeight - fontAscent }; Control._FontHeightSizes[font] = result; return result; }; public static AddHeader(control: Control, text: string, size: string | number, options: { isHorizontal: boolean, controlFirst: boolean }): StackPanel { let panel = new BABYLON.GUI.StackPanel("panel"); let isHorizontal = options ? options.isHorizontal : true; let controlFirst = options ? options.controlFirst : true; panel.isVertical = !isHorizontal; let header = new BABYLON.GUI.TextBlock("header"); header.text = text; header.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; if (isHorizontal) { header.width = size; } else { header.height = size; } if (controlFirst) { panel.addControl(control); panel.addControl(header); header.paddingLeft = "5px"; } else { panel.addControl(header); panel.addControl(control); header.paddingRight = "5px"; } return panel; } protected static drawEllipse(x:number, y:number, width:number, height:number, context:CanvasRenderingContext2D):void{ context.translate(x, y); context.scale(width, height); context.beginPath(); context.arc(0, 0, 1, 0, 2 * Math.PI); context.closePath(); context.scale(1/width, 1/height); context.translate(-x, -y); } } }