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

excaliburjs / Excalibur / 15354777440

30 May 2025 08:03PM UTC coverage: 87.858% (-1.5%) from 89.344%
15354777440

Pull #3385

github

web-flow
Merge a00f57733 into e6ec66358
Pull Request #3385: updated Meet action to add tolerance

5002 of 6948 branches covered (71.99%)

3 of 5 new or added lines in 2 files covered. (60.0%)

872 existing lines in 83 files now uncovered.

13661 of 15549 relevant lines covered (87.86%)

25187.01 hits per line

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

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

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

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

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

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

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

97
  private _logger = Logger.getInstance();
8✔
98

99
  private _config: NineSliceConfig;
100
  constructor(config: NineSliceConfig) {
101
    super(config);
8✔
102
    this._config = config;
8✔
103
    this._imgSource = config.source;
8✔
104

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

115
    this._initialize();
8✔
116

117
    this._imgSource.ready.then(() => {
8✔
118
      this._initialize();
8✔
119
    });
120
  }
121

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

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

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

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

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

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

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

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

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

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

258
      this._drawTile(
5✔
259
        ex,
260
        this._canvasA,
261

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

267
      // Top, middle, horizontal stretching
268
      this._drawTile(
5✔
269
        ex,
270
        this._canvasB,
271

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

282
      // Top right, no stretching
283

284
      this._drawTile(
5✔
285
        ex,
286
        this._canvasC,
287

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

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

296
      // middle, left, vertical stretching
297

298
      this._drawTile(
5✔
299
        ex,
300
        this._canvasD,
301
        new Vector(
302
          this._config.sourceConfig.leftMargin,
303

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

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

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

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

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

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

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

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

357
      // bottom middle, horizontal stretching
358
      this._drawTile(
5✔
359
        ex,
360
        this._canvasH,
361

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

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

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

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

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

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

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

405
    aCtx?.drawImage(this._sourceSprite, 0, 0, this._canvasA.width, this._canvasA.height, 0, 0, this._canvasA.width, this._canvasA.height);
16!
406

407
    // top slice
408

409
    this._canvasB.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
16✔
410
    this._canvasB.height = this._config.sourceConfig.topMargin;
16✔
411

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

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

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

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

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

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

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

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

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

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

543
  clone(): NineSlice {
544
    return new NineSlice(this._config);
1✔
545
  }
546

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

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

592
      case NineSliceStretch.TileFit:
593
        const reducedTileSize = destinationSize / numTiles;
50✔
594
        const position = tileNum * reducedTileSize;
50✔
595
        return {
50✔
596
          tempPosition: position,
597
          tempSize: reducedTileSize
598
        };
599
    }
600
  }
601
}
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