• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

3.45
/src/engine/Graphics/NineSlice.ts
1
import { Graphic, GraphicOptions } from './Graphic';
2
import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
3
import { ImageSource } from './ImageSource';
4
import { Logger } from '../Util/Log';
5
import { Vector } from '../Math/vector';
6

7
/**
8
 * Nine slice stretch mode
9
 */
10
export enum NineSliceStretch {
1✔
11
  /**
12
   * Stretch the image across a dimension
13
   */
14
  Stretch = 'stretch',
1✔
15
  /**
16
   * Tile the image across a dimension
17
   */
18
  Tile = 'tile',
1✔
19
  /**
20
   * Tile the image across a dimension but only by whole image amounts
21
   */
22
  TileFit = 'tile-fit'
1✔
23
}
24

25
export type NineSliceConfig = GraphicOptions & {
26
  /**
27
   * Final width of the nine slice graphic
28
   */
29
  width: number;
30
  /**
31
   * Final height of the nine slice graphic
32
   */
33
  height: number;
34
  /**
35
   *  Image source that's loaded from a Loader or individually
36
   *
37
   */
38
  source: ImageSource;
39

40
  /**
41
   *  Configuration for the source
42
   *
43
   *  Details for the source image, including:
44
   *
45
   *  width and height as numbers of the source image
46
   *
47
   *  and the 9 slice margins
48
   */
49
  sourceConfig: {
50
    width: number;
51
    height: number;
52
    topMargin: number;
53
    leftMargin: number;
54
    bottomMargin: number;
55
    rightMargin: number;
56
  };
57

58
  /**
59
   *  Configuration for the destination
60
   *
61
   *  Details for the destination image, including:
62
   *
63
   *  stretching strategies for horizontal and vertical stretching
64
   *
65
   *  and flag for drawing the center tile if desired
66
   */
67
  destinationConfig: {
68
    /**
69
     * Draw the center part of the nine slice, if false it's a completely transparent gap
70
     */
71
    drawCenter: boolean;
72
    /**
73
     * Horizontal stretch configuration
74
     */
75
    horizontalStretch: NineSliceStretch;
76
    /**
77
     * Vertical stretch configuration
78
     */
79
    verticalStretch: NineSliceStretch;
80
  };
81
};
82

