UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

401 lines (336 loc) 11.4 kB
/** * @author Richard Davey <rich@phaser.io> * @copyright 2013-2026 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ // Hardcoded extension dictionary, as defined by the PCT format specification var EXT = { 1: '.png', 2: '.webp', 3: '.jpg', 4: '.jpeg', 5: '.gif' }; var isAllDigits = function (str) { if (str.length === 0) { return false; } for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); if (c < 48 || c > 57) { return false; } } return true; }; var zeroPad = function (n, len) { var s = String(n); while (s.length < len) { s = '0' + s; } return s; }; var resolveFullName = function (raw, folders, extSuffix) { var name = raw; var folder = ''; // Folder index: "N/rest" var slashIdx = name.indexOf('/'); if (slashIdx > 0 && isAllDigits(name.substring(0, slashIdx))) { var folderIdx = parseInt(name.substring(0, slashIdx), 10); folder = folders[folderIdx]; name = name.substring(slashIdx + 1); } // Extension index: "name~N" (per-name, overrides line-level) var extMatch = /~([1-5])$/.exec(name); if (extMatch) { var ext = EXT[parseInt(extMatch[1], 10)]; name = name.substring(0, name.length - 2) + ext; } else if (extSuffix) { name = name + extSuffix; } if (folder) { return folder + '/' + name; } return name; }; var expandNames = function (line, folders) { // Check for trailing extension suffix: ~N at very end var extSuffix = ''; var extMatch = /~([1-5])$/.exec(line); if (extMatch) { extSuffix = EXT[parseInt(extMatch[1], 10)]; line = line.substring(0, line.length - 2); } var results = []; var segments = line.split(','); for (var s = 0; s < segments.length; s++) { var segment = segments[s]; var hashIdx = segment.indexOf('#'); if (hashIdx >= 0) { var prefix = segment.substring(0, hashIdx); var range = segment.substring(hashIdx + 1); var dashIdx = range.indexOf('-'); var startStr = range.substring(0, dashIdx); var endStr = range.substring(dashIdx + 1); var start = parseInt(startStr, 10); var end = parseInt(endStr, 10); var padLen = (startStr.length > 1 && startStr.charAt(0) === '0') ? startStr.length : 0; for (var i = start; i <= end; i++) { var numStr = (padLen > 0) ? zeroPad(i, padLen) : String(i); results.push(resolveFullName(prefix + numStr, folders, extSuffix)); } } else { results.push(resolveFullName(segment, folders, extSuffix)); } } return results; }; /** * Decodes a Phaser Compact Texture Atlas (PCT) file from its raw text representation into a * structured object. * * This is a standalone helper used by both the PCT atlas loader and the `PCT` texture parser. * It converts the line-oriented PCT text format into an object containing a `pages` array * (one entry per atlas page), a `folders` dictionary, and a `frames` map with fully-resolved * frame names and positions. Frame `page` indices map directly to the `pages` array. * * The function validates the version header and rejects files with an unsupported major * version or a missing `PCT:` header, returning `null` in those cases after logging a warning. * Unknown line prefixes introduced in future minor versions are silently skipped, as required * by the specification. * * See the Phaser Compact Texture Atlas Format Specification document for a full description * of the format and the semantics of the returned object. * * @function Phaser.Textures.Parsers.PCTDecode * @memberof Phaser.Textures.Parsers * @since 4.0.0 * * @param {string} text - The raw text contents of a `.pct` file. * * @return {?{pages: object[], folders: string[], frames: object}} The decoded PCT structure, or `null` if the input is invalid. */ var PCTDecode = function (text) { if (typeof text !== 'string' || text.length === 0) { console.warn('Invalid PCT file: empty or not a string'); return null; } var lines = text.split('\n'); // Validate the version header - must be the first line var firstLine = lines[0]; if (firstLine.charCodeAt(firstLine.length - 1) === 13) { firstLine = firstLine.substring(0, firstLine.length - 1); } if (!firstLine || firstLine.indexOf('PCT:') !== 0) { console.warn('Not a PCT file: missing PCT: header'); return null; } var versionStr = firstLine.substring(4); var versionParts = versionStr.split('.'); var major = parseInt(versionParts[0], 10); if (isNaN(major) || major > 1) { console.warn('Unsupported PCT version: ' + versionStr); return null; } var pages = []; var folders = []; var frames = {}; var currentPage = 0; var pendingBlock = null; for (var i = 1; i < lines.length; i++) { var line = lines[i]; if (!line) { continue; } // Strip trailing \r if the file uses CRLF line endings if (line.charCodeAt(line.length - 1) === 13) { line = line.substring(0, line.length - 1); if (!line) { continue; } } // A pending block ALWAYS consumes the next non-empty line as its // names line, regardless of what prefix character that line starts // with. This must be checked before any prefix-based dispatch below. if (pendingBlock !== null) { var blk = pendingBlock; pendingBlock = null; var padding = pages[blk.page].padding; var cellW = blk.frameW + padding * 2; var cellH = blk.frameH + padding * 2; var names = expandNames(line, folders); for (var n = 0; n < names.length; n++) { var col = n % blk.cols; var row = Math.floor(n / blk.cols); var blockName = names[n]; var blockFrame = { key: blockName, page: blk.page, x: blk.x + col * cellW + padding, y: blk.y + row * cellH + padding, w: blk.frameW, h: blk.frameH, trimmed: blk.trimmed, rotated: false }; if (blk.trimmed) { blockFrame.sourceW = blk.sourceW; blockFrame.sourceH = blk.sourceH; blockFrame.trimX = blk.trimX; blockFrame.trimY = blk.trimY; } else { blockFrame.sourceW = blk.frameW; blockFrame.sourceH = blk.frameH; blockFrame.trimX = 0; blockFrame.trimY = 0; } frames[blockName] = blockFrame; } continue; } var prefix2 = line.substring(0, 2); if (prefix2 === 'P:') { var parts = line.substring(2).split(','); pages.push({ filename: parts[0], format: parts[1], width: parseInt(parts[2], 10), height: parseInt(parts[3], 10), padding: parseInt(parts[4], 10) }); } else if (prefix2 === 'F:') { folders.push(line.substring(2)); } else if (line.charAt(0) === '#') { currentPage = parseInt(line.substring(1), 10); } else if (prefix2 === 'B:') { var trimParts = line.substring(2).split('|'); var main = trimParts[0].split(','); var block = { page: currentPage, x: parseInt(main[0], 10), y: parseInt(main[1], 10), cols: parseInt(main[2], 10), frameW: parseInt(main[3], 10), frameH: parseInt(main[4], 10), trimmed: trimParts.length > 1 }; if (block.trimmed) { var trim = trimParts[1].split(','); block.sourceW = parseInt(trim[0], 10); block.sourceH = parseInt(trim[1], 10); block.trimX = parseInt(trim[2], 10); block.trimY = parseInt(trim[3], 10); } pendingBlock = block; } else if (prefix2 === 'A:') { // Alias: A:originalName=dupName1,dupName2,... var eqIdx = line.indexOf('=', 2); if (eqIdx === -1) { continue; } var originalName = resolveFullName(line.substring(2, eqIdx), folders, ''); var dupNames = expandNames(line.substring(eqIdx + 1), folders); var orig = frames[originalName]; if (orig) { for (var d = 0; d < dupNames.length; d++) { var dupName = dupNames[d]; var dup = {}; for (var k in orig) { dup[k] = orig[k]; } dup.key = dupName; frames[dupName] = dup; } } } else { // Individual frame line var fparts = line.split('|'); if (fparts.length < 3) { // Unknown or future line type — skip safely continue; } var fname = resolveFullName(fparts[0], folders, ''); var flags = parseInt(fparts[1], 10); var isTrimmed = (flags & 2) !== 0; var fv = fparts[2].split(','); var iframe = { key: fname, page: currentPage, x: parseInt(fv[0], 10), y: parseInt(fv[1], 10), w: parseInt(fv[2], 10), h: parseInt(fv[3], 10), trimmed: isTrimmed, rotated: (flags & 1) !== 0 }; if (isTrimmed) { // Trim data is packed into a single segment as "sw,sh,sx,sy", // mirroring the layout used by the B: block header trim fields. var tv = fparts[3].split(','); iframe.sourceW = parseInt(tv[0], 10); iframe.sourceH = parseInt(tv[1], 10); iframe.trimX = parseInt(tv[2], 10); iframe.trimY = parseInt(tv[3], 10); } else { iframe.sourceW = iframe.w; iframe.sourceH = iframe.h; iframe.trimX = 0; iframe.trimY = 0; } frames[fname] = iframe; } } return { pages: pages, folders: folders, frames: frames }; }; module.exports = PCTDecode;