<commit_before>/**
 * Event emitter.
 *
 * @class OO.EventEmitter
 * @constructor
 * @property {Object} bindings
 */
oo.EventEmitter = function OoEventEmitter() {
	// Properties
	this.bindings = {};
};

/* Methods */

/**
 * Add a listener to events of a specific event.
 *
 * @method
 * @param {string} event Type of event to listen to
 * @param {Function} callback Function to call when event occurs
 * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments
 * @param {Object} [context=null] Object to use as context for callback function or call method on
 * @throws {Error} Listener argument is not a function or method name
 * @chainable
 */
oo.EventEmitter.prototype.on = function ( event, callback, args, context ) {
	// Validate callback
	if ( typeof callback !== 'function' ) {
		throw new Error( 'Invalid callback. Function or method name expected.' );
	}

	// Auto-initialize binding
	if ( !( event in this.bindings ) ) {
		this.bindings[event] = [];
	}

	// Add binding
	this.bindings[event].push( {
		'callback': callback,
		'args': args,
		'context': context || null
	} );
	return this;
};

/**
 * Adds a one-time listener to a specific event.
 *
 * @method
 * @param {string} event Type of event to listen to
 * @param {Function} listener Listener to call when event occurs
 * @chainable
 */
oo.EventEmitter.prototype.once = function ( event, listener ) {
	var eventEmitter = this;
	return this.on( event, function listenerWrapper() {
		eventEmitter.off( event, listenerWrapper );
		listener.apply( eventEmitter, Array.prototype.slice.call( arguments, 0 ) );
	} );
};

/**
 * Remove a specific listener from a specific event.
 *
 * @method
 * @param {string} event Type of event to remove listener from
 * @param {Function} [callback] Listener to remove, omit to remove all
 * @chainable
 * @throws {Error} Listener argument is not a function
 */
oo.EventEmitter.prototype.off = function ( event, callback ) {
	var i, bindings;

	if ( arguments.length === 1 ) {
		// Remove all bindings for event
		if ( event in this.bindings ) {
			delete this.bindings[event];
		}
	} else {
		if ( typeof callback !== 'function' ) {
			throw new Error( 'Invalid callback. Function expected.' );
		}
		if ( !( event in this.bindings ) || !this.bindings[event].length ) {
			// No matching bindings
			return this;
		}
		// Remove matching handlers
		bindings = this.bindings[event];
		i = bindings.length;
		while ( i-- ) {
			if ( bindings[i].callback === callback ) {
				bindings.splice( i, 1 );
			}
		}
		// Cleanup if now empty
		if ( bindings.length === 0 ) {
			delete this.bindings[event];
		}
	}
	return this;
};

/**
 * Emit an event.
 * TODO: Should this be chainable? What is the usefulness of the boolean
 * return value here?
 *
 * @method
 * @param {string} event Type of event
 * @param {Mixed} args First in a list of variadic arguments passed to event handler (optional)
 * @returns {boolean} If event was handled by at least one listener
 */
oo.EventEmitter.prototype.emit = function ( event ) {
	var i, len, binding, bindings, args;

	if ( event in this.bindings ) {
		// Slicing ensures that we don't get tripped up by event handlers that add/remove bindings
		bindings = this.bindings[event].slice();
		args = Array.prototype.slice.call( arguments, 1 );
		for ( i = 0, len = bindings.length; i < len; i++ ) {
			binding = bindings[i];
			binding.callback.apply(
				binding.context,
				binding.args ? binding.args.concat( args ) : args
			);
		}
		return true;
	}
	return false;
};

/**
 * Connect event handlers to an object.
 *
 * @method
 * @param {Object} context Object to call methods on when events occur
 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} methods List of
 * event bindings keyed by event name containing either method names, functions or arrays containing
 * method name or function followed by a list of arguments to be passed to callback before emitted
 * arguments
 * @chainable
 */
oo.EventEmitter.prototype.connect = function ( context, methods ) {
	var method, callback, args, event;

	for ( event in methods ) {
		method = methods[event];
		// Allow providing additional args
		if ( Array.isArray( method ) ) {
			args = method.slice( 1 );
			method = method[0];
		} else {
			args = [];
		}
		// Allow callback to be a method name
		if ( typeof method === 'string' ) {
			// Validate method
			if ( !context[method] || typeof context[method] !== 'function' ) {
				throw new Error( 'Method not found: ' + method );
			}
			// Resolve to function
			callback = context[method];
		} else {
			callback = method;
		}
		// Add binding
		this.on.apply( this, [ event, callback, args, context ] );
	}
	return this;
};

/**
 * Disconnect event handlers from an object.
 *
 * @method
 * @param {Object} context Object to disconnect methods from
 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} [methods] List of
 * event bindings keyed by event name containing either method names or functions
 * @chainable
 */
oo.EventEmitter.prototype.disconnect = function ( context, methods ) {
	var i, method, callback, event, bindings;

	if ( methods ) {
		for ( event in methods ) {
			method = methods[event];
			if ( typeof method === 'string' ) {
				// Validate method
				if ( !context[method] || typeof context[method] !== 'function' ) {
					throw new Error( 'Method not found: ' + method );
				}
				// Resolve to function
				callback = context[method];
			} else {
				callback = method;
			}
			bindings = this.bindings[event];
			i = bindings.length;
			while ( i-- ) {
				if ( bindings[i].context === context && bindings[i].callback === callback ) {
					bindings.splice( i, 1 );
				}
			}
			if ( bindings.length === 0 ) {
				delete this.bindings[event];
			}
		}
	} else {
		for ( event in this.bindings ) {
			bindings = this.bindings[event];
			i = bindings.length;
			while ( i-- ) {
				if ( bindings[i].context === context ) {
					bindings.splice( i, 1 );
				}
			}
			if ( bindings.length === 0 ) {
				delete this.bindings[event];
			}
		}
	}

	return this;
};
<commit_msg>Re-use #off in #connect and add context argument to #off<commit_after>/**
 * Event emitter.
 *
 * @class OO.EventEmitter
 * @constructor
 * @property {Object} bindings
 */
oo.EventEmitter = function OoEventEmitter() {
	// Properties
	this.bindings = {};
};

/* Methods */

/**
 * Add a listener to events of a specific event.
 *
 * If the callback/context are already bound to the event, they will not be bound again.
 *
 * @method
 * @param {string} event Type of event to listen to
 * @param {Function} callback Function to call when event occurs
 * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments
 * @param {Object} [context=null] Object to use as context for callback function or call method on
 * @throws {Error} Listener argument is not a function or method name
 * @chainable
 */
oo.EventEmitter.prototype.on = function ( event, callback, args, context ) {
	var i, bindings, binding;

	// Validate callback
	if ( typeof callback !== 'function' ) {
		throw new Error( 'Invalid callback. Function or method name expected.' );
	}
	// Fallback to null context
	if ( arguments.length < 4 ) {
		context = null;
	}
	if ( this.bindings.hasOwnProperty( event ) ) {
		// Check for duplicate callback and context for this event
		bindings = this.bindings[event];
		i = bindings.length;
		while ( i-- ) {
			binding = bindings[i];
			if ( bindings.callback === callback && bindings.context === context ) {
				return this;
			}
		}
	} else {
		// Auto-initialize bindings list
		bindings = this.bindings[event] = [];
	}
	// Add binding
	bindings.push( {
		'callback': callback,
		'args': args,
		'context': context
	} );
	return this;
};

/**
 * Adds a one-time listener to a specific event.
 *
 * @method
 * @param {string} event Type of event to listen to
 * @param {Function} listener Listener to call when event occurs
 * @chainable
 */
oo.EventEmitter.prototype.once = function ( event, listener ) {
	var eventEmitter = this;
	return this.on( event, function listenerWrapper() {
		eventEmitter.off( event, listenerWrapper );
		listener.apply( eventEmitter, Array.prototype.slice.call( arguments, 0 ) );
	} );
};

/**
 * Remove a specific listener from a specific event.
 *
 * @method
 * @param {string} event Type of event to remove listener from
 * @param {Function} [callback] Listener to remove, omit to remove all
 * @param {Object} [context=null] Object used context for callback function or method
 * @chainable
 * @throws {Error} Listener argument is not a function
 */
oo.EventEmitter.prototype.off = function ( event, callback, context ) {
	var i, bindings;

	if ( arguments.length === 1 ) {
		// Remove all bindings for event
		if ( event in this.bindings ) {
			delete this.bindings[event];
		}
	} else {
		if ( typeof callback !== 'function' ) {
			throw new Error( 'Invalid callback. Function expected.' );
		}
		if ( !( event in this.bindings ) || !this.bindings[event].length ) {
			// No matching bindings
			return this;
		}
		// Fallback to null context
		if ( arguments.length < 3 ) {
			context = null;
		}
		// Remove matching handlers
		bindings = this.bindings[event];
		i = bindings.length;
		while ( i-- ) {
			if ( bindings[i].callback === callback && bindings[i].context === context ) {
				bindings.splice( i, 1 );
			}
		}
		// Cleanup if now empty
		if ( bindings.length === 0 ) {
			delete this.bindings[event];
		}
	}
	return this;
};

/**
 * Emit an event.
 * TODO: Should this be chainable? What is the usefulness of the boolean
 * return value here?
 *
 * @method
 * @param {string} event Type of event
 * @param {Mixed} args First in a list of variadic arguments passed to event handler (optional)
 * @returns {boolean} If event was handled by at least one listener
 */
oo.EventEmitter.prototype.emit = function ( event ) {
	var i, len, binding, bindings, args;

	if ( event in this.bindings ) {
		// Slicing ensures that we don't get tripped up by event handlers that add/remove bindings
		bindings = this.bindings[event].slice();
		args = Array.prototype.slice.call( arguments, 1 );
		for ( i = 0, len = bindings.length; i < len; i++ ) {
			binding = bindings[i];
			binding.callback.apply(
				binding.context,
				binding.args ? binding.args.concat( args ) : args
			);
		}
		return true;
	}
	return false;
};

/**
 * Connect event handlers to an object.
 *
 * @method
 * @param {Object} context Object to call methods on when events occur
 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} methods List of
 * event bindings keyed by event name containing either method names, functions or arrays containing
 * method name or function followed by a list of arguments to be passed to callback before emitted
 * arguments
 * @chainable
 */
oo.EventEmitter.prototype.connect = function ( context, methods ) {
	var method, callback, args, event;

	for ( event in methods ) {
		method = methods[event];
		// Allow providing additional args
		if ( Array.isArray( method ) ) {
			args = method.slice( 1 );
			method = method[0];
		} else {
			args = [];
		}
		// Allow callback to be a method name
		if ( typeof method === 'string' ) {
			// Validate method
			if ( !context[method] || typeof context[method] !== 'function' ) {
				throw new Error( 'Method not found: ' + method );
			}
			// Resolve to function
			callback = context[method];
		} else {
			callback = method;
		}
		// Add binding
		this.on.apply( this, [ event, callback, args, context ] );
	}
	return this;
};

/**
 * Disconnect event handlers from an object.
 *
 * @method
 * @param {Object} context Object to disconnect methods from
 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} [methods] List of
 * event bindings keyed by event name containing either method names or functions
 * @chainable
 */