83
export class NineSlice extends Graphic {
84
  private _imgSource: ImageSource;
85
  private _sourceSprite?: HTMLImageElement;
86
  private _canvasA: HTMLCanvasElement;
87
  private _canvasB: HTMLCanvasElement;
88
  private _canvasC: HTMLCanvasElement;
89
  private _canvasD: HTMLCanvasElement;
90
  private _canvasE: HTMLCanvasElement;
91
  private _canvasF: HTMLCanvasElement;
92
  private _canvasG: HTMLCanvasElement;
93
  private _canvasH: HTMLCanvasElement;
94
  private _canvasI: HTMLCanvasElement;
95

UNCOV
96
  private _logger = Logger.getInstance();
×
97

98
  private _config: NineSliceConfig;
99
  constructor(config: NineSliceConfig) {
UNCOV
100
    super(config);
×
UNCOV
101
    this._config = config;
×
UNCOV
102
    this._imgSource = config.source;
×
103

UNCOV
104
    this._canvasA = document.createElement('canvas');
×
UNCOV
105
    this._canvasB = document.createElement('canvas');
×
UNCOV
106
    this._canvasC = document.createElement('canvas');
×
UNCOV
107
    this._canvasD = document.createElement('canvas');
×
UNCOV
108
    this._canvasE = document.createElement('canvas');
×
UNCOV
109
    this._canvasF = document.createElement('canvas');
×
UNCOV
110
    this._canvasG = document.createElement('canvas');
×
UNCOV
111
    this._canvasH = document.createElement('canvas');
×
UNCOV
112
    this._canvasI = document.createElement('canvas');
×
113

UNCOV
114
    this._initialize();
×
115

UNCOV
116
    this._imgSource.ready.then(() => {
×
UNCOV
117
      this._initialize();
×
118
    });
119
  }
120

121
  /**
122
   * Sets the target width of the 9 slice (pixels), and recalculates the 9 slice if desired (auto)
123
   * @param newWidth
124
   * @param auto
125
   */
126
  setTargetWidth(newWidth: number, auto: boolean = false) {
×
UNCOV
127
    this._config.width = newWidth;
×
UNCOV
128
    if (auto) {
×
129
      this._initialize();
×
130
    }
131
  }
132

133
  /**
134
   * Sets the target height of the 9 slice (pixels), and recalculates the 9 slice if desired (auto)
135
   * @param newHeight
136
   * @param auto
137
   */
138
  setTargetHeight(newHeight: number, auto: boolean = false) {
×
UNCOV
139
    this._config.height = newHeight;
×
UNCOV
140
    if (auto) {
×
141
      this._initialize();
×
142
    }
143
  }
144

145
  /**
146
   *  Sets the 9 slice margins (pixels), and recalculates the 9 slice if desired (auto)
147
   */
148
  setMargins(left: number, top: number, right: number, bottom: number, auto: boolean = false) {
×
UNCOV
149
    this._config.sourceConfig.leftMargin = left;
×
UNCOV
150
    this._config.sourceConfig.topMargin = top;
×
UNCOV
151
    this._config.sourceConfig.rightMargin = right;
×
UNCOV
152
    this._config.sourceConfig.bottomMargin = bottom;
×
UNCOV
153
    if (auto) {
×
154
      this._initialize();
×
155
    }
156
  }
157

158
  /**
159
   *  Sets the stretching strategy for the 9 slice, and recalculates the 9 slice if desired (auto)
160
   *
161
   */
162
  setStretch(type: 'horizontal' | 'vertical' | 'both', stretch: NineSliceStretch, auto: boolean = false) {
×
UNCOV
163
    if (type === 'horizontal') {
×
UNCOV
164
      this._config.destinationConfig.horizontalStretch = stretch;
×
UNCOV
165
    } else if (type === 'vertical') {
×
UNCOV
166
      this._config.destinationConfig.verticalStretch = stretch;
×
167
    } else {
168
      this._config.destinationConfig.horizontalStretch = stretch;
×
169
      this._config.destinationConfig.verticalStretch = stretch;
×
170
    }
UNCOV
171
    if (auto) {
×
172
      this._initialize();
×
173
    }
174
  }
175

176
  /**
177
   *  Returns the config of the 9 slice
178
   */
179
  getConfig(): NineSliceConfig {
UNCOV
180
    return this._config;
×
181
  }
182

183
  /**
184
   * Draws 1 of the 9 tiles based on parameters passed in
185
   * context is the ExcaliburGraphicsContext from the _drawImage function
186
   * destinationSize is the size of the destination image as a vector (width,height)
187
   * targetCanvas is the canvas to draw to
188
   * horizontalStretch and verticalStretch are the horizontal and vertical stretching strategies
189
   * marginW and marginH are optional margins for the 9 slice for positioning
190
   * @param context
191
   * @param targetCanvas
192
   * @param destinationSize
193
   * @param horizontalStretch
194
   * @param verticalStretch
195
   * @param marginWidth
196
   * @param marginHeight
197
   */
198
  protected _drawTile(
199
    context: ExcaliburGraphicsContext,
200
    targetCanvas: HTMLCanvasElement,
201
    destinationSize: Vector,
202
    horizontalStretch: NineSliceStretch,
203
    verticalStretch: NineSliceStretch,
204
    marginWidth?: number,
205
    marginHeight?: number
206
  ) {
UNCOV
207
    const tempMarginW = marginWidth || 0;
×
UNCOV
208
    const tempMarginH = marginHeight || 0;
×
209
    let tempSizeX: number, tempPositionX: number, tempSizeY: number, tempPositionY: number;
UNCOV
210
    const numTilesX = this._getNumberOfTiles(targetCanvas.width, destinationSize.x, horizontalStretch);
×
UNCOV
211
    const numTilesY = this._getNumberOfTiles(targetCanvas.height, destinationSize.y, verticalStretch);
×
212

UNCOV
213
    for (let i = 0; i < numTilesX; i++) {
×
UNCOV
214
      for (let j = 0; j < numTilesY; j++) {
×
UNCOV
215
        let { tempSize, tempPosition } = this._calculateParams(
×
216
          i,
217
          numTilesX,
218
          targetCanvas.width,
219
          destinationSize.x,
220
          this._config.destinationConfig.horizontalStretch
221
        );
UNCOV
222
        tempSizeX = tempSize;
×
UNCOV
223
        tempPositionX = tempPosition;
×
224

UNCOV
225
        ({ tempSize, tempPosition } = this._calculateParams(
×
226
          j,
227
          numTilesY,
228
          targetCanvas.height,
229
          destinationSize.y,
230
          this._config.destinationConfig.verticalStretch
231
        ));
UNCOV
232
        tempSizeY = tempSize;
×
UNCOV
233
        tempPositionY = tempPosition;
×
234

UNCOV
235
        context.drawImage(
×
236
          targetCanvas,
237
          0,
238
          0,
239
          targetCanvas.width,
240
          targetCanvas.height,
241
          tempMarginW + tempPositionX,
242
          tempMarginH + tempPositionY,
243
          tempSizeX,
244
          tempSizeY
245
        );
246
      }
247
    }
248
  }
249

250
  /**
251
   *  Draws the 9 slices to the canvas
252
   */
253
  protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number): void {
UNCOV
254
    if (this._imgSource.isLoaded()) {
×
255
      // Top left, no stretching
256

UNCOV
257
      this._drawTile(
×
258
        ex,
259
        this._canvasA,
260

261
        new Vector(this._config.sourceConfig.leftMargin, this._config.sourceConfig.topMargin),
262
        this._config.destinationConfig.horizontalStretch,
263
        this._config.destinationConfig.verticalStretch
264
      );
265

266
      // Top, middle, horizontal stretching
UNCOV
267
      this._drawTile(
×
268
        ex,
269
        this._canvasB,
270

271
        new Vector(
272
          this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
273
          this._config.sourceConfig.topMargin
274
        ),
275
        this._config.destinationConfig.horizontalStretch,
276
        this._config.destinationConfig.verticalStretch,
277
        this._config.sourceConfig.leftMargin,
278
        0
279
      );
280

281
      // Top right, no stretching
282

UNCOV
283
      this._drawTile(
×
284
        ex,
285
        this._canvasC,
286

287
        new Vector(this._config.sourceConfig.rightMargin, this._config.sourceConfig.topMargin),
288
        this._config.destinationConfig.horizontalStretch,
289
        this._config.destinationConfig.verticalStretch,
290

291
        this._config.width - this._config.sourceConfig.rightMargin,
292
        0
293
      );
294

295
      // middle, left, vertical stretching
296

UNCOV
297
      this._drawTile(
×
298
        ex,
299
        this._canvasD,
300
        new Vector(
301
          this._config.sourceConfig.leftMargin,
302

303
          this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
304
        ),
305
        this._config.destinationConfig.horizontalStretch,
306
        this._config.destinationConfig.verticalStretch,
307
        0,
308
        this._config.sourceConfig.topMargin
309
      );
310

311
      // center, both stretching
UNCOV
312
      if (this._config.destinationConfig.drawCenter) {
×
UNCOV
313
        this._drawTile(
×
314
          ex,
315
          this._canvasE,
316
          new Vector(
317
            this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
318

319
            this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
320
          ),
321
          this._config.destinationConfig.horizontalStretch,
322
          this._config.destinationConfig.verticalStretch,
323
          this._config.sourceConfig.leftMargin,
324
          this._config.sourceConfig.topMargin
325
        );
326
      }
327
      // middle, right, vertical stretching
UNCOV
328
      this._drawTile(
×
329
        ex,
330
        this._canvasF,
331

332
        new Vector(
333
          this._config.sourceConfig.rightMargin,
334

335
          this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
336
        ),
337
        this._config.destinationConfig.horizontalStretch,
338
        this._config.destinationConfig.verticalStretch,
339

340
        this._config.width - this._config.sourceConfig.rightMargin,
341
        this._config.sourceConfig.topMargin
342
      );
343

344
      // bottom left, no stretching
UNCOV
345
      this._drawTile(
×
346
        ex,
347
        this._canvasG,
348
        new Vector(this._config.sourceConfig.leftMargin, this._config.sourceConfig.bottomMargin),
349
        this._config.destinationConfig.horizontalStretch,
350
        this._config.destinationConfig.verticalStretch,
351
        0,
352

353
        this._config.height - this._config.sourceConfig.bottomMargin
354
      );
355

356
      // bottom middle, horizontal stretching
UNCOV
357
      this._drawTile(
×
358
        ex,
359
        this._canvasH,
360

361
        new Vector(
362
          this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
363
          this._config.sourceConfig.bottomMargin
364
        ),
365
        this._config.destinationConfig.horizontalStretch,
366
        this._config.destinationConfig.verticalStretch,
367
        this._config.sourceConfig.leftMargin,
368

369
        this._config.height - this._config.sourceConfig.bottomMargin
370
      );
371

372
      // bottom right, no stretching
UNCOV
373
      this._drawTile(
×
374
        ex,
375
        this._canvasI,
376
        new Vector(this._config.sourceConfig.rightMargin, this._config.sourceConfig.bottomMargin),
377
        this._config.destinationConfig.horizontalStretch,
378
        this._config.destinationConfig.verticalStretch,
379

380
        this._config.width - this._config.sourceConfig.rightMargin,
381

382
        this._config.height - this._config.sourceConfig.bottomMargin
383
      );
384
    } else {
385
      this._logger.warnOnce(
×
386
        `NineSlice ImageSource ${this._imgSource.path}` +
387
          ` is not yet loaded and won't be drawn. Please call .load() or include in a Loader.\n\n` +
388
          `Read https://excaliburjs.com/docs/imagesource for more information.`
389
      );
390
    }
391
  }
