/* eslint-env qunit */
import window from 'global/window';
import Html5 from '../../../src/js/tech/html5.js';
import { constructColor } from '../../../src/js/tracks/text-track-display.js';
import Component from '../../../src/js/component.js';

import * as browser from '../../../src/js/utils/browser.js';
import TestHelpers from '../test-helpers.js';
import document from 'global/document';
import sinon from 'sinon';

QUnit.module('Text Track Display', {
  beforeEach(assert) {
    this.clock = sinon.useFakeTimers();
  },
  afterEach(assert) {
    this.clock.restore();
  }
});

const getMenuItemByLanguage = function(items, language) {
  for (let i = items.length - 1; i > 0; i--) {
    const captionMenuItem = items[i];
    const trackLanguage = captionMenuItem.track.language;

    if (trackLanguage && trackLanguage === language) {
      return captionMenuItem;
    }
  }
};

QUnit.test('if native text tracks are not supported, create a texttrackdisplay', function(assert) {
  const oldTestVid = Html5.TEST_VID;
  const oldIsFirefox = browser.IS_FIREFOX;
  const oldTextTrackDisplay = Component.getComponent('TextTrackDisplay');
  const tag = document.createElement('video');
  const track1 = document.createElement('track');
  const track2 = document.createElement('track');

  track1.kind = 'captions';
  track1.label = 'en';
  track1.language = 'English';
  track1.src = 'en.vtt';
  tag.appendChild(track1);

  track2.kind = 'captions';
  track2.label = 'es';
  track2.language = 'Spanish';
  track2.src = 'es.vtt';
  tag.appendChild(track2);

  Html5.TEST_VID = {
    textTracks: []
  };

  browser.stub_IS_FIREFOX(true);

  const fakeTTDSpy = sinon.spy();

  class FakeTTD extends Component {
    constructor(player, options) {
      super(player, options);
      fakeTTDSpy();
    }
  }

  Component.registerComponent('TextTrackDisplay', FakeTTD);

  const player = TestHelpers.makePlayer({}, tag);

  assert.strictEqual(fakeTTDSpy.callCount, 1, 'text track display was created');

  Html5.TEST_VID = oldTestVid;
  browser.stub_IS_FIREFOX(oldIsFirefox);
  Component.registerComponent('TextTrackDisplay', oldTextTrackDisplay);

  player.dispose();
});

QUnit.test('shows the default caption track first', function(assert) {
  const player = TestHelpers.makePlayer();
  const track1 = {
    kind: 'captions',
    label: 'English',
    language: 'en',
    src: 'en.vtt',
    default: true
  };
  const track2 = {
    kind: 'captions',
    label: 'Spanish',
    language: 'es',
    src: 'es.vtt'
  };

  // Add the text tracks
  const englishTrack = player.addRemoteTextTrack(track1, true).track;
  const spanishTrack = player.addRemoteTextTrack(track2, true).track;

  // Make sure the ready handler runs
  this.clock.tick(1);

  assert.ok(englishTrack.mode === 'showing', 'English track should be showing');
  assert.ok(spanishTrack.mode === 'disabled', 'Spanish track should not be showing');
  player.dispose();
});

if (!Html5.supportsNativeTextTracks()) {
  QUnit.test('text track display should attach screen orientation change event handler', function(assert) {
    const oldScreen = window.screen;
    const removeHandlerSpy = sinon.spy();
    let changeHandlerSpy;
    let changeHandlerAttached;

    window.screen = {
      orientation: {
        addEventListener: (type, func) => {
          changeHandlerAttached = true;
          changeHandlerSpy = sinon.spy();
        },
        dispatchEvent: (type) => changeHandlerSpy(),
        removeEventListener: removeHandlerSpy
      }
    };

    const player = TestHelpers.makePlayer();

    this.clock.tick(1);

    assert.true(changeHandlerAttached, 'screen orientation change event handler was not attached');
    assert.strictEqual(changeHandlerSpy.callCount, 0, 'screen orientation change event handler should not be called');

    window.screen.orientation.dispatchEvent('change');

    assert.strictEqual(changeHandlerSpy.callCount, 1, 'screen orientation change event handler was not called');

    player.dispose();

    assert.strictEqual(
      removeHandlerSpy.callCount,
      1,
      'screen orientation change event handler was not removed during player dispose'
    );
    window.screen = oldScreen;
  });

  QUnit.test('selectedlanguagechange is triggered by a track mode change', function(assert) {
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const spy = sinon.spy();
    const selectedLanguageHandler = function(event) {
      spy();
    };
    const englishTrack = player.addRemoteTextTrack(track1, true).track;

    player.textTracks().addEventListener('selectedlanguagechange', selectedLanguageHandler);
    englishTrack.mode = 'showing';

    assert.strictEqual(spy.callCount, 1, 'selectedlanguagechange event was fired');
    player.dispose();
    player.textTracks().removeEventListener('selectedlanguagechange', selectedLanguageHandler);
  });

  QUnit.test("if user-selected language is unavailable, don't pick a track to show", function(assert) {
    // The video has no default language but has ‘English’ captions only
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const captionsButton = player.controlBar.getChild('SubsCapsButton');

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    const englishTrack = player.addRemoteTextTrack(track1, true).track;

    // Force 'es' as user-selected track
    player.cache_.selectedLanguage = { language: 'es', kind: 'captions' };

    this.clock.tick(1);
    player.play();

    assert.ok(!captionsButton.hasClass('vjs-hidden'), 'The captions button is shown');
    assert.ok(englishTrack.mode === 'disabled', 'English track should be disabled');
    player.dispose();
  });

  QUnit.test('the user-selected language takes priority over default language', function(assert) {
    // The video has ‘English’ captions as default, but has ‘Spanish’ captions also
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt',
      default: true
    };
    const track2 = {
      kind: 'captions',
      label: 'Spanish',
      language: 'es',
      src: 'es.vtt'
    };

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    const englishTrack = player.addRemoteTextTrack(track1, true).track;
    const spanishTrack = player.addRemoteTextTrack(track2, true).track;

    // Force 'es' as user-selected track
    player.cache_.selectedLanguage = { enabled: true, language: 'es', kind: 'captions' };
    this.clock.tick(1);

    assert.ok(spanishTrack.mode === 'showing', 'Spanish captions should be shown');
    assert.ok(englishTrack.mode === 'disabled', 'English captions should be hidden');
    player.dispose();
  });

  QUnit.test("don't select user language if it is an empty string", function(assert) {
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const track2 = {
      kind: 'captions',
      label: 'Spanish',
      language: 'es',
      src: 'es.vtt'
    };
    const track3 = {
      kind: 'metadata',
      label: 'segment-metadata'
    };

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    const englishTrack = player.addRemoteTextTrack(track1, true).track;
    const spanishTrack = player.addRemoteTextTrack(track2, true).track;
    const metadataTrack = player.addRemoteTextTrack(track3, true).track;

    // Force empty string ('') as "user-selected" track
    player.cache_.selectedLanguage = { enabled: true, language: '', kind: 'captions' };
    this.clock.tick(1);

    assert.equal(spanishTrack.mode, 'disabled', 'Spanish captions should be disabled');
    assert.equal(englishTrack.mode, 'disabled', 'English captions should be disabled');
    assert.notEqual(metadataTrack.mode, 'showing', 'Metadata track should not be showing');

    // Force es as "user-selected" track
    player.cache_.selectedLanguage = { enabled: true, language: 'es', kind: 'captions' };
    player.trigger('loadedmetadata');

    assert.equal(spanishTrack.mode, 'showing', 'Spanish captions should be showing');
    assert.equal(englishTrack.mode, 'disabled', 'English captions should be disabled');
    assert.notEqual(metadataTrack.mode, 'showing', 'Metadata track should not be showing');

    player.dispose();
  });

  QUnit.test("matching both the selectedLanguage's language and kind takes priority over just matching the language", function(assert) {
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const track2 = {
      kind: 'subtitles',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    const captionTrack = player.addRemoteTextTrack(track1, true).track;
    const subsTrack = player.addRemoteTextTrack(track2, true).track;

    // Force English captions as user-selected track
    player.cache_.selectedLanguage = { enabled: true, language: 'en', kind: 'captions' };
    this.clock.tick(1);

    assert.ok(captionTrack.mode === 'showing', 'Captions track should be preselected');
    assert.ok(subsTrack.mode === 'disabled', 'Subtitles track should remain disabled');
    player.dispose();
  });

  QUnit.test('the user-selected language is used for subsequent source changes', function(assert) {
    // Start with two captions tracks: English and Spanish
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const track2 = {
      kind: 'captions',
      label: 'Spanish',
      language: 'es',
      src: 'es.vtt'
    };
    const tracks = player.tech_.remoteTextTracks();
    const captionsButton = player.controlBar.getChild('SubsCapsButton');
    let esCaptionMenuItem;
    let enCaptionMenuItem;

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    player.addRemoteTextTrack(track1, true);
    player.addRemoteTextTrack(track2, true);

    // Keep track of menu items
    esCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'es');
    enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');

    // The user chooses Spanish
    player.play();
    esCaptionMenuItem.trigger('click');

    // Track mode changes on user-selection
    assert.ok(
      esCaptionMenuItem.track.mode === 'showing',
      'Spanish should be showing after selection'
    );
    assert.ok(
      enCaptionMenuItem.track.mode === 'disabled',
      'English should be disabled after selecting Spanish'
    );
    assert.deepEqual(
      player.cache_.selectedLanguage,
      { enabled: true, language: 'es', kind: 'captions' }
    );

    // Switch source and remove old tracks
    player.tech_.src({type: 'video/mp4', src: 'http://example.com'});
    while (tracks.length > 0) {
      player.removeRemoteTextTrack(tracks[0]);
    }
    // Add tracks for the new source
    // change the kind of track to subtitles
    track1.kind = 'subtitles';
    track2.kind = 'subtitles';
    const englishTrack = player.addRemoteTextTrack(track1, true).track;
    const spanishTrack = player.addRemoteTextTrack(track2, true).track;

    // Make sure player ready handler runs
    this.clock.tick(1);

    // Keep track of menu items
    esCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'es');
    enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');

    // The user-selection should have persisted
    assert.ok(
      esCaptionMenuItem.track.mode === 'showing',
      'Spanish should remain showing'
    );
    assert.ok(
      enCaptionMenuItem.track.mode === 'disabled',
      'English should remain disabled'
    );
    assert.deepEqual(
      player.cache_.selectedLanguage,
      { enabled: true, language: 'es', kind: 'captions' }
    );

    assert.ok(spanishTrack.mode === 'showing', 'Spanish track remains showing');
    assert.ok(englishTrack.mode === 'disabled', 'English track remains disabled');
    player.dispose();
  });

  QUnit.test('the user-selected language is cleared on turning off captions', function(assert) {
    const player = TestHelpers.makePlayer();
    const track1 = {
      kind: 'captions',
      label: 'English',
      language: 'en',
      src: 'en.vtt'
    };
    const captionsButton = player.controlBar.getChild('SubsCapsButton');

    player.src({type: 'video/mp4', src: 'http://google.com'});
    // manualCleanUp = true by default
    const englishTrack = player.addRemoteTextTrack(track1, true).track;
    // Keep track of menu items
    const enCaptionMenuItem = getMenuItemByLanguage(captionsButton.items, 'en');
    // we know the position of the OffTextTrackMenuItem
    const offMenuItem = captionsButton.items[1];

    // Select English initially
    player.play();
    enCaptionMenuItem.trigger('click');

    assert.deepEqual(
      player.cache_.selectedLanguage,
      { enabled: true, language: 'en', kind: 'captions' }, 'English track is selected'
    );
    assert.ok(englishTrack.mode === 'showing', 'English track should be showing');

    // Select the off button
    offMenuItem.trigger('click');

    assert.deepEqual(
      player.cache_.selectedLanguage,
      { enabled: false }, 'selectedLanguage is cleared'
    );
    assert.ok(englishTrack.mode === 'disabled', 'English track is disabled');
    player.dispose();
  });

  QUnit.test('a color can be constructed from a three digit hex code', function(assert) {
    const hex = '#f0e';

    // f gets mapped to ff -> 255 in decimal,
    // 0 gets mapped to 00 -> 0 in decimal,
    // e gets mapped to ee -> 238 in decimal.
    assert.equal(constructColor(hex, 1), 'rgba(255,0,238,1)');
  });

  QUnit.test('a color can be constructed from a six digit hex code', function(assert) {
    const hex = '#f604e2';

    // f6 -> 246 in decimal,
    // 04 -> 4 in decimal,
    // e2 -> 226 in decimal.
    assert.equal(constructColor(hex, 1), 'rgba(246,4,226,1)');
  });

  QUnit.test('an invalid hex code will throw an error', function(assert) {
    const hex = '#f';

    assert.throws(
      function() {
        constructColor(hex, 1);
      },
      new Error('Invalid color code provided, #f; must be formatted as e.g. #f0e or #f604e2.'),
      'colors must be valid hex codes.'
    );
  });

  QUnit.test('text track display should overlay a video', function(assert) {
    const tag = document.createElement('video');

    tag.width = 320;
    tag.height = 180;
    const player = TestHelpers.makePlayer({}, tag);
    const textTrackDisplay = player.getChild('TextTrackDisplay');
    const textTrackDisplayStyle = textTrackDisplay.el().style;

    assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
    assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');

    // video aspect ratio equal to NaN
    player.tech_.videoWidth = () => 0;
    player.tech_.videoHeight = () => 0;

    assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
    assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');

    // video aspect ratio 2:1
    player.tech_.videoWidth = () => 100;
    player.tech_.videoHeight = () => 50;

    textTrackDisplay.updateDisplayOverlay();

    assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
    assert.ok(textTrackDisplayStyle.insetBlock === '10px', 'text track display style insetBlock equal to 10px');

    // video aspect ratio 4:3
    player.tech_.videoWidth = () => 100;
    player.tech_.videoHeight = () => 75;

    textTrackDisplay.updateDisplayOverlay();

    assert.ok(textTrackDisplayStyle.insetInline === '40px', 'text track display style insetInline equal to 40px');
    assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');

    // video aspect ratio 16:9
    player.tech_.videoWidth = () => 320;
    player.tech_.videoHeight = () => 180;

    textTrackDisplay.updateDisplayOverlay();

    assert.ok(textTrackDisplayStyle.insetInline === '', 'text track display style insetInline equal to empty string');
    assert.ok(textTrackDisplayStyle.insetBlock === '', 'text track display style insetBlock equal to empty string');

    player.dispose();
  });
}


assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:00', 'player progress bar time tooltip is 0:00');

<!DOCTYPE html>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<link rel="stylesheet" type="text/css" href="../css/guides.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700,700italic,600,600italic' rel='stylesheet' type='text/css'><link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.8.0/styles/solarized_light.min.css" />

<link rel="canonical" href="https://docs.videojs.com/tutorial-text-tracks.html">

This documentation is for an outdated version of Video.js. See documentation for the current release.

Text Tracks

Text Tracks are a function of HTML5 video for providing time triggered text to the viewer. Video.js makes tracks work across all browsers. There are currently five types of tracks:

Subtitles: Translations of the dialogue in the video for when audio is available but not understood. Subtitles are shown over the video.

Captions: Transcription of the dialogue, sound effects, musical cues, and other audio information for when the viewer is deaf/hard of hearing, or the video is muted. Captions are also shown over the video.

Chapters: Chapter titles that are used to create navigation within the video. Typically they're in the form of a list of chapters that the viewer can click on to go to a specific chapter.

Descriptions: Text descriptions of what's happening in the video for when the video portion isn't available, because the viewer is blind, not using a screen, or driving and about to crash because they're trying to enjoy a video while driving. Descriptions are read by a screen reader or turned into a separate audio track.

Metadata: Tracks that have data meant for javascript to parse and do something with. These aren't shown to the user.

Creating the Text File

Timed text requires a text file in WebVTT format. This format defines a list of "cues" that have a start time, and end time, and text to display. Microsoft has a builder that can help you get started on the file.

When creating captions, there's also additional [caption formatting techniques] (http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/SCC_FORMAT.HTML#style) that would be good to use, like brackets around sound effects: [ sound effect ]. If you'd like a more in depth style guide for captioning, you can reference the Captioning Key, but keep in mind not all features are supported by WebVTT or (more likely) the Video.js WebVTT implementation.

Adding to Video.js

Once you have your WebVTT file created, you can add it to Video.js using the track tag. Put your track tag after all the source elements, and before any fallback content.

<video id="example_video_1" class="video-js"
  controls preload="auto" width="640" height="264"
  data-setup='{"example_option":true}'>
 <source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4" />
 <source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm" />
 <source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg" />

 <track kind="captions" src="http://example.com/path/to/captions.vtt" srclang="en" label="English" default>

</video>

You can also add tracks programatically.

Subtitles from Another Domain

Because we're pulling in the text track file via Javascript, the same-origin policy applies. If you'd like to have a player served from one domain,
but the text track served from another, you'll need to enable CORS in order to do so.
In addition to enabling CORS on the server serving the text tracks, you will need to add the crossorigin attribute to the video element itself. This attribute has two values anonymous and use-credentials. Most users will want to use anonymous with cross-origin tracks.
It can be added to the video element like so:

<video class="video-js" crossorigin="anonymous">
  <source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">
  <track src="http://example.com/oceans.vtt" kind="captions" srclang="en" label="English">
</video>

One thing to be aware of is that in this case the video files themselves will also needs CORS headers applied to it. This is because some browsers apply the crossorigin attribute to the video source itself and not just the tracks and is considered a security concern by the spec.

Track Attributes

Additional settings for track tags.

kind

One of the five track types listed above. Kind defaults to subtitles if no kind is included.

label

The label for the track that will be show to the user, for example in a menu that list the different languages available for subtitles.

default

The default attribute can be used to have a track default to showing. Otherwise the viewer would need to select their language from the captions or subtitles menu.
NOTE: For chapters, default is required if you want the chapters menu to show.

srclang

The two-letter code (valid BCP 47 language tag) for the language of the text track, for example "en" for English. A list of language codes is available here.

Interacting with Text Tracks

Showing tracks programmatically

Some of you would want to turn captions on and off programmatically rather than just forcing the user to do so themselves. This can be easily achieved by modifying the mode of the text tracks.
The mode can be one of three values disabled, hidden, and showing.
When a text track's mode is disabled, the track does not show on screen as the video is playing.
When the mode is set to showing, the track is visible to the viewer and updates while the video is playing.
You can change of a particular track like so:

let tracks = player.textTracks();

for (let i = 0; i < tracks.length; i++) {
  let track = tracks[i];

  // find the captions track that's in english
  if (track.kind === 'captions' && track.language === 'en') {
    track.mode = 'showing';
  }
}

Doing something when a cue becomes active

Above, we mentioned that mode can also be hidden, what this means is that the track will update
as the video is playing but it won't be visible to the viewer. This is most useful for metadata text tracks.
One usecase for metadata text tracks is to have something happen when their cues become active, to do so, you listen to the cuechange event on the track. These events fire when the mode is showing as well.
Here's an example:

let tracks = player.textTracks();
let metadataTrack;

for (let i = 0; i < tracks.length; i++) {
  let track = tracks[i];

  // find the metadata track that's labeled ads
  if (track.kind === 'captions' && track.label === 'ads') {
    track.mode = 'hidden';
    // store it for usage outside of the loop
    metadataTrack = track;
  }
}

metadataTrack.addEventListener('cuechange', function() {
  player.ads.startLinearAdMode();
});

Emulated Text Tracks

By default, video.js will try and use native text tracks if possible and fall back to emulated text tracks if the native functionality is broken or incomplete or non-existent.
The Flash tech will always use the emulated text track functionality.
The video.js API and TextTrack objects were modeled after the w3c's specification.
video.js uses Mozilla's vtt.js library to parse and display its emulated text tracks.

If you wanted to disable native text track functionality and force video.js to use emulated text tracks always, you can supply the nativeTextTracks option to the tech like so:

let player = videojs('myvideo', {
  html5: {
    nativeTextTracks: false
  }
});

Text Track Settings

When using emulated Text Tracks, captions will have an additional item in the menu called "caption settings".
This allows the viewer of the player to change some styles of how the captions are displayed on screen.

If you don't want that, you can disable it by turning off the text track settings component and hiding the menu item like so:

let player = videojs('myvideo', {
  // make the text track settings dialog not initialize
  textTrackSettings: false
});

/* hide the captions settings item from the captions menu */
.vjs-texttrack-settings {
  display: none;
}

Text Track Precedence

In general, the Descriptions tracks is of lower precedence than captions and subtitles.
What this means for you?

