UNPKG

pencil.js

Version:

Nice modular interactive 2D drawing library.

262 lines (235 loc) 6.84 kB
import BaseEvent from "@pencil.js/base-event"; import Image from "@pencil.js/image"; import { modulo } from "@pencil.js/math"; import minimatch from "minimatch"; /** * @module Sprite */ /** * Sprite class * <br><img src="./media/examples/sprite.gif" alt="sprite demo"/> * @class * @extends {module:Image} */ export default class Sprite extends Image { /** * @typedef {Object} Frame * @prop {Number} x - Horizontal position * @prop {Number} y - Vertical position * @prop {Number} w - Width * @prop {Number} h - Height */ /** * @typedef {Object} FrameData * @prop {Frame} frame - Data about this frame in the sprite-sheet * @prop {Frame} spriteSourceSize - Data about the original file */ /** * Sprite constructor * @param {PositionDefinition} positionDefinition - * @param {String} url - * @param {Array<FrameData>} frames - * @param {SpriteOptions} [options] - Drawing options */ constructor (positionDefinition, url, frames, options) { super(positionDefinition, url, options); this.frames = frames; this.frame = 0; this.isPaused = false; } /** * @inheritDoc * @return {Sprite} Itself */ makePath (ctx) { const { sourceSize } = this.frames[Math.floor(this.frame)]; this.width = sourceSize.w; this.height = sourceSize.h; super.makePath(ctx); if (this.isLoaded) { const frameNumber = Math.floor(this.frame); if (!this.isPaused && (this.options.loop || this.frame < this.frames.length - 1)) { this.setFrame(this.frame + this.options.speed); } const nextFrame = Math.floor(this.frame); if (nextFrame !== frameNumber) { this.fire(new BaseEvent(Sprite.events.frame, this)); if (!this.options.loop && nextFrame === this.frames.length - 1) { this.fire(new BaseEvent(Sprite.events.end, this)); } } } return this; } /** * @inheritDoc * @return {Sprite} Itself */ draw (ctx) { const { frame, spriteSourceSize } = this.frames[Math.floor(this.frame)]; ctx.drawImage( this.file, frame.x, frame.y, frame.w, frame.h, spriteSourceSize.x, spriteSourceSize.y, spriteSourceSize.w, spriteSourceSize.h, ); return this; } /** * Play the sprite animation * @param {Number} [speed] - Choose a play speed * @return {Sprite} Itself */ play (speed) { this.isPaused = false; if (speed !== undefined) { this.options.speed = speed; } return this; } /** * Put the sprite on pause * @return {Sprite} Itself */ pause () { this.isPaused = true; return this; } /** * * @param {Number} frame - Number of the frame to set * @return {Sprite} Itself */ setFrame (frame) { this.frame = modulo(frame, this.frames.length); return this; } /** * @inheritDoc */ toJSON () { const { frames, frame, isPaused } = this; return { ...super.toJSON(), frames, frame, isPaused, }; } /** * * @param {Object} definition - * @return {Sprite} */ static from (definition) { const { position, url, frames, frame, isPaused, options } = definition; const sprite = new Sprite(position, url, frames, options); sprite.setFrame(frame); if (isPaused) { sprite.pause(); } return sprite; } /** * Load and return a spritesheet json file * @param {String} url - Url to the file * @return {Spritesheet} */ static async sheet (url) { const response = await window.fetch(url); const json = await response.json(); json.meta.file = await this.load(json.meta.image); // eslint-disable-next-line no-use-before-define return new Spritesheet(json); } /** * @typedef {Object} SpriteOptions * @extends ComponentOptions * @prop {Number} [speed=1] - * @prop {Boolean} [loop=true] - */ /** * @type {SpriteOptions} */ static get defaultOptions () { return { ...super.defaultOptions, speed: 1, loop: true, }; } /** * @typedef {Object} SpriteEvents * @extends ContainerEvent * @prop {String} start - * @prop {String} frame - * @prop {String} end - */ /** * @type {SpriteEvents} */ static get events () { return { ...super.events, start: "sprite-start", frame: "sprite-frame", end: "sprite-end", }; } } /** * Spritesheet class * @class */ class Spritesheet { /** * Spritesheet constructor * @param {Object} json - */ constructor (json) { this.json = json; } /** * Getter for the image file * @return {Image} */ get file () { return this.json.meta.file; } /** * Return all the frames corresponding to a selector * @param {String|Function|RegExp} [selector="*"] - Match against the spritesheet images name using a glob pattern, a validation function or a regular expression * @return {Array} */ get (selector = "*") { const filter = ((matcher) => { if (typeof matcher === "function") { return matcher; } if (typeof matcher === "string") { const glob = new minimatch.Minimatch(matcher, { dot: true, matchBase: true, }); return string => matcher === string || glob.match(string); } if (matcher instanceof RegExp) { return string => matcher.test(string); } return () => false; })(selector); const { frames } = this.json; return Object.keys(frames) .filter(filter) .map(key => frames[key]); } /** * Group images from the spritesheet into a single sprite * @param {PositionDefinition} position - Position of the sprite * @param {String|Function|RegExp} [selector="*"] - Match against the spritesheet images name using a glob pattern, a validation function or a regular expression * @param {ImageOptions} [options] - Options of the sprite * @return {Sprite} */ extract (position, selector, options) { return new Sprite(position, this.file, this.get(selector), options); } }