392

393
  /**
394
   * Slices the source sprite into the 9 slice canvases internally
395
   */
396
  protected _initialize() {
UNCOV
397
    this._sourceSprite = this._imgSource.image;
×
398

399
    // top left slice
UNCOV
400
    this._canvasA.width = this._config.sourceConfig.leftMargin;
×
UNCOV
401
    this._canvasA.height = this._config.sourceConfig.topMargin;
×
UNCOV
402
    const aCtx = this._canvasA.getContext('2d');
×
403

UNCOV
404
    aCtx?.drawImage(this._sourceSprite, 0, 0, this._canvasA.width, this._canvasA.height, 0, 0, this._canvasA.width, this._canvasA.height);
×
405

406
    // top slice
407

UNCOV
408
    this._canvasB.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
×
UNCOV
409
    this._canvasB.height = this._config.sourceConfig.topMargin;
×
410

UNCOV
411
    const bCtx = this._canvasB.getContext('2d');
×
UNCOV
412
    bCtx?.drawImage(
×
413
      this._sourceSprite,
414
      this._config.sourceConfig.leftMargin,
415
      0,
416
      this._canvasB.width,
417
      this._canvasB.height,
418
      0,
419
      0,
420
      this._canvasB.width,
421
      this._canvasB.height
422
    );
423

424
    // top right slice
UNCOV
425
    this._canvasC.width = this._config.sourceConfig.rightMargin;
×
UNCOV
426
    this._canvasC.height = this._config.sourceConfig.topMargin;
×
UNCOV
427
    const cCtx = this._canvasC.getContext('2d');
×
UNCOV
428
    cCtx?.drawImage(
×
429
      this._sourceSprite,
430
      this._sourceSprite.width - this._config.sourceConfig.rightMargin,
431
      0,
432
      this._canvasC.width,
433
      this._canvasC.height,
434
      0,
435
      0,
436
      this._canvasC.width,
437
      this._canvasC.height
438
    );
439

440
    // middle left slice
UNCOV
441
    this._canvasD.width = this._config.sourceConfig.leftMargin;
×
UNCOV
442
    this._canvasD.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
×
UNCOV
443
    const dCtx = this._canvasD.getContext('2d');
×
UNCOV
444
    dCtx?.drawImage(
×
445
      this._sourceSprite,
446
      0,
447
      this._config.sourceConfig.topMargin,
448
      this._canvasD.width,
449
      this._canvasD.height,
450
      0,
451
      0,
452
      this._canvasD.width,
453
      this._canvasD.height
454
    );
455

456
    // middle slice
UNCOV
457
    this._canvasE.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
×
UNCOV
458
    this._canvasE.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
×
UNCOV
459
    const eCtx = this._canvasE.getContext('2d');
×
UNCOV
460
    eCtx?.drawImage(
×
461
      this._sourceSprite,
462
      this._config.sourceConfig.leftMargin,
463
      this._config.sourceConfig.topMargin,
464
      this._canvasE.width,
465
      this._canvasE.height,
466
      0,
467
      0,
468
      this._canvasE.width,
469
      this._canvasE.height
470
    );
471

472
    // middle right slice
UNCOV
473
    this._canvasF.width = this._config.sourceConfig.rightMargin;
×
UNCOV
474
    this._canvasF.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
×
UNCOV
475
    const fCtx = this._canvasF.getContext('2d');
×
UNCOV
476
    fCtx?.drawImage(
×
477
      this._sourceSprite,
478

479
      this._config.sourceConfig.width - this._config.sourceConfig.rightMargin,
480
      this._config.sourceConfig.topMargin,
481
      this._canvasF.width,
482
      this._canvasF.height,
483
      0,
484
      0,
485
      this._canvasF.width,
486
      this._canvasF.height
487
    );
488

489
    // bottom left slice
UNCOV
490
    this._canvasG.width = this._config.sourceConfig.leftMargin;
×
UNCOV
491
    this._canvasG.height = this._config.sourceConfig.bottomMargin;
×
UNCOV
492
    const gCtx = this._canvasG.getContext('2d');
×
UNCOV
493
    gCtx?.drawImage(
×
494
      this._sourceSprite,
495
      0,
496
      this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
497
      this._canvasG.width,
498
      this._canvasG.height,
499
      0,
500
      0,
501
      this._canvasG.width,
502
      this._canvasG.height
503
    );
504

505
    // bottom slice
UNCOV
506
    this._canvasH.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
×
UNCOV
507
    this._canvasH.height = this._config.sourceConfig.bottomMargin;
×
UNCOV
508
    const hCtx = this._canvasH.getContext('2d');
×
UNCOV
509
    hCtx?.drawImage(
×
510
      this._sourceSprite,
511
      this._config.sourceConfig.leftMargin,
512
      this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
513
      this._canvasH.width,
514
      this._canvasH.height,
515
      0,
516
      0,
517
      this._canvasH.width,
518
      this._canvasH.height
519
    );
520

521
    // bottom right slice
UNCOV
522
    this._canvasI.width = this._config.sourceConfig.rightMargin;
×
UNCOV
523
    this._canvasI.height = this._config.sourceConfig.bottomMargin;
×
UNCOV
524
    const iCtx = this._canvasI.getContext('2d');
×
UNCOV
525
    iCtx?.drawImage(
×
526
      this._sourceSprite,
527
      this._sourceSprite.width - this._config.sourceConfig.rightMargin,
528
      this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
529
      this._canvasI.width,
530
      this._canvasI.height,
531
      0,
532
      0,
533
      this._canvasI.width,
534
      this._canvasI.height
535
    );
536
  }