If you are using the default attribute, videojs will choose the first track that is marked as default and turn it on. If There are multiple tracks marked default, it will try and turn on the first captions or subtitles track before any descriptions tracks.

This only applied to the emulated captions support, native text tracks behavior will change depending on the browser

If you select a given track from the menu, videojs will turn off all the other tracks of the same kind. This may seem like you can have both subtitles and captions turned on at the same time but unfortuantely, at this time we only support one track being displayed at a time.

This means that for emulated text tracks, we'll choose the first captions or subtitles track that is enabled to display.

When native text tracks are supported, we will still disable the other tracks of the same kind but it is possible that multiple text tracks are shown.

If a descriptions track is selected and subsequently a subtitles or captions track is selected, the descriptions track is disabled and its menu button is also disabled.

When enabling a track programmatically, there's not much checking that videojs does.

For emulated text tracks, when it's time to display the captions, video.js would choose the first track that's showing, again choosing subtitles or captions over descriptions, if necessary.

For native text tracks, this behavior depends on the browser. Some browsers will let you have multiple text tracks but others will disable all other tracks when a new one is selected.

API

player.textTracks() -> TextTrackList

This is the main interface into the text tracks of the player.
It return a TextTrackList which lists all the tracks on the player.

player.remoteTextTracks() -> TextTrackList

This is a helper method to get a list of all the tracks that were created from track elements or that were added to the player by the addRemoteTextTrack method. All these tracks are removeable from the player, where-as not all tracks from player.textTracks() are necessarily removeable.

player.remoteTextTrackEls() -> HTMLTrackElementList

Another helper method, this is a list of all the track elements associated with the player. Both emulated or otherwise.

player.addTextTrack(String kind, [String label [, String language]]) -> TextTrack

This is based on the w3c spec API and when given a kind and an optional label and language, will create a new text track for you to use.
This method is intended for purely programmatic usage of tracks and has one important limitation:
tracks created using this method cannot be removed. The native addTextTrack does not have a corresponding removeTextTrack, so, we actually discourage the usage of this method.

player.addRemoteTextTrack(Object options) -> HTMLTrackElement

This function takes an options object that looks pretty similar to the track element and returns a HTMLTrackElement.
This object has a track property on it which is the actual TextTrack object.
This TextTrack object is equivalent to the one that can be returned from player.addTextTrack with the added bonus that it can be removed from the player.
Internally, video.js will either add a <track> element for you, or emulate that depending on whether native text tracks are supported or not.
The options available are:

kind

label

language (also srclang)

id

src

player.removeRemoteTextTrack(HTMLTrackElement|TextTrack)

This function takes either an HTMLTrackElement or a TextTrack object and removes it from the player.


<!DOCTYPE html>

  <meta charset="utf-8" />
  <title>Video.js Sandbox</title>
  <link href="../dist/video-js.css" rel="stylesheet" type="text/css">

    <video id='start-test-video' height=264 width=640 src="https://vjs.zencdn.net/v/oceans.mp4" controls></video>

Press play on the video to start tests. (as we need user permission to autoplay)


#define SHADER_NAME PHASER_MULTI_VS

precision highp float;

uniform mat4 uProjectionMatrix;

attribute vec2 inPosition;
attribute vec2 inTexCoord;
attribute float inTexId;
attribute float inTintEffect;
attribute vec4 inTint;

varying vec2 outTexCoord;
varying float outTexId;
varying float outTintEffect;
varying vec4 outTint;

void main ()
{
    gl_Position = uProjectionMatrix * vec4(inPosition, 1.0, 1.0);

    outTexCoord = inTexCoord;
    outTexId = inTexId;
    outTint = inTint;
    outTintEffect = inTintEffect;
}


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Calculate the full bounds, in local and world space, of a BitmapText Game Object.
 *
 * Returns a BitmapTextSize object that contains global and local variants of the Game Objects x and y coordinates and
 * its width and height. Also includes an array of the line lengths and all word positions.
 *
 * The global position and size take into account the Game Object's position and scale.
 *
 * The local position and size just takes into account the font data.
 *
 * @function GetBitmapTextSize
 * @since 3.0.0
 * @private
 *
 * @param {(Phaser.GameObjects.DynamicBitmapText|Phaser.GameObjects.BitmapText)} src - The BitmapText to calculate the bounds values for.
 * @param {boolean} [round=false] - Whether to round the positions to the nearest integer.
 * @param {boolean} [updateOrigin=false] - Whether to update the origin of the BitmapText after bounds calculations?
 * @param {object} [out] - Object to store the results in, to save constant object creation. If not provided an empty object is returned.
 *
 * @return {Phaser.Types.GameObjects.BitmapText.BitmapTextSize} The calculated bounds values of the BitmapText.
 */
var GetBitmapTextSize = function (src, round, updateOrigin, out)
{
    if (updateOrigin === undefined) { updateOrigin = false; }

    if (out === undefined)
    {
        out = {
            local: {
                x: 0,
                y: 0,
                width: 0,
                height: 0
            },
            global: {
                x: 0,
                y: 0,
                width: 0,
                height: 0
            },
            lines: {
                shortest: 0,
                longest: 0,
                lengths: null,
                height: 0
            },
            wrappedText: '',
            words: [],
            characters: [],
            scaleX: 0,
            scaleY: 0
        };

        return out;
    }

    var text = src.text;
    var textLength = text.length;
    var maxWidth = src.maxWidth;
    var wordWrapCharCode = src.wordWrapCharCode;

    var bx = Number.MAX_VALUE;
    var by = Number.MAX_VALUE;
    var bw = 0;
    var bh = 0;

    var chars = src.fontData.chars;
    var lineHeight = src.fontData.lineHeight;
    var letterSpacing = src.letterSpacing;

    var xAdvance = 0;
    var yAdvance = 0;

    var charCode = 0;

    var glyph = null;

    var align = src._align;

    var x = 0;
    var y = 0;

    var scale = (src.fontSize / src.fontData.size);
    var sx = scale * src.scaleX;
    var sy = scale * src.scaleY;

    var lastGlyph = null;
    var lastCharCode = 0;
    var lineWidths = [];
    var shortestLine = Number.MAX_VALUE;
    var longestLine = 0;
    var currentLine = 0;
    var currentLineWidth = 0;

    var i;
    var words = [];
    var characters = [];
    var current = null;

    //  Scan for breach of maxWidth and insert carriage-returns
    if (maxWidth > 0)
    {
        for (i = 0; i < textLength; i++)
        {
            charCode = text.charCodeAt(i);

            if (charCode === 10)
            {
                if (current !== null)
                {
                    words.push({
                        word: current.word,
                        i: current.i,
                        x: current.x * sx,
                        y: current.y * sy,
                        w: current.w * sx,
                        h: current.h * sy,
                        cr: true
                    });

                    current = null;
                }

                xAdvance = 0;
                yAdvance += lineHeight;
                lastGlyph = null;

                continue;
            }

            glyph = chars[charCode];

            if (!glyph)
            {
                continue;
            }

            if (lastGlyph !== null)
            {
                var glyphKerningOffset = glyph.kerning[lastCharCode];
            }

            if (charCode === wordWrapCharCode)
            {
                if (current !== null)
                {
                    words.push({
                        word: current.word,
                        i: current.i,
                        x: current.x * sx,
                        y: current.y * sy,
                        w: current.w * sx,
                        h: current.h * sy,
                        cr: false
                    });

                    current = null;
                }
            }
            else
            {
                if (current === null)
                {
                    //  We're starting a new word, recording the starting index, etc
                    current = { word: '', i: i, x: xAdvance, y: yAdvance, w: 0, h: lineHeight, cr: false };
                }

                current.word = current.word.concat(text[i]);
                current.w += glyph.xOffset + glyph.xAdvance + ((glyphKerningOffset !== undefined) ? glyphKerningOffset : 0);
            }

            xAdvance += glyph.xAdvance + letterSpacing;
            lastGlyph = glyph;
            lastCharCode = charCode;
        }

        //  Last word
        if (current !== null)
        {
            words.push({
                word: current.word,
                i: current.i,
                x: current.x * sx,
                y: current.y * sy,
                w: current.w * sx,
                h: current.h * sy,
                cr: false
            });
        }

        //  Reset for the next loop
        xAdvance = 0;
        yAdvance = 0;
        lastGlyph = null;
        lastCharCode = 0;

        //  Loop through the words array and see if we've got any > maxWidth
        var prev;
        var offset = 0;
        var crs = [];

        for (i = 0; i < words.length; i++)
        {
            var entry = words[i];
            var left = entry.x;
            var right = entry.x + entry.w;

            if (prev)
            {
                var diff = left - (prev.x + prev.w);

                offset = left - (diff + prev.w);

                prev = null;
            }

            var checkLeft = left - offset;
            var checkRight = right - offset;

            if (checkLeft > maxWidth || checkRight > maxWidth)
            {
                crs.push(entry.i - 1);

                if (entry.cr)
                {
                    crs.push(entry.i + entry.word.length);

                    offset = 0;
                    prev = null;
                }
                else
                {
                    prev = entry;
                }
            }
            else if (entry.cr)
            {
                crs.push(entry.i + entry.word.length);

                offset = 0;
                prev = null;
            }
        }

        var stringInsert = function (str, index, value)
        {
            return str.substr(0, index) + value + str.substr(index + 1);
        };

        for (i = crs.length - 1; i >= 0; i--)
        {
            // eslint-disable-next-line quotes
            text = stringInsert(text, crs[i], "\n");
        }

        out.wrappedText = text;

        textLength = text.length;

        //  Recalculated in the next loop
        words = [];
        current = null;
    }

    var charIndex = 0;

    for (i = 0; i < textLength; i++)
    {
        charCode = text.charCodeAt(i);

        if (charCode === 10)
        {
            if (current !== null)
            {
                words.push({
                    word: current.word,
                    i: current.i,
                    x: current.x * sx,
                    y: current.y * sy,
                    w: current.w * sx,
                    h: current.h * sy
                });

                current = null;
            }

            xAdvance = 0;
            yAdvance += lineHeight;
            lastGlyph = null;

            lineWidths[currentLine] = currentLineWidth;

            if (currentLineWidth > longestLine)
            {
                longestLine = currentLineWidth;
            }

            if (currentLineWidth < shortestLine)
            {
                shortestLine = currentLineWidth;
            }

            currentLine++;
            currentLineWidth = 0;

            continue;
        }

        glyph = chars[charCode];

        if (!glyph)
        {
            continue;
        }

        x = xAdvance;
        y = yAdvance;

        if (lastGlyph !== null)
        {
            var kerningOffset = glyph.kerning[lastCharCode];

            x += (kerningOffset !== undefined) ? kerningOffset : 0;
        }

        if (bx > x)
        {
            bx = x;
        }

        if (by > y)
        {
            by = y;
        }

        var gw = x + glyph.xAdvance;
        var gh = y + lineHeight;

        if (bw < gw)
        {
            bw = gw;
        }

        if (bh < gh)
        {
            bh = gh;
        }

        var charWidth = glyph.xOffset + glyph.xAdvance + ((kerningOffset !== undefined) ? kerningOffset : 0);

        if (charCode === wordWrapCharCode)
        {
            if (current !== null)
            {
                words.push({
                    word: current.word,
                    i: current.i,
                    x: current.x * sx,
                    y: current.y * sy,
                    w: current.w * sx,
                    h: current.h * sy
                });

                current = null;
            }
        }
        else
        {
            if (current === null)
            {
                //  We're starting a new word, recording the starting index, etc
                current = { word: '', i: charIndex, x: xAdvance, y: yAdvance, w: 0, h: lineHeight };
            }

            current.word = current.word.concat(text[i]);
            current.w += charWidth;
        }

        characters.push({
            i: charIndex,
            char: text[i],
            code: charCode,
            x: (glyph.xOffset + x) * scale,
            y: (glyph.yOffset + yAdvance) * scale,
            w: glyph.width * scale,
            h: glyph.height * scale,
            t: yAdvance * scale,
            r: gw * scale,
            b: lineHeight * scale,
            line: currentLine,
            glyph: glyph
        });

        xAdvance += glyph.xAdvance + letterSpacing + ((kerningOffset !== undefined) ? kerningOffset : 0);
        lastGlyph = glyph;
        lastCharCode = charCode;
        currentLineWidth = gw * scale;
        charIndex++;
    }

    //  Last word
    if (current !== null)
    {
        words.push({
            word: current.word,
            i: current.i,
            x: current.x * sx,
            y: current.y * sy,
            w: current.w * sx,
            h: current.h * sy
        });
    }

    lineWidths[currentLine] = currentLineWidth;

    if (currentLineWidth > longestLine)
    {
        longestLine = currentLineWidth;
    }

    if (currentLineWidth < shortestLine)
    {
        shortestLine = currentLineWidth;
    }

    //  Adjust all of the character positions based on alignment
    if (align > 0)
    {
        for (var c = 0; c < characters.length; c++)
        {
            var currentChar = characters[c];

            if (align === 1)
            {
                var ax1 = ((longestLine - lineWidths[currentChar.line]) / 2);

                currentChar.x += ax1;
                currentChar.r += ax1;
            }
            else if (align === 2)
            {
                var ax2 = (longestLine - lineWidths[currentChar.line]);

                currentChar.x += ax2;
                currentChar.r += ax2;
            }
        }
    }

    var local = out.local;
    var global = out.global;
    var lines = out.lines;

    local.x = bx * scale;
    local.y = by * scale;
    local.width = bw * scale;
    local.height = bh * scale;

    global.x = (src.x - src._displayOriginX) + (bx * sx);
    global.y = (src.y - src._displayOriginY) + (by * sy);

    global.width = bw * sx;
    global.height = bh * sy;

    lines.shortest = shortestLine;
    lines.longest = longestLine;
    lines.lengths = lineWidths;

    if (round)
    {
        local.x = Math.ceil(local.x);
        local.y = Math.ceil(local.y);
        local.width = Math.ceil(local.width);
        local.height = Math.ceil(local.height);

        global.x = Math.ceil(global.x);
        global.y = Math.ceil(global.y);
        global.width = Math.ceil(global.width);
        global.height = Math.ceil(global.height);

        lines.shortest = Math.ceil(shortestLine);
        lines.longest = Math.ceil(longestLine);
    }

    if (updateOrigin)
    {
        src._displayOriginX = (src.originX * local.width);
        src._displayOriginY = (src.originY * local.height);

        global.x = src.x - (src._displayOriginX * src.scaleX);
        global.y = src.y - (src._displayOriginY * src.scaleY);

        if (round)
        {
            global.x = Math.ceil(global.x);
            global.y = Math.ceil(global.y);
        }
    }

    out.words = words;
    out.characters = characters;
    out.lines.height = lineHeight;
    out.scale = scale;
    out.scaleX = src.scaleX;
    out.scaleY = src.scaleY;

    return out;
};

