<commit_before>(function(root) {
  'use strict';

  function _isFunction(v) {
    return typeof v === 'function';
  }

  function _result(v) {
    return _isFunction(v) ? v() : v;
  }

  var toString = Object.prototype.toString;

  function Spectrogram(canvas, options) {
    if (!(this instanceof Spectrogram)) {
      return new Spectrogram(canvas, options);
    }

    var baseCanvasOptions = options.canvas || {};
    this._audioEnded = null;
    this._paused = null;
    this._pausedAt = null;
    this._startedAt = null;
    this._sources = {
      audioBufferStream: null,
      userMediaStream: null
    };
    this._baseCanvas = canvas;
    this._baseCanvasContext = this._baseCanvas.getContext('2d');

    this._baseCanvas.width = _result(baseCanvasOptions.width) || this._baseCanvas.width;
    this._baseCanvas.height = _result(baseCanvasOptions.height) || this._baseCanvas.height;

    window.onresize = function() {
      this._baseCanvas.width = _result(baseCanvasOptions.width) || this._baseCanvas.width;
      this._baseCanvas.height = _result(baseCanvasOptions.height) || this._baseCanvas.height;
    }.bind(this);

    var audioOptions = options.audio || {};
    this.audio = audioOptions;

    var colors = [];

    if (typeof options.colors === 'function') {
      colors = options.colors(275);
    } else {
      colors = this._generateDefaultColors(275);
    }

    this._colors = colors;

    this._baseCanvasContext.fillStyle = this._getColor(0);
    this._baseCanvasContext.fillRect(0, 0, this._baseCanvas.width, this._baseCanvas.height);
  }

  Spectrogram.prototype._init = function() {
    var source = this._sources.audioBufferStream;
    source.scriptNode = source.audioContext.createScriptProcessor(2048, 1, 1);
    source.scriptNode.connect(source.audioContext.destination);
    source.scriptNode.onaudioprocess = function(event) {
      var array = new Uint8Array(source.analyser.frequencyBinCount);
      source.analyser.getByteFrequencyData(array);

      this._draw(array, source.canvasContext);
    }.bind(this);

    source.sourceNode.onended = function() {
      this.stop();
    }.bind(this);

    source.analyser = source.audioContext.createAnalyser();
    source.analyser.smoothingTimeConstant = 0;
    source.analyser.fftSize = 1024;

    source.analyser.connect(source.scriptNode);
    source.sourceNode.connect(source.analyser);
    if (this.audio.enable) {
      source.sourceNode.connect(source.audioContext.destination);
    }
  };

  Spectrogram.prototype._draw = function(array, canvasContext) {
      if (this._paused) {
        return false;
      }

      var canvas = canvasContext.canvas;
      var width = canvas.width;
      var height = canvas.height;
      var tempCanvasContext = canvasContext._tempContext;
      var tempCanvas = tempCanvasContext.canvas;
      tempCanvasContext.drawImage(canvas, 0, 0, width, height);

      for (var i = 0; i < array.length; i++) {
        var value = array[i];
        canvasContext.fillStyle = this._getColor(value);
        if (this._audioEnded) {
          canvasContext.fillStyle = this._getColor(0);
        }
        canvasContext.fillRect(width - 1, height - i, 1, 1);
      }

      canvasContext.translate(-1, 0);
      // draw prev canvas before translation
      canvasContext.drawImage(tempCanvas, 0, 0, width, height, 0, 0, width, height);
      canvasContext.drawImage(tempCanvas, 0, 0, width, height, 0, 0, width, height);
      // reset transformation matrix
      canvasContext.setTransform(1, 0, 0, 1, 0, 0);

      this._baseCanvasContext.drawImage(canvas, 0, 0, width, height);
  };

  Spectrogram.prototype._startMediaStreamDraw = function(analyser, canvasContext) {
    window.requestAnimationFrame(this._startMediaStreamDraw.bind(this, analyser, canvasContext));
    var audioData = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(audioData);
    this._draw(audioData, canvasContext);
  };

  Spectrogram.prototype.connectSource = function(audioBuffer, audioContext) {
    var source = this._sources.audioBufferStream || {};

    // clear current audio process
    if (toString.call(source.scriptNode) === '[object ScriptProcessorNode]') {
      source.scriptNode.onaudioprocess = null;
    }

    if (toString.call(audioBuffer) === '[object AudioBuffer]') {
      audioContext = (!audioContext && source.audioBuffer.context) || (!audioContext && source.audioContext) || audioContext;

      var sourceNode = audioContext.createBufferSource();
      sourceNode.buffer = audioBuffer;

      var canvasContext = source.canvasContext;

      if (!source.canvasContext) {
        var canvas = document.createElement('canvas');
        canvas.width = this._baseCanvas.width;
        canvas.height = this._baseCanvas.height;
        canvasContext = canvas.getContext('2d');

        var tempCanvas = document.createElement('canvas');
        tempCanvas.width = canvas.width;
        tempCanvas.height = canvas.height;

        canvasContext._tempContext = tempCanvas.getContext('2d');
      }

      source = {
        audioBuffer: audioBuffer,
        audioContext: audioContext,
        sourceNode: sourceNode,
        analyser: null,
        scriptNode: null,
        canvasContext: canvasContext
      };

      this._sources.audioBufferStream = source;
      this._init();
    }

    if (toString.call(audioBuffer) === '[object AnalyserNode]') {
      source = this._sources.userMediaStream || {};
      source.analyser = audioBuffer;
      this._sources.userMediaStream = source;
    }
  };

  Spectrogram.prototype.start = function(offset) {
    var source = this._sources.audioBufferStream;
    var sourceMedia = this._sources.userMediaStream;

    if (source && source.sourceNode) {
      source.sourceNode.start(0, offset||0);
      this._audioEnded = false;
      this._paused = false;
      this._startedAt = Date.now();
    }

    // media stream uses an analyser for audio data
    if (sourceMedia && sourceMedia.analyser) {
      source = sourceMedia;
      var canvas = document.createElement('canvas');
      canvas.width = this._baseCanvas.width;
      canvas.height = this._baseCanvas.height;
      var canvasContext = canvas.getContext('2d');

      var tempCanvas = document.createElement('canvas');
      tempCanvas.width = canvas.width;
      tempCanvas.height = canvas.height;

      canvasContext._tempContext = tempCanvas.getContext('2d');

      this._startMediaStreamDraw(source.analyser, canvasContext);
    }
  };

  Spectrogram.prototype.stop = function() {
    var source = this._sources[0];
    if (source && source.sourceNode) {
      source.sourceNode.stop();
    }
    this._audioEnded = true;
  };

  Spectrogram.prototype.pause = function() {
    this.stop();
    this._paused = true;
    this._pausedAt = Date.now() - this._startedAt;
  };

  Spectrogram.prototype.resume = function(offset) {
    var source = this._sources[0];
    this._paused = false;
    if (this._pausedAt) {
		  this._startedAt = Date.now() - this._pausedAt;
      this.connectSource(source.audioBuffer, source.audioContext);
      this.start(offset || (this._pausedAt / 1000));
    }
  };

  Spectrogram.prototype.clear = function(canvasContext) {
    var source = this._sources[0];

    this.stop();

    if (toString.call(source.scriptNode) === '[object ScriptProcessorNode]') {
      source.scriptNode.onaudioprocess = null;
    }

    canvasContext = canvasContext || source.canvasContext;
    var canvas = canvasContext.canvas;
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);
    canvasContext._tempContext.clearRect(0, 0, canvas.width, canvas.height);
    this._baseCanvasContext.clearRect(0, 0, canvas.width, canvas.height);
  };

  Spectrogram.prototype._generateDefaultColors = function(steps) {
    var frequency = Math.PI / steps;
    var amplitude = 127;
    var center = 128;
    var slice = (Math.PI / 2) * 3.1;
    var colors = [];

    function toRGBString(v) {
      return 'rgba(' + [v,v,v,1].toString() + ')';
    }

    for (var i = 0; i < steps; i++) {
      var v = (Math.sin((frequency * i) + slice) * amplitude + center) >> 0;

      colors.push(toRGBString(v));
    }

    return colors;
  };

  Spectrogram.prototype._getColor = function(index) {
    var color = this._colors[index>>0];

    if (typeof color === 'undefined') {
      color = this._colors[0];
    }

    return color;
  };

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = Spectrogram;
    }
    exports.Spectrogram = Spectrogram;
  } else if (typeof define === 'function' && define.amd) {
    define([], function() {
      return Spectrogram;
    });
  } else {
    root.Spectrogram = Spectrogram;
  }

})(this);
<commit_msg>fixed sources object access + pausedAt flag calculation<commit_after>(function(root) {
  'use strict';

  function _isFunction(v) {
    return typeof v === 'function';
  }

  function _result(v) {
    return _isFunction(v) ? v() : v;
  }

  var toString = Object.prototype.toString;

  function Spectrogram(canvas, options) {
    if (!(this instanceof Spectrogram)) {
      return new Spectrogram(canvas, options);
    }

    var baseCanvasOptions = options.canvas || {};
    this._audioEnded = null;
    this._paused = null;
    this._pausedAt = null;
    this._startedAt = null;
    this._sources = {
      audioBufferStream: null,
      userMediaStream: null
    };
    this._baseCanvas = canvas;
    this._baseCanvasContext = this._baseCanvas.getContext('2d');

    this._baseCanvas.width = _result(baseCanvasOptions.width) || this._baseCanvas.width;
    this._baseCanvas.height = _result(baseCanvasOptions.height) || this._baseCanvas.height;

    window.onresize = function() {
      this._baseCanvas.width = _result(baseCanvasOptions.width) || this._baseCanvas.width;
      this._baseCanvas.height = _result(baseCanvasOptions.height) || this._baseCanvas.height;
    }.bind(this);

    var audioOptions = options.audio || {};
    this.audio = audioOptions;

    var colors = [];

    if (typeof options.colors === 'function') {
      colors = options.colors(275);
    } else {
      colors = this._generateDefaultColors(275);
    }

    this._colors = colors;

    this._baseCanvasContext.fillStyle = this._getColor(0);
    this._baseCanvasContext.fillRect(0, 0, this._baseCanvas.width, this._baseCanvas.height);
  }

  Spectrogram.prototype._init = function() {
    var source = this._sources.audioBufferStream;
    source.scriptNode = source.audioContext.createScriptProcessor(2048, 1, 1);
    source.scriptNode.connect(source.audioContext.destination);
    source.scriptNode.onaudioprocess = function(event) {
      var array = new Uint8Array(source.analyser.frequencyBinCount);
      source.analyser.getByteFrequencyData(array);

      this._draw(array, source.canvasContext);
    }.bind(this);

    source.sourceNode.onended = function() {
      this.stop();
    }.bind(this);

    source.analyser = source.audioContext.createAnalyser();
    source.analyser.smoothingTimeConstant = 0;
    source.analyser.fftSize = 1024;

    source.analyser.connect(source.scriptNode);
    source.sourceNode.connect(source.analyser);
    if (this.audio.enable) {
      source.sourceNode.connect(source.audioContext.destination);
    }
  };

  Spectrogram.prototype._draw = function(array, canvasContext) {
      if (this._paused) {
        return false;
      }

      var canvas = canvasContext.canvas;
      var width = canvas.width;
      var height = canvas.height;
      var tempCanvasContext = canvasContext._tempContext;
      var tempCanvas = tempCanvasContext.canvas;
      tempCanvasContext.drawImage(canvas, 0, 0, width, height);

      for (var i = 0; i < array.length; i++) {
        var value = array[i];
        canvasContext.fillStyle = this._getColor(value);
        if (this._audioEnded) {
          canvasContext.fillStyle = this._getColor(0);
        }
        canvasContext.fillRect(width - 1, height - i, 1, 1);
      }

      canvasContext.translate(-1, 0);
      // draw prev canvas before translation
      canvasContext.drawImage(tempCanvas, 0, 0, width, height, 0, 0, width, height);
      canvasContext.drawImage(tempCanvas, 0, 0, width, height, 0, 0, width, height);
      // reset transformation matrix
      canvasContext.setTransform(1, 0, 0, 1, 0, 0);

      this._baseCanvasContext.drawImage(canvas, 0, 0, width, height);
  };

  Spectrogram.prototype._startMediaStreamDraw = function(analyser, canvasContext) {
    window.requestAnimationFrame(this._startMediaStreamDraw.bind(this, analyser, canvasContext));
    var audioData = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(audioData);
    this._draw(audioData, canvasContext);
  };

  Spectrogram.prototype.connectSource = function(audioBuffer, audioContext) {
    var source = this._sources.audioBufferStream || {};

    // clear current audio process
    if (toString.call(source.scriptNode) === '[object ScriptProcessorNode]') {
      source.scriptNode.onaudioprocess = null;
    }

    if (toString.call(audioBuffer) === '[object AudioBuffer]') {
      audioContext = (!audioContext && source.audioBuffer.context) || (!audioContext && source.audioContext) || audioContext;

      var sourceNode = audioContext.createBufferSource();
      sourceNode.buffer = audioBuffer;

      var canvasContext = source.canvasContext;

      if (!source.canvasContext) {
        var canvas = document.createElement('canvas');
        canvas.width = this._baseCanvas.width;
        canvas.height = this._baseCanvas.height;
        canvasContext = canvas.getContext('2d');

        var tempCanvas = document.createElement('canvas');
        tempCanvas.width = canvas.width;
        tempCanvas.height = canvas.height;

        canvasContext._tempContext = tempCanvas.getContext('2d');
      }

      source = {
        audioBuffer: audioBuffer,
        audioContext: audioContext,
        sourceNode: sourceNode,
        analyser: null,
        scriptNode: null,
        canvasContext: canvasContext
      };

      this._sources.audioBufferStream = source;
      this._init();
    }

    if (toString.call(audioBuffer) === '[object AnalyserNode]') {
      source = this._sources.userMediaStream || {};
      source.analyser = audioBuffer;
      this._sources.userMediaStream = source;
    }
  };

  Spectrogram.prototype.start = function(offset) {
    var source = this._sources.audioBufferStream;
    var sourceMedia = this._sources.userMediaStream;

    if (source && source.sourceNode) {
      source.sourceNode.start(0, offset||0);
      this._audioEnded = false;
      this._paused = false;
      this._startedAt = Date.now();
    }

    // media stream uses an analyser for audio data
    if (sourceMedia && sourceMedia.analyser) {
      source = sourceMedia;
      var canvas = document.createElement('canvas');
      canvas.width = this._baseCanvas.width;
      canvas.height = this._baseCanvas.height;
      var canvasContext = canvas.getContext('2d');

      var tempCanvas = document.createElement('canvas');
      tempCanvas.width = canvas.width;
      tempCanvas.height = canvas.height;

      canvasContext._tempContext = tempCanvas.getContext('2d');

      this._startMediaStreamDraw(source.analyser, canvasContext);
    }
  };

  Spectrogram.prototype.stop = function() {
    var source = this._sources[Object.keys(this._sources)[0]];
    if (source && source.sourceNode) {
      source.sourceNode.stop();
    }
    this._audioEnded = true;
  };

  Spectrogram.prototype.pause = function() {
    this.stop();
    this._paused = true;
    this._pausedAt += Date.now() - this._startedAt;
  };

  Spectrogram.prototype.resume = function(offset) {
    var source = this._sources[Object.keys(this._sources)[0]];
    this._paused = false;
    if (this._pausedAt) {
      this.connectSource(source.audioBuffer, source.audioContext);
      this.start(offset || (this._pausedAt / 1000));
    }
  };

  Spectrogram.prototype.clear = function(canvasContext) {
    var source = this._sources[Object.keys(this._sources)[0]];

    this.stop();

    if (toString.call(source.scriptNode) === '[object ScriptProcessorNode]') {
      source.scriptNode.onaudioprocess = null;
    }

    canvasContext = canvasContext || source.canvasContext;
    var canvas = canvasContext.canvas;
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);
    canvasContext._tempContext.clearRect(0, 0, canvas.width, canvas.height);
    this._baseCanvasContext.clearRect(0, 0, canvas.width, canvas.height);
  };

  Spectrogram.prototype._generateDefaultColors = function(steps) {
    var frequency = Math.PI / steps;
    var amplitude = 127;
    var center = 128;
    var slice = (Math.PI / 2) * 3.1;
    var colors = [];

    function toRGBString(v) {
      return 'rgba(' + [v,v,v,1].toString() + ')';
    }

    for (var i = 0; i < steps; i++) {
      var v = (Math.sin((frequency * i) + slice) * amplitude + center) >> 0;

      colors.push(toRGBString(v));
    }

    return colors;
  };

  Spectrogram.prototype._getColor = function(index) {
    var color = this._colors[index>>0];

    if (typeof color === 'undefined') {
      color = this._colors[0];
    }

    return color;
  };

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = Spectrogram;
    }
    exports.Spectrogram = Spectrogram;
  } else if (typeof define === 'function' && define.amd) {
    define([], function() {
      return Spectrogram;
    });
  } else {
    root.Spectrogram = Spectrogram;
  }

})(this);
<|endoftext|>


<commit_before>/**
 * Idea Hub setup component stories.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { MODULES_IDEA_HUB } from '../../datastore/constants';
import { CORE_MODULES } from '../../../../googlesitekit/modules/datastore/constants';
import WithRegistrySetup from '../../../../../../tests/js/WithRegistrySetup';
import SetupMain from './SetupMain';

const features = [ 'ideaHubModule' ];

function Template() {
	return (

							<SetupMain />

	);
}

export const DefaultSetup = Template.bind( null );
DefaultSetup.storyName = 'Setup form';
DefaultSetup.parameters = { features };
DefaultSetup.decorators = [
	( Story ) => {
		const moduleFixture = [
			{
				slug: 'idea-hub',
				name: 'Idea Hub',
				description:
					"Idea Hub suggests what you can write about next, based on searches that haven't been answered yet",
				homepage: 'https://www.google.com/webmasters/verification/home',
				internal: true,
				order: 0,
				active: false,
				connected: false,
				dependencies: [],
				dependants: [],
				owner: null,
				forceActive: true,
			},
		];

		const setupRegistry = ( registry ) => {
			registry
				.dispatch( CORE_MODULES )
				.receiveGetModules( moduleFixture );
			registry
				.dispatch( MODULES_IDEA_HUB )
				.receiveGetSettings( { tosAccepted: false } );
		};

		return (
			<WithRegistrySetup func={ setupRegistry }>
				<Story />
			</WithRegistrySetup>
		);
	},
];

export default {
	title: 'Modules/Idea Hub/Setup',
};
<commit_msg>Update Idea Hub CTA module description in Storybook.<commit_after>/**
 * Idea Hub setup component stories.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { MODULES_IDEA_HUB } from '../../datastore/constants';
import { CORE_MODULES } from '../../../../googlesitekit/modules/datastore/constants';
import WithRegistrySetup from '../../../../../../tests/js/WithRegistrySetup';
import SetupMain from './SetupMain';

const features = [ 'ideaHubModule' ];

function Template() {
	return (

							<SetupMain />

	);
}

export const DefaultSetup = Template.bind( null );
DefaultSetup.storyName = 'Setup form';
DefaultSetup.parameters = { features };
DefaultSetup.decorators = [
	( Story ) => {
		const moduleFixture = [
			{
				slug: 'idea-hub',
				name: 'Idea Hub',
				description:
					'Idea Hub suggests what you can write about next, from actual questions people asked on Google Search',
				internal: true,
				order: 0,
				active: false,
				connected: false,
				dependencies: [],
				dependants: [],
				owner: null,
				forceActive: true,
			},
		];

		const setupRegistry = ( registry ) => {
			registry
				.dispatch( CORE_MODULES )
				.receiveGetModules( moduleFixture );
			registry
				.dispatch( MODULES_IDEA_HUB )
				.receiveGetSettings( { tosAccepted: false } );
		};

		return (
			<WithRegistrySetup func={ setupRegistry }>
				<Story />
			</WithRegistrySetup>
		);
	},
];

export default {
	title: 'Modules/Idea Hub/Setup',
};
<|endoftext|>


<commit_before>/* jshint maxdepth:7,node:true*/
steal(function(){

	function each(items, callback){
		for ( var i = 0; i < items.length; i++ ) {
			callback(items[i], i);
		}
	}

	function makeMap(str){
		var obj = {}, items = str.split(",");
		each(items, function(name){
			obj[name] = true;
		});
		return obj;
	}

	function handleIntermediate(intermediate, handler){
		for(var i = 0, len = intermediate.length; i < len; i++) {
			var item = intermediate[i];
			handler[item.tokenType].apply(handler, item.args);
		}
		return intermediate;
	}

	var alphaNumericHU = "-:A-Za-z0-9_",
		attributeNames = "[^=>\\s\\/]+",
		spaceEQspace = "\\s*=\\s*",
		singleCurly = "\{[^\}\{]\}",
		doubleCurly = "\{\{[^\}]\}\}\}?",
		attributeEqAndValue = "(?:"+spaceEQspace+"(?:"+
		  "(?:"+doubleCurly+")|(?:"+singleCurly+")|(?:\"[^\"]*\")|(?:'[^']*')|[^>\\s]+))?",
		matchStash = "\\{\\{[^\\}]*\\}\\}\\}?",
		stash = "\\{\\{([^\\}]*)\\}\\}\\}?",
		startTag = new RegExp("^<(["+alphaNumericHU+"]+)"+
				"(" +
					"(?:\\s*"+
						"(?:(?:"+
							"(?:"+attributeNames+")?"+
							attributeEqAndValue+")|"+
	                   "(?:"+matchStash+")+)"+
	                ")*"+
	            ")\\s*(\\/?)>"),
		endTag = new RegExp("^<\\/(["+alphaNumericHU+"]+)[^>]*>"),
		mustache = new RegExp(stash,"g"),
		txtBreak = /<|\{\{/,
		space = /\s/;

	// Empty Elements - HTML 5
	var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");

	// Block Elements - HTML 5
	// a is traditionally inline, but should allow block-level elments inside it, so it should be treated like a block-level element when parsed
	var block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video");

	// Inline Elements - HTML 5
	var inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");

	// Elements that you can, intentionally, leave open
	// (and which close themselves)
	var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");

	// Attributes that have their values filled in disabled="disabled"
	// var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");

	// Special Elements (can contain anything)
	var special = makeMap("script");

	// Callback names on `handler`.
	var tokenTypes = "start,end,close,attrStart,attrEnd,attrValue,chars,comment,special,done".split(",");

	var fn = function(){};

	var HTMLParser = function (html, handler, returnIntermediate) {
		if(typeof html === "object") {
			return handleIntermediate(html, handler);
		}
		var intermediate = [];
		handler = handler || {};
		if(returnIntermediate) {
			// overwrite handlers so they add to intermediate
			each(tokenTypes, function(name){
				var callback = handler[name] || fn;
				handler[name] = function(){
					if( callback.apply(this, arguments) !== false ) {
						intermediate.push({tokenType: name, args: [].slice.call(arguments, 0) });
					}
				};
			});
		}

		function parseStartTag(tag, tagName, rest, unary) {
			tagName = tagName.toLowerCase();

			if (block[tagName]) {
				while (stack.last() && inline[stack.last()]) {
					parseEndTag("", stack.last());
				}
			}

			if (closeSelf[tagName] && stack.last() === tagName) {
				parseEndTag("", tagName);
			}

			unary = empty[tagName] || !!unary;

			handler.start(tagName, unary);

			if (!unary) {
				stack.push(tagName);
			}
			// find attribute or special
			HTMLParser.parseAttrs(rest, handler);

			handler.end(tagName,unary);

		}

		function parseEndTag(tag, tagName) {
			// If no tag name is provided, clean shop
			var pos;
			if (!tagName) {
				pos = 0;
			}

				// Find the closest opened tag of the same type
			else {
				for (pos = stack.length - 1; pos >= 0; pos--) {
					if (stack[pos] === tagName) {
						break;
					}
				}

			}

			if (pos >= 0) {
				// Close all the open elements, up the stack
				for (var i = stack.length - 1; i >= pos; i--) {
					if (handler.close) {
						handler.close(stack[i]);
					}
				}

				// Remove the open elements from the stack
				stack.length = pos;
			}
		}

		function parseMustache(mustache, inside){
			if(handler.special){
				handler.special(inside);
			}
		}
		var callChars = function(){
			if(charsText) {
				if(handler.chars) {
					handler.chars(charsText);
				}
			}
			charsText = "";
		};

		var index,
			chars,
			match,
			stack = [],
			last = html,
			// an accumulating text for the next .chars callback
			charsText = "";
		stack.last = function () {
			return this[this.length - 1];
		};

		while (html) {

			chars = true;

			// Make sure we're not in a script or style element
			if (!stack.last() || !special[stack.last()]) {

				// Comment
				if (html.indexOf("<!--") === 0) {
					index = html.indexOf("-->");

					if (index >= 0) {
						callChars();
						if (handler.comment) {
							handler.comment(html.substring(4, index));
						}
						html = html.substring(index + 3);
						chars = false;
					}

					// end tag
				} else if (html.indexOf("</") === 0) {
					match = html.match(endTag);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(endTag, parseEndTag);
						chars = false;
					}

					// start tag
				} else if (html.indexOf("<") === 0) {
					match = html.match(startTag);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(startTag, parseStartTag);
						chars = false;
					}
				} else if (html.indexOf("{{") === 0 ) {
					match = html.match(mustache);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(mustache, parseMustache);
					}
				}

				if (chars) {
					index = html.search(txtBreak);
					if(index === 0 && html === last) {
						charsText += html.charAt(0);
						html = html.substr(1);
						index = html.search(txtBreak);
					}

					var text = index < 0 ? html : html.substring(0, index);
					html = index < 0 ? "" : html.substring(index);

					if (text) {
						charsText += text;
					}

				}

			} else {
				html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) {
					text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");
					if (handler.chars) {
						handler.chars(text);
					}
					return "";
				});

				parseEndTag("", stack.last());
			}

			if (html === last) {
				throw new Error("Parse Error: " + html);
			}

			last = html;
		}
		callChars();
		// Clean up any remaining tags
		parseEndTag();

		handler.done();
		return intermediate;
	};

	var callAttrStart = function(state, curIndex, handler, rest){
		state.attrStart = rest.substring(typeof state.nameStart === "number" ? state.nameStart : curIndex, curIndex);
		handler.attrStart(state.attrStart);
		state.inName = false;
	};

	var callAttrEnd = function(state, curIndex, handler, rest){
		if(state.valueStart !== undefined && state.valueStart < curIndex) {
			handler.attrValue(rest.substring(state.valueStart, curIndex));
		}
		// if this never got to be inValue, like `DISABLED` then send a attrValue
		else if(!state.inValue){
			//handler.attrValue(state.attrStart);
		}
		handler.attrEnd(state.attrStart);
		state.attrStart = undefined;
		state.valueStart = undefined;
		state.inValue = false;
		state.inName = false;
		state.inQuote = false;
		state.lookingForName = true;
	};

	HTMLParser.parseAttrs = function(rest, handler){
		if(!rest) {
			return;
		}
		var i = 0;
		var curIndex;
		var state = {
			inDoubleCurly: false,
			inName: false,
			nameStart: undefined,
			inValue: false,
			valueStart: undefined,
			inQuote: false,
			attrStart: undefined,
			lookingForName: true,
			lookingForValue: false,
			lookingForEq : false
		};
		while(i < rest.length) {
			curIndex = i;
			var cur = rest.charAt(i);
			var next = rest.charAt(i+1);
			var nextNext = rest.charAt(i+2);
			i++;

			if(cur === "{" && next === "{") {
				if(state.inValue && curIndex > state.valueStart) {
					handler.attrValue(rest.substring(state.valueStart, curIndex));
				}
				// `{{#foo}}DISABLED{{/foo}}`
				else if(state.inName && state.nameStart < curIndex) {
					callAttrStart(state, curIndex, handler, rest);
					callAttrEnd(state, curIndex, handler, rest);
				}
				// foo={{bar}}
				else if(state.lookingForValue){
					state.inValue = true;
				}
				state.inDoubleCurly = true;
				state.doubleCurlyStart = curIndex+2;
				i++;
			}
			else if(state.inDoubleCurly) {
				if(cur === "}" && next === "}") {
					// for `{{{}}}`
					var isTriple = nextNext === "}" ?  1: 0;
					handler.special(rest.substring(state.doubleCurlyStart, curIndex));
					state.inDoubleCurly = false;
					if(state.inValue) {
						state.valueStart = curIndex+2+isTriple;
					}
					i += (1+isTriple);
				}
			}
			else if(state.inValue) {
				if(state.inQuote) {
					if(cur === state.inQuote) {
						callAttrEnd(state, curIndex, handler, rest);
					}
				}
				else if(space.test(cur)) {
					callAttrEnd(state, curIndex, handler, rest);
				}
			}
			// if we hit an = outside a value
			else if(cur === "=" && (state.lookingForEq || state.lookingForName || state.inName)) {

				// if we haven't yet started this attribute `{{}}=foo` case:
				if(!state.attrStart) {
					callAttrStart(state, curIndex, handler, rest);
				}
				state.lookingForValue = true;
				state.lookingForEq = false;
				state.lookingForName = false;
			}
			// if we are currently in a name, check if we found a space
			else if(state.inName) {
				if(space.test(cur)) {
					callAttrStart(state, curIndex, handler, rest);
					state.lookingForEq = true;
				}
			}
			else if(state.lookingForName) {
				if(!space.test(cur)) {
					// might have just started a name, we need to close it
					if(state.attrStart) {
						callAttrEnd(state, curIndex, handler, rest);
					}
					state.nameStart = curIndex;
					state.inName = true;
				}
			}
			else if(state.lookingForValue) {
				if(!space.test(cur)) {
					state.lookingForValue = false;
					state.inValue = true;
					if(cur === "'" || cur === '"') {
						state.inQuote = cur;
						state.valueStart = curIndex+1;
					} else {
						state.valueStart = curIndex;
					}
				}
			}
		}

		if(state.inName) {
			callAttrStart(state, curIndex+1, handler, rest);
			callAttrEnd(state, curIndex+1, handler, rest);
		} else if(state.lookingForEq) {
			callAttrEnd(state, curIndex+1, handler, rest);
		} else if(state.inValue) {
			callAttrEnd(state, curIndex+1, handler, rest);
		}

	};

	return HTMLParser;

});
<commit_msg>linting<commit_after>/* jshint maxdepth:7,node:true*/
steal(function(){

	function each(items, callback){
		for ( var i = 0; i < items.length; i++ ) {
			callback(items[i], i);
		}
	}

	function makeMap(str){
		var obj = {}, items = str.split(",");
		each(items, function(name){
			obj[name] = true;
		});
		return obj;
	}

	function handleIntermediate(intermediate, handler){
		for(var i = 0, len = intermediate.length; i < len; i++) {
			var item = intermediate[i];
			handler[item.tokenType].apply(handler, item.args);
		}
		return intermediate;
	}

	var alphaNumericHU = "-:A-Za-z0-9_",
		attributeNames = "[^=>\\s\\/]+",
		spaceEQspace = "\\s*=\\s*",
		singleCurly = "\\{[^\\}\\{]\\}",
		doubleCurly = "\\{\\{[^\\}]\\}\\}\\}?",
		attributeEqAndValue = "(?:"+spaceEQspace+"(?:"+
		  "(?:"+doubleCurly+")|(?:"+singleCurly+")|(?:\"[^\"]*\")|(?:'[^']*')|[^>\\s]+))?",
		matchStash = "\\{\\{[^\\}]*\\}\\}\\}?",
		stash = "\\{\\{([^\\}]*)\\}\\}\\}?",
		startTag = new RegExp("^<(["+alphaNumericHU+"]+)"+
				"(" +
					"(?:\\s*"+
						"(?:(?:"+
							"(?:"+attributeNames+")?"+
							attributeEqAndValue+")|"+
	                   "(?:"+matchStash+")+)"+
	                ")*"+
	            ")\\s*(\\/?)>"),
		endTag = new RegExp("^<\\/(["+alphaNumericHU+"]+)[^>]*>"),
		mustache = new RegExp(stash,"g"),
		txtBreak = /<|\{\{/,
		space = /\s/;

	// Empty Elements - HTML 5
	var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");

	// Block Elements - HTML 5
	// a is traditionally inline, but should allow block-level elments inside it, so it should be treated like a block-level element when parsed
	var block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video");

	// Inline Elements - HTML 5
	var inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");

	// Elements that you can, intentionally, leave open
	// (and which close themselves)
	var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");

	// Attributes that have their values filled in disabled="disabled"
	// var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");

	// Special Elements (can contain anything)
	var special = makeMap("script");

	// Callback names on `handler`.
	var tokenTypes = "start,end,close,attrStart,attrEnd,attrValue,chars,comment,special,done".split(",");

	var fn = function(){};

	var HTMLParser = function (html, handler, returnIntermediate) {
		if(typeof html === "object") {
			return handleIntermediate(html, handler);
		}
		var intermediate = [];
		handler = handler || {};
		if(returnIntermediate) {
			// overwrite handlers so they add to intermediate
			each(tokenTypes, function(name){
				var callback = handler[name] || fn;
				handler[name] = function(){
					if( callback.apply(this, arguments) !== false ) {
						intermediate.push({tokenType: name, args: [].slice.call(arguments, 0) });
					}
				};
			});
		}

		function parseStartTag(tag, tagName, rest, unary) {
			tagName = tagName.toLowerCase();

			if (block[tagName]) {
				while (stack.last() && inline[stack.last()]) {
					parseEndTag("", stack.last());
				}
			}

			if (closeSelf[tagName] && stack.last() === tagName) {
				parseEndTag("", tagName);
			}

			unary = empty[tagName] || !!unary;

			handler.start(tagName, unary);

			if (!unary) {
				stack.push(tagName);
			}
			// find attribute or special
			HTMLParser.parseAttrs(rest, handler);

			handler.end(tagName,unary);

		}

		function parseEndTag(tag, tagName) {
			// If no tag name is provided, clean shop
			var pos;
			if (!tagName) {
				pos = 0;
			}

				// Find the closest opened tag of the same type
			else {
				for (pos = stack.length - 1; pos >= 0; pos--) {
					if (stack[pos] === tagName) {
						break;
					}
				}

			}

			if (pos >= 0) {
				// Close all the open elements, up the stack
				for (var i = stack.length - 1; i >= pos; i--) {
					if (handler.close) {
						handler.close(stack[i]);
					}
				}

				// Remove the open elements from the stack
				stack.length = pos;
			}
		}

		function parseMustache(mustache, inside){
			if(handler.special){
				handler.special(inside);
			}
		}
		var callChars = function(){
			if(charsText) {
				if(handler.chars) {
					handler.chars(charsText);
				}
			}
			charsText = "";
		};

		var index,
			chars,
			match,
			stack = [],
			last = html,
			// an accumulating text for the next .chars callback
			charsText = "";
		stack.last = function () {
			return this[this.length - 1];
		};

		while (html) {

			chars = true;

			// Make sure we're not in a script or style element
			if (!stack.last() || !special[stack.last()]) {

				// Comment
				if (html.indexOf("<!--") === 0) {
					index = html.indexOf("-->");

					if (index >= 0) {
						callChars();
						if (handler.comment) {
							handler.comment(html.substring(4, index));
						}
						html = html.substring(index + 3);
						chars = false;
					}

					// end tag
				} else if (html.indexOf("</") === 0) {
					match = html.match(endTag);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(endTag, parseEndTag);
						chars = false;
					}

					// start tag
				} else if (html.indexOf("<") === 0) {
					match = html.match(startTag);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(startTag, parseStartTag);
						chars = false;
					}
				} else if (html.indexOf("{{") === 0 ) {
					match = html.match(mustache);

					if (match) {
						callChars();
						html = html.substring(match[0].length);
						match[0].replace(mustache, parseMustache);
					}
				}

				if (chars) {
					index = html.search(txtBreak);
					if(index === 0 && html === last) {
						charsText += html.charAt(0);
						html = html.substr(1);
						index = html.search(txtBreak);
					}

					var text = index < 0 ? html : html.substring(0, index);
					html = index < 0 ? "" : html.substring(index);

					if (text) {
						charsText += text;
					}

				}

			} else {
				html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) {
					text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");
					if (handler.chars) {
						handler.chars(text);
					}
					return "";
				});

				parseEndTag("", stack.last());
			}

			if (html === last) {
				throw new Error("Parse Error: " + html);
			}

			last = html;
		}
		callChars();
		// Clean up any remaining tags
		parseEndTag();

		handler.done();
		return intermediate;
	};

	var callAttrStart = function(state, curIndex, handler, rest){
		state.attrStart = rest.substring(typeof state.nameStart === "number" ? state.nameStart : curIndex, curIndex);
		handler.attrStart(state.attrStart);
		state.inName = false;
	};

	var callAttrEnd = function(state, curIndex, handler, rest){
		if(state.valueStart !== undefined && state.valueStart < curIndex) {
			handler.attrValue(rest.substring(state.valueStart, curIndex));
		}
		// if this never got to be inValue, like `DISABLED` then send a attrValue
		else if(!state.inValue){
			//handler.attrValue(state.attrStart);
		}
		handler.attrEnd(state.attrStart);
		state.attrStart = undefined;
		state.valueStart = undefined;
		state.inValue = false;
		state.inName = false;
		state.inQuote = false;
		state.lookingForName = true;
	};

	HTMLParser.parseAttrs = function(rest, handler){
		if(!rest) {
			return;
		}
		var i = 0;
		var curIndex;
		var state = {
			inDoubleCurly: false,
			inName: false,
			nameStart: undefined,
			inValue: false,
			valueStart: undefined,
			inQuote: false,
			attrStart: undefined,
			lookingForName: true,
			lookingForValue: false,
			lookingForEq : false
		};
		while(i < rest.length) {
			curIndex = i;
			var cur = rest.charAt(i);
			var next = rest.charAt(i+1);
			var nextNext = rest.charAt(i+2);
			i++;

			if(cur === "{" && next === "{") {
				if(state.inValue && curIndex > state.valueStart) {
					handler.attrValue(rest.substring(state.valueStart, curIndex));
				}
				// `{{#foo}}DISABLED{{/foo}}`
				else if(state.inName && state.nameStart < curIndex) {
					callAttrStart(state, curIndex, handler, rest);
					callAttrEnd(state, curIndex, handler, rest);
				}
				// foo={{bar}}
				else if(state.lookingForValue){
					state.inValue = true;
				}
				state.inDoubleCurly = true;
				state.doubleCurlyStart = curIndex+2;
				i++;
			}
			else if(state.inDoubleCurly) {
				if(cur === "}" && next === "}") {
					// for `{{{}}}`
					var isTriple = nextNext === "}" ?  1: 0;
					handler.special(rest.substring(state.doubleCurlyStart, curIndex));
					state.inDoubleCurly = false;
					if(state.inValue) {
						state.valueStart = curIndex+2+isTriple;
					}
					i += (1+isTriple);
				}
			}
			else if(state.inValue) {
				if(state.inQuote) {
					if(cur === state.inQuote) {
						callAttrEnd(state, curIndex, handler, rest);
					}
				}
				else if(space.test(cur)) {
					callAttrEnd(state, curIndex, handler, rest);
				}
			}
			// if we hit an = outside a value
			else if(cur === "=" && (state.lookingForEq || state.lookingForName || state.inName)) {

				// if we haven't yet started this attribute `{{}}=foo` case:
				if(!state.attrStart) {
					callAttrStart(state, curIndex, handler, rest);
				}
				state.lookingForValue = true;
				state.lookingForEq = false;
				state.lookingForName = false;
			}
			// if we are currently in a name, check if we found a space
			else if(state.inName) {
				if(space.test(cur)) {
					callAttrStart(state, curIndex, handler, rest);
					state.lookingForEq = true;
				}
			}
			else if(state.lookingForName) {
				if(!space.test(cur)) {
					// might have just started a name, we need to close it
					if(state.attrStart) {
						callAttrEnd(state, curIndex, handler, rest);
					}
					state.nameStart = curIndex;
					state.inName = true;
				}
			}
			else if(state.lookingForValue) {
				if(!space.test(cur)) {
					state.lookingForValue = false;
					state.inValue = true;
					if(cur === "'" || cur === '"') {
						state.inQuote = cur;
						state.valueStart = curIndex+1;
					} else {
						state.valueStart = curIndex;
					}
				}
			}
		}

		if(state.inName) {
			callAttrStart(state, curIndex+1, handler, rest);
			callAttrEnd(state, curIndex+1, handler, rest);
		} else if(state.lookingForEq) {
			callAttrEnd(state, curIndex+1, handler, rest);
		} else if(state.inValue) {
			callAttrEnd(state, curIndex+1, handler, rest);
		}

	};

	return HTMLParser;

});
<|endoftext|>


<commit_before>import { Texture } from '@pixi/core';
import { Sprite } from '@pixi/sprite';
import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker';

/**
 * An AnimatedSprite is a simple way to display an animation depicted by a list of textures.
 *
 * ```js
 * let alienImages = ["image_sequence_01.png","image_sequence_02.png","image_sequence_03.png","image_sequence_04.png"];
 * let textureArray = [];
 *
 * for (let i=0; i < 4; i++)
 * {
 *      let texture = PIXI.Texture.from(alienImages[i]);
 *      textureArray.push(texture);
 * };
 *
 * let animatedSprite = new PIXI.AnimatedSprite(textureArray);
 * ```
 *
 * The more efficient and simpler way to create an animated sprite is using a {@link PIXI.Spritesheet}
 * containing the animation definitions:
 *
 * ```js
 * PIXI.loader.add("assets/spritesheet.json").load(setup);
 *
 * function setup() {
 *   let sheet = PIXI.loader.resources["assets/spritesheet.json"].spritesheet;
 *   animatedSprite = new PIXI.AnimatedSprite(sheet.animations["image_sequence"]);
 *   ...
 * }
 * ```
 *
 * @class
 * @extends PIXI.Sprite
 * @memberof PIXI
 */
export default class AnimatedSprite extends Sprite
{
    /**
     * @param {PIXI.Texture[]|PIXI.AnimatedSprite.FrameObject[]} textures - An array of {@link PIXI.Texture} or frame
     *  objects that make up the animation.
     * @param {boolean} [autoUpdate=true] - Whether to use PIXI.Ticker.shared to auto update animation time.
     */
    constructor(textures, autoUpdate)
    {
        super(textures[0] instanceof Texture ? textures[0] : textures[0].texture);

        /**
         * @type {PIXI.Texture[]}
         * @private
         */
        this._textures = null;

        /**
         * @type {number[]}
         * @private
         */
        this._durations = null;

        this.textures = textures;

        /**
         * `true` uses PIXI.Ticker.shared to auto update animation time.
         * @type {boolean}
         * @default true
         * @private
         */
        this._autoUpdate = autoUpdate !== false;

        /**
         * The speed that the AnimatedSprite will play at. Higher is faster, lower is slower.
         *
         * @member {number}
         * @default 1
         */
        this.animationSpeed = 1;

        /**
         * Whether or not the animate sprite repeats after playing.
         *
         * @member {boolean}
         * @default true
         */
        this.loop = true;

        /**
         * Function to call when a AnimatedSprite finishes playing.
         *
         * @member {Function}
         */
        this.onComplete = null;

        /**
         * Function to call when a AnimatedSprite changes which texture is being rendered.
         *
         * @member {Function}
         */
        this.onFrameChange = null;

        /**
         * Function to call when `loop` is true, and an AnimatedSprite is played and loops around to start again.
         *
         * @member {Function}
         */
        this.onLoop = null;

        /**
         * Elapsed time since animation has been started, used internally to display current texture.
         *
         * @member {number}
         * @private
         */
        this._currentTime = 0;

        /**
         * Indicates if the AnimatedSprite is currently playing.
         *
         * @member {boolean}
         * @readonly
         */
        this.playing = false;
    }

    /**
     * Stops the AnimatedSprite.
     *
     */
    stop()
    {
        if (!this.playing)
        {
            return;
        }

        this.playing = false;
        if (this._autoUpdate)
        {
            Ticker.shared.remove(this.update, this);
        }
    }

    /**
     * Plays the AnimatedSprite.
     *
     */
    play()
    {
        if (this.playing)
        {
            return;
        }

        this.playing = true;
        if (this._autoUpdate)
        {
            Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH);
        }
    }

    /**
     * Stops the AnimatedSprite and goes to a specific frame.
     *
     * @param {number} frameNumber - Frame index to stop at.
     */
    gotoAndStop(frameNumber)
    {
        this.stop();

        const previousFrame = this.currentFrame;

        this._currentTime = frameNumber;

        if (previousFrame !== this.currentFrame)
        {
            this.updateTexture();
        }
    }

    /**
     * Goes to a specific frame and begins playing the AnimatedSprite.
     *
     * @param {number} frameNumber - Frame index to start at.
     */
    gotoAndPlay(frameNumber)
    {
        const previousFrame = this.currentFrame;

        this._currentTime = frameNumber;

        if (previousFrame !== this.currentFrame)
        {
            this.updateTexture();
        }

        this.play();
    }

    /**
     * Updates the object transform for rendering.
     *
     * @private
     * @param {number} deltaTime - Time since last tick.
     */
    update(deltaTime)
    {
        const elapsed = this.animationSpeed * deltaTime;
        const previousFrame = this.currentFrame;

        if (this._durations !== null)
        {
            let lag = this._currentTime % 1 * this._durations[this.currentFrame];

            lag += elapsed / 60 * 1000;

            while (lag < 0)
            {
                this._currentTime--;
                lag += this._durations[this.currentFrame];
            }

            const sign = Math.sign(this.animationSpeed * deltaTime);

            this._currentTime = Math.floor(this._currentTime);

            while (lag >= this._durations[this.currentFrame])
            {
                lag -= this._durations[this.currentFrame] * sign;
                this._currentTime += sign;
            }

            this._currentTime += lag / this._durations[this.currentFrame];
        }
        else
        {
            this._currentTime += elapsed;
        }

        if (this._currentTime < 0 && !this.loop)
        {
            this.gotoAndStop(0);

            if (this.onComplete)
            {
                this.onComplete();
            }
        }
        else if (this._currentTime >= this._textures.length && !this.loop)
        {
            this.gotoAndStop(this._textures.length - 1);

            if (this.onComplete)
            {
                this.onComplete();
            }
        }
        else if (previousFrame !== this.currentFrame)
        {
            if (this.loop && this.onLoop)
            {
                if (this.animationSpeed > 0 && this.currentFrame < previousFrame)
                {
                    this.onLoop();
                }
                else if (this.animationSpeed < 0 && this.currentFrame > previousFrame)
                {
                    this.onLoop();
                }
            }

            this.updateTexture();
        }
    }

    /**
     * Updates the displayed texture to match the current frame index.
     *
     * @private
     */
    updateTexture()
    {
        this._texture = this._textures[this.currentFrame];
        this._textureID = -1;
        this._textureTrimmedID = -1;
        this.cachedTint = 0xFFFFFF;
        this.uvs = this._texture._uvs.uvsFloat32;

        if (this.onFrameChange)
        {
            this.onFrameChange(this.currentFrame);
        }
    }

    /**
     * Stops the AnimatedSprite and destroys it.
     *
     * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options
     *  have been set to that value.
     * @param {boolean} [options.children=false] - If set to true, all the children will have their destroy
     *      method called as well. 'options' will be passed on to those calls.
     * @param {boolean} [options.texture=false] - Should it destroy the current texture of the sprite as well.
     * @param {boolean} [options.baseTexture=false] - Should it destroy the base texture of the sprite as well.
     */
    destroy(options)
    {
        this.stop();
        super.destroy(options);
    }

    /**
     * A short hand way of creating a movieclip from an array of frame ids.
     *
     * @static
     * @param {string[]} frames - The array of frames ids the movieclip will use as its texture frames.
     * @return {AnimatedSprite} The new animated sprite with the specified frames.
     */
    static fromFrames(frames)
    {
        const textures = [];

        for (let i = 0; i < frames.length; ++i)
        {
            textures.push(Texture.from(frames[i]));
        }

        return new AnimatedSprite(textures);
    }

    /**
     * A short hand way of creating a movieclip from an array of image ids.
     *
     * @static
     * @param {string[]} images - The array of image urls the movieclip will use as its texture frames.
     * @return {AnimatedSprite} The new animate sprite with the specified images as frames.
     */
    static fromImages(images)
    {
        const textures = [];

        for (let i = 0; i < images.length; ++i)
        {
            textures.push(Texture.from(images[i]));
        }

        return new AnimatedSprite(textures);
    }

    /**
     * The total number of frames in the AnimatedSprite. This is the same as number of textures
     * assigned to the AnimatedSprite.
     *
     * @readonly
     * @member {number}
     * @default 0
     */
    get totalFrames()
    {
        return this._textures.length;
    }

    /**
     * The array of textures used for this AnimatedSprite.
     *
     * @member {PIXI.Texture[]}
     */
    get textures()
    {
        return this._textures;
    }

    set textures(value) // eslint-disable-line require-jsdoc
    {
        if (value[0] instanceof Texture)
        {
            this._textures = value;
            this._durations = null;
        }
        else
        {
            this._textures = [];
            this._durations = [];

            for (let i = 0; i < value.length; i++)
            {
                this._textures.push(value[i].texture);
                this._durations.push(value[i].time);
            }
        }
        this.gotoAndStop(0);
        this.updateTexture();
    }

    /**
    * The AnimatedSprites current frame index.
    *
    * @member {number}
    * @readonly
    */
    get currentFrame()
    {
        let currentFrame = Math.floor(this._currentTime) % this._textures.length;

        if (currentFrame < 0)
        {
            currentFrame += this._textures.length;
        }

        return currentFrame;
    }
}

/**
 * @memberof PIXI.AnimatedSprite
 * @typedef {object} FrameObject
 * @type {object}
 * @property {PIXI.Texture} texture - The {@link PIXI.Texture} of the frame
 * @property {number} time - the duration of the frame in ms
 */
<commit_msg>AnimatedSprite does not full destroy properly (#5363)<commit_after>import { Texture } from '@pixi/core';
import { Sprite } from '@pixi/sprite';
import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker';

/**
 * An AnimatedSprite is a simple way to display an animation depicted by a list of textures.
 *
 * ```js
 * let alienImages = ["image_sequence_01.png","image_sequence_02.png","image_sequence_03.png","image_sequence_04.png"];
 * let textureArray = [];
 *
 * for (let i=0; i < 4; i++)
 * {
 *      let texture = PIXI.Texture.from(alienImages[i]);
 *      textureArray.push(texture);
 * };
 *
 * let animatedSprite = new PIXI.AnimatedSprite(textureArray);
 * ```
 *
 * The more efficient and simpler way to create an animated sprite is using a {@link PIXI.Spritesheet}
 * containing the animation definitions:
 *
 * ```js
 * PIXI.loader.add("assets/spritesheet.json").load(setup);
 *
 * function setup() {
 *   let sheet = PIXI.loader.resources["assets/spritesheet.json"].spritesheet;
 *   animatedSprite = new PIXI.AnimatedSprite(sheet.animations["image_sequence"]);
 *   ...
 * }
 * ```
 *
 * @class
 * @extends PIXI.Sprite
 * @memberof PIXI
 */
export default class AnimatedSprite extends Sprite
{
    /**
     * @param {PIXI.Texture[]|PIXI.AnimatedSprite.FrameObject[]} textures - An array of {@link PIXI.Texture} or frame
     *  objects that make up the animation.
     * @param {boolean} [autoUpdate=true] - Whether to use PIXI.Ticker.shared to auto update animation time.
     */
    constructor(textures, autoUpdate)
    {
        super(textures[0] instanceof Texture ? textures[0] : textures[0].texture);

        /**
         * @type {PIXI.Texture[]}
         * @private
         */
        this._textures = null;

        /**
         * @type {number[]}
         * @private
         */
        this._durations = null;

        this.textures = textures;

        /**
         * `true` uses PIXI.Ticker.shared to auto update animation time.
         * @type {boolean}
         * @default true
         * @private
         */
        this._autoUpdate = autoUpdate !== false;

        /**
         * The speed that the AnimatedSprite will play at. Higher is faster, lower is slower.
         *
         * @member {number}
         * @default 1
         */
        this.animationSpeed = 1;

        /**
         * Whether or not the animate sprite repeats after playing.
         *
         * @member {boolean}
         * @default true
         */
        this.loop = true;

        /**
         * Function to call when a AnimatedSprite finishes playing.
         *
         * @member {Function}
         */
        this.onComplete = null;

        /**
         * Function to call when a AnimatedSprite changes which texture is being rendered.
         *
         * @member {Function}
         */
        this.onFrameChange = null;

        /**
         * Function to call when `loop` is true, and an AnimatedSprite is played and loops around to start again.
         *
         * @member {Function}
         */
        this.onLoop = null;

        /**
         * Elapsed time since animation has been started, used internally to display current texture.
         *
         * @member {number}
         * @private
         */
        this._currentTime = 0;

        /**
         * Indicates if the AnimatedSprite is currently playing.
         *
         * @member {boolean}
         * @readonly
         */
        this.playing = false;
    }

    /**
     * Stops the AnimatedSprite.
     *
     */
    stop()
    {
        if (!this.playing)
        {
            return;
        }

        this.playing = false;
        if (this._autoUpdate)
        {
            Ticker.shared.remove(this.update, this);
        }
    }

    /**
     * Plays the AnimatedSprite.
     *
     */
    play()
    {
        if (this.playing)
        {
            return;
        }

        this.playing = true;
        if (this._autoUpdate)
        {
            Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH);
        }
    }

    /**
     * Stops the AnimatedSprite and goes to a specific frame.
     *
     * @param {number} frameNumber - Frame index to stop at.
     */
    gotoAndStop(frameNumber)
    {
        this.stop();

        const previousFrame = this.currentFrame;

        this._currentTime = frameNumber;

        if (previousFrame !== this.currentFrame)
        {
            this.updateTexture();
        }
    }

    /**
     * Goes to a specific frame and begins playing the AnimatedSprite.
     *
     * @param {number} frameNumber - Frame index to start at.
     */
    gotoAndPlay(frameNumber)
    {
        const previousFrame = this.currentFrame;

        this._currentTime = frameNumber;

        if (previousFrame !== this.currentFrame)
        {
            this.updateTexture();
        }

        this.play();
    }

    /**
     * Updates the object transform for rendering.
     *
     * @private
     * @param {number} deltaTime - Time since last tick.
     */
    update(deltaTime)
    {
        const elapsed = this.animationSpeed * deltaTime;
        const previousFrame = this.currentFrame;

        if (this._durations !== null)
        {
            let lag = this._currentTime % 1 * this._durations[this.currentFrame];

            lag += elapsed / 60 * 1000;

            while (lag < 0)
            {
                this._currentTime--;
                lag += this._durations[this.currentFrame];
            }

            const sign = Math.sign(this.animationSpeed * deltaTime);

            this._currentTime = Math.floor(this._currentTime);

            while (lag >= this._durations[this.currentFrame])
            {
                lag -= this._durations[this.currentFrame] * sign;
                this._currentTime += sign;
            }

            this._currentTime += lag / this._durations[this.currentFrame];
        }
        else
        {
            this._currentTime += elapsed;
        }

        if (this._currentTime < 0 && !this.loop)
        {
            this.gotoAndStop(0);

            if (this.onComplete)
            {
                this.onComplete();
            }
        }
        else if (this._currentTime >= this._textures.length && !this.loop)
        {
            this.gotoAndStop(this._textures.length - 1);

            if (this.onComplete)
            {
                this.onComplete();
            }
        }
        else if (previousFrame !== this.currentFrame)
        {
            if (this.loop && this.onLoop)
            {
                if (this.animationSpeed > 0 && this.currentFrame < previousFrame)
                {
                    this.onLoop();
                }
                else if (this.animationSpeed < 0 && this.currentFrame > previousFrame)
                {
                    this.onLoop();
                }
            }

            this.updateTexture();
        }
    }

    /**
     * Updates the displayed texture to match the current frame index.
     *
     * @private
     */
    updateTexture()
    {
        this._texture = this._textures[this.currentFrame];
        this._textureID = -1;
        this._textureTrimmedID = -1;
        this.cachedTint = 0xFFFFFF;
        this.uvs = this._texture._uvs.uvsFloat32;

        if (this.onFrameChange)
        {
            this.onFrameChange(this.currentFrame);
        }
    }

    /**
     * Stops the AnimatedSprite and destroys it.
     *
     * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options
     *  have been set to that value.
     * @param {boolean} [options.children=false] - If set to true, all the children will have their destroy
     *      method called as well. 'options' will be passed on to those calls.
     * @param {boolean} [options.texture=false] - Should it destroy the current texture of the sprite as well.
     * @param {boolean} [options.baseTexture=false] - Should it destroy the base texture of the sprite as well.
     */
    destroy(options)
    {
        this.stop();
        super.destroy(options);

        this.onComplete = null;
        this.onFrameChange = null;
        this.onLoop = null;
    }

    /**
     * A short hand way of creating a movieclip from an array of frame ids.
     *
     * @static
     * @param {string[]} frames - The array of frames ids the movieclip will use as its texture frames.
     * @return {AnimatedSprite} The new animated sprite with the specified frames.
     */
    static fromFrames(frames)
    {
        const textures = [];

        for (let i = 0; i < frames.length; ++i)
        {
            textures.push(Texture.from(frames[i]));
        }

        return new AnimatedSprite(textures);
    }

    /**
     * A short hand way of creating a movieclip from an array of image ids.
     *
     * @static
     * @param {string[]} images - The array of image urls the movieclip will use as its texture frames.
     * @return {AnimatedSprite} The new animate sprite with the specified images as frames.
     */
    static fromImages(images)
    {
        const textures = [];

        for (let i = 0; i < images.length; ++i)
        {
            textures.push(Texture.from(images[i]));
        }

        return new AnimatedSprite(textures);
    }

    /**
     * The total number of frames in the AnimatedSprite. This is the same as number of textures
     * assigned to the AnimatedSprite.
     *
     * @readonly
     * @member {number}
     * @default 0
     */
    get totalFrames()
    {
        return this._textures.length;
    }

    /**
     * The array of textures used for this AnimatedSprite.
     *
     * @member {PIXI.Texture[]}
     */
    get textures()
    {
        return this._textures;
    }

    set textures(value) // eslint-disable-line require-jsdoc
    {
        if (value[0] instanceof Texture)
        {
            this._textures = value;
            this._durations = null;
        }
        else
        {
            this._textures = [];
            this._durations = [];

            for (let i = 0; i < value.length; i++)
            {
                this._textures.push(value[i].texture);
                this._durations.push(value[i].time);
            }
        }
        this.gotoAndStop(0);
        this.updateTexture();
    }

    /**
    * The AnimatedSprites current frame index.
    *
    * @member {number}
    * @readonly
    */
    get currentFrame()
    {
        let currentFrame = Math.floor(this._currentTime) % this._textures.length;

        if (currentFrame < 0)
        {
            currentFrame += this._textures.length;
        }

        return currentFrame;
    }
}

/**
 * @memberof PIXI.AnimatedSprite
 * @typedef {object} FrameObject
 * @type {object}
 * @property {PIXI.Texture} texture - The {@link PIXI.Texture} of the frame
 * @property {number} time - the duration of the frame in ms
 */
<|endoftext|>


<commit_before>import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';

// For props table
import HeaderRow from '@ichef/gypcrete/src/HeaderRow';

import BasicUsage from './BasicUsage';
import OptionalArea from './OptionalArea';

storiesOf('HeaderRow', module)
    .add('Basic usage', withInfo()(BasicUsage))
    .add('Optional area', withInfo()(OptionalArea))
    // Props table
    .addPropsTable(() => <HeaderRow />);
<commit_msg>Add description for <HeaderRow> optional area example<commit_after>import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';

// For props table
import HeaderRow from '@ichef/gypcrete/src/HeaderRow';

import BasicUsage from './BasicUsage';
import OptionalArea from './OptionalArea';

storiesOf('HeaderRow', module)
    .add('Basic usage', withInfo()(BasicUsage))
    .add(
        'Optional area',
        withInfo('Remove an area from DOM by explictly setting it to false.')(OptionalArea)
    )

    // Props table
    .addPropsTable(() => <HeaderRow />);
<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import {storiesOf} from '@storybook/react'
import {action} from '@storybook/addon-actions'
import {withKnobs, boolean} from '@storybook/addon-knobs'

import Select from './'
import {Select as Raw} from './Select'

const options = [
  {key: 1, display: 'Option 1'},
  {key: 2, display: 'Option 2'},
  {key: 3, display: 'Option 3'},
  {key: 4, display: 'Option 4'},
  {key: 5, display: 'Option 5'}
]

class Story extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isLoading: false,
      options: null,
      value: [{key: 3, display: 'Option 3.'}],
      moreOptionsAvailable: false,
      tooltips: {}
    }
  }

  fetchOptions = () => {
    if (this.state.options === null) {
      this.setState({...this.state, options: null, isLoading: true})
      setTimeout(() => { this.setState({...this.state, options, isLoading: false}) }, 2000)
    }
  }

  searchOptions = () => {
    this.setState({...this.state, options: null, isLoading: true})
    setTimeout(() => {
      this.setState({
        ...this.state,
        options: options.filter((e, idx) => idx % 2 === 0),
        isLoading: false,
        moreOptionsAvailable: true
      })
    }, 2000)
  }

  setValue = value => {
    this.setState({...this.state, value})
  }

  loadToolTip = key => {
    if (!this.state.tooltips[key]) {
      this.setState({...this.state, tooltips: {...this.state.tooltips, [key]: `TOOL TIP ${key}`}})
    }
  }

  render() {
    return (

        <Select
          isMulti={this.props.isMulti}
          fetchOptions={this.fetchOptions}
          searchOptions={this.searchOptions}
          options={this.state.options}
          isLoading={this.state.isLoading}
          onChange={this.setValue}
          value={this.state.value}
          noResultsText="No."
          moreOptionsAvailable={this.state.moreOptionsAvailable}
          moreOptionsAvailableText="More options available"
          openAdvancedSearch={action('open advanced search')}
          tooltips={this.state.tooltips}
          loadTooltip={this.loadToolTip}
        />

    )
  }
}

Story.propTypes = {
  isMulti: PropTypes.bool

}

storiesOf('Select', module)
  .addDecorator(withKnobs)
  .add(
    'Select',
    () => <Story isMulti={boolean('isMulti', true)} action={action}/>,
    {info: {propTables: [Raw], propTablesExclude: [Story], source: false}}
  )
<commit_msg>refactor(tocco-ui): story adjustments according #613 & fix test<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import {storiesOf} from '@storybook/react'
import {action} from '@storybook/addon-actions'
import {withKnobs, boolean} from '@storybook/addon-knobs'

import Select from './'
import {Select as Raw} from './Select'

const options = [
  {key: 1, display: 'Option 1'},
  {key: 2, display: 'Option 2'},
  {key: 3, display: 'Option 3'},
  {key: 4, display: 'Option 4'},
  {key: 5, display: 'Option 5'}
]

class Story extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isLoading: false,
      options: null,
      value: [{key: 3, display: 'Option 3'}],
      moreOptionsAvailable: false,
      tooltips: {}
    }
  }

  fetchOptions = () => {
    if (this.state.options === null) {
      this.setState({...this.state, options: null, isLoading: true})
      setTimeout(() => { this.setState({...this.state, options, isLoading: false}) }, 2000)
    }
  }

  searchOptions = () => {
    this.setState({...this.state, options: null, isLoading: true})
    setTimeout(() => {
      this.setState({
        ...this.state,
        options: options.filter((e, idx) => idx % 2 === 0),
        isLoading: false,
        moreOptionsAvailable: true
      })
    }, 2000)
  }

  setValue = value => {
    this.setState({...this.state, value})
  }

  loadToolTip = key => {
    if (!this.state.tooltips[key]) {
      this.setState({...this.state, tooltips: {...this.state.tooltips, [key]: `TOOL TIP ${key}`}})
    }
  }

  render() {
    return (

        <Select
          isMulti={this.props.isMulti}
          fetchOptions={this.fetchOptions}
          searchOptions={this.searchOptions}
          options={this.state.options}
          isLoading={this.state.isLoading}
          onChange={this.setValue}
          value={this.state.value}
          noResultsText="No more options."
          moreOptionsAvailable={this.state.moreOptionsAvailable}
          moreOptionsAvailableText="More options available"
          openAdvancedSearch={action('open advanced search')}
          tooltips={this.state.tooltips}
          loadTooltip={this.loadToolTip}
        />

    )
  }
}

Story.propTypes = {
  isMulti: PropTypes.bool
}

storiesOf('Select', module)
  .addDecorator(withKnobs)
  .add(
    'Select',
    () => <Story isMulti={boolean('isMulti', true)} action={action}/>,
    {info: {propTables: [Raw], propTablesExclude: [Story], source: false}}
  )
<|endoftext|>


<commit_before>define(function () {
  'use strict';

  var $ = global.jQuery;

  function getPromisedString() {
    return new Promise(function (fulfill) {
      fulfill('simple promised string!');
    });
  }

  function asyncFN(async) {
    return async(function * () {
      var str = yield getPromisedString();
      return 'This is just a ' + str;
    });
  }

  function asyncPromiseFN(async) {
    return async.Promise(function * (fulfill) {
      var str = yield getPromisedString();
      str = 'This is just a ' + str;
      fulfill(str);
      return str;
    });
  }

  fix.test('async', {
    message: 'async module provides a means to deal with function generators',
    resolver: function (assert, async) {
      var asyncValue = asyncFN(async)(),
        asyncPromiseValue = asyncPromiseFN(async);

      return Promise.all([asyncValue, asyncPromiseValue]);
    }
  }).then(function (assert, async, promisedArray) {

    assert.strictEqual(typeof async, 'function', 'async is actually a function');

    assert.strictEqual(typeof async.Promise, 'function', 'async.Promise is also a function');

    assert.ok($.isArray(promisedArray), 'both promised values are present');

    assert.strictEqual(promisedArray.length, 2, 'promisedArray has two member as we expect');

    assert.strictEqual(promisedArray[0], 'This is just a simple promised string!', 'async works as expected!');

    assert.strictEqual(promisedArray[0], promisedArray[1], 'async and async.Promise both work as expected!');

  });
});<commit_msg>test async rejections<commit_after>define(function () {
  'use strict';

  var $ = global.jQuery;

  function getPromisedString() {
    return new Promise(function (fulfill) {
      fulfill('simple promised string!');
    });
  }

  function asyncFN(async) {
    return async(function * () {
      var str = yield getPromisedString();
      return 'This is just a ' + str;
    });
  }

  function asyncPromiseFN(async) {
    return async.Promise(function * (fulfill) {
      var str = yield getPromisedString();
      str = 'This is just a ' + str;
      fulfill(str);
      return str;
    });
  }

  function asyncRejectedPromiseFN(async) {
    return async.Promise(function * (fulfill, reject) {
      var str = yield getPromisedString();
      str = 'This is just a ' + str;
      reject(str);
      return str;
    });
  }

  fix.test('async', {
    message: 'async module provides a means to deal with function generators',
    resolver: function (assert, async) {
      var asyncValue = asyncFN(async)(),
        asyncPromiseValue = asyncPromiseFN(async),
        rejectedPromise;

      rejectedPromise = asyncRejectedPromiseFN(async)
        .catch(function (str) {
          return 'ERROR:' + str;
        });

      return Promise.all([asyncValue, asyncPromiseValue, rejectedPromise]);
    }
  }).then(function (assert, async, promisedArray) {

    assert.strictEqual(typeof async, 'function', 'async is actually a function');

    assert.strictEqual(typeof async.Promise, 'function', 'async.Promise is also a function');

    assert.ok($.isArray(promisedArray), 'both promised values are present');

    assert.strictEqual(promisedArray.length, 3, 'promisedArray has two member as we expect');

    assert.strictEqual(promisedArray[0], 'This is just a simple promised string!', 'async works as expected!');

    assert.strictEqual(promisedArray[0], promisedArray[1], 'async and async.Promise both work as expected!');

    assert.strictEqual(promisedArray[2], 'ERROR:' + promisedArray[0], 'async handles rejection and exceptions!');

    var genFN,
      asyncGen1,
      asyncGen2;

    genFN = function * () {
      var str = yield 'str';
      return str;
    };

    asyncGen1 = async(genFN);
    asyncGen2 = async(genFN);

    assert.strictEqual(asyncGen1, asyncGen2, 'async caches the async alternative of function generators!');

  });
});<|endoftext|>


<commit_before>import { COLORS } from './colors';
import { DISPLAY_MODES } from './displayModes';

/**
 * Cursor implements low-level API to terminal control codes.
 *
 * @see http://www.termsys.demon.co.uk/vtansi.htm
 * @see http://misc.flogisoft.com/bash/tip_colors_and_formatting
 * @see http://man7.org/linux/man-pages/man4/console_codes.4.html
 * @see http://www.x.org/docs/xterm/ctlseqs.pdf
 * @since 1.0.0
 */
export default class Cursor {
  /**
   * Creates cursor that writes direct to `stdout`.
   *
   * @constructor
   */
  constructor() {
    this._width = process.stdout.columns;
    this._height = process.stdout.rows;

    this._x = 0;
    this._y = 0;

    this._background = 'black';
    this._foreground = 'white';
    this._display = false;

    this._buffer = Array.from({length: this._width * this._height}).fill(' ');
  }

  /**
   * Get index of the buffer from (x, y) coordinates of from current pointer.
   *
   * @param {Number} [x] X coordinate on the terminal
   * @param {Number} [y] Y coordinate on the terminal
   * @returns {Number} Returns index in the buffer array
   */
  getBufferPointer(x = this._x, y = this._y) {
    return y * this._width + x;
  }

  /**
   * Get (x, y) coordinate from the buffer pointer.
   *
   * @param {Number} index Index in the buffer
   * @returns {Array} Returns an array [x, y]
   */
  getXYFromPointer(index) {
    return [index - (Math.floor(index / this._width) * this._width), Math.floor(index / this._width)];
  }

  /**
   * Write to the buffer.
   * Usually it's just a text that you want to print out but also can be a Buffer with control codes.
   * Cursor has a feature to buffering data, so when you will be ready to push to the stream, call {@link flush} method.
   *
   * @param {Buffer|String} data Data to write to the stream
   * @returns {Cursor}
   */
  write(data) {
    // TODO: refactor here
    data.split('').forEach(char => {
      const [x, y] = [this._x, this._y];
      const pointer = this.getBufferPointer(x, y);
      const position = Cursor.encodeToVT100(`[${Math.floor(y + 1)};${Math.floor(x + 1)}f`);
      const foreground = Cursor.encodeToVT100(`[38;5;${COLORS[this._foreground.toUpperCase()]}m`);
      const background = Cursor.encodeToVT100(`[48;5;${COLORS[this._background.toUpperCase()]}m`);
      const display = this._display ? Cursor.encodeToVT100(`[${this._display}m`) : '';
      const reset = Cursor.encodeToVT100(`[${DISPLAY_MODES.RESET_ALL}m`);

      if (0 <= x && x < this._width && 0 <= y && y < this._height) {
        this._buffer[pointer] = position + foreground + background + display + char + reset;
      }

      this._x++;
    });

    return this;
  }

  /**
   * Write from the buffer to stream and clear it up.
   *
   * @returns {Cursor}
   */
  flush() {
    // TODO: make diff and write only diff
    process.stdout.write(this._buffer.join(''));
    return this;
  }

  /**
   * Draw an image in terminal.
   * Supports only by few terminals, as I know only in iTerm 2 (v2.9).
   *
   * @param {String} image Base64 encoded image content
   * @param {Number|String} [width='auto'] Width to render, can be 100 (cells), 100px, 100% or auto
   * @param {Number|String} [height='auto'] Height to render, can be 100 (cells), 100px, 100% or auto
   * @param {Boolean} [preserveAspectRatio=true] If set to false, the image's aspect ratio will not be respected
   * @returns {Cursor}
   */
  image({image, width='auto', height='auto', preserveAspectRatio = true}) {
    const args = `width=${width};height=${height};preserveAspectRatio=${preserveAspectRatio ? 1 : 0};inline=1`;
    return this.write(Cursor.encodeToVT100(`]1337;File=${args}:${image}^G`));
  }

  /**
   * Move the cursor up.
   *
   * @param {Number} [y=1] Rows count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  up(y = 1) {
    this._y -= y;
    return this;
  }

  /**
   * Move the cursor down.
   *
   * @param {Number} [y=1] Rows count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  down(y = 1) {
    this._y += y;
    return this;
  }

  /**
   * Move the cursor right.
   *
   * @param {Number} [x=1] Columns count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  right(x = 1) {
    this._x += x;
    return this;
  }

  /**
   * Move the cursor left.
   *
   * @param {Number} [x=1] Columns count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  left(x = 1) {
    this._x -= x;
    return this;
  }

  /**
   * Move the cursor position relative current coordinates.
   *
   * @param {Number} x Offset by X coordinate
   * @param {Number} y Offset by Y coordinate
   * @returns {Cursor}
   */
  moveBy(x, y) {
    if (x < 0) this.left(-x);
    if (x > 0) this.right(x);

    if (y < 0) this.up(-y);
    if (y > 0) this.down(y);

    return this;
  }

  /**
   * Set the cursor position by absolute coordinates.
   *
   * @param {Number} x X coordinate
   * @param {Number} y Y coordinate
   * @returns {Cursor}
   */
  moveTo(x, y) {
    this._x = x;
    this._y = y;

    return this;
  }

  /**
   * Set the foreground color.
   * This color is used when text is rendering.
   *
   * @param {String} color Color name
   * @returns {Cursor}
   */
  foreground(color) {
    this._foreground = color;
    return this;
  }

  /**
   * Set the background color.
   * This color is used for filling the whole cell in the TTY.
   *
   * @param {String} color Color name
   * @returns {Cursor}
   */
  background(color) {
    this._background = color;
    return this;
  }

  /**
   * Change display mode (format of text).
   * You can also use helper methods like {@link bold} or {@link blink}, etc...
   *
   * @param {Number} mode Mode identifier from {@link DISPLAY_MODES}
   * @returns {Cursor}
   */
  display(mode) {
    this._display = mode;
    return this;
  }

  /**
   * Toggle bold display mode.
   *
   * @param {Boolean} [isBold=true] If false, disables bold mode
   * @returns {Cursor}
   */
  bold(isBold = true) {
    return this.display(isBold ? DISPLAY_MODES.BOLD : DISPLAY_MODES.RESET_BOLD);
  }

  /**
   * Toggle dim display mode.
   *
   * @param {Boolean} [isDim=true] If false, disables dim mode
   * @returns {Cursor}
   */
  dim(isDim = true) {
    return this.display(isDim ? DISPLAY_MODES.DIM : DISPLAY_MODES.RESET_DIM);
  }

  /**
   * Toggle underlined display mode.
   *
   * @param {Boolean} [isUnderlined=true] If false, disables underlined mode
   * @returns {Cursor}
   */
  underlined(isUnderlined = true) {
    return this.display(isUnderlined ? DISPLAY_MODES.UNDERLINED : DISPLAY_MODES.RESET_UNDERLINED);
  }

  /**
   * Toggle blink display mode.
   *
   * @param {Boolean} [isBlink=true] If false, disables blink mode
   * @returns {Cursor}
   */
  blink(isBlink = true) {
    return this.display(isBlink ? DISPLAY_MODES.BLINK : DISPLAY_MODES.RESET_BLINK);
  }

  /**
   * Toggle reverse display mode.
   *
   * @param {Boolean} [isReverse=true] If false, disables reverse display mode
   * @returns {Cursor}
   */
  reverse(isReverse = true) {
    return this.display(isReverse ? DISPLAY_MODES.REVERSE : DISPLAY_MODES.RESET_REVERSE);
  }

  /**
   * Toggle hidden display mode.
   *
   * @param {Boolean} [isHidden=true] If false, disables hidden display mode
   * @returns {Cursor}
   */
  hidden(isHidden = true) {
    return this.display(isHidden ? DISPLAY_MODES.HIDDEN : DISPLAY_MODES.RESET_HIDDEN);
  }

  /**
   * Erase from current position to end of the line.
   *
   * @returns {Cursor}
   */
  eraseToEnd() {
    for (let x = this._x; x < this._width; x++) this._buffer[this.getBufferPointer(x, this._y)] = ' ';
    return this;
  }

  /**
   * Erase from current position to start of the line.
   *
   * @returns {Cursor}
   */
  eraseToStart() {
    for (let x = this._x; x >= 0; x--) this._buffer[this.getBufferPointer(x, this._y)] = ' ';
    return this;
  }

  /**
   * Erase from current line to down.
   *
   * @returns {Cursor}
   */
  eraseToDown() {
    for (let y = this._y; y < this._height; y++) {
      for (let x = 0; x < this._width; x++) {
        this._buffer[this.getBufferPointer(x, y)] = ' ';
      }
    }

    return this;
  }

  /**
   * Erase from current line to up.
   *
   * @returns {Cursor}
   */
  eraseToUp() {
    for (let y = this._y; y >= 0; y--) {
      for (let x = 0; x < this._width; x++) {
        this._buffer[this.getBufferPointer(x, y)] = ' ';
      }
    }

    return this;
  }

  /**
   * Erase current line.
   *
   * @returns {Cursor}
   */
  eraseLine() {
    for (let x = 0; x < this._width; x++) this._buffer[this.getBufferPointer(x, this._y)] = '';
    return this;
  }

  /**
   * Erase the entire screen.
   *
   * @returns {Cursor}
   */
  eraseScreen() {
    this._buffer.fill(' ');
    return this;
  }

  /**
   * Set the cursor invisible.
   *
   * @returns {Cursor}
   */
  hideCursor() {
    process.stdout.write(Cursor.encodeToVT100('[?25l'));
    return this;
  }

  /**
   * Set the cursor visible.
   *
   * @returns {Cursor}
   */
  showCursor() {
    process.stdout.write(Cursor.encodeToVT100('[?25h'));
    return this;
  }

  /**
   * Reset all terminal settings to default.
   *
   * @returns {Cursor}
   */
  resetTTY() {
    this.eraseScreen();
    process.stdout.write(Cursor.encodeToVT100('c'));
    return this;
  }

  /**
   * Bytes to encode to VT100 standard.
   *
   * @static
   * @param {String} string
   * @returns {Buffer} Returns encoded bytes
   */
  static encodeToVT100(string) {
    return new Buffer([0x1b].concat(string.split('').map(item => item.charCodeAt(0))));
  }

  /**
   * Parse ASCII control codes and return array with them.
   *
   * @param {String} _string
   * @returns {Array}
   */
  static decodeFromVT100(_string) {
    // TODO: refactor here
    const string = new Buffer(_string, 'utf-8');
    const codes = [];

    let start = -1;

    for (let i = 0; i < string.length; i++) {
      if (string[i] === 27) {
        if (start >= 0) codes.push(string.slice(start, i));
        start = i;
      } else if (start >= 0 && i === string.length - 1) {
        codes.push(string.slice(start));
      }
    }

    return codes;
  }

  /**
   * Wrapper around `new Cursor()`.
   *
   * @static
   * @returns {Cursor}
   */
  static create() {
    return new this();
  }
}
<commit_msg>refactor(cursor): Move building ASCII sequence to separate method<commit_after>import { COLORS } from './colors';
import { DISPLAY_MODES } from './displayModes';

/**
 * Cursor implements low-level API to terminal control codes.
 *
 * @see http://www.termsys.demon.co.uk/vtansi.htm
 * @see http://misc.flogisoft.com/bash/tip_colors_and_formatting
 * @see http://man7.org/linux/man-pages/man4/console_codes.4.html
 * @see http://www.x.org/docs/xterm/ctlseqs.pdf
 * @since 1.0.0
 */
export default class Cursor {
  /**
   * Creates cursor that writes direct to `stdout`.
   *
   * @constructor
   */
  constructor() {
    this._width = process.stdout.columns;
    this._height = process.stdout.rows;

    this._x = 0;
    this._y = 0;

    this._background = false;
    this._foreground = false;
    this._display = false;

    this._buffer = Array.from({length: this._width * this._height}).fill(' ');
  }

  /**
   * Wrap char with all control codes needed for rendering the cell.
   *
   * @param {String} char
   * @param {Object} options
   * @returns {String} Returns ready to flush string with ASCII control codes
   */
  wrap(char, options = {}) {
    const {x = this._x, y = this._y, background = this._background, foreground = this._foreground, display = this._display} = options;

    return (
      Cursor.encodeToVT100(`[${Math.floor(y + 1)};${Math.floor(x + 1)}f`) +
      (background ? Cursor.encodeToVT100(`[48;5;${COLORS[background.toUpperCase()]}m`) : '') +
      (foreground ? Cursor.encodeToVT100(`[38;5;${COLORS[foreground.toUpperCase()]}m`) : '') +
      (display ? Cursor.encodeToVT100(`[${display}m`) : '') +
      char +
      Cursor.encodeToVT100(`[${DISPLAY_MODES.RESET_ALL}m`)
    );
  }

  /**
   * Get index of the buffer from (x, y) coordinates of from current pointer.
   *
   * @param {Number} [x] X coordinate on the terminal
   * @param {Number} [y] Y coordinate on the terminal
   * @returns {Number} Returns index in the buffer array
   */
  getBufferPointer(x = this._x, y = this._y) {
    return y * this._width + x;
  }

  /**
   * Get (x, y) coordinate from the buffer pointer.
   *
   * @param {Number} index Index in the buffer
   * @returns {Array} Returns an array [x, y]
   */
  getXYFromPointer(index) {
    return [index - (Math.floor(index / this._width) * this._width), Math.floor(index / this._width)];
  }

  /**
   * Write to the buffer.
   * Usually it's just a text that you want to print out but also can be a Buffer with control codes.
   * Cursor has a feature to buffering data, so when you will be ready to push to the stream, call {@link flush} method.
   *
   * @param {Buffer|String} data Data to write to the stream
   * @returns {Cursor}
   */
  write(data) {
    // TODO: refactor here
    data.split('').forEach(char => {
      const [x, y] = [this._x, this._y];
      const pointer = this.getBufferPointer(x, y);

      if (0 <= x && x < this._width && 0 <= y && y < this._height) this._buffer[pointer] = this.wrap(char);

      this._x++;
    });

    return this;
  }

  /**
   * Write from the buffer to stream and clear it up.
   *
   * @returns {Cursor}
   */
  flush() {
    // TODO: make diff and write only diff
    process.stdout.write(this._buffer.join(''));
    return this;
  }

  /**
   * Draw an image in terminal.
   * Supports only by few terminals, as I know only in iTerm 2 (v2.9).
   *
   * @param {String} image Base64 encoded image content
   * @param {Number|String} [width='auto'] Width to render, can be 100 (cells), 100px, 100% or auto
   * @param {Number|String} [height='auto'] Height to render, can be 100 (cells), 100px, 100% or auto
   * @param {Boolean} [preserveAspectRatio=true] If set to false, the image's aspect ratio will not be respected
   * @returns {Cursor}
   */
  image({image, width='auto', height='auto', preserveAspectRatio = true}) {
    const args = `width=${width};height=${height};preserveAspectRatio=${preserveAspectRatio ? 1 : 0};inline=1`;
    return this.write(Cursor.encodeToVT100(`]1337;File=${args}:${image}^G`));
  }

  /**
   * Move the cursor up.
   *
   * @param {Number} [y=1] Rows count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  up(y = 1) {
    this._y -= y;
    return this;
  }

  /**
   * Move the cursor down.
   *
   * @param {Number} [y=1] Rows count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  down(y = 1) {
    this._y += y;
    return this;
  }

  /**
   * Move the cursor right.
   *
   * @param {Number} [x=1] Columns count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  right(x = 1) {
    this._x += x;
    return this;
  }

  /**
   * Move the cursor left.
   *
   * @param {Number} [x=1] Columns count must be positive number otherwise it just wouldn't work
   * @returns {Cursor}
   */
  left(x = 1) {
    this._x -= x;
    return this;
  }

  /**
   * Move the cursor position relative current coordinates.
   *
   * @param {Number} x Offset by X coordinate
   * @param {Number} y Offset by Y coordinate
   * @returns {Cursor}
   */
  moveBy(x, y) {
    if (x < 0) this.left(-x);
    if (x > 0) this.right(x);

    if (y < 0) this.up(-y);
    if (y > 0) this.down(y);

    return this;
  }

  /**
   * Set the cursor position by absolute coordinates.
   *
   * @param {Number} x X coordinate
   * @param {Number} y Y coordinate
   * @returns {Cursor}
   */
  moveTo(x, y) {
    this._x = x;
    this._y = y;

    return this;
  }

  /**
   * Set the foreground color.
   * This color is used when text is rendering.
   *
   * @param {String} color Color name
   * @returns {Cursor}
   */
  foreground(color) {
    this._foreground = color;
    return this;
  }

  /**
   * Set the background color.
   * This color is used for filling the whole cell in the TTY.
   *
   * @param {String} color Color name
   * @returns {Cursor}
   */
  background(color) {
    this._background = color;
    return this;
  }

  /**
   * Change display mode (format of text).
   * You can also use helper methods like {@link bold} or {@link blink}, etc...
   *
   * @param {Number} mode Mode identifier from {@link DISPLAY_MODES}
   * @returns {Cursor}
   */
  display(mode) {
    this._display = mode;
    return this;
  }

  /**
   * Toggle bold display mode.
   *
   * @param {Boolean} [isBold=true] If false, disables bold mode
   * @returns {Cursor}
   */
  bold(isBold = true) {
    return this.display(isBold ? DISPLAY_MODES.BOLD : DISPLAY_MODES.RESET_BOLD);
  }

  /**
   * Toggle dim display mode.
   *
   * @param {Boolean} [isDim=true] If false, disables dim mode
   * @returns {Cursor}
   */
  dim(isDim = true) {
    return this.display(isDim ? DISPLAY_MODES.DIM : DISPLAY_MODES.RESET_DIM);
  }

  /**
   * Toggle underlined display mode.
   *
   * @param {Boolean} [isUnderlined=true] If false, disables underlined mode
   * @returns {Cursor}
   */
  underlined(isUnderlined = true) {
    return this.display(isUnderlined ? DISPLAY_MODES.UNDERLINED : DISPLAY_MODES.RESET_UNDERLINED);
  }

  /**
   * Toggle blink display mode.
   *
   * @param {Boolean} [isBlink=true] If false, disables blink mode
   * @returns {Cursor}
   */
  blink(isBlink = true) {
    return this.display(isBlink ? DISPLAY_MODES.BLINK : DISPLAY_MODES.RESET_BLINK);
  }

  /**
   * Toggle reverse display mode.
   *
   * @param {Boolean} [isReverse=true] If false, disables reverse display mode
   * @returns {Cursor}
   */
  reverse(isReverse = true) {
    return this.display(isReverse ? DISPLAY_MODES.REVERSE : DISPLAY_MODES.RESET_REVERSE);
  }

  /**
   * Toggle hidden display mode.
   *
   * @param {Boolean} [isHidden=true] If false, disables hidden display mode
   * @returns {Cursor}
   */
  hidden(isHidden = true) {
    return this.display(isHidden ? DISPLAY_MODES.HIDDEN : DISPLAY_MODES.RESET_HIDDEN);
  }

  /**
   * Erase from current position to end of the line.
   *
   * @returns {Cursor}
   */
  eraseToEnd() {
    for (let x = this._x; x < this._width; x++) this._buffer[this.getBufferPointer(x, this._y)] = ' ';
    return this;
  }

  /**
   * Erase from current position to start of the line.
   *
   * @returns {Cursor}
   */
  eraseToStart() {
    for (let x = this._x; x >= 0; x--) this._buffer[this.getBufferPointer(x, this._y)] = ' ';
    return this;
  }

  /**
   * Erase from current line to down.
   *
   * @returns {Cursor}
   */
  eraseToDown() {
    for (let y = this._y; y < this._height; y++) {
      for (let x = 0; x < this._width; x++) {
        this._buffer[this.getBufferPointer(x, y)] = ' ';
      }
    }

    return this;
  }

  /**
   * Erase from current line to up.
   *
   * @returns {Cursor}
   */
  eraseToUp() {
    for (let y = this._y; y >= 0; y--) {
      for (let x = 0; x < this._width; x++) {
        this._buffer[this.getBufferPointer(x, y)] = ' ';
      }
    }

    return this;
  }

  /**
   * Erase current line.
   *
   * @returns {Cursor}
   */
  eraseLine() {
    for (let x = 0; x < this._width; x++) this._buffer[this.getBufferPointer(x, this._y)] = '';
    return this;
  }

  /**
   * Erase the entire screen.
   *
   * @returns {Cursor}
   */
  eraseScreen() {
    this._buffer.fill(' ');
    return this;
  }

  /**
   * Set the cursor invisible.
   *
   * @returns {Cursor}
   */
  hideCursor() {
    process.stdout.write(Cursor.encodeToVT100('[?25l'));
    return this;
  }

  /**
   * Set the cursor visible.
   *
   * @returns {Cursor}
   */
  showCursor() {
    process.stdout.write(Cursor.encodeToVT100('[?25h'));
    return this;
  }

  /**
   * Reset all terminal settings to default.
   *
   * @returns {Cursor}
   */
  resetTTY() {
    process.stdout.write(Cursor.encodeToVT100('c'));
    return this;
  }

  /**
   * Bytes to encode to VT100 standard.
   *
   * @static
   * @param {String} string
   * @returns {Buffer} Returns encoded bytes
   */
  static encodeToVT100(string) {
    return new Buffer([0x1b].concat(string.split('').map(item => item.charCodeAt(0))));
  }

  /**
   * Wrapper around `new Cursor()`.
   *
   * @static
   * @returns {Cursor}
   */
  static create() {
    return new this();
  }
}
<|endoftext|>