oo.EventEmitter.prototype.disconnect = function ( context, methods ) {
	var i, method, callback, event, bindings;

	if ( methods ) {
		// Remove specific connections to the context
		for ( event in methods ) {
			method = methods[event];
			if ( typeof method === 'string' ) {
				// Validate method
				if ( !context[method] || typeof context[method] !== 'function' ) {
					throw new Error( 'Method not found: ' + method );
				}
				// Resolve to function
				callback = context[method];
			} else {
				callback = method;
			}
			this.off( event, callback, context );
		}
	} else {
		// Remove all connections to the context
		for ( event in this.bindings ) {
			bindings = this.bindings[event];
			i = bindings.length;
			while ( i-- ) {
				if ( bindings[i].context === context ) {
					this.off( event, bindings[i].callback, context );
				}
			}
		}
	}

	return this;
};
<|endoftext|>


<commit_before>/**
 * @module skottie-player-sk
 * @description
skottie-player-sk

 *
 *

 *   Displays a CanvasKit-based Skottie animation and provides various controls.
 *

 *
 */
import { $$ } from 'common-sk/modules/dom'
import 'elements-sk/icon/pause-icon-sk'
import 'elements-sk/icon/play-arrow-icon-sk'
import 'elements-sk/icon/settings-icon-sk'
import 'elements-sk/spinner-sk'
import { define } from 'elements-sk/define'
import { html, render } from 'lit-html'
import { repeat } from 'lit-html/directives/repeat';

const CanvasKitInit = require('../../build/canvaskit/canvaskit.js');

const loadingTemplate = (ele) => html`

Loading

  <spinner-sk active></spinner-sk>

`;

const settingsTemplate = (ele) => html`

Colors

    <select id=color-prop-select class=skottie-player-property-select
            @input=${ele._onPropertySelect} ?disabled=${ele._props.color.empty()}>
      ${repeat(ele._props.color.list, (c) => c.key, (c, index) => html`
        <option value=${index}>${c.key}</option>
      `)}
    <select>
    <input type=color class=skottie-player-picker id=color-picker
           value=${hexColor(ele._props.color.current().value)}
           @input=${ele._onColorInput} ?disabled=${ele._props.color.empty()}>

Opacity

    <select id=opacity-prop-select class=skottie-player-property-select
            @input=${ele._onPropertySelect} ?disabled=${ele._props.opacity.empty()}>
      ${repeat(ele._props.opacity.list, (o) => o.key, (o, index) => html`
        <option value=${index}>${o.key}</option>
      `)}
    <select>
    <input type=range min=0 max=100 class=skottie-player-picker id=opacity-picker
           value=${ele._props.opacity.current().value}
           @input=${ele._onOpacityInput} ?disabled=${ele._props.opacity.empty()}>

Segments

    <select id=segment-prop-select class=skottie-player-property-select
            style='width: 100%' @input=${ele._onPropertySelect}>
      ${repeat(ele._props.segments, (s) => s.name, (s, index) => html`
        <option value=${index}>${segmentLabel(s)}</option>
      `)}
    <select>

    <input type=button value=Close @click=${ele._onSettings}>

`;

function segmentLabel(s) {
  return s.name + ' [' + s.t0.toFixed(2) + ' .. ' + s.t1.toFixed(2) + ']';
}

function hexColor(c) {
  let rgb = c & 0x00ffffff;
  return '#' + rgb.toString(16).padStart(6, '0');
}

const runningTemplate = (ele) => html`

    <canvas class=skottie-canvas id=skottie
            width=${ele._config.width * window.devicePixelRatio}
            height=${ele._config.height * window.devicePixelRatio}
            style='width: ${ele._config.width}px; height: ${ele._config.height}px;'>
      Your browser does not support the canvas tag.
    </canvas>

      <play-arrow-icon-sk @click=${ele._onPlay} ?hidden=${!ele._state.paused}></play-arrow-icon-sk>
      <pause-icon-sk @click=${ele._onPause} ?hidden=${ele._state.paused}></pause-icon-sk>
      <input type=range min=0 max=100 @input=${ele._onScrub} @change=${ele._onScrubEnd}
             class=skottie-player-scrubber>
      <settings-icon-sk @click=${ele._onSettings}></settings-icon-sk>

  ${settingsTemplate(ele)}

`;

// This element might be loaded from a different site, and that means we need
// to be careful about how we construct the URL back to the canvas.wasm file.
// Start by recording the script origin.
const scriptOrigin = new URL(document.currentScript.src).origin;

const canvasReady = CanvasKitInit({
  locateFile: (file) => {
    return `${scriptOrigin}/static/${file}`;
  },
});

define('skottie-player-sk', class extends HTMLElement {
  constructor() {
    super();

    this._engine = {
      kit:       null, // CanvasKit instance
      context:   null, // CK context.
      animation: null, // Skottie Animation instance
      surface:   null, // SkSurface
      canvas:    null, // Cached SkCanvas (surface.getCanvas()).
    };

    this._state = {
      loading:        true,
      paused:         this.hasAttribute('paused'),
      scrubPlaying:   false, // Animation was playing when the user started scrubbing.
      duration:       0,     // Animation duration (ms).
      nativeFps:      0,     // Animation fps.
      timeOrigin:     0,     // Animation start time (ms).
      seekPoint:      0,     // Normalized [0..1] animation progress.
      showSettings:   (new URL(document.location)).searchParams.has('settings'),
      currentSegment: { 'name': '', 't0': 0, 't1': 1},  // One of the _props.segments
    };

    function PropList(list, defaultVal) {
      this.list       = list;
      this.defaultVal = defaultVal;
      this.index      = 0;
      this.empty      = () => { return !this.list.length; }
      this.current    = () => {
        return this.index >= this.list.length
            ? this.defaultVal
            : this.list[this.index];
      }
    };

    this._props = {
      color:    new PropList([], 0.0), // Configurable color properties
      opacity:  new PropList([], 1.0), // Configurable opacity properties
      segments: [],                    // Selectable animation segments
    };
  }

  connectedCallback() {
    this._config = {
      width:    this.hasAttribute('width')  ? this.getAttribute('width')  : 256,
      height:   this.hasAttribute('height') ? this.getAttribute('height') : 256,
      controls: (new URL(document.location)).searchParams.has('controls'),
    };
    this._render();
  }

  initialize(config) {
    this._config.width  = config.width;
    this._config.height = config.height;
    this._config.fps    = config.fps;

    this._render();
    return canvasReady.then((ck) => {
      // Set a large-ish decode cache limit to accommodate potentially large images.
      const CACHE_SIZE = 512 * 1024 * 1024;
      ck.setDecodeCacheLimitBytes(CACHE_SIZE);

      this._engine.kit = ck;
      this._initializeSkottie(config.lottie, config.assets);
      this._render();
    });
  }

  duration() {
    return this._state.duration * (this._state.currentSegment.t1 - this._state.currentSegment.t0);
  }

  seek(t) {
    this._state.timeOrigin = (Date.now() - this.duration() * t);

    if (!this.isPlaying()) {
      // Force-draw a static frame when paused.
      this._updateSeekPoint();
      this._drawFrame();
    }
  }

  isPlaying() {
    return !this._state.paused;
  }

  pause() {
    if (this.isPlaying()) {
      this._state.paused = true;
      // Save the exact/current seek point at pause time.
      this._updateSeekPoint();
    }
  }

  play() {
    if (!this.isPlaying()) {
      this._state.paused = false;
      // Shift timeOrigin to continue from where we paused.
      this.seek(this._state.seekPoint);
      this._drawFrame();
    }
  }

  _initializeSkottie(lottieJSON, assets) {
    this._state.loading = false;

    // Rebuild the surface only if needed.
    if (!this._engine.surface ||
        this._engine.surface.width  != this._config.width ||
        this._engine.surface.height != this._config.height) {

      this._render();

      this._engine.surface && this._engine.surface.delete();
      let canvasEle = $$('#skottie', this);
      this._engine.surface = this._engine.kit.MakeCanvasSurface(canvasEle);
      if (!this._engine.surface) {
        throw new Error('Could not make SkSurface.');
      }
      // We don't need to call .delete() on the canvas because
      // the parent surface will do that for us.
      this._engine.canvas = this._engine.surface.getCanvas();

      this._engine.context = this._engine.kit.currentContext();
    }

    this._engine.animation && this._engine.animation.delete();

    this._engine.animation = this._engine.kit.MakeManagedAnimation(
                                          JSON.stringify(lottieJSON), assets);
    if (!this._engine.animation) {
      throw new Error('Could not parse Lottie JSON.');
    }

    this._state.duration  = this._engine.animation.duration() * 1000;
    this._state.nativeFps = this._engine.animation.fps();
    this.seek(0);

    this._props.color.list   = this._engine.animation.getColorProps();
    this._props.opacity.list = this._engine.animation.getOpacityProps();
    this._props.segments     = [ { 'name': 'Full timeline', 't0': 0, 't1': 1 } ]
                                   .concat(this._engine.animation.getMarkers());
    this._currentSegment     = this._props.segments[0];

    this._render(); // re-render for animation-dependent elements (properties, etc).

    this._drawFrame(true);
  }

  _updateSeekPoint() {
    // t is in animation segment domain.
    let t = ((Date.now() - this._state.timeOrigin) / this.duration()) % 1;

    // map to the global animation timeline
    this._state.seekPoint = this._state.currentSegment.t0
                          + t * (this._state.currentSegment.t1 - this._state.currentSegment.t0);
    if (this._config.controls) {
      let scrubber = this.querySelector('.skottie-player-scrubber');
      if (scrubber) {
        scrubber.value = this._state.seekPoint * 100;
      }
    }
  }

  _drawFrame(firstFrame) {
    if (!this._engine.animation || !this._engine.canvas) {
      return;
    }

    // When paused, the progress is fully controlled externally.
    if (this.isPlaying()) {
      this._updateSeekPoint();
      window.requestAnimationFrame(this._drawFrame.bind(this));
    }

    let frame = this._state.seekPoint * this.duration() * this._state.nativeFps / 1000;
    if (this._config.fps) {
      // When a render FPS is specified, quantize to the desired rate.
      const fpsScale = this._config.fps / this._state.nativeFps;
      frame = Math.trunc(frame * fpsScale) / fpsScale;
    }

    this._engine.kit.setCurrentContext(this._engine.context);
    var damage = this._engine.animation.seekFrame(frame);
    // Only draw frames when the content changes.
    // TODO: SkRect::isEmpty()?
    if (firstFrame || (damage.fRight > damage.fLeft && damage.fBottom > damage.fTop)) {
      this._engine.animation.render(this._engine.canvas, {
                                    fLeft: 0,
                                    fTop:  0,
                                    fRight:  this._config.width  * window.devicePixelRatio,
                                    fBottom: this._config.height * window.devicePixelRatio });
      this._engine.surface.flush();
    }
  }

  _render() {
    render(this._state.loading
               ? loadingTemplate(this)
               : runningTemplate(this),
           this, {eventContext: this});
  }

  _onPlay() {
    this.play();
    this._render();
  }

  _onPause() {
    this.pause();
    this._render();
  }

  // This fires every time the user moves the scrub slider.
  _onScrub(e) {
    this.seek(e.currentTarget.value / 100);

    // Pause the animation while dragging the slider.
    if (this.isPlaying()) {
      this._state.scrubPlaying = true;
      this.pause();
    }
  }

  // This fires when the user releases the scrub slider.
  _onScrubEnd(e) {
    if (this._state.scrubPlaying) {
      this._state.scrubPlaying = false;
      this.play();
    }
  }

  _onSettings() {
    this._state.showSettings = !this._state.showSettings;
    this._render();
  }

  _onPropertySelect(e) {
    switch (e.target.id) {
    case 'color-prop-select':
      this._props.color.index = e.target.value;
      this.querySelector('#color-picker').value = hexColor(this._props.color.current().value);
      break;
    case 'opacity-prop-select':
      this._props.opacity.index = e.target.value;
      this.querySelector('#opacity-picker').value = this._props.opacity.current().value;
      break;
    case 'segment-prop-select':
      this._state.currentSegment = this._props.segments[e.target.value];
      this.seek(0);
      this._render();
      break;
    }
  }

  _onColorInput(e) {
    let val = e.target.value;
    let prop = this._props.color.current();
    prop.value = this._engine.kit.Color(parseInt(val.substring(1, 3), 16),
                                        parseInt(val.substring(3, 5), 16),
                                        parseInt(val.substring(5, 7), 16),
                                        1.0); // Treat colors as fully opaque.

    this._engine.animation.setColor(prop.key, prop.value);
    this._render();

    if (!this.isPlaying()) {
      this._drawFrame();
    }
  }

  _onOpacityInput(e) {
    let prop = this._props.opacity.current();
    prop.value = Number(e.target.value);

    this._engine.animation.setOpacity(prop.key, prop.value);
    this._render();

    if (!this.isPlaying()) {
      this._drawFrame();
    }
  }
});
<commit_msg>[skottie-player] Fix handling of segmented playback<commit_after>/**
 * @module skottie-player-sk
 * @description
skottie-player-sk

 *
 *

 *   Displays a CanvasKit-based Skottie animation and provides various controls.
 *

 *
 */
import { $$ } from 'common-sk/modules/dom'
import 'elements-sk/icon/pause-icon-sk'
import 'elements-sk/icon/play-arrow-icon-sk'
import 'elements-sk/icon/settings-icon-sk'
import 'elements-sk/spinner-sk'
import { define } from 'elements-sk/define'
import { html, render } from 'lit-html'
import { repeat } from 'lit-html/directives/repeat';

const CanvasKitInit = require('../../build/canvaskit/canvaskit.js');

const loadingTemplate = (ele) => html`

Loading

  <spinner-sk active></spinner-sk>

`;

const settingsTemplate = (ele) => html`

Colors

    <select id=color-prop-select class=skottie-player-property-select
            @input=${ele._onPropertySelect} ?disabled=${ele._props.color.empty()}>
      ${repeat(ele._props.color.list, (c) => c.key, (c, index) => html`
        <option value=${index}>${c.key}</option>
      `)}
    <select>
    <input type=color class=skottie-player-picker id=color-picker
           value=${hexColor(ele._props.color.current().value)}
           @input=${ele._onColorInput} ?disabled=${ele._props.color.empty()}>

Opacity

    <select id=opacity-prop-select class=skottie-player-property-select
            @input=${ele._onPropertySelect} ?disabled=${ele._props.opacity.empty()}>
      ${repeat(ele._props.opacity.list, (o) => o.key, (o, index) => html`
        <option value=${index}>${o.key}</option>
      `)}
    <select>
    <input type=range min=0 max=100 class=skottie-player-picker id=opacity-picker
           value=${ele._props.opacity.current().value}
           @input=${ele._onOpacityInput} ?disabled=${ele._props.opacity.empty()}>

Segments

    <select id=segment-prop-select class=skottie-player-property-select
            style='width: 100%' @input=${ele._onPropertySelect}>
      ${repeat(ele._props.segments, (s) => s.name, (s, index) => html`
        <option value=${index}>${segmentLabel(s)}</option>
      `)}
    <select>

    <input type=button value=Close @click=${ele._onSettings}>

`;

function segmentLabel(s) {
  return s.name + ' [' + s.t0.toFixed(2) + ' .. ' + s.t1.toFixed(2) + ']';
}

function hexColor(c) {
  let rgb = c & 0x00ffffff;
  return '#' + rgb.toString(16).padStart(6, '0');
}

const runningTemplate = (ele) => html`

    <canvas class=skottie-canvas id=skottie
            width=${ele._config.width * window.devicePixelRatio}
            height=${ele._config.height * window.devicePixelRatio}
            style='width: ${ele._config.width}px; height: ${ele._config.height}px;'>
      Your browser does not support the canvas tag.
    </canvas>

      <play-arrow-icon-sk @click=${ele._onPlay} ?hidden=${!ele._state.paused}></play-arrow-icon-sk>
      <pause-icon-sk @click=${ele._onPause} ?hidden=${ele._state.paused}></pause-icon-sk>
      <input type=range min=0 max=100 @input=${ele._onScrub} @change=${ele._onScrubEnd}
             class=skottie-player-scrubber>
      <settings-icon-sk @click=${ele._onSettings}></settings-icon-sk>

  ${settingsTemplate(ele)}

`;

// This element might be loaded from a different site, and that means we need
// to be careful about how we construct the URL back to the canvas.wasm file.
// Start by recording the script origin.
const scriptOrigin = new URL(document.currentScript.src).origin;

const canvasReady = CanvasKitInit({
  locateFile: (file) => {
    return `${scriptOrigin}/static/${file}`;
  },
});

define('skottie-player-sk', class extends HTMLElement {
  constructor() {
    super();

    this._engine = {
      kit:       null, // CanvasKit instance
      context:   null, // CK context.
      animation: null, // Skottie Animation instance
      surface:   null, // SkSurface
      canvas:    null, // Cached SkCanvas (surface.getCanvas()).
    };

    this._state = {
      loading:        true,
      paused:         this.hasAttribute('paused'),
      scrubPlaying:   false, // Animation was playing when the user started scrubbing.
      duration:       0,     // Animation duration (ms).
      nativeFps:      0,     // Animation fps.
      timeOrigin:     0,     // Animation start time (ms).
      seekPoint:      0,     // Normalized [0..1] animation progress.
      showSettings:   (new URL(document.location)).searchParams.has('settings'),
      currentSegment: { 'name': '', 't0': 0, 't1': 1},  // One of the _props.segments
    };

    function PropList(list, defaultVal) {
      this.list       = list;
      this.defaultVal = defaultVal;
      this.index      = 0;
      this.empty      = () => { return !this.list.length; }
      this.current    = () => {
        return this.index >= this.list.length
            ? this.defaultVal
            : this.list[this.index];
      }
    };

    this._props = {
      color:    new PropList([], 0.0), // Configurable color properties
      opacity:  new PropList([], 1.0), // Configurable opacity properties
      segments: [],                    // Selectable animation segments
    };
  }

  connectedCallback() {
    this._config = {
      width:    this.hasAttribute('width')  ? this.getAttribute('width')  : 256,
      height:   this.hasAttribute('height') ? this.getAttribute('height') : 256,
      controls: (new URL(document.location)).searchParams.has('controls'),
    };
    this._render();
  }

  initialize(config) {
    this._config.width  = config.width;
    this._config.height = config.height;
    this._config.fps    = config.fps;

    this._render();
    return canvasReady.then((ck) => {
      // Set a large-ish decode cache limit to accommodate potentially large images.
      const CACHE_SIZE = 512 * 1024 * 1024;
      ck.setDecodeCacheLimitBytes(CACHE_SIZE);

      this._engine.kit = ck;
      this._initializeSkottie(config.lottie, config.assets);
      this._render();
    });
  }

  duration() {
    return this._state.duration * (this._state.currentSegment.t1 - this._state.currentSegment.t0);
  }

  seek(t) {
    this._state.timeOrigin = (Date.now() - this.duration() * t);

    if (!this.isPlaying()) {
      // Force-draw a static frame when paused.
      this._updateSeekPoint();
      this._drawFrame();
    }
  }

  isPlaying() {
    return !this._state.paused;
  }

  pause() {
    if (this.isPlaying()) {
      this._state.paused = true;
      // Save the exact/current seek point at pause time.
      this._updateSeekPoint();
    }
  }

  play() {
    if (!this.isPlaying()) {
      this._state.paused = false;
      // Shift timeOrigin to continue from where we paused.
      this.seek(this._state.seekPoint);
      this._drawFrame();
    }
  }

  _initializeSkottie(lottieJSON, assets) {
    this._state.loading = false;

    // Rebuild the surface only if needed.
    if (!this._engine.surface ||
        this._engine.surface.width  != this._config.width ||
        this._engine.surface.height != this._config.height) {

      this._render();

      this._engine.surface && this._engine.surface.delete();
      let canvasEle = $$('#skottie', this);
      this._engine.surface = this._engine.kit.MakeCanvasSurface(canvasEle);
      if (!this._engine.surface) {
        throw new Error('Could not make SkSurface.');
      }
      // We don't need to call .delete() on the canvas because
      // the parent surface will do that for us.
      this._engine.canvas = this._engine.surface.getCanvas();

      this._engine.context = this._engine.kit.currentContext();
    }

    this._engine.animation && this._engine.animation.delete();

    this._engine.animation = this._engine.kit.MakeManagedAnimation(
                                          JSON.stringify(lottieJSON), assets);
    if (!this._engine.animation) {
      throw new Error('Could not parse Lottie JSON.');
    }

    this._state.duration  = this._engine.animation.duration() * 1000;
    this._state.nativeFps = this._engine.animation.fps();
    this.seek(0);

    this._props.color.list   = this._engine.animation.getColorProps();
    this._props.opacity.list = this._engine.animation.getOpacityProps();
    this._props.segments     = [ { 'name': 'Full timeline', 't0': 0, 't1': 1 } ]
                                   .concat(this._engine.animation.getMarkers());
    this._currentSegment     = this._props.segments[0];

    this._render(); // re-render for animation-dependent elements (properties, etc).

    this._drawFrame(true);
  }

  _updateSeekPoint() {
    // t is in animation segment domain.
    let t = ((Date.now() - this._state.timeOrigin) / this.duration()) % 1;

    // map to the global animation timeline
    this._state.seekPoint = this._state.currentSegment.t0
                          + t * (this._state.currentSegment.t1 - this._state.currentSegment.t0);
    if (this._config.controls) {
      let scrubber = this.querySelector('.skottie-player-scrubber');
      if (scrubber) {
        scrubber.value = this._state.seekPoint * 100;
      }
    }
  }

  _drawFrame(firstFrame) {
    if (!this._engine.animation || !this._engine.canvas) {
      return;
    }

    // When paused, the progress is fully controlled externally.
    if (this.isPlaying()) {
      this._updateSeekPoint();
      window.requestAnimationFrame(this._drawFrame.bind(this));
    }

    let frame = this._state.seekPoint * this._state.duration * this._state.nativeFps / 1000;
    if (this._config.fps) {
      // When a render FPS is specified, quantize to the desired rate.
      const fpsScale = this._config.fps / this._state.nativeFps;
      frame = Math.trunc(frame * fpsScale) / fpsScale;
    }

    this._engine.kit.setCurrentContext(this._engine.context);
    var damage = this._engine.animation.seekFrame(frame);
    // Only draw frames when the content changes.
    // TODO: SkRect::isEmpty()?
    if (firstFrame || (damage.fRight > damage.fLeft && damage.fBottom > damage.fTop)) {
      this._engine.animation.render(this._engine.canvas, {
                                    fLeft: 0,
                                    fTop:  0,
                                    fRight:  this._config.width  * window.devicePixelRatio,
                                    fBottom: this._config.height * window.devicePixelRatio });
      this._engine.surface.flush();
    }
  }

  _render() {
    render(this._state.loading
               ? loadingTemplate(this)
               : runningTemplate(this),
           this, {eventContext: this});
  }

  _onPlay() {
    this.play();
    this._render();
  }

  _onPause() {
    this.pause();
    this._render();
  }

  // This fires every time the user moves the scrub slider.
  _onScrub(e) {
    this.seek(e.currentTarget.value / 100);

    // Pause the animation while dragging the slider.
    if (this.isPlaying()) {
      this._state.scrubPlaying = true;
      this.pause();
    }
  }

  // This fires when the user releases the scrub slider.
  _onScrubEnd(e) {
    if (this._state.scrubPlaying) {
      this._state.scrubPlaying = false;
      this.play();
    }
  }

  _onSettings() {
    this._state.showSettings = !this._state.showSettings;
    this._render();
  }

  _onPropertySelect(e) {
    switch (e.target.id) {
    case 'color-prop-select':
      this._props.color.index = e.target.value;
      this.querySelector('#color-picker').value = hexColor(this._props.color.current().value);
      break;
    case 'opacity-prop-select':
      this._props.opacity.index = e.target.value;
      this.querySelector('#opacity-picker').value = this._props.opacity.current().value;
      break;
    case 'segment-prop-select':
      this._state.currentSegment = this._props.segments[e.target.value];
      this.seek(0);
      this._render();
      break;
    }
  }

  _onColorInput(e) {
    let val = e.target.value;
    let prop = this._props.color.current();
    prop.value = this._engine.kit.Color(parseInt(val.substring(1, 3), 16),
                                        parseInt(val.substring(3, 5), 16),
                                        parseInt(val.substring(5, 7), 16),
                                        1.0); // Treat colors as fully opaque.

    this._engine.animation.setColor(prop.key, prop.value);
    this._render();

    if (!this.isPlaying()) {
      this._drawFrame();
    }
  }

  _onOpacityInput(e) {
    let prop = this._props.opacity.current();
    prop.value = Number(e.target.value);

    this._engine.animation.setOpacity(prop.key, prop.value);
    this._render();

    if (!this.isPlaying()) {
      this._drawFrame();
    }
  }
});
<|endoftext|>