module.exports = GetBitmapTextSize;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Vector2 = require('./Vector2');

/**
 * Returns a Vector2 containing the x and y position of the given index in a `width` x `height` sized grid.
 *
 * For example, in a 6 x 4 grid, index 16 would equal x: 4 y: 2.
 *
 * If the given index is out of range an empty Vector2 is returned.
 *
 * @function Phaser.Math.ToXY
 * @since 3.19.0
 *
 * @param {number} index - The position within the grid to get the x/y value for.
 * @param {number} width - The width of the grid.
 * @param {number} height - The height of the grid.
 * @param {Phaser.Math.Vector2} [out] - An optional Vector2 to store the result in. If not given, a new Vector2 instance will be created.
 *
 * @return {Phaser.Math.Vector2} A Vector2 where the x and y properties contain the given grid index.
 */
var ToXY = function (index, width, height, out)
{
    if (out === undefined) { out = new Vector2(); }

    var x = 0;
    var y = 0;
    var total = width * height;

    if (index > 0 && index <= total)
    {
        if (index > width - 1)
        {
            y = Math.floor(index / width);
            x = index - (y * width);
        }
        else
        {
            x = index;
        }
    }

    return out.set(x, y);
};

module.exports = ToXY;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Decode base-64 encoded data, for example as exported by Tiled.
 *
 * @function Phaser.Tilemaps.Parsers.Tiled.Base64Decode
 * @since 3.0.0
 *
 * @param {object} data - Base-64 encoded data to decode.
 *
 * @return {array} Array containing the decoded bytes.
 */
var Base64Decode = function (data)
{
    var binaryString = window.atob(data);
    var len = binaryString.length;
    var bytes = new Array(len / 4);

    // Interpret binaryString as an array of bytes representing little-endian encoded uint32 values.
    for (var i = 0; i < len; i += 4)
    {
        bytes[i / 4] = (
            binaryString.charCodeAt(i) |
            binaryString.charCodeAt(i + 1) << 8 |
            binaryString.charCodeAt(i + 2) << 16 |
            binaryString.charCodeAt(i + 3) << 24
        ) >>> 0;
    }

    return bytes;
};

module.exports = Base64Decode;


'#else',
'precision mediump float;',
'#endif',


src/phaser-arcade-physics.js
src/animations/config.json
src/physics/matter-js/lib/
src/physics/matter-js/poly-decomp/
src/polyfills/
src/renderer/webgl/shaders/
src/geom/polygon/Earcut.js
src/utils/array/StableSort.js
src/utils/object/Extend.js
src/structs/RTree.js
src/layer3d/
plugins/spine/dist/
plugins/spine/src/runtimes/
scripts/
webpack.*
webpack.config.js
webpack.dist.config.js
webpack.fb.config.js
webpack.fb.dist.config.js


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var SmoothStep = require('../SmoothStep');

/**
 * A Smooth Step interpolation method.
 *
 * @function Phaser.Math.Interpolation.SmoothStep
 * @since 3.9.0
 * @see {@link https://en.wikipedia.org/wiki/Smoothstep}
 *
 * @param {number} t - The percentage of interpolation, between 0 and 1.
 * @param {number} min - The minimum value, also known as the 'left edge', assumed smaller than the 'right edge'.
 * @param {number} max - The maximum value, also known as the 'right edge', assumed greater than the 'left edge'.
 *
 * @return {number} The interpolated value.
 */
var SmoothStepInterpolation = function (t, min, max)
{
    return min + (max - min) * SmoothStep(t, 0, 1);
};

module.exports = SmoothStepInterpolation;


#define SHADER_NAME PHASER_MULTI_V2_FS
#define numTextures %count%

precision highp float;

uniform sampler2D uMainSampler[%count%];

varying vec2 outTexCoord;
varying float outTexId;
varying float outTintEffect;
varying vec4 outTint;

vec4 getSampler (int index, vec2 uv)
{
    for (int i = 0; i < numTextures; ++i)
    {
        if (i == index)
        {
            return texture2D(uMainSampler[i], uv);
        }
    }

    //  Return black
    return vec4(0);
}

void main ()
{
    vec4 texel = vec4(outTint.bgr * outTint.a, outTint.a);

    vec4 texture = getSampler(int(outTexId), outTexCoord);

    //  Multiply texture tint
    vec4 color = texture * texel;

    if (outTintEffect == 1.0)
    {
        //  Solid color + texture alpha
        color.rgb = mix(texture.rgb, outTint.bgr * outTint.a, texture.a);
    }
    else if (outTintEffect == 2.0)
    {
        //  Solid color, no texture
        color = texel;
    }

    gl_FragColor = color;
}


/**
 * @typedef {object} Phaser.Types.Core.BannerConfig
 * @since 3.0.0
 *
 * @property {boolean} [hidePhaser=false] - Omit Phaser's name and version from the banner.
 * @property {string} [text='#ffffff'] - The color of the banner text.
 * @property {string[]} [background] - The background colors of the banner.
 */


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * A component to set restitution on objects.
 *
 * @namespace Phaser.Physics.Matter.Components.Bounce
 * @since 3.0.0
 */
var Bounce = {

    /**
     * Sets the restitution on the physics object.
     *
     * @method Phaser.Physics.Matter.Components.Bounce#setBounce
     * @since 3.0.0
     *
     * @param {number} value - A Number that defines the restitution (elasticity) of the body. The value is always positive and is in the range (0, 1). A value of 0 means collisions may be perfectly inelastic and no bouncing may occur. A value of 0.8 means the body may bounce back with approximately 80% of its kinetic energy. Note that collision response is based on pairs of bodies, and that restitution values are combined with the following formula: `Math.max(bodyA.restitution, bodyB.restitution)`
     *
     * @return {Phaser.GameObjects.GameObject} This Game Object.
     */
    setBounce: function (value)
    {
        this.body.restitution = value;

        return this;
    }

};

module.exports = Bounce;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var BaseCamera = require('./BaseCamera');
var CenterOn = require('../../geom/rectangle/CenterOn');
var Clamp = require('../../math/Clamp');
var Class = require('../../utils/Class');
var Components = require('../../gameobjects/components');
var Effects = require('./effects');
var Events = require('./events');
var Linear = require('../../math/Linear');
var Rectangle = require('../../geom/rectangle/Rectangle');
var Vector2 = require('../../math/Vector2');

/**
 * @classdesc
 * A Camera.
 *
 * The Camera is the way in which all games are rendered in Phaser. They provide a view into your game world,
 * and can be positioned, rotated, zoomed and scrolled accordingly.
 *
 * A Camera consists of two elements: The viewport and the scroll values.
 *
 * The viewport is the physical position and size of the Camera within your game. Cameras, by default, are
 * created the same size as your game, but their position and size can be set to anything. This means if you
 * wanted to create a camera that was 320x200 in size, positioned in the bottom-right corner of your game,
 * you'd adjust the viewport to do that (using methods like `setViewport` and `setSize`).
 *
 * If you wish to change where the Camera is looking in your game, then you scroll it. You can do this
 * via the properties `scrollX` and `scrollY` or the method `setScroll`. Scrolling has no impact on the
 * viewport, and changing the viewport has no impact on the scrolling.
 *
 * By default a Camera will render all Game Objects it can see. You can change this using the `ignore` method,
 * allowing you to filter Game Objects out on a per-Camera basis.
 *
 * A Camera also has built-in special effects including Fade, Flash and Camera Shake.
 *
 * @class Camera
 * @memberof Phaser.Cameras.Scene2D
 * @constructor
 * @since 3.0.0
 *
 * @extends Phaser.Cameras.Scene2D.BaseCamera
 * @extends Phaser.GameObjects.Components.Pipeline
 *
 * @param {number} x - The x position of the Camera, relative to the top-left of the game canvas.
 * @param {number} y - The y position of the Camera, relative to the top-left of the game canvas.
 * @param {number} width - The width of the Camera, in pixels.
 * @param {number} height - The height of the Camera, in pixels.
 */