537

538
  /**
539
   * Clones the 9 slice
540
   */
541

542
  clone(): NineSlice {
UNCOV
543
    return new NineSlice(this._config);
×
544
  }
545

546
  /**
547
   * Returns the number of tiles
548
   */
549
  protected _getNumberOfTiles(tileSize: number, destinationSize: number, strategy: NineSliceStretch): number {
UNCOV
550
    switch (strategy) {
×
551
      case NineSliceStretch.Stretch:
×
UNCOV
552
        return 1;
×
553
      case NineSliceStretch.Tile:
UNCOV
554
        return Math.ceil(destinationSize / tileSize);
×
555
      case NineSliceStretch.TileFit:
UNCOV
556
        return Math.ceil(destinationSize / tileSize);
×
557
    }
558
  }
559

560
  /**
561
   * Returns the position and size of the tile
562
   */
563
  protected _calculateParams(
564
    tileNum: number,
565
    numTiles: number,
566
    tileSize: number,
567
    destinationSize: number,
568
    strategy: NineSliceStretch
569
  ): { tempPosition: number; tempSize: number } {
UNCOV
570
    switch (strategy) {
×
571
      case NineSliceStretch.Stretch:
×
UNCOV
572
        return {
×
573
          tempPosition: 0,
574
          tempSize: destinationSize
575
        };
576
      case NineSliceStretch.Tile:
577
        // if last tile, adjust size
UNCOV
578
        if (tileNum === numTiles - 1) {
×
579
          //last tile
UNCOV
580
          return {
×
581
            tempPosition: tileNum * tileSize,
582
            tempSize: tileSize - (numTiles * tileSize - destinationSize)
583
          };
584
        } else {
UNCOV
585
          return {
×
586
            tempPosition: tileNum * tileSize,
587
            tempSize: tileSize
588
          };
589
        }
590

591
      case NineSliceStretch.TileFit:
UNCOV
592
        const reducedTileSize = destinationSize / numTiles;
×
UNCOV
593
        const position = tileNum * reducedTileSize;
×
UNCOV
594
        return {
×
595
          tempPosition: position,
596
          tempSize: reducedTileSize
597
        };
598
    }
599
  }
600
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc