image.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { Control } from "./control";
  2. import { Nullable, Tools, Observable } from "babylonjs";
  3. import { Measure } from "2D";
  4. /**
  5. * Class used to create 2D images
  6. */
  7. export class Image extends Control {
  8. private static _WorkingCanvas: Nullable<HTMLCanvasElement> = null;
  9. private _domImage: HTMLImageElement;
  10. private _imageWidth: number;
  11. private _imageHeight: number;
  12. private _loaded = false;
  13. private _stretch = Image.STRETCH_FILL;
  14. private _source: Nullable<string>;
  15. private _autoScale = false;
  16. private _sourceLeft = 0;
  17. private _sourceTop = 0;
  18. private _sourceWidth = 0;
  19. private _sourceHeight = 0;
  20. private _cellWidth: number = 0;
  21. private _cellHeight: number = 0;
  22. private _cellId: number = -1;
  23. private _useNinePatch = false;
  24. private _populateNinePatchSlicesFromImage = false;
  25. private _sliceLeft: number;
  26. private _sliceRight: number;
  27. private _sliceTop: number;
  28. private _sliceBottom: number;
  29. /**
  30. * Observable notified when the content is loaded
  31. */
  32. public onImageLoadedObservable = new Observable<Image>();
  33. /**
  34. * Gets a boolean indicating that the content is loaded
  35. */
  36. public get isLoaded(): boolean {
  37. return this._loaded;
  38. }
  39. /**
  40. * Gets or sets a boolean indicating if nine patch should be used
  41. */
  42. public get useNinePatch(): boolean {
  43. return this._useNinePatch;
  44. }
  45. public set useNinePatch(value: boolean) {
  46. if (this._useNinePatch === value) {
  47. return;
  48. }
  49. this._useNinePatch = value;
  50. this._markAsDirty();
  51. }
  52. /**
  53. * Gets or sets a boolean indicating if nine patch slices (left, top, right, bottom) should be read from image data
  54. */
  55. public get populateNinePatchSlicesFromImage(): boolean {
  56. return this._populateNinePatchSlicesFromImage;
  57. }
  58. public set populateNinePatchSlicesFromImage(value: boolean) {
  59. if (this._populateNinePatchSlicesFromImage === value) {
  60. return;
  61. }
  62. this._populateNinePatchSlicesFromImage = value;
  63. if (this._populateNinePatchSlicesFromImage && this._loaded) {
  64. this._extractNinePatchSliceDataFromImage();
  65. }
  66. }
  67. /**
  68. * Gets or sets the left value for slicing (9-patch)
  69. */
  70. public get sliceLeft(): number {
  71. return this._sliceLeft;
  72. }
  73. public set sliceLeft(value: number) {
  74. if (this._sliceLeft === value) {
  75. return;
  76. }
  77. this._sliceLeft = value;
  78. this._markAsDirty();
  79. }
  80. /**
  81. * Gets or sets the right value for slicing (9-patch)
  82. */
  83. public get sliceRight(): number {
  84. return this._sliceRight;
  85. }
  86. public set sliceRight(value: number) {
  87. if (this._sliceRight === value) {
  88. return;
  89. }
  90. this._sliceRight = value;
  91. this._markAsDirty();
  92. }
  93. /**
  94. * Gets or sets the top value for slicing (9-patch)
  95. */
  96. public get sliceTop(): number {
  97. return this._sliceTop;
  98. }
  99. public set sliceTop(value: number) {
  100. if (this._sliceTop === value) {
  101. return;
  102. }
  103. this._sliceTop = value;
  104. this._markAsDirty();
  105. }
  106. /**
  107. * Gets or sets the bottom value for slicing (9-patch)
  108. */
  109. public get sliceBottom(): number {
  110. return this._sliceBottom;
  111. }
  112. public set sliceBottom(value: number) {
  113. if (this._sliceBottom === value) {
  114. return;
  115. }
  116. this._sliceBottom = value;
  117. this._markAsDirty();
  118. }
  119. /**
  120. * Gets or sets the left coordinate in the source image
  121. */
  122. public get sourceLeft(): number {
  123. return this._sourceLeft;
  124. }
  125. public set sourceLeft(value: number) {
  126. if (this._sourceLeft === value) {
  127. return;
  128. }
  129. this._sourceLeft = value;
  130. this._markAsDirty();
  131. }
  132. /**
  133. * Gets or sets the top coordinate in the source image
  134. */
  135. public get sourceTop(): number {
  136. return this._sourceTop;
  137. }
  138. public set sourceTop(value: number) {
  139. if (this._sourceTop === value) {
  140. return;
  141. }
  142. this._sourceTop = value;
  143. this._markAsDirty();
  144. }
  145. /**
  146. * Gets or sets the width to capture in the source image
  147. */
  148. public get sourceWidth(): number {
  149. return this._sourceWidth;
  150. }
  151. public set sourceWidth(value: number) {
  152. if (this._sourceWidth === value) {
  153. return;
  154. }
  155. this._sourceWidth = value;
  156. this._markAsDirty();
  157. }
  158. /**
  159. * Gets or sets the height to capture in the source image
  160. */
  161. public get sourceHeight(): number {
  162. return this._sourceHeight;
  163. }
  164. public set sourceHeight(value: number) {
  165. if (this._sourceHeight === value) {
  166. return;
  167. }
  168. this._sourceHeight = value;
  169. this._markAsDirty();
  170. }
  171. /**
  172. * Gets or sets a boolean indicating if the image can force its container to adapt its size
  173. * @see http://doc.babylonjs.com/how_to/gui#image
  174. */
  175. public get autoScale(): boolean {
  176. return this._autoScale;
  177. }
  178. public set autoScale(value: boolean) {
  179. if (this._autoScale === value) {
  180. return;
  181. }
  182. this._autoScale = value;
  183. if (value && this._loaded) {
  184. this.synchronizeSizeWithContent();
  185. }
  186. }
  187. /** Gets or sets the streching mode used by the image */
  188. public get stretch(): number {
  189. return this._stretch;
  190. }
  191. public set stretch(value: number) {
  192. if (this._stretch === value) {
  193. return;
  194. }
  195. this._stretch = value;
  196. this._markAsDirty();
  197. }
  198. /**
  199. * Gets or sets the internal DOM image used to render the control
  200. */
  201. public set domImage(value: HTMLImageElement) {
  202. this._domImage = value;
  203. this._loaded = false;
  204. if (this._domImage.width) {
  205. this._onImageLoaded();
  206. } else {
  207. this._domImage.onload = () => {
  208. this._onImageLoaded();
  209. };
  210. }
  211. }
  212. public get domImage(): HTMLImageElement {
  213. return this._domImage;
  214. }
  215. private _onImageLoaded(): void {
  216. this._imageWidth = this._domImage.width;
  217. this._imageHeight = this._domImage.height;
  218. this._loaded = true;
  219. if (this._populateNinePatchSlicesFromImage) {
  220. this._extractNinePatchSliceDataFromImage();
  221. }
  222. if (this._autoScale) {
  223. this.synchronizeSizeWithContent();
  224. }
  225. this.onImageLoadedObservable.notifyObservers(this);
  226. this._markAsDirty();
  227. }
  228. private _extractNinePatchSliceDataFromImage() {
  229. if (!Image._WorkingCanvas) {
  230. Image._WorkingCanvas = document.createElement('canvas');
  231. }
  232. const canvas = Image._WorkingCanvas;
  233. const context = canvas.getContext('2d')!;
  234. const width = this._domImage.width;
  235. const height = this._domImage.height;
  236. canvas.width = width;
  237. canvas.height = height;
  238. context.drawImage(this._domImage, 0, 0, width, height);
  239. const imageData = context.getImageData(0, 0, width, height);
  240. // Left and right
  241. this._sliceLeft = -1;
  242. this._sliceRight = -1;
  243. for (var x = 0; x < width; x++) {
  244. const alpha = imageData.data[x * 4 + 3];
  245. if (alpha > 127 && this._sliceLeft === -1) {
  246. this._sliceLeft = x;
  247. continue;
  248. }
  249. if (alpha < 127 && this._sliceLeft > -1) {
  250. this._sliceRight = x;
  251. break;
  252. }
  253. }
  254. // top and bottom
  255. this._sliceTop = -1;
  256. this._sliceBottom = -1;
  257. for (var y = 0; y < height; y++) {
  258. const alpha = imageData.data[y * width * 4 + 3];
  259. if (alpha > 127 && this._sliceTop === -1) {
  260. this._sliceTop = y;
  261. continue;
  262. }
  263. if (alpha < 127 && this._sliceTop > -1) {
  264. this._sliceBottom = y;
  265. break;
  266. }
  267. }
  268. }
  269. /**
  270. * Gets or sets image source url
  271. */
  272. public set source(value: Nullable<string>) {
  273. if (this._source === value) {
  274. return;
  275. }
  276. this._loaded = false;
  277. this._source = value;
  278. this._domImage = document.createElement("img");
  279. this._domImage.onload = () => {
  280. this._onImageLoaded();
  281. };
  282. if (value) {
  283. Tools.SetCorsBehavior(value, this._domImage);
  284. this._domImage.src = value;
  285. }
  286. }
  287. /**
  288. * Gets or sets the cell width to use when animation sheet is enabled
  289. * @see http://doc.babylonjs.com/how_to/gui#image
  290. */
  291. get cellWidth(): number {
  292. return this._cellWidth;
  293. }
  294. set cellWidth(value: number) {
  295. if (this._cellWidth === value) {
  296. return;
  297. }
  298. this._cellWidth = value;
  299. this._markAsDirty();
  300. }
  301. /**
  302. * Gets or sets the cell height to use when animation sheet is enabled
  303. * @see http://doc.babylonjs.com/how_to/gui#image
  304. */
  305. get cellHeight(): number {
  306. return this._cellHeight;
  307. }
  308. set cellHeight(value: number) {
  309. if (this._cellHeight === value) {
  310. return;
  311. }
  312. this._cellHeight = value;
  313. this._markAsDirty();
  314. }
  315. /**
  316. * Gets or sets the cell id to use (this will turn on the animation sheet mode)
  317. * @see http://doc.babylonjs.com/how_to/gui#image
  318. */
  319. get cellId(): number {
  320. return this._cellId;
  321. }
  322. set cellId(value: number) {
  323. if (this._cellId === value) {
  324. return;
  325. }
  326. this._cellId = value;
  327. this._markAsDirty();
  328. }
  329. /**
  330. * Creates a new Image
  331. * @param name defines the control name
  332. * @param url defines the image url
  333. */
  334. constructor(public name?: string, url: Nullable<string> = null) {
  335. super(name);
  336. this.source = url;
  337. }
  338. protected _getTypeName(): string {
  339. return "Image";
  340. }
  341. /** Force the control to synchronize with its content */
  342. public synchronizeSizeWithContent() {
  343. if (!this._loaded) {
  344. return;
  345. }
  346. this.width = this._domImage.width + "px";
  347. this.height = this._domImage.height + "px";
  348. }
  349. protected _processMeasures(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
  350. if (this._loaded) {
  351. switch (this._stretch) {
  352. case Image.STRETCH_NONE:
  353. break;
  354. case Image.STRETCH_FILL:
  355. break;
  356. case Image.STRETCH_UNIFORM:
  357. break;
  358. case Image.STRETCH_EXTEND:
  359. if (this._autoScale) {
  360. this.synchronizeSizeWithContent();
  361. }
  362. if (this.parent && this.parent.parent) { // Will update root size if root is not the top root
  363. this.parent.adaptWidthToChildren = true;
  364. this.parent.adaptHeightToChildren = true;
  365. }
  366. break;
  367. }
  368. }
  369. super._processMeasures(parentMeasure, context);
  370. }
  371. public _draw(context: CanvasRenderingContext2D): void {
  372. context.save();
  373. if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
  374. context.shadowColor = this.shadowColor;
  375. context.shadowBlur = this.shadowBlur;
  376. context.shadowOffsetX = this.shadowOffsetX;
  377. context.shadowOffsetY = this.shadowOffsetY;
  378. }
  379. let x, y, width, height;
  380. if (this.cellId == -1) {
  381. x = this._sourceLeft;
  382. y = this._sourceTop;
  383. width = this._sourceWidth ? this._sourceWidth : this._imageWidth;
  384. height = this._sourceHeight ? this._sourceHeight : this._imageHeight;
  385. }
  386. else {
  387. let rowCount = this._domImage.naturalWidth / this.cellWidth;
  388. let column = (this.cellId / rowCount) >> 0;
  389. let row = this.cellId % rowCount;
  390. x = this.cellWidth * row;
  391. y = this.cellHeight * column;
  392. width = this.cellWidth;
  393. height = this.cellHeight;
  394. }
  395. this._applyStates(context);
  396. if (this._loaded) {
  397. switch (this._stretch) {
  398. case Image.STRETCH_NONE:
  399. context.drawImage(this._domImage, x, y, width, height,
  400. this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  401. break;
  402. case Image.STRETCH_FILL:
  403. context.drawImage(this._domImage, x, y, width, height,
  404. this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  405. break;
  406. case Image.STRETCH_UNIFORM:
  407. var hRatio = this._currentMeasure.width / width;
  408. var vRatio = this._currentMeasure.height / height;
  409. var ratio = Math.min(hRatio, vRatio);
  410. var centerX = (this._currentMeasure.width - width * ratio) / 2;
  411. var centerY = (this._currentMeasure.height - height * ratio) / 2;
  412. context.drawImage(this._domImage, x, y, width, height,
  413. this._currentMeasure.left + centerX, this._currentMeasure.top + centerY, width * ratio, height * ratio);
  414. break;
  415. case Image.STRETCH_EXTEND:
  416. context.drawImage(this._domImage, x, y, width, height,
  417. this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  418. break;
  419. case Image.STRETCH_NINE_PATCH:
  420. this._renderNinePatch(context);
  421. break;
  422. }
  423. }
  424. context.restore();
  425. }
  426. private _renderCornerPatch(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, targetX: number, targetY: number): void {
  427. context.drawImage(this._domImage, x, y, width, height, this._currentMeasure.left + targetX, this._currentMeasure.top + targetY, width, height);
  428. }
  429. private _renderNinePatch(context: CanvasRenderingContext2D): void {
  430. const width = this._imageWidth - 2;
  431. const height = this._imageHeight - 2;
  432. // Corners
  433. this._renderCornerPatch(context, 1, 1, this._sliceLeft, this._sliceTop, 0, 0);
  434. this._renderCornerPatch(context, 1, this._sliceBottom, this._sliceLeft, height - this._sliceBottom, 0, this._currentMeasure.height - height + this._sliceBottom);
  435. this._renderCornerPatch(context, this._sliceRight, 1, width - this._sliceRight, this._sliceTop, this._currentMeasure.width - width + this._sliceRight, 0);
  436. this._renderCornerPatch(context, this._sliceRight, this._sliceBottom, width - this._sliceRight, height - this._sliceBottom, this._currentMeasure.width - width + this._sliceRight, this._currentMeasure.height - height + this._sliceBottom);
  437. // Center
  438. context.drawImage(this._domImage, this._sliceLeft, this._sliceTop, this._sliceRight - this._sliceLeft, this._sliceBottom - this._sliceTop,
  439. this._currentMeasure.left + this._sliceLeft, this._currentMeasure.top + this._sliceTop, this._currentMeasure.width - width + this._sliceRight - this.sliceLeft, this._currentMeasure.height - height + this._sliceBottom - this._sliceTop);
  440. }
  441. public dispose() {
  442. super.dispose();
  443. this.onImageLoadedObservable.clear();
  444. }
  445. // Static
  446. /** STRETCH_NONE */
  447. public static readonly STRETCH_NONE = 0;
  448. /** STRETCH_FILL */
  449. public static readonly STRETCH_FILL = 1;
  450. /** STRETCH_UNIFORM */
  451. public static readonly STRETCH_UNIFORM = 2;
  452. /** STRETCH_EXTEND */
  453. public static readonly STRETCH_EXTEND = 3;
  454. /** NINE_PATCH */
  455. public static readonly STRETCH_NINE_PATCH = 4;
  456. }