var Camera = new Class({

    Extends: BaseCamera,

    Mixins: [
        Components.Pipeline
    ],

    initialize:

    function Camera (x, y, width, height)
    {
        BaseCamera.call(this, x, y, width, height);

        this.postPipelines = [];
        this.pipelineData = {};

        /**
         * Does this Camera allow the Game Objects it renders to receive input events?
         *
         * @name Phaser.Cameras.Scene2D.Camera#inputEnabled
         * @type {boolean}
         * @default true
         * @since 3.0.0
         */
        this.inputEnabled = true;

        /**
         * The Camera Fade effect handler.
         * To fade this camera see the `Camera.fade` methods.
         *
         * @name Phaser.Cameras.Scene2D.Camera#fadeEffect
         * @type {Phaser.Cameras.Scene2D.Effects.Fade}
         * @since 3.5.0
         */
        this.fadeEffect = new Effects.Fade(this);

        /**
         * The Camera Flash effect handler.
         * To flash this camera see the `Camera.flash` method.
         *
         * @name Phaser.Cameras.Scene2D.Camera#flashEffect
         * @type {Phaser.Cameras.Scene2D.Effects.Flash}
         * @since 3.5.0
         */
        this.flashEffect = new Effects.Flash(this);

        /**
         * The Camera Shake effect handler.
         * To shake this camera see the `Camera.shake` method.
         *
         * @name Phaser.Cameras.Scene2D.Camera#shakeEffect
         * @type {Phaser.Cameras.Scene2D.Effects.Shake}
         * @since 3.5.0
         */
        this.shakeEffect = new Effects.Shake(this);

        /**
         * The Camera Pan effect handler.
         * To pan this camera see the `Camera.pan` method.
         *
         * @name Phaser.Cameras.Scene2D.Camera#panEffect
         * @type {Phaser.Cameras.Scene2D.Effects.Pan}
         * @since 3.11.0
         */
        this.panEffect = new Effects.Pan(this);

        /**
         * The Camera Rotate To effect handler.
         * To rotate this camera see the `Camera.rotateTo` method.
         *
         * @name Phaser.Cameras.Scene2D.Camera#rotateToEffect
         * @type {Phaser.Cameras.Scene2D.Effects.RotateTo}
         * @since 3.23.0
         */
        this.rotateToEffect = new Effects.RotateTo(this);

        /**
         * The Camera Zoom effect handler.
         * To zoom this camera see the `Camera.zoom` method.
         *
         * @name Phaser.Cameras.Scene2D.Camera#zoomEffect
         * @type {Phaser.Cameras.Scene2D.Effects.Zoom}
         * @since 3.11.0
         */
        this.zoomEffect = new Effects.Zoom(this);

        /**
         * The linear interpolation value to use when following a target.
         *
         * Can also be set via `setLerp` or as part of the `startFollow` call.
         *
         * The default values of 1 means the camera will instantly snap to the target coordinates.
         * A lower value, such as 0.1 means the camera will more slowly track the target, giving
         * a smooth transition. You can set the horizontal and vertical values independently, and also
         * adjust this value in real-time during your game.
         *
         * Be sure to keep the value between 0 and 1. A value of zero will disable tracking on that axis.
         *
         * @name Phaser.Cameras.Scene2D.Camera#lerp
         * @type {Phaser.Math.Vector2}
         * @since 3.9.0
         */
        this.lerp = new Vector2(1, 1);

        /**
         * The values stored in this property are subtracted from the Camera targets position, allowing you to
         * offset the camera from the actual target x/y coordinates by this amount.
         * Can also be set via `setFollowOffset` or as part of the `startFollow` call.
         *
         * @name Phaser.Cameras.Scene2D.Camera#followOffset
         * @type {Phaser.Math.Vector2}
         * @since 3.9.0
         */
        this.followOffset = new Vector2();

        /**
         * The Camera dead zone.
         *
         * The deadzone is only used when the camera is following a target.
         *
         * It defines a rectangular region within which if the target is present, the camera will not scroll.
         * If the target moves outside of this area, the camera will begin scrolling in order to follow it.
         *
         * The `lerp` values that you can set for a follower target also apply when using a deadzone.
         *
         * You can directly set this property to be an instance of a Rectangle. Or, you can use the
         * `setDeadzone` method for a chainable approach.
         *
         * The rectangle you provide can have its dimensions adjusted dynamically, however, please
         * note that its position is updated every frame, as it is constantly re-centered on the cameras mid point.
         *
         * Calling `setDeadzone` with no arguments will reset an active deadzone, as will setting this property
         * to `null`.
         *
         * @name Phaser.Cameras.Scene2D.Camera#deadzone
         * @type {?Phaser.Geom.Rectangle}
         * @since 3.11.0
         */
        this.deadzone = null;

        /**
         * Internal follow target reference.
         *
         * @name Phaser.Cameras.Scene2D.Camera#_follow
         * @type {?any}
         * @private
         * @default null
         * @since 3.0.0
         */
        this._follow = null;
    },

    /**
     * Sets the Camera dead zone.
     *
     * The deadzone is only used when the camera is following a target.
     *
     * It defines a rectangular region within which if the target is present, the camera will not scroll.
     * If the target moves outside of this area, the camera will begin scrolling in order to follow it.
     *
     * The deadzone rectangle is re-positioned every frame so that it is centered on the mid-point
     * of the camera. This allows you to use the object for additional game related checks, such as
     * testing if an object is within it or not via a Rectangle.contains call.
     *
     * The `lerp` values that you can set for a follower target also apply when using a deadzone.
     *
     * Calling this method with no arguments will reset an active deadzone.
     *
     * @method Phaser.Cameras.Scene2D.Camera#setDeadzone
     * @since 3.11.0
     *
     * @param {number} [width] - The width of the deadzone rectangle in pixels. If not specified the deadzone is removed.
     * @param {number} [height] - The height of the deadzone rectangle in pixels.
     *
     * @return {this} This Camera instance.
     */
    setDeadzone: function (width, height)
    {
        if (width === undefined)
        {
            this.deadzone = null;
        }
        else
        {
            if (this.deadzone)
            {
                this.deadzone.width = width;
                this.deadzone.height = height;
            }
            else
            {
                this.deadzone = new Rectangle(0, 0, width, height);
            }

            if (this._follow)
            {
                var originX = this.width / 2;
                var originY = this.height / 2;

                var fx = this._follow.x - this.followOffset.x;
                var fy = this._follow.y - this.followOffset.y;

                this.midPoint.set(fx, fy);

                this.scrollX = fx - originX;
                this.scrollY = fy - originY;
            }

            CenterOn(this.deadzone, this.midPoint.x, this.midPoint.y);
        }

        return this;
    },

    /**
     * Fades the Camera in from the given color over the duration specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#fadeIn
     * @fires Phaser.Cameras.Scene2D.Events#FADE_IN_START
     * @fires Phaser.Cameras.Scene2D.Events#FADE_IN_COMPLETE
     * @since 3.3.0
     *
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {number} [red=0] - The amount to fade the red channel towards. A value between 0 and 255.
     * @param {number} [green=0] - The amount to fade the green channel towards. A value between 0 and 255.
     * @param {number} [blue=0] - The amount to fade the blue channel towards. A value between 0 and 255.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    fadeIn: function (duration, red, green, blue, callback, context)
    {
        return this.fadeEffect.start(false, duration, red, green, blue, true, callback, context);
    },

    /**
     * Fades the Camera out to the given color over the duration specified.
     * This is an alias for Camera.fade that forces the fade to start, regardless of existing fades.
     *
     * @method Phaser.Cameras.Scene2D.Camera#fadeOut
     * @fires Phaser.Cameras.Scene2D.Events#FADE_OUT_START
     * @fires Phaser.Cameras.Scene2D.Events#FADE_OUT_COMPLETE
     * @since 3.3.0
     *
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {number} [red=0] - The amount to fade the red channel towards. A value between 0 and 255.
     * @param {number} [green=0] - The amount to fade the green channel towards. A value between 0 and 255.
     * @param {number} [blue=0] - The amount to fade the blue channel towards. A value between 0 and 255.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    fadeOut: function (duration, red, green, blue, callback, context)
    {
        return this.fadeEffect.start(true, duration, red, green, blue, true, callback, context);
    },

    /**
     * Fades the Camera from the given color to transparent over the duration specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#fadeFrom
     * @fires Phaser.Cameras.Scene2D.Events#FADE_IN_START
     * @fires Phaser.Cameras.Scene2D.Events#FADE_IN_COMPLETE
     * @since 3.5.0
     *
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {number} [red=0] - The amount to fade the red channel towards. A value between 0 and 255.
     * @param {number} [green=0] - The amount to fade the green channel towards. A value between 0 and 255.
     * @param {number} [blue=0] - The amount to fade the blue channel towards. A value between 0 and 255.
     * @param {boolean} [force=false] - Force the effect to start immediately, even if already running.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    fadeFrom: function (duration, red, green, blue, force, callback, context)
    {
        return this.fadeEffect.start(false, duration, red, green, blue, force, callback, context);
    },

    /**
     * Fades the Camera from transparent to the given color over the duration specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#fade
     * @fires Phaser.Cameras.Scene2D.Events#FADE_OUT_START
     * @fires Phaser.Cameras.Scene2D.Events#FADE_OUT_COMPLETE
     * @since 3.0.0
     *
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {number} [red=0] - The amount to fade the red channel towards. A value between 0 and 255.
     * @param {number} [green=0] - The amount to fade the green channel towards. A value between 0 and 255.
     * @param {number} [blue=0] - The amount to fade the blue channel towards. A value between 0 and 255.
     * @param {boolean} [force=false] - Force the effect to start immediately, even if already running.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    fade: function (duration, red, green, blue, force, callback, context)
    {
        return this.fadeEffect.start(true, duration, red, green, blue, force, callback, context);
    },

    /**
     * Flashes the Camera by setting it to the given color immediately and then fading it away again quickly over the duration specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#flash
     * @fires Phaser.Cameras.Scene2D.Events#FLASH_START
     * @fires Phaser.Cameras.Scene2D.Events#FLASH_COMPLETE
     * @since 3.0.0
     *
     * @param {number} [duration=250] - The duration of the effect in milliseconds.
     * @param {number} [red=255] - The amount to fade the red channel towards. A value between 0 and 255.
     * @param {number} [green=255] - The amount to fade the green channel towards. A value between 0 and 255.
     * @param {number} [blue=255] - The amount to fade the blue channel towards. A value between 0 and 255.
     * @param {boolean} [force=false] - Force the effect to start immediately, even if already running.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    flash: function (duration, red, green, blue, force, callback, context)
    {
        return this.flashEffect.start(duration, red, green, blue, force, callback, context);
    },

    /**
     * Shakes the Camera by the given intensity over the duration specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#shake
     * @fires Phaser.Cameras.Scene2D.Events#SHAKE_START
     * @fires Phaser.Cameras.Scene2D.Events#SHAKE_COMPLETE
     * @since 3.0.0
     *
     * @param {number} [duration=100] - The duration of the effect in milliseconds.
     * @param {(number|Phaser.Math.Vector2)} [intensity=0.05] - The intensity of the shake.
     * @param {boolean} [force=false] - Force the shake effect to start immediately, even if already running.
     * @param {function} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent two arguments: A reference to the camera and a progress amount between 0 and 1 indicating how complete the effect is.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    shake: function (duration, intensity, force, callback, context)
    {
        return this.shakeEffect.start(duration, intensity, force, callback, context);
    },

    /**
     * This effect will scroll the Camera so that the center of its viewport finishes at the given destination,
     * over the duration and with the ease specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#pan
     * @fires Phaser.Cameras.Scene2D.Events#PAN_START
     * @fires Phaser.Cameras.Scene2D.Events#PAN_COMPLETE
     * @since 3.11.0
     *
     * @param {number} x - The destination x coordinate to scroll the center of the Camera viewport to.
     * @param {number} y - The destination y coordinate to scroll the center of the Camera viewport to.
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {(string|function)} [ease='Linear'] - The ease to use for the pan. Can be any of the Phaser Easing constants or a custom function.
     * @param {boolean} [force=false] - Force the pan effect to start immediately, even if already running.
     * @param {Phaser.Types.Cameras.Scene2D.CameraPanCallback} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent four arguments: A reference to the camera, a progress amount between 0 and 1 indicating how complete the effect is,
     * the current camera scroll x coordinate and the current camera scroll y coordinate.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    pan: function (x, y, duration, ease, force, callback, context)
    {
        return this.panEffect.start(x, y, duration, ease, force, callback, context);
    },

    /**
     * This effect will rotate the Camera so that the viewport finishes at the given angle in radians,
     * over the duration and with the ease specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#rotateTo
     * @since 3.23.0
     *
     * @param {number} radians - The destination angle in radians to rotate the Camera viewport to. If the angle is positive then the rotation is clockwise else anticlockwise
     * @param {boolean} [shortestPath=false] - If shortest path is set to true the camera will rotate in the quickest direction clockwise or anti-clockwise.
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {(string|function)} [ease='Linear'] - The ease to use for the rotation. Can be any of the Phaser Easing constants or a custom function.
     * @param {boolean} [force=false] - Force the rotation effect to start immediately, even if already running.
     * @param {CameraRotateCallback} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent four arguments: A reference to the camera, a progress amount between 0 and 1 indicating how complete the effect is,
     * the current camera rotation angle in radians.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {Phaser.Cameras.Scene2D.Camera} This Camera instance.
     */
    rotateTo: function (radians, shortestPath, duration, ease, force, callback, context)
    {
        return this.rotateToEffect.start(radians, shortestPath, duration, ease, force, callback, context);
    },

    /**
     * This effect will zoom the Camera to the given scale, over the duration and with the ease specified.
     *
     * @method Phaser.Cameras.Scene2D.Camera#zoomTo
     * @fires Phaser.Cameras.Scene2D.Events#ZOOM_START
     * @fires Phaser.Cameras.Scene2D.Events#ZOOM_COMPLETE
     * @since 3.11.0
     *
     * @param {number} zoom - The target Camera zoom value.
     * @param {number} [duration=1000] - The duration of the effect in milliseconds.
     * @param {(string|function)} [ease='Linear'] - The ease to use for the pan. Can be any of the Phaser Easing constants or a custom function.
     * @param {boolean} [force=false] - Force the pan effect to start immediately, even if already running.
     * @param {Phaser.Types.Cameras.Scene2D.CameraPanCallback} [callback] - This callback will be invoked every frame for the duration of the effect.
     * It is sent four arguments: A reference to the camera, a progress amount between 0 and 1 indicating how complete the effect is,
     * the current camera scroll x coordinate and the current camera scroll y coordinate.
     * @param {any} [context] - The context in which the callback is invoked. Defaults to the Scene to which the Camera belongs.
     *
     * @return {this} This Camera instance.
     */
    zoomTo: function (zoom, duration, ease, force, callback, context)
    {
        return this.zoomEffect.start(zoom, duration, ease, force, callback, context);
    },

    /**
     * Internal preRender step.
     *
     * @method Phaser.Cameras.Scene2D.Camera#preRender
     * @protected
     * @since 3.0.0
     */
    preRender: function ()
    {
        this.renderList.length = 0;

        var width = this.width;
        var height = this.height;

        var halfWidth = width * 0.5;
        var halfHeight = height * 0.5;

        var zoom = this.zoom;
        var matrix = this.matrix;

        var originX = width * this.originX;
        var originY = height * this.originY;

        var follow = this._follow;
        var deadzone = this.deadzone;

        var sx = this.scrollX;
        var sy = this.scrollY;

        if (deadzone)
        {
            CenterOn(deadzone, this.midPoint.x, this.midPoint.y);
        }

        var emitFollowEvent = false;

        if (this.roundPixels)
        {
            originX = Math.floor(originX);
            originY = Math.floor(originY);
        }

        if (follow && !this.panEffect.isRunning)
        {
            var lerp = this.lerp;

            var fx = follow.x - this.followOffset.x;
            var fy = follow.y - this.followOffset.y;

            if (this.roundPixels)
            {
                fx = Math.floor(fx);
                fy = Math.floor(fy);
            }

            if (deadzone)
            {
                if (fx < deadzone.x)
                {
                    sx = Linear(sx, sx - (deadzone.x - fx), lerp.x);
                }
                else if (fx > deadzone.right)
                {
                    sx = Linear(sx, sx + (fx - deadzone.right), lerp.x);
                }

                if (fy < deadzone.y)
                {
                    sy = Linear(sy, sy - (deadzone.y - fy), lerp.y);
                }
                else if (fy > deadzone.bottom)
                {
                    sy = Linear(sy, sy + (fy - deadzone.bottom), lerp.y);
                }
            }
            else
            {
                sx = Linear(sx, fx - originX, lerp.x);
                sy = Linear(sy, fy - originY, lerp.y);
            }

            emitFollowEvent = true;
        }

        if (this.useBounds)
        {
            sx = this.clampX(sx);
            sy = this.clampY(sy);
        }

        if (this.roundPixels)
        {
            sx = Math.floor(sx);
            sy = Math.floor(sy);
        }

        //  Values are in pixels and not impacted by zooming the Camera
        this.scrollX = sx;
        this.scrollY = sy;

        var midX = sx + halfWidth;
        var midY = sy + halfHeight;

        //  The center of the camera, in world space, so taking zoom into account
        //  Basically the pixel value of what it's looking at in the middle of the cam
        this.midPoint.set(midX, midY);

        var displayWidth = width / zoom;
        var displayHeight = height / zoom;

        var vwx = midX - (displayWidth / 2);
        var vwy = midY - (displayHeight / 2);

        if (this.roundPixels)
        {
            vwx = Math.floor(vwx);
            vwy = Math.floor(vwy);
        }

        this.worldView.setTo(vwx, vwy, displayWidth, displayHeight);

        matrix.applyITRS(Math.floor(this.x + originX), Math.floor(this.y + originY), this.rotation, zoom, zoom);
        matrix.translate(-originX, -originY);

        this.shakeEffect.preRender();

        if (emitFollowEvent)
        {
            this.emit(Events.FOLLOW_UPDATE, this, follow);
        }
    },

    /**
     * Sets the linear interpolation value to use when following a target.
     *
     * The default values of 1 means the camera will instantly snap to the target coordinates.
     * A lower value, such as 0.1 means the camera will more slowly track the target, giving
     * a smooth transition. You can set the horizontal and vertical values independently, and also
     * adjust this value in real-time during your game.
     *
     * Be sure to keep the value between 0 and 1. A value of zero will disable tracking on that axis.
     *
     * @method Phaser.Cameras.Scene2D.Camera#setLerp
     * @since 3.9.0
     *
     * @param {number} [x=1] - The amount added to the horizontal linear interpolation of the follow target.
     * @param {number} [y=1] - The amount added to the vertical linear interpolation of the follow target.
     *
     * @return {this} This Camera instance.
     */
    setLerp: function (x, y)
    {
        if (x === undefined) { x = 1; }
        if (y === undefined) { y = x; }

        this.lerp.set(x, y);

        return this;
    },

    /**
     * Sets the horizontal and vertical offset of the camera from its follow target.
     * The values are subtracted from the targets position during the Cameras update step.
     *
     * @method Phaser.Cameras.Scene2D.Camera#setFollowOffset
     * @since 3.9.0
     *
     * @param {number} [x=0] - The horizontal offset from the camera follow target.x position.
     * @param {number} [y=0] - The vertical offset from the camera follow target.y position.
     *
     * @return {this} This Camera instance.
     */
    setFollowOffset: function (x, y)
    {
        if (x === undefined) { x = 0; }
        if (y === undefined) { y = 0; }

        this.followOffset.set(x, y);

        return this;
    },

    /**
     * Sets the Camera to follow a Game Object.
     *
     * When enabled the Camera will automatically adjust its scroll position to keep the target Game Object
     * in its center.
     *
     * You can set the linear interpolation value used in the follow code.
     * Use low lerp values (such as 0.1) to automatically smooth the camera motion.
     *
     * If you find you're getting a slight "jitter" effect when following an object it's probably to do with sub-pixel
     * rendering of the targets position. This can be rounded by setting the `roundPixels` argument to `true` to
     * force full pixel rounding rendering. Note that this can still be broken if you have specified a non-integer zoom
     * value on the camera. So be sure to keep the camera zoom to integers.
     *
     * @method Phaser.Cameras.Scene2D.Camera#startFollow
     * @since 3.0.0
     *
     * @param {(Phaser.GameObjects.GameObject|object)} target - The target for the Camera to follow.
     * @param {boolean} [roundPixels=false] - Round the camera position to whole integers to avoid sub-pixel rendering?
     * @param {number} [lerpX=1] - A value between 0 and 1. This value specifies the amount of linear interpolation to use when horizontally tracking the target. The closer the value to 1, the faster the camera will track.
     * @param {number} [lerpY=1] - A value between 0 and 1. This value specifies the amount of linear interpolation to use when vertically tracking the target. The closer the value to 1, the faster the camera will track.
     * @param {number} [offsetX=0] - The horizontal offset from the camera follow target.x position.
     * @param {number} [offsetY=0] - The vertical offset from the camera follow target.y position.
     *
     * @return {this} This Camera instance.
     */
    startFollow: function (target, roundPixels, lerpX, lerpY, offsetX, offsetY)
    {
        if (roundPixels === undefined) { roundPixels = false; }
        if (lerpX === undefined) { lerpX = 1; }
        if (lerpY === undefined) { lerpY = lerpX; }
        if (offsetX === undefined) { offsetX = 0; }
        if (offsetY === undefined) { offsetY = offsetX; }

        this._follow = target;

        this.roundPixels = roundPixels;

        lerpX = Clamp(lerpX, 0, 1);
        lerpY = Clamp(lerpY, 0, 1);

        this.lerp.set(lerpX, lerpY);

        this.followOffset.set(offsetX, offsetY);

        var originX = this.width / 2;
        var originY = this.height / 2;

        var fx = target.x - offsetX;
        var fy = target.y - offsetY;

        this.midPoint.set(fx, fy);

        this.scrollX = fx - originX;
        this.scrollY = fy - originY;

        if (this.useBounds)
        {
            this.scrollX = this.clampX(this.scrollX);
            this.scrollY = this.clampY(this.scrollY);
        }

        return this;
    },

    /**
     * Stops a Camera from following a Game Object, if previously set via `Camera.startFollow`.
     *
     * @method Phaser.Cameras.Scene2D.Camera#stopFollow
     * @since 3.0.0
     *
     * @return {this} This Camera instance.
     */
    stopFollow: function ()
    {
        this._follow = null;

        return this;
    },

    /**
     * Resets any active FX, such as a fade, flash or shake. Useful to call after a fade in order to
     * remove the fade.
     *
     * @method Phaser.Cameras.Scene2D.Camera#resetFX
     * @since 3.0.0
     *
     * @return {this} This Camera instance.
     */
    resetFX: function ()
    {
        this.rotateToEffect.reset();
        this.panEffect.reset();
        this.shakeEffect.reset();
        this.flashEffect.reset();
        this.fadeEffect.reset();

        return this;
    },

    /**
     * Internal method called automatically by the Camera Manager.
     *
     * @method Phaser.Cameras.Scene2D.Camera#update
     * @protected
     * @since 3.0.0
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time, in ms, elapsed since the last frame.
     */
    update: function (time, delta)
    {
        if (this.visible)
        {
            this.rotateToEffect.update(time, delta);
            this.panEffect.update(time, delta);
            this.zoomEffect.update(time, delta);
            this.shakeEffect.update(time, delta);
            this.flashEffect.update(time, delta);
            this.fadeEffect.update(time, delta);
        }
    },

    /**
     * Destroys this Camera instance. You rarely need to call this directly.
     *
     * Called by the Camera Manager. If you wish to destroy a Camera please use `CameraManager.remove` as
     * cameras are stored in a pool, ready for recycling later, and calling this directly will prevent that.
     *
     * @method Phaser.Cameras.Scene2D.Camera#destroy
     * @fires Phaser.Cameras.Scene2D.Events#DESTROY
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.resetFX();

        BaseCamera.prototype.destroy.call(this);

        this._follow = null;

        this.deadzone = null;
    }

});

module.exports = Camera;


'precision highp float;',
'',
'uniform mat4 uProjectionMatrix;',
'',

module.exports = [
    '#define SHADER_NAME PHASER_MULTI_V2_FS',
    '#define numTextures %count%',
    '',
    'precision highp float;',
    '',
    'uniform sampler2D uMainSampler[%count%];',
    '',
    'varying vec2 outTexCoord;',
    'varying float outTexId;',
    'varying float outTintEffect;',
    'varying vec4 outTint;',
    '',
    'vec4 getSampler (int index, vec2 uv)',
    '{',
    '    for (int i = 0; i < numTextures; ++i)',
    '    {',
    '        if (i == index)',
    '        {',
    '            return texture2D(uMainSampler[i], uv);',
    '        }',
    '    }',
    '',
    '    //  Return black',
    '    return vec4(0);',
    '}',
    '',
    'void main ()',
    '{',
    '    vec4 texel = vec4(outTint.bgr * outTint.a, outTint.a);',
    '',
    '    vec4 texture = getSampler(int(outTexId), outTexCoord);',
    '',
    '    //  Multiply texture tint',
    '    vec4 color = texture * texel;',
    '',
    '    if (outTintEffect == 1.0)',
    '    {',
    '        //  Solid color + texture alpha',
    '        color.rgb = mix(texture.rgb, outTint.bgr * outTint.a, texture.a);',
    '    }',
    '    else if (outTintEffect == 2.0)',
    '    {',
    '        //  Solid color, no texture',
    '        color = texel;',
    '    }',
    '',
    '    gl_FragColor = color;',
    '}',
    ''
].join('\n');


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Checks if two Rectangles intersect.
 *
 * A Rectangle intersects another Rectangle if any part of its bounds is within the other Rectangle's bounds.
 * As such, the two Rectangles are considered "solid".
 * A Rectangle with no width or no height will never intersect another Rectangle.
 *
 * @function Phaser.Geom.Intersects.RectangleToRectangle
 * @since 3.0.0
 *
 * @param {Phaser.Geom.Rectangle} rectA - The first Rectangle to check for intersection.
 * @param {Phaser.Geom.Rectangle} rectB - The second Rectangle to check for intersection.
 *
 * @return {boolean} `true` if the two Rectangles intersect, otherwise `false`.
 */
var RectangleToRectangle = function (rectA, rectB)
{
    if (rectA.width <= 0 || rectA.height <= 0 || rectB.width <= 0 || rectB.height <= 0)
    {
        return false;
    }

    return !(rectA.right < rectB.x || rectA.bottom < rectB.y || rectA.x > rectB.right || rectA.y > rectB.bottom);
};

module.exports = RectangleToRectangle;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Provides methods for setting the gravity properties of an Arcade Physics Game Object.
 * Should be applied as a mixin and not used directly.
 *
 * @namespace Phaser.Physics.Arcade.Components.Gravity
 * @since 3.0.0
 */
var Gravity = {

    /**
     * Set the X and Y values of the gravitational pull to act upon this Arcade Physics Game Object. Values can be positive or negative. Larger values result in a stronger effect.
     *
     * If only one value is provided, this value will be used for both the X and Y axis.
     *
     * @method Phaser.Physics.Arcade.Components.Gravity#setGravity
     * @since 3.0.0
     *
     * @param {number} x - The gravitational force to be applied to the X-axis.
     * @param {number} [y=x] - The gravitational force to be applied to the Y-axis. If this is not specified, the X value will be used.
     *
     * @return {this} This Game Object.
     */
    setGravity: function (x, y)
    {
        this.body.gravity.set(x, y);

        return this;
    },

    /**
     * Set the gravitational force to be applied to the X axis. Value can be positive or negative. Larger values result in a stronger effect.
     *
     * @method Phaser.Physics.Arcade.Components.Gravity#setGravityX
     * @since 3.0.0
     *
     * @param {number} x - The gravitational force to be applied to the X-axis.
     *
     * @return {this} This Game Object.
     */
    setGravityX: function (x)
    {
        this.body.gravity.x = x;

        return this;
    },

    /**
     * Set the gravitational force to be applied to the Y axis. Value can be positive or negative. Larger values result in a stronger effect.
     *
     * @method Phaser.Physics.Arcade.Components.Gravity#setGravityY
     * @since 3.0.0
     *
     * @param {number} y - The gravitational force to be applied to the Y-axis.
     *
     * @return {this} This Game Object.
     */
    setGravityY: function (y)
    {
        this.body.gravity.y = y;

        return this;
    }

};

module.exports = Gravity;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Class = require('../../utils/Class');

/**
 * @classdesc
 * Contains information about a specific Gamepad Axis.
 * Axis objects are created automatically by the Gamepad as they are needed.
 *
 * @class Axis
 * @memberof Phaser.Input.Gamepad
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Input.Gamepad.Gamepad} pad - A reference to the Gamepad that this Axis belongs to.
 * @param {number} index - The index of this Axis.
 */
var Axis = new Class({

    initialize:

    function Axis (pad, index)
    {
        /**
         * A reference to the Gamepad that this Axis belongs to.
         *
         * @name Phaser.Input.Gamepad.Axis#pad
         * @type {Phaser.Input.Gamepad.Gamepad}
         * @since 3.0.0
         */
        this.pad = pad;

        /**
         * An event emitter to use to emit the axis events.
         *
         * @name Phaser.Input.Gamepad.Axis#events
         * @type {Phaser.Events.EventEmitter}
         * @since 3.0.0
         */
        this.events = pad.events;

        /**
         * The index of this Axis.
         *
         * @name Phaser.Input.Gamepad.Axis#index
         * @type {number}
         * @since 3.0.0
         */
        this.index = index;

        /**
         * The raw axis value, between -1 and 1 with 0 being dead center.
         * Use the method `getValue` to get a normalized value with the threshold applied.
         *
         * @name Phaser.Input.Gamepad.Axis#value
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.value = 0;

        /**
         * Movement tolerance threshold below which axis values are ignored in `getValue`.
         *
         * @name Phaser.Input.Gamepad.Axis#threshold
         * @type {number}
         * @default 0.1
         * @since 3.0.0
         */
        this.threshold = 0.1;
    },

    /**
     * Internal update handler for this Axis.
     * Called automatically by the Gamepad as part of its update.
     *
     * @method Phaser.Input.Gamepad.Axis#update
     * @private
     * @since 3.0.0
     *
     * @param {number} value - The value of the axis movement.
     */
    update: function (value)
    {
        this.value = value;
    },

    /**
     * Applies the `threshold` value to the axis and returns it.
     *
     * @method Phaser.Input.Gamepad.Axis#getValue
     * @since 3.0.0
     *
     * @return {number} The axis value, adjusted for the movement threshold.
     */
    getValue: function ()
    {
        return (Math.abs(this.value) < this.threshold) ? 0 : this.value;
    },

    /**
     * Destroys this Axis instance and releases external references it holds.
     *
     * @method Phaser.Input.Gamepad.Axis#destroy
     * @since 3.10.0
     */
    destroy: function ()
    {
        this.pad = null;
        this.events = null;
    }

});

module.exports = Axis;


#define SHADER_NAME PHASER_MULTI_V2_FS
#define numTextures %count%
precision highp float;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Calculates the area of the given Rectangle object.
 *
 * @function Phaser.Geom.Rectangle.Area
 * @since 3.0.0
 *
 * @param {Phaser.Geom.Rectangle} rect - The rectangle to calculate the area of.
 *
 * @return {number} The area of the Rectangle object.
 */
var Area = function (rect)
{
    return rect.width * rect.height;
};

module.exports = Area;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * @namespace Phaser.Cameras.Scene2D.Effects
 */

module.exports = {

    Fade: require('./Fade'),
    Flash: require('./Flash'),
    Pan: require('./Pan'),
    Shake: require('./Shake'),
    RotateTo: require('./RotateTo'),
    Zoom: require('./Zoom')

};


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Parses out the Wangset information from Tiled 1.1.5+ map data, if present.
 *
 * Since a given tile can be in more than one wangset, the resulting properties
 * are nested. `tile.data.wangid[someWangsetName]` will return the array-based wang id in
 * this implementation.
 *
 * Note that we're not guaranteed that there will be any 'normal' tiles if the only
 * thing in the tilset are wangtile definitions, so this has to be parsed separately.
 *
 * See https://doc.mapeditor.org/en/latest/manual/using-wang-tiles/ for more information.
 *
 * @function Phaser.Tilemaps.Parsers.Tiled.ParseWangsets
 * @since 3.53.0
 *
 * @param {Array.<object>} wangsets - The array of wangset objects (parsed from JSON)
 * @param {object} datas - The field into which to put wangset data from Tiled.
 *
 * @return {object} An object containing the tileset and image collection data.
 */
var ParseWangsets = function (wangsets, datas)
{
    for (var w = 0; w < wangsets.length; w++)
    {
        var wangset = wangsets[w];
        var identifier = w;

        if (wangset.name && wangset.name !== '')
        {
            identifier = wangset.name;
        }

        if (Array.isArray(wangset.wangtiles) && wangset.wangtiles.length > 0)
        {
            var edgeColors = {};
            var cornerColors = {};

            var c;
            var color;
            var colorIndex;

            // Tiled before v2020.09.09
            if (Array.isArray(wangset.edgecolors))
            {
                for (c = 0; c < wangset.edgecolors.length; c++)
                {
                    colorIndex = 1 + c;
                    color = wangset.edgecolors[c];

                    if (color.name !== '')
                    {
                        edgeColors[colorIndex] = color.name;
                    }
                }
            }

            if (Array.isArray(wangset.cornercolors))
            {
                for (c = 0; c < wangset.cornercolors.length; c++)
                {
                    colorIndex = 1 + c;
                    color = wangset.cornercolors[c];

                    if (color.name !== '')
                    {
                        cornerColors[colorIndex] = color.name;
                    }
                }
            }

            // Tiled after v2020.09.09
            if (Array.isArray(wangset.colors))
            {
                for (c = 0; c < wangset.colors.length; c++)
                {
                    color = wangset.colors[c];
                    colorIndex = 1 + c;

                    if (color.name !== '')
                    {
                        edgeColors[colorIndex] = cornerColors[colorIndex] = color.name;
                    }
                }
            }

            // The wangid layout is north, northeast, east, southeast, etc.
            var idLayout = [
                edgeColors, cornerColors, edgeColors, cornerColors,
                edgeColors, cornerColors, edgeColors, cornerColors
            ];

            for (var t = 0; t < wangset.wangtiles.length; t++)
            {
                var wangtile = wangset.wangtiles[t];

                var obj = (datas[wangtile.tileid] || (datas[wangtile.tileid] = {}));

                obj = (obj.wangid || (obj.wangid = {}));

                var wangid = [];

                for (var i = 0; i < Math.min(idLayout.length, wangtile.wangid.length); i++)
                {
                    color = wangtile.wangid[i];

                    if (color === 0)
                    {
                        wangid.push(undefined);
                        continue;
                    }

                    var renamed = idLayout[i][color];

                    if (renamed !== undefined)
                    {
                        wangid.push(renamed);
                        continue;
                    }

                    wangid.push(color);
                }

                obj[identifier] = wangid;
            }
        }
    }
};

module.exports = ParseWangsets;


module.exports = [
    '#define SHADER_NAME PHASER_MULTI_VS',
    '',
    'precision highp float;',
    '',
    'uniform mat4 uProjectionMatrix;',
    '',
    'attribute vec2 inPosition;',
    'attribute vec2 inTexCoord;',
    'attribute float inTexId;',
    'attribute float inTintEffect;',
    'attribute vec4 inTint;',
    '',
    'varying vec2 outTexCoord;',
    'varying float outTexId;',
    'varying float outTintEffect;',
    'varying vec4 outTint;',
    '',
    'void main ()',
    '{',
    '    gl_Position = uProjectionMatrix * vec4(inPosition, 1.0, 1.0);',
    '',
    '    outTexCoord = inTexCoord;',
    '    outTexId = inTexId;',
    '    outTint = inTint;',
    '    outTintEffect = inTintEffect;',
    '}',
    ''
].join('\n');


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var RotateMatrix = require('./RotateMatrix');

/**
 * Rotates the array matrix to the left (or 90 degrees)
 *
 * A matrix is a two-dimensional array (array of arrays), where all sub-arrays (rows)
 * have the same length. There must be at least two rows. This is an example matrix:
 *
 * ```
 * [
 *    [ 1, 1, 1, 1, 1, 1 ],
 *    [ 2, 0, 0, 0, 0, 4 ],
 *    [ 2, 0, 1, 2, 0, 4 ],
 *    [ 2, 0, 3, 4, 0, 4 ],
 *    [ 2, 0, 0, 0, 0, 4 ],
 *    [ 3, 3, 3, 3, 3, 3 ]
 * ]
 * ```
 *
 * @function Phaser.Utils.Array.Matrix.RotateLeft
 * @since 3.0.0
 *
 * @generic T
 * @genericUse {T[][]} - [matrix,$return]
 *
 * @param {T[][]} [matrix] - The array to rotate.
 *
 * @return {T[][]} The rotated matrix array. The source matrix should be discard for the returned matrix.
 */
var RotateLeft = function (matrix)
{
    return RotateMatrix(matrix, 90);
};

module.exports = RotateLeft;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var HexStringToColor = require('./HexStringToColor');
var IntegerToColor = require('./IntegerToColor');
var ObjectToColor = require('./ObjectToColor');
var RGBStringToColor = require('./RGBStringToColor');

/**
 * Converts the given source color value into an instance of a Color class.
 * The value can be either a string, prefixed with `rgb` or a hex string, a number or an Object.
 *
 * @function Phaser.Display.Color.ValueToColor
 * @since 3.0.0
 *
 * @param {(string|number|Phaser.Types.Display.InputColorObject)} input - The source color value to convert.
 *
 * @return {Phaser.Display.Color} A Color object.
 */
var ValueToColor = function (input)
{
    var t = typeof input;

    switch (t)
    {
        case 'string':

            if (input.substr(0, 3).toLowerCase() === 'rgb')
            {
                return RGBStringToColor(input);
            }
            else
            {
                return HexStringToColor(input);
            }

        case 'number':

            return IntegerToColor(input);

        case 'object':

            return ObjectToColor(input);
    }
};

module.exports = ValueToColor;


/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

/**
 * Returns the amount the Game Object is visually offset from its x coordinate.
 * This is the same as `width * origin.x`.
 * This value will only be > 0 if `origin.x` is not equal to zero.
 *
 * @function Phaser.Display.Bounds.GetOffsetX
 * @since 3.0.0
 *
 * @param {Phaser.GameObjects.GameObject} gameObject - The Game Object to get the bounds value from.
 *
 * @return {number} The horizontal offset of the Game Object.
 */
var GetOffsetX = function (gameObject)
{
    return gameObject.width * gameObject.originX;
};

module.exports = GetOffsetX;


#define SHADER_NAME PHASER_MULTI_VS
precision highp float;