<commit_before>var fs = require('fs');
var os = require('os');
var path = require('path');
var should = require('should');
var actionheroPrototype = require(__dirname + "/../../actionhero.js").actionheroPrototype;
var actionhero = new actionheroPrototype();
var api;

describe('Core: Cache', function(){

  before(function(done){
    actionhero.start(function(err, a){
      api = a;
      done();
    })
  });

  after(function(done){
    actionhero.stop(function(err){
      done();
    });
  });

  it('cache methods should exist', function(done){
    api.cache.should.be.an.instanceOf(Object);
    api.cache.save.should.be.an.instanceOf(Function);
    api.cache.load.should.be.an.instanceOf(Function);
    api.cache.destroy.should.be.an.instanceOf(Function);
    done();
  });

  it('cache.save', function(done){
    api.cache.save('testKey','abc123',null,function(err, resp){
      should.not.exist(err);
      resp.should.equal(true);
      done();
    });
  });

  it('cache.load', function(done){
    api.cache.load('testKey',function(err, resp){
      resp.should.equal('abc123');
      done();
    });
  });

  it('cache.load failures', function(done){
    api.cache.load('something else',function(err, resp){
      String(err).should.equal('Error: Object not found');
      should.equal(null, resp);
      done();
    });
  });

  it('cache.destroy', function(done){
    api.cache.destroy('testKey',function(err, resp){
      resp.should.equal(true);
      done();
    });
  });

  it('cache.destroy failure', function(done){
    api.cache.destroy('testKey',function(err, resp){
      resp.should.equal(false);
      done();
    });
  });

  it('cache.save with expire time', function(done){
    api.cache.save('testKey','abc123',10,function(err, resp){
      resp.should.equal(true);
      done();
    });
  });

  it('cache.load with expired items should not return them', function(done){
    api.cache.save('testKey_slow', 'abc123', 10, function(err, save_resp){
      save_resp.should.equal(true);
      setTimeout(function(){
        api.cache.load('testKey_slow', function(err, load_resp){
          String(err).should.equal('Error: Object expired')
          should.equal(null, load_resp);
          done();
        });
      }, 20);
    });
  });

  it('cache.load with negative expire times will never load', function(done){
    api.cache.save('testKeyInThePast', 'abc123', -1, function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('testKeyInThePast', function(err, load_resp){
        (String(err).indexOf('Error: Object') >= 0).should.equal(true)
        should.equal(null, load_resp);
        done();
      });
    });
  });

  it('cache.save does not need to pass expireTime', function(done){
    api.cache.save('testKeyForNullExpireTime', 'abc123', function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('testKeyForNullExpireTime', function(err, load_resp){
        load_resp.should.equal('abc123');
        done();
      });
    });
  });

  it('cache.load without changing the expireTime will re-apply the redis expire', function(done){
    var key = 'testKey'
    api.cache.save(key, 'val', 1000, function(err, save_resp){
      api.cache.load(key, function(err, load_resp){
        load_resp.should.equal('val');
        setTimeout(function(){
          api.cache.load(key, function(err, load_resp){
            String(err).should.equal('Error: Object not found')
            should.equal(null, load_resp);
            done();
          });
        }, 1001);
      });
    });
  });

  it('cache.load with options that extending expireTime should return cached item', function(done){
    var expireTime = 400
    var timeout = 320
    //save the initial key
    api.cache.save('testKey_slow', 'abc123', expireTime, function(err, saveResp){
      saveResp.should.equal(true)
      //wait for `timeout` and try to load the key
      setTimeout(function(){
        api.cache.load('testKey_slow', {expireTimeMS: expireTime}, function(err, loadResp){
          loadResp.should.equal('abc123')
          //wait another `timeout` and load the key again within the extended expire time
          setTimeout(function(){
            api.cache.load('testKey_slow', function(err, loadResp){
              loadResp.should.equal('abc123')
              //wait another `timeout` and the key load should fail without the extension
              setTimeout(function(){
                api.cache.load('testKey_slow', function(err, loadResp){
                  String(err).should.equal('Error: Object expired')
                  should.equal(null, loadResp)
                  done()
                })
              },timeout)
            });
          },timeout)
        })
      },timeout)
    })
  })

  it('cache.save works with arrays', function(done){
    api.cache.save('array_key', [1, 2, 3], function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('array_key', function(err, load_resp){
        load_resp[0].should.equal(1);
        load_resp[1].should.equal(2);
        load_resp[2].should.equal(3);
        done();
      });
    });
  });

  it('cache.save works with objects', function(done){
    var data = {};
    data.thing = 'stuff';
    data.otherThing = [1, 2, 3];
    api.cache.save('obj_key', data, function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('obj_key', function(err, load_resp){
        load_resp.thing.should.equal('stuff');
        load_resp.otherThing[0].should.equal(1);
        load_resp.otherThing[1].should.equal(2);
        load_resp.otherThing[2].should.equal(3);
        done();
      });
    });
  });

  it('can clear the cache entirely', function(done){
    api.cache.save('thingA', 123, function(){
      api.cache.size(function(err, count){
        (count > 0).should.equal(true);
        api.cache.clear(function(){
          api.cache.size(function(err, count){
            count.should.equal(0);
            done();
          });
        });
      });
    });
  });

  describe('cache dump files', function(){

    if (typeof os.tmpdir != 'function'){ os.tmpdir = os.tmpDir } // resolution for node v0.8.x
    var file = os.tmpdir() + path.sep + "cacheDump";

    it('can read write the cache to a dump file', function(done){
      api.cache.clear(function(){
        api.cache.save('thingA', 123, function(){
          api.cache.dumpWrite(file, function(error, count){
            count.should.equal(1);
            var body = JSON.parse(String(fs.readFileSync(file)));
            var content = JSON.parse(body['actionhero:cache:thingA']);
            content.value.should.equal(123);
            done();
          });
        });
      });
    });

    it('can laod the cache from a dump file', function(done){
      api.cache.clear(function(){
        api.cache.dumpRead(file, function(error, count){
          count.should.equal(1);
          api.cache.load('thingA', function(err, value){
            value.should.equal(123);
            done();
          });
        });
      });
    });

  })

})<commit_msg>ensure that loading from api.cache will re-set the key to expire<commit_after>var fs = require('fs');
var os = require('os');
var path = require('path');
var should = require('should');
var actionheroPrototype = require(__dirname + "/../../actionhero.js").actionheroPrototype;
var actionhero = new actionheroPrototype();
var api;

describe('Core: Cache', function(){

  before(function(done){
    actionhero.start(function(err, a){
      api = a;
      done();
    })
  });

  after(function(done){
    actionhero.stop(function(err){
      done();
    });
  });

  it('cache methods should exist', function(done){
    api.cache.should.be.an.instanceOf(Object);
    api.cache.save.should.be.an.instanceOf(Function);
    api.cache.load.should.be.an.instanceOf(Function);
    api.cache.destroy.should.be.an.instanceOf(Function);
    done();
  });

  it('cache.save', function(done){
    api.cache.save('testKey','abc123',null,function(err, resp){
      should.not.exist(err);
      resp.should.equal(true);
      done();
    });
  });

  it('cache.load', function(done){
    api.cache.load('testKey',function(err, resp){
      resp.should.equal('abc123');
      done();
    });
  });

  it('cache.load failures', function(done){
    api.cache.load('something else',function(err, resp){
      String(err).should.equal('Error: Object not found');
      should.equal(null, resp);
      done();
    });
  });

  it('cache.destroy', function(done){
    api.cache.destroy('testKey',function(err, resp){
      resp.should.equal(true);
      done();
    });
  });

  it('cache.destroy failure', function(done){
    api.cache.destroy('testKey',function(err, resp){
      resp.should.equal(false);
      done();
    });
  });

  it('cache.save with expire time', function(done){
    api.cache.save('testKey','abc123',10,function(err, resp){
      resp.should.equal(true);
      done();
    });
  });

  it('cache.load with expired items should not return them', function(done){
    api.cache.save('testKey_slow', 'abc123', 10, function(err, save_resp){
      save_resp.should.equal(true);
      setTimeout(function(){
        api.cache.load('testKey_slow', function(err, load_resp){
          String(err).should.equal('Error: Object expired')
          should.equal(null, load_resp);
          done();
        });
      }, 20);
    });
  });

  it('cache.load with negative expire times will never load', function(done){
    api.cache.save('testKeyInThePast', 'abc123', -1, function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('testKeyInThePast', function(err, load_resp){
        (String(err).indexOf('Error: Object') >= 0).should.equal(true)
        should.equal(null, load_resp);
        done();
      });
    });
  });

  it('cache.save does not need to pass expireTime', function(done){
    api.cache.save('testKeyForNullExpireTime', 'abc123', function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('testKeyForNullExpireTime', function(err, load_resp){
        load_resp.should.equal('abc123');
        done();
      });
    });
  });

  it('cache.load without changing the expireTime will re-apply the redis expire', function(done){
    var key = 'testKey'
    api.cache.save(key, 'val', 1000, function(err, save_resp){
      api.cache.load(key, function(err, load_resp){
        load_resp.should.equal('val');
        setTimeout(function(){
          api.cache.load(key, function(err, load_resp){
            String(err).should.equal('Error: Object not found')
            should.equal(null, load_resp);
            done();
          });
        }, 2001);
      });
    });
  });

  it('cache.load with options that extending expireTime should return cached item', function(done){
    var expireTime = 400
    var timeout = 320
    //save the initial key
    api.cache.save('testKey_slow', 'abc123', expireTime, function(err, saveResp){
      saveResp.should.equal(true)
      //wait for `timeout` and try to load the key
      setTimeout(function(){
        api.cache.load('testKey_slow', {expireTimeMS: expireTime}, function(err, loadResp){
          loadResp.should.equal('abc123')
          //wait another `timeout` and load the key again within the extended expire time
          setTimeout(function(){
            api.cache.load('testKey_slow', function(err, loadResp){
              loadResp.should.equal('abc123')
              //wait another `timeout` and the key load should fail without the extension
              setTimeout(function(){
                api.cache.load('testKey_slow', function(err, loadResp){
                  String(err).should.equal('Error: Object expired')
                  should.equal(null, loadResp)
                  done()
                })
              },timeout)
            });
          },timeout)
        })
      },timeout)
    })
  })

  it('cache.save works with arrays', function(done){
    api.cache.save('array_key', [1, 2, 3], function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('array_key', function(err, load_resp){
        load_resp[0].should.equal(1);
        load_resp[1].should.equal(2);
        load_resp[2].should.equal(3);
        done();
      });
    });
  });

  it('cache.save works with objects', function(done){
    var data = {};
    data.thing = 'stuff';
    data.otherThing = [1, 2, 3];
    api.cache.save('obj_key', data, function(err, save_resp){
      save_resp.should.equal(true);
      api.cache.load('obj_key', function(err, load_resp){
        load_resp.thing.should.equal('stuff');
        load_resp.otherThing[0].should.equal(1);
        load_resp.otherThing[1].should.equal(2);
        load_resp.otherThing[2].should.equal(3);
        done();
      });
    });
  });

  it('can clear the cache entirely', function(done){
    api.cache.save('thingA', 123, function(){
      api.cache.size(function(err, count){
        (count > 0).should.equal(true);
        api.cache.clear(function(){
          api.cache.size(function(err, count){
            count.should.equal(0);
            done();
          });
        });
      });
    });
  });

  describe('cache dump files', function(){

    if (typeof os.tmpdir != 'function'){ os.tmpdir = os.tmpDir } // resolution for node v0.8.x
    var file = os.tmpdir() + path.sep + "cacheDump";

    it('can read write the cache to a dump file', function(done){
      api.cache.clear(function(){
        api.cache.save('thingA', 123, function(){
          api.cache.dumpWrite(file, function(error, count){
            count.should.equal(1);
            var body = JSON.parse(String(fs.readFileSync(file)));
            var content = JSON.parse(body['actionhero:cache:thingA']);
            content.value.should.equal(123);
            done();
          });
        });
      });
    });

    it('can laod the cache from a dump file', function(done){
      api.cache.clear(function(){
        api.cache.dumpRead(file, function(error, count){
          count.should.equal(1);
          api.cache.load('thingA', function(err, value){
            value.should.equal(123);
            done();
          });
        });
      });
    });

  })

})<|endoftext|>


<commit_before>/**
 * @flow
 */
import React from 'react';
import express from 'express';

import type { Router } from 'react-router';

/**
 * A React class, that can be use instead of React Router's Route class, that can
 */
const ExpressRoute = React.createClass({
  statics: {
    hasRouter: true,
    getRouter(props: {[key: string]: any}): ExpressRouter {
      if(props.use)        return props.use;
      else if(props.src)  return express.static(props.src);

      throw new Error("RouterRoute must have 'use' or 'src' prop.");
    }
  },
  propTypes: {
    path: React.PropTypes.string,
    src: React.PropTypes.string,
    use: React.PropTypes.any,
  },
  render(): React.Element {
    throw new Error('RouterRoute should never be rendered');
  }
});
export default ExpressRoute;

/**
 * Check if the given route contains an express router.
 *
 * @param route  The route to check for a router
 *
 * @return      TRUE if the given route contains a router, else FALSE
 */
export function hasExpressRouter(route: Router): bool {
  return route.type.hasRouter? true: false;
}

/**
 * Gets the express router in the given route.
 *
 * @param route  The route to get the router from
 *
 * @return      The router from the route
 */
export function getExpressRouterFrom(route: Router): ExpressRouter {
  if(!hasExpressRouter(route)) {
    throw new Error("Routes passed to 'getRouterFrom' must be an EpressRoute.");
  }

  return route.type.getRouter(route.props);
}<commit_msg>Move 'getRouter(...)' Function<commit_after>/**
 * @flow
 */
import React from 'react';
import express from 'express';

import type { Router } from 'react-router';

/**
 * A React class, that can be use instead of React Router's Route class, that can
 */
const ExpressRoute = React.createClass({
  statics: {
    hasRouter: true
  },
  propTypes: {
    path: React.PropTypes.string,
    src: React.PropTypes.string,
    use: React.PropTypes.any,
  },
  render(): React.Element {
    throw new Error('RouterRoute should never be rendered');
  }
});
export default ExpressRoute;

/**
 * Check if the given route contains an express router.
 *
 * @param route  The route to check for a router
 *
 * @return      TRUE if the given route contains a router, else FALSE
 */
export function hasExpressRouter(route: Router): bool {
  return route.type.hasRouter? true: false;
}

/**
 * Gets the express router in the given route.
 *
 * @param route  The route to get the router from
 *
 * @return      The router from the route
 */
export function getExpressRouterFrom(route: Router): ExpressRouter {
  if(!hasExpressRouter(route)) {
    throw new Error("Routes passed to 'getRouterFrom' must be an EpressRoute.");
  }

  return getRouter(route.props);
}

function getRouter(props: {[key: string]: any}): ExpressRouter {
  if(props.use)       return props.use;
  else if(props.src)  return express.static(props.src);
  else                throw new Error("RouterRoute must have 'use' or 'src' prop.");
}<|endoftext|>


<commit_before>import * as THREE from 'three';

class FollowCamera extends THREE.PerspectiveCamera {

  constructor(baseRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(-1, 0, 0), Math.PI/6),
              maxRotDeviation = 30*(Math.PI/180),
              followVel = 0.002,
              angVel = 0.002,
              baseDistance = 10,
              fov = 75,
              aspect = window.innerWidth / window.innerHeight,
              near = 0.1,
              far = 10000) {
    super(fov, aspect, near, far);

    this.targetBack = new THREE.Vector3(0, 0, 1);
    this.targetBackRot = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 0), 0);
    this.baseRotation = baseRotation;
    this.currRotation = baseRotation.clone();
    this.maxRotDeviation = maxRotDeviation;
    this.followVel = followVel;
    this.angVel = angVel;
    this.baseDistance = baseDistance;
    this.target = null;
  }

  setTarget(target) {
    this.target = target;
    let targetPos = this.target.position;
    let posVec = this.targetBack.clone().applyQuaternion(this.baseRotation).setLength(this.baseDistance);
    this.position.set(targetPos.x + posVec.x, targetPos.y + posVec.y, targetPos.z + posVec.z);
    this.lookAt(targetPos);
  }

  update(delta, heading) {
    let targetPos = this.target.position;
    let posVec = this.calcPosVec(delta, heading);

    this.position.set(targetPos.x + posVec.x, targetPos.y + posVec.y, targetPos.z + posVec.z);
    this.lookAt(targetPos);
  }

  calcPosVec(delta, heading) {

    let rotation = this.calcRotation(delta, heading);
    let backVec = this.calcBackVec(delta);

    return backVec.setLength(this.baseDistance).applyQuaternion(rotation);
  }

  calcRotation(delta, heading) {
    let goalQuat;
    if (heading.length() != 0) {
      let axisAngleHeading = heading.set(heading.y, heading.x, 0);
      let maxRotDevQuat = new THREE.Quaternion().setFromAxisAngle(heading, this.maxRotDeviation);
      goalQuat = this.baseRotation.clone().multiply(maxRotDevQuat);
    } else {
      goalQuat = this.baseRotation.clone();
    }

    let rotDiff = this.quaternionAngleDiff(this.currRotation, goalQuat);
    let t = Math.min(1, delta*(this.angVel/rotDiff));
    return this.currRotation.slerp(goalQuat, t).clone();
  }

  quaternionAngleDiff(q1, q2) {
    return Math.acos(q1.dot(q2))/(q1.length()*q2.length());
  }

  calcBackVec(delta) {
    let backPos = this.targetBack;
    let targetPos = this.target.position;
    let posDiff = new THREE.Vector3(backPos.x - targetPos.x, backPos.y - targetPos.y, backPos.z - targetPos.z);

    if (posDiff.length() != 0 && !isNaN(delta)) {
      posDiff.normalize();
      let angle = backPos.angleTo(posDiff);
      let t = Math.min(1, delta*(this.followVel/angle));
      this.targetBack.lerp(posDiff, t);
    }

    return this.targetBack.clone();
  }

}

export default FollowCamera;
<commit_msg>got the follow camera working like a rope, but the ball itself starts to spasm in some odd situations<commit_after>import * as THREE from 'three';

class FollowCamera extends THREE.PerspectiveCamera {

  constructor(baseRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(-1, 0, 0), Math.PI/24),
              maxRotDeviation = 30*(Math.PI/180),
              followVel = 0.02,
              angVel = 0.002,
              baseDistance = 10,
              fov = 75,
              aspect = window.innerWidth / window.innerHeight,
              near = 0.1,
              far = 10000) {
    super(fov, aspect, near, far);

    this.truePos = null;
    this.baseRotation = baseRotation;
    this.currRotation = baseRotation.clone();
    this.maxRotDeviation = maxRotDeviation;
    this.followVel = followVel;
    this.angVel = angVel;
    this.baseDistance = baseDistance;
    this.target = null;
  }

  setTarget(target) {
    this.target = target;
    let targetPos = this.target.position;
    this.truePos = new THREE.Vector3(targetPos.x, targetPos.y, targetPos.z + this.baseDistance);
    let posVec = this.getRelativePos().setLength(this.baseDistance).applyQuaternion(this.baseRotation);
    this.position.set(targetPos.x + posVec.x, targetPos.y + posVec.y, targetPos.z + posVec.z);
    this.lookAt(targetPos);
  }

  update(delta, heading) {
    let targetPos = this.target.position;
    let posVec = this.calcPosVec(delta, heading);

    this.position.set(targetPos.x + posVec.x, targetPos.y + posVec.y, targetPos.z + posVec.z);
    this.lookAt(targetPos);
  }

  calcPosVec(delta, heading) {

    let rotation = this.calcRotation(delta, heading);
    let posVec = this.calcBackVec(delta);

    return posVec.applyQuaternion(rotation);
  }

  calcRotation(delta, heading) {
    let goalQuat;
    if (heading.length() != 0) {
      let axisAngleHeading = heading.set(heading.y, heading.x, 0);
      let maxRotDevQuat = new THREE.Quaternion().setFromAxisAngle(heading, this.maxRotDeviation);
      goalQuat = this.baseRotation.clone().multiply(maxRotDevQuat);
    } else {
      goalQuat = this.baseRotation.clone();
    }

    let rotDiff = this.quaternionAngleDiff(this.currRotation, goalQuat);
    let t = Math.min(1, delta*(this.angVel/rotDiff));
    return this.currRotation.slerp(goalQuat, t).clone();
  }

  quaternionAngleDiff(q1, q2) {
    return Math.acos(q1.dot(q2))/(q1.length()*q2.length());
  }

  calcBackVec(delta) {
    let thispos = this.truePos.clone();
    let targetPos = this.target.position.clone();
    let posDiff = new THREE.Vector3(targetPos.x - thispos.x, targetPos.y - thispos.y, targetPos.z - thispos.z);

    /*
    if (Math.abs(Math.atan(posDiff.y/posDiff.x)) < Math.PI/4) {
        thispos.y = 0;
    }
    */

    if (posDiff.length() > this.baseDistance && !isNaN(delta)) {
        posDiff.multiplyScalar(1 - this.baseDistance/posDiff.length());
        this.truePos.lerp(thispos.add(posDiff), delta*this.followVel);
    }

    return this.getRelativePos();
  }

  getRelativePos() {
    let thisPos = this.truePos;
    let targetPos = this.target.position;
    return new THREE.Vector3(thisPos.x - targetPos.x, thisPos.y - targetPos.y, thisPos.z - targetPos.z);
  }

}

export default FollowCamera;
<|endoftext|>


<commit_before>/*!
 *
 * # BiB/i Extension: Unzipper (core)
 *
 * - "EPUB-Zip Utility for BiB/i (core)"
 * - Copyright (c) Satoru MATSUSHIMA - http://bibi.epub.link/ or https://github.com/satorumurmur/bibi
 * - Licensed under the MIT license. - http://www.opensource.org/licenses/mit-license.php
 */

// requires: JSZip & base64.js

Bibi.x({

    name: "Unzipper",
    description: "EPUB-Zip Unarchiver for BiB/i",
    author: "Satoru MATSUSHIMA (@satorumurmur)",
    version: Bibi["version"],
    build: Bibi["build"]

})(function() {

    if(!window.File) return;

    O.ZippedEPUBAvailable = true;

    var CatcherLabel = "Drop me an EPUB file or click me...";

    C.Veil.Catcher = C.Veil.appendChild(
        sML.create("p", { id: "bibi-veil-catcher", title: CatcherLabel, innerHTML: '' + CatcherLabel + '' }, { display: "none" })
    );

    C.Veil.Catcher.addEventListener("click", function() {
        if(!this.Input) this.Input = this.appendChild(
            sML.create("input", { type: "file",
                onchange: function(Eve) {
                    B.initialize(Eve.target.files[0]);
                }
            })
        );
        this.Input.click();
    });

    if(!O.Mobile) {
        document.body.addEventListener("dragenter", function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 0.9; sML.addClass(O.HTML, "dragenter"); }, 1);
        document.body.addEventListener("dragover",  function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 0.9; }, 1);
        document.body.addEventListener("dragleave", function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 1.0; sML.removeClass(O.HTML, "dragenter"); }, 1);
        document.body.addEventListener("drop",      function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 1.0;
            L.loadBook(Eve.dataTransfer.files[0]);
        }, 1);
    }

});

B.dropOrClick = function() {
    C.Veil.Catcher.style.display = "block";
    C.note('Drop an EPUB file into this window. Or click and select EPUB file.');
};

B.loadZippedEPUB = function() {
    if(B.Online) {
        O.download(B.Path, "text/plain;charset=x-user-defined").then(function(XHR) {
            B.unzip(XHR.responseText);
            B.initialize.resolve();
        }).catch(function() {
            B.loadMimetype().then(function() {
                B.initialize.resolve();
            }).catch(function() {
                B.initialize.reject('EPUB Not Found.');
            });
        });
    } else {
        sML.edit(new FileReader(), {
            onload  : function() {
                B.unzip(this.result);
                B.initialize.resolve();
            },
            onerror : function() {
                O.Body.style.opacity = 1;
                B.initialize.reject('Error. Something trouble...');
            }
        }).readAsArrayBuffer(B.File);
        C.Veil.Catcher.style.opacity = 0;
    }
};

B.unzip = function(FileText) {

    O.log("Unzipping EPUB: " + B.Path + " ...", "*:");

    B.Files = {};
    var FileCount = { All: 0, HTMLs: 0, CSSs: 0, SVGs: 0, Images: 0, Fonts: 0, Audios: 0, Videos: 0, PDFs: 0, Etc: 0 };

    B.File = (new JSZip()).load(FileText);

    for(var FileName in B.File.files) {
        if(B.File.files[FileName]._data) {
            FileCount.All++;
                 if(         /\.(x?html?)$/i.test(FileName)) FileCount.HTMLs++;
            else if(             /\.(css)$/i.test(FileName)) FileCount.CSSs++;
            else if(             /\.(svg)$/i.test(FileName)) FileCount.SVGs++;
            else if(   /\.(gif|jpe?g|png)$/i.test(FileName)) FileCount.Images++;
            else if(    /\.(woff|otf|ttf)$/i.test(FileName)) FileCount.Fonts++;
            else if( /\.(m4a|aac|mp3|ogg)$/i.test(FileName)) FileCount.Audios++;
            else if(/\.(mp4|m4v|ogv|webm)$/i.test(FileName)) FileCount.Videos++;
            else if(             /\.(pdf)$/i.test(FileName)) FileCount.PDFs++;
            else                                             FileCount.Etc++;
            B.Files[FileName] = B.isBin(FileName) ? B.File.file(FileName).asBinary() : Base64.btou(B.File.file(FileName).asText());
        }
    }
    delete B.File;

    B.FileDigit = (FileCount.All + "").length;

    if(FileCount.All) {
        var FileNumbers =                              sML.String.pad(FileCount.All,    0, B.FileDigit) + ' File'  + (FileCount.All    >= 2 ? 's' : '');
        var FileNumbersBreakdown = [];
        if(FileCount.HTMLs)  FileNumbersBreakdown.push(sML.String.pad(FileCount.HTMLs,  0, B.FileDigit) + ' HTML'  + (FileCount.HTMLs  >= 2 ? 's' : ''));
        if(FileCount.CSSs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.CSSs,   0, B.FileDigit) + ' CSS'   + (FileCount.CSSs   >= 2 ? 's' : ''));
        if(FileCount.SVGs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.SVGs,   0, B.FileDigit) + ' SVG'   + (FileCount.SVGs   >= 2 ? 's' : ''));
        if(FileCount.Images) FileNumbersBreakdown.push(sML.String.pad(FileCount.Images, 0, B.FileDigit) + ' Image' + (FileCount.Images >= 2 ? 's' : ''));
        if(FileCount.Fonts)  FileNumbersBreakdown.push(sML.String.pad(FileCount.Fonts,  0, B.FileDigit) + ' Font'  + (FileCount.Fonts  >= 2 ? 's' : ''));
        if(FileCount.Audios) FileNumbersBreakdown.push(sML.String.pad(FileCount.Audios, 0, B.FileDigit) + ' Audio' + (FileCount.Audios >= 2 ? 's' : ''));
        if(FileCount.Videos) FileNumbersBreakdown.push(sML.String.pad(FileCount.Videos, 0, B.FileDigit) + ' Video' + (FileCount.Videos >= 2 ? 's' : ''));
        if(FileCount.PDFs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.PDFs,   0, B.FileDigit) + ' PDF'   + (FileCount.PDFs   >= 2 ? 's' : ''));
        if(FileCount.Etc)    FileNumbersBreakdown.push(sML.String.pad(FileCount.Etc,    0, B.FileDigit) + ' etc.');
        if(FileNumbersBreakdown.length) FileNumbers += ' (' + FileNumbersBreakdown.join(', ') + ')';
        O.log(FileNumbers, "-*");
    }

    var Preprocessed = { CSS: 0, SVG: 0, HTML: 0 };

    // CSS
    for(var FilePath in B.Files) {
        if(!/\.css$/.test(FilePath)) continue;
        B.Files[FilePath] = (function(FilePath) { var getImportedCSS = arguments.callee;
            if(!B.Files[FilePath]) return "";
            var RE = /@import[ \t]*(?:url\()?["']?(?!(?:https?|data):)(.+?)['"]?(?:\))?[ \t]*;/g;
            var Imports = B.Files[FilePath].match(RE);
            if(Imports) {
                sML.each(Imports, function() {
                    var ImportPath = O.getPath(FilePath.replace(/[^\/]+$/, ""), this.replace(RE, "$1"));
                    if(B.Files[ImportPath]) B.Files[FilePath] = B.Files[FilePath].replace(this, getImportedCSS(ImportPath));
                });
            }
            B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
                "url" : "gif|png|jpe?g|svg|ttf|otf|woff"
            }, function() {
                return /url\(["']?(?!(?:https?|data):)(.+?)['"]?\)/g;
            });
            return B.Files[FilePath];
        })(FilePath);
        Preprocessed.CSS++;
    }
    if(Preprocessed.CSS) O.log(sML.String.pad(Preprocessed.CSS, 0, B.FileDigit) + ' CSS' + (Preprocessed.CSS >= 2 ? "s" : "") + " Preprocessed.", "-*");

    // SVG
    for(var FilePath in B.Files) {
        if(!/\.svg$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "href"       : "css",
            "src"        : "gif|png|jpe?g|svg|js",
            "xlink:href" : "gif|png|jpe?g"
        });
        Preprocessed.SVG++;
    }
    if(Preprocessed.SVG) O.log(sML.String.pad(Preprocessed.SVG, 0, B.FileDigit) + ' SVG' + (Preprocessed.SVG >= 2 ? "s" : "") + " Preprocessed.", "-*");

    // HTML
    for(var FilePath in B.Files) {
        if(!/\.x?html?$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "href"       : "css",
            "src"        : "gif|png|jpe?g|svg|js|mp[34]|m4[av]|webm",
            "xlink:href" : "gif|png|jpe?g"
        });
    }
    for(var FilePath in B.Files) {
        if(!/\.x?html?$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "src" : "x?html?"
        });
        Preprocessed.HTML++;
    }
    if(Preprocessed.HTML) O.log(sML.String.pad(Preprocessed.HTML, 0, B.FileDigit) + ' HTML' + (Preprocessed.HTML >= 2 ? "s" : "") + " Preprocessed.", "-*");

    O.log("EPUB Unzipped.", "/*");

};

B.getDataURI = function(FilePath) {
    for(var ContentType in B.getDataURI.ContentTypeList) {
        if(B.getDataURI.ContentTypeList[ContentType].test(FilePath)) {
            return "data:" + ContentType + ";base64," + (B.isBin(FilePath) ? btoa(B.Files[FilePath]) : Base64.encode(B.Files[FilePath]));
        }
    }
    return "";
};

B.getDataURI.ContentTypeList = {
    "image/gif"             :   /\.gif$/i,
    "image/png"             :   /\.png$/i,
    "image/jpeg"            : /\.jpe?g$/i,
    "image/svg+xml"         :   /\.svg$/i,
    "video/mp4"             :   /\.mp4$/i,
    "video/webm"            :  /\.webm$/i,
    "audio/mpeg"            :   /\.mp3$/i,
    "audio/mp4"             :   /\.mp4$/i,
    "font/truetype"         :   /\.ttf$/i,
    "font/opentype"         :   /\.otf$/i,
    "font/woff"             :  /\.woff$/i,
    "text/css"              :   /\.css$/i,
    "text/javascript"       :    /\.js$/i,
    "text/html"             : /\.html?$/i,
    "application/xhtml+xml" : /\.xhtml$/i,
    "application/xml"       :   /\.xml$/i,
    "application/pdf"       :   /\.pdf$/i
};

B.isBin = function(T) {
    return /\.(gif|jpe?g|png|ttf|otf|woff|mp[g34]|m4[av]|ogg|webm|pdf)$/i.test(T);
};

B.replaceResourceRefferences = function(FilePath, ExtLists, getMatchRE) {
    if(typeof getMatchRE != "function") getMatchRE = function(At) { return (new RegExp('<\\??[a-zA-Z1-6:\-]+[^>]*? ' + At + '[ \t]*=[ \t]*[\'"](?!(?:https?|data):)([^"]+?)[\'"]', "g")); };
    var Source = B.Files[FilePath].replace(/[\r\n]/g, "\n").replace(/\r/g, "\n");
    var FileDir = FilePath.replace(/\/?[^\/]+$/, "");
    for(var Attribute in ExtLists) {
        var MatchRE = getMatchRE(Attribute);
        var Matches = Source.match(MatchRE);
        if(Matches) {
            var ExtRE = new RegExp('\.' + ExtLists[Attribute] + '$', "i");
            sML.each(Matches, function() {
                var ResPathInSource = this.replace(MatchRE, "$1");
                var ResPath = O.getPath(FileDir, (!/^(\.*\/+|#)/.test(ResPathInSource) ? "./" : "") + ResPathInSource);
                var ResFnH = ResPath.split("#"), ResFile = ResFnH[0] ? ResFnH[0] : FilePath, ResHash = ResFnH[1] ? ResFnH[1] : "";
                if(ExtRE.test(ResFile) && typeof B.Files[ResFile] == "string") Source = Source.replace(this, this.replace(ResPathInSource, B.getDataURI(ResFile) + (ResHash ? "#" + ResHash : "")));
            });
        }
    }
    /*
    if(Shelter.length) for(var i = 0, L = Shelter.length; i < L; i++) Source = Source.replace('<bibi:ignored number="' + i + '" />', Shelter[i]);
    Source = Source.replace(/<bibi:lf \/>/g, "\n");
    */
    return Source;
};<commit_msg>Optimize unzipping books containing invisible/unnessesary files or directories such as ".svn".<commit_after>/*!
 *
 * # BiB/i Extension: Unzipper (core)
 *
 * - "EPUB-Zip Utility for BiB/i (core)"
 * - Copyright (c) Satoru MATSUSHIMA - http://bibi.epub.link/ or https://github.com/satorumurmur/bibi
 * - Licensed under the MIT license. - http://www.opensource.org/licenses/mit-license.php
 */

// requires: JSZip & base64.js

Bibi.x({

    name: "Unzipper",
    description: "EPUB-Zip Unarchiver for BiB/i",
    author: "Satoru MATSUSHIMA (@satorumurmur)",
    version: Bibi["version"],
    build: Bibi["build"]

})(function() {

    if(!window.File) return;

    O.ZippedEPUBAvailable = true;

    var CatcherLabel = "Drop me an EPUB file or click me...";

    C.Veil.Catcher = C.Veil.appendChild(
        sML.create("p", { id: "bibi-veil-catcher", title: CatcherLabel, innerHTML: '' + CatcherLabel + '' }, { display: "none" })
    );

    C.Veil.Catcher.addEventListener("click", function() {
        if(!this.Input) this.Input = this.appendChild(
            sML.create("input", { type: "file",
                onchange: function(Eve) {
                    L.loadBook(Eve.target.files[0]);
                }
            })
        );
        this.Input.click();
    });

    if(!O.Mobile) {
        document.body.addEventListener("dragenter", function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 0.9; sML.addClass(O.HTML, "dragenter"); }, 1);
        document.body.addEventListener("dragover",  function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 0.9; }, 1);
        document.body.addEventListener("dragleave", function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 1.0; sML.removeClass(O.HTML, "dragenter"); }, 1);
        document.body.addEventListener("drop",      function(Eve) { Eve.preventDefault(); O.Body.style.opacity = 1.0;
            L.loadBook(Eve.dataTransfer.files[0]);
        }, 1);
    }

});

B.dropOrClick = function() {
    C.Veil.Catcher.style.display = "block";
    C.note('Drop an EPUB file into this window. Or click and select EPUB file.');
};

B.loadZippedEPUB = function() {
    if(B.Online) {
        O.download(B.Path, "text/plain;charset=x-user-defined").then(function(XHR) {
            B.unzip(XHR.responseText);
            B.initialize.resolve();
        }).catch(function() {
            B.loadMimetype().then(function() {
                B.initialize.resolve();
            }).catch(function() {
                B.initialize.reject('EPUB Not Found.');
            });
        });
    } else {
        sML.edit(new FileReader(), {
            onload  : function() {
                B.unzip(this.result);
                B.initialize.resolve();
            },
            onerror : function() {
                O.Body.style.opacity = 1;
                B.initialize.reject('Error. Something trouble...');
            }
        }).readAsArrayBuffer(B.File);
        C.Veil.Catcher.style.opacity = 0;
    }
};

B.unzip = function(FileText) {

    O.log("Unzipping EPUB: " + B.Path + " ...", "*:");

    B.Files = {};
    var FileCount = { All: 0, HTMLs: 0, CSSs: 0, SVGs: 0, Images: 0, Fonts: 0, Audios: 0, Videos: 0, PDFs: 0, Etc: 0 };

    B.File = (new JSZip()).load(FileText);

    for(var FileName in B.File.files) {
        if(!B.isMustToBeIgnore(FileName) && B.File.files[FileName]._data) {
            FileCount.All++;
                 if(         /\.(x?html?)$/i.test(FileName)) FileCount.HTMLs++;
            else if(             /\.(css)$/i.test(FileName)) FileCount.CSSs++;
            else if(             /\.(svg)$/i.test(FileName)) FileCount.SVGs++;
            else if(   /\.(gif|jpe?g|png)$/i.test(FileName)) FileCount.Images++;
            else if(    /\.(woff|otf|ttf)$/i.test(FileName)) FileCount.Fonts++;
            else if( /\.(m4a|aac|mp3|ogg)$/i.test(FileName)) FileCount.Audios++;
            else if(/\.(mp4|m4v|ogv|webm)$/i.test(FileName)) FileCount.Videos++;
            else if(             /\.(pdf)$/i.test(FileName)) FileCount.PDFs++;
            else                                             FileCount.Etc++;
            B.Files[FileName] = B.isBin(FileName) ? B.File.file(FileName).asBinary() : Base64.btou(B.File.file(FileName).asText());
        }
    }
    delete B.File;

    B.FileDigit = (FileCount.All + "").length;

    if(FileCount.All) {
        var FileNumbers =                              sML.String.pad(FileCount.All,    0, B.FileDigit) + ' File'  + (FileCount.All    >= 2 ? 's' : '');
        var FileNumbersBreakdown = [];
        if(FileCount.HTMLs)  FileNumbersBreakdown.push(sML.String.pad(FileCount.HTMLs,  0, B.FileDigit) + ' HTML'  + (FileCount.HTMLs  >= 2 ? 's' : ''));
        if(FileCount.CSSs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.CSSs,   0, B.FileDigit) + ' CSS'   + (FileCount.CSSs   >= 2 ? 's' : ''));
        if(FileCount.SVGs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.SVGs,   0, B.FileDigit) + ' SVG'   + (FileCount.SVGs   >= 2 ? 's' : ''));
        if(FileCount.Images) FileNumbersBreakdown.push(sML.String.pad(FileCount.Images, 0, B.FileDigit) + ' Image' + (FileCount.Images >= 2 ? 's' : ''));
        if(FileCount.Fonts)  FileNumbersBreakdown.push(sML.String.pad(FileCount.Fonts,  0, B.FileDigit) + ' Font'  + (FileCount.Fonts  >= 2 ? 's' : ''));
        if(FileCount.Audios) FileNumbersBreakdown.push(sML.String.pad(FileCount.Audios, 0, B.FileDigit) + ' Audio' + (FileCount.Audios >= 2 ? 's' : ''));
        if(FileCount.Videos) FileNumbersBreakdown.push(sML.String.pad(FileCount.Videos, 0, B.FileDigit) + ' Video' + (FileCount.Videos >= 2 ? 's' : ''));
        if(FileCount.PDFs)   FileNumbersBreakdown.push(sML.String.pad(FileCount.PDFs,   0, B.FileDigit) + ' PDF'   + (FileCount.PDFs   >= 2 ? 's' : ''));
        if(FileCount.Etc)    FileNumbersBreakdown.push(sML.String.pad(FileCount.Etc,    0, B.FileDigit) + ' etc.');
        if(FileNumbersBreakdown.length) FileNumbers += ' (' + FileNumbersBreakdown.join(', ') + ')';
        O.log(FileNumbers, "-*");
    }

    var Preprocessed = { CSS: 0, SVG: 0, HTML: 0 };

    // CSS
    for(var FilePath in B.Files) {
        if(!/\.css$/.test(FilePath)) continue;
        B.Files[FilePath] = (function(FilePath) { var getImportedCSS = arguments.callee;
            if(!B.Files[FilePath]) return "";
            var RE = /@import[ \t]*(?:url\()?["']?(?!(?:https?|data):)(.+?)['"]?(?:\))?[ \t]*;/g;
            var Imports = B.Files[FilePath].match(RE);
            if(Imports) {
                sML.each(Imports, function() {
                    var ImportPath = O.getPath(FilePath.replace(/[^\/]+$/, ""), this.replace(RE, "$1"));
                    if(B.Files[ImportPath]) B.Files[FilePath] = B.Files[FilePath].replace(this, getImportedCSS(ImportPath));
                });
            }
            B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
                "url" : "gif|png|jpe?g|svg|ttf|otf|woff"
            }, function() {
                return /url\(["']?(?!(?:https?|data):)(.+?)['"]?\)/g;
            });
            return B.Files[FilePath];
        })(FilePath);
        Preprocessed.CSS++;
    }
    if(Preprocessed.CSS) O.log(sML.String.pad(Preprocessed.CSS, 0, B.FileDigit) + ' CSS' + (Preprocessed.CSS >= 2 ? "s" : "") + " Preprocessed.", "-*");

    // SVG
    for(var FilePath in B.Files) {
        if(!/\.svg$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "href"       : "css",
            "src"        : "gif|png|jpe?g|svg|js",
            "xlink:href" : "gif|png|jpe?g"
        });
        Preprocessed.SVG++;
    }
    if(Preprocessed.SVG) O.log(sML.String.pad(Preprocessed.SVG, 0, B.FileDigit) + ' SVG' + (Preprocessed.SVG >= 2 ? "s" : "") + " Preprocessed.", "-*");

    // HTML
    for(var FilePath in B.Files) {
        if(!/\.x?html?$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "href"       : "css",
            "src"        : "gif|png|jpe?g|svg|js|mp[34]|m4[av]|webm",
            "xlink:href" : "gif|png|jpe?g"
        });
    }
    for(var FilePath in B.Files) {
        if(!/\.x?html?$/.test(FilePath)) continue;
        B.Files[FilePath] = B.replaceResourceRefferences(FilePath, {
            "src" : "x?html?"
        });
        Preprocessed.HTML++;
    }
    if(Preprocessed.HTML) O.log(sML.String.pad(Preprocessed.HTML, 0, B.FileDigit) + ' HTML' + (Preprocessed.HTML >= 2 ? "s" : "") + " Preprocessed.", "-*");

    O.log("EPUB Unzipped.", "/*");

};

B.getDataURI = function(FilePath) {
    for(var ContentType in B.getDataURI.ContentTypeList) {
        if(B.getDataURI.ContentTypeList[ContentType].test(FilePath)) {
            return "data:" + ContentType + ";base64," + (B.isBin(FilePath) ? btoa(B.Files[FilePath]) : Base64.encode(B.Files[FilePath]));
        }
    }
    return "";
};

B.getDataURI.ContentTypeList = {
    "image/gif"             :   /\.gif$/i,
    "image/png"             :   /\.png$/i,
    "image/jpeg"            : /\.jpe?g$/i,
    "image/svg+xml"         :   /\.svg$/i,
    "video/mp4"             :   /\.mp4$/i,
    "video/webm"            :  /\.webm$/i,
    "audio/mpeg"            :   /\.mp3$/i,
    "audio/mp4"             :   /\.mp4$/i,
    "font/truetype"         :   /\.ttf$/i,
    "font/opentype"         :   /\.otf$/i,
    "font/woff"             :  /\.woff$/i,
    "text/css"              :   /\.css$/i,
    "text/javascript"       :    /\.js$/i,
    "text/html"             : /\.html?$/i,
    "application/xhtml+xml" : /\.xhtml$/i,
    "application/xml"       :   /\.xml$/i,
    "application/pdf"       :   /\.pdf$/i
};

B.isMustToBeIgnore = function(FileName) {
    if(/\.(git|svn|bundle|sass-cache)\//.test(FileName)) return true;
    if(/(\.(DS_Store|AppleDouble|LSOverride|Spotlight-V100|Trashes|gitignore)|Thumbs\.db|Gemfile(\.lock)?|config\.(rb|ru))$/.test(FileName)) return true;
    return false;
};

B.isBin = function(FileName) {
    if(/\.(gif|jpe?g|png|ttf|otf|woff|mp[g34]|m4[av]|ogg|webm|pdf)$/i.test(FileName)) return true;
    return false;
};

B.replaceResourceRefferences = function(FilePath, ExtLists, getMatchRE) {
    if(typeof getMatchRE != "function") getMatchRE = function(At) { return (new RegExp('<\\??[a-zA-Z1-6:\-]+[^>]*? ' + At + '[ \t]*=[ \t]*[\'"](?!(?:https?|data):)([^"]+?)[\'"]', "g")); };
    var Source = B.Files[FilePath].replace(/[\r\n]/g, "\n").replace(/\r/g, "\n");
    var FileDir = FilePath.replace(/\/?[^\/]+$/, "");
    for(var Attribute in ExtLists) {
        var MatchRE = getMatchRE(Attribute);
        var Matches = Source.match(MatchRE);
        if(Matches) {
            var ExtRE = new RegExp('\.' + ExtLists[Attribute] + '$', "i");
            sML.each(Matches, function() {
                var ResPathInSource = this.replace(MatchRE, "$1");
                var ResPath = O.getPath(FileDir, (!/^(\.*\/+|#)/.test(ResPathInSource) ? "./" : "") + ResPathInSource);
                var ResFnH = ResPath.split("#"), ResFile = ResFnH[0] ? ResFnH[0] : FilePath, ResHash = ResFnH[1] ? ResFnH[1] : "";
                if(ExtRE.test(ResFile) && typeof B.Files[ResFile] == "string") Source = Source.replace(this, this.replace(ResPathInSource, B.getDataURI(ResFile) + (ResHash ? "#" + ResHash : "")));
            });
        }
    }
    /*
    if(Shelter.length) for(var i = 0, L = Shelter.length; i < L; i++) Source = Source.replace('<bibi:ignored number="' + i + '" />', Shelter[i]);
    Source = Source.replace(/<bibi:lf \/>/g, "\n");
    */
    return Source;
};<|endoftext|>


<commit_before>import seek from 'dom-seek'

function getFirstTextNode(node) {
  if (node.nodeType === Node.TEXT_NODE) return node;
  let walker = global.document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
  return walker.firstChild();
}

export default class TextPositionAnchor {
  constructor(root, start, end) {
    if (root === undefined) {
      throw new Error('missing required parameter "root"');
    }
    if (start === undefined) {
      throw new Error('missing required parameter "start"');
    }
    if (end === undefined) {
      throw new Error('missing required parameter "end"');
    }
    this.root = root;
    this.start = start;
    this.end = end;
  }

  static fromRange(root, range) {
    if (root === undefined) {
      throw new Error('missing required parameter "root"');
    }
    if (range === undefined) {
      throw new Error('missing required parameter "range"');
    }

    let startNode = range.startContainer;
    let startOffset = range.startOffset;

    if (startNode.nodeType !== Node.TEXT_NODE) {
      if (startOffset === startNode.childNodes.length) {
        startNode = startNode.childNodes[startOffset - 1];
        startNode = getFirstTextNode(startNode);
        startOffset = startNode.textContent.length;
      } else {
        startNode = startNode.childNodes[startOffset];
        startNode = getFirstTextNode(startNode);
        startOffset = 0;
      }
    }

    let endNode = range.endContainer;
    let endOffset = range.endOffset;

    if (endNode.nodeType !== Node.TEXT_NODE) {
      if (endOffset === endNode.childNodes.length) {
        endNode = endNode.childNodes[endOffset - 1];
        endNode = getFirstTextNode(endNode);
        endOffset = endNode.textContent.length;
      } else {
        endNode = endNode.childNodes[endOffset];
        endNode = getFirstTextNode(endNode);
        endOffset = 0;
      }
    }

    let iter = global.document.createNodeIterator(root, NodeFilter.SHOW_TEXT);

    let start = seek(iter, startNode);
    let end = start + seek(iter, endNode);

    return new TextPositionAnchor(root, start + startOffset, end + endOffset);
  }

  static fromSelector(root, selector = {}) {
    return new TextPositionAnchor(root, selector.start, selector.end);
  }

  toRange() {
    let root = this.root;
    let range = global.document.createRange();
    let iter = global.document.createNodeIterator(root, NodeFilter.SHOW_TEXT);

    let {start, end} = this;
    let count = seek(iter, start);
    let remainder = start - count;

    if (iter.pointerBeforeReferenceNode) {
      range.setStart(iter.referenceNode, remainder);
    } else {
      range.setStart(iter.nextNode(), remainder);
      iter.previousNode();
    }

    let length = (end - start) + remainder;

    count = seek(iter, length);
    remainder = length - count;

    if (iter.pointerBeforeReferenceNode) {
      range.setEnd(iter.referenceNode, remainder);
    } else {
      range.setEnd(iter.nextNode(), remainder);
    }

    return range;
  }

  toSelector() {
    return {
      type: 'TextPositionSelector',
      start: this.start,
      end: this.end,
    };
  }
}
<commit_msg>Some helpful comments<commit_after>import seek from 'dom-seek'

function getFirstTextNode(node) {
  if (node.nodeType === Node.TEXT_NODE) return node;
  let walker = global.document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
  return walker.firstChild();
}

export default class TextPositionAnchor {
  constructor(root, start, end) {
    if (root === undefined) {
      throw new Error('missing required parameter "root"');
    }
    if (start === undefined) {
      throw new Error('missing required parameter "start"');
    }
    if (end === undefined) {
      throw new Error('missing required parameter "end"');
    }
    this.root = root;
    this.start = start;
    this.end = end;
  }

  static fromRange(root, range) {
    if (root === undefined) {
      throw new Error('missing required parameter "root"');
    }
    if (range === undefined) {
      throw new Error('missing required parameter "range"');
    }

    let startNode = range.startContainer;
    let startOffset = range.startOffset;

    // Drill down to a text node if the range starts at the container boundary.
    if (startNode.nodeType !== Node.TEXT_NODE) {
      if (startOffset === startNode.childNodes.length) {
        startNode = startNode.childNodes[startOffset - 1];
        startNode = getFirstTextNode(startNode);
        startOffset = startNode.textContent.length;
      } else {
        startNode = startNode.childNodes[startOffset];
        startNode = getFirstTextNode(startNode);
        startOffset = 0;
      }
    }

    let endNode = range.endContainer;
    let endOffset = range.endOffset;

    // Drill down to a text node if the range ends at the container boundary.
    if (endNode.nodeType !== Node.TEXT_NODE) {
      if (endOffset === endNode.childNodes.length) {
        endNode = endNode.childNodes[endOffset - 1];
        endNode = getFirstTextNode(endNode);
        endOffset = endNode.textContent.length;
      } else {
        endNode = endNode.childNodes[endOffset];
        endNode = getFirstTextNode(endNode);
        endOffset = 0;
      }
    }

    let iter = global.document.createNodeIterator(root, NodeFilter.SHOW_TEXT);
    let start = seek(iter, startNode);
    let end = start + seek(iter, endNode);

    return new TextPositionAnchor(root, start + startOffset, end + endOffset);
  }

  static fromSelector(root, selector = {}) {
    return new TextPositionAnchor(root, selector.start, selector.end);
  }

  toRange() {
    let root = this.root;
    let range = global.document.createRange();
    let iter = global.document.createNodeIterator(root, NodeFilter.SHOW_TEXT);

    let {start, end} = this;
    let count = seek(iter, start);
    let remainder = start - count;

    if (iter.pointerBeforeReferenceNode) {
      range.setStart(iter.referenceNode, remainder);
    } else {
      // If the iterator advanced it will be left with its pointer after the
      // reference node. The next node that is needed to create the range.
      range.setStart(iter.nextNode(), remainder);
      iter.previousNode();  // Rewind so as not to change the next result.
    }

    let length = (end - start) + remainder;
    count = seek(iter, length);
    remainder = length - count;

    if (iter.pointerBeforeReferenceNode) {
      range.setEnd(iter.referenceNode, remainder);
    } else {
      // Same as above, but no need to rewind.
      range.setEnd(iter.nextNode(), remainder);
    }

    return range;
  }

  toSelector() {
    return {
      type: 'TextPositionSelector',
      start: this.start,
      end: this.end,
    };
  }
}
<|endoftext|>