image.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. import { Nullable } from "babylonjs/types";
  2. import { Observable } from "babylonjs/Misc/observable";
  3. import { Tools } from "babylonjs/Misc/tools";
  4. import { Control } from "./control";
  5. import { Measure } from "../measure";
  6. import { _TypeStore } from "babylonjs/Misc/typeStore";
  7. import { serialize } from "babylonjs/Misc/decorators";
  8. /**
  9. * Class used to create 2D images
  10. */
  11. export class Image extends Control {
  12. private _workingCanvas: Nullable<HTMLCanvasElement> = null;
  13. private _domImage: HTMLImageElement;
  14. private _imageWidth: number;
  15. private _imageHeight: number;
  16. private _loaded = false;
  17. private _stretch = Image.STRETCH_FILL;
  18. private _source: Nullable<string>;
  19. private _autoScale = false;
  20. private _sourceLeft = 0;
  21. private _sourceTop = 0;
  22. private _sourceWidth = 0;
  23. private _sourceHeight = 0;
  24. private _svgAttributesComputationCompleted: boolean = false;
  25. private _isSVG: boolean = false;
  26. private _cellWidth: number = 0;
  27. private _cellHeight: number = 0;
  28. private _cellId: number = -1;
  29. private _populateNinePatchSlicesFromImage = false;
  30. private _sliceLeft: number;
  31. private _sliceRight: number;
  32. private _sliceTop: number;
  33. private _sliceBottom: number;
  34. private _detectPointerOnOpaqueOnly: boolean;
  35. private _imageDataCache: {
  36. data: Uint8ClampedArray | null;
  37. key: string;
  38. } = { data: null, key: "" };
  39. /**
  40. * Observable notified when the content is loaded
  41. */
  42. public onImageLoadedObservable = new Observable<Image>();
  43. /**
  44. * Observable notified when _sourceLeft, _sourceTop, _sourceWidth and _sourceHeight are computed
  45. */
  46. public onSVGAttributesComputedObservable = new Observable<Image>();
  47. /**
  48. * Gets a boolean indicating that the content is loaded
  49. */
  50. public get isLoaded(): boolean {
  51. return this._loaded;
  52. }
  53. /**
  54. * Gets or sets a boolean indicating if nine patch slices (left, top, right, bottom) should be read from image data
  55. */
  56. @serialize()
  57. public get populateNinePatchSlicesFromImage(): boolean {
  58. return this._populateNinePatchSlicesFromImage;
  59. }
  60. public set populateNinePatchSlicesFromImage(value: boolean) {
  61. if (this._populateNinePatchSlicesFromImage === value) {
  62. return;
  63. }
  64. this._populateNinePatchSlicesFromImage = value;
  65. if (this._populateNinePatchSlicesFromImage && this._loaded) {
  66. this._extractNinePatchSliceDataFromImage();
  67. }
  68. }
  69. /**
  70. * Gets or sets a boolean indicating if pointers should only be validated on pixels with alpha > 0.
  71. * Beware using this as this will comsume more memory as the image has to be stored twice
  72. */
  73. @serialize()
  74. public get detectPointerOnOpaqueOnly(): boolean {
  75. return this._detectPointerOnOpaqueOnly;
  76. }
  77. public set detectPointerOnOpaqueOnly(value: boolean) {
  78. if (this._detectPointerOnOpaqueOnly === value) {
  79. return;
  80. }
  81. this._detectPointerOnOpaqueOnly = value;
  82. }
  83. /**
  84. * Gets or sets the left value for slicing (9-patch)
  85. */
  86. @serialize()
  87. public get sliceLeft(): number {
  88. return this._sliceLeft;
  89. }
  90. public set sliceLeft(value: number) {
  91. if (this._sliceLeft === value) {
  92. return;
  93. }
  94. this._sliceLeft = value;
  95. this._markAsDirty();
  96. }
  97. /**
  98. * Gets or sets the right value for slicing (9-patch)
  99. */
  100. @serialize()
  101. public get sliceRight(): number {
  102. return this._sliceRight;
  103. }
  104. public set sliceRight(value: number) {
  105. if (this._sliceRight === value) {
  106. return;
  107. }
  108. this._sliceRight = value;
  109. this._markAsDirty();
  110. }
  111. /**
  112. * Gets or sets the top value for slicing (9-patch)
  113. */
  114. @serialize()
  115. public get sliceTop(): number {
  116. return this._sliceTop;
  117. }
  118. public set sliceTop(value: number) {
  119. if (this._sliceTop === value) {
  120. return;
  121. }
  122. this._sliceTop = value;
  123. this._markAsDirty();
  124. }
  125. /**
  126. * Gets or sets the bottom value for slicing (9-patch)
  127. */
  128. @serialize()
  129. public get sliceBottom(): number {
  130. return this._sliceBottom;
  131. }
  132. public set sliceBottom(value: number) {
  133. if (this._sliceBottom === value) {
  134. return;
  135. }
  136. this._sliceBottom = value;
  137. this._markAsDirty();
  138. }
  139. /**
  140. * Gets or sets the left coordinate in the source image
  141. */
  142. @serialize()
  143. public get sourceLeft(): number {
  144. return this._sourceLeft;
  145. }
  146. public set sourceLeft(value: number) {
  147. if (this._sourceLeft === value) {
  148. return;
  149. }
  150. this._sourceLeft = value;
  151. this._markAsDirty();
  152. }
  153. /**
  154. * Gets or sets the top coordinate in the source image
  155. */
  156. @serialize()
  157. public get sourceTop(): number {
  158. return this._sourceTop;
  159. }
  160. public set sourceTop(value: number) {
  161. if (this._sourceTop === value) {
  162. return;
  163. }
  164. this._sourceTop = value;
  165. this._markAsDirty();
  166. }
  167. /**
  168. * Gets or sets the width to capture in the source image
  169. */
  170. @serialize()
  171. public get sourceWidth(): number {
  172. return this._sourceWidth;
  173. }
  174. public set sourceWidth(value: number) {
  175. if (this._sourceWidth === value) {
  176. return;
  177. }
  178. this._sourceWidth = value;
  179. this._markAsDirty();
  180. }
  181. /**
  182. * Gets or sets the height to capture in the source image
  183. */
  184. @serialize()
  185. public get sourceHeight(): number {
  186. return this._sourceHeight;
  187. }
  188. public set sourceHeight(value: number) {
  189. if (this._sourceHeight === value) {
  190. return;
  191. }
  192. this._sourceHeight = value;
  193. this._markAsDirty();
  194. }
  195. /** Indicates if the format of the image is SVG */
  196. public get isSVG(): boolean {
  197. return this._isSVG;
  198. }
  199. /** Gets the status of the SVG attributes computation (sourceLeft, sourceTop, sourceWidth, sourceHeight) */
  200. public get svgAttributesComputationCompleted(): boolean {
  201. return this._svgAttributesComputationCompleted;
  202. }
  203. /**
  204. * Gets or sets a boolean indicating if the image can force its container to adapt its size
  205. * @see https://doc.babylonjs.com/how_to/gui#image
  206. */
  207. @serialize()
  208. public get autoScale(): boolean {
  209. return this._autoScale;
  210. }
  211. public set autoScale(value: boolean) {
  212. if (this._autoScale === value) {
  213. return;
  214. }
  215. this._autoScale = value;
  216. if (value && this._loaded) {
  217. this.synchronizeSizeWithContent();
  218. }
  219. }
  220. /** Gets or sets the streching mode used by the image */
  221. @serialize()
  222. public get stretch(): number {
  223. return this._stretch;
  224. }
  225. public set stretch(value: number) {
  226. if (this._stretch === value) {
  227. return;
  228. }
  229. this._stretch = value;
  230. this._markAsDirty();
  231. }
  232. /** @hidden */
  233. public _rotate90(n: number, preserveProperties: boolean = false): Image {
  234. let canvas = document.createElement("canvas");
  235. const context = canvas.getContext("2d")!;
  236. const width = this._domImage.width;
  237. const height = this._domImage.height;
  238. canvas.width = height;
  239. canvas.height = width;
  240. context.translate(canvas.width / 2, canvas.height / 2);
  241. context.rotate((n * Math.PI) / 2);
  242. context.drawImage(this._domImage, 0, 0, width, height, -width / 2, -height / 2, width, height);
  243. const dataUrl: string = canvas.toDataURL("image/jpg");
  244. const rotatedImage = new Image(this.name + "rotated", dataUrl);
  245. if (preserveProperties) {
  246. rotatedImage._stretch = this._stretch;
  247. rotatedImage._autoScale = this._autoScale;
  248. rotatedImage._cellId = this._cellId;
  249. rotatedImage._cellWidth = n % 1 ? this._cellHeight : this._cellWidth;
  250. rotatedImage._cellHeight = n % 1 ? this._cellWidth : this._cellHeight;
  251. }
  252. this._handleRotationForSVGImage(this, rotatedImage, n);
  253. this._imageDataCache.data = null;
  254. return rotatedImage;
  255. }
  256. private _handleRotationForSVGImage(srcImage: Image, dstImage: Image, n: number): void {
  257. if (!srcImage._isSVG) {
  258. return;
  259. }
  260. if (srcImage._svgAttributesComputationCompleted) {
  261. this._rotate90SourceProperties(srcImage, dstImage, n);
  262. this._markAsDirty();
  263. } else {
  264. srcImage.onSVGAttributesComputedObservable.addOnce(() => {
  265. this._rotate90SourceProperties(srcImage, dstImage, n);
  266. this._markAsDirty();
  267. });
  268. }
  269. }
  270. private _rotate90SourceProperties(srcImage: Image, dstImage: Image, n: number): void {
  271. let srcLeft = srcImage.sourceLeft,
  272. srcTop = srcImage.sourceTop,
  273. srcWidth = srcImage.domImage.width,
  274. srcHeight = srcImage.domImage.height;
  275. let dstLeft = srcLeft,
  276. dstTop = srcTop,
  277. dstWidth = srcImage.sourceWidth,
  278. dstHeight = srcImage.sourceHeight;
  279. if (n != 0) {
  280. let mult = n < 0 ? -1 : 1;
  281. n = n % 4;
  282. for (let i = 0; i < Math.abs(n); ++i) {
  283. dstLeft = -(srcTop - srcHeight / 2) * mult + srcHeight / 2;
  284. dstTop = (srcLeft - srcWidth / 2) * mult + srcWidth / 2;
  285. [dstWidth, dstHeight] = [dstHeight, dstWidth];
  286. if (n < 0) {
  287. dstTop -= dstHeight;
  288. } else {
  289. dstLeft -= dstWidth;
  290. }
  291. srcLeft = dstLeft;
  292. srcTop = dstTop;
  293. [srcWidth, srcHeight] = [srcHeight, srcWidth];
  294. }
  295. }
  296. dstImage.sourceLeft = dstLeft;
  297. dstImage.sourceTop = dstTop;
  298. dstImage.sourceWidth = dstWidth;
  299. dstImage.sourceHeight = dstHeight;
  300. }
  301. /**
  302. * Gets or sets the internal DOM image used to render the control
  303. */
  304. public set domImage(value: HTMLImageElement) {
  305. this._domImage = value;
  306. this._loaded = false;
  307. this._imageDataCache.data = null;
  308. if (this._domImage.width) {
  309. this._onImageLoaded();
  310. } else {
  311. this._domImage.onload = () => {
  312. this._onImageLoaded();
  313. };
  314. }
  315. }
  316. public get domImage(): HTMLImageElement {
  317. return this._domImage;
  318. }
  319. private _onImageLoaded(): void {
  320. this._imageDataCache.data = null;
  321. this._imageWidth = this._domImage.width;
  322. this._imageHeight = this._domImage.height;
  323. this._loaded = true;
  324. if (this._populateNinePatchSlicesFromImage) {
  325. this._extractNinePatchSliceDataFromImage();
  326. }
  327. if (this._autoScale) {
  328. this.synchronizeSizeWithContent();
  329. }
  330. this.onImageLoadedObservable.notifyObservers(this);
  331. this._markAsDirty();
  332. }
  333. private _extractNinePatchSliceDataFromImage() {
  334. if (!this._workingCanvas) {
  335. this._workingCanvas = document.createElement("canvas");
  336. }
  337. const canvas = this._workingCanvas;
  338. const context = canvas.getContext("2d")!;
  339. const width = this._domImage.width;
  340. const height = this._domImage.height;
  341. canvas.width = width;
  342. canvas.height = height;
  343. context.drawImage(this._domImage, 0, 0, width, height);
  344. const imageData = context.getImageData(0, 0, width, height);
  345. // Left and right
  346. this._sliceLeft = -1;
  347. this._sliceRight = -1;
  348. for (var x = 0; x < width; x++) {
  349. const alpha = imageData.data[x * 4 + 3];
  350. if (alpha > 127 && this._sliceLeft === -1) {
  351. this._sliceLeft = x;
  352. continue;
  353. }
  354. if (alpha < 127 && this._sliceLeft > -1) {
  355. this._sliceRight = x;
  356. break;
  357. }
  358. }
  359. // top and bottom
  360. this._sliceTop = -1;
  361. this._sliceBottom = -1;
  362. for (var y = 0; y < height; y++) {
  363. const alpha = imageData.data[y * width * 4 + 3];
  364. if (alpha > 127 && this._sliceTop === -1) {
  365. this._sliceTop = y;
  366. continue;
  367. }
  368. if (alpha < 127 && this._sliceTop > -1) {
  369. this._sliceBottom = y;
  370. break;
  371. }
  372. }
  373. }
  374. /**
  375. * Gets the image source url
  376. */
  377. @serialize()
  378. public get source() {
  379. return this._source;
  380. }
  381. /**
  382. * Gets or sets image source url
  383. */
  384. public set source(value: Nullable<string>) {
  385. if (this._source === value) {
  386. return;
  387. }
  388. this._loaded = false;
  389. this._source = value;
  390. this._imageDataCache.data = null;
  391. if (value) {
  392. value = this._svgCheck(value);
  393. }
  394. this._domImage = document.createElement("img");
  395. this._domImage.onload = () => {
  396. this._onImageLoaded();
  397. };
  398. if (value) {
  399. Tools.SetCorsBehavior(value, this._domImage);
  400. this._domImage.src = value;
  401. }
  402. }
  403. /**
  404. * Checks for svg document with icon id present
  405. */
  406. private _svgCheck(value: string): string {
  407. if (window.SVGSVGElement && value.search(/.svg#/gi) !== -1 && value.indexOf("#") === value.lastIndexOf("#")) {
  408. this._isSVG = true;
  409. var svgsrc = value.split("#")[0];
  410. var elemid = value.split("#")[1];
  411. // check if object alr exist in document
  412. var svgExist = <HTMLObjectElement>document.body.querySelector('object[data="' + svgsrc + '"]');
  413. if (svgExist) {
  414. var svgDoc = svgExist.contentDocument;
  415. // get viewbox width and height, get svg document width and height in px
  416. if (svgDoc && svgDoc.documentElement) {
  417. var vb = svgDoc.documentElement.getAttribute("viewBox");
  418. var docwidth = Number(svgDoc.documentElement.getAttribute("width"));
  419. var docheight = Number(svgDoc.documentElement.getAttribute("height"));
  420. var elem = <SVGGraphicsElement>(<unknown>svgDoc.getElementById(elemid));
  421. if (elem && vb && docwidth && docheight) {
  422. this._getSVGAttribs(svgExist, elemid);
  423. return value;
  424. }
  425. }
  426. // wait for object to load
  427. svgExist.addEventListener("load", () => {
  428. this._getSVGAttribs(svgExist, elemid);
  429. });
  430. } else {
  431. // create document object
  432. var svgImage = document.createElement("object");
  433. svgImage.data = svgsrc;
  434. svgImage.type = "image/svg+xml";
  435. svgImage.width = "0%";
  436. svgImage.height = "0%";
  437. document.body.appendChild(svgImage);
  438. // when the object has loaded, get the element attribs
  439. svgImage.onload = () => {
  440. var svgobj = <HTMLObjectElement>document.body.querySelector('object[data="' + svgsrc + '"]');
  441. if (svgobj) {
  442. this._getSVGAttribs(svgobj, elemid);
  443. }
  444. };
  445. }
  446. return svgsrc;
  447. } else {
  448. return value;
  449. }
  450. }
  451. /**
  452. * Sets sourceLeft, sourceTop, sourceWidth, sourceHeight automatically
  453. * given external svg file and icon id
  454. */
  455. private _getSVGAttribs(svgsrc: HTMLObjectElement, elemid: string) {
  456. var svgDoc = svgsrc.contentDocument;
  457. // get viewbox width and height, get svg document width and height in px
  458. if (svgDoc && svgDoc.documentElement) {
  459. var vb = svgDoc.documentElement.getAttribute("viewBox");
  460. var docwidth = Number(svgDoc.documentElement.getAttribute("width"));
  461. var docheight = Number(svgDoc.documentElement.getAttribute("height"));
  462. // get element bbox and matrix transform
  463. var elem = svgDoc.getElementById(elemid) as Nullable<SVGGraphicsElement>;
  464. if (vb && docwidth && docheight && elem) {
  465. var vb_width = Number(vb.split(" ")[2]);
  466. var vb_height = Number(vb.split(" ")[3]);
  467. var elem_bbox = elem.getBBox();
  468. var elem_matrix_a = 1;
  469. var elem_matrix_d = 1;
  470. var elem_matrix_e = 0;
  471. var elem_matrix_f = 0;
  472. if (elem.transform && elem.transform.baseVal.consolidate()) {
  473. elem_matrix_a = elem.transform.baseVal.consolidate().matrix.a;
  474. elem_matrix_d = elem.transform.baseVal.consolidate().matrix.d;
  475. elem_matrix_e = elem.transform.baseVal.consolidate().matrix.e;
  476. elem_matrix_f = elem.transform.baseVal.consolidate().matrix.f;
  477. }
  478. // compute source coordinates and dimensions
  479. this.sourceLeft = ((elem_matrix_a * elem_bbox.x + elem_matrix_e) * docwidth) / vb_width;
  480. this.sourceTop = ((elem_matrix_d * elem_bbox.y + elem_matrix_f) * docheight) / vb_height;
  481. this.sourceWidth = elem_bbox.width * elem_matrix_a * (docwidth / vb_width);
  482. this.sourceHeight = elem_bbox.height * elem_matrix_d * (docheight / vb_height);
  483. this._svgAttributesComputationCompleted = true;
  484. this.onSVGAttributesComputedObservable.notifyObservers(this);
  485. }
  486. }
  487. }
  488. /**
  489. * Gets or sets the cell width to use when animation sheet is enabled
  490. * @see https://doc.babylonjs.com/how_to/gui#image
  491. */
  492. get cellWidth(): number {
  493. return this._cellWidth;
  494. }
  495. set cellWidth(value: number) {
  496. if (this._cellWidth === value) {
  497. return;
  498. }
  499. this._cellWidth = value;
  500. this._markAsDirty();
  501. }
  502. /**
  503. * Gets or sets the cell height to use when animation sheet is enabled
  504. * @see https://doc.babylonjs.com/how_to/gui#image
  505. */
  506. get cellHeight(): number {
  507. return this._cellHeight;
  508. }
  509. set cellHeight(value: number) {
  510. if (this._cellHeight === value) {
  511. return;
  512. }
  513. this._cellHeight = value;
  514. this._markAsDirty();
  515. }
  516. /**
  517. * Gets or sets the cell id to use (this will turn on the animation sheet mode)
  518. * @see https://doc.babylonjs.com/how_to/gui#image
  519. */
  520. get cellId(): number {
  521. return this._cellId;
  522. }
  523. set cellId(value: number) {
  524. if (this._cellId === value) {
  525. return;
  526. }
  527. this._cellId = value;
  528. this._markAsDirty();
  529. }
  530. /**
  531. * Creates a new Image
  532. * @param name defines the control name
  533. * @param url defines the image url
  534. */
  535. constructor(public name?: string, url: Nullable<string> = null) {
  536. super(name);
  537. this.source = url;
  538. }
  539. /**
  540. * Tests if a given coordinates belong to the current control
  541. * @param x defines x coordinate to test
  542. * @param y defines y coordinate to test
  543. * @returns true if the coordinates are inside the control
  544. */
  545. public contains(x: number, y: number): boolean {
  546. if (!super.contains(x, y)) {
  547. return false;
  548. }
  549. if (!this._detectPointerOnOpaqueOnly || !this._workingCanvas) {
  550. return true;
  551. }
  552. const width = this._currentMeasure.width | 0;
  553. const height = this._currentMeasure.height | 0;
  554. const key = width + "_" + height;
  555. let imageData = this._imageDataCache.data;
  556. if (!imageData || this._imageDataCache.key !== key) {
  557. const canvas = this._workingCanvas;
  558. const context = canvas.getContext("2d")!;
  559. this._imageDataCache.data = imageData = context.getImageData(0, 0, width, height).data;
  560. this._imageDataCache.key = key;
  561. }
  562. x = (x - this._currentMeasure.left) | 0;
  563. y = (y - this._currentMeasure.top) | 0;
  564. const pickedPixel = imageData[(x + y * width) * 4 + 3];
  565. return pickedPixel > 0;
  566. }
  567. protected _getTypeName(): string {
  568. return "Image";
  569. }
  570. /** Force the control to synchronize with its content */
  571. public synchronizeSizeWithContent() {
  572. if (!this._loaded) {
  573. return;
  574. }
  575. this.width = this._domImage.width + "px";
  576. this.height = this._domImage.height + "px";
  577. }
  578. protected _processMeasures(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
  579. if (this._loaded) {
  580. switch (this._stretch) {
  581. case Image.STRETCH_NONE:
  582. break;
  583. case Image.STRETCH_FILL:
  584. break;
  585. case Image.STRETCH_UNIFORM:
  586. break;
  587. case Image.STRETCH_NINE_PATCH:
  588. break;
  589. case Image.STRETCH_EXTEND:
  590. if (this._autoScale) {
  591. this.synchronizeSizeWithContent();
  592. }
  593. if (this.parent && this.parent.parent) {
  594. // Will update root size if root is not the top root
  595. this.parent.adaptWidthToChildren = true;
  596. this.parent.adaptHeightToChildren = true;
  597. }
  598. break;
  599. }
  600. }
  601. super._processMeasures(parentMeasure, context);
  602. }
  603. private _prepareWorkingCanvasForOpaqueDetection() {
  604. if (!this._detectPointerOnOpaqueOnly) {
  605. return;
  606. }
  607. if (!this._workingCanvas) {
  608. this._workingCanvas = document.createElement("canvas");
  609. }
  610. const canvas = this._workingCanvas;
  611. const width = this._currentMeasure.width;
  612. const height = this._currentMeasure.height;
  613. const context = canvas.getContext("2d")!;
  614. canvas.width = width;
  615. canvas.height = height;
  616. context.clearRect(0, 0, width, height);
  617. }
  618. private _drawImage(context: CanvasRenderingContext2D, sx: number, sy: number, sw: number, sh: number, tx: number, ty: number, tw: number, th: number) {
  619. context.drawImage(this._domImage, sx, sy, sw, sh, tx, ty, tw, th);
  620. if (!this._detectPointerOnOpaqueOnly) {
  621. return;
  622. }
  623. const canvas = this._workingCanvas!;
  624. context = canvas.getContext("2d")!;
  625. context.drawImage(this._domImage, sx, sy, sw, sh, tx - this._currentMeasure.left, ty - this._currentMeasure.top, tw, th);
  626. }
  627. public _draw(context: CanvasRenderingContext2D): void {
  628. context.save();
  629. if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
  630. context.shadowColor = this.shadowColor;
  631. context.shadowBlur = this.shadowBlur;
  632. context.shadowOffsetX = this.shadowOffsetX;
  633. context.shadowOffsetY = this.shadowOffsetY;
  634. }
  635. let x, y, width, height;
  636. if (this.cellId == -1) {
  637. x = this._sourceLeft;
  638. y = this._sourceTop;
  639. width = this._sourceWidth ? this._sourceWidth : this._imageWidth;
  640. height = this._sourceHeight ? this._sourceHeight : this._imageHeight;
  641. } else {
  642. let rowCount = this._domImage.naturalWidth / this.cellWidth;
  643. let column = (this.cellId / rowCount) >> 0;
  644. let row = this.cellId % rowCount;
  645. x = this.cellWidth * row;
  646. y = this.cellHeight * column;
  647. width = this.cellWidth;
  648. height = this.cellHeight;
  649. }
  650. this._prepareWorkingCanvasForOpaqueDetection();
  651. this._applyStates(context);
  652. if (this._loaded) {
  653. switch (this._stretch) {
  654. case Image.STRETCH_NONE:
  655. this._drawImage(context, x, y, width, height, this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  656. break;
  657. case Image.STRETCH_FILL:
  658. this._drawImage(context, x, y, width, height, this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  659. break;
  660. case Image.STRETCH_UNIFORM:
  661. var hRatio = this._currentMeasure.width / width;
  662. var vRatio = this._currentMeasure.height / height;
  663. var ratio = Math.min(hRatio, vRatio);
  664. var centerX = (this._currentMeasure.width - width * ratio) / 2;
  665. var centerY = (this._currentMeasure.height - height * ratio) / 2;
  666. this._drawImage(context, x, y, width, height, this._currentMeasure.left + centerX, this._currentMeasure.top + centerY, width * ratio, height * ratio);
  667. break;
  668. case Image.STRETCH_EXTEND:
  669. this._drawImage(context, x, y, width, height, this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  670. break;
  671. case Image.STRETCH_NINE_PATCH:
  672. this._renderNinePatch(context);
  673. break;
  674. }
  675. }
  676. context.restore();
  677. }
  678. private _renderCornerPatch(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, targetX: number, targetY: number): void {
  679. this._drawImage(context, x, y, width, height, this._currentMeasure.left + targetX, this._currentMeasure.top + targetY, width, height);
  680. }
  681. private _renderNinePatch(context: CanvasRenderingContext2D): void {
  682. let height = this._imageHeight;
  683. let leftWidth = this._sliceLeft;
  684. let topHeight = this._sliceTop;
  685. let bottomHeight = this._imageHeight - this._sliceBottom;
  686. let rightWidth = this._imageWidth - this._sliceRight;
  687. let left = 0;
  688. let top = 0;
  689. if (this._populateNinePatchSlicesFromImage) {
  690. left = 1;
  691. top = 1;
  692. height -= 2;
  693. leftWidth -= 1;
  694. topHeight -= 1;
  695. bottomHeight -= 1;
  696. rightWidth -= 1;
  697. }
  698. const centerWidth = this._sliceRight - this._sliceLeft;
  699. const targetCenterWidth = this._currentMeasure.width - rightWidth - this.sliceLeft;
  700. const targetTopHeight = this._currentMeasure.height - height + this._sliceBottom;
  701. // Corners
  702. this._renderCornerPatch(context, left, top, leftWidth, topHeight, 0, 0);
  703. this._renderCornerPatch(context, left, this._sliceBottom, leftWidth, height - this._sliceBottom, 0, targetTopHeight);
  704. this._renderCornerPatch(context, this._sliceRight, top, rightWidth, topHeight, this._currentMeasure.width - rightWidth, 0);
  705. this._renderCornerPatch(context, this._sliceRight, this._sliceBottom, rightWidth, height - this._sliceBottom, this._currentMeasure.width - rightWidth, targetTopHeight);
  706. // Center
  707. this._drawImage(context, this._sliceLeft, this._sliceTop, centerWidth, this._sliceBottom - this._sliceTop, this._currentMeasure.left + leftWidth, this._currentMeasure.top + topHeight, targetCenterWidth, targetTopHeight - topHeight);
  708. // Borders
  709. this._drawImage(context, left, this._sliceTop, leftWidth, this._sliceBottom - this._sliceTop, this._currentMeasure.left, this._currentMeasure.top + topHeight, leftWidth, targetTopHeight - topHeight);
  710. this._drawImage(context, this._sliceRight, this._sliceTop, leftWidth, this._sliceBottom - this._sliceTop, this._currentMeasure.left + this._currentMeasure.width - rightWidth, this._currentMeasure.top + topHeight, leftWidth, targetTopHeight - topHeight);
  711. this._drawImage(context, this._sliceLeft, top, centerWidth, topHeight, this._currentMeasure.left + leftWidth, this._currentMeasure.top, targetCenterWidth, topHeight);
  712. this._drawImage(context, this._sliceLeft, this._sliceBottom, centerWidth, bottomHeight, this._currentMeasure.left + leftWidth, this._currentMeasure.top + targetTopHeight, targetCenterWidth, bottomHeight);
  713. }
  714. public dispose() {
  715. super.dispose();
  716. this.onImageLoadedObservable.clear();
  717. this.onSVGAttributesComputedObservable.clear();
  718. }
  719. // Static
  720. /** STRETCH_NONE */
  721. public static readonly STRETCH_NONE = 0;
  722. /** STRETCH_FILL */
  723. public static readonly STRETCH_FILL = 1;
  724. /** STRETCH_UNIFORM */
  725. public static readonly STRETCH_UNIFORM = 2;
  726. /** STRETCH_EXTEND */
  727. public static readonly STRETCH_EXTEND = 3;
  728. /** NINE_PATCH */
  729. public static readonly STRETCH_NINE_PATCH = 4;
  730. }
  731. _TypeStore.RegisteredTypes["BABYLON.GUI.Image"] = Image;