import { VYI } from './vyi.mjs';
import { Frame } from './frame.mjs';
/**
* @public
*/
export class Icon {
/**
* A map of icons that are state of this icon.
* @private
* @type {Map}
*/
states = new Map();
/**
* A map of frames that are the frames of this icon.
* @private
* @type {Map}
*/
frames = new Map();
/**
* The width of this icon. All states and frames of this icon must match this size.
* @private
* @type {number}
*/
width = 32;
/**
* The height of this icon. All states and frames of this icon must match this size.
* @private
* @type {number}
*/
height = 32;
/**
* The data URL of the sprite in this frame.
* @private
* @type {string}
*/
dataURL;
/**
* The delay of this frame.
* @private
* @type {number}
*/
delay = 100;
/**
* The name of this icon.
* @private
* @type {string}
*/
name = '';
/**
* The icon that owns this icon. This means this icon is state.
* @private
* @type {Icon}
*/
parent;
/**
* The vyi this icon belongs to.
*
* @private
* @type {VYI}
*/
vyi;
/**
* A random unique Id attached to each icon to distinguish them from others in the event another icon shares the same name.
*
* @private
* @type {string}
*/
id;
/**
* An set of used Ids to prevent collusion between duplicate named icons.
*
* @private
* @type {Set}
*/
static reservedIds = new Set();
/**
* Generates a UUID (Universally Unique Identifier) version 4.
*
* @private
* @returns {string} The generated UUID.
*/
static generateId() {
const genId = () => {
return Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, '0');
}
let id = genId();
while (this.reservedIds.has(id)) {
id = genId();
}
this.reservedIds.add(id);
return id;
}
/**
* Creates this icon instance.
* @param {Array} [pIconData] - The icon data that is used to build this icon.
*/
constructor(pIconData) {
this.parse(pIconData);
this.assignId();
}
/**
* Sets the parent for this state.
* @private
* @param {Icon} pParent - The parent icon of this state.
*/
setParent(pParent) {
if (!pParent || this.parent) return;
if (pParent instanceof Icon) {
this.parent = pParent;
}
}
/**
* Removes the parent and vyi from this state.
* @private
*/
removeParent() {
if (this.parent) {
this.parent = null;
this.vyi = null;
}
}
/**
* Sets the vyi of this icon.
*
* @private
* @param {VYI} pVyi - The vyi that owns this icon.
*/
setVyi(pVyi) {
if (!pVyi) return;
if (pVyi instanceof VYI) {
this.vyi = pVyi;
if (this.getStateCount() > 0) {
const states = this.getStates();
states.forEach((pState) => {
pState.setParent(this);
pState.setVyi(this.vyi);
});
}
const frames = this.getFrames();
frames.forEach((pFrame) => pFrame.setParent(this));
}
}
/**
* Removes the vyi from this icon.
* @private
*/
removeVyi() {
this.vyi = null;
}
/**
* Assigns an Id to this icon.
* @private
*/
assignId() {
this.id = Icon.generateId();
}
/**
* Gets the id of this icon.
* @returns {string} The id of this icon.
*/
getId() {
return this.id;
}
/**
* Gets the vyi this icon belongs to.
* @returns {VYI} The vyi this icon belongs to.
*/
getVyi() {
return this.vyi;
}
/**
* Gets the icon this state belongs to. If this icon is not a state, it will return undefined.
* @returns {Icon|undefined} The icon this state belongs to.
*/
getParent() {
return this.parent;
}
/**
* Gets the number of states this icon has.
* @returns {number} The amount of states this icon has.
*/
getStateCount() {
return this.states.size;
}
/**
* Gets the number of frames this icon has.
* @returns {number} The amount of frames this icon has.
*/
getFrameCount() {
return this.frames.size;
}
/**
* Parses through the icon data and adds data to this icon.
* @param {Array} pIconData - The icon data that is used to build this icon.
*/
parse(pIconData) {
if (!pIconData) return;
// Loop through the icon data and create this icon
const iconName = pIconData[0];
const iconWidth = pIconData[1];
const iconHeight = pIconData[2];
const iconDelay = pIconData[3];
const iconDataURL = pIconData[4];
const frameArray = pIconData[5];
const stateArray = pIconData[6];
// Set name
this.rename(iconName);
// Set size
this.setSize(iconWidth, iconHeight);
// Set icon delay
this.setDelay(iconDelay);
// Set dataURL
this.setDataURL(iconDataURL);
if (Array.isArray(frameArray)) {
// If the frame array has data then we need to store it.
frameArray.forEach((pFrame) => {
// pFrame is an array holding the datalURL and frameDelay of the frame
this.addFrame(pFrame);
});
}
if (Array.isArray(stateArray)) {
// If the state array has data then we need to store it.
stateArray.forEach((pStateData) => {
const state = new Icon();
const stateName = pStateData[0];
const stateDataURL = pStateData[1];
const stateDelay = pStateData[2];
const stateFrameArray = pStateData[3];
state.rename(stateName);
state.setSize(iconWidth, iconHeight);
state.setDelay(stateDelay);
state.setDataURL(stateDataURL);
if (Array.isArray(stateFrameArray)) {
stateFrameArray.forEach((pFrame) => {
state.addFrame(pFrame);
});
}
this.addState(state);
});
}
}
/**
* Sets the size of this icon.
* @param {number} pWidth - The width of this icon.
* @param {number} pHeight - THe height of this icon.
* @returns {self} This icon instance.
*/
setSize(pWidth, pHeight) {
if (typeof pWidth === 'number') {
this.width = pWidth;
}
if (typeof pHeight === 'number') {
this.height = pHeight;
}
}
/**
* Gets the width of the icon.
* @returns {number} The width of the icon.
*/
getWidth() {
return this.width;
}
/**
* Gets the height of the icon.
* @returns {number} The height of the icon.
*/
getHeight() {
return this.height;
}
/**
* Gets the width and height of this icon and returns it.
* @returns {Object} An object with the width and height of this icon.
*/
getSize() {
return { width: this.width, height: this.height };
}
/**
* Sets the data url of this icon.
* @param {DataURL} pDataURL - The base64 data of this image.
* @returns {self} This icon instance.
*/
setDataURL(pDataURL) {
if (typeof pDataURL === 'string') {
this.dataURL = pDataURL;
} else {
VYI.logger.prefix('Vyi-module').error('Invalid data url type!');
}
return this;
}
/**
* Gets the data URL of this icon.
* @returns {DataURL} - The base64 data of this image.
*/
getDataURL() {
return this.dataURL;
}
/**
* Sets the frame delay of this icon.
* @param {number} pDelay - The delay to set this frame to.
* @returns {self} This icon instance.
*/
setDelay(pDelay) {
if (typeof pDelay === 'number') {
this.delay = pDelay;
} else {
VYI.logger.prefix('Vyi-module').error('Invalid delay type!');
}
return this;
}
/**
* Gets the delay of this icon.
* @returns {number} The delay of this icon.
*/
getDelay() {
return this.delay;
}
/**
* Changes the name of this icon.
* @param {string} pName - The new name of the icon.
* @returns {self} This icon instance.
*/
rename(pName) {
if (typeof pName === 'string') {
this.name = pName;
} else {
VYI.logger.prefix('Vyi-module').error('Invalid type for pName!');
}
return this;
}
/**
* Returns the name of this icon.
* @returns {string} The name of this icon.
*/
getName() {
return this.name;
}
/**
* Sets all the frames belonging to this icon to the same delay.
* @param {number} pDelay - The delay to set all frames to.
* @returns {self} This icon instance.
*/
setAllFrameDelays(pDelay) {
if (typeof pDelay === 'number') {
this.setDelay(pDelay);
this.getFrames().forEach((pFrame) => pFrame.setDelay(pDelay));
} else {
VYI.logger.prefix('Vyi-module').error('Invalid type for pDelay!');
}
return this;
}
/**
* Adds a new frame to this icon.
* @param {Frame|Array} pFrameData - The frame data to give this frame.
* @returns {Frame|undefined} The frame that was added or undefined.
*/
addFrame(pFrameData) {
if (!pFrameData) {
VYI.logger.prefix('Vyi-module').error('No frame data passed!');
return;
}
if (!(pFrameData instanceof Frame) && !Array.isArray(pFrameData)) {
VYI.logger.prefix('Vyi-module').error('Invalid frame data type passed!');
return;
}
// Create the icon instance
const frame = pFrameData instanceof Frame
? pFrameData
: new Frame(pFrameData);
if (frame.getWidth() !== frame.getWidth() || frame.getHeight() !== frame.getHeight()) {
VYI.logger.prefix('Vyi-module').error('Frame dimensions do not match parent!');
return;
}
frame.setParent(this);
this.frames.set(this.frames.size, frame);
this.indexFrames();
return frame;
}
/**
* Removes the frame passed.
* @param {Frame} pFrame - The frame to remove from this icon.
* @returns {self} This icon instance.
*/
removeFrame(pFrame) {
if (!pFrame) return;
if (pFrame instanceof Frame) {
if (this.frames.delete(pFrame.index)) {
pFrame.removeParent();
this.indexFrames();
}
}
return this;
}
/**
* Removes the frame via it's index.
* @param {number} pIndex - The index of the frame to remove.
* @returns {self} This icon instance.
*/
removeFrameByIndex(pIndex) {
const frame = this.getFrame(pIndex);
this.removeFrame(frame);
return this;
}
/**
* Index the frames properly.
* @private
*/
indexFrames() {
const frames = this.getFrames();
this.frames.clear();
frames.forEach((pFrame, pIndex) => {
pFrame.index = pIndex;
this.frames.set(pIndex, pFrame);
});
}
/**
* Reorders the frame in the animation. The index of the passed frame will be swapped with the frame at pIndex.
* The "first" frame of the animation is technically this icon's dataURL. So if you are aiming to change the order of this icon and convert it into a frame.
* pCurrentIndex must be set to -1 to match this icon.
*
* @param {number} pCurrentIndex - The current index of the frame.
* @param {number} pIndex - The index the frame will be moving to.
* @returns {self} This icon instance.
*/
reorderFrame(pCurrentIndex, pIndex) {
if (typeof pCurrentIndex === 'number' && typeof pIndex === 'number') {
let frameAtIndex = this.getFrame(pIndex);
let currentFrame = pCurrentIndex === -1
? this
: this.getFrame(pCurrentIndex)
// If both frames can be found, we can swap their data.
if (currentFrame && frameAtIndex) {
// Store frame data
const currentFrameDataURL = currentFrame.getDataURL();
const currentFrameDelay = currentFrame.getDelay();
const frameAtIndexDataURL = frameAtIndex.getDataURL();
const frameAtIndexDelay = frameAtIndex.getDelay();
// Swap data from frame
currentFrame.setDataURL(frameAtIndexDataURL);
currentFrame.setDelay(frameAtIndexDelay);
// Swap data to frame
frameAtIndex.setDataURL(currentFrameDataURL);
frameAtIndex.setDelay(currentFrameDelay);
} else {
VYI.logger.prefix('Vyi-module').error('There was no frame found at pCurrentIndex, or there was no frame found at pIndex!');
}
} else {
VYI.logger.prefix('Vyi-module').error('Invalid type used!');
}
return this;
}
/**
* Gets the frame existing at pIndex.
* Frame 0 will actually be frame "1" in the animation. As this icon will actually be frame 0.
* If you are trying to get "frame" 1. Then you will need to use the icon's delay and data url. As that is frame 0.
* @param {number} pIndex - The index of the frame to get.
* @returns {Frame|undefined} The frame found at pIndex.
*/
getFrame(pIndex) {
return this.frames.get(pIndex);
}
/**
* Returns an array of all the frames this icons has.
* @returns {Array} An array of frames this icon has.
*/
getFrames() {
return Array.from(this.frames.values());
}
/**
* Gets all the frames belonging to this icon.
* @private
* @returns {Array} An array containing the frame data of all frames.
*/
getFramesData() {
const frameDataArray = this.getFrames().map((pFrame) => pFrame.export());
return frameDataArray;
}
/**
* Adds this icon data as a state. A state is also an icon.
* @param {Array} pIconData - The data used to create this state icon.
* @returns {Icon|undefined} The state that was added or undefined.
*/
addState(pIconData) {
if (!pIconData) {
VYI.logger.prefix('Vyi-module').error('No icon data passed!');
return;
}
if (!(pIconData instanceof Icon) && !Array.isArray(pIconData)) {
VYI.logger.prefix('Vyi-module').error('Invalid icon data type passed!');
return;
}
// Create the icon instance
const state = pIconData instanceof Icon
? pIconData
: new Icon(pIconData);
if (state.getWidth() !== this.getWidth() || state.getHeight() !== this.getHeight()) {
VYI.logger.prefix('Vyi-module').error('State dimensions do not match parent!');
return;
}
state.setParent(this);
state.setVyi(this.vyi);
this.states.set(state.id, state);
return state;
}
/**
* Removes the state passed.
* @param {Icon} pState - The state to remove from this icon.
* @returns {self} This icon instance.
*/
removeState(pState) {
if (pState instanceof Icon) {
if (this.states.delete(pState.id)) {;
pState.removeParent();
}
}
return this;
}
/**
* Removes the state via it's name. The LAST defined icon that has the passed name will be removed. As names are not unique.
* @param {string} pName - The name to use to find the state.
* @returns {self} This icon instance.
*/
removeStateByName(pName) {
const state = this.getState(pName);
this.removeState(state);
return this;
}
/**
* Removes the state via it's id.
* @param {string} pName - The id to use to find the state.
* @returns {self} This icon instance.
*/
removeStateById(pId) {
const state = this.getStateById(pId);
this.removeState(state);
return this;
}
/**
* Gets the state that has the name pName. The LAST defined state that has the passed name will be returned.
* @param {string} pName - The name of the state to get.
* @returns {Icon} The state that has the name of pName.
*/
getState(pName) {
if (typeof pName === 'string') {
const states = this.getStates();
for (let i = states.length - 1; i >= 0; i--) {
const state = states[i];
// If the icon has the same name, return that icon
if (state.getName() === pName) {
return state;
}
}
} else {
VYI.logger.prefix('Vyi-module').error('Invalid name type used!');
}
}
/**
* Gets the state by the id provided.
* @private
* @param {string} pId - The id of the state.
* @returns {Icon} The state that has the id that was passed.
*/
getStateById(pId) {
if (!pId) return;
return this.states.get(pId);
}
/**
* Returns an array of all the states this icon has.
* @returns {Icon[]} An array of states this icon has.
*/
getStates() {
return Array.from(this.states.values());
}
/**
* Returns an array of all the state names this icon has.
* @returns {string[]} An array of the state names.
*/
getStateNames() {
return this.getStates().map((pState) => pState.getName());
}
/**
* Gets all the states belonging to this icon.
* @private
* @returns {object[]} An array containing the state data of all frames.
*/
getStatesData() {
const stateDataArray = this.getStates().map((pState) => pState.exportAsState());
return stateDataArray;
}
/**
* Exports this icon as if it was a state in the proper vyi format.
* @private
* @returns {Array} An array of data related to this icon as if it were a state.
*/
exportAsState() {
const stateData = [];
// state name
stateData[0] = this.getName();
// state dataURL
stateData[1] = this.getDataURL();
// state frame delay
stateData[2] = this.getDelay();
// state frame array
stateData[3] = this.getFrames().map((pFrame) => pFrame.export());
return stateData;
}
/**
* Exports this icon's data into proper vyi format.
* @returns {Array} An array of data related to this icon in the proper vyi format.
*/
export() {
const iconData = [];
// icon name
iconData[0] = this.getName();
// icon width
iconData[1] = this.getWidth();
// icon height
iconData[2] = this.getHeight();
// frame delay
iconData[3] = this.getDelay();
// icon DataURL
iconData[4] = this.getDataURL();
// frame array
iconData[5] = this.getFramesData();
// This is actually an optional data entry into the vyi, only used if states actually exist on this icon.
if (this.states.size > 0) {
iconData[6] = this.getStatesData();
}
return iconData;
}
}