/*!! * flipnote.js v6.3.0 * https://flipnote.js.org * A JavaScript library for Flipnote Studio animation files * 2018 - 2025 James Daniel * Flipnote Studio is (c) Nintendo Co., Ltd. This project isn't affiliated with or endorsed by them in any way. */ var flipnote = (function (exports) { 'use strict'; /** * Flipnote region */ exports.FlipnoteRegion = void 0; (function (FlipnoteRegion) { /** * Europe and Oceania */ FlipnoteRegion["EUR"] = "EUR"; /** * Americas */ FlipnoteRegion["USA"] = "USA"; /** * Japan */ FlipnoteRegion["JPN"] = "JPN"; /** * Unidentified (possibly never used) */ FlipnoteRegion["UNKNOWN"] = "UNKNOWN"; })(exports.FlipnoteRegion || (exports.FlipnoteRegion = {})); /** * Identifies which animation format a Flipnote uses */ exports.FlipnoteFormat = void 0; (function (FlipnoteFormat) { /** * Animation format used by Flipnote Studio (Nintendo DSiWare) */ FlipnoteFormat["PPM"] = "PPM"; /** * Animation format used by Flipnote Studio 3D (Nintendo 3DS) */ FlipnoteFormat["KWZ"] = "KWZ"; })(exports.FlipnoteFormat || (exports.FlipnoteFormat = {})); /** * Buffer format for a FlipnoteThumbImage */ exports.FlipnoteThumbImageFormat = void 0; (function (FlipnoteThumbImageFormat) { FlipnoteThumbImageFormat[FlipnoteThumbImageFormat["Jpeg"] = 0] = "Jpeg"; FlipnoteThumbImageFormat[FlipnoteThumbImageFormat["Rgba"] = 1] = "Rgba"; })(exports.FlipnoteThumbImageFormat || (exports.FlipnoteThumbImageFormat = {})); /** * stereoscopic eye view (left/right) for 3D effects */ exports.FlipnoteStereoscopicEye = void 0; (function (FlipnoteStereoscopicEye) { FlipnoteStereoscopicEye[FlipnoteStereoscopicEye["Left"] = 0] = "Left"; FlipnoteStereoscopicEye[FlipnoteStereoscopicEye["Right"] = 1] = "Right"; })(exports.FlipnoteStereoscopicEye || (exports.FlipnoteStereoscopicEye = {})); /** * Identifies a Flipnote audio track type */ exports.FlipnoteAudioTrack = void 0; (function (FlipnoteAudioTrack) { /** * Background music track */ FlipnoteAudioTrack[FlipnoteAudioTrack["BGM"] = 0] = "BGM"; /** * Sound effect 1 track */ FlipnoteAudioTrack[FlipnoteAudioTrack["SE1"] = 1] = "SE1"; /** * Sound effect 2 track */ FlipnoteAudioTrack[FlipnoteAudioTrack["SE2"] = 2] = "SE2"; /** * Sound effect 3 track */ FlipnoteAudioTrack[FlipnoteAudioTrack["SE3"] = 3] = "SE3"; /** * Sound effect 4 track (only used by KWZ files) */ FlipnoteAudioTrack[FlipnoteAudioTrack["SE4"] = 4] = "SE4"; })(exports.FlipnoteAudioTrack || (exports.FlipnoteAudioTrack = {})); /** * {@link FlipnoteAudioTrack}, but just sound effect tracks */ exports.FlipnoteSoundEffectTrack = void 0; (function (FlipnoteSoundEffectTrack) { FlipnoteSoundEffectTrack[FlipnoteSoundEffectTrack["SE1"] = 1] = "SE1"; FlipnoteSoundEffectTrack[FlipnoteSoundEffectTrack["SE2"] = 2] = "SE2"; FlipnoteSoundEffectTrack[FlipnoteSoundEffectTrack["SE3"] = 3] = "SE3"; FlipnoteSoundEffectTrack[FlipnoteSoundEffectTrack["SE4"] = 4] = "SE4"; })(exports.FlipnoteSoundEffectTrack || (exports.FlipnoteSoundEffectTrack = {})); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * @internal */ class ByteArray { constructor() { /** * @internal */ this.pageSize = 2048 * 2; /** * Allocated size counting all pages. * @internal */ this.allocSize = 0; /** * Number of bytes actually used. * @internal */ this.realSize = 0; /** * @internal */ this.pages = []; /** * @internal */ this.numPages = 0; /** * Page to write to. * @internal */ this.pageIdx = 0; /** * Position in page to write to. * @internal */ this.pagePtr = 0; // /** * Position in file. * @internal */ this.realPtr = 0; this.newPage(); } /** * @internal */ set pointer(ptr) { this.setPointer(ptr); } /** * @internal */ get pointer() { return this.realPtr; } /** * @internal */ newPage() { this.pages[this.numPages] = new Uint8Array(this.pageSize); this.numPages = this.pages.length; this.allocSize = this.numPages * this.pageSize; } /** * @internal */ setPointer(ptr) { // allocate enough pages to include pointer while (ptr >= this.allocSize) { this.newPage(); } // increase real file size if the end is reached if (ptr > this.realSize) this.realSize = ptr; // update ptrs this.pageIdx = Math.floor(ptr / this.pageSize); this.pagePtr = ptr % this.pageSize; this.realPtr = ptr; } /** * @internal */ writeByte(value) { this.pages[this.pageIdx][this.pagePtr] = value; this.setPointer(this.realPtr + 1); } /** * @internal */ writeBytes(bytes, srcPtr, length) { for (let l = length || bytes.length, i = srcPtr || 0; i < l; i++) this.writeByte(bytes[i]); } /** * @internal */ writeChars(str) { for (let i = 0; i < str.length; i++) { this.writeByte(str.charCodeAt(i)); } } /** * @internal */ writeU8(value) { this.writeByte(value & 0xFF); } /** * @internal */ writeU16(value) { this.writeByte((value >>> 0) & 0xFF); this.writeByte((value >>> 8) & 0xFF); } /** * @internal */ writeU32(value) { this.writeByte((value >>> 0) & 0xFF); this.writeByte((value >>> 8) & 0xFF); this.writeByte((value >>> 16) & 0xFF); this.writeByte((value >>> 24) & 0xFF); } getBytes() { const bytes = new Uint8Array(this.realSize); const numPages = this.numPages; for (let i = 0; i < numPages; i++) { const page = this.pages[i]; if (i === numPages - 1) // last page bytes.set(page.slice(0, this.realSize % this.pageSize), i * this.pageSize); else bytes.set(page, i * this.pageSize); } return bytes; } getBuffer() { const bytes = this.getBytes(); return bytes.buffer; } } /** * @internal */ const hexFromBytes = (bytes, reverse = false) => { let hex = []; for (let i = 0; i < bytes.length; i++) hex.push(bytes[i].toString(16).padStart(2, '0')); if (reverse) hex.reverse(); return hex.join('').toUpperCase(); }; /** * @internal */ const hexToBytes = (hex, reverse = false) => { const hexSize = hex.length; const bytes = new Uint8Array(hexSize / 2); for (let i = 0, j = 0; i < hexSize; i += 2) bytes[j++] = parseInt(hex.slice(i, i + 2), 16); if (reverse) bytes.reverse(); return bytes; }; /** * Wrapper around the DataView API to keep track of the offset into the data, * also provides some utils for reading ascii strings etc. * @internal */ class DataStream { constructor(buffer) { this.buffer = buffer; this.data = new DataView(buffer); this.pointer = 0; } /** * Returns the data as an Uint8Array of bytes. */ get bytes() { return new Uint8Array(this.buffer); } /** * Returns the total number of bytes in the data. */ get numBytes() { return this.data.byteLength; } /** * @internal */ seek(offset, whence) { switch (whence) { case 2 /* SeekOrigin.End */: this.pointer = this.data.byteLength + offset; break; case 1 /* SeekOrigin.Current */: this.pointer += offset; break; case 0 /* SeekOrigin.Begin */: default: this.pointer = offset; break; } } /** * @internal */ readUint8() { const val = this.data.getUint8(this.pointer); this.pointer += 1; return val; } /** * @internal */ writeUint8(value) { this.data.setUint8(this.pointer, value); this.pointer += 1; } /** * @internal */ readInt8() { const val = this.data.getInt8(this.pointer); this.pointer += 1; return val; } /** * @internal */ writeInt8(value) { this.data.setInt8(this.pointer, value); this.pointer += 1; } /** * @internal */ readUint16(littleEndian = true) { const val = this.data.getUint16(this.pointer, littleEndian); this.pointer += 2; return val; } /** * @internal */ writeUint16(value, littleEndian = true) { this.data.setUint16(this.pointer, value, littleEndian); this.pointer += 2; } /** * @internal */ readInt16(littleEndian = true) { const val = this.data.getInt16(this.pointer, littleEndian); this.pointer += 2; return val; } /** * @internal */ writeInt16(value, littleEndian = true) { this.data.setInt16(this.pointer, value, littleEndian); this.pointer += 2; } /** * @internal */ readUint32(littleEndian = true) { const val = this.data.getUint32(this.pointer, littleEndian); this.pointer += 4; return val; } /** * @internal */ writeUint32(value, littleEndian = true) { this.data.setUint32(this.pointer, value, littleEndian); this.pointer += 4; } /** * @internal */ readInt32(littleEndian = true) { const val = this.data.getInt32(this.pointer, littleEndian); this.pointer += 4; return val; } /** * @internal */ writeInt32(value, littleEndian = true) { this.data.setInt32(this.pointer, value, littleEndian); this.pointer += 4; } /** * @internal */ readBytes(count) { const bytes = new Uint8Array(this.data.buffer, this.pointer, count); this.pointer += bytes.byteLength; return bytes; } /** * @internal */ writeBytes(bytes) { bytes.forEach((byte) => this.writeUint8(byte)); } /** * @internal */ readHex(count, reverse = false) { const bytes = this.readBytes(count); return hexFromBytes(bytes, reverse); } /** * @internal */ readChar() { const char = this.readUint8(); return String.fromCharCode(char); } /** * @internal */ readWideChar() { const char = this.readUint16(); return String.fromCharCode(char); } /** * @internal */ readChars(count) { const chars = this.readBytes(count); let str = ''; for (let i = 0; i < chars.length; i++) { const char = chars[i]; if (char === 0) break; str += String.fromCharCode(char); } return str; } /** * @internal */ writeChars(string) { for (let i = 0; i < string.length; i++) { const char = string.charCodeAt(i); this.writeUint8(char); } } /** * @internal */ readWideChars(count) { const chars = new Uint16Array(this.data.buffer, this.pointer, count); let str = ''; for (let i = 0; i < chars.length; i++) { const char = chars[i]; if (char == 0) break; str += String.fromCharCode(char); } this.pointer += chars.byteLength; return str; } end() { return this.pointer >= this.data.byteLength; } } /** * Clamp a number n between l and h. * @internal */ const clamp = (n, l, h) => { if (n < l) return l; if (n > h) return h; return n; }; /** * Interpolate between a and b - returns a if fac = 0, b if fac = 1, and somewhere between if 0 < fac < 1. * @internal */ const lerp = (a, b, fac) => a + fac * (b - a); /** * Assert condition is true. * @internal */ function assert(condition, errMsg = 'Assert failed') { if (!condition) err(errMsg); } /** * Assert that a numerical value is between upper and lower bounds. * @internal */ const assertRange = (value, min, max, name = '') => assert(value >= min && value <= max, `flipnote.js error: ${name || 'value'} ${value} should be between ${min} and ${max}`); /** * Assert condition is true. * @internal */ const err = (errMsg = 'Assert failed') => { throw new Error('flipnote.js error: ' + errMsg); }; /** * Webpack tries to replace inline calls to require() with polyfills, * but we don't want that, since we only use require to add extra features in NodeJs environments. * * Modified from: * https://github.com/getsentry/sentry-javascript/blob/bd35d7364191ebed994fb132ff31031117c1823f/packages/utils/src/misc.ts#L9-L11 * https://github.com/getsentry/sentry-javascript/blob/89bca28994a0eaab9bc784841872b12a1f4a875c/packages/hub/src/hub.ts#L340 * @internal */ const dynamicRequire = (nodeModule, p) => { try { return nodeModule.require(p); } catch { throw new Error(`Could not require(${p})`); } }; /** * Safely get global scope object. * @internal */ const getGlobalObject = () => { return isNode ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}; }; /** * Utils to find out information about the current code execution environment. */ /** * Is the code running in a browser environment? * @internal */ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; /** * Assert that the current environment should support browser APIs. * @internal */ const assertBrowserEnv = () => assert(isBrowser, 'This feature is only available in browser environments'); /** * Is the code running in a Node environment? * @internal */ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; /** * Assert that the current environment should support NodeJS APIs. * @internal */ const assertNodeEnv = () => assert(isNode, 'This feature is only available in NodeJS environments'); // TODO: Deno support? /** * Is the code running in a Web Worker environment? * @internal */ const isWebWorker = typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope'; /** * @internal */ const raf = isBrowser && (window.requestAnimationFrame || window.webkitRequestAnimationFrame); /** * @internal */ const nextPaint = (callback) => { if (isBrowser) raf(() => raf(() => callback())); else callback(); }; /** * Gracefully handles a given Promise factory. * * Example: * `const [ error, data ] = await until(() => asyncAction())` * * @internal */ const until = async (promise) => { try { const data = await promise().catch((error) => { throw error; }); return [null, data]; } catch (error) { return [error, null]; } }; /** * @internal */ ((function () { if (!isBrowser) { return function () { }; } const a = document.createElement('a'); return function (blob, filename) { const url = window.URL.createObjectURL(blob); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); }; }))(); var _a$2; /** * Base Flipnote parser class * * This doesn't implement any parsing functionality itself, * it just provides a consistent API for every format parser to implement. * @group File Parser */ class BaseParser extends DataStream { constructor() { /** * Static file format info */ super(...arguments); /** * Instance file format info */ /** * Custom object tag */ this[_a$2] = 'Flipnote'; /** * Default formats used for {@link getTitle}. * @group Meta */ this.titleFormats = { COMMENT: 'Comment by $USERNAME', FLIPNOTE: 'Flipnote by $USERNAME', ICON: 'Folder icon' }; /** * File audio track info, see {@link FlipnoteAudioTrackInfo}. * @group Meta */ this.soundMeta = new Map(); /** * Animation frame global layer visibility. * @group Image */ this.layerVisibility = { 1: true, 2: true, 3: true }; /** * (KWZ only) Indicates whether or not this file is a Flipnote Studio 3D folder icon. * @group Meta */ this.isFolderIcon = false; /** * (KWZ only) Indicates whether or not this file is a handwritten comment from Flipnote Gallery World. * @group Meta */ this.isComment = false; /** * (KWZ only) Indicates whether or not this Flipnote is a PPM to KWZ conversion from Flipnote Studio 3D's DSi Library service. * @group Meta */ this.isDsiLibraryNote = false; } /** * Get file default title - e.g. "Flipnote by Y", "Comment by X", etc. * A format object can be passed for localization, where `$USERNAME` gets replaced by author name: * ```js * { * COMMENT: 'Comment by $USERNAME', * FLIPNOTE: 'Flipnote by $USERNAME', * ICON: 'Folder icon' * } * ``` * @group Meta */ getTitle(formats = this.titleFormats) { if (this.isFolderIcon) return formats.ICON; const title = this.isComment ? formats.COMMENT : formats.FLIPNOTE; return title.replace('$USERNAME', this.meta.current.username); } /** * Returns the Flipnote title when casting a parser instance to a string. * * ```js * const str = 'Title: ' + note; * // str === 'Title: Flipnote by username' * ``` * @group Utility */ toString() { return this.getTitle(); } /** * Allows for frame index iteration when using the parser instance as a for..of iterator. * * ```js * for (const frameIndex of note) { * // do something with frameIndex... * } * ``` * @group Utility */ *[(_a$2 = Symbol.toStringTag, Symbol.iterator)]() { for (let i = 0; i < this.frameCount; i++) yield i; } /** * Get the pixels for a given frame layer, as palette indices * * :::tip * Layer indices are not guaranteed to be sorted by 3D depth in KWZs, use [getFrameLayerOrder](/api/classes/ppmparser/#getframelayerorder) to get the correct sort order first. * ::: * * :::tip * If the visibility flag for this layer is turned off, the result will be empty * ::: * * @group Image */ getLayerPixels(frameIndex, layerIndex, imageBuffer = new Uint8Array(this.imageWidth * this.imageHeight), depthStrength = 0, depthEye = exports.FlipnoteStereoscopicEye.Left) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); assertRange(layerIndex, 0, this.numLayers - 1, 'Layer index'); // palette const palette = this.getFramePaletteIndices(frameIndex); const palettePtr = layerIndex * this.numLayerColors; // raw pixels const layers = this.decodeFrame(frameIndex); const layerBuffer = layers[layerIndex]; const depth = Math.floor(this.getFrameLayerDepths(frameIndex)[layerIndex] * depthStrength); const depthShift = ((depthEye == exports.FlipnoteStereoscopicEye.Left) ? -depth : depth); // image dimensions and crop const srcStride = this.srcWidth; const dstStride = this.imageWidth; const width = this.imageWidth; const height = this.imageHeight; const xOffs = this.imageOffsetX; const yOffs = this.imageOffsetY; // clear image buffer before writing imageBuffer.fill(0); // handle layer visibility by returning a blank image if the layer is invisible if (!this.layerVisibility[layerIndex + 1]) return imageBuffer; // convert to palette indices and crop for (let srcY = yOffs, dstY = 0; dstY < height; srcY++, dstY++) { for (let srcX = xOffs, dstX = 0; dstX < width; srcX++, dstX++) { const srcPtr = srcY * srcStride + srcX; const dstPtr = dstY * dstStride + dstX + depthShift; let pixel = layerBuffer[srcPtr]; if (pixel !== 0) imageBuffer[dstPtr] = palette[palettePtr + pixel]; } } return imageBuffer; } /** * Get the pixels for a given frame layer, as RGBA pixels * * :::tip * Layer indices are not guaranteed to be sorted by 3D depth in KWZs, use [getFrameLayerOrder](/api/classes/ppmparser/#getframelayerorder) to get the correct sort order first. * ::: * * :::tip * If the visibility flag for this layer is turned off, the result will be empty * ::: * * @group Image */ getLayerPixelsRgba(frameIndex, layerIndex, imageBuffer = new Uint32Array(this.imageWidth * this.imageHeight), paletteBuffer = new Uint32Array(16), depthStrength = 0, depthEye = exports.FlipnoteStereoscopicEye.Left) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); assertRange(layerIndex, 0, this.numLayers - 1, 'Layer index'); // palette this.getFramePaletteUint32(frameIndex, paletteBuffer); const palettePtr = layerIndex * this.numLayerColors; // raw pixels const layers = this.decodeFrame(frameIndex); const layerBuffer = layers[layerIndex]; // depths const depth = Math.floor(this.getFrameLayerDepths(frameIndex)[layerIndex] * depthStrength); const depthShift = ((depthEye == exports.FlipnoteStereoscopicEye.Left) ? -depth : depth); // image dimensions and crop const srcStride = this.srcWidth; const dstStride = this.imageWidth; const width = this.imageWidth - depth; const height = this.imageHeight; const xOffs = this.imageOffsetX; const yOffs = this.imageOffsetY; // clear image buffer before writing imageBuffer.fill(0); // handle layer visibility by returning a blank image if the layer is invisible if (!this.layerVisibility[layerIndex + 1]) return imageBuffer; // convert to palette indices and crop for (let srcY = yOffs, dstY = 0; dstY < height; srcY++, dstY++) { for (let srcX = xOffs, dstX = 0; dstX < width; srcX++, dstX++) { const srcPtr = srcY * srcStride + srcX; const dstPtr = dstY * dstStride + dstX + depthShift; let pixel = layerBuffer[srcPtr]; if (pixel !== 0) imageBuffer[dstPtr] = paletteBuffer[palettePtr + pixel]; } } return imageBuffer; } /** * Get the image for a given frame, as palette indices * @group Image */ getFramePixels(frameIndex, imageBuffer = new Uint8Array(this.imageWidth * this.imageHeight), depthStrength = 0, depthEye = exports.FlipnoteStereoscopicEye.Left) { // image dimensions and crop const srcStride = this.srcWidth; this.imageWidth; const width = this.imageWidth; const height = this.imageHeight; const xOffs = this.imageOffsetX; const yOffs = this.imageOffsetY; // palette const palette = this.getFramePaletteIndices(frameIndex); // clear framebuffer with paper color imageBuffer.fill(palette[0]); // get layer info + decode into buffers const layerOrder = this.getFrameLayerOrder(frameIndex); const layerDepth = this.getFrameLayerDepths(frameIndex); const layers = this.decodeFrame(frameIndex); // merge layers into framebuffer for (let i = 0; i < this.numLayers; i++) { const layerIndex = layerOrder[i]; const layerBuffer = layers[layerIndex]; const palettePtr = layerIndex * this.numLayerColors; const depth = Math.floor(layerDepth[layerIndex] * depthStrength); const depthShift = ((depthEye == exports.FlipnoteStereoscopicEye.Left) ? -depth : depth); // skip if layer is not visible if (!this.layerVisibility[layerIndex + 1]) continue; // merge layer into rgb buffer for (let srcY = yOffs, dstY = 0; dstY < height; srcY++, dstY++) { for (let srcX = xOffs, dstX = 0; dstX < width; srcX++, dstX++) { const srcPtr = srcY * srcStride + srcX; const dstPtr = dstY * width + dstX + depthShift; let pixel = layerBuffer[srcPtr]; if (pixel !== 0) imageBuffer[dstPtr] = palette[palettePtr + pixel]; } } } return imageBuffer; } /** * Get the image for a given frame as an uint32 array of RGBA pixels * @group Image */ getFramePixelsRgba(frameIndex, imageBuffer = new Uint32Array(this.imageWidth * this.imageHeight), paletteBuffer = new Uint32Array(16), depthStrength = 0, depthEye = exports.FlipnoteStereoscopicEye.Left) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); // image dimensions and crop const srcStride = this.srcWidth; const dstStride = this.imageWidth; const width = this.imageWidth; const height = this.imageHeight; const xOffs = this.imageOffsetX; const yOffs = this.imageOffsetY; // palette this.getFramePaletteUint32(frameIndex, paletteBuffer); // clear framebuffer with paper color imageBuffer.fill(paletteBuffer[0]); // get layer info + decode into buffers const layerOrder = this.getFrameLayerOrder(frameIndex); const layerDepth = this.getFrameLayerDepths(frameIndex); const layers = this.decodeFrame(frameIndex); // merge layers into framebuffer for (let i = 0; i < this.numLayers; i++) { const layerIndex = layerOrder[i]; // skip if layer is not visible if (!this.layerVisibility[layerIndex + 1]) continue; const layerBuffer = layers[layerIndex]; const palettePtr = layerIndex * this.numLayerColors; const depth = Math.floor(layerDepth[layerIndex] * depthStrength); const depthShift = ((depthEye == exports.FlipnoteStereoscopicEye.Left) ? -depth : depth); for (let srcY = yOffs, dstY = 0; srcY < height; srcY++, dstY++) { for (let srcX = xOffs, dstX = 0; srcX < width; srcX++, dstX++) { const srcPtr = srcY * srcStride + srcX; const dstPtr = dstY * dstStride + dstX + depthShift; let pixel = layerBuffer[srcPtr]; if (pixel !== 0) imageBuffer[dstPtr] = paletteBuffer[palettePtr + pixel]; } } } return imageBuffer; } /** * Get the color palette for a given frame, as an uint32 array * @group Image */ getFramePaletteUint32(frameIndex, paletteBuffer = new Uint32Array(16)) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); const colors = this.getFramePalette(frameIndex); paletteBuffer.fill(0); colors.forEach(([r, g, b, a], i) => paletteBuffer[i] = (a << 24) | (b << 16) | (g << 8) | r); return paletteBuffer; } /** * Get the usage flags for a given track across every frame * @returns an array of booleans for every frame, indicating whether the track is used on that frame * @group Audio */ getSoundEffectFlagsForTrack(trackId) { return this.getSoundEffectFlags().map(flags => flags[trackId]); } ; /** * Is a given track used on a given frame * @group Audio */ isSoundEffectUsedOnFrame(trackId, frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); if (!this.soundEffectTracks.includes(trackId)) return false; return this.getFrameSoundEffectFlags(frameIndex)[trackId]; } /** * Does an audio track exist in the Flipnote? * @returns boolean * @group Audio */ hasAudioTrack(trackId) { return this.soundMeta.has(trackId) && this.soundMeta.get(trackId).length > 0; } } /** * Match an FSID from Flipnote Studio * e.g. 1440D700CEF78DA8 * @internal */ const REGEX_PPM_FSID = /^[0159]{1}[0-9A-F]{6}0[0-9A-F]{8}$/; /** * @internal * There are several known exceptions to the standard FSID format, all from Nintendo or Hatena developer/event accounts (mario, zelda 25th, etc). * This list was compiled from data provided by the Flipnote Archive, so it can be considered comprehensive enough to match any Flipnote you may encounter. */ const PPM_FSID_SPECIAL_CASE = [ '01FACA7A4367FC5F', '03D6E959E2F9A42D', '03F80445160587FA', '04068426E1008915', '092A3EC8199FD5D5', '0B8D56BA1BD441B8', '0E61C75C9B5AD90B', '14E494E35A443235' ]; /** * Indicates whether the input is a valid Flipnote Studio user ID */ const isPpmFsid = (fsid) => REGEX_PPM_FSID.test(fsid) || PPM_FSID_SPECIAL_CASE.includes(fsid); /** * Get the region for any valid Flipnote Studio user ID */ const getPpmFsidRegion = (fsid) => { switch (fsid.charAt(0)) { case '0': case '1': return exports.FlipnoteRegion.JPN; case '5': return exports.FlipnoteRegion.USA; case '9': return exports.FlipnoteRegion.EUR; default: return exports.FlipnoteRegion.UNKNOWN; } }; /** * @internal */ const ADPCM_INDEX_TABLE_2BIT = new Int8Array([ -1, 2, -1, 2 ]); /** * @internal */ const ADPCM_INDEX_TABLE_4BIT = new Int8Array([ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ]); /** * @internal */ const ADPCM_STEP_TABLE = new Int16Array([ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767, 0 ]); /** * @internal */ const pcmGetSample = (src, srcSize, srcPtr) => { if (srcPtr < 0 || srcPtr >= srcSize) return 0; return src[srcPtr]; }; /** * Zero-order hold (nearest neighbour) audio interpolation * Credit to SimonTime for the original C version * @internal */ const pcmResampleNearestNeighbour = (src, srcFreq, dstFreq) => { const srcLength = src.length; const srcDuration = srcLength / srcFreq; const dstLength = srcDuration * dstFreq; const dst = new Int16Array(dstLength); const adjFreq = srcFreq / dstFreq; for (let dstPtr = 0; dstPtr < dstLength; dstPtr++) dst[dstPtr] = pcmGetSample(src, srcLength, Math.floor(dstPtr * adjFreq)); return dst; }; /** * Simple linear audio interpolation * @internal */ const pcmResampleLinear = (src, srcFreq, dstFreq) => { const srcLength = src.length; const srcDuration = srcLength / srcFreq; const dstLength = srcDuration * dstFreq; const dst = new Int16Array(dstLength); const adjFreq = srcFreq / dstFreq; for (let dstPtr = 0, adj = 0, srcPtr = 0, weight = 0; dstPtr < dstLength; dstPtr++) { adj = dstPtr * adjFreq; srcPtr = Math.floor(adj); weight = adj % 1; dst[dstPtr] = lerp(pcmGetSample(src, srcLength, srcPtr), pcmGetSample(src, srcLength, srcPtr + 1), weight); } return dst; }; /** * Get a ratio of how many audio samples hit the pcm_s16_le clipping bounds * This can be used to detect corrupted audio * @internal */ const pcmGetClippingRatio = (src) => { const numSamples = src.length; let numClippedSamples = 0; for (let i = 0; i < numSamples; i++) { const sample = src[i]; if (sample <= -32768 || sample >= 32767) numClippedSamples += 1; } return numClippedSamples / numSamples; }; /** * Get the root mean square of a PCM track * @internal */ const pcmGetRms = (src) => { const numSamples = src.length; let rms = 0; for (let i = 0; i < numSamples; i++) { rms += Math.pow(src[i], 2); } return Math.sqrt(rms / numSamples); }; /** * Number of seconds between the UNIX timestamp epoch (jan 1 1970) and the Nintendo timestamp epoch (jan 1 2000). * @internal */ const NINTENDO_UNIX_EPOCH = 946684800; /** * Convert a Nintendo DS or 3DS timestamp integer to a JS Date object. * @internal */ const dateFromNintendoTimestamp = (timestamp) => new Date((timestamp + NINTENDO_UNIX_EPOCH) * 1000); /** * Convert a JS Date to a Nintendo timestamp integer. */ const dateToNintendoTimestamp = (date) => Math.floor((date.getTime() / 1000) - NINTENDO_UNIX_EPOCH); /** * Get the duration (in seconds) of a number of frames running at a specified framerate. * @internal */ const timeGetNoteDuration = (frameCount, framerate) => // multiply and divide by 100 to get around floating precision issues ((frameCount * 100) * (1 / framerate)) / 100; /** * Same SubtleCrypto API is available in browser and node, but in node it isn't global * @internal */ const SUBTLE_CRYPTO = (() => { if (isBrowser || isWebWorker) { const global = getGlobalObject(); return (global.crypto || global.msCrypto).subtle; } else if (isNode) return dynamicRequire(module, 'crypto').webcrypto.subtle; })(); /** * Crypto algo used * @internal */ const ALGORITHM = 'RSASSA-PKCS1-v1_5'; /** * @internal */ const rsaLoadPublicKey = async (pemKey, hashType) => { // remove PEM header and footer const lines = pemKey .split('\n') .filter(line => !line.startsWith('-----') && !line.endsWith('-----')) .join(''); // base64 decode const keyPlaintext = atob(lines); // convert to byte array const keyBytes = new Uint8Array(keyPlaintext.length) .map((_, i) => keyPlaintext.charCodeAt(i)); // create crypto api key return await SUBTLE_CRYPTO.importKey('spki', keyBytes.buffer, { name: ALGORITHM, hash: hashType, }, false, ['verify']); }; /** * @internal */ const rsaVerify = async (key, signature, data) => await SUBTLE_CRYPTO.verify(ALGORITHM, key, signature, data); var _PpmParser_instances, _PpmParser_layerBuffers, _PpmParser_soundFlags, _PpmParser_prevLayerBuffers, _PpmParser_lineEncodingBuffers, _PpmParser_prevDecodedFrame, _PpmParser_frameDataLength, _PpmParser_soundDataLength, _PpmParser_soundDataOffset, _PpmParser_frameOffsets, _PpmParser_decodeHeader, _PpmParser_readFilename, _PpmParser_decodeMeta, _PpmParser_decodeAnimationHeader, _PpmParser_decodeSoundHeader, _PpmParser_isKeyFrame, _PpmParser_pcmAudioMix, _a$1; /** * PPM framerates in frames per second, indexed by the in-app frame speed. * Frame speed 0 is never normally used */ const PPM_FRAMERATES = [0.5, 0.5, 1, 2, 4, 6, 12, 20, 30]; /** * PPM frame color defines (red, green, blue, alpha) */ const PPM_PALETTE = { WHITE: [0xff, 0xff, 0xff, 0xff], BLACK: [0x0e, 0x0e, 0x0e, 0xff], RED: [0xff, 0x2a, 0x2a, 0xff], BLUE: [0x0a, 0x39, 0xff, 0xff] }; /** * @internal * PPM thumbnail color defines (in ABGR order) */ const PPM_THUMB_PALETTE = [ 0xFFFFFFFF, 0xFF525252, 0xFFFFFFFF, 0xFF9C9C9C, 0xFF4448FF, 0xFF4F51C8, 0xFFACADFF, 0xFF00FF00, 0xFFFF4048, 0xFFB84F51, 0xFFFFABAD, 0xFF00FF00, 0xFFB757B6, 0xFF00FF00, 0xFF00FF00, 0xFF00FF00, ]; /** * @internal */ const PPM_FRAME_DATA_MAX_SIZE = 736800; /** * RSA public key used to verify that the PPM file signature is genuine. * * This **cannot** be used to resign Flipnotes, it can only verify that they are valid */ const PPM_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCPLwTL6oSflv+gjywi/sM0TUB 90xqOvuCpjduETjPoN2FwMebxNjdKIqHUyDu4AvrQ6BDJc6gKUbZ1E27BGZoCPH4 9zQRb+zAM6M9EjHwQ6BABr0u2TcF7xGg2uQ9MBWz9AfbVQ91NjfrNWo0f7UPmffv 1VvixmTk1BCtavZxBwIDAQAB -----END PUBLIC KEY-----`; /** * Parser class for (DSiWare) Flipnote Studio's PPM animation format. Format docs: https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format * @group File Parser */ class PpmParser extends BaseParser { static matchBuffer(buffer) { // check the buffer's magic to identify which format it uses const magicBytes = new Uint8Array(buffer.slice(0, 4)); const magic = (magicBytes[0] << 24) | (magicBytes[1] << 16) | (magicBytes[2] << 8) | magicBytes[3]; // check if magic is PARA (ppm magic) return magic === 0x50415241; } /** * Create a new PPM file parser instance * @param arrayBuffer an ArrayBuffer containing file data * @param settings parser settings (none currently implemented) */ constructor(arrayBuffer, settings = {}) { super(arrayBuffer); _PpmParser_instances.add(this); /** * File format type, reflects {@link PpmParser.format}. * @group Meta */ this.format = exports.FlipnoteFormat.PPM; /** * Custom object tag. * @group Utility */ this[_a$1] = 'Flipnote Studio PPM animation file'; /** * Animation frame width, reflects {@link PpmParser.width}. * @group Image */ this.imageWidth = PpmParser.width; /** * Animation frame height, reflects {@link PpmParser.height}. * @group Image */ this.imageHeight = PpmParser.height; /** * Animation frame aspect ratio, reflects {@link PpmParser.aspect}. * @group Image */ this.aspect = PpmParser.aspect; /** * X offset for the top-left corner of the animation frame. * @group Image */ this.imageOffsetX = 0; /** * Y offset for the top-left corner of the animation frame. * @group Image */ this.imageOffsetY = 0; /** * Number of animation frame layers, reflects {@link PpmParser.numLayers}. * @group Image */ this.numLayers = PpmParser.numLayers; /** * Number of colors per layer (aside from transparent), reflects {@link PpmParser.numLayerColors}. * @group Image */ this.numLayerColors = PpmParser.numLayerColors; /** * Key used for Flipnote verification, in PEM format. * @group Verification */ this.publicKey = PpmParser.publicKey; /** * @internal * @group Image */ this.srcWidth = PpmParser.width; /** * Which audio tracks are available in this format, reflects {@link PpmParser.audioTracks}. * @group Audio */ this.audioTracks = PpmParser.audioTracks; /** * Which sound effect tracks are available in this format, reflects {@link PpmParser.soundEffectTracks}. * @group Audio */ this.soundEffectTracks = PpmParser.soundEffectTracks; /** * Audio track base sample rate, reflects {@link PpmParser.rawSampleRate}. * @group Audio */ this.rawSampleRate = PpmParser.rawSampleRate; /** * Audio output sample rate, reflects {@link PpmParser.sampleRate}. * @group Audio */ this.sampleRate = PpmParser.sampleRate; /** * Global animation frame color palette, reflects {@link PpmParser.globalPalette}. * @group Image */ this.globalPalette = PpmParser.globalPalette; _PpmParser_layerBuffers.set(this, void 0); _PpmParser_soundFlags.set(this, void 0); _PpmParser_prevLayerBuffers.set(this, void 0); _PpmParser_lineEncodingBuffers.set(this, void 0); _PpmParser_prevDecodedFrame.set(this, null); _PpmParser_frameDataLength.set(this, void 0); _PpmParser_soundDataLength.set(this, void 0); _PpmParser_soundDataOffset.set(this, void 0); _PpmParser_frameOffsets.set(this, void 0); __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_decodeHeader).call(this); __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_decodeAnimationHeader).call(this); __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_decodeSoundHeader).call(this); // this is always true afaik, it's likely just a remnant from development // doesn't hurt to be accurate though... if (((this.version >> 4) & 0xf) !== 0) { __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_decodeMeta).call(this); } // create image buffers __classPrivateFieldSet(this, _PpmParser_layerBuffers, [ new Uint8Array(PpmParser.width * PpmParser.height), new Uint8Array(PpmParser.width * PpmParser.height) ], "f"); __classPrivateFieldSet(this, _PpmParser_prevLayerBuffers, [ new Uint8Array(PpmParser.width * PpmParser.height), new Uint8Array(PpmParser.width * PpmParser.height) ], "f"); __classPrivateFieldSet(this, _PpmParser_lineEncodingBuffers, [ new Uint8Array(PpmParser.height), new Uint8Array(PpmParser.height) ], "f"); __classPrivateFieldSet(this, _PpmParser_prevDecodedFrame, null, "f"); } /** * Decodes the thumbnail image embedded in the Flipnote. Will return a {@link FlipnoteThumbImage} containing raw RGBA data. * * Note: For most purposes, you should probably just decode the thumbnail frame to get a higher resolution image. * @group Meta */ getThumbnailImage() { this.seek(0xA0); const data = this.readBytes(1536); const pixels = new Uint32Array(64 * 48); let ptr = 0; for (let tileY = 0; tileY < 48; tileY += 8) { for (let tileX = 0; tileX < 64; tileX += 8) { for (let line = 0; line < 8; line += 1) { for (let pixel = 0; pixel < 8; pixel += 2) { const x = tileX + pixel; const y = tileY + line; pixels[y * 64 + x] = PPM_THUMB_PALETTE[data[ptr] & 0xF]; pixels[y * 64 + x + 1] = PPM_THUMB_PALETTE[(data[ptr] << 4) & 0xF]; } } } } return { format: exports.FlipnoteThumbImageFormat.Rgba, width: 64, height: 48, data: pixels.buffer }; } /** * Get the memory meter level for the Flipnote. * This is a value between 0 and 1 indicating how "full" the Flipnote is, based on the size limit of Flipnote Studio. * * Values will never be below 0, but can be above 1 if the Flipnote is larger than the size limit - it is technically possible to exceed the size limit by one frame. * * @group Meta */ getMemoryMeterLevel() { const level = __classPrivateFieldGet(this, _PpmParser_frameDataLength, "f") / PPM_FRAME_DATA_MAX_SIZE; if (level < 0) return 0; // No upper limit; can technically be exceeded by the size of a single frame return level; } /** * Decode a frame, returning the raw pixel buffers for each layer * @group Image */ decodeFrame(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); // return existing layer buffers if no new frame has been decoded since the last call if (__classPrivateFieldGet(this, _PpmParser_prevDecodedFrame, "f") === frameIndex) return __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f"); // if necessary, decode previous frames until a keyframe is reached if (__classPrivateFieldGet(this, _PpmParser_prevDecodedFrame, "f") !== frameIndex - 1 && (!__classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_isKeyFrame).call(this, frameIndex)) && frameIndex !== 0) this.decodeFrame(frameIndex - 1); __classPrivateFieldSet(this, _PpmParser_prevDecodedFrame, frameIndex, "f"); // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#animation-data this.seek(__classPrivateFieldGet(this, _PpmParser_frameOffsets, "f")[frameIndex]); const header = this.readUint8(); const isKeyFrame = (header >> 7) & 0x1; const isTranslated = (header >> 5) & 0x3; // reset current layer buffers __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[0].fill(0); __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[1].fill(0); let translateX = 0; let translateY = 0; if (isTranslated) { translateX = this.readInt8(); translateY = this.readInt8(); } // unpack line encodings for each layer for (let layerIndex = 0; layerIndex < 2; layerIndex++) { const lineEncodingBuffer = __classPrivateFieldGet(this, _PpmParser_lineEncodingBuffers, "f")[layerIndex]; lineEncodingBuffer.fill(0); for (let ptr = 0; ptr < lineEncodingBuffer.length;) { let byte = this.readUint8(); // the 4 lines in this byte are all empty (type 0) - skip if (byte === 0) { ptr += 4; continue; } // unpack 4 line types from the current byte lineEncodingBuffer[ptr++] = byte & 0x03; lineEncodingBuffer[ptr++] = (byte >> 2) & 0x03; lineEncodingBuffer[ptr++] = (byte >> 4) & 0x03; lineEncodingBuffer[ptr++] = (byte >> 6) & 0x03; } } // unpack layer bitmaps for (let layerIndex = 0; layerIndex < 2; layerIndex++) { const pixelBuffer = __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[layerIndex]; const lineEncodingBuffer = __classPrivateFieldGet(this, _PpmParser_lineEncodingBuffers, "f")[layerIndex]; for (let y = 0; y < PpmParser.height; y++) { let pixelBufferPtr = y * PpmParser.width; const lineType = lineEncodingBuffer[y]; switch (lineType) { // line type 0 = blank line, decode nothing case 0: break; // line type 1 = compressed bitmap line case 1: // read lineHeader as a big-endian int var lineHeader = this.readUint32(false); // loop through each bit in the line header // shift line header to the left by 1 bit every iteration, // so on the next loop cycle the next bit will be checked // and if the line header equals 0, no more bits are set, // the rest of the line is empty and can be skipped for (; lineHeader !== 0; lineHeader <<= 1, pixelBufferPtr += 8) { // if the bit is set, this 8-pix wide chunk is stored // else we can just leave it blank and move on to the next chunk if (lineHeader & 0x80000000) { let chunk = this.readUint8(); // unpack chunk bits // the chunk if shifted right 1 bit on every loop // if the chunk equals 0, no more bits are set, // so the rest of the chunk is empty and can be skipped for (let pixel = 0; chunk !== 0; pixel++, chunk >>= 1) pixelBuffer[pixelBufferPtr + pixel] = chunk & 0x1; } } break; // line type 2 = compressed bitmap line like type 1, but all pixels are set to 1 first case 2: // line type 2 starts as an inverted line pixelBuffer.fill(1, pixelBufferPtr, pixelBufferPtr + PpmParser.width); // read lineHeader as a big-endian int var lineHeader = this.readUint32(false); // loop through each bit in the line header // shift line header to the left by 1 bit every iteration, // so on the next loop cycle the next bit will be checked // and if the line header equals 0, no more bits are set, // the rest of the line is empty and can be skipped for (; lineHeader !== 0; lineHeader <<= 1, pixelBufferPtr += 8) { // if the bit is set, this 8-pix wide chunk is stored // else we can just leave it blank and move on to the next chunk if (lineHeader & 0x80000000) { let chunk = this.readUint8(); // unpack chunk bits for (let pixel = 0; pixel < 8; pixel++, chunk >>= 1) pixelBuffer[pixelBufferPtr + pixel] = chunk & 0x1; } } break; // line type 3 = raw bitmap line case 3: for (let chunk = 0, i = 0; i < PpmParser.width; i++) { if (i % 8 === 0) chunk = this.readUint8(); pixelBuffer[pixelBufferPtr++] = chunk & 0x1; chunk >>= 1; } break; } } } // if the current frame is based on changes from the previous one, merge them by XORing their values const layer1 = __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[0]; const layer2 = __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[1]; const layer1Prev = __classPrivateFieldGet(this, _PpmParser_prevLayerBuffers, "f")[0]; const layer2Prev = __classPrivateFieldGet(this, _PpmParser_prevLayerBuffers, "f")[1]; // fast diffing if the frame isn't translated if (!isKeyFrame && translateX === 0 && translateY === 0) { const size = PpmParser.height * PpmParser.width; for (let i = 0; i < size; i++) { layer1[i] ^= layer1Prev[i]; layer2[i] ^= layer2Prev[i]; } } // slower diffing if the frame is translated else if (!isKeyFrame) { const w = PpmParser.width; const h = PpmParser.height; const startX = Math.max(translateX, 0); const startY = Math.max(translateY, 0); const endX = Math.min(w + translateX, w); const endY = Math.min(h + translateY, h); const shift = translateY * w + translateX; let dest, src; // loop through each line for (let y = startY; y < endY; y++) { // loop through each pixel in the line for (let x = startX; x < endX; x++) { dest = y * w + x; src = dest - shift; // diff pixels with a binary XOR layer1[dest] ^= layer1Prev[src]; layer2[dest] ^= layer2Prev[src]; } } } // copy the current layer buffers to the previous ones __classPrivateFieldGet(this, _PpmParser_prevLayerBuffers, "f")[0].set(__classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[0]); __classPrivateFieldGet(this, _PpmParser_prevLayerBuffers, "f")[1].set(__classPrivateFieldGet(this, _PpmParser_layerBuffers, "f")[1]); return __classPrivateFieldGet(this, _PpmParser_layerBuffers, "f"); } /** * Get the color palette indices for a given frame. RGBA colors for these values can be indexed from {@link PpmParser.globalPalette} * Returns an array where: * - index 0 is the paper color index * - index 1 is the layer 1 color index * - index 2 is the layer 2 color index * @group Image */ getFramePaletteIndices(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _PpmParser_frameOffsets, "f")[frameIndex]); const header = this.readUint8(); const isInverted = (header & 0x1) !== 1; const penMap = [ isInverted ? 0 : 1, // pen index 0 isn't used in normal cases isInverted ? 0 : 1, 2, 3, ]; return [ isInverted ? 1 : 0, // paper penMap[(header >> 1) & 0x3], // layer 1 color penMap[(header >> 3) & 0x3], // layer 2 color ]; } /** * Get the RGBA colors for a given frame * * Returns an array where: * - index 0 is the paper color * - index 1 is the layer 1 color * - index 2 is the layer 2 color * @group Image */ getFramePalette(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); const indices = this.getFramePaletteIndices(frameIndex); return indices.map(colorIndex => this.globalPalette[colorIndex]); } /** * Determines if a given frame is a video key frame or not. This returns an array of booleans for each layer, since in the KWZ format, keyframe encoding is done on a per-layer basis. * @param frameIndex * @group Image */ getIsKeyFrame(frameIndex) { const flag = __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_isKeyFrame).call(this, frameIndex) === 1; return [flag, flag]; } /** * Get the 3D depths for each layer in a given frame. The PPM format doesn't actually store this information, so `0` is returned for both layers. This method is only here for consistency with KWZ. * @param frameIndex * @group Image */ getFrameLayerDepths(frameIndex) { return [0, 0]; } /** * Get the FSID for a given frame's original author. The PPM format doesn't actually store this information, so the current author FSID is returned. This method is only here for consistency with KWZ. * @param frameIndex * @group Meta */ getFrameAuthor(frameIndex) { return this.meta.current.fsid; } /** * Get the camera flags for a given frame. The PPM format doesn't actually store this information so `false` will be returned for both layers. This method is only here for consistency with KWZ. * @group Image * @returns Array of booleans, indicating whether each layer uses a photo or not */ getFrameCameraFlags(frameIndex) { return [false, false]; } /** * Get the layer draw order for a given frame * @group Image * @returns Array of layer indices, in the order they should be drawn */ getFrameLayerOrder(frameIndex) { return [1, 0]; } /** * Get the sound effect flags for every frame in the Flipnote * @group Audio */ decodeSoundFlags() { if (__classPrivateFieldGet(this, _PpmParser_soundFlags, "f") !== undefined) return __classPrivateFieldGet(this, _PpmParser_soundFlags, "f"); assert(0x06A0 + __classPrivateFieldGet(this, _PpmParser_frameDataLength, "f") < this.numBytes); // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#sound-effect-flags this.seek(0x06A0 + __classPrivateFieldGet(this, _PpmParser_frameDataLength, "f")); const numFlags = this.frameCount; const flags = this.readBytes(numFlags); __classPrivateFieldSet(this, _PpmParser_soundFlags, new Array(numFlags), "f"); for (let i = 0; i < numFlags; i++) { const byte = flags[i]; __classPrivateFieldGet(this, _PpmParser_soundFlags, "f")[i] = [ (byte & 0x1) !== 0, // SE1 bitflag (byte & 0x2) !== 0, // SE2 bitflag (byte & 0x4) !== 0, // SE3 bitflag ]; } return __classPrivateFieldGet(this, _PpmParser_soundFlags, "f"); } /** * Get the sound effect usage flags for every frame * @group Audio */ getSoundEffectFlags() { return this.decodeSoundFlags().map(frameFlags => ({ [exports.FlipnoteSoundEffectTrack.SE1]: frameFlags[0], [exports.FlipnoteSoundEffectTrack.SE2]: frameFlags[1], [exports.FlipnoteSoundEffectTrack.SE3]: frameFlags[2], [exports.FlipnoteSoundEffectTrack.SE4]: false })); } /** * Get the sound effect usage flags for a given frame * @group Audio */ getFrameSoundEffectFlags(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(0x06A0 + __classPrivateFieldGet(this, _PpmParser_frameDataLength, "f") + frameIndex); const byte = this.readUint8(); return { [exports.FlipnoteSoundEffectTrack.SE1]: (byte & 0x1) !== 0, [exports.FlipnoteSoundEffectTrack.SE2]: (byte & 0x2) !== 0, [exports.FlipnoteSoundEffectTrack.SE3]: (byte & 0x4) !== 0, [exports.FlipnoteSoundEffectTrack.SE4]: false }; } /** * Get the duration of a given track in seconds * @returns number * @group Audio */ getAudioTrackDuration(trackId) { const trackMeta = this.soundMeta.get(trackId); if (trackMeta.length === 0) return 0; if (trackId === exports.FlipnoteAudioTrack.BGM) { const bgmAdjust = (1 / this.bgmrate) / (1 / this.framerate); const freq = this.rawSampleRate * bgmAdjust; return ((trackMeta.length - 4) * 2) / freq; } return ((trackMeta.length - 4) * 2) / this.sampleRate; } /** * Get the raw compressed audio data for a given track * @returns byte array * @group Audio */ getAudioTrackRaw(trackId) { const trackMeta = this.soundMeta.get(trackId); assert(trackMeta.ptr + trackMeta.length < this.numBytes); this.seek(trackMeta.ptr); return this.readBytes(trackMeta.length); } /** * Get the decoded audio data for a given track, using the track's native samplerate * @returns Signed 16-bit PCM audio * @group Audio */ decodeAudioTrack(trackId) { // note this doesn't resample // decode a 4 bit IMA adpcm audio track // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#sound-data const trackMeta = this.soundMeta.get(trackId); assert(trackMeta.ptr + trackMeta.length < this.numBytes); this.seek(trackMeta.ptr); let srcPtr = 0; let dstPtr = 0; let sample = 0; let predictor = this.readInt16(); let stepIndex = this.readUint8(); this.readUint8(); const srcSize = trackMeta.length - 4; const src = this.readBytes(srcSize); const dst = new Int16Array(srcSize * 2); let lowNibble = true; while (srcPtr < srcSize) { // switch between high and low nibble each loop iteration // increments srcPtr after every high nibble if (lowNibble) sample = src[srcPtr] & 0xF; else sample = src[srcPtr++] >> 4; lowNibble = !lowNibble; const step = ADPCM_STEP_TABLE[stepIndex]; let diff = step >> 3; if (sample & 1) diff += step >> 2; if (sample & 2) diff += step >> 1; if (sample & 4) diff += step; if (sample & 8) diff = -diff; predictor += diff; predictor = clamp(predictor, -32768, 32767); stepIndex += ADPCM_INDEX_TABLE_4BIT[sample]; stepIndex = clamp(stepIndex, 0, 88); dst[dstPtr++] = predictor; } return dst; } /** * Get the decoded audio data for a given track, using the specified samplerate * @returns Signed 16-bit PCM audio * @group Audio */ getAudioTrackPcm(trackId, dstFreq = this.sampleRate) { const srcPcm = this.decodeAudioTrack(trackId); let srcFreq = this.rawSampleRate; if (trackId === exports.FlipnoteAudioTrack.BGM) { const bgmAdjust = (1 / this.bgmrate) / (1 / this.framerate); srcFreq = this.rawSampleRate * bgmAdjust; } if (srcFreq !== dstFreq) return pcmResampleNearestNeighbour(srcPcm, srcFreq, dstFreq); return srcPcm; } /** * Get the full mixed audio for the Flipnote, using the specified samplerate * @returns Signed 16-bit PCM audio * @group Audio */ getAudioMasterPcm(dstFreq = this.sampleRate) { const dstSize = Math.ceil(this.duration * dstFreq); const master = new Int16Array(dstSize); const hasBgm = this.hasAudioTrack(exports.FlipnoteAudioTrack.BGM); const hasSe1 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE1); const hasSe2 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE2); const hasSe3 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE3); // Mix background music if (hasBgm) { const bgmPcm = this.getAudioTrackPcm(exports.FlipnoteAudioTrack.BGM, dstFreq); __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_pcmAudioMix).call(this, bgmPcm, master, 0); } // Mix sound effects if (hasSe1 || hasSe2 || hasSe3) { const samplesPerFrame = dstFreq / this.framerate; const se1Pcm = hasSe1 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE1, dstFreq) : null; const se2Pcm = hasSe2 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE2, dstFreq) : null; const se3Pcm = hasSe3 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE3, dstFreq) : null; const seFlags = this.decodeSoundFlags(); for (let frame = 0; frame < this.frameCount; frame++) { const seOffset = Math.ceil(frame * samplesPerFrame); const flag = seFlags[frame]; if (hasSe1 && flag[0]) __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_pcmAudioMix).call(this, se1Pcm, master, seOffset); if (hasSe2 && flag[1]) __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_pcmAudioMix).call(this, se2Pcm, master, seOffset); if (hasSe3 && flag[2]) __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_pcmAudioMix).call(this, se3Pcm, master, seOffset); } } this.audioClipRatio = pcmGetClippingRatio(master); return master; } /** * Get the body of the Flipnote - the data that is digested for the signature * @group Verification */ getBody() { const bodyEnd = __classPrivateFieldGet(this, _PpmParser_soundDataOffset, "f") + __classPrivateFieldGet(this, _PpmParser_soundDataLength, "f") + 32; return this.bytes.subarray(0, bodyEnd); } /** * Get the Flipnote's signature data * @group Verification */ getSignature() { const bodyEnd = __classPrivateFieldGet(this, _PpmParser_soundDataOffset, "f") + __classPrivateFieldGet(this, _PpmParser_soundDataLength, "f") + 32; return this.bytes.subarray(bodyEnd, bodyEnd + 128); } /** * Verify whether this Flipnote's signature is valid * @group Verification */ async verify() { const key = await rsaLoadPublicKey(PPM_PUBLIC_KEY, 'SHA-1'); return await rsaVerify(key, this.getSignature(), this.getBody()); } } _PpmParser_layerBuffers = new WeakMap(), _PpmParser_soundFlags = new WeakMap(), _PpmParser_prevLayerBuffers = new WeakMap(), _PpmParser_lineEncodingBuffers = new WeakMap(), _PpmParser_prevDecodedFrame = new WeakMap(), _PpmParser_frameDataLength = new WeakMap(), _PpmParser_soundDataLength = new WeakMap(), _PpmParser_soundDataOffset = new WeakMap(), _PpmParser_frameOffsets = new WeakMap(), _PpmParser_instances = new WeakSet(), _a$1 = Symbol.toStringTag, _PpmParser_decodeHeader = function _PpmParser_decodeHeader() { assert(16 < this.numBytes); this.seek(4); // decode header // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#header __classPrivateFieldSet(this, _PpmParser_frameDataLength, this.readUint32(), "f"); __classPrivateFieldSet(this, _PpmParser_soundDataLength, this.readUint32(), "f"); this.frameCount = this.readUint16() + 1; this.version = this.readUint16(); // sound data offset = frame data offset + frame data length + sound effect flags let soundDataOffset = 0x06A0 + __classPrivateFieldGet(this, _PpmParser_frameDataLength, "f") + this.frameCount; if (soundDataOffset % 4 !== 0) soundDataOffset += 4 - (soundDataOffset % 4); assert(soundDataOffset < this.numBytes); __classPrivateFieldSet(this, _PpmParser_soundDataOffset, soundDataOffset, "f"); }, _PpmParser_readFilename = function _PpmParser_readFilename() { const mac = this.readHex(3); const random = this.readChars(13); const edits = this.readUint16().toString().padStart(3, '0'); return `${mac}_${random}_${edits}`; }, _PpmParser_decodeMeta = function _PpmParser_decodeMeta() { // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#metadata assert(0x06A8 < this.numBytes); this.seek(0x10); const lock = this.readUint16(); const thumbIndex = this.readInt16(); const rootAuthorName = this.readWideChars(11); const parentAuthorName = this.readWideChars(11); const currentAuthorName = this.readWideChars(11); const parentAuthorId = this.readHex(8, true); const currentAuthorId = this.readHex(8, true); const parentFilename = __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_readFilename).call(this); const currentFilename = __classPrivateFieldGet(this, _PpmParser_instances, "m", _PpmParser_readFilename).call(this); const rootAuthorId = this.readHex(8, true); this.seek(0x9A); const timestamp = dateFromNintendoTimestamp(this.readInt32()); this.seek(0x06A6); const flags = this.readUint16(); this.thumbFrameIndex = thumbIndex; this.layerVisibility = { 1: (flags & 0x10) === 0, 2: (flags & 0x20) === 0, 3: false }; this.isSpinoff = (currentAuthorId !== parentAuthorId) || (currentAuthorId !== rootAuthorId); this.meta = { lock: lock === 1, loop: (flags >> 1 & 0x1) === 1, advancedTools: undefined, isSpinoff: this.isSpinoff, frameCount: this.frameCount, frameSpeed: this.frameSpeed, bgmSpeed: this.bgmSpeed, duration: this.duration, thumbIndex: thumbIndex, timestamp: timestamp, root: { username: rootAuthorName, fsid: rootAuthorId, region: getPpmFsidRegion(rootAuthorId), filename: null }, parent: { username: parentAuthorName, fsid: parentAuthorId, region: getPpmFsidRegion(parentAuthorId), filename: parentFilename }, current: { username: currentAuthorName, fsid: currentAuthorId, region: getPpmFsidRegion(currentAuthorId), filename: currentFilename }, }; }, _PpmParser_decodeAnimationHeader = function _PpmParser_decodeAnimationHeader() { // jump to the start of the animation data section // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#animation-header this.seek(0x06A0); const offsetTableLength = this.readUint16(); const numOffsets = offsetTableLength / 4; assert(numOffsets <= this.frameCount); // skip padding + flags this.seek(0x06A8); // read frame offsets and build them into a table const frameOffsets = new Uint32Array(numOffsets); for (let n = 0; n < numOffsets; n++) { const ptr = 0x06A8 + offsetTableLength + this.readUint32(); assert(ptr < this.numBytes, `Frame ${n} pointer is out of bounds`); frameOffsets[n] = ptr; } __classPrivateFieldSet(this, _PpmParser_frameOffsets, frameOffsets, "f"); }, _PpmParser_decodeSoundHeader = function _PpmParser_decodeSoundHeader() { // https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/PPM-format#sound-header let ptr = __classPrivateFieldGet(this, _PpmParser_soundDataOffset, "f"); this.seek(ptr); const bgmLen = this.readUint32(); const se1Len = this.readUint32(); const se2Len = this.readUint32(); const se3Len = this.readUint32(); this.frameSpeed = 8 - this.readUint8(); this.bgmSpeed = 8 - this.readUint8(); assert(this.frameSpeed <= 8 && this.bgmSpeed <= 8); ptr += 32; this.framerate = PPM_FRAMERATES[this.frameSpeed]; this.duration = timeGetNoteDuration(this.frameCount, this.framerate); this.bgmrate = PPM_FRAMERATES[this.bgmSpeed]; const soundMeta = new Map(); soundMeta.set(exports.FlipnoteAudioTrack.BGM, { ptr: ptr, length: bgmLen }); soundMeta.set(exports.FlipnoteAudioTrack.SE1, { ptr: ptr += bgmLen, length: se1Len }); soundMeta.set(exports.FlipnoteAudioTrack.SE2, { ptr: ptr += se1Len, length: se2Len }); soundMeta.set(exports.FlipnoteAudioTrack.SE3, { ptr: ptr += se2Len, length: se3Len }); this.soundMeta = soundMeta; }, _PpmParser_isKeyFrame = function _PpmParser_isKeyFrame(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _PpmParser_frameOffsets, "f")[frameIndex]); const header = this.readUint8(); return (header >> 7) & 0x1; }, _PpmParser_pcmAudioMix = function _PpmParser_pcmAudioMix(src, dst, dstOffset = 0) { const srcSize = src.length; const dstSize = dst.length; for (let n = 0; n < srcSize; n++) { if (dstOffset + n > dstSize) break; // half src volume const samp = dst[dstOffset + n] + (src[n] / 2); dst[dstOffset + n] = clamp(samp, -32768, 32767); } }; /** * Default PPM parser settings. */ PpmParser.defaultSettings = {}; /** * File format type. */ PpmParser.format = exports.FlipnoteFormat.PPM; /** * Animation frame width. */ PpmParser.width = 256; /** * Animation frame height. */ PpmParser.height = 192; /** * Animation frame aspect ratio. */ PpmParser.aspect = 3 / 4; /** * Number of animation frame layers. */ PpmParser.numLayers = 2; /** * Number of colors per layer (aside from transparent). */ PpmParser.numLayerColors = 1; /** * Audio track base sample rate. */ PpmParser.rawSampleRate = 8180; /** * Nintendo DSi audio output rate. */ PpmParser.sampleRate = 32768; // Maybe actually more like 32720; /** * Which audio tracks are available in this format. */ PpmParser.audioTracks = [ exports.FlipnoteAudioTrack.BGM, exports.FlipnoteAudioTrack.SE1, exports.FlipnoteAudioTrack.SE2, exports.FlipnoteAudioTrack.SE3 ]; /** * Which sound effect tracks are available in this format. */ PpmParser.soundEffectTracks = [ exports.FlipnoteSoundEffectTrack.SE1, exports.FlipnoteSoundEffectTrack.SE2, exports.FlipnoteSoundEffectTrack.SE3, ]; /** * Global animation frame color palette. */ PpmParser.globalPalette = [ PPM_PALETTE.WHITE, PPM_PALETTE.BLACK, PPM_PALETTE.RED, PPM_PALETTE.BLUE ]; /** * Public key used for Flipnote verification, in PEM format. */ PpmParser.publicKey = PPM_PUBLIC_KEY; /** * Match an FSID from Flipnote Studio 3D * e.g. 003f-0b7e-82a6-fe0bda * @internal */ const REGEX_KWZ_FSID = /^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{6}$/; /** * Match an FSID from a DSi Library note (PPM to KWZ conversion) * e.g. 10b8-b909-5180-9b2013 * @internal */ const REGEX_KWZ_DSI_LIBRARY_FSID = /^(00|10|12|14)[0-9a-f]{2}-[0-9a-f]{4}-[0-9a-f]{3}0-[0-9a-f]{4}[0159]{1}[0-9a-f]{1}$/; /** * KWZ equivalents of PPM_FSID_SPECIAL_CASE * @internal */ const KWZ_DSI_LIBRARY_FSID_SPECIAL_CASE_SUFFIX = [ '5f-fc67-437a-cafa01', '2d-a4f9-e259-e9d603', 'fa-8705-1645-04f803', '15-8900-e126-840604', 'd5-d59f-19c8-3e2a09', 'b8-41d4-1bba-568d0b', '0b-d95a-9b5c-c7610e', '35-3244-5ae3-94e414', ]; /** * Indicates whether the input is a valid Flipnote Studio 3D user ID */ const isKwzFsid = (fsid) => REGEX_KWZ_FSID.test(fsid); /** * Indicates whether the input is a valid DSi Library user ID */ const isKwzDsiLibraryFsid = (fsid) => { if (REGEX_KWZ_DSI_LIBRARY_FSID.test(fsid)) return true; for (let suffix of KWZ_DSI_LIBRARY_FSID_SPECIAL_CASE_SUFFIX) { if (fsid.endsWith(suffix)) return true; } return false; }; /** * Get the region for any valid Flipnote Studio 3D user ID. * * :::tip * This may be incorrect for IDs that are not from the DSi Library. * ::: */ const getKwzFsidRegion = (fsid) => { if (isKwzDsiLibraryFsid(fsid)) { switch (fsid.charAt(19)) { case '0': case '1': return exports.FlipnoteRegion.JPN; case '5': return exports.FlipnoteRegion.USA; case '9': return exports.FlipnoteRegion.EUR; default: return exports.FlipnoteRegion.UNKNOWN; } } return exports.FlipnoteRegion.UNKNOWN; }; /** * Format a hex string with dashes, to match the format used to display Flipnote Studio IDs in the app. */ const kwzFsidFormat = (hex) => { return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 18)}`.toLowerCase(); }; /** * Unformat a Flipnote Studio ID string back into regular hex. */ const kwzFsidUnformat = (fsid) => { return fsid.replace(/-/g, '').toUpperCase(); }; var _KwzParser_instances, _KwzParser_settings, _KwzParser_sectionMap, _KwzParser_bodyEndOffset, _KwzParser_layerBuffers, _KwzParser_soundFlags, _KwzParser_prevDecodedFrame, _KwzParser_frameMetaOffsets, _KwzParser_frameDataOffsets, _KwzParser_frameLayerSizes, _KwzParser_frameDataTotalSize, _KwzParser_bitIndex, _KwzParser_bitValue, _KwzParser_buildSectionMap, _KwzParser_readBits, _KwzParser_readFsid, _KwzParser_readFilename, _KwzParser_decodeMeta, _KwzParser_decodeMetaQuick, _KwzParser_getFrameOffsets, _KwzParser_decodeSoundHeader, _a; /** * KWZ framerates in frames per second, indexed by the in-app frame speed */ const KWZ_FRAMERATES = [.2, .5, 1, 2, 4, 6, 8, 12, 20, 24, 30]; /** * KWZ color defines (red, green, blue, alpha) */ const KWZ_PALETTE = { WHITE: [0xff, 0xff, 0xff, 0xff], BLACK: [0x10, 0x10, 0x10, 0xff], RED: [0xff, 0x10, 0x10, 0xff], YELLOW: [0xff, 0xe7, 0x00, 0xff], GREEN: [0x00, 0x86, 0x31, 0xff], BLUE: [0x00, 0x38, 0xce, 0xff], NONE: [0xff, 0xff, 0xff, 0x00] }; /** * RSA public key used to verify that the KWZ file signature is genuine. * * This **cannot** be used to resign Flipnotes, it can only verify that they are valid */ const KWZ_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuv+zHAXXvbbtRqxADDeJ ArX2b9RMxj3T+qpRg3FnIE/jeU3tj7eoDzsMduY+D/UT9CSnP+QHYY/vf0n5lqX9 s6ljoZAmyUuruyj1e5Bg+fkDEu/yPEPQjqhbyywCyYL4TEAOJveopUBx9fdQxUJ6 J4J5oCE/Im1kFrlGW+puARiHmt3mmUyNzO8bI/Jx3cGSfoOHJG1foEaQsI5aaKqA pBqxtzvwqMhudcZtAWSyRMBMlndvkRnVTDNTfTXLOYdHShCIgnKULCTH87uLBIP/ nsmr4/bnQz8q2rp/HyVO+0yjR6mVr0NX5APJQ+6riJmGg3t3VOldhKP7aTHDUW+h kQIDAQAB -----END PUBLIC KEY-----`; /** * Pre computed bitmasks for readBits; done as a slight optimization * @internal */ const BITMASKS = new Uint16Array(16); for (let i = 0; i < 16; i++) { BITMASKS[i] = (1 << i) - 1; } /** * Every possible sequence of pixels for each 8-pixel line * @internal */ const KWZ_LINE_TABLE = new Uint8Array(6561 * 8); /** * Same lines as KWZ_LINE_TABLE, but the pixels are shift-rotated to the left by one place * @internal */ const KWZ_LINE_TABLE_SHIFT = new Uint8Array(6561 * 8); /** * @internal */ var offset = 0; for (let a = 0; a < 3; a++) for (let b = 0; b < 3; b++) for (let c = 0; c < 3; c++) for (let d = 0; d < 3; d++) for (let e = 0; e < 3; e++) for (let f = 0; f < 3; f++) for (let g = 0; g < 3; g++) for (let h = 0; h < 3; h++) { KWZ_LINE_TABLE.set([b, a, d, c, f, e, h, g], offset); KWZ_LINE_TABLE_SHIFT.set([a, d, c, f, e, h, g, b], offset); offset += 8; } /** * Commonly used lines - represents lines where all the pixels are empty, full, * or include a pattern produced by the paint tool, etc * @internal */ const KWZ_LINE_TABLE_COMMON = new Uint8Array(32 * 8); /** * Same lines as common line table, but shift-rotates one place to the left * @internal */ const KWZ_LINE_TABLE_COMMON_SHIFT = new Uint8Array(32 * 8); [ 0x0000, 0x0CD0, 0x19A0, 0x02D9, 0x088B, 0x0051, 0x00F3, 0x0009, 0x001B, 0x0001, 0x0003, 0x05B2, 0x1116, 0x00A2, 0x01E6, 0x0012, 0x0036, 0x0002, 0x0006, 0x0B64, 0x08DC, 0x0144, 0x00FC, 0x0024, 0x001C, 0x0004, 0x0334, 0x099C, 0x0668, 0x1338, 0x1004, 0x166C ].forEach((value, i) => { const lineTablePtr = value * 8; const pixels = KWZ_LINE_TABLE.subarray(lineTablePtr, lineTablePtr + 8); const shiftPixels = KWZ_LINE_TABLE_SHIFT.subarray(lineTablePtr, lineTablePtr + 8); KWZ_LINE_TABLE_COMMON.set(pixels, i * 8); KWZ_LINE_TABLE_COMMON_SHIFT.set(shiftPixels, i * 8); }); /** * Parser class for Flipnote Studio 3D's KWZ animation format. KWZ format docs: https://github.com/Flipnote-Collective/flipnote-studio-3d-docs/wiki/KWZ-Format * @group File Parser */ class KwzParser extends BaseParser { static matchBuffer(buffer) { // check the buffer's magic to identify which format it uses const magicBytes = new Uint8Array(buffer.slice(0, 4)); const magic = (magicBytes[0] << 24) | (magicBytes[1] << 16) | (magicBytes[2] << 8); // check if magic is KFH (kwz magic) or KIC (fs3d folder icon) return magic === 0x4B464800 || magic === 0x4B494300; } /** * Create a new KWZ file parser instance * @param arrayBuffer an ArrayBuffer containing file data * @param settings parser settings */ constructor(arrayBuffer, settings = {}) { super(arrayBuffer); _KwzParser_instances.add(this); /** * File format type, reflects {@link KwzParser.format} */ this.format = exports.FlipnoteFormat.KWZ; /** * Custom object tag */ this[_a] = 'Flipnote Studio 3D KWZ animation file'; /** * Animation frame width, reflects {@link KwzParser.width} */ this.imageWidth = KwzParser.width; /** * Animation frame height, reflects {@link KwzParser.height} */ this.imageHeight = KwzParser.height; /** * Animation frame aspect ratio, reflects {@link KwzParser.aspect} */ this.aspect = KwzParser.aspect; /** * X offset for the top-left corner of the animation frame */ this.imageOffsetX = 0; /** * Y offset for the top-left corner of the animation frame */ this.imageOffsetY = 0; /** * Number of animation frame layers, reflects {@link KwzParser.numLayers} */ this.numLayers = KwzParser.numLayers; /** * Number of colors per layer (aside from transparent), reflects {@link KwzParser.numLayerColors} */ this.numLayerColors = KwzParser.numLayerColors; /** * key used for Flipnote verification, in PEM format */ this.publicKey = KwzParser.publicKey; /** * @internal */ this.srcWidth = KwzParser.width; /** * Which audio tracks are available in this format, reflects {@link KwzParser.audioTracks} */ this.audioTracks = KwzParser.audioTracks; /** * Which sound effect tracks are available in this format, reflects {@link KwzParser.soundEffectTracks} */ this.soundEffectTracks = KwzParser.soundEffectTracks; /** * Audio track base sample rate, reflects {@link KwzParser.rawSampleRate} */ this.rawSampleRate = KwzParser.rawSampleRate; /** * Audio output sample rate, reflects {@link KwzParser.sampleRate} */ this.sampleRate = KwzParser.sampleRate; /** * Global animation frame color palette, reflects {@link KwzParser.globalPalette} */ this.globalPalette = KwzParser.globalPalette; _KwzParser_settings.set(this, void 0); _KwzParser_sectionMap.set(this, void 0); _KwzParser_bodyEndOffset.set(this, void 0); _KwzParser_layerBuffers.set(this, void 0); _KwzParser_soundFlags.set(this, void 0); // sound effect flag cache _KwzParser_prevDecodedFrame.set(this, null); // frameMeta: Map; _KwzParser_frameMetaOffsets.set(this, void 0); _KwzParser_frameDataOffsets.set(this, void 0); _KwzParser_frameLayerSizes.set(this, void 0); _KwzParser_frameDataTotalSize.set(this, void 0); _KwzParser_bitIndex.set(this, 0); _KwzParser_bitValue.set(this, 0); __classPrivateFieldSet(this, _KwzParser_settings, { ...KwzParser.defaultSettings, ...settings }, "f"); __classPrivateFieldSet(this, _KwzParser_layerBuffers, [ new Uint8Array(KwzParser.width * KwzParser.height), new Uint8Array(KwzParser.width * KwzParser.height), new Uint8Array(KwzParser.width * KwzParser.height), ], "f"); // skip through the file and read all of the section headers so we can locate them __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_buildSectionMap).call(this); // if the KIC section is present, we're dealing with a folder icon // these are single-frame KWZs without a KFH section for metadata, or a KSN section for sound // while the data for a full frame (320*240) is present, only the top-left 24*24 pixels are used if (__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KIC')) { this.isFolderIcon = true; // icons still use the full 320 * 240 frame size, so we just set up our image crop to deal with that this.imageWidth = 24; this.imageHeight = 24; this.frameCount = 1; this.frameSpeed = 0; this.framerate = KWZ_FRAMERATES[0]; this.thumbFrameIndex = 0; __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_getFrameOffsets).call(this); } // if the KSN section is not present, then this is a handwritten comment from the Flipnote Gallery World online service // these are single-frame KWZs, just with no sound else if (!__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KSN')) { this.isComment = true; __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_decodeMeta).call(this); __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_getFrameOffsets).call(this); } // else let's assume this is a regular note else { __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_decodeMeta).call(this); __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_getFrameOffsets).call(this); __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_decodeSoundHeader).call(this); } // apply special optimizations for converted DSi library notes if (__classPrivateFieldGet(this, _KwzParser_settings, "f").dsiLibraryNote) { this.isDsiLibraryNote = true; } // automatically crop out the border around every frame if (__classPrivateFieldGet(this, _KwzParser_settings, "f").borderCrop) { // dsi library notes can be cropped to their original resolution if (this.isDsiLibraryNote) { this.imageOffsetX = 32; this.imageOffsetY = 24; this.imageWidth = 256; this.imageHeight = 192; } // even standard notes have a bit of a border... else if (!this.isFolderIcon) { this.imageOffsetX = 5; this.imageOffsetY = 5; this.imageWidth = 310; this.imageHeight = 230; } } } /** * Decodes the thumbnail image embedded in the Flipnote. Will return a {@link FlipnoteThumbImage} containing JPEG data. * * Note: For most purposes, you should probably just decode the thumbnail frame to get a higher resolution image. * @group Meta */ getThumbnailImage() { assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KTN'), 'KTN section missing - Note that folder icons and comments do not contain thumbnail data'); const ktn = __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KTN'); this.seek(ktn.ptr + 12); const bytes = this.readBytes(ktn.length - 12); return { format: exports.FlipnoteThumbImageFormat.Jpeg, width: 80, height: 64, data: bytes.buffer }; } /** * Get the memory meter level for the Flipnote. * This is a value between 0 and 1 indicating how "full" the Flipnote is, based on the size calculation formula inside Flipnote Studio 3D. * * Values will never be below 0, but can be above 1 if the Flipnote is larger than the size limit - it is technically possible to exceed the size limit by one frame. * * @group Meta */ getMemoryMeterLevel() { // NOTE: Flipnote Studio 3D seems to have a couple of different calculations for the actual memory limit // This is based on the function at 0x002b4224, which gives the level used for the memory bar itself // A slightly different calculation is used when deciding if a new frame can be added, unsure why! assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KMI') && __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KMC')); const sampleRate = this.rawSampleRate; const bytesPerSecond = sampleRate / 2; const bgmMaxSize = bytesPerSecond * 60; const seMaxSize = bytesPerSecond * 2; const audioMaxSize = bgmMaxSize + seMaxSize * 4; const totalSize = __classPrivateFieldGet(this, _KwzParser_frameDataTotalSize, "f"); // The function at 0x0031f258 gives the max size used in this calculation // I'm actually unsure what the (something + 39) is, but maximum possible audio size seems to fit well enough for now :^) const maxSize = 4219447 - ((audioMaxSize + 39) & 0xfffffffc); const level = (totalSize + 57600) / maxSize; return level; } /** * Get the color palette indices for a given frame. RGBA colors for these values can be indexed from {@link KwzParser.globalPalette} * * Returns an array where: * - index 0 is the paper color index * - index 1 is the layer A color 1 index * - index 2 is the layer A color 2 index * - index 3 is the layer B color 1 index * - index 4 is the layer B color 2 index * - index 5 is the layer C color 1 index * - index 6 is the layer C color 2 index * @group Image */ getFramePaletteIndices(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex]); const flags = this.readUint32(); return [ flags & 0xF, (flags >> 8) & 0xF, (flags >> 12) & 0xF, (flags >> 16) & 0xF, (flags >> 20) & 0xF, (flags >> 24) & 0xF, (flags >> 28) & 0xF, ]; } /** * Get the RGBA colors for a given frame * * Returns an array where: * - index 0 is the paper color * - index 1 is the layer A color 1 * - index 2 is the layer A color 2 * - index 3 is the layer B color 1 * - index 4 is the layer B color 2 * - index 5 is the layer C color 1 * - index 6 is the layer C color 2 * @group Image */ getFramePalette(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); const indices = this.getFramePaletteIndices(frameIndex); return indices.map(colorIndex => this.globalPalette[colorIndex]); } getFrameDiffingFlag(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex]); return (this.readUint32() >> 4) & 0x07; } /** * Determines if a given frame is a video key frame or not. This returns an array of booleans for each layer, since keyframe encoding is done on a per-layer basis. * @param frameIndex * @group Image */ getIsKeyFrame(frameIndex) { const flag = this.getFrameDiffingFlag(frameIndex); return [ (flag & 0x1) === 0, (flag & 0x2) === 0, (flag & 0x4) === 0, ]; } /** * Get the 3D depths for each layer in a given frame. * @param frameIndex * @group Image */ getFrameLayerDepths(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex] + 0x14); return [ this.readUint8(), this.readUint8(), this.readUint8() ]; } /** * Get the FSID for a given frame's original author. * @param frameIndex * @group Meta */ getFrameAuthor(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex] + 0xA); return __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFsid).call(this); } /** * Get the camera flags for a given frame * @group Image * @returns Array of booleans, indicating whether each layer uses a photo or not */ getFrameCameraFlags(frameIndex) { this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex] + 0x1A); const cameraFlags = this.readUint8(); return [ (cameraFlags & 0x1) !== 0, (cameraFlags & 0x2) !== 0, (cameraFlags & 0x4) !== 0, ]; } /** * Get the layer draw order for a given frame * @group Image */ getFrameLayerOrder(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); const depths = this.getFrameLayerDepths(frameIndex); return [2, 1, 0].sort((a, b) => depths[b] - depths[a]); } /** * Decode a frame, returning the raw pixel buffers for each layer * @group Image */ decodeFrame(frameIndex, diffingFlag = 0x7, isPrevFrame = false) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); // return existing layer buffers if no new frame has been decoded since the last call if (__classPrivateFieldGet(this, _KwzParser_prevDecodedFrame, "f") === frameIndex) return __classPrivateFieldGet(this, _KwzParser_layerBuffers, "f"); // the prevDecodedFrame check is an optimization for decoding frames in full sequence if (__classPrivateFieldGet(this, _KwzParser_prevDecodedFrame, "f") !== frameIndex - 1 && frameIndex !== 0) { // if this frame is being decoded as a prev frame, then we only want to decode the layers necessary // diffingFlag is negated with ~ so if no layers are diff-based, diffingFlag is 0 if (isPrevFrame) diffingFlag = diffingFlag & ~this.getFrameDiffingFlag(frameIndex + 1); // if diffing flag isn't 0, decode the previous frame before this one if (diffingFlag !== 0) this.decodeFrame(frameIndex - 1, diffingFlag, true); } let framePtr = __classPrivateFieldGet(this, _KwzParser_frameDataOffsets, "f")[frameIndex]; const layerSizes = __classPrivateFieldGet(this, _KwzParser_frameLayerSizes, "f")[frameIndex]; for (let layerIndex = 0; layerIndex < 3; layerIndex++) { // dsi gallery conversions don't use the third layer, so it can be skipped if this is set if (__classPrivateFieldGet(this, _KwzParser_settings, "f").dsiLibraryNote && layerIndex === 3) break; this.seek(framePtr); let layerSize = layerSizes[layerIndex]; framePtr += layerSize; const pixelBuffer = __classPrivateFieldGet(this, _KwzParser_layerBuffers, "f")[layerIndex]; // if the layer is 38 bytes then it hasn't changed at all since the previous frame, so we can skip it if (layerSize === 38) continue; // if this layer doesn't need to be decoded for diffing if (((diffingFlag >> layerIndex) & 0x1) === 0) continue; // reset readbits state __classPrivateFieldSet(this, _KwzParser_bitIndex, 16, "f"); __classPrivateFieldSet(this, _KwzParser_bitValue, 0, "f"); // tile skip counter let skipTileCounter = 0; for (let tileOffsetY = 0; tileOffsetY < 240; tileOffsetY += 128) { for (let tileOffsetX = 0; tileOffsetX < 320; tileOffsetX += 128) { // loop small tiles for (let subTileOffsetY = 0; subTileOffsetY < 128; subTileOffsetY += 8) { const y = tileOffsetY + subTileOffsetY; if (y >= 240) break; for (let subTileOffsetX = 0; subTileOffsetX < 128; subTileOffsetX += 8) { const x = tileOffsetX + subTileOffsetX; if (x >= 320) break; // continue to next tile loop if skipTileCounter is > 0 if (skipTileCounter > 0) { skipTileCounter -= 1; continue; } let pixelBufferPtr = y * KwzParser.width + x; const tileType = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 3); if (tileType === 0) { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5) * 8; const pixels = KWZ_LINE_TABLE_COMMON.subarray(linePtr, linePtr + 8); pixelBuffer.set(pixels, pixelBufferPtr); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); } else if (tileType === 1) { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 13) * 8; const pixels = KWZ_LINE_TABLE.subarray(linePtr, linePtr + 8); pixelBuffer.set(pixels, pixelBufferPtr); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); pixelBuffer.set(pixels, pixelBufferPtr += 320); } else if (tileType === 2) { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5) * 8; const a = KWZ_LINE_TABLE_COMMON.subarray(linePtr, linePtr + 8); const b = KWZ_LINE_TABLE_COMMON_SHIFT.subarray(linePtr, linePtr + 8); pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); } else if (tileType === 3) { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 13) * 8; const a = KWZ_LINE_TABLE.subarray(linePtr, linePtr + 8); const b = KWZ_LINE_TABLE_SHIFT.subarray(linePtr, linePtr + 8); pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); } // most common tile type else if (tileType === 4) { const flags = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 8); for (let mask = 1; mask < 0xFF; mask <<= 1) { if (flags & mask) { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5) * 8; const pixels = KWZ_LINE_TABLE_COMMON.subarray(linePtr, linePtr + 8); pixelBuffer.set(pixels, pixelBufferPtr); } else { const linePtr = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 13) * 8; const pixels = KWZ_LINE_TABLE.subarray(linePtr, linePtr + 8); pixelBuffer.set(pixels, pixelBufferPtr); } pixelBufferPtr += 320; } } else if (tileType === 5) { skipTileCounter = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5); continue; } // type 6 doesnt exist else if (tileType === 7) { let pattern = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 2); let useCommonLines = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 1); let a, b; if (useCommonLines !== 0) { const linePtrA = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5) * 8; const linePtrB = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 5) * 8; a = KWZ_LINE_TABLE_COMMON.subarray(linePtrA, linePtrA + 8); b = KWZ_LINE_TABLE_COMMON.subarray(linePtrB, linePtrB + 8); pattern += 1; } else { const linePtrA = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 13) * 8; const linePtrB = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readBits).call(this, 13) * 8; a = KWZ_LINE_TABLE.subarray(linePtrA, linePtrA + 8); b = KWZ_LINE_TABLE.subarray(linePtrB, linePtrB + 8); } switch (pattern % 4) { case 0: pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); break; case 1: pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); break; case 2: pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); break; case 3: pixelBuffer.set(a, pixelBufferPtr); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); pixelBuffer.set(a, pixelBufferPtr += 320); pixelBuffer.set(b, pixelBufferPtr += 320); break; } } } } } } } __classPrivateFieldSet(this, _KwzParser_prevDecodedFrame, frameIndex, "f"); return __classPrivateFieldGet(this, _KwzParser_layerBuffers, "f"); } decodeFrameSoundFlags(frameIndex) { assertRange(frameIndex, 0, this.frameCount - 1, 'Frame index'); this.seek(__classPrivateFieldGet(this, _KwzParser_frameMetaOffsets, "f")[frameIndex] + 0x17); const soundFlags = this.readUint8(); return [ (soundFlags & 0x1) !== 0, (soundFlags & 0x2) !== 0, (soundFlags & 0x4) !== 0, (soundFlags & 0x8) !== 0, ]; } /** * Get the sound effect flags for every frame in the Flipnote * @group Audio */ decodeSoundFlags() { if (__classPrivateFieldGet(this, _KwzParser_soundFlags, "f") !== undefined) return __classPrivateFieldGet(this, _KwzParser_soundFlags, "f"); __classPrivateFieldSet(this, _KwzParser_soundFlags, new Array(this.frameCount) .fill(false) .map((_, i) => this.decodeFrameSoundFlags(i)), "f"); return __classPrivateFieldGet(this, _KwzParser_soundFlags, "f"); } /** * Get the sound effect usage flags for every frame * @group Audio */ getSoundEffectFlags() { return this.decodeSoundFlags().map((frameFlags) => ({ [exports.FlipnoteSoundEffectTrack.SE1]: frameFlags[0], [exports.FlipnoteSoundEffectTrack.SE2]: frameFlags[1], [exports.FlipnoteSoundEffectTrack.SE3]: frameFlags[2], [exports.FlipnoteSoundEffectTrack.SE4]: frameFlags[3], })); } /** * Get the sound effect usage for a given frame * @param frameIndex * @group Audio */ getFrameSoundEffectFlags(frameIndex) { const frameFlags = this.decodeFrameSoundFlags(frameIndex); return { [exports.FlipnoteSoundEffectTrack.SE1]: frameFlags[0], [exports.FlipnoteSoundEffectTrack.SE2]: frameFlags[1], [exports.FlipnoteSoundEffectTrack.SE3]: frameFlags[2], [exports.FlipnoteSoundEffectTrack.SE4]: frameFlags[3], }; } /** * Get the duration of a given track in seconds * @returns number * @group Audio */ getAudioTrackDuration(trackId) { const trackMeta = this.soundMeta.get(trackId); if (trackMeta.length === 0) return 0; const decoded = this.decodeAudioTrack(trackId); if (trackId === exports.FlipnoteAudioTrack.BGM) { const bgmAdjust = (1 / this.bgmrate) / (1 / this.framerate); const freq = this.rawSampleRate * bgmAdjust; return decoded.length / freq; } return decoded.length / this.sampleRate; } /** * Get the raw compressed audio data for a given track * @returns Byte array * @group Audio */ getAudioTrackRaw(trackId) { const trackMeta = this.soundMeta.get(trackId); assert(trackMeta.ptr + trackMeta.length < this.numBytes); return new Uint8Array(this.buffer, trackMeta.ptr, trackMeta.length); } decodeAdpcm(src, dst, predictor = 0, stepIndex = 40) { const srcSize = src.length; let dstPtr = 0; let sample = 0; let step = 0; let diff = 0; // loop through each byte in the raw adpcm data for (let srcPtr = 0; srcPtr < srcSize; srcPtr++) { let currByte = src[srcPtr]; let currBit = 0; while (currBit < 8) { // 2 bit sample if (stepIndex < 18 || currBit > 4) { sample = currByte & 0x3; step = ADPCM_STEP_TABLE[stepIndex]; diff = step >> 3; if (sample & 1) diff += step; if (sample & 2) diff = -diff; predictor += diff; stepIndex += ADPCM_INDEX_TABLE_2BIT[sample]; currByte >>= 2; currBit += 2; } // 4 bit sample else { sample = currByte & 0xf; step = ADPCM_STEP_TABLE[stepIndex]; diff = step >> 3; if (sample & 1) diff += step >> 2; if (sample & 2) diff += step >> 1; if (sample & 4) diff += step; if (sample & 8) diff = -diff; predictor += diff; stepIndex += ADPCM_INDEX_TABLE_4BIT[sample]; currByte >>= 4; currBit += 4; } stepIndex = clamp(stepIndex, 0, 79); // clamp as 12 bit then scale to 16 predictor = clamp(predictor, -2048, 2047); dst[dstPtr] = predictor * 16; dstPtr += 1; } } return dstPtr; } /** * Get the decoded audio data for a given track, using the track's native samplerate * @returns Signed 16-bit PCM audio * @group Audio */ decodeAudioTrack(trackId) { const settings = __classPrivateFieldGet(this, _KwzParser_settings, "f"); const src = this.getAudioTrackRaw(trackId); const dstSize = this.rawSampleRate * 60; // enough for 60 seconds, the max bgm size const dst = new Int16Array(dstSize); // initial decoder state let predictor = 0; let stepIndex = 40; // Nintendo messed up the initial adpcm state for a bunch of the PPM conversions on DSi Library // they are effectively random, so you can optionally provide your own state values, or let the lib make a best guess if (this.isDsiLibraryNote) { if (trackId === exports.FlipnoteAudioTrack.BGM) { // passing an initial index or predictor value should disable bruteforcing let doGuess = true; // allow manual overrides for default predictor if (settings.initialBgmPredictor !== null) { predictor = settings.initialBgmPredictor; doGuess = false; } // allow manual overrides for default step index if (settings.initialBgmStepIndex !== null) { stepIndex = settings.initialBgmStepIndex; doGuess = false; } // bruteforce step index by finding the lowest track root mean square if (doGuess && settings.guessInitialBgmState) { let bestRms = 0xFFFFFFFF; // arbitrarily large let bestStepIndex = 0; for (stepIndex = 0; stepIndex <= 40; stepIndex++) { const dstPtr = this.decodeAdpcm(src, dst, predictor, stepIndex); const rms = pcmGetRms(dst.subarray(0, dstPtr)); // uses same underlying memory as dst if (rms < bestRms) { bestRms = rms; bestStepIndex = stepIndex; } } stepIndex = bestStepIndex; } } else { const trackIndex = this.soundEffectTracks.indexOf(trackId); // allow manual overrides for default predictor if (Array.isArray(settings.initialSePredictors) && settings.initialSePredictors[trackIndex] !== undefined) predictor = settings.initialSePredictors[trackIndex]; // allow manual overrides for default step index if (Array.isArray(settings.initialSeStepIndices) && settings.initialSeStepIndices[trackIndex] !== undefined) stepIndex = settings.initialSeStepIndices[trackIndex]; } } // decode track const dstPtr = this.decodeAdpcm(src, dst, predictor, stepIndex); // copy part of dst with slice() so dst buffer can be garbage collected return dst.slice(0, dstPtr); } /** * Get the decoded audio data for a given track, using the specified samplerate * @returns Signed 16-bit PCM audio * @group Audio */ getAudioTrackPcm(trackId, dstFreq = this.sampleRate) { const srcPcm = this.decodeAudioTrack(trackId); let srcFreq = this.rawSampleRate; if (trackId === exports.FlipnoteAudioTrack.BGM) { const bgmAdjust = (1 / this.bgmrate) / (1 / this.framerate); srcFreq = this.rawSampleRate * bgmAdjust; } if (srcFreq !== dstFreq) return pcmResampleLinear(srcPcm, srcFreq, dstFreq); return srcPcm; } pcmAudioMix(src, dst, dstOffset = 0) { const srcSize = src.length; const dstSize = dst.length; for (let n = 0; n < srcSize; n++) { if (dstOffset + n > dstSize) break; // half src volume const samp = dst[dstOffset + n] + src[n]; dst[dstOffset + n] = clamp(samp, -32768, 32767); } } /** * Get the full mixed audio for the Flipnote, using the specified samplerate * @returns Signed 16-bit PCM audio * @group Audio */ getAudioMasterPcm(dstFreq = this.sampleRate) { const dstSize = Math.ceil(this.duration * dstFreq); const master = new Int16Array(dstSize); const hasBgm = this.hasAudioTrack(exports.FlipnoteAudioTrack.BGM); const hasSe1 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE1); const hasSe2 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE2); const hasSe3 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE3); const hasSe4 = this.hasAudioTrack(exports.FlipnoteAudioTrack.SE4); // Mix background music if (hasBgm) { const bgmPcm = this.getAudioTrackPcm(exports.FlipnoteAudioTrack.BGM, dstFreq); this.pcmAudioMix(bgmPcm, master, 0); } // Mix sound effects if (hasSe1 || hasSe2 || hasSe3 || hasSe4) { const samplesPerFrame = dstFreq / this.framerate; const se1Pcm = hasSe1 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE1, dstFreq) : null; const se2Pcm = hasSe2 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE2, dstFreq) : null; const se3Pcm = hasSe3 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE3, dstFreq) : null; const se4Pcm = hasSe4 ? this.getAudioTrackPcm(exports.FlipnoteAudioTrack.SE4, dstFreq) : null; const soundEffectFlags = this.decodeSoundFlags(); for (let i = 0; i < this.frameCount; i++) { const seFlags = soundEffectFlags[i]; const seOffset = Math.ceil(i * samplesPerFrame); if (hasSe1 && seFlags[0]) this.pcmAudioMix(se1Pcm, master, seOffset); if (hasSe2 && seFlags[1]) this.pcmAudioMix(se2Pcm, master, seOffset); if (hasSe3 && seFlags[2]) this.pcmAudioMix(se3Pcm, master, seOffset); if (hasSe4 && seFlags[3]) this.pcmAudioMix(se4Pcm, master, seOffset); } } this.audioClipRatio = pcmGetClippingRatio(master); return master; } /** * Get the body of the Flipnote - the data that is digested for the signature * @group Verification */ getBody() { const bodyEnd = __classPrivateFieldGet(this, _KwzParser_bodyEndOffset, "f"); return this.bytes.subarray(0, bodyEnd); } /** * Get the Flipnote's signature data * @group Verification */ getSignature() { const bodyEnd = __classPrivateFieldGet(this, _KwzParser_bodyEndOffset, "f"); return this.bytes.subarray(bodyEnd, bodyEnd + 256); } /** * Verify whether this Flipnote's signature is valid * @group Verification */ async verify() { const key = await rsaLoadPublicKey(KWZ_PUBLIC_KEY, 'SHA-256'); return await rsaVerify(key, this.getSignature(), this.getBody()); } } _KwzParser_settings = new WeakMap(), _KwzParser_sectionMap = new WeakMap(), _KwzParser_bodyEndOffset = new WeakMap(), _KwzParser_layerBuffers = new WeakMap(), _KwzParser_soundFlags = new WeakMap(), _KwzParser_prevDecodedFrame = new WeakMap(), _KwzParser_frameMetaOffsets = new WeakMap(), _KwzParser_frameDataOffsets = new WeakMap(), _KwzParser_frameLayerSizes = new WeakMap(), _KwzParser_frameDataTotalSize = new WeakMap(), _KwzParser_bitIndex = new WeakMap(), _KwzParser_bitValue = new WeakMap(), _KwzParser_instances = new WeakSet(), _a = Symbol.toStringTag, _KwzParser_buildSectionMap = function _KwzParser_buildSectionMap() { const fileSize = this.numBytes - 256; const sectionMap = new Map(); let sectionCount = 0; let ptr = 0; // counting sections should mitigate against one of mrnbayoh's notehax exploits while (ptr < fileSize && sectionCount < 6) { this.seek(ptr); const magic = this.readChars(4).substring(0, 3); const length = this.readUint32(); sectionMap.set(magic, { ptr, length }); ptr += length + 8; sectionCount += 1; } __classPrivateFieldSet(this, _KwzParser_bodyEndOffset, ptr, "f"); __classPrivateFieldSet(this, _KwzParser_sectionMap, sectionMap, "f"); assert(sectionMap.has('KMC') && sectionMap.has('KMI')); }, _KwzParser_readBits = function _KwzParser_readBits(num) { // assert(num < 16); if (__classPrivateFieldGet(this, _KwzParser_bitIndex, "f") + num > 16) { const nextBits = this.readUint16(); __classPrivateFieldSet(this, _KwzParser_bitValue, __classPrivateFieldGet(this, _KwzParser_bitValue, "f") | nextBits << (16 - __classPrivateFieldGet(this, _KwzParser_bitIndex, "f")), "f"); __classPrivateFieldSet(this, _KwzParser_bitIndex, __classPrivateFieldGet(this, _KwzParser_bitIndex, "f") - 16, "f"); } const result = __classPrivateFieldGet(this, _KwzParser_bitValue, "f") & BITMASKS[num]; __classPrivateFieldSet(this, _KwzParser_bitValue, __classPrivateFieldGet(this, _KwzParser_bitValue, "f") >> num, "f"); __classPrivateFieldSet(this, _KwzParser_bitIndex, __classPrivateFieldGet(this, _KwzParser_bitIndex, "f") + num, "f"); return result; }, _KwzParser_readFsid = function _KwzParser_readFsid() { if (__classPrivateFieldGet(this, _KwzParser_settings, "f").dsiLibraryNote) { // format as DSi PPM FSID const hex = this.readHex(10, true); return hex.slice(2, 18); } return kwzFsidFormat(this.readHex(10)); }, _KwzParser_readFilename = function _KwzParser_readFilename() { const ptr = this.pointer; const chars = this.readChars(28); if (chars.length === 28) return chars; // Otherwise, this is likely a DSi Library note, // where sometimes Nintendo's buggy PPM converter includes the original packed PPM filename this.seek(ptr); const mac = this.readHex(3); const random = this.readChars(13); const edits = this.readUint16().toString().padStart(3, '0'); this.seek(ptr + 28); return `${mac}_${random}_${edits}`; }, _KwzParser_decodeMeta = function _KwzParser_decodeMeta() { if (__classPrivateFieldGet(this, _KwzParser_settings, "f").quickMeta) return __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_decodeMetaQuick).call(this); assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KFH')); this.seek(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KFH').ptr + 12); const creationTime = dateFromNintendoTimestamp(this.readUint32()); const modifiedTime = dateFromNintendoTimestamp(this.readUint32()); // const simonTime = this.readUint32(); const rootAuthorId = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFsid).call(this); const parentAuthorId = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFsid).call(this); const currentAuthorId = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFsid).call(this); const rootAuthorName = this.readWideChars(11); const parentAuthorName = this.readWideChars(11); const currentAuthorName = this.readWideChars(11); const rootFilename = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFilename).call(this); const parentFilename = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFilename).call(this); const currentFilename = __classPrivateFieldGet(this, _KwzParser_instances, "m", _KwzParser_readFilename).call(this); const frameCount = this.readUint16(); const thumbIndex = this.readUint16(); const flags = this.readUint16(); const frameSpeed = this.readUint8(); const layerFlags = this.readUint8(); this.isSpinoff = (currentAuthorId !== parentAuthorId) || (currentAuthorId !== rootAuthorId); this.frameCount = frameCount; this.frameSpeed = frameSpeed; this.framerate = KWZ_FRAMERATES[frameSpeed]; this.duration = timeGetNoteDuration(this.frameCount, this.framerate); this.thumbFrameIndex = thumbIndex; this.layerVisibility = { 1: (layerFlags & 0x1) === 0, 2: (layerFlags & 0x2) === 0, 3: (layerFlags & 0x3) === 0, }; // Try to auto-detect whether the current author ID matches a converted PPM ID // if (isKwzDsiLibraryFsid(currentAuthorId)) { // this.isDsiLibraryNote = true; // } this.meta = { lock: (flags & 0x1) !== 0, loop: (flags & 0x2) !== 0, advancedTools: (flags & 0x4) !== 0, isSpinoff: this.isSpinoff, frameCount: frameCount, frameSpeed: frameSpeed, duration: this.duration, thumbIndex: thumbIndex, timestamp: modifiedTime, creationTimestamp: creationTime, root: { username: rootAuthorName, fsid: rootAuthorId, region: getKwzFsidRegion(rootAuthorId), filename: rootFilename, isDsiFilename: rootFilename.length !== 28 }, parent: { username: parentAuthorName, fsid: parentAuthorId, region: getKwzFsidRegion(parentAuthorId), filename: parentFilename, isDsiFilename: parentFilename.length !== 28 }, current: { username: currentAuthorName, fsid: currentAuthorId, region: getKwzFsidRegion(currentAuthorId), filename: currentFilename, isDsiFilename: currentFilename.length !== 28 }, }; }, _KwzParser_decodeMetaQuick = function _KwzParser_decodeMetaQuick() { assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KFH')); this.seek(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KFH').ptr + 0x8 + 0xC4); const frameCount = this.readUint16(); const thumbFrameIndex = this.readUint16(); this.readUint16(); const frameSpeed = this.readUint8(); const layerFlags = this.readUint8(); this.frameCount = frameCount; this.thumbFrameIndex = thumbFrameIndex; this.frameSpeed = frameSpeed; this.framerate = KWZ_FRAMERATES[frameSpeed]; this.duration = timeGetNoteDuration(this.frameCount, this.framerate); this.layerVisibility = { 1: (layerFlags & 0x1) === 0, 2: (layerFlags & 0x2) === 0, 3: (layerFlags & 0x3) === 0, }; }, _KwzParser_getFrameOffsets = function _KwzParser_getFrameOffsets() { assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KMI') && __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KMC')); const numFrames = this.frameCount; const kmiSection = __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KMI'); const kmcSection = __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KMC'); assert(kmiSection.length / 28 >= numFrames); const frameMetaOffsets = new Uint32Array(numFrames); const frameDataOffsets = new Uint32Array(numFrames); const frameLayerSizes = []; let frameMetaPtr = kmiSection.ptr + 8; let frameDataPtr = kmcSection.ptr + 12; let totalSize = 0; for (let frameIndex = 0; frameIndex < numFrames; frameIndex++) { this.seek(frameMetaPtr + 4); const layerASize = this.readUint16(); const layerBSize = this.readUint16(); const layerCSize = this.readUint16(); frameMetaOffsets[frameIndex] = frameMetaPtr; frameDataOffsets[frameIndex] = frameDataPtr; frameMetaPtr += 28; frameDataPtr += layerASize + layerBSize + layerCSize; assert(frameMetaPtr < this.numBytes, `frame${frameIndex} meta pointer out of bounds`); assert(frameDataPtr < this.numBytes, `frame${frameIndex} data pointer out of bounds`); frameLayerSizes.push([layerASize, layerBSize, layerCSize]); totalSize += layerASize + layerBSize + layerCSize; } __classPrivateFieldSet(this, _KwzParser_frameMetaOffsets, frameMetaOffsets, "f"); __classPrivateFieldSet(this, _KwzParser_frameDataOffsets, frameDataOffsets, "f"); __classPrivateFieldSet(this, _KwzParser_frameLayerSizes, frameLayerSizes, "f"); __classPrivateFieldSet(this, _KwzParser_frameDataTotalSize, totalSize, "f"); }, _KwzParser_decodeSoundHeader = function _KwzParser_decodeSoundHeader() { assert(__classPrivateFieldGet(this, _KwzParser_sectionMap, "f").has('KSN')); let ptr = __classPrivateFieldGet(this, _KwzParser_sectionMap, "f").get('KSN').ptr + 8; this.seek(ptr); this.bgmSpeed = this.readUint32(); assert(this.bgmSpeed <= 10); this.bgmrate = KWZ_FRAMERATES[this.bgmSpeed]; const trackSizes = new Uint32Array(this.buffer, ptr + 4, 20); const soundMeta = new Map(); soundMeta.set(exports.FlipnoteAudioTrack.BGM, { ptr: ptr += 28, length: trackSizes[0] }); soundMeta.set(exports.FlipnoteAudioTrack.SE1, { ptr: ptr += trackSizes[0], length: trackSizes[1] }); soundMeta.set(exports.FlipnoteAudioTrack.SE2, { ptr: ptr += trackSizes[1], length: trackSizes[2] }); soundMeta.set(exports.FlipnoteAudioTrack.SE3, { ptr: ptr += trackSizes[2], length: trackSizes[3] }); soundMeta.set(exports.FlipnoteAudioTrack.SE4, { ptr: ptr += trackSizes[3], length: trackSizes[4] }); this.soundMeta = soundMeta; }; /** * Default KWZ parser settings */ KwzParser.defaultSettings = { quickMeta: false, dsiLibraryNote: false, borderCrop: false, guessInitialBgmState: true, initialBgmPredictor: null, initialBgmStepIndex: null, initialSePredictors: null, initialSeStepIndices: null, }; /** * File format type */ KwzParser.format = exports.FlipnoteFormat.KWZ; /** * Animation frame width */ KwzParser.width = 320; /** * Animation frame height */ KwzParser.height = 240; /** * Animation frame aspect ratio */ KwzParser.aspect = 3 / 4; /** * Number of animation frame layers */ KwzParser.numLayers = 3; /** * Number of colors per layer (aside from transparent) */ KwzParser.numLayerColors = 2; /** * Audio track base sample rate */ KwzParser.rawSampleRate = 16364; /** * Audio output sample rate */ KwzParser.sampleRate = 32768; /** * Which audio tracks are available in this format */ KwzParser.audioTracks = [ exports.FlipnoteAudioTrack.BGM, exports.FlipnoteAudioTrack.SE1, exports.FlipnoteAudioTrack.SE2, exports.FlipnoteAudioTrack.SE3, exports.FlipnoteAudioTrack.SE4, ]; /** * Which sound effect tracks are available in this format */ KwzParser.soundEffectTracks = [ exports.FlipnoteSoundEffectTrack.SE1, exports.FlipnoteSoundEffectTrack.SE2, exports.FlipnoteSoundEffectTrack.SE3, exports.FlipnoteSoundEffectTrack.SE4, ]; /** * Global animation frame color palette */ KwzParser.globalPalette = [ KWZ_PALETTE.WHITE, KWZ_PALETTE.BLACK, KWZ_PALETTE.RED, KWZ_PALETTE.YELLOW, KWZ_PALETTE.GREEN, KWZ_PALETTE.BLUE, KWZ_PALETTE.NONE, ]; /** * Public key used for Flipnote verification, in PEM format */ KwzParser.publicKey = KWZ_PUBLIC_KEY; /** * Indicates whether the input is a valid Flipnote Studio or Flipnote Studio 3D user ID. */ const isFsid = (fsid) => isPpmFsid(fsid) || isKwzFsid(fsid); /** * Get the region for any valid Flipnote Studio or Flipnote Studio 3D user ID */ const getFsidRegion = (fsid) => { if (isPpmFsid(fsid)) return getPpmFsidRegion(fsid); else if (isKwzFsid(fsid)) return getKwzFsidRegion(fsid); return exports.FlipnoteRegion.UNKNOWN; }; /** * Convert a PPM Flipnote Studio ID to the format used by KWZ Flipnote Studio IDs (as seen in Nintendo DSi Library Flipnotes). * Will return `null` if the conversion could not be made. * * :::tip * KWZ Flipnote Studio IDs contain an extra two characters at the beginning. * It is not possible to resolve these from a PPM Flipnote Studio ID. * ::: */ const ppmFsidToKwzFsidSuffix = (fsid) => { if (isPpmFsid(fsid)) { const a = fsid.slice(14, 16); const b = fsid.slice(12, 14); const c = fsid.slice(10, 12); const d = fsid.slice(8, 10); const e = fsid.slice(6, 8); const f = fsid.slice(4, 6); const g = fsid.slice(2, 4); const h = fsid.slice(0, 2); return `${a}-${b}${c}-${d}${e}-${f}${g}${h}`.toLocaleLowerCase(); } return null; }; /** * Convert a PPM Flipnote Studio ID to an array of all possible matching KWZ Flipnote Studio IDs (as seen in Nintendo DSi Library Flipnotes). * Will return `null` if the conversion could not be made. */ const ppmFsidToPossibleKwzFsids = (fsid) => { const kwzIdSuffix = ppmFsidToKwzFsidSuffix(fsid); if (kwzIdSuffix) { return [ '00' + kwzIdSuffix, '10' + kwzIdSuffix, '12' + kwzIdSuffix, '14' + kwzIdSuffix, ]; } return null; }; /** * Convert a KWZ Flipnote Studio ID (from a Nintendo DSi Library Flipnote) to the format used by PPM Flipnote Studio IDs. * Will return `null` if the conversion could not be made. */ const kwzFsidToPpmFsid = (fsid) => { if (isKwzDsiLibraryFsid(fsid)) { const a = fsid.slice(19, 21); const b = fsid.slice(17, 19); const c = fsid.slice(15, 17); const d = fsid.slice(12, 14); const e = fsid.slice(10, 12); const f = fsid.slice(7, 9); const g = fsid.slice(5, 7); const h = fsid.slice(2, 4); return `${a}${b}${c}${d}${e}${f}${g}${h}`.toUpperCase(); } return null; }; /** * ## Flipnote Studio IDs * * Flipnote Studio and Flipnote Studio 3D both generate a "unique" ID for the user when they first launch the app. * This ID is then saved into any Flipnotes they create, and was also used as their unique identifier when they went online. * * In both apps, your Flipnote Studio ID can be found in the settings menu. * * This module contains functions for working with those IDs. */ var index$3 = /*#__PURE__*/Object.freeze({ __proto__: null, getFsidRegion: getFsidRegion, getKwzFsidRegion: getKwzFsidRegion, getPpmFsidRegion: getPpmFsidRegion, isFsid: isFsid, isKwzDsiLibraryFsid: isKwzDsiLibraryFsid, isKwzFsid: isKwzFsid, isPpmFsid: isPpmFsid, kwzFsidFormat: kwzFsidFormat, kwzFsidToPpmFsid: kwzFsidToPpmFsid, kwzFsidUnformat: kwzFsidUnformat, ppmFsidToKwzFsidSuffix: ppmFsidToKwzFsidSuffix, ppmFsidToPossibleKwzFsids: ppmFsidToPossibleKwzFsids }); const REGEX_PPM_LOCAL_FILENAME = /^[0-9A-Z]{1}[0-9A-F]{5}_[0-9A-F]{13}_[0-9]{3}$/; const REGEX_PPM_FILENAME = /^[0-9A-F]{6}_[0-9A-F]{13}_[0-9]{3}$/; const CHECKSUM_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; /** * Determines if a string matches the PPM filename format. */ const isPpmFilename = (filename) => REGEX_PPM_LOCAL_FILENAME.test(filename); /** * Does the same thing as {@link isPpmFilename}, expect it only matches "basic" filenames, without the checksum character that is added when saving a Flipnote to the filesystem. */ const isPpmBasicFilename = (filename) => REGEX_PPM_FILENAME.test(filename); /** * */ const ppmFilenameCalculateCheckDigit = (filename) => { let sum = parseInt(filename.slice(0, 2), 16); for (let i = 1; i < 16; i++) { const char = filename.charCodeAt(i); sum = (sum + char) & 0xff; } return CHECKSUM_ALPHABET[sum % 36]; }; /** * */ const ppmFilenameDecode = (filename) => { const macSuffix = filename.slice(0, 6); const random1 = filename.slice(7, 12); const random2 = filename.slice(12, 19); const edits = parseInt(filename.slice(-3)); return { macSuffix, random1, random2, edits }; }; /** * */ const ppmFilenameEncode = (filename) => { const edits = filename.edits.toString().padEnd(3, '0'); return `${filename.macSuffix}_${filename.random1}_${filename.random2}_${edits}`; }; const REGEX_KWZ_FILENAME = /^[0-5a-z]{28}$/; const BASE32_ALPHABET = 'cwmfjordvegbalksnthpyxquiz012345'; const base32Decode = (src) => { const srcSize = src.length; let srcPtr = 0; const dst = new Uint8Array(srcSize * 5 / 8); let dstPtr = 0; let value = 0; for (let i = 0; i < srcSize; i++) { value = (value << 5) | BASE32_ALPHABET.indexOf(src[i]); srcPtr += 5; if (srcPtr >= 8) { dst[dstPtr++] = (value >>> (srcPtr - 8)) & 0xFF; srcPtr -= 8; } } return dst; }; const base32Encode = (src) => { const srcSize = src.byteLength; let srcPtr = 0; let dst = ''; let value = 0; for (let i = 0; i < srcSize; i++) { value = (value << 8) | src[i]; srcPtr += 8; while (srcPtr >= 5) { dst += BASE32_ALPHABET[(value >>> (srcPtr - 5)) & 31]; srcPtr -= 5; } } if (srcPtr > 0) dst += BASE32_ALPHABET[(value << (5 - srcPtr)) & 31]; return dst; }; /** * Determines if a string matches the KWZ filename format. */ const isKwzFilename = (filename) => REGEX_KWZ_FILENAME.test(filename); /** * Decode a KWZ filename into its constituent parts. */ const kwzFilenameDecode = (filename) => { const bytes = base32Decode(filename); const data = new DataView(bytes.buffer); const fsid = kwzFsidFormat(hexFromBytes(bytes.slice(0, 9))); const created = dateFromNintendoTimestamp(data.getUint32(9, true)); const edited = dateFromNintendoTimestamp(data.getUint32(13, true)); return { fsid, created, edited }; }; /** * Encode a KWZ filename from its parts; i.e. do the inverse of {@link kwzFilenameDecode}. */ const kwzFilenameEncode = (filename) => { const bytes = new Uint8Array(17); const data = new DataView(bytes.buffer); const fsid = kwzFsidUnformat(filename.fsid); bytes.set(hexToBytes(fsid)); data.setUint32(9, dateToNintendoTimestamp(filename.created), true); data.setUint32(13, dateToNintendoTimestamp(filename.edited), true); return base32Encode(bytes); }; /** * ## Flipnote Filenames * * Flipnote Studio and Flipnote Studio 3D both autogenerate filenames when Flipnotes are saved. * This module contains functions for testing and working with those Flipnote filenames. */ var index$2 = /*#__PURE__*/Object.freeze({ __proto__: null, isKwzFilename: isKwzFilename, isPpmBasicFilename: isPpmBasicFilename, isPpmFilename: isPpmFilename, kwzFilenameDecode: kwzFilenameDecode, kwzFilenameEncode: kwzFilenameEncode, ppmFilenameCalculateCheckDigit: ppmFilenameCalculateCheckDigit, ppmFilenameDecode: ppmFilenameDecode, ppmFilenameEncode: ppmFilenameEncode }); const XOR_KEY = [ 0xF7, 0x4C, 0x6A, 0x3A, 0xFB, 0x82, 0xA6, 0x37, 0x6E, 0x11, 0x38, 0xCF, 0xA0, 0xDD, 0x85, 0xC0, 0xC7, 0x9B, 0xC4, 0xD8, 0xDD, 0x28, 0x8A, 0x87, 0x53, 0x20, 0xEE, 0xE0, 0x0B, 0xEB, 0x43, 0xA0, 0xDB, 0x55, 0x0F, 0x75, 0x36, 0x37, 0xEB, 0x35, 0x6A, 0x34, 0x7F, 0xB5, 0x0F, 0x99, 0xF7, 0xEF, 0x43, 0x25, 0xCE, 0xA0, 0x29, 0x46, 0xD9, 0xD4, 0x4D, 0xBB, 0x04, 0x66, 0x68, 0x08, 0xF1, 0xF8, ]; class BasePlaylistParser extends DataStream { constructor(buffer) { super(buffer); /** * List of filepaths in the playlist. */ this.entries = []; } addEntry(full) { const parts = full.split('/'); const name = parts.at(-1); const lastDot = full.lastIndexOf('.'); this.entries.push({ full, name, base: full.slice(0, lastDot), ext: full.slice(lastDot + 1), folder: parts.at(-2), parentFolder: parts.at(-3) }); } } /** * Parses .pls and .lst playlist files from Flipnote Studio (DSiWare). * * > This only supports playlists from version 2 of Flipnote Studio. * > Since version 1 was only ever released in Japan (and for a short period of time at that) I didn't bother including support. * * File format documentation is at https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/.pls-and-.lst-files */ class PpmPlaylist extends BasePlaylistParser { constructor(buffer) { super(buffer); this.format = exports.FlipnoteFormat.PPM; // Decrypt const bytes = this.bytes; const size = this.numBytes; for (let i = 0; i < size; i++) bytes[i] = bytes[i] ^ XOR_KEY[i % 64]; // Parse let currPath = ''; while (!this.end()) { const char = this.readChar(); if (char === '\x0A') { this.addEntry(currPath); currPath = ''; continue; } currPath += char; } } } /** * Parses .lst playlist files from Flipnote Studio 3D. * * File format documentation is at https://github.com/Flipnote-Collective/flipnote-studio-3d-docs/wiki/lst-format */ class KwzPlaylist extends BasePlaylistParser { constructor(buffer) { super(buffer); this.format = exports.FlipnoteFormat.KWZ; // Decrypt const bytes = this.bytes; const size = this.numBytes; for (let i = 0; i < size; i++) bytes[i] = bytes[i] ^ XOR_KEY[i % 61]; // Yes, the KWZ playlist format doesn't use the full key length. // Parse let currPath = ''; this.seek(4); const separator = this.readWideChar(); while (!this.end()) { const char = this.readWideChar(); if (char === separator) { this.addEntry(currPath); currPath = ''; continue; } currPath += char; } } } /** * Loader for web url strings (Browser only) * @group Loader */ const urlLoader = { name: 'url', matches(source) { return typeof source === 'string'; }, async load(source) { const response = await fetch(source); assert(response.status >= 200 && response.status < 300, `Failed to load Flipnote from URL, response failed with status ${response.status}`); return await response.arrayBuffer(); } }; /** * Loader for File objects (browser only) * @group Loader */ const fileLoader = { name: 'file', matches(source) { return isBrowser && typeof File !== 'undefined' && typeof FileReader !== 'undefined' && source instanceof File; }, async load(source) { return source.arrayBuffer(); } }; /** * Loader for Blob objects (browser only) * @group Loader */ const blobLoader = { name: 'blob', matches(source) { return isBrowser && typeof Blob !== 'undefined' && typeof Response !== 'undefined' && source instanceof Blob; }, async load(source) { return source.arrayBuffer(); } }; /** * Loader for Buffer objects (Node only) * @group Loader */ const nodeBufferLoader = { name: 'node-buffer', matches(source) { return isNode && (source instanceof Buffer); }, async load(source) { return source.buffer; } }; /** * Loader for ArrayBuffer objects. * @group Loader */ const arrayBufferLoader = { name: 'array-buffer', matches(source) { return source instanceof ArrayBuffer; }, async load(source) { return source; } }; const LOADER_REGISTRY = new Map(); /** * Resolve a source, using the current loaders list. * Returns an ArrayBuffer containing the data loaded from the source. */ const load = (source) => { for (let [name, loader] of LOADER_REGISTRY) { if (!loader.matches(source)) continue; try { return loader.load(source); } catch (e) { err(`Failed to load Flipnote from source, loader "${name}" failed with error ${err}`); } } err('No loader available for source type'); }; /** * List all currently registered loaders, as an object. */ const list = () => Object.fromEntries(LOADER_REGISTRY.entries()); /** * Clear all currently registered loaders. */ const clear = () => LOADER_REGISTRY.clear(); /** * Register a resource loader to use when loading Flipnotes. * A loader should take a source and return an ArrayBuffer. */ const register = (loader) => { LOADER_REGISTRY.set(loader.name, loader); }; register(arrayBufferLoader); register(nodeBufferLoader); register(blobLoader); register(fileLoader); register(urlLoader); var index$1 = /*#__PURE__*/Object.freeze({ __proto__: null, clear: clear, list: list, load: load, register: register }); /** * Parse a playlist file (.pls or .lst) from Flipnote Studio or Flipnote Studio 3D. */ const parse$1 = async (format, source) => { const buffer = await load(source); if (format === exports.FlipnoteFormat.PPM || format === 'ppm') return new PpmPlaylist(buffer); if (format === exports.FlipnoteFormat.KWZ || format === 'kwz') return new KwzPlaylist(buffer); }; var index = /*#__PURE__*/Object.freeze({ __proto__: null, KwzPlaylist: KwzPlaylist, PpmPlaylist: PpmPlaylist, parse: parse$1 }); /** * Load a Flipnote from a given source, returning a promise with a parser object. * It will auto-detect the Flipnote format and return either a {@link PpmParser} or {@link KwzParser} accordingly. * * @param source - Source to load a Flipnote from. This will attempt to use one of the registered {@link loaders} to load the Flipnote. * Depending on the operating environment, the default loader set supports the following sources: * - A string representing a web URL. * - An {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer}. * - A {@link https://developer.mozilla.org/en-US/docs/Web/API/File | File} object (Browser only). * - A {@link https://nodejs.org/api/buffer.html | Buffer} object (NodeJS only). * * @param parserConfig - Config settings to pass to the parser, see {@link FlipnoteParserSettings}. */ const parse = async (source, parserConfig) => { const buffer = await load(source); if (PpmParser.matchBuffer(buffer)) return new PpmParser(buffer, parserConfig); if (KwzParser.matchBuffer(buffer)) return new KwzParser(buffer, parserConfig); err('Could not identify source as a valid Flipnote file'); }; /** * @deprecated Use {@link parse} instead. */ const parseSource = (...args) => { console.warn('parseSource() is deprecated, please use parse() instead'); return parse(...args); }; /** * Player event types */ exports.PlayerEvent = void 0; (function (PlayerEvent) { PlayerEvent["__Any"] = "any"; PlayerEvent["Play"] = "play"; PlayerEvent["Pause"] = "pause"; PlayerEvent["CanPlay"] = "canplay"; PlayerEvent["CanPlayThrough"] = "canplaythrough"; PlayerEvent["SeekStart"] = "seeking"; PlayerEvent["SeekEnd"] = "seeked"; PlayerEvent["Duration"] = "durationchange"; PlayerEvent["Loop"] = "loop"; PlayerEvent["Ended"] = "ended"; PlayerEvent["VolumeChange"] = "volumechange"; PlayerEvent["Progress"] = "progress"; PlayerEvent["TimeUpdate"] = "timeupdate"; PlayerEvent["FrameUpdate"] = "frameupdate"; PlayerEvent["FrameNext"] = "framenext"; PlayerEvent["FramePrev"] = "frameprev"; PlayerEvent["FrameFirst"] = "framefirst"; PlayerEvent["FrameLast"] = "framelast"; PlayerEvent["Ready"] = "ready"; PlayerEvent["Load"] = "load"; PlayerEvent["LoadStart"] = "loadstart"; PlayerEvent["LoadedData"] = "loadeddata"; PlayerEvent["LoadedMeta"] = "loadedmetadata"; PlayerEvent["Emptied"] = "emptied"; PlayerEvent["Close"] = "close"; PlayerEvent["Error"] = "error"; PlayerEvent["Destroy"] = "destroy"; })(exports.PlayerEvent || (exports.PlayerEvent = {})); /** * @internal */ const supportedEvents = [ exports.PlayerEvent.Play, exports.PlayerEvent.Pause, exports.PlayerEvent.CanPlay, exports.PlayerEvent.CanPlayThrough, exports.PlayerEvent.SeekStart, exports.PlayerEvent.SeekEnd, exports.PlayerEvent.Duration, exports.PlayerEvent.Loop, exports.PlayerEvent.Ended, exports.PlayerEvent.VolumeChange, exports.PlayerEvent.Progress, exports.PlayerEvent.TimeUpdate, exports.PlayerEvent.FrameUpdate, exports.PlayerEvent.FrameNext, exports.PlayerEvent.FramePrev, exports.PlayerEvent.FrameFirst, exports.PlayerEvent.FrameLast, exports.PlayerEvent.Ready, exports.PlayerEvent.Load, exports.PlayerEvent.LoadStart, exports.PlayerEvent.LoadedData, exports.PlayerEvent.LoadedMeta, exports.PlayerEvent.Emptied, exports.PlayerEvent.Close, exports.PlayerEvent.Error, ]; /** * @internal */ const createTimeRanges = (ranges) => ({ length: ranges.length, start: (i) => ranges[i][0], end: (i) => ranges[i][1], }); /** * @internal */ const padNumber = (num, strLength) => num.toString().padStart(strLength, '0'); /** * @internal */ const formatTime = (seconds) => { const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${m}:${padNumber(s, 2)}`; }; exports.CanvasStereoscopicMode = void 0; (function (CanvasStereoscopicMode) { CanvasStereoscopicMode[CanvasStereoscopicMode["None"] = 0] = "None"; CanvasStereoscopicMode[CanvasStereoscopicMode["Dual"] = 1] = "Dual"; // not actually supported, sorry! CanvasStereoscopicMode[CanvasStereoscopicMode["Anaglyph"] = 2] = "Anaglyph"; })(exports.CanvasStereoscopicMode || (exports.CanvasStereoscopicMode = {})); /** * @internal */ class CanvasInterface { constructor(parent, width, height, options) { } } /* @license twgl.js 5.5.4 Copyright (c) 2015, Gregg Tavares All Rights Reserved. Available via the MIT license. see: http://github.com/greggman/twgl.js for details */ /* * Copyright 2019 Gregg Tavares * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ /* * Copyright 2019 Gregg Tavares * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ /* DataType */ const BYTE$2 = 0x1400; const UNSIGNED_BYTE$3 = 0x1401; const SHORT$2 = 0x1402; const UNSIGNED_SHORT$3 = 0x1403; const INT$3 = 0x1404; const UNSIGNED_INT$3 = 0x1405; const FLOAT$3 = 0x1406; const UNSIGNED_SHORT_4_4_4_4$1 = 0x8033; const UNSIGNED_SHORT_5_5_5_1$1 = 0x8034; const UNSIGNED_SHORT_5_6_5$1 = 0x8363; const HALF_FLOAT$1 = 0x140B; const UNSIGNED_INT_2_10_10_10_REV$1 = 0x8368; const UNSIGNED_INT_10F_11F_11F_REV$1 = 0x8C3B; const UNSIGNED_INT_5_9_9_9_REV$1 = 0x8C3E; const FLOAT_32_UNSIGNED_INT_24_8_REV$1 = 0x8DAD; const UNSIGNED_INT_24_8$1 = 0x84FA; const glTypeToTypedArray = {}; { const tt = glTypeToTypedArray; tt[BYTE$2] = Int8Array; tt[UNSIGNED_BYTE$3] = Uint8Array; tt[SHORT$2] = Int16Array; tt[UNSIGNED_SHORT$3] = Uint16Array; tt[INT$3] = Int32Array; tt[UNSIGNED_INT$3] = Uint32Array; tt[FLOAT$3] = Float32Array; tt[UNSIGNED_SHORT_4_4_4_4$1] = Uint16Array; tt[UNSIGNED_SHORT_5_5_5_1$1] = Uint16Array; tt[UNSIGNED_SHORT_5_6_5$1] = Uint16Array; tt[HALF_FLOAT$1] = Uint16Array; tt[UNSIGNED_INT_2_10_10_10_REV$1] = Uint32Array; tt[UNSIGNED_INT_10F_11F_11F_REV$1] = Uint32Array; tt[UNSIGNED_INT_5_9_9_9_REV$1] = Uint32Array; tt[FLOAT_32_UNSIGNED_INT_24_8_REV$1] = Uint32Array; tt[UNSIGNED_INT_24_8$1] = Uint32Array; } /** * Get the GL type for a typedArray * @param {ArrayBufferView} typedArray a typedArray * @return {number} the GL type for array. For example pass in an `Int8Array` and `gl.BYTE` will * be returned. Pass in a `Uint32Array` and `gl.UNSIGNED_INT` will be returned * @memberOf module:twgl/typedArray */ function getGLTypeForTypedArray(typedArray) { if (typedArray instanceof Int8Array) { return BYTE$2; } // eslint-disable-line if (typedArray instanceof Uint8Array) { return UNSIGNED_BYTE$3; } // eslint-disable-line if (typedArray instanceof Uint8ClampedArray) { return UNSIGNED_BYTE$3; } // eslint-disable-line if (typedArray instanceof Int16Array) { return SHORT$2; } // eslint-disable-line if (typedArray instanceof Uint16Array) { return UNSIGNED_SHORT$3; } // eslint-disable-line if (typedArray instanceof Int32Array) { return INT$3; } // eslint-disable-line if (typedArray instanceof Uint32Array) { return UNSIGNED_INT$3; } // eslint-disable-line if (typedArray instanceof Float32Array) { return FLOAT$3; } // eslint-disable-line throw new Error('unsupported typed array type'); } /** * Get the GL type for a typedArray type * @param {ArrayBufferView} typedArrayType a typedArray constructor * @return {number} the GL type for type. For example pass in `Int8Array` and `gl.BYTE` will * be returned. Pass in `Uint32Array` and `gl.UNSIGNED_INT` will be returned * @memberOf module:twgl/typedArray */ function getGLTypeForTypedArrayType(typedArrayType) { if (typedArrayType === Int8Array) { return BYTE$2; } // eslint-disable-line if (typedArrayType === Uint8Array) { return UNSIGNED_BYTE$3; } // eslint-disable-line if (typedArrayType === Uint8ClampedArray) { return UNSIGNED_BYTE$3; } // eslint-disable-line if (typedArrayType === Int16Array) { return SHORT$2; } // eslint-disable-line if (typedArrayType === Uint16Array) { return UNSIGNED_SHORT$3; } // eslint-disable-line if (typedArrayType === Int32Array) { return INT$3; } // eslint-disable-line if (typedArrayType === Uint32Array) { return UNSIGNED_INT$3; } // eslint-disable-line if (typedArrayType === Float32Array) { return FLOAT$3; } // eslint-disable-line throw new Error('unsupported typed array type'); } /** * Get the typed array constructor for a given GL type * @param {number} type the GL type. (eg: `gl.UNSIGNED_INT`) * @return {function} the constructor for a the corresponding typed array. (eg. `Uint32Array`). * @memberOf module:twgl/typedArray */ function getTypedArrayTypeForGLType(type) { const CTOR = glTypeToTypedArray[type]; if (!CTOR) { throw new Error('unknown gl type'); } return CTOR; } const isArrayBuffer$1 = typeof SharedArrayBuffer !== 'undefined' ? function isArrayBufferOrSharedArrayBuffer(a) { return a && a.buffer && (a.buffer instanceof ArrayBuffer || a.buffer instanceof SharedArrayBuffer); } : function isArrayBuffer(a) { return a && a.buffer && a.buffer instanceof ArrayBuffer; }; const isTypeWeakMaps = new Map(); function isType(object, type) { if (!object || typeof object !== 'object') { return false; } let weakMap = isTypeWeakMaps.get(type); if (!weakMap) { weakMap = new WeakMap(); isTypeWeakMaps.set(type, weakMap); } let isOfType = weakMap.get(object); if (isOfType === undefined) { const s = Object.prototype.toString.call(object); isOfType = s.substring(8, s.length - 1) === type; weakMap.set(object, isOfType); } return isOfType; } function isBuffer(gl, t) { return typeof WebGLBuffer !== 'undefined' && isType(t, 'WebGLBuffer'); } function isTexture(gl, t) { return typeof WebGLTexture !== 'undefined' && isType(t, 'WebGLTexture'); } /* * Copyright 2019 Gregg Tavares * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ const STATIC_DRAW = 0x88e4; const ARRAY_BUFFER$1 = 0x8892; const ELEMENT_ARRAY_BUFFER$2 = 0x8893; const BUFFER_SIZE = 0x8764; const BYTE$1 = 0x1400; const UNSIGNED_BYTE$2 = 0x1401; const SHORT$1 = 0x1402; const UNSIGNED_SHORT$2 = 0x1403; const INT$2 = 0x1404; const UNSIGNED_INT$2 = 0x1405; const FLOAT$2 = 0x1406; const defaults$2 = { attribPrefix: "", }; function setBufferFromTypedArray(gl, type, buffer, array, drawType) { gl.bindBuffer(type, buffer); gl.bufferData(type, array, drawType || STATIC_DRAW); } /** * Given typed array creates a WebGLBuffer and copies the typed array * into it. * * @param {WebGLRenderingContext} gl A WebGLRenderingContext * @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView|WebGLBuffer} typedArray the typed array. Note: If a WebGLBuffer is passed in it will just be returned. No action will be taken * @param {number} [type] the GL bind type for the buffer. Default = `gl.ARRAY_BUFFER`. * @param {number} [drawType] the GL draw type for the buffer. Default = 'gl.STATIC_DRAW`. * @return {WebGLBuffer} the created WebGLBuffer * @memberOf module:twgl/attributes */ function createBufferFromTypedArray(gl, typedArray, type, drawType) { if (isBuffer(gl, typedArray)) { return typedArray; } type = type || ARRAY_BUFFER$1; const buffer = gl.createBuffer(); setBufferFromTypedArray(gl, type, buffer, typedArray, drawType); return buffer; } function isIndices(name) { return name === "indices"; } // This is really just a guess. Though I can't really imagine using // anything else? Maybe for some compression? function getNormalizationForTypedArrayType(typedArrayType) { if (typedArrayType === Int8Array) { return true; } // eslint-disable-line if (typedArrayType === Uint8Array) { return true; } // eslint-disable-line return false; } function getArray$1(array) { return array.length ? array : array.data; } const texcoordRE = /coord|texture/i; const colorRE = /color|colour/i; function guessNumComponentsFromName(name, length) { let numComponents; if (texcoordRE.test(name)) { numComponents = 2; } else if (colorRE.test(name)) { numComponents = 4; } else { numComponents = 3; // position, normals, indices ... } if (length % numComponents > 0) { throw new Error(`Can not guess numComponents for attribute '${name}'. Tried ${numComponents} but ${length} values is not evenly divisible by ${numComponents}. You should specify it.`); } return numComponents; } function getNumComponents$1(array, arrayName, numValues) { return array.numComponents || array.size || guessNumComponentsFromName(arrayName, numValues || getArray$1(array).length); } function makeTypedArray(array, name) { if (isArrayBuffer$1(array)) { return array; } if (isArrayBuffer$1(array.data)) { return array.data; } if (Array.isArray(array)) { array = { data: array, }; } let Type = array.type ? typedArrayTypeFromGLTypeOrTypedArrayCtor(array.type) : undefined; if (!Type) { if (isIndices(name)) { Type = Uint16Array; } else { Type = Float32Array; } } return new Type(array.data); } function glTypeFromGLTypeOrTypedArrayType(glTypeOrTypedArrayCtor) { return typeof glTypeOrTypedArrayCtor === 'number' ? glTypeOrTypedArrayCtor : glTypeOrTypedArrayCtor ? getGLTypeForTypedArrayType(glTypeOrTypedArrayCtor) : FLOAT$2; } function typedArrayTypeFromGLTypeOrTypedArrayCtor(glTypeOrTypedArrayCtor) { return typeof glTypeOrTypedArrayCtor === 'number' ? getTypedArrayTypeForGLType(glTypeOrTypedArrayCtor) : glTypeOrTypedArrayCtor || Float32Array; } function attribBufferFromBuffer(gl, array/*, arrayName */) { return { buffer: array.buffer, numValues: 2 * 3 * 4, // safely divided by 2, 3, 4 type: glTypeFromGLTypeOrTypedArrayType(array.type), arrayType: typedArrayTypeFromGLTypeOrTypedArrayCtor(array.type), }; } function attribBufferFromSize(gl, array/*, arrayName*/) { const numValues = array.data || array; const arrayType = typedArrayTypeFromGLTypeOrTypedArrayCtor(array.type); const numBytes = numValues * arrayType.BYTES_PER_ELEMENT; const buffer = gl.createBuffer(); gl.bindBuffer(ARRAY_BUFFER$1, buffer); gl.bufferData(ARRAY_BUFFER$1, numBytes, array.drawType || STATIC_DRAW); return { buffer, numValues, type: getGLTypeForTypedArrayType(arrayType), arrayType, }; } function attribBufferFromArrayLike(gl, array, arrayName) { const typedArray = makeTypedArray(array, arrayName); return { arrayType: typedArray.constructor, buffer: createBufferFromTypedArray(gl, typedArray, undefined, array.drawType), type: getGLTypeForTypedArray(typedArray), numValues: 0, }; } /** * The info for an attribute. This is effectively just the arguments to `gl.vertexAttribPointer` plus the WebGLBuffer * for the attribute. * * @typedef {Object} AttribInfo * @property {number[]|ArrayBufferView} [value] a constant value for the attribute. Note: if this is set the attribute will be * disabled and set to this constant value and all other values will be ignored. * @property {number} [numComponents] the number of components for this attribute. * @property {number} [size] synonym for `numComponents`. * @property {number} [type] the type of the attribute (eg. `gl.FLOAT`, `gl.UNSIGNED_BYTE`, etc...) Default = `gl.FLOAT` * @property {boolean} [normalize] whether or not to normalize the data. Default = false * @property {number} [offset] offset into buffer in bytes. Default = 0 * @property {number} [stride] the stride in bytes per element. Default = 0 * @property {number} [divisor] the divisor in instances. Default = 0. * Requires WebGL2 or the ANGLE_instanced_arrays extension. * and, if you're using WebGL1 you must have called {@link module:twgl.addExtensionsToContext} * @property {WebGLBuffer} buffer the buffer that contains the data for this attribute * @property {number} [drawType] the draw type passed to gl.bufferData. Default = gl.STATIC_DRAW * @memberOf module:twgl */ /** * @typedef {(Int8ArrayConstructor|Uint8ArrayConstructor|Int16ArrayConstructor|Uint16ArrayConstructor|Int32ArrayConstructor|Uint32ArrayConstructor|Float32ArrayConstructor)} TypedArrayConstructor */ /** * Use this type of array spec when TWGL can't guess the type or number of components of an array * @typedef {Object} FullArraySpec * @property {number[]|ArrayBufferView} [value] a constant value for the attribute. Note: if this is set the attribute will be * disabled and set to this constant value and all other values will be ignored. * @property {(number|number[]|ArrayBufferView)} [data] The data of the array. A number alone becomes the number of elements of type. * @property {number} [numComponents] number of components for `vertexAttribPointer`. Default is based on the name of the array. * If `coord` is in the name assumes `numComponents = 2`. * If `color` is in the name assumes `numComponents = 4`. * otherwise assumes `numComponents = 3` * @property {number|TypedArrayConstructor} [type] type. This is used if `data` is a JavaScript array, or `buffer` is passed in, or `data` is a number. * It can either be the constructor for a typedarray. (eg. `Uint8Array`) OR a WebGL type, (eg `gl.UNSIGNED_BYTE`). * For example if you want colors in a `Uint8Array` you might have a `FullArraySpec` like `{ type: gl.UNSIGNED_BYTE, data: [255,0,255,255, ...], }`. * @property {number} [size] synonym for `numComponents`. * @property {boolean} [normalize] normalize for `vertexAttribPointer`. Default is true if type is `Int8Array` or `Uint8Array` otherwise false. * @property {number} [stride] stride for `vertexAttribPointer`. Default = 0 * @property {number} [offset] offset for `vertexAttribPointer`. Default = 0 * @property {number} [divisor] divisor for `vertexAttribDivisor`. Default = 0. * Requires WebGL2 or the ANGLE_instanced_arrays extension. * and, if you using WebGL1 you must have called {@link module:twgl.addExtensionsToContext} * @property {string} [attrib] name of attribute this array maps to. Defaults to same name as array prefixed by the default attribPrefix. * @property {string} [name] synonym for `attrib`. * @property {string} [attribName] synonym for `attrib`. * @property {WebGLBuffer} [buffer] Buffer to use for this attribute. This lets you use your own buffer * but you will need to supply `numComponents` and `type`. You can effectively pass an `AttribInfo` * to provide this. Example: * * const bufferInfo1 = twgl.createBufferInfoFromArrays(gl, { * position: [1, 2, 3, ... ], * }); * const bufferInfo2 = twgl.createBufferInfoFromArrays(gl, { * position: bufferInfo1.attribs.position, // use the same buffer from bufferInfo1 * }); * * @property {number} [drawType] the draw type passed to gl.bufferData. Default = gl.STATIC_DRAW * @memberOf module:twgl */ /** * An individual array in {@link module:twgl.Arrays} * * When passed to {@link module:twgl.createBufferInfoFromArrays} if an ArraySpec is `number[]` or `ArrayBufferView` * the types will be guessed based on the name. `indices` will be `Uint16Array`, everything else will * be `Float32Array`. If an ArraySpec is a number it's the number of floats for an empty (zeroed) buffer. * * @typedef {(number|number[]|ArrayBufferView|module:twgl.FullArraySpec)} ArraySpec * @memberOf module:twgl */ /** * This is a JavaScript object of arrays by name. The names should match your shader's attributes. If your * attributes have a common prefix you can specify it by calling {@link module:twgl.setAttributePrefix}. * * Bare JavaScript Arrays * * var arrays = { * position: [-1, 1, 0], * normal: [0, 1, 0], * ... * } * * Bare TypedArrays * * var arrays = { * position: new Float32Array([-1, 1, 0]), * color: new Uint8Array([255, 128, 64, 255]), * ... * } * * * Will guess at `numComponents` if not specified based on name. * * If `coord` is in the name assumes `numComponents = 2` * * If `color` is in the name assumes `numComponents = 4` * * otherwise assumes `numComponents = 3` * * Objects with various fields. See {@link module:twgl.FullArraySpec}. * * var arrays = { * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, * }; * * @typedef {Object.} Arrays * @memberOf module:twgl */ /** * Creates a set of attribute data and WebGLBuffers from set of arrays * * Given * * var arrays = { * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, * color: { numComponents: 4, data: [255, 255, 255, 255, 255, 0, 0, 255, 0, 0, 255, 255], type: Uint8Array, }, * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, * }; * * returns something like * * var attribs = { * position: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, * texcoord: { numComponents: 2, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, * normal: { numComponents: 3, type: gl.FLOAT, normalize: false, buffer: WebGLBuffer, }, * color: { numComponents: 4, type: gl.UNSIGNED_BYTE, normalize: true, buffer: WebGLBuffer, }, * }; * * notes: * * * Arrays can take various forms * * Bare JavaScript Arrays * * var arrays = { * position: [-1, 1, 0], * normal: [0, 1, 0], * ... * } * * Bare TypedArrays * * var arrays = { * position: new Float32Array([-1, 1, 0]), * color: new Uint8Array([255, 128, 64, 255]), * ... * } * * * Will guess at `numComponents` if not specified based on name. * * If `coord` is in the name assumes `numComponents = 2` * * If `color` is in the name assumes `numComponents = 4` * * otherwise assumes `numComponents = 3` * * @param {WebGLRenderingContext} gl The webgl rendering context. * @param {module:twgl.Arrays} arrays The arrays * @param {module:twgl.BufferInfo} [srcBufferInfo] a BufferInfo to copy from * This lets you share buffers. Any arrays you supply will override * the buffers from srcBufferInfo. * @return {Object.} the attribs * @memberOf module:twgl/attributes */ function createAttribsFromArrays(gl, arrays) { const attribs = {}; Object.keys(arrays).forEach(function(arrayName) { if (!isIndices(arrayName)) { const array = arrays[arrayName]; const attribName = array.attrib || array.name || array.attribName || (defaults$2.attribPrefix + arrayName); if (array.value) { if (!Array.isArray(array.value) && !isArrayBuffer$1(array.value)) { throw new Error('array.value is not array or typedarray'); } attribs[attribName] = { value: array.value, }; } else { let fn; if (array.buffer && array.buffer instanceof WebGLBuffer) { fn = attribBufferFromBuffer; } else if (typeof array === "number" || typeof array.data === "number") { fn = attribBufferFromSize; } else { fn = attribBufferFromArrayLike; } const {buffer, type, numValues, arrayType} = fn(gl, array, arrayName); const normalization = array.normalize !== undefined ? array.normalize : getNormalizationForTypedArrayType(arrayType); const numComponents = getNumComponents$1(array, arrayName, numValues); attribs[attribName] = { buffer: buffer, numComponents: numComponents, type: type, normalize: normalization, stride: array.stride || 0, offset: array.offset || 0, divisor: array.divisor === undefined ? undefined : array.divisor, drawType: array.drawType, }; } } }); gl.bindBuffer(ARRAY_BUFFER$1, null); return attribs; } function getBytesPerValueForGLType(gl, type) { if (type === BYTE$1) return 1; // eslint-disable-line if (type === UNSIGNED_BYTE$2) return 1; // eslint-disable-line if (type === SHORT$1) return 2; // eslint-disable-line if (type === UNSIGNED_SHORT$2) return 2; // eslint-disable-line if (type === INT$2) return 4; // eslint-disable-line if (type === UNSIGNED_INT$2) return 4; // eslint-disable-line if (type === FLOAT$2) return 4; // eslint-disable-line return 0; } // Tries to get the number of elements from a set of arrays. const positionKeys = ['position', 'positions', 'a_position']; function getNumElementsFromAttributes(gl, attribs) { let key; let ii; for (ii = 0; ii < positionKeys.length; ++ii) { key = positionKeys[ii]; if (key in attribs) { break; } key = defaults$2.attribPrefix + key; if (key in attribs) { break; } } if (ii === positionKeys.length) { key = Object.keys(attribs)[0]; } const attrib = attribs[key]; if (!attrib.buffer) { return 1; // There's no buffer } gl.bindBuffer(ARRAY_BUFFER$1, attrib.buffer); const numBytes = gl.getBufferParameter(ARRAY_BUFFER$1, BUFFER_SIZE); gl.bindBuffer(ARRAY_BUFFER$1, null); const bytesPerValue = getBytesPerValueForGLType(gl, attrib.type); const totalElements = numBytes / bytesPerValue; const numComponents = attrib.numComponents || attrib.size; // TODO: check stride const numElements = totalElements / numComponents; if (numElements % 1 !== 0) { throw new Error(`numComponents ${numComponents} not correct for length ${length}`); } return numElements; } /** * @typedef {Object} BufferInfo * @property {number} numElements The number of elements to pass to `gl.drawArrays` or `gl.drawElements`. * @property {number} [elementType] The type of indices `UNSIGNED_BYTE`, `UNSIGNED_SHORT` etc.. * @property {WebGLBuffer} [indices] The indices `ELEMENT_ARRAY_BUFFER` if any indices exist. * @property {Object.} [attribs] The attribs appropriate to call `setAttributes` * @memberOf module:twgl */ /** * Creates a BufferInfo from an object of arrays. * * This can be passed to {@link module:twgl.setBuffersAndAttributes} and to * {@link module:twgl:drawBufferInfo}. * * Given an object like * * var arrays = { * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, * normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, * indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, * }; * * Creates an BufferInfo like this * * bufferInfo = { * numElements: 4, // or whatever the number of elements is * indices: WebGLBuffer, // this property will not exist if there are no indices * attribs: { * position: { buffer: WebGLBuffer, numComponents: 3, }, * normal: { buffer: WebGLBuffer, numComponents: 3, }, * texcoord: { buffer: WebGLBuffer, numComponents: 2, }, * }, * }; * * The properties of arrays can be JavaScript arrays in which case the number of components * will be guessed. * * var arrays = { * position: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], * texcoord: [0, 0, 0, 1, 1, 0, 1, 1], * normal: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], * indices: [0, 1, 2, 1, 2, 3], * }; * * They can also be TypedArrays * * var arrays = { * position: new Float32Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]), * texcoord: new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]), * normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]), * indices: new Uint16Array([0, 1, 2, 1, 2, 3]), * }; * * Or AugmentedTypedArrays * * var positions = createAugmentedTypedArray(3, 4); * var texcoords = createAugmentedTypedArray(2, 4); * var normals = createAugmentedTypedArray(3, 4); * var indices = createAugmentedTypedArray(3, 2, Uint16Array); * * positions.push([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]); * texcoords.push([0, 0, 0, 1, 1, 0, 1, 1]); * normals.push([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]); * indices.push([0, 1, 2, 1, 2, 3]); * * var arrays = { * position: positions, * texcoord: texcoords, * normal: normals, * indices: indices, * }; * * For the last example it is equivalent to * * var bufferInfo = { * attribs: { * position: { numComponents: 3, buffer: gl.createBuffer(), }, * texcoord: { numComponents: 2, buffer: gl.createBuffer(), }, * normal: { numComponents: 3, buffer: gl.createBuffer(), }, * }, * indices: gl.createBuffer(), * numElements: 6, * }; * * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.position.buffer); * gl.bufferData(gl.ARRAY_BUFFER, arrays.position, gl.STATIC_DRAW); * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.texcoord.buffer); * gl.bufferData(gl.ARRAY_BUFFER, arrays.texcoord, gl.STATIC_DRAW); * gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.normal.buffer); * gl.bufferData(gl.ARRAY_BUFFER, arrays.normal, gl.STATIC_DRAW); * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices); * gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, arrays.indices, gl.STATIC_DRAW); * * @param {WebGLRenderingContext} gl A WebGLRenderingContext * @param {module:twgl.Arrays} arrays Your data * @param {module:twgl.BufferInfo} [srcBufferInfo] An existing * buffer info to start from. WebGLBuffers etc specified * in the srcBufferInfo will be used in a new BufferInfo * with any arrays specified overriding the ones in * srcBufferInfo. * @return {module:twgl.BufferInfo} A BufferInfo * @memberOf module:twgl/attributes */ function createBufferInfoFromArrays(gl, arrays, srcBufferInfo) { const newAttribs = createAttribsFromArrays(gl, arrays); const bufferInfo = Object.assign({}, srcBufferInfo ? srcBufferInfo : {}); bufferInfo.attribs = Object.assign({}, srcBufferInfo ? srcBufferInfo.attribs : {}, newAttribs); const indices = arrays.indices; if (indices) { const newIndices = makeTypedArray(indices, "indices"); bufferInfo.indices = createBufferFromTypedArray(gl, newIndices, ELEMENT_ARRAY_BUFFER$2); bufferInfo.numElements = newIndices.length; bufferInfo.elementType = getGLTypeForTypedArray(newIndices); } else if (!bufferInfo.numElements) { bufferInfo.numElements = getNumElementsFromAttributes(gl, bufferInfo.attribs); } return bufferInfo; } /* * Copyright 2019 Gregg Tavares * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ /** * Gets the gl version as a number * @param {WebGLRenderingContext} gl A WebGLRenderingContext * @return {number} version of gl * @private */ //function getVersionAsNumber(gl) { // return parseFloat(gl.getParameter(gl.VERSION).substr(6)); //} /** * Check if context is WebGL 2.0 * @param {WebGLRenderingContext} gl A WebGLRenderingContext * @return {bool} true if it's WebGL 2.0 * @memberOf module:twgl */ function isWebGL2(gl) { // This is the correct check but it's slow // return gl.getParameter(gl.VERSION).indexOf("WebGL 2.0") === 0; // This might also be the correct check but I'm assuming it's slow-ish // return gl instanceof WebGL2RenderingContext; return !!gl.texStorage2D; } const TEXTURE0 = 0x84c0; const ARRAY_BUFFER = 0x8892; const ELEMENT_ARRAY_BUFFER$1 = 0x8893; const ACTIVE_UNIFORMS = 0x8b86; const ACTIVE_ATTRIBUTES = 0x8b89; const TRANSFORM_FEEDBACK_VARYINGS = 0x8c83; const ACTIVE_UNIFORM_BLOCKS = 0x8a36; const UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8a44; const UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8a46; const UNIFORM_BLOCK_DATA_SIZE = 0x8a40; const UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8a43; const FLOAT = 0x1406; const FLOAT_VEC2 = 0x8B50; const FLOAT_VEC3 = 0x8B51; const FLOAT_VEC4 = 0x8B52; const INT = 0x1404; const INT_VEC2 = 0x8B53; const INT_VEC3 = 0x8B54; const INT_VEC4 = 0x8B55; const BOOL = 0x8B56; const BOOL_VEC2 = 0x8B57; const BOOL_VEC3 = 0x8B58; const BOOL_VEC4 = 0x8B59; const FLOAT_MAT2 = 0x8B5A; const FLOAT_MAT3 = 0x8B5B; const FLOAT_MAT4 = 0x8B5C; const SAMPLER_2D = 0x8B5E; const SAMPLER_CUBE = 0x8B60; const SAMPLER_3D = 0x8B5F; const SAMPLER_2D_SHADOW = 0x8B62; const FLOAT_MAT2x3 = 0x8B65; const FLOAT_MAT2x4 = 0x8B66; const FLOAT_MAT3x2 = 0x8B67; const FLOAT_MAT3x4 = 0x8B68; const FLOAT_MAT4x2 = 0x8B69; const FLOAT_MAT4x3 = 0x8B6A; const SAMPLER_2D_ARRAY = 0x8DC1; const SAMPLER_2D_ARRAY_SHADOW = 0x8DC4; const SAMPLER_CUBE_SHADOW = 0x8DC5; const UNSIGNED_INT = 0x1405; const UNSIGNED_INT_VEC2 = 0x8DC6; const UNSIGNED_INT_VEC3 = 0x8DC7; const UNSIGNED_INT_VEC4 = 0x8DC8; const INT_SAMPLER_2D = 0x8DCA; const INT_SAMPLER_3D = 0x8DCB; const INT_SAMPLER_CUBE = 0x8DCC; const INT_SAMPLER_2D_ARRAY = 0x8DCF; const UNSIGNED_INT_SAMPLER_2D = 0x8DD2; const UNSIGNED_INT_SAMPLER_3D = 0x8DD3; const UNSIGNED_INT_SAMPLER_CUBE = 0x8DD4; const UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8DD7; const TEXTURE_2D$1 = 0x0DE1; const TEXTURE_CUBE_MAP = 0x8513; const TEXTURE_3D = 0x806F; const TEXTURE_2D_ARRAY = 0x8C1A; const typeMap = {}; /** * Returns the corresponding bind point for a given sampler type * @private */ function getBindPointForSamplerType(gl, type) { return typeMap[type].bindPoint; } // This kind of sucks! If you could compose functions as in `var fn = gl[name];` // this code could be a lot smaller but that is sadly really slow (T_T) function floatSetter(gl, location) { return function(v) { gl.uniform1f(location, v); }; } function floatArraySetter(gl, location) { return function(v) { gl.uniform1fv(location, v); }; } function floatVec2Setter(gl, location) { return function(v) { gl.uniform2fv(location, v); }; } function floatVec3Setter(gl, location) { return function(v) { gl.uniform3fv(location, v); }; } function floatVec4Setter(gl, location) { return function(v) { gl.uniform4fv(location, v); }; } function intSetter(gl, location) { return function(v) { gl.uniform1i(location, v); }; } function intArraySetter(gl, location) { return function(v) { gl.uniform1iv(location, v); }; } function intVec2Setter(gl, location) { return function(v) { gl.uniform2iv(location, v); }; } function intVec3Setter(gl, location) { return function(v) { gl.uniform3iv(location, v); }; } function intVec4Setter(gl, location) { return function(v) { gl.uniform4iv(location, v); }; } function uintSetter(gl, location) { return function(v) { gl.uniform1ui(location, v); }; } function uintArraySetter(gl, location) { return function(v) { gl.uniform1uiv(location, v); }; } function uintVec2Setter(gl, location) { return function(v) { gl.uniform2uiv(location, v); }; } function uintVec3Setter(gl, location) { return function(v) { gl.uniform3uiv(location, v); }; } function uintVec4Setter(gl, location) { return function(v) { gl.uniform4uiv(location, v); }; } function floatMat2Setter(gl, location) { return function(v) { gl.uniformMatrix2fv(location, false, v); }; } function floatMat3Setter(gl, location) { return function(v) { gl.uniformMatrix3fv(location, false, v); }; } function floatMat4Setter(gl, location) { return function(v) { gl.uniformMatrix4fv(location, false, v); }; } function floatMat23Setter(gl, location) { return function(v) { gl.uniformMatrix2x3fv(location, false, v); }; } function floatMat32Setter(gl, location) { return function(v) { gl.uniformMatrix3x2fv(location, false, v); }; } function floatMat24Setter(gl, location) { return function(v) { gl.uniformMatrix2x4fv(location, false, v); }; } function floatMat42Setter(gl, location) { return function(v) { gl.uniformMatrix4x2fv(location, false, v); }; } function floatMat34Setter(gl, location) { return function(v) { gl.uniformMatrix3x4fv(location, false, v); }; } function floatMat43Setter(gl, location) { return function(v) { gl.uniformMatrix4x3fv(location, false, v); }; } function samplerSetter(gl, type, unit, location) { const bindPoint = getBindPointForSamplerType(gl, type); return isWebGL2(gl) ? function(textureOrPair) { let texture; let sampler; if (!textureOrPair || isTexture(gl, textureOrPair)) { texture = textureOrPair; sampler = null; } else { texture = textureOrPair.texture; sampler = textureOrPair.sampler; } gl.uniform1i(location, unit); gl.activeTexture(TEXTURE0 + unit); gl.bindTexture(bindPoint, texture); gl.bindSampler(unit, sampler); } : function(texture) { gl.uniform1i(location, unit); gl.activeTexture(TEXTURE0 + unit); gl.bindTexture(bindPoint, texture); }; } function samplerArraySetter(gl, type, unit, location, size) { const bindPoint = getBindPointForSamplerType(gl, type); const units = new Int32Array(size); for (let ii = 0; ii < size; ++ii) { units[ii] = unit + ii; } return isWebGL2(gl) ? function(textures) { gl.uniform1iv(location, units); textures.forEach(function(textureOrPair, index) { gl.activeTexture(TEXTURE0 + units[index]); let texture; let sampler; if (!textureOrPair || isTexture(gl, textureOrPair)) { texture = textureOrPair; sampler = null; } else { texture = textureOrPair.texture; sampler = textureOrPair.sampler; } gl.bindSampler(unit, sampler); gl.bindTexture(bindPoint, texture); }); } : function(textures) { gl.uniform1iv(location, units); textures.forEach(function(texture, index) { gl.activeTexture(TEXTURE0 + units[index]); gl.bindTexture(bindPoint, texture); }); }; } typeMap[FLOAT] = { Type: Float32Array, size: 4, setter: floatSetter, arraySetter: floatArraySetter, }; typeMap[FLOAT_VEC2] = { Type: Float32Array, size: 8, setter: floatVec2Setter, cols: 2, }; typeMap[FLOAT_VEC3] = { Type: Float32Array, size: 12, setter: floatVec3Setter, cols: 3, }; typeMap[FLOAT_VEC4] = { Type: Float32Array, size: 16, setter: floatVec4Setter, cols: 4, }; typeMap[INT] = { Type: Int32Array, size: 4, setter: intSetter, arraySetter: intArraySetter, }; typeMap[INT_VEC2] = { Type: Int32Array, size: 8, setter: intVec2Setter, cols: 2, }; typeMap[INT_VEC3] = { Type: Int32Array, size: 12, setter: intVec3Setter, cols: 3, }; typeMap[INT_VEC4] = { Type: Int32Array, size: 16, setter: intVec4Setter, cols: 4, }; typeMap[UNSIGNED_INT] = { Type: Uint32Array, size: 4, setter: uintSetter, arraySetter: uintArraySetter, }; typeMap[UNSIGNED_INT_VEC2] = { Type: Uint32Array, size: 8, setter: uintVec2Setter, cols: 2, }; typeMap[UNSIGNED_INT_VEC3] = { Type: Uint32Array, size: 12, setter: uintVec3Setter, cols: 3, }; typeMap[UNSIGNED_INT_VEC4] = { Type: Uint32Array, size: 16, setter: uintVec4Setter, cols: 4, }; typeMap[BOOL] = { Type: Uint32Array, size: 4, setter: intSetter, arraySetter: intArraySetter, }; typeMap[BOOL_VEC2] = { Type: Uint32Array, size: 8, setter: intVec2Setter, cols: 2, }; typeMap[BOOL_VEC3] = { Type: Uint32Array, size: 12, setter: intVec3Setter, cols: 3, }; typeMap[BOOL_VEC4] = { Type: Uint32Array, size: 16, setter: intVec4Setter, cols: 4, }; typeMap[FLOAT_MAT2] = { Type: Float32Array, size: 32, setter: floatMat2Setter, rows: 2, cols: 2, }; typeMap[FLOAT_MAT3] = { Type: Float32Array, size: 48, setter: floatMat3Setter, rows: 3, cols: 3, }; typeMap[FLOAT_MAT4] = { Type: Float32Array, size: 64, setter: floatMat4Setter, rows: 4, cols: 4, }; typeMap[FLOAT_MAT2x3] = { Type: Float32Array, size: 32, setter: floatMat23Setter, rows: 2, cols: 3, }; typeMap[FLOAT_MAT2x4] = { Type: Float32Array, size: 32, setter: floatMat24Setter, rows: 2, cols: 4, }; typeMap[FLOAT_MAT3x2] = { Type: Float32Array, size: 48, setter: floatMat32Setter, rows: 3, cols: 2, }; typeMap[FLOAT_MAT3x4] = { Type: Float32Array, size: 48, setter: floatMat34Setter, rows: 3, cols: 4, }; typeMap[FLOAT_MAT4x2] = { Type: Float32Array, size: 64, setter: floatMat42Setter, rows: 4, cols: 2, }; typeMap[FLOAT_MAT4x3] = { Type: Float32Array, size: 64, setter: floatMat43Setter, rows: 4, cols: 3, }; typeMap[SAMPLER_2D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D$1, }; typeMap[SAMPLER_CUBE] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_CUBE_MAP, }; typeMap[SAMPLER_3D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_3D, }; typeMap[SAMPLER_2D_SHADOW] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D$1, }; typeMap[SAMPLER_2D_ARRAY] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D_ARRAY, }; typeMap[SAMPLER_2D_ARRAY_SHADOW] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D_ARRAY, }; typeMap[SAMPLER_CUBE_SHADOW] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_CUBE_MAP, }; typeMap[INT_SAMPLER_2D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D$1, }; typeMap[INT_SAMPLER_3D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_3D, }; typeMap[INT_SAMPLER_CUBE] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_CUBE_MAP, }; typeMap[INT_SAMPLER_2D_ARRAY] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D_ARRAY, }; typeMap[UNSIGNED_INT_SAMPLER_2D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D$1, }; typeMap[UNSIGNED_INT_SAMPLER_3D] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_3D, }; typeMap[UNSIGNED_INT_SAMPLER_CUBE] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_CUBE_MAP, }; typeMap[UNSIGNED_INT_SAMPLER_2D_ARRAY] = { Type: null, size: 0, setter: samplerSetter, arraySetter: samplerArraySetter, bindPoint: TEXTURE_2D_ARRAY, }; function floatAttribSetter(gl, index) { return function(b) { if (b.value) { gl.disableVertexAttribArray(index); switch (b.value.length) { case 4: gl.vertexAttrib4fv(index, b.value); break; case 3: gl.vertexAttrib3fv(index, b.value); break; case 2: gl.vertexAttrib2fv(index, b.value); break; case 1: gl.vertexAttrib1fv(index, b.value); break; default: throw new Error('the length of a float constant value must be between 1 and 4!'); } } else { gl.bindBuffer(ARRAY_BUFFER, b.buffer); gl.enableVertexAttribArray(index); gl.vertexAttribPointer( index, b.numComponents || b.size, b.type || FLOAT, b.normalize || false, b.stride || 0, b.offset || 0); if (gl.vertexAttribDivisor) { gl.vertexAttribDivisor(index, b.divisor || 0); } } }; } function intAttribSetter(gl, index) { return function(b) { if (b.value) { gl.disableVertexAttribArray(index); if (b.value.length === 4) { gl.vertexAttrib4iv(index, b.value); } else { throw new Error('The length of an integer constant value must be 4!'); } } else { gl.bindBuffer(ARRAY_BUFFER, b.buffer); gl.enableVertexAttribArray(index); gl.vertexAttribIPointer( index, b.numComponents || b.size, b.type || INT, b.stride || 0, b.offset || 0); if (gl.vertexAttribDivisor) { gl.vertexAttribDivisor(index, b.divisor || 0); } } }; } function uintAttribSetter(gl, index) { return function(b) { if (b.value) { gl.disableVertexAttribArray(index); if (b.value.length === 4) { gl.vertexAttrib4uiv(index, b.value); } else { throw new Error('The length of an unsigned integer constant value must be 4!'); } } else { gl.bindBuffer(ARRAY_BUFFER, b.buffer); gl.enableVertexAttribArray(index); gl.vertexAttribIPointer( index, b.numComponents || b.size, b.type || UNSIGNED_INT, b.stride || 0, b.offset || 0); if (gl.vertexAttribDivisor) { gl.vertexAttribDivisor(index, b.divisor || 0); } } }; } function matAttribSetter(gl, index, typeInfo) { const defaultSize = typeInfo.size; const count = typeInfo.count; return function(b) { gl.bindBuffer(ARRAY_BUFFER, b.buffer); const numComponents = b.size || b.numComponents || defaultSize; const size = numComponents / count; const type = b.type || FLOAT; const typeInfo = typeMap[type]; const stride = typeInfo.size * numComponents; const normalize = b.normalize || false; const offset = b.offset || 0; const rowOffset = stride / count; for (let i = 0; i < count; ++i) { gl.enableVertexAttribArray(index + i); gl.vertexAttribPointer( index + i, size, type, normalize, stride, offset + rowOffset * i); if (gl.vertexAttribDivisor) { gl.vertexAttribDivisor(index + i, b.divisor || 0); } } }; } const attrTypeMap = {}; attrTypeMap[FLOAT] = { size: 4, setter: floatAttribSetter, }; attrTypeMap[FLOAT_VEC2] = { size: 8, setter: floatAttribSetter, }; attrTypeMap[FLOAT_VEC3] = { size: 12, setter: floatAttribSetter, }; attrTypeMap[FLOAT_VEC4] = { size: 16, setter: floatAttribSetter, }; attrTypeMap[INT] = { size: 4, setter: intAttribSetter, }; attrTypeMap[INT_VEC2] = { size: 8, setter: intAttribSetter, }; attrTypeMap[INT_VEC3] = { size: 12, setter: intAttribSetter, }; attrTypeMap[INT_VEC4] = { size: 16, setter: intAttribSetter, }; attrTypeMap[UNSIGNED_INT] = { size: 4, setter: uintAttribSetter, }; attrTypeMap[UNSIGNED_INT_VEC2] = { size: 8, setter: uintAttribSetter, }; attrTypeMap[UNSIGNED_INT_VEC3] = { size: 12, setter: uintAttribSetter, }; attrTypeMap[UNSIGNED_INT_VEC4] = { size: 16, setter: uintAttribSetter, }; attrTypeMap[BOOL] = { size: 4, setter: intAttribSetter, }; attrTypeMap[BOOL_VEC2] = { size: 8, setter: intAttribSetter, }; attrTypeMap[BOOL_VEC3] = { size: 12, setter: intAttribSetter, }; attrTypeMap[BOOL_VEC4] = { size: 16, setter: intAttribSetter, }; attrTypeMap[FLOAT_MAT2] = { size: 4, setter: matAttribSetter, count: 2, }; attrTypeMap[FLOAT_MAT3] = { size: 9, setter: matAttribSetter, count: 3, }; attrTypeMap[FLOAT_MAT4] = { size: 16, setter: matAttribSetter, count: 4, }; /** * Returns true if attribute/uniform is a reserved/built in * * It makes no sense to me why GL returns these because it's * illegal to call `gl.getUniformLocation` and `gl.getAttribLocation` * with names that start with `gl_` (and `webgl_` in WebGL) * * I can only assume they are there because they might count * when computing the number of uniforms/attributes used when you want to * know if you are near the limit. That doesn't really make sense * to me but the fact that these get returned are in the spec. * * @param {WebGLActiveInfo} info As returned from `gl.getActiveUniform` or * `gl.getActiveAttrib`. * @return {bool} true if it's reserved * @private */ function isBuiltIn(info) { const name = info.name; return name.startsWith("gl_") || name.startsWith("webgl_"); } const tokenRE = /(\.|\[|]|\w+)/g; const isDigit = s => s >= '0' && s <= '9'; function addSetterToUniformTree(fullPath, setter, node, uniformSetters) { const tokens = fullPath.split(tokenRE).filter(s => s !== ''); let tokenNdx = 0; let path = ''; for (;;) { const token = tokens[tokenNdx++]; // has to be name or number path += token; const isArrayIndex = isDigit(token[0]); const accessor = isArrayIndex ? parseInt(token) : token; if (isArrayIndex) { path += tokens[tokenNdx++]; // skip ']' } const isLastToken = tokenNdx === tokens.length; if (isLastToken) { node[accessor] = setter; break; } else { const token = tokens[tokenNdx++]; // has to be . or [ const isArray = token === '['; const child = node[accessor] || (isArray ? [] : {}); node[accessor] = child; node = child; uniformSetters[path] = uniformSetters[path] || function(node) { return function(value) { setUniformTree(node, value); }; }(child); path += token; } } } /** * Creates setter functions for all uniforms of a shader * program. * * @see {@link module:twgl.setUniforms} * * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. * @param {WebGLProgram} program the program to create setters for. * @returns {Object.} an object with a setter by name for each uniform * @memberOf module:twgl/programs */ function createUniformSetters(gl, program) { let textureUnit = 0; /** * Creates a setter for a uniform of the given program with it's * location embedded in the setter. * @param {WebGLProgram} program * @param {WebGLUniformInfo} uniformInfo * @returns {function} the created setter. */ function createUniformSetter(program, uniformInfo, location) { const isArray = uniformInfo.name.endsWith("[0]"); const type = uniformInfo.type; const typeInfo = typeMap[type]; if (!typeInfo) { throw new Error(`unknown type: 0x${type.toString(16)}`); // we should never get here. } let setter; if (typeInfo.bindPoint) { // it's a sampler const unit = textureUnit; textureUnit += uniformInfo.size; if (isArray) { setter = typeInfo.arraySetter(gl, type, unit, location, uniformInfo.size); } else { setter = typeInfo.setter(gl, type, unit, location, uniformInfo.size); } } else { if (typeInfo.arraySetter && isArray) { setter = typeInfo.arraySetter(gl, location); } else { setter = typeInfo.setter(gl, location); } } setter.location = location; return setter; } const uniformSetters = {}; const uniformTree = {}; const numUniforms = gl.getProgramParameter(program, ACTIVE_UNIFORMS); for (let ii = 0; ii < numUniforms; ++ii) { const uniformInfo = gl.getActiveUniform(program, ii); if (isBuiltIn(uniformInfo)) { continue; } let name = uniformInfo.name; // remove the array suffix. if (name.endsWith("[0]")) { name = name.substr(0, name.length - 3); } const location = gl.getUniformLocation(program, uniformInfo.name); // the uniform will have no location if it's in a uniform block if (location) { const setter = createUniformSetter(program, uniformInfo, location); uniformSetters[name] = setter; addSetterToUniformTree(name, setter, uniformTree, uniformSetters); } } return uniformSetters; } /** * @typedef {Object} TransformFeedbackInfo * @property {number} index index of transform feedback * @property {number} type GL type * @property {number} size 1 - 4 * @memberOf module:twgl */ /** * Create TransformFeedbackInfo for passing to bindTransformFeedbackInfo. * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. * @param {WebGLProgram} program an existing WebGLProgram. * @return {Object} * @memberOf module:twgl */ function createTransformFeedbackInfo(gl, program) { const info = {}; const numVaryings = gl.getProgramParameter(program, TRANSFORM_FEEDBACK_VARYINGS); for (let ii = 0; ii < numVaryings; ++ii) { const varying = gl.getTransformFeedbackVarying(program, ii); info[varying.name] = { index: ii, type: varying.type, size: varying.size, }; } return info; } /** * @typedef {Object} UniformData * @property {string} name The name of the uniform * @property {number} type The WebGL type enum for this uniform * @property {number} size The number of elements for this uniform * @property {number} blockNdx The block index this uniform appears in * @property {number} offset The byte offset in the block for this uniform's value * @memberOf module:twgl */ /** * The specification for one UniformBlockObject * * @typedef {Object} BlockSpec * @property {number} index The index of the block. * @property {number} size The size in bytes needed for the block * @property {number[]} uniformIndices The indices of the uniforms used by the block. These indices * correspond to entries in a UniformData array in the {@link module:twgl.UniformBlockSpec}. * @property {bool} usedByVertexShader Self explanatory * @property {bool} usedByFragmentShader Self explanatory * @property {bool} used Self explanatory * @memberOf module:twgl */ /** * A `UniformBlockSpec` represents the data needed to create and bind * UniformBlockObjects for a given program * * @typedef {Object} UniformBlockSpec * @property {Object.} blockSpecs The BlockSpec for each block by block name * @property {UniformData[]} uniformData An array of data for each uniform by uniform index. * @memberOf module:twgl */ /** * Creates a UniformBlockSpec for the given program. * * A UniformBlockSpec represents the data needed to create and bind * UniformBlockObjects * * @param {WebGL2RenderingContext} gl A WebGL2 Rendering Context * @param {WebGLProgram} program A WebGLProgram for a successfully linked program * @return {module:twgl.UniformBlockSpec} The created UniformBlockSpec * @memberOf module:twgl/programs */ function createUniformBlockSpecFromProgram(gl, program) { const numUniforms = gl.getProgramParameter(program, ACTIVE_UNIFORMS); const uniformData = []; const uniformIndices = []; for (let ii = 0; ii < numUniforms; ++ii) { uniformIndices.push(ii); uniformData.push({}); const uniformInfo = gl.getActiveUniform(program, ii); uniformData[ii].name = uniformInfo.name; } [ [ "UNIFORM_TYPE", "type" ], [ "UNIFORM_SIZE", "size" ], // num elements [ "UNIFORM_BLOCK_INDEX", "blockNdx" ], [ "UNIFORM_OFFSET", "offset", ], ].forEach(function(pair) { const pname = pair[0]; const key = pair[1]; gl.getActiveUniforms(program, uniformIndices, gl[pname]).forEach(function(value, ndx) { uniformData[ndx][key] = value; }); }); const blockSpecs = {}; const numUniformBlocks = gl.getProgramParameter(program, ACTIVE_UNIFORM_BLOCKS); for (let ii = 0; ii < numUniformBlocks; ++ii) { const name = gl.getActiveUniformBlockName(program, ii); const blockSpec = { index: gl.getUniformBlockIndex(program, name), usedByVertexShader: gl.getActiveUniformBlockParameter(program, ii, UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER), usedByFragmentShader: gl.getActiveUniformBlockParameter(program, ii, UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER), size: gl.getActiveUniformBlockParameter(program, ii, UNIFORM_BLOCK_DATA_SIZE), uniformIndices: gl.getActiveUniformBlockParameter(program, ii, UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES), }; blockSpec.used = blockSpec.usedByVertexShader || blockSpec.usedByFragmentShader; blockSpecs[name] = blockSpec; } return { blockSpecs: blockSpecs, uniformData: uniformData, }; } function setUniformTree(tree, values) { for (const name in values) { const prop = tree[name]; if (typeof prop === 'function') { prop(values[name]); } else { setUniformTree(tree[name], values[name]); } } } /** * Set uniforms and binds related textures. * * example: * * const programInfo = createProgramInfo( * gl, ["some-vs", "some-fs"]); * * const tex1 = gl.createTexture(); * const tex2 = gl.createTexture(); * * ... assume we setup the textures with data ... * * const uniforms = { * u_someSampler: tex1, * u_someOtherSampler: tex2, * u_someColor: [1,0,0,1], * u_somePosition: [0,1,1], * u_someMatrix: [ * 1,0,0,0, * 0,1,0,0, * 0,0,1,0, * 0,0,0,0, * ], * }; * * gl.useProgram(programInfo.program); * * This will automatically bind the textures AND set the * uniforms. * * twgl.setUniforms(programInfo, uniforms); * * For the example above it is equivalent to * * let texUnit = 0; * gl.activeTexture(gl.TEXTURE0 + texUnit); * gl.bindTexture(gl.TEXTURE_2D, tex1); * gl.uniform1i(u_someSamplerLocation, texUnit++); * gl.activeTexture(gl.TEXTURE0 + texUnit); * gl.bindTexture(gl.TEXTURE_2D, tex2); * gl.uniform1i(u_someSamplerLocation, texUnit++); * gl.uniform4fv(u_someColorLocation, [1, 0, 0, 1]); * gl.uniform3fv(u_somePositionLocation, [0, 1, 1]); * gl.uniformMatrix4fv(u_someMatrix, false, [ * 1,0,0,0, * 0,1,0,0, * 0,0,1,0, * 0,0,0,0, * ]); * * Note it is perfectly reasonable to call `setUniforms` multiple times. For example * * const uniforms = { * u_someSampler: tex1, * u_someOtherSampler: tex2, * }; * * const moreUniforms { * u_someColor: [1,0,0,1], * u_somePosition: [0,1,1], * u_someMatrix: [ * 1,0,0,0, * 0,1,0,0, * 0,0,1,0, * 0,0,0,0, * ], * }; * * twgl.setUniforms(programInfo, uniforms); * twgl.setUniforms(programInfo, moreUniforms); * * You can also add WebGLSamplers to uniform samplers as in * * const uniforms = { * u_someSampler: { * texture: someWebGLTexture, * sampler: someWebGLSampler, * }, * }; * * In which case both the sampler and texture will be bound to the * same unit. * * @param {(module:twgl.ProgramInfo|Object.)} setters a `ProgramInfo` as returned from `createProgramInfo` or the setters returned from * `createUniformSetters`. * @param {Object.} values an object with values for the * uniforms. * You can pass multiple objects by putting them in an array or by calling with more arguments.For example * * const sharedUniforms = { * u_fogNear: 10, * u_projection: ... * ... * }; * * const localUniforms = { * u_world: ... * u_diffuseColor: ... * }; * * twgl.setUniforms(programInfo, sharedUniforms, localUniforms); * * // is the same as * * twgl.setUniforms(programInfo, [sharedUniforms, localUniforms]); * * // is the same as * * twgl.setUniforms(programInfo, sharedUniforms); * twgl.setUniforms(programInfo, localUniforms}; * * You can also fill out structure and array values either via * shortcut. Example * * // -- in shader -- * struct Light { * float intensity; * vec4 color; * float nearFar[2]; * }; * uniform Light lights[2]; * * // in JavaScript * * twgl.setUniforms(programInfo, { * lights: [ * { intensity: 5.0, color: [1, 0, 0, 1], nearFar[0.1, 10] }, * { intensity: 2.0, color: [0, 0, 1, 1], nearFar[0.2, 15] }, * ], * }); * * or the more traditional way * * twgl.setUniforms(programInfo, { * "lights[0].intensity": 5.0, * "lights[0].color": [1, 0, 0, 1], * "lights[0].nearFar": [0.1, 10], * "lights[1].intensity": 2.0, * "lights[1].color": [0, 0, 1, 1], * "lights[1].nearFar": [0.2, 15], * }); * * You can also specify partial paths * * twgl.setUniforms(programInfo, { * 'lights[1]': { intensity: 5.0, color: [1, 0, 0, 1], nearFar[0.2, 15] }, * }); * * But you can not specify leaf array indices * * twgl.setUniforms(programInfo, { * 'lights[1].nearFar[1]': 15, // BAD! nearFar is a leaf * 'lights[1].nearFar': [0.2, 15], // GOOD * }); * * @memberOf module:twgl/programs */ function setUniforms(setters, ...args) { // eslint-disable-line const actualSetters = setters.uniformSetters || setters; const numArgs = args.length; for (let aNdx = 0; aNdx < numArgs; ++aNdx) { const values = args[aNdx]; if (Array.isArray(values)) { const numValues = values.length; for (let ii = 0; ii < numValues; ++ii) { setUniforms(actualSetters, values[ii]); } } else { for (const name in values) { const setter = actualSetters[name]; if (setter) { setter(values[name]); } } } } } /** * Creates setter functions for all attributes of a shader * program. You can pass this to {@link module:twgl.setBuffersAndAttributes} to set all your buffers and attributes. * * @see {@link module:twgl.setAttributes} for example * @param {WebGLRenderingContext} gl The WebGLRenderingContext to use. * @param {WebGLProgram} program the program to create setters for. * @return {Object.} an object with a setter for each attribute by name. * @memberOf module:twgl/programs */ function createAttributeSetters(gl, program) { const attribSetters = { }; const numAttribs = gl.getProgramParameter(program, ACTIVE_ATTRIBUTES); for (let ii = 0; ii < numAttribs; ++ii) { const attribInfo = gl.getActiveAttrib(program, ii); if (isBuiltIn(attribInfo)) { continue; } const index = gl.getAttribLocation(program, attribInfo.name); const typeInfo = attrTypeMap[attribInfo.type]; const setter = typeInfo.setter(gl, index, typeInfo); setter.location = index; attribSetters[attribInfo.name] = setter; } return attribSetters; } /** * Sets attributes and binds buffers (deprecated... use {@link module:twgl.setBuffersAndAttributes}) * * Example: * * const program = createProgramFromScripts( * gl, ["some-vs", "some-fs"); * * const attribSetters = createAttributeSetters(program); * * const positionBuffer = gl.createBuffer(); * const texcoordBuffer = gl.createBuffer(); * * const attribs = { * a_position: {buffer: positionBuffer, numComponents: 3}, * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, * }; * * gl.useProgram(program); * * This will automatically bind the buffers AND set the * attributes. * * setAttributes(attribSetters, attribs); * * Properties of attribs. For each attrib you can add * properties: * * * type: the type of data in the buffer. Default = gl.FLOAT * * normalize: whether or not to normalize the data. Default = false * * stride: the stride. Default = 0 * * offset: offset into the buffer. Default = 0 * * divisor: the divisor for instances. Default = undefined * * For example if you had 3 value float positions, 2 value * float texcoord and 4 value uint8 colors you'd setup your * attribs like this * * const attribs = { * a_position: {buffer: positionBuffer, numComponents: 3}, * a_texcoord: {buffer: texcoordBuffer, numComponents: 2}, * a_color: { * buffer: colorBuffer, * numComponents: 4, * type: gl.UNSIGNED_BYTE, * normalize: true, * }, * }; * * @param {Object.} setters Attribute setters as returned from createAttributeSetters * @param {Object.} buffers AttribInfos mapped by attribute name. * @memberOf module:twgl/programs * @deprecated use {@link module:twgl.setBuffersAndAttributes} * @private */ function setAttributes(setters, buffers) { for (const name in buffers) { const setter = setters[name]; if (setter) { setter(buffers[name]); } } } /** * Sets attributes and buffers including the `ELEMENT_ARRAY_BUFFER` if appropriate * * Example: * * const programInfo = createProgramInfo( * gl, ["some-vs", "some-fs"); * * const arrays = { * position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, * texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, * }; * * const bufferInfo = createBufferInfoFromArrays(gl, arrays); * * gl.useProgram(programInfo.program); * * This will automatically bind the buffers AND set the * attributes. * * setBuffersAndAttributes(gl, programInfo, bufferInfo); * * For the example above it is equivalent to * * gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); * gl.enableVertexAttribArray(a_positionLocation); * gl.vertexAttribPointer(a_positionLocation, 3, gl.FLOAT, false, 0, 0); * gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); * gl.enableVertexAttribArray(a_texcoordLocation); * gl.vertexAttribPointer(a_texcoordLocation, 4, gl.FLOAT, false, 0, 0); * * @param {WebGLRenderingContext} gl A WebGLRenderingContext. * @param {(module:twgl.ProgramInfo|Object.)} setters A `ProgramInfo` as returned from {@link module:twgl.createProgramInfo} or Attribute setters as returned from {@link module:twgl.createAttributeSetters} * @param {(module:twgl.BufferInfo|module:twgl.VertexArrayInfo)} buffers a `BufferInfo` as returned from {@link module:twgl.createBufferInfoFromArrays}. * or a `VertexArrayInfo` as returned from {@link module:twgl.createVertexArrayInfo} * @memberOf module:twgl/programs */ function setBuffersAndAttributes(gl, programInfo, buffers) { if (buffers.vertexArrayObject) { gl.bindVertexArray(buffers.vertexArrayObject); } else { setAttributes(programInfo.attribSetters || programInfo, buffers.attribs); if (buffers.indices) { gl.bindBuffer(ELEMENT_ARRAY_BUFFER$1, buffers.indices); } } } /** * @typedef {Object} ProgramInfo * @property {WebGLProgram} program A shader program * @property {Object} uniformSetters object of setters as returned from createUniformSetters, * @property {Object} attribSetters object of setters as returned from createAttribSetters, * @property {module:twgl.UniformBlockSpec} [uniformBlockSpec] a uniform block spec for making UniformBlockInfos with createUniformBlockInfo etc.. * @property {Object} [transformFeedbackInfo] info for transform feedbacks * @memberOf module:twgl */ /** * Creates a ProgramInfo from an existing program. * * A ProgramInfo contains * * programInfo = { * program: WebGLProgram, * uniformSetters: object of setters as returned from createUniformSetters, * attribSetters: object of setters as returned from createAttribSetters, * } * * @param {WebGLRenderingContext} gl The WebGLRenderingContext * to use. * @param {WebGLProgram} program an existing WebGLProgram. * @return {module:twgl.ProgramInfo} The created ProgramInfo. * @memberOf module:twgl/programs */ function createProgramInfoFromProgram(gl, program) { const uniformSetters = createUniformSetters(gl, program); const attribSetters = createAttributeSetters(gl, program); const programInfo = { program, uniformSetters, attribSetters, }; if (isWebGL2(gl)) { programInfo.uniformBlockSpec = createUniformBlockSpecFromProgram(gl, program); programInfo.transformFeedbackInfo = createTransformFeedbackInfo(gl, program); } return programInfo; } var vertShaderLayer = "#define GLSLIFY 1\nattribute vec4 position;attribute vec2 texcoord;varying vec2 v_uv;uniform bool u_flipY;uniform vec2 u_textureSize;uniform int u_3d_eye;uniform float u_3d_depth;uniform float u_3d_strength;void main(){vec4 pos=position;float depthDirection=u_3d_eye==0 ?-1.0 : 1.0;float depthShift=floor(u_3d_depth*u_3d_strength)/(u_textureSize.x/2.0)*depthDirection;pos.x+=depthShift;pos.y*=u_flipY ?-1.0 : 1.0;v_uv=texcoord;gl_Position=pos;}"; // eslint-disable-line var fragShaderLayer = "precision highp float;\n#define GLSLIFY 1\nvarying vec2 v_uv;uniform sampler2D u_tex;uniform int u_3d_mode;void main(){vec4 col=texture2D(u_tex,v_uv);if(col.a==0.0){discard;}gl_FragColor=col;}"; // eslint-disable-line var vertShaderUpscale = "#define GLSLIFY 1\nattribute vec4 position;attribute vec2 texcoord;varying vec2 v_texel;varying vec2 v_uv;varying float v_scale;uniform bool u_flipY;uniform vec2 u_textureSize;uniform vec2 u_screenSize;void main(){v_uv=texcoord;v_scale=floor(u_screenSize.y/u_textureSize.y+0.01);gl_Position=position;if(u_flipY){gl_Position.y*=-1.;}}"; // eslint-disable-line var fragShaderUpscale = "precision highp float;\n#define GLSLIFY 1\nvarying vec2 v_uv;uniform sampler2D u_tex;varying float v_scale;uniform vec2 u_textureSize;uniform vec2 u_screenSize;void main(){vec2 v_texel=v_uv*u_textureSize;vec2 texel_floored=floor(v_texel);vec2 s=fract(v_texel);float region_range=0.5-0.5/v_scale;vec2 center_dist=s-0.5;vec2 f=(center_dist-clamp(center_dist,-region_range,region_range))*v_scale+0.5;vec2 mod_texel=texel_floored+f;vec2 coord=mod_texel.xy/u_textureSize.xy;gl_FragColor=texture2D(u_tex,coord);}"; // eslint-disable-line var _WebglCanvas_instances, _WebglCanvas_options, _WebglCanvas_layerProgram, _WebglCanvas_upscaleProgram, _WebglCanvas_quadBuffer, _WebglCanvas_paletteBuffer, _WebglCanvas_layerTexture, _WebglCanvas_layerTexturePixelBuffer, _WebglCanvas_layerTexturePixels, _WebglCanvas_frameTexture, _WebglCanvas_frameBuffer, _WebglCanvas_textureTypes, _WebglCanvas_textureSizes, _WebglCanvas_frameBufferTextures, _WebglCanvas_applyFirefoxFix, _WebglCanvas_refs, _WebglCanvas_isCtxLost, _WebglCanvas_init, _WebglCanvas_drawLayers, _WebglCanvas_upscale, _WebglCanvas_createProgram, _WebglCanvas_createShader, _WebglCanvas_createScreenQuad, _WebglCanvas_setBuffersAndAttribs, _WebglCanvas_createTexture, _WebglCanvas_resizeTexture, _WebglCanvas_createFramebuffer, _WebglCanvas_useFramebuffer, _WebglCanvas_resizeFramebuffer, _WebglCanvas_checkContextLoss, _WebglCanvas_handleContextLoss, _WebglCanvas_handleContextRestored; /** * Flipnote renderer for the {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API WebGL} API. * * Only available in browser contexts. */ class WebglCanvas { static isSupported() { if (!isBrowser) return false; let testCanvas = document.createElement('canvas'); let testCtx = testCanvas.getContext('2d'); const supported = testCtx !== null; testCanvas = null; testCtx = null; return supported; } /** * Creates a new WebGlCanvas instance * @param el - Canvas HTML element to use as a rendering surface * @param width - Canvas width in CSS pixels * @param height - Canvas height in CSS pixels * * The ratio between `width` and `height` should be 3:4 for best results */ constructor(parent, width = 640, height = 480, options = {}) { _WebglCanvas_instances.add(this); /** * */ this.supportedStereoscopeModes = [ exports.CanvasStereoscopicMode.None, exports.CanvasStereoscopicMode.Dual, // CanvasStereoscopicMode.Anaglyph, // couldn't get this working, despite spending lots of time on it :/ ]; /** * */ this.stereoscopeMode = exports.CanvasStereoscopicMode.None; /** * */ this.stereoscopeStrength = 0; _WebglCanvas_options.set(this, void 0); _WebglCanvas_layerProgram.set(this, void 0); // for drawing renderbuffer w/ filtering _WebglCanvas_upscaleProgram.set(this, void 0); // for drawing renderbuffer w/ filtering _WebglCanvas_quadBuffer.set(this, void 0); _WebglCanvas_paletteBuffer.set(this, new Uint32Array(16)); _WebglCanvas_layerTexture.set(this, void 0); _WebglCanvas_layerTexturePixelBuffer.set(this, void 0); _WebglCanvas_layerTexturePixels.set(this, void 0); // will be same memory as layerTexturePixelBuffer, just uint8 for webgl texture _WebglCanvas_frameTexture.set(this, void 0); _WebglCanvas_frameBuffer.set(this, void 0); _WebglCanvas_textureTypes.set(this, new Map()); _WebglCanvas_textureSizes.set(this, new Map()); _WebglCanvas_frameBufferTextures.set(this, new Map()); _WebglCanvas_applyFirefoxFix.set(this, false); _WebglCanvas_refs.set(this, { programs: [], shaders: [], textures: [], buffers: [], frameBuffers: [] }); _WebglCanvas_isCtxLost.set(this, false); _WebglCanvas_handleContextLoss.set(this, (e) => { this.destroy(); if (e) e.preventDefault(); if (!__classPrivateFieldGet(this, _WebglCanvas_isCtxLost, "f")) __classPrivateFieldGet(this, _WebglCanvas_options, "f").onlost(); __classPrivateFieldSet(this, _WebglCanvas_isCtxLost, true, "f"); }); _WebglCanvas_handleContextRestored.set(this, (e) => { __classPrivateFieldSet(this, _WebglCanvas_isCtxLost, false, "f"); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_init).call(this); __classPrivateFieldGet(this, _WebglCanvas_options, "f").onrestored(); }); assertBrowserEnv(); __classPrivateFieldSet(this, _WebglCanvas_options, { ...WebglCanvas.defaultOptions, ...options }, "f"); this.width = width; this.height = height; this.canvas = document.createElement('canvas'); this.canvas.addEventListener('webglcontextlost', __classPrivateFieldGet(this, _WebglCanvas_handleContextLoss, "f"), false); this.canvas.addEventListener('webglcontextrestored', __classPrivateFieldGet(this, _WebglCanvas_handleContextRestored, "f"), false); this.canvas.className = 'FlipnoteCanvas FlipnoteCanvas--webgl'; this.gl = this.canvas.getContext('webgl', { antialias: false, alpha: true }); if (parent) parent.appendChild(this.canvas); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_init).call(this); } /** * Resize the canvas surface * @param width - New canvas width, in CSS pixels * @param height - New canvas height, in CSS pixels * * The ratio between `width` and `height` should be 3:4 for best results */ setCanvasSize(width, height) { const dpi = __classPrivateFieldGet(this, _WebglCanvas_options, "f").useDpi ? (window.devicePixelRatio || 1) : 1; const internalWidth = width * dpi; const internalHeight = height * dpi; this.width = width; this.height = height; this.canvas.width = internalWidth; this.canvas.height = internalHeight; this.dstWidth = internalWidth; this.dstHeight = internalHeight; this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this); } /** * Sets the note to use for this player */ setNote(note) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const width = note.imageWidth; const height = note.imageHeight; this.note = note; this.srcWidth = width; this.srcHeight = height; __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_resizeFramebuffer).call(this, __classPrivateFieldGet(this, _WebglCanvas_frameBuffer, "f"), width, height); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_resizeTexture).call(this, __classPrivateFieldGet(this, _WebglCanvas_layerTexture, "f"), width, height); __classPrivateFieldSet(this, _WebglCanvas_layerTexturePixelBuffer, new Uint32Array(width * height), "f"); __classPrivateFieldSet(this, _WebglCanvas_layerTexturePixels, new Uint8Array(__classPrivateFieldGet(this, _WebglCanvas_layerTexturePixelBuffer, "f").buffer), "f"); // same memory buffer as rgbaData this.frameIndex = undefined; // set canvas alt text this.canvas.title = note.getTitle(); } /** * Clear the canvas * @param color optional RGBA color to use as a background color */ clear(color) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const paperColor = color ?? this.note.getFramePalette(this.frameIndex)[0]; const [r, g, b, a] = paperColor; gl.clearColor(r / 255, g / 255, b / 255, a / 255); gl.clear(gl.COLOR_BUFFER_BIT); } /** * Draw a frame from the currently loaded Flipnote * @param frameIndex */ drawFrame(frameIndex) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const mode = this.stereoscopeMode; const strength = this.stereoscopeStrength; this.frameIndex = frameIndex; if (mode === exports.CanvasStereoscopicMode.None) { __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_drawLayers).call(this, frameIndex); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_useFramebuffer).call(this, null); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_upscale).call(this, gl.drawingBufferWidth, gl.drawingBufferHeight); } else if (mode === exports.CanvasStereoscopicMode.Dual) { __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_drawLayers).call(this, frameIndex, strength, exports.FlipnoteStereoscopicEye.Left); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_useFramebuffer).call(this, null, 0, 0, gl.drawingBufferWidth / 2, gl.drawingBufferHeight); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_upscale).call(this, gl.drawingBufferWidth / 2, gl.drawingBufferHeight); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_drawLayers).call(this, frameIndex, strength, exports.FlipnoteStereoscopicEye.Right); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_useFramebuffer).call(this, null, gl.drawingBufferWidth / 2, 0, gl.drawingBufferWidth / 2, gl.drawingBufferHeight); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_upscale).call(this, gl.drawingBufferWidth / 2, gl.drawingBufferHeight); } } requestStereoScopeMode(mode) { if (this.supportedStereoscopeModes.includes(mode)) this.stereoscopeMode = mode; else this.stereoscopeMode = exports.CanvasStereoscopicMode.None; this.forceUpdate(); } forceUpdate() { if (this.frameIndex !== undefined) this.drawFrame(this.frameIndex); } /** * Returns true if the webGL context has returned an error */ isErrorState() { const gl = this.gl; return gl === null || gl.getError() !== gl.NO_ERROR; } /** * Get the contents of the canvas as a data URL. * * @param type image mime type (`image/jpeg`, `image/png`, etc) * @param quality image quality where supported, between 0 and 1 */ getDataUrl(type, quality) { return this.canvas.toDataURL(type, quality); } /** * Get the contents of the canvas as a blob. * * @param type image mime type (`image/jpeg`, `image/png`, etc) * @param quality image quality where supported, between 0 and 1 */ async getBlob(type, quality) { return new Promise((resolve, reject) => this.canvas.toBlob(resolve, type, quality)); } /** * Frees any resources used by this canvas instance */ destroy() { const refs = __classPrivateFieldGet(this, _WebglCanvas_refs, "f"); const gl = this.gl; const canvas = this.canvas; refs.shaders.forEach((shader) => { gl.deleteShader(shader); }); refs.shaders = []; refs.textures.forEach((texture) => { gl.deleteTexture(texture); }); refs.textures = []; refs.buffers.forEach((buffer) => { gl.deleteBuffer(buffer); }); refs.buffers = []; refs.frameBuffers.forEach((fb) => { gl.deleteFramebuffer(fb); }); refs.frameBuffers = []; refs.programs.forEach((program) => { gl.deleteProgram(program); }); refs.programs = []; __classPrivateFieldSet(this, _WebglCanvas_paletteBuffer, null, "f"); __classPrivateFieldSet(this, _WebglCanvas_layerTexturePixelBuffer, null, "f"); __classPrivateFieldSet(this, _WebglCanvas_layerTexturePixels, null, "f"); __classPrivateFieldGet(this, _WebglCanvas_textureTypes, "f").clear(); __classPrivateFieldGet(this, _WebglCanvas_textureSizes, "f").clear(); __classPrivateFieldGet(this, _WebglCanvas_frameBufferTextures, "f").clear(); if (canvas && canvas.parentElement) { // shrink the canvas to reduce memory usage until it is garbage collected canvas.width = 1; canvas.height = 1; // remove canvas from dom canvas.parentNode.removeChild(canvas); } } } _WebglCanvas_options = new WeakMap(), _WebglCanvas_layerProgram = new WeakMap(), _WebglCanvas_upscaleProgram = new WeakMap(), _WebglCanvas_quadBuffer = new WeakMap(), _WebglCanvas_paletteBuffer = new WeakMap(), _WebglCanvas_layerTexture = new WeakMap(), _WebglCanvas_layerTexturePixelBuffer = new WeakMap(), _WebglCanvas_layerTexturePixels = new WeakMap(), _WebglCanvas_frameTexture = new WeakMap(), _WebglCanvas_frameBuffer = new WeakMap(), _WebglCanvas_textureTypes = new WeakMap(), _WebglCanvas_textureSizes = new WeakMap(), _WebglCanvas_frameBufferTextures = new WeakMap(), _WebglCanvas_applyFirefoxFix = new WeakMap(), _WebglCanvas_refs = new WeakMap(), _WebglCanvas_isCtxLost = new WeakMap(), _WebglCanvas_handleContextLoss = new WeakMap(), _WebglCanvas_handleContextRestored = new WeakMap(), _WebglCanvas_instances = new WeakSet(), _WebglCanvas_init = function _WebglCanvas_init() { this.setCanvasSize(this.width, this.height); const gl = this.gl; if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; __classPrivateFieldSet(this, _WebglCanvas_layerProgram, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createProgram).call(this, vertShaderLayer, fragShaderLayer), "f"); __classPrivateFieldSet(this, _WebglCanvas_upscaleProgram, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createProgram).call(this, vertShaderUpscale, fragShaderUpscale), "f"); __classPrivateFieldSet(this, _WebglCanvas_quadBuffer, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createScreenQuad).call(this, -1, -1, 2, 2, 1, 1), "f"); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_setBuffersAndAttribs).call(this, __classPrivateFieldGet(this, _WebglCanvas_layerProgram, "f"), __classPrivateFieldGet(this, _WebglCanvas_quadBuffer, "f")); __classPrivateFieldSet(this, _WebglCanvas_layerTexture, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createTexture).call(this, gl.RGBA, gl.LINEAR, gl.CLAMP_TO_EDGE), "f"); __classPrivateFieldSet(this, _WebglCanvas_frameTexture, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createTexture).call(this, gl.RGBA, gl.LINEAR, gl.CLAMP_TO_EDGE), "f"); __classPrivateFieldSet(this, _WebglCanvas_frameBuffer, __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createFramebuffer).call(this, __classPrivateFieldGet(this, _WebglCanvas_frameTexture, "f")), "f"); const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); const userAgent = navigator.userAgent; const isMacFirefox = userAgent.includes('Firefox') && userAgent.includes('Mac'); __classPrivateFieldSet(this, _WebglCanvas_applyFirefoxFix, isMacFirefox && renderer.includes('Apple M'), "f"); }, _WebglCanvas_drawLayers = function _WebglCanvas_drawLayers(frameIndex, depthStrength = 0, depthEye = exports.FlipnoteStereoscopicEye.Left, shouldClear = true) { const gl = this.gl; const note = this.note; const srcWidth = this.srcWidth; const srcHeight = this.srcHeight; const numLayers = note.numLayers; const layerOrder = note.getFrameLayerOrder(frameIndex); const layerDepths = note.getFrameLayerDepths(frameIndex); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_useFramebuffer).call(this, __classPrivateFieldGet(this, _WebglCanvas_frameBuffer, "f")); if (shouldClear) this.clear(); gl.useProgram(__classPrivateFieldGet(this, _WebglCanvas_layerProgram, "f").program); for (let i = 0; i < numLayers; i++) { const layerIndex = layerOrder[i]; note.getLayerPixelsRgba(frameIndex, layerIndex, __classPrivateFieldGet(this, _WebglCanvas_layerTexturePixelBuffer, "f"), __classPrivateFieldGet(this, _WebglCanvas_paletteBuffer, "f")); setUniforms(__classPrivateFieldGet(this, _WebglCanvas_layerProgram, "f"), { u_flipY: true, u_tex: __classPrivateFieldGet(this, _WebglCanvas_layerTexture, "f"), u_textureSize: [srcWidth, srcHeight], u_3d_mode: this.stereoscopeMode, u_3d_eye: depthEye, u_3d_depth: layerDepths[layerIndex], u_3d_strength: depthStrength, }); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, srcWidth, srcHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, __classPrivateFieldGet(this, _WebglCanvas_layerTexturePixels, "f")); gl.drawElements(gl.TRIANGLES, __classPrivateFieldGet(this, _WebglCanvas_quadBuffer, "f").numElements, __classPrivateFieldGet(this, _WebglCanvas_quadBuffer, "f").elementType, 0); } }, _WebglCanvas_upscale = function _WebglCanvas_upscale(width, height) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; gl.useProgram(__classPrivateFieldGet(this, _WebglCanvas_upscaleProgram, "f").program); setUniforms(__classPrivateFieldGet(this, _WebglCanvas_upscaleProgram, "f"), { // u_flipY: true, u_tex: __classPrivateFieldGet(this, _WebglCanvas_frameTexture, "f"), u_textureSize: [this.srcWidth, this.srcHeight], u_screenSize: [width, height], }); gl.drawElements(gl.TRIANGLES, __classPrivateFieldGet(this, _WebglCanvas_quadBuffer, "f").numElements, __classPrivateFieldGet(this, _WebglCanvas_quadBuffer, "f").elementType, 0); }, _WebglCanvas_createProgram = function _WebglCanvas_createProgram(vertexShaderSource, fragmentShaderSource) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const vert = __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createShader).call(this, gl.VERTEX_SHADER, vertexShaderSource); const frag = __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_createShader).call(this, gl.FRAGMENT_SHADER, fragmentShaderSource); const program = gl.createProgram(); // set up shaders gl.attachShader(program, vert); gl.attachShader(program, frag); // link program gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const log = gl.getProgramInfoLog(program); gl.deleteProgram(program); throw new Error(log); } const programInfo = createProgramInfoFromProgram(gl, program); __classPrivateFieldGet(this, _WebglCanvas_refs, "f").programs.push(program); return programInfo; }, _WebglCanvas_createShader = function _WebglCanvas_createShader(type, source) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); // test if shader compilation was successful if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Error(log); } __classPrivateFieldGet(this, _WebglCanvas_refs, "f").shaders.push(shader); return shader; }, _WebglCanvas_createScreenQuad = function _WebglCanvas_createScreenQuad(x0, y0, width, height, xSubdivs, ySubdivs) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const numVerts = (xSubdivs + 1) * (ySubdivs + 1); const numVertsAcross = xSubdivs + 1; const positions = new Float32Array(numVerts * 2); const texCoords = new Float32Array(numVerts * 2); let positionPtr = 0; let texCoordPtr = 0; for (let y = 0; y <= ySubdivs; y++) { for (let x = 0; x <= xSubdivs; x++) { const u = x / xSubdivs; const v = y / ySubdivs; positions[positionPtr++] = x0 + width * u; positions[positionPtr++] = y0 + height * v; texCoords[texCoordPtr++] = u; texCoords[texCoordPtr++] = v; } } const indices = new Uint16Array(xSubdivs * ySubdivs * 2 * 3); let indicesPtr = 0; for (let y = 0; y < ySubdivs; y++) { for (let x = 0; x < xSubdivs; x++) { // triangle 1 indices[indicesPtr++] = (y + 0) * numVertsAcross + x; indices[indicesPtr++] = (y + 1) * numVertsAcross + x; indices[indicesPtr++] = (y + 0) * numVertsAcross + x + 1; // triangle 2 indices[indicesPtr++] = (y + 0) * numVertsAcross + x + 1; indices[indicesPtr++] = (y + 1) * numVertsAcross + x; indices[indicesPtr++] = (y + 1) * numVertsAcross + x + 1; } } const bufferInfo = createBufferInfoFromArrays(this.gl, { position: { numComponents: 2, data: positions }, texcoord: { numComponents: 2, data: texCoords }, indices: indices }); // collect references to buffer objects for (let name in bufferInfo.attribs) __classPrivateFieldGet(this, _WebglCanvas_refs, "f").buffers.push(bufferInfo.attribs[name].buffer); return bufferInfo; }, _WebglCanvas_setBuffersAndAttribs = function _WebglCanvas_setBuffersAndAttribs(program, buffer) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; setBuffersAndAttributes(this.gl, program.attribSetters, buffer); }, _WebglCanvas_createTexture = function _WebglCanvas_createTexture(type, minMag, wrap, width = 1, height = 1) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minMag); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, minMag); gl.texImage2D(gl.TEXTURE_2D, 0, type, width, height, 0, type, gl.UNSIGNED_BYTE, null); __classPrivateFieldGet(this, _WebglCanvas_refs, "f").textures.push(tex); __classPrivateFieldGet(this, _WebglCanvas_textureTypes, "f").set(tex, type); __classPrivateFieldGet(this, _WebglCanvas_textureSizes, "f").set(tex, { width, height }); return tex; }, _WebglCanvas_resizeTexture = function _WebglCanvas_resizeTexture(texture, width, height) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const textureType = __classPrivateFieldGet(this, _WebglCanvas_textureTypes, "f").get(texture); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, textureType, width, height, 0, textureType, gl.UNSIGNED_BYTE, null); __classPrivateFieldGet(this, _WebglCanvas_textureSizes, "f").set(texture, { width, height }); }, _WebglCanvas_createFramebuffer = function _WebglCanvas_createFramebuffer(texture) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); __classPrivateFieldGet(this, _WebglCanvas_refs, "f").frameBuffers.push(fb); __classPrivateFieldGet(this, _WebglCanvas_frameBufferTextures, "f").set(fb, texture); return fb; }, _WebglCanvas_useFramebuffer = function _WebglCanvas_useFramebuffer(fb, viewX, viewY, viewWidth, viewHeight) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const gl = this.gl; if (fb === null) { gl.bindFramebuffer(gl.FRAMEBUFFER, null); /** * Firefox on Apple Silicon Macs seems to have some kind of viewport sizing bug that I can't track down. * Details here: https://github.com/jaames/flipnote.js/issues/30#issuecomment-2134602056 * Not sure what's causing it, but this hack fixes it for now. * Need to test whether only specific versions of Firefox are affected, if it's only an Apple Silicon thing, etc, etc... */ if (__classPrivateFieldGet(this, _WebglCanvas_applyFirefoxFix, "f")) { const srcWidth = this.srcWidth; const srcHeight = this.srcHeight; const sx = gl.drawingBufferWidth / srcWidth; const sy = gl.drawingBufferHeight / srcHeight; const adj = srcWidth === 256 ? 1 : 0; // ??????? why viewWidth = gl.drawingBufferWidth * (sx - adj); viewHeight = gl.drawingBufferHeight * (sy - adj); viewX = -(viewWidth - srcWidth * sx); viewY = -(viewHeight - srcHeight * sy); } gl.viewport(viewX ?? 0, viewY ?? 0, viewWidth ?? gl.drawingBufferWidth, viewHeight ?? gl.drawingBufferHeight); } else { const tex = __classPrivateFieldGet(this, _WebglCanvas_frameBufferTextures, "f").get(fb); const { width, height } = __classPrivateFieldGet(this, _WebglCanvas_textureSizes, "f").get(tex); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.viewport(viewX ?? 0, viewY ?? 0, viewWidth ?? width, viewHeight ?? height); } }, _WebglCanvas_resizeFramebuffer = function _WebglCanvas_resizeFramebuffer(fb, width, height) { if (__classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_checkContextLoss).call(this)) return; const texture = __classPrivateFieldGet(this, _WebglCanvas_frameBufferTextures, "f").get(fb); __classPrivateFieldGet(this, _WebglCanvas_instances, "m", _WebglCanvas_resizeTexture).call(this, texture, width, height); }, _WebglCanvas_checkContextLoss = function _WebglCanvas_checkContextLoss() { const isLost = __classPrivateFieldGet(this, _WebglCanvas_isCtxLost, "f") || this.isErrorState(); if (isLost) __classPrivateFieldGet(this, _WebglCanvas_handleContextLoss, "f").call(this); return isLost; }; WebglCanvas.defaultOptions = { onlost: () => { }, onrestored: () => { }, useDpi: true }; var _Html5Canvas_options, _Html5Canvas_srcCanvas, _Html5Canvas_srcCtx, _Html5Canvas_frameImage, _Html5Canvas_paletteBuffer, _Html5Canvas_frameBuffer; /** * Flipnote renderer for the [HTML5 2D canvas API](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) */ class Html5Canvas { static isSupported() { if (!isBrowser) return false; let testCanvas = document.createElement('canvas'); let testCtx = testCanvas.getContext('2d'); const supported = testCtx !== null; testCanvas = null; testCtx = null; return supported; } constructor(parent, width, height, options = {}) { /** * */ this.supportedStereoscopeModes = [ exports.CanvasStereoscopicMode.None ]; /** * */ this.stereoscopeMode = exports.CanvasStereoscopicMode.None; /** * */ this.stereoscopeStrength = 0; _Html5Canvas_options.set(this, void 0); _Html5Canvas_srcCanvas.set(this, void 0); _Html5Canvas_srcCtx.set(this, void 0); _Html5Canvas_frameImage.set(this, void 0); _Html5Canvas_paletteBuffer.set(this, new Uint32Array(16)); _Html5Canvas_frameBuffer.set(this, void 0); assertBrowserEnv(); __classPrivateFieldSet(this, _Html5Canvas_options, { ...Html5Canvas.defaultOptions, ...options }, "f"); this.width = width; this.height = height; this.canvas = document.createElement('canvas'); this.canvas.className = 'FlipnoteCanvas FlipnoteCanvas--html5'; this.ctx = this.canvas.getContext('2d'); __classPrivateFieldSet(this, _Html5Canvas_srcCanvas, document.createElement('canvas'), "f"); __classPrivateFieldSet(this, _Html5Canvas_srcCtx, __classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f").getContext('2d'), "f"); assert(this.ctx !== null && __classPrivateFieldGet(this, _Html5Canvas_srcCtx, "f") !== null, 'Could not create HTML5 canvas'); if (parent) parent.appendChild(this.canvas); this.setCanvasSize(width, height); } /** * Resize the canvas surface * @param width - New canvas width, in CSS pixels * @param height - New canvas height, in CSS pixels * * The ratio between `width` and `height` should be 3:4 for best results */ setCanvasSize(width, height) { const canvas = this.canvas; const useDpi = __classPrivateFieldGet(this, _Html5Canvas_options, "f").useDpi; const dpi = useDpi ? (window.devicePixelRatio || 1) : 1; const internalWidth = width * dpi; const internalHeight = height * dpi; this.width = width; this.height = height; this.dstWidth = internalWidth; this.dstHeight = internalHeight; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; canvas.width = internalWidth; canvas.height = internalHeight; } /** */ setNote(note) { const width = note.imageWidth; const height = note.imageHeight; this.note = note; this.srcWidth = width; this.srcHeight = height; __classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f").width = width; __classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f").height = height; // create image data to fit note size __classPrivateFieldSet(this, _Html5Canvas_frameImage, __classPrivateFieldGet(this, _Html5Canvas_srcCtx, "f").createImageData(width, height), "f"); // uint32 view of the img buffer memory __classPrivateFieldSet(this, _Html5Canvas_frameBuffer, new Uint32Array(__classPrivateFieldGet(this, _Html5Canvas_frameImage, "f").data.buffer), "f"); this.frameIndex = undefined; // set canvas alt text this.canvas.title = note.getTitle(); } /** * Clear the canvas * @param color optional RGBA color to use as a background color */ clear(color) { // clear framebuffer __classPrivateFieldGet(this, _Html5Canvas_frameBuffer, "f").fill(0); // clear canvas this.ctx.clearRect(0, 0, this.dstWidth, this.dstHeight); // fill canvas with paper color if (color) { const [r, g, b, a] = color; this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; this.ctx.fillRect(0, 0, this.dstWidth, this.dstHeight); } } drawFrame(frameIndex) { // clear whatever's already been drawn this.clear(); // optionally enable image smoothing if (!__classPrivateFieldGet(this, _Html5Canvas_options, "f").useSmoothing) this.ctx.imageSmoothingEnabled = false; // get frame pixels as RGBA buffer this.note.getFramePixelsRgba(frameIndex, __classPrivateFieldGet(this, _Html5Canvas_frameBuffer, "f"), __classPrivateFieldGet(this, _Html5Canvas_paletteBuffer, "f")); // put framebuffer pixels into the src canvas __classPrivateFieldGet(this, _Html5Canvas_srcCtx, "f").putImageData(__classPrivateFieldGet(this, _Html5Canvas_frameImage, "f"), 0, 0); // composite src canvas to dst (so image scaling can be handled) this.ctx.drawImage(__classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f"), 0, 0, this.srcWidth, this.srcHeight, 0, 0, this.dstWidth, this.dstHeight); this.frameIndex = frameIndex; } requestStereoScopeMode(mode) { if (this.supportedStereoscopeModes.includes(mode)) this.stereoscopeMode = mode; else this.stereoscopeMode = exports.CanvasStereoscopicMode.None; this.forceUpdate(); } forceUpdate() { if (this.frameIndex !== undefined) this.drawFrame(this.frameIndex); } getDataUrl(type, quality) { return this.canvas.toDataURL(type, quality); } async getBlob(type, quality) { return new Promise((resolve, reject) => this.canvas.toBlob(resolve, type, quality)); } destroy() { __classPrivateFieldSet(this, _Html5Canvas_frameImage, null, "f"); __classPrivateFieldSet(this, _Html5Canvas_paletteBuffer, null, "f"); __classPrivateFieldSet(this, _Html5Canvas_frameBuffer, null, "f"); this.canvas.parentNode.removeChild(this.canvas); this.canvas.width = 1; this.canvas.height = 1; this.canvas = null; __classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f").width = 1; __classPrivateFieldGet(this, _Html5Canvas_srcCanvas, "f").height = 1; __classPrivateFieldSet(this, _Html5Canvas_srcCanvas, null, "f"); } } _Html5Canvas_options = new WeakMap(), _Html5Canvas_srcCanvas = new WeakMap(), _Html5Canvas_srcCtx = new WeakMap(), _Html5Canvas_frameImage = new WeakMap(), _Html5Canvas_paletteBuffer = new WeakMap(), _Html5Canvas_frameBuffer = new WeakMap(); Html5Canvas.defaultOptions = { useDpi: true, useSmoothing: true, }; var _UniversalCanvas_instances, _UniversalCanvas_rendererStack, _UniversalCanvas_rendererStackIdx, _UniversalCanvas_parent, _UniversalCanvas_options, _UniversalCanvas_setSubRenderer; class UniversalCanvas { constructor(parent, width = 640, height = 480, options = {}) { _UniversalCanvas_instances.add(this); /** * */ this.isReady = false; /** * */ this.isHtml5 = false; /** * */ this.supportedStereoscopeModes = []; /** * */ this.stereoscopeMode = exports.CanvasStereoscopicMode.None; /** * */ this.stereoscopeStrength = 1; _UniversalCanvas_rendererStack.set(this, [ WebglCanvas, Html5Canvas ]); _UniversalCanvas_rendererStackIdx.set(this, 0); _UniversalCanvas_parent.set(this, void 0); _UniversalCanvas_options.set(this, {}); this.width = width; this.height = height; __classPrivateFieldSet(this, _UniversalCanvas_parent, parent, "f"); __classPrivateFieldSet(this, _UniversalCanvas_options, options, "f"); __classPrivateFieldGet(this, _UniversalCanvas_instances, "m", _UniversalCanvas_setSubRenderer).call(this, __classPrivateFieldGet(this, _UniversalCanvas_rendererStack, "f")[0]); } fallbackIfPossible() { if (__classPrivateFieldGet(this, _UniversalCanvas_rendererStackIdx, "f") >= __classPrivateFieldGet(this, _UniversalCanvas_rendererStack, "f").length) throw new Error('No renderer to fall back to'); __classPrivateFieldSet(this, _UniversalCanvas_rendererStackIdx, __classPrivateFieldGet(this, _UniversalCanvas_rendererStackIdx, "f") + 1, "f"); __classPrivateFieldGet(this, _UniversalCanvas_instances, "m", _UniversalCanvas_setSubRenderer).call(this, __classPrivateFieldGet(this, _UniversalCanvas_rendererStack, "f")[__classPrivateFieldGet(this, _UniversalCanvas_rendererStackIdx, "f")]); } // for backwards compat switchToHtml5() { __classPrivateFieldGet(this, _UniversalCanvas_instances, "m", _UniversalCanvas_setSubRenderer).call(this, Html5Canvas); } setCanvasSize(width, height) { const renderer = this.renderer; renderer.setCanvasSize(width, height); this.width = width; this.width = height; this.dstWidth = renderer.dstWidth; this.dstHeight = renderer.dstHeight; } setNote(note) { this.note = note; this.renderer.setNote(note); this.frameIndex = undefined; this.srcWidth = this.renderer.srcWidth; this.srcHeight = this.renderer.srcHeight; } clear(color) { this.renderer.clear(color); } drawFrame(frameIndex) { this.renderer.drawFrame(frameIndex); this.frameIndex = frameIndex; } forceUpdate() { this.renderer.forceUpdate(); } requestStereoScopeMode(mode) { this.renderer.requestStereoScopeMode(mode); this.stereoscopeMode = this.renderer.stereoscopeMode; } getDataUrl(type, quality) { return this.renderer.getDataUrl(); } async getBlob(type, quality) { return this.renderer.getBlob(); } destroy() { this.renderer.destroy(); this.note = null; } } _UniversalCanvas_rendererStack = new WeakMap(), _UniversalCanvas_rendererStackIdx = new WeakMap(), _UniversalCanvas_parent = new WeakMap(), _UniversalCanvas_options = new WeakMap(), _UniversalCanvas_instances = new WeakSet(), _UniversalCanvas_setSubRenderer = function _UniversalCanvas_setSubRenderer(Canvas) { let immediateLoss = false; const renderer = new Canvas(__classPrivateFieldGet(this, _UniversalCanvas_parent, "f"), this.width, this.height, { ...__classPrivateFieldGet(this, _UniversalCanvas_options, "f"), onlost: () => { immediateLoss = true; this.fallbackIfPossible(); } }); // if onlost was called immediately, we succeed to the fallback if (immediateLoss) return; if (this.note) { renderer.setNote(this.note); renderer.frameIndex = this.renderer?.frameIndex; renderer.forceUpdate(); } if (this.renderer) this.renderer.destroy(); this.isHtml5 = renderer instanceof Html5Canvas; this.isReady = true; this.renderer = renderer; __classPrivateFieldSet(this, _UniversalCanvas_rendererStackIdx, __classPrivateFieldGet(this, _UniversalCanvas_rendererStack, "f").indexOf(Canvas), "f"); this.supportedStereoscopeModes = renderer.supportedStereoscopeModes; renderer.stereoscopeStrength = this.stereoscopeStrength; this.requestStereoScopeMode(this.stereoscopeMode); }; var _WebAudioPlayer_instances, _WebAudioPlayer_volume, _WebAudioPlayer_loop, _WebAudioPlayer_startTime, _WebAudioPlayer_ctxStartTime, _WebAudioPlayer_nodeRefs, _WebAudioPlayer_buffer, _WebAudioPlayer_gainNode, _WebAudioPlayer_source, _WebAudioPlayer_getCtx, _WebAudioPlayer_connectEqNodesTo, _WebAudioPlayer_initNodes; /** * @internal */ const _AudioContext = (() => { if (isBrowser) return (window.AudioContext || window.webkitAudioContext); return null; })(); /** * Audio player built around the {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API | Web Audio API}. * * Capable of playing PCM streams, with volume adjustment and an optional equalizer. Only available in browser contexts. */ class WebAudioPlayer { constructor() { _WebAudioPlayer_instances.add(this); /** * Whether the audio is being run through an equalizer or not. */ this.useEq = false; /** * Whether to connect the output to an audio analyser (see {@link analyser}). */ this.useAnalyser = false; /** * Default equalizer settings. Credit to {@link https://www.sudomemo.net/ | Sudomemo} for these. */ this.eqSettings = [ [31.25, 4.1], [62.5, 1.2], [125, 0], [250, -4.1], [500, -2.3], [1000, 0.5], [2000, 6.5], [8000, 5.1], [16000, 5.1] ]; _WebAudioPlayer_volume.set(this, 1); _WebAudioPlayer_loop.set(this, false); _WebAudioPlayer_startTime.set(this, 0); _WebAudioPlayer_ctxStartTime.set(this, 0); _WebAudioPlayer_nodeRefs.set(this, []); _WebAudioPlayer_buffer.set(this, void 0); _WebAudioPlayer_gainNode.set(this, void 0); _WebAudioPlayer_source.set(this, void 0); assertBrowserEnv(); } /** * The audio output volume. Range is 0 to 1. */ set volume(value) { this.setVolume(value); } get volume() { return __classPrivateFieldGet(this, _WebAudioPlayer_volume, "f"); } /** * Whether the audio should loop after it has ended */ set loop(value) { __classPrivateFieldSet(this, _WebAudioPlayer_loop, value, "f"); if (__classPrivateFieldGet(this, _WebAudioPlayer_source, "f")) __classPrivateFieldGet(this, _WebAudioPlayer_source, "f").loop = value; } get loop() { return __classPrivateFieldGet(this, _WebAudioPlayer_loop, "f"); } /** * Set the audio buffer to play * @param inputBuffer * @param sampleRate - For best results, this should be a multiple of 16364 */ setBuffer(inputBuffer, sampleRate) { const ctx = __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_getCtx).call(this); const numSamples = inputBuffer.length; const audioBuffer = ctx.createBuffer(1, numSamples, sampleRate); const channelData = audioBuffer.getChannelData(0); if (inputBuffer instanceof Float32Array) channelData.set(inputBuffer, 0); else if (inputBuffer instanceof Int16Array) { for (let i = 0; i < numSamples; i++) { channelData[i] = inputBuffer[i] / 32768; } } __classPrivateFieldSet(this, _WebAudioPlayer_buffer, audioBuffer, "f"); this.sampleRate = sampleRate; } setAnalyserEnabled(on) { this.useAnalyser = on; __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_initNodes).call(this); } /** * Sets the audio volume level * @param value - range is 0 to 1 */ setVolume(value) { __classPrivateFieldSet(this, _WebAudioPlayer_volume, value, "f"); if (__classPrivateFieldGet(this, _WebAudioPlayer_gainNode, "f")) { // human perception of loudness is logarithmic, rather than linear // https://www.dr-lex.be/info-stuff/volumecontrols.html __classPrivateFieldGet(this, _WebAudioPlayer_gainNode, "f").gain.value = Math.pow(value, 2); } } /** * Begin playback from a specific point * * Note that the WebAudioPlayer doesn't keep track of audio playback itself. We rely on the {@link Player} API for that. * @param currentTime initial playback position, in seconds */ playFrom(currentTime) { __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_initNodes).call(this); __classPrivateFieldSet(this, _WebAudioPlayer_startTime, currentTime, "f"); __classPrivateFieldSet(this, _WebAudioPlayer_ctxStartTime, this.ctx.currentTime, "f"); __classPrivateFieldGet(this, _WebAudioPlayer_source, "f").loop = __classPrivateFieldGet(this, _WebAudioPlayer_loop, "f"); __classPrivateFieldGet(this, _WebAudioPlayer_source, "f").start(0, currentTime); } /** * Stops the audio playback */ stop() { if (__classPrivateFieldGet(this, _WebAudioPlayer_source, "f")) __classPrivateFieldGet(this, _WebAudioPlayer_source, "f").stop(0); } /** * Get the current playback time, in seconds */ getCurrentTime() { return __classPrivateFieldGet(this, _WebAudioPlayer_startTime, "f") + (this.ctx.currentTime - __classPrivateFieldGet(this, _WebAudioPlayer_ctxStartTime, "f")); } /** * Frees any resources used by this canvas instance */ async destroy() { this.stop(); const ctx = __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_getCtx).call(this); __classPrivateFieldGet(this, _WebAudioPlayer_nodeRefs, "f").forEach(node => node.disconnect()); __classPrivateFieldSet(this, _WebAudioPlayer_nodeRefs, [], "f"); this.analyser = undefined; if (ctx.state !== 'closed' && typeof ctx.close === 'function') await ctx.close(); __classPrivateFieldSet(this, _WebAudioPlayer_buffer, null, "f"); } } _WebAudioPlayer_volume = new WeakMap(), _WebAudioPlayer_loop = new WeakMap(), _WebAudioPlayer_startTime = new WeakMap(), _WebAudioPlayer_ctxStartTime = new WeakMap(), _WebAudioPlayer_nodeRefs = new WeakMap(), _WebAudioPlayer_buffer = new WeakMap(), _WebAudioPlayer_gainNode = new WeakMap(), _WebAudioPlayer_source = new WeakMap(), _WebAudioPlayer_instances = new WeakSet(), _WebAudioPlayer_getCtx = function _WebAudioPlayer_getCtx() { if (!this.ctx) this.ctx = new _AudioContext(); return this.ctx; }, _WebAudioPlayer_connectEqNodesTo = function _WebAudioPlayer_connectEqNodesTo(inNode) { const ctx = __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_getCtx).call(this); const eqSettings = this.eqSettings; let lastNode = inNode; eqSettings.forEach(([frequency, gain], index) => { const node = ctx.createBiquadFilter(); __classPrivateFieldGet(this, _WebAudioPlayer_nodeRefs, "f").push(node); node.frequency.value = frequency; node.gain.value = gain; if (index === 0) node.type = 'lowshelf'; else if (index === eqSettings.length - 1) node.type = 'highshelf'; else node.type = 'peaking'; lastNode.connect(node); lastNode = node; }); return lastNode; }, _WebAudioPlayer_initNodes = function _WebAudioPlayer_initNodes() { const ctx = __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_getCtx).call(this); __classPrivateFieldSet(this, _WebAudioPlayer_nodeRefs, [], "f"); const source = ctx.createBufferSource(); __classPrivateFieldGet(this, _WebAudioPlayer_nodeRefs, "f").push(source); source.buffer = __classPrivateFieldGet(this, _WebAudioPlayer_buffer, "f"); const gainNode = ctx.createGain(); __classPrivateFieldGet(this, _WebAudioPlayer_nodeRefs, "f").push(gainNode); if (this.useEq) { const eq = __classPrivateFieldGet(this, _WebAudioPlayer_instances, "m", _WebAudioPlayer_connectEqNodesTo).call(this, source); eq.connect(gainNode); } else { source.connect(gainNode); } if (this.useAnalyser) { const analyserNode = ctx.createAnalyser(); __classPrivateFieldGet(this, _WebAudioPlayer_nodeRefs, "f").push(analyserNode); this.analyser = analyserNode; gainNode.connect(analyserNode); analyserNode.connect(ctx.destination); } else { this.analyser = undefined; gainNode.connect(ctx.destination); } __classPrivateFieldSet(this, _WebAudioPlayer_source, source, "f"); __classPrivateFieldSet(this, _WebAudioPlayer_gainNode, gainNode, "f"); this.setVolume(__classPrivateFieldGet(this, _WebAudioPlayer_volume, "f")); }; var _Player_instances, _Player_playAudio, _Player_stopAudio; /** * Flipnote Player API (exported as `flipnote.Player`) - provides a {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement | MediaElement}-like interface for loading Flipnotes and playing them. * This is intended for cases where you want to implement your own player UI, if you just want a pre-built player with some nice UI controls, check out the [Web Components](/web-components/) page instead! * * ### Create a new player * * You'll need an element in your page's HTML to act as a wrapper for the player: * * ```html *
* ``` * * Then you can create a new `Player` instance by passing a CSS selector that matches the wrapper element, plus the desired width and height. * * ```js * const player = new flipnote.Player('#player-wrapper', 320, 240); * ``` * * ### Load a Flipnote * * Load a Flipnote from a valid {@link FlipnoteSource}: * * ```js * player.load('./path/to/flipnote.ppm'); * ``` * * ### Listen to events * * Use the {@link on} method to register event listeners: * * ```js * player.on('play', function() { * // do something when the Flipnote starts playing... * }); * ``` */ class Player { /** * Create a new Player instance * * @param parent - Element to mount the rendering canvas to * @param width - Canvas width (pixels) * @param height - Canvas height (pixels) * * The ratio between `width` and `height` should be 3:4 for best results */ constructor(parent, width, height, parserSettings = {}) { _Player_instances.add(this); /** * Animation duration, in seconds */ this.duration = 0; /** * Automatically begin playback after a Flipnote is loaded */ this.autoplay = false; /** * Array of events supported by this player */ this.supportedEvents = supportedEvents; /** * @internal */ this._src = null; /** * @internal */ this._loop = false; /** * @internal */ this._volume = 1; /** * @internal */ this._muted = false; /** * @internal */ this._frame = null; /** * @internal */ this._hasEnded = false; /** * @internal */ this.isNoteLoaded = false; /** * @internal */ this.events = new Map(); /** * @internal */ this.playbackStartTime = 0; /** * @internal */ this.playbackTime = 0; /** * @internal */ this.playbackLoopId = null; /** * @internal */ this.showThumbnail = true; /** * @internal */ this.hasPlaybackStarted = false; /** * @internal */ this.isPlaying = false; /** * @internal */ this.wasPlaying = false; /** * @internal */ this.isSeeking = false; /** * Playback animation loop * @internal * @group Playback Control */ this.playbackLoop = (timestamp) => { if (!this.isPlaying) return; const now = timestamp / 1000; const duration = this.duration; const currAudioTime = this.audio.getCurrentTime(); let currPlaybackTime = now - this.playbackStartTime; // try to keep playback time in sync with the audio if there's any slipping if (Math.abs((currPlaybackTime % duration) - (currAudioTime % duration)) > 0.01) currPlaybackTime = currAudioTime; // handle playback end, if reached if (currPlaybackTime >= duration) { if (this.loop) { this.playbackStartTime = now; this.emit(exports.PlayerEvent.Loop); } else { this.pause(); this._hasEnded = true; this.emit(exports.PlayerEvent.Ended); return; } } this.setCurrentTime(currPlaybackTime % duration); this.playbackLoopId = requestAnimationFrame(this.playbackLoop); }; assertBrowserEnv(); // if parent is a string, use it to select an Element, else assume it's an Element const mountPoint = ('string' == typeof parent) ? document.querySelector(parent) : parent; this.parserSettings = parserSettings; this.renderer = new UniversalCanvas(mountPoint, width, height, { onlost: () => this.emit(exports.PlayerEvent.Error), onrestored: () => this.reload() }); this.audio = new WebAudioPlayer(); this.el = mountPoint; } /** * The currently loaded Flipnote source, if there is one * @group Lifecycle * @deprecated */ get src() { return this._src; } set src(source) { throw new Error('Setting a Player source has been deprecated, please use the load() method instead'); } /** * Indicates whether playback is currently paused * @group Playback Control */ get paused() { return !this.isPlaying; } set paused(isPaused) { if (isPaused) this.pause(); else this.play(); } /** * Current animation frame index. * @group Playback Control */ get currentFrame() { return this._frame; } set currentFrame(frameIndex) { this.setCurrentFrame(frameIndex); } /** * Current animation playback position, in seconds. * @group Playback Control */ get currentTime() { return this.isNoteLoaded ? this.playbackTime : null; } set currentTime(value) { this.setCurrentTime(value); } /** * Current animation playback progress, as a percentage out of 100. * @group Playback Control */ get progress() { return this.isNoteLoaded ? (this.playbackTime / this.duration) * 100 : null; } set progress(value) { this.setProgress(value); } /** * Audio volume, range `0` to `1`. * @group Audio Control */ get volume() { return this.getVolume(); } set volume(value) { this.setVolume(value); } /** * Audio mute state. * @group Audio Control */ get muted() { return this.getMuted(); } set muted(value) { this.setMuted(value); } /** * Indicates whether playback should loop once the end is reached * @group Playback Control */ get loop() { return this.getLoop(); } set loop(value) { this.setLoop(value); } /** * Animation frame rate, measured in frames per second. * @group Playback Control */ get framerate() { return this.note.framerate; } /** * Animation frame count. * @group Frame Control */ get frameCount() { return this.note.frameCount; } /** * Animation frame speed. * @group Frame Control */ get frameSpeed() { return this.note.frameSpeed; } /** * Implementation of the `HTMLMediaElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered | buffered } property. * @group HTMLVideoElement compatibility */ get buffered() { return createTimeRanges([[0, this.duration]]); } /** * Implementation of the `HTMLMediaElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable | seekable} property * @group HTMLVideoElement compatibility */ get seekable() { return createTimeRanges([[0, this.duration]]); } /** * Implementation of the `HTMLMediaElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentSrc | currentSrc} property * @group HTMLVideoElement compatibility */ get currentSrc() { return this._src; } /** * Implementation of the `HTMLVideoElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth | videoWidth} property * @group HTMLVideoElement compatibility */ get videoWidth() { return this.isNoteLoaded ? this.note.imageWidth : 0; } /** * Implementation of the `HTMLVideoElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight | videoHeight} property * @group HTMLVideoElement compatibility */ get videoHeight() { return this.isNoteLoaded ? this.note.imageHeight : 0; } /** * Open a Flipnote from a source * @group Lifecycle */ async load(source) { // close currently open note first if (this.isNoteLoaded) this.closeNote(); // keep track of source this._src = source; // if no source specified, just reset everything if (!source) return this.openNote(this.note); // otherwise do a normal load this.emit(exports.PlayerEvent.LoadStart); const [err, note] = await until(() => parse(source, this.parserSettings)); if (err) { this.emit(exports.PlayerEvent.Error, err); throw new Error(`Error loading Flipnote: ${err.message}`); } this.openNote(note); } /** * Reload the current Flipnote. * @group Lifecycle */ async reload() { if (this.note) return await this.load(this.note.buffer); } /** * Reload the current Flipnote, applying new parser settings. * @group Lifecycle */ async updateSettings(settings) { this.parserSettings = settings; return await this.reload(); } /** * Close the currently loaded Flipnote * @group Lifecycle */ closeNote() { this.pause(); this.note = null; this.isNoteLoaded = false; this.meta = null; this._src = null; this._frame = 0; // this.playbackFrame = null; this.playbackTime = 0; this.duration = 0; this.loop = false; this.isPlaying = false; this.wasPlaying = false; this.hasPlaybackStarted = false; this.showThumbnail = true; this.renderer.clear(); } /** * Open a Flipnote into the player * @group Lifecycle */ openNote(note) { if (this.isNoteLoaded) this.closeNote(); this.note = note; this.meta = note.meta; this.emit(exports.PlayerEvent.LoadedMeta); this.noteFormat = note.format; this.duration = note.duration; this.playbackTime = 0; this._frame = 0; this.isNoteLoaded = true; this.isPlaying = false; this.wasPlaying = false; this.hasPlaybackStarted = false; this.layerVisibility = note.layerVisibility; this.showThumbnail = true; this.audio.setBuffer(note.getAudioMasterPcm(), note.sampleRate); this.emit(exports.PlayerEvent.CanPlay); this.emit(exports.PlayerEvent.CanPlayThrough); this.setLoop(note.meta.loop); this.renderer.setNote(note); this.drawFrame(note.thumbFrameIndex); this.emit(exports.PlayerEvent.LoadedData); this.emit(exports.PlayerEvent.Load); this.emit(exports.PlayerEvent.Ready); if (this.autoplay) this.play(); } /** * Set the current playback time * @group Playback Control */ setCurrentTime(value) { this.assertNoteLoaded(); const i = Math.floor(value / (1 / this.framerate)); this.setCurrentFrame(i); this.playbackTime = value; this.emit(exports.PlayerEvent.Progress, this.progress); } /** * Get the current playback time * @group Playback Control */ getCurrentTime() { return this.currentTime; } /** * Get the current time as a counter string, like `"0:00 / 1:00"` * @group Playback Control */ getTimeCounter() { const currentTime = formatTime(Math.max(this.currentTime, 0)); const duration = formatTime(this.duration); return `${currentTime} / ${duration}`; } /** * Get the current frame index as a counter string, like `"001 / 999"` * @group Playback Control */ getFrameCounter() { const frame = padNumber(this.currentFrame + 1, 3); const total = padNumber(this.frameCount, 3); return `${frame} / ${total}`; } /** * Set the current playback progress as a percentage (`0` to `100`) * @group Playback Control */ setProgress(value) { this.assertNoteLoaded(); assertRange(value, 0, 100, 'progress'); this.currentTime = this.duration * (value / 100); } /** * Get the current playback progress as a percentage (0 to 100) * @group Playback Control */ getProgress() { return this.progress; } /** * Begin animation playback starting at the current position * @group Playback Control */ async play() { this.assertNoteLoaded(); if (this.isPlaying) return; // if the flipnote hasn't looped and is at the end, rewind it to 0 if (this._hasEnded) { this.playbackTime = 0; this._hasEnded = false; } const now = performance.now(); this.playbackStartTime = (now / 1000) - this.playbackTime; this.isPlaying = true; this.hasPlaybackStarted = true; __classPrivateFieldGet(this, _Player_instances, "m", _Player_playAudio).call(this); this.playbackLoop(now); this.emit(exports.PlayerEvent.Play); } /** * Pause animation playback at the current position * @group Playback Control */ pause() { if (!this.isPlaying) return; this.isPlaying = false; if (this.playbackLoopId !== null) cancelAnimationFrame(this.playbackLoopId); __classPrivateFieldGet(this, _Player_instances, "m", _Player_stopAudio).call(this); this.emit(exports.PlayerEvent.Pause); } /** * Resumes animation playback if paused, otherwise pauses * @group Playback Control */ togglePlay() { if (!this.isPlaying) this.play(); else this.pause(); } /** * Determines if playback is currently paused * @group Playback Control */ getPaused() { return !this.isPlaying; } /** * Get the duration of the Flipnote in seconds * @group Playback Control */ getDuration() { return this.duration; } /** * Determines if playback is looped * @group Playback Control */ getLoop() { return this._loop; } /** * Set the playback loop * @group Playback Control */ setLoop(loop) { this._loop = loop; this.audio.loop = loop; } /** * Switch the playback loop between on and off * @group Playback Control */ toggleLoop() { this.setLoop(!this._loop); } /** * Jump to a given animation frame * @group Frame Control */ setCurrentFrame(newFrameValue) { this.assertNoteLoaded(); const newFrameIndex = Math.max(0, Math.min(Math.floor(newFrameValue), this.frameCount - 1)); if (newFrameIndex === this.currentFrame && !this.showThumbnail) return; this._frame = newFrameIndex; this.drawFrame(newFrameIndex); this.showThumbnail = false; if (!this.isPlaying) { this.playbackTime = newFrameIndex * (1 / this.framerate); this.emit(exports.PlayerEvent.SeekEnd); } this.emit(exports.PlayerEvent.FrameUpdate, this.currentFrame); this.emit(exports.PlayerEvent.Progress, this.progress); this.emit(exports.PlayerEvent.TimeUpdate, this.currentFrame); } /** * Jump to the next animation frame * If the animation loops, and is currently on its last frame, it will wrap to the first frame * @group Frame Control */ nextFrame() { if ((this.loop) && (this.currentFrame === this.frameCount - 1)) this.currentFrame = 0; else this.currentFrame += 1; this.emit(exports.PlayerEvent.FrameNext); } /** * Jump to the next animation frame * If the animation loops, and is currently on its first frame, it will wrap to the last frame * @group Frame Control */ prevFrame() { if ((this.loop) && (this.currentFrame === 0)) this.currentFrame = this.frameCount - 1; else this.currentFrame -= 1; this.emit(exports.PlayerEvent.FramePrev); } /** * Jump to the last animation frame * @group Frame Control */ lastFrame() { this.currentFrame = this.frameCount - 1; this.emit(exports.PlayerEvent.FrameLast); } /** * Jump to the first animation frame * @group Frame Control */ firstFrame() { this.currentFrame = 0; this.emit(exports.PlayerEvent.FrameFirst); } /** * Jump to the thumbnail frame * @group Frame Control */ thumbnailFrame() { this.currentFrame = this.note.thumbFrameIndex; } /** * Begins a seek operation * @group Playback Control */ startSeek() { if (!this.isSeeking) { this.emit(exports.PlayerEvent.SeekStart); this.wasPlaying = this.isPlaying; this.pause(); this.isSeeking = true; } } /** * Seek the playback progress to a different position * @param position - animation playback position, range `0` to `1` * @group Playback Control */ seek(position) { if (this.isSeeking) this.progress = position * 100; } /** * Ends a seek operation * @group Playback Control */ endSeek() { if (this.isSeeking && this.wasPlaying === true) this.play(); this.wasPlaying = false; this.isSeeking = false; } /** * Draws the specified animation frame to the canvas. Note that this doesn't update the playback time or anything, it simply decodes a given frame and displays it. * @param frameIndex * @group Display Control */ drawFrame(frameIndex) { this.renderer.drawFrame(frameIndex); } /** * Forces the current animation frame to be redrawn * @group Display Control */ forceUpdate() { this.renderer.forceUpdate(); } /** * Resize the playback canvas to a new size * @param width - new canvas width (pixels) * @param height - new canvas height (pixels) * * The ratio between `width` and `height` should be 3:4 for best results * * @group Display Control */ resize(width, height) { if (height !== width * .75) console.warn(`Canvas width to height ratio should be 3:4 for best results (got ${width}x${height})`); this.renderer.setCanvasSize(width, height); this.forceUpdate(); } /** * Sets whether an animation layer should be visible throughout the entire animation * @param layer - layer index, starting at 1 * @param value - `true` for visible, `false` for invisible * * @group Display Control */ setLayerVisibility(layer, value) { this.note.layerVisibility[layer] = value; this.layerVisibility[layer] = value; this.forceUpdate(); } /** * Returns the visibility state for a given layer * @param layer - layer index, starting at 1 * * @group Display Control */ getLayerVisibility(layer) { return this.layerVisibility[layer]; } /** * Toggles whether an animation layer should be visible throughout the entire animation * * @group Display Control */ toggleLayerVisibility(layerIndex) { this.setLayerVisibility(layerIndex, !this.layerVisibility[layerIndex]); } /** * Toggle audio Sudomemo equalizer filter. * @group Audio Control */ toggleAudioEq() { this.setAudioEq(!this.audio.useEq); } /** * Turn audio Sudomemo equalizer filter on or off. * @group Audio Control */ setAudioEq(state) { if (this.isPlaying) { this.wasPlaying = true; __classPrivateFieldGet(this, _Player_instances, "m", _Player_stopAudio).call(this); } this.audio.useEq = state; if (this.wasPlaying) { this.wasPlaying = false; __classPrivateFieldGet(this, _Player_instances, "m", _Player_playAudio).call(this); } } /** * Turn the audio off * @group Audio Control */ mute() { this.setMuted(true); } /** * Turn the audio on * @group Audio Control */ unmute() { this.setMuted(false); } /** * Turn the audio on or off * @group Audio Control */ setMuted(isMute) { if (isMute) this.audio.volume = 0; else this.audio.volume = this._volume; this._muted = isMute; this.emit(exports.PlayerEvent.VolumeChange, this.audio.volume); } /** * Get the audio mute state * @group Audio Control */ getMuted() { return this.volume === 0 ? true : this._muted; } /** * Switch the audio between muted and unmuted * @group Audio Control */ toggleMuted() { this.setMuted(!this._muted); } /** * Set the audio volume * @group Audio Control */ setVolume(volume) { assertRange(volume, 0, 1, 'volume'); this._volume = volume; this.audio.volume = volume; this.emit(exports.PlayerEvent.VolumeChange, this.audio.volume); } /** * Get the current audio volume * @group Audio Control */ getVolume() { return this._muted ? 0 : this._volume; } /** * Implementation of the `HTMLVideoElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/seekToNextFrame | seekToNextFrame} method * @group HTMLVideoElement compatibility */ seekToNextFrame() { this.nextFrame(); } /** * Implementation of the `HTMLMediaElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/fastSeek | fastSeek} method * @group HTMLVideoElement compatibility */ fastSeek(time) { this.currentTime = time; } /** * Implementation of the `HTMLVideoElement` {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/getVideoPlaybackQuality | getVideoPlaybackQuality } method * @group HTMLVideoElement compatibility */ canPlayType(mediaType) { switch (mediaType) { case 'application/x-ppm': case 'application/x-kwz': case 'video/x-ppm': case 'video/x-kwz': // lauren is planning on registering these officially case 'video/vnd.nintendo.ugomemo.ppm': case 'video/vnd.nintendo.ugomemo.kwz': return 'probably'; case 'application/octet-stream': return 'maybe'; default: return ''; } } /** * Implementation of the `HTMLVideoElement` [https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/getVideoPlaybackQuality](getVideoPlaybackQuality) method * @group HTMLVideoElement compatibility */ getVideoPlaybackQuality() { const quality = { creationTime: 0, droppedVideoFrames: 0, // @ts-ignore corruptedVideoFrames: 0, totalVideoFrames: this.frameCount }; return quality; } /** * Implementation of the `HTMLVideoElement` [https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture](requestPictureInPicture) method. Not currently working, only a stub. * @group HTMLVideoElement compatibility */ requestPictureInPicture() { throw new Error('Not implemented'); } /** * Implementation of the `HTMLVideoElement` [https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/captureStream](captureStream) method. Not currently working, only a stub. * @group HTMLVideoElement compatibility */ captureStream() { throw new Error('Not implemented'); } /** * Add an event callback * @group Event API */ on(eventType, listener) { const events = this.events; const eventList = Array.isArray(eventType) ? eventType : [eventType]; eventList.forEach(eventType => { if (!events.has(eventType)) events.set(eventType, [listener]); else events.get(eventType).push(listener); }); } /** * Remove an event callback * @group Event API */ off(eventType, callback) { const events = this.events; const eventList = Array.isArray(eventType) ? eventType : [eventType]; eventList.forEach(eventType => { if (!events.has(eventType)) return; const callbackList = events.get(eventType); events.set(eventType, callbackList.splice(callbackList.indexOf(callback), 1)); }); } /** * Emit an event - mostly used internally * @group Event API */ emit(eventType, ...args) { const events = this.events; if (eventType !== exports.PlayerEvent.__Any && events.has(eventType)) { const callbackList = events.get(eventType); callbackList.forEach(callback => callback.apply(null, args)); // call onwhatever() function for this event, if one has been added const listenerName = `on${eventType}`; const thisAsAny = this; if (typeof thisAsAny[listenerName] === 'function') thisAsAny[listenerName].apply(null, args); } // "any" event listeners fire for all events, and receive eventType as their first param if (events.has(exports.PlayerEvent.__Any)) { const callbackList = events.get(exports.PlayerEvent.__Any); callbackList.forEach(callback => callback.apply(null, [eventType, ...args])); } } /** * Remove all registered event callbacks * @group Event API */ clearEvents() { this.events.clear(); } /** * Destroy a Player instance * @group Lifecycle */ async destroy() { this.clearEvents(); this.emit(exports.PlayerEvent.Destroy); this.closeNote(); await this.renderer.destroy(); await this.audio.destroy(); } /** * @internal */ assertNoteLoaded() { assert(this.isNoteLoaded, 'No Flipnote is currently loaded in this player'); } } _Player_instances = new WeakSet(), _Player_playAudio = function _Player_playAudio() { this.audio.playFrom(this.currentTime); }, _Player_stopAudio = function _Player_stopAudio() { this.audio.stop(); }; /** * This is a bit of a hack to get a player component class to wrap a Player instance, * while also inheriting all of the Player API's methods and properties. * * The resulting PlayerMixinClass will get a Player instance on this.player, * and all of the Player API methods and properties applied as wrappers. * * e.g. * - PlayerMixinClass.play() will have the same behavior as Player.play(), but will call this.player.play() internally. * - PlayerMixinClass.paused will have getters and setters to match it to this.player.paused. * @internal */ function PlayerMixin(Target) { class PlayerMixinClass extends Target { // Mixin needs to re-define all the normal player properties, but most should be made readonly anyway... get renderer() { return this.player.renderer; } get note() { return this.player.note; } get noteFormat() { return this.player.noteFormat; } get meta() { return this.player.meta; } get duration() { return this.player.duration; } get layerVisibility() { return this.player.layerVisibility; } get autoplay() { return this.player.autoplay; } set autoplay(value) { this.player.autoplay = value; } } // add all Player API methods and getter/setter props to target for (let key of Reflect.ownKeys(Player.prototype)) { let desc = Object.getOwnPropertyDescriptor(Player.prototype, key); // don't override stuff that already exists, and ignore JS prototype junk if (key in Target.prototype || key === 'constructor' || key === 'name' || key === 'prototype') continue; // ignore private props if (typeof key === "string" && key.startsWith("#")) continue; // override methods to call e.g. `this.player.methodName()` when `methodName()` is called if (desc.value && typeof desc.value === 'function') { Object.defineProperty(PlayerMixinClass.prototype, key, { ...desc, value(...args) { return this.player[key](...args); } }); } // override getters and setters so that e.g. `property` will always reflect `this.player.property` else if (desc.get || desc.set) { Object.defineProperty(PlayerMixinClass.prototype, key, { ...desc, set(value) { this.player[key] = value; }, get() { return this.player[key]; } }); } } return PlayerMixinClass; } class EncoderBase { constructor() { this.dataUrl = null; } /** * Returns the file data as a NodeJS {@link https://nodejs.org/api/buffer.html | Buffer}. * * > Note: This method does not work outside of NodeJS environments. */ getBuffer() { assertNodeEnv(); return Buffer.from(this.getArrayBuffer()); } /** * Returns the file data as a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob | Blob}. */ getBlob() { assertBrowserEnv(); return new Blob([this.getArrayBuffer()], { type: this.mimeType }); } /** * Returns the file data as an {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL | Object URL}. * * > Note: This method does not work outside of browser environments. */ getUrl() { assertBrowserEnv(); if (this.dataUrl) return this.dataUrl; return window.URL.createObjectURL(this.getBlob()); } /** * Revokes this file's {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL | Object URL} if one has been created, use this when the url created with {@link getUrl} is no longer needed, to preserve memory. * * > Note: This method does not work outside of browser environments. */ revokeUrl() { assertBrowserEnv(); if (this.dataUrl) window.URL.revokeObjectURL(this.dataUrl); } } /* LZWEncoder.js Authors Kevin Weiner (original Java version - kweiner@fmsware.com) Thibault Imbert (AS3 version - bytearray.org) Johan Nordberg (JS version - code@johan-nordberg.com) James Daniel (ES6/TS version) Acknowledgements GIFCOMPR.C - GIF Image compression routines Lempel-Ziv compression based on 'compress'. GIF modifications by David Rowley (mgardi@watdcsu.waterloo.edu) GIF Image compression - modified 'compress' Based on: compress.c - File compression ala IEEE Computer, June 1984. By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) Jim McKie (decvax!mcvax!jim) Steve Davies (decvax!vax135!petsd!peora!srd) Ken Turkowski (decvax!decwrl!turtlevax!ken) James A. Woods (decvax!ihnp4!ames!jaw) Joe Orost (decvax!vax135!petsd!joe) */ /** * @internal */ const EOF = -1; /** * @internal */ const BITS = 12; /** * @internal */ const HSIZE = 5003; // 80% occupancy /** * @internal */ const masks = [ 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF ]; /** * @internal */ class LzwCompressor { constructor(width, height, colorDepth) { this.accum = new Uint8Array(256); this.htab = new Int32Array(HSIZE); this.codetab = new Int32Array(HSIZE); this.cur_accum = 0; this.cur_bits = 0; this.curPixel = 0; this.free_ent = 0; // first unused entry // block compression parameters -- after all codes are used up, // and compression rate changes, start over. this.clear_flg = false; // Algorithm: use open addressing double hashing (no chaining) on the // prefix code / next character combination. We do a variant of Knuth's // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime // secondary probe. Here, the modular division first probe is gives way // to a faster exclusive-or manipulation. Also do block compression with // an adaptive reset, whereby the code table is cleared when the compression // ratio decreases, but after the table fills. The variable-length output // codes are re-sized at this point, and a special CLEAR code is generated // for the decompressor. Late addition: construct the table according to // file size for noticeable speed improvement on small files. Please direct // questions about this implementation to ames!jaw. this.g_init_bits = undefined; this.ClearCode = undefined; this.EOFCode = undefined; this.width = width; this.height = height; this.colorDepth = colorDepth; this.reset(); } reset() { this.initCodeSize = Math.max(2, this.colorDepth); this.accum.fill(0); this.htab.fill(0); this.codetab.fill(0); this.cur_accum = 0; this.cur_bits = 0; this.curPixel = 0; this.free_ent = 0; // first unused entry this.maxcode; // block compression parameters -- after all codes are used up, // and compression rate changes, start over. this.clear_flg = false; // Algorithm: use open addressing double hashing (no chaining) on the // prefix code / next character combination. We do a variant of Knuth's // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime // secondary probe. Here, the modular division first probe is gives way // to a faster exclusive-or manipulation. Also do block compression with // an adaptive reset, whereby the code table is cleared when the compression // ratio decreases, but after the table fills. The variable-length output // codes are re-sized at this point, and a special CLEAR code is generated // for the decompressor. Late addition: construct the table according to // file size for noticeable speed improvement on small files. Please direct // questions about this implementation to ames!jaw. this.g_init_bits = undefined; this.ClearCode = undefined; this.EOFCode = undefined; } // Add a character to the end of the current packet, and if it is 254 // characters, flush the packet to disk. char_out(c, outs) { this.accum[this.a_count++] = c; if (this.a_count >= 254) this.flush_char(outs); } // Clear out the hash table // table clear for block compress cl_block(outs) { this.cl_hash(HSIZE); this.free_ent = this.ClearCode + 2; this.clear_flg = true; this.output(this.ClearCode, outs); } // Reset code table cl_hash(hsize) { for (var i = 0; i < hsize; ++i) this.htab[i] = -1; } compress(init_bits, outs) { var fcode, c, i, ent, disp, hsize_reg, hshift; // Set up the globals: this.g_init_bits - initial number of bits this.g_init_bits = init_bits; // Set up the necessary values this.clear_flg = false; this.n_bits = this.g_init_bits; this.maxcode = this.get_maxcode(this.n_bits); this.ClearCode = 1 << (init_bits - 1); this.EOFCode = this.ClearCode + 1; this.free_ent = this.ClearCode + 2; this.a_count = 0; // clear packet ent = this.nextPixel(); hshift = 0; for (fcode = HSIZE; fcode < 65536; fcode *= 2) ++hshift; hshift = 8 - hshift; // set hash code range bound hsize_reg = HSIZE; this.cl_hash(hsize_reg); // clear hash table this.output(this.ClearCode, outs); outer_loop: while ((c = this.nextPixel()) != EOF) { fcode = (c << BITS) + ent; i = (c << hshift) ^ ent; // xor hashing if (this.htab[i] === fcode) { ent = this.codetab[i]; continue; } else if (this.htab[i] >= 0) { // non-empty slot disp = hsize_reg - i; // secondary hash (after G. Knott) if (i === 0) { disp = 1; } do { if ((i -= disp) < 0) { i += hsize_reg; } if (this.htab[i] === fcode) { ent = this.codetab[i]; continue outer_loop; } } while (this.htab[i] >= 0); } this.output(ent, outs); ent = c; if (this.free_ent < 1 << BITS) { this.codetab[i] = this.free_ent++; // code -> hasthis.htable this.htab[i] = fcode; } else { this.cl_block(outs); } } // Put out the final code. this.output(ent, outs); this.output(this.EOFCode, outs); } encode(pixels, outs) { this.pixels = pixels; outs.writeByte(this.initCodeSize); // write 'initial code size' byte this.remaining = this.width * this.height; // reset navigation variables this.curPixel = 0; this.compress(this.initCodeSize + 1, outs); // compress and write the pixel data outs.writeByte(0); // write block terminator } // Flush the packet to disk, and reset the this.accumulator flush_char(outs) { if (this.a_count > 0) { outs.writeByte(this.a_count); outs.writeBytes(this.accum, 0, this.a_count); this.a_count = 0; } } get_maxcode(n_bits) { return (1 << n_bits) - 1; } // Return the next pixel from the image nextPixel() { if (this.remaining === 0) return EOF; --this.remaining; var pix = this.pixels[this.curPixel++]; return pix & 0xff; } output(code, outs) { this.cur_accum &= masks[this.cur_bits]; if (this.cur_bits > 0) this.cur_accum |= (code << this.cur_bits); else this.cur_accum = code; this.cur_bits += this.n_bits; while (this.cur_bits >= 8) { this.char_out((this.cur_accum & 0xff), outs); this.cur_accum >>= 8; this.cur_bits -= 8; } // If the next entry is going to be too big for the code size, // then increase it, if possible. if (this.free_ent > this.maxcode || this.clear_flg) { if (this.clear_flg) { this.maxcode = this.get_maxcode(this.n_bits = this.g_init_bits); this.clear_flg = false; } else { ++this.n_bits; if (this.n_bits == BITS) this.maxcode = 1 << BITS; else this.maxcode = this.get_maxcode(this.n_bits); } } if (code == this.EOFCode) { // At EOF, write the rest of the buffer. while (this.cur_bits > 0) { this.char_out((this.cur_accum & 0xff), outs); this.cur_accum >>= 8; this.cur_bits -= 8; } this.flush_char(outs); } } } var _GifImage_instances, _GifImage_data, _GifImage_compressor, _GifImage_writeFirstFrame, _GifImage_writeAdditionalFrame, _GifImage_writeHeader, _GifImage_writeGraphicControlExt, _GifImage_writeLogicalScreenDescriptor, _GifImage_writeNetscapeExt, _GifImage_writeColorTable, _GifImage_writeImageDescriptor, _GifImage_colorTableSize, _GifImage_writePixels; /** * GIF image encoder, supports static single-frame GIF export as well as animated GIF. * * @group File Encoder */ class GifImage extends EncoderBase { /** * Create a new GIF image object. * * @param width Image width * @param height Image height * @param settings Whether the gif should loop, the delay between frames, etc. See {@link GifEncoderSettings} */ constructor(width, height, settings = {}) { super(); _GifImage_instances.add(this); this.mimeType = 'gif/image'; /** * Number of current GIF frames. */ this.frameCount = 0; _GifImage_data.set(this, void 0); _GifImage_compressor.set(this, void 0); this.width = width; this.height = height; __classPrivateFieldSet(this, _GifImage_data, new ByteArray(), "f"); this.settings = { ...GifImage.defaultSettings, ...settings }; __classPrivateFieldSet(this, _GifImage_compressor, new LzwCompressor(width, height, settings.colorDepth), "f"); } /** * Create an animated GIF image from a Flipnote. * * This will encode the entire animation, so depending on the number of frames it could take a while to return. * @param flipnote {@link Flipnote} object ({@link PpmParser} or {@link KwzParser} instance) * @param settings Whether the gif should loop, the delay between frames, etc. See {@link GifEncoderSettings} */ static fromFlipnote(flipnote, settings = {}) { const gif = new GifImage(flipnote.imageWidth, flipnote.imageHeight, { delay: 100 / flipnote.framerate, repeat: flipnote.meta?.loop ? -1 : 0, ...settings }); gif.palette = flipnote.globalPalette; for (let frameIndex = 0; frameIndex < flipnote.frameCount; frameIndex++) gif.writeFrame(flipnote.getFramePixels(frameIndex)); gif.finish(); return gif; } /** * Create an GIF image from a single Flipnote frame. * @param flipnote * @param frameIndex animation frame index to encode * @param settings whether the gif should loop, the delay between frames, etc. See {@link GifEncoderSettings} */ static fromFlipnoteFrame(flipnote, frameIndex, settings = {}) { const gif = new GifImage(flipnote.imageWidth, flipnote.imageHeight, { delay: 0, repeat: 0, ...settings, }); gif.palette = flipnote.globalPalette; gif.writeFrame(flipnote.getFramePixels(frameIndex)); gif.finish(); return gif; } /** * Add a frame to the GIF image. * @param pixels Raw pixels to encode, must be an uncompressed 8bit array of palette indices with a size matching image width * image height */ writeFrame(pixels) { if (this.frameCount === 0) __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeFirstFrame).call(this, pixels); else __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeAdditionalFrame).call(this, pixels); this.frameCount += 1; } /** * Call once all frames have been written to finish the GIF image. */ finish() { __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0x3B); } /** * Returns the GIF image data as an {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer}. */ getArrayBuffer() { return __classPrivateFieldGet(this, _GifImage_data, "f").getBuffer(); } /** * Returns the GIF image data as an {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image | Image} object. * * Note: This method does not work outside of browser environments */ getImage() { assertBrowserEnv(); const img = new Image(this.width, this.height); img.src = this.getUrl(); return img; } } _GifImage_data = new WeakMap(), _GifImage_compressor = new WeakMap(), _GifImage_instances = new WeakSet(), _GifImage_writeFirstFrame = function _GifImage_writeFirstFrame(pixels) { __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeHeader).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeLogicalScreenDescriptor).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeColorTable).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeNetscapeExt).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeGraphicControlExt).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeImageDescriptor).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writePixels).call(this, pixels); }, _GifImage_writeAdditionalFrame = function _GifImage_writeAdditionalFrame(pixels) { __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeGraphicControlExt).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writeImageDescriptor).call(this); __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_writePixels).call(this, pixels); }, _GifImage_writeHeader = function _GifImage_writeHeader() { __classPrivateFieldGet(this, _GifImage_data, "f").writeChars('GIF89a'); }, _GifImage_writeGraphicControlExt = function _GifImage_writeGraphicControlExt() { __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0x21); // extension introducer __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0xf9); // GCE label __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(4); // data block size // packed fields __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0); __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.settings.delay); // delay x 1/100 sec __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0); // transparent color index __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0); // block terminator }, _GifImage_writeLogicalScreenDescriptor = function _GifImage_writeLogicalScreenDescriptor() { const palette = this.palette; const colorDepth = this.settings.colorDepth; const globalColorTableFlag = 1; const sortFlag = 0; const globalColorTableSize = __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_colorTableSize).call(this, palette.length) - 1; const fields = (globalColorTableFlag << 7) | ((colorDepth - 1) << 4) | (sortFlag << 3) | globalColorTableSize; const backgroundColorIndex = 0; const pixelAspectRatio = 0; __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.width); __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.height); __classPrivateFieldGet(this, _GifImage_data, "f").writeBytes([fields, backgroundColorIndex, pixelAspectRatio]); }, _GifImage_writeNetscapeExt = function _GifImage_writeNetscapeExt() { __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0x21); // extension introducer __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0xff); // app extension label __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(11); // block size __classPrivateFieldGet(this, _GifImage_data, "f").writeChars('NETSCAPE2.0'); // app id + auth code __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(3); // sub-block size __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(1); // loop sub-block id __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.settings.repeat); // loop count (extra iterations, 0=repeat forever) __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0); // block terminator }, _GifImage_writeColorTable = function _GifImage_writeColorTable() { const palette = this.palette; const colorTableLength = 1 << __classPrivateFieldGet(this, _GifImage_instances, "m", _GifImage_colorTableSize).call(this, palette.length); for (let i = 0; i < colorTableLength; i++) { let color = [0, 0, 0]; if (i < palette.length) { color = palette[i]; } __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(color[0]); __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(color[1]); __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(color[2]); } }, _GifImage_writeImageDescriptor = function _GifImage_writeImageDescriptor() { __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0x2c); // image separator __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(0); // x position __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(0); // y position __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.width); // image size __classPrivateFieldGet(this, _GifImage_data, "f").writeU16(this.height); __classPrivateFieldGet(this, _GifImage_data, "f").writeByte(0); // global palette }, _GifImage_colorTableSize = function _GifImage_colorTableSize(length) { return Math.max(Math.ceil(Math.log2(length)), 1); }, _GifImage_writePixels = function _GifImage_writePixels(pixels) { __classPrivateFieldGet(this, _GifImage_compressor, "f").colorDepth = this.settings.colorDepth; __classPrivateFieldGet(this, _GifImage_compressor, "f").reset(); __classPrivateFieldGet(this, _GifImage_compressor, "f").encode(pixels, __classPrivateFieldGet(this, _GifImage_data, "f")); }; /** * Default GIF encoder settings */ GifImage.defaultSettings = { delay: 100, repeat: -1, colorDepth: 8 }; var _WavAudio_header, _WavAudio_pcmData; /** * Wav audio object. Used to create a {@link https://en.wikipedia.org/wiki/WAV | WAV} file from a PCM audio stream or a {@link Flipnote} object. * * Currently only supports PCM s16_le audio encoding. * * @group File Encoder */ class WavAudio extends EncoderBase { /** * Create a new WAV audio object * @param sampleRate audio samplerate * @param channels number of audio channels * @param bitsPerSample number of bits per sample */ constructor(sampleRate, channels = 1, bitsPerSample = 16) { super(); _WavAudio_header.set(this, void 0); _WavAudio_pcmData.set(this, void 0); this.sampleRate = sampleRate; this.channels = channels; this.bitsPerSample = bitsPerSample; // Write WAV file header // Reference: http://www.topherlee.com/software/pcm-tut-wavformat.html const headerBuffer = new ArrayBuffer(44); const header = new DataStream(headerBuffer); // 'RIFF' indent header.writeChars('RIFF'); // filesize (set later) header.writeUint32(0); // 'WAVE' indent header.writeChars('WAVE'); // 'fmt ' section header header.writeChars('fmt '); // fmt section length header.writeUint32(16); // specify audio format is pcm (type 1) header.writeUint16(1); // number of audio channels header.writeUint16(this.channels); // audio sample rate header.writeUint32(this.sampleRate); // byterate = (sampleRate * bitsPerSample * channelCount) / 8 header.writeUint32((this.sampleRate * this.bitsPerSample * this.channels) / 8); // blockalign = (bitsPerSample * channels) / 8 header.writeUint16((this.bitsPerSample * this.channels) / 8); // bits per sample header.writeUint16(this.bitsPerSample); // 'data' section header header.writeChars('data'); // data section length (set later) header.writeUint32(0); __classPrivateFieldSet(this, _WavAudio_header, header, "f"); __classPrivateFieldSet(this, _WavAudio_pcmData, null, "f"); } /** * Create a WAV audio file from a Flipnote's master audio track * @param flipnote * @param trackId */ static fromFlipnote(note, sampleRate = null) { sampleRate ?? (sampleRate = note.sampleRate); const wav = new WavAudio(sampleRate, 1, 16); const pcm = note.getAudioMasterPcm(sampleRate); wav.writeSamples(pcm); return wav; } /** * Create a WAV audio file from a given Flipnote audio track * @param flipnote * @param trackId */ static fromFlipnoteTrack(flipnote, trackId, sampleRate = null) { sampleRate ?? (sampleRate = flipnote.sampleRate); const wav = new WavAudio(sampleRate, 1, 16); const pcm = flipnote.getAudioTrackPcm(trackId, sampleRate); wav.writeSamples(pcm); return wav; } /** * Add PCM audio frames to the WAV * @param pcmData signed int16 PCM audio samples */ writeSamples(pcmData) { let header = __classPrivateFieldGet(this, _WavAudio_header, "f"); // fill in filesize header.seek(4); header.writeUint32(header.numBytes + pcmData.byteLength); // fill in data section length header.seek(40); header.writeUint32(pcmData.byteLength); __classPrivateFieldSet(this, _WavAudio_pcmData, pcmData, "f"); } /** * Returns the WAV audio data as an {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer} */ getArrayBuffer() { const headerBytes = __classPrivateFieldGet(this, _WavAudio_header, "f").bytes; const pcmBytes = new Uint8Array(__classPrivateFieldGet(this, _WavAudio_pcmData, "f").buffer); const resultBytes = new Uint8Array(__classPrivateFieldGet(this, _WavAudio_header, "f").numBytes + __classPrivateFieldGet(this, _WavAudio_pcmData, "f").byteLength); resultBytes.set(headerBytes); resultBytes.set(pcmBytes, headerBytes.byteLength); return resultBytes.buffer; } } _WavAudio_header = new WeakMap(), _WavAudio_pcmData = new WeakMap(); // Entrypoint for web and node /** * flipnote.js library version (exported as `flipnote.version`). * You can find the latest version on the project's [NPM](https://www.npmjs.com/package/flipnote.js) page. */ const version = "6.3.0"; // replaced by @rollup/plugin-replace; /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$4=globalThis,e$7=t$4.ShadowRoot&&(void 0===t$4.ShadyCSS||t$4.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$2=Symbol(),o$5=new WeakMap;let n$4 = class n{constructor(t,e,o){if(this._$cssResult$=true,o!==s$2)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$7&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$5.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$5.set(s,t));}return t}toString(){return this.cssText}};const r$5=t=>new n$4("string"==typeof t?t:t+"",void 0,s$2),i$4=(t,...e)=>{const o=1===t.length?t[0]:e.reduce(((e,s,o)=>e+(t=>{if(true===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1]),t[0]);return new n$4(o,t,s$2)},S$1=(s,o)=>{if(e$7)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$4.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$2=e$7?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$5(e)})(t):t; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const{is:i$3,defineProperty:e$6,getOwnPropertyDescriptor:r$4,getOwnPropertyNames:h$1,getOwnPropertySymbols:o$4,getPrototypeOf:n$3}=Object,a$1=globalThis,c$1=a$1.trustedTypes,l$1=c$1?c$1.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$1=(t,s)=>!i$3(t,s),y$1={attribute:true,type:String,converter:u$1,reflect:false,hasChanged:f$1};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;class b extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=y$1){if(s.state&&(s.attribute=false),this._$Ei(),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),r=this.getPropertyDescriptor(t,i,s);void 0!==r&&e$6(this.prototype,t,r);}}static getPropertyDescriptor(t,s,i){const{get:e,set:h}=r$4(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get(){return e?.call(this)},set(s){const r=e?.call(this);h.call(this,s),this.requestUpdate(t,r,i);},configurable:true,enumerable:true}}static getPropertyOptions(t){return this.elementProperties.get(t)??y$1}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$3(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...h$1(t),...o$4(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$2(s));}else void 0!==s&&i.push(c$2(s));return i}static _$Eu(t,s){const i=s.attribute;return false===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=false,this.hasUpdated=false,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)));}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$EO?.delete(t);}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(true),this._$EO?.forEach((t=>t.hostConnected?.()));}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach((t=>t.hostDisconnected?.()));}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$EC(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&true===i.reflect){const r=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==r?this.removeAttribute(e):this.setAttribute(e,r),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),r="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e,this[e]=r.fromAttribute(s,t.type),this._$Em=null;}}requestUpdate(t,s,i){if(void 0!==t){if(i??=this.constructor.getPropertyOptions(t),!(i.hasChanged??f$1)(this[t],s))return;this.P(t,s,i);} false===this.isUpdatePending&&(this._$ES=this._$ET());}P(t,s,i){this._$AL.has(t)||this._$AL.set(t,s),true===i.reflect&&this._$Em!==t&&(this._$Ej??=new Set).add(t);}async _$ET(){this.isUpdatePending=true;try{await this._$ES;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t) true!==i.wrapped||this._$AL.has(s)||void 0===this[s]||this.P(s,this[s],i);}let t=false;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$EU();}catch(s){throw t=false,this._$EU(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$EO?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=true,this.firstUpdated(t)),this.updated(t);}_$EU(){this._$AL=new Map,this.isUpdatePending=false;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return true}update(t){this._$Ej&&=this._$Ej.forEach((t=>this._$EC(t,this[t]))),this._$EU();}updated(t){}firstUpdated(t){}}b.elementStyles=[],b.shadowRootOptions={mode:"open"},b[d$1("elementProperties")]=new Map,b[d$1("finalized")]=new Map,p$1?.({ReactiveElement:b}),(a$1.reactiveElementVersions??=[]).push("2.0.4"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$3=globalThis,i$2=t$3.trustedTypes,s$1=i$2?i$2.createPolicy("lit-html",{createHTML:t=>t}):void 0,e$5="$lit$",h=`lit$${Math.random().toFixed(9).slice(2)}$`,o$3="?"+h,n$2=`<${o$3}>`,r$3=document,l=()=>r$3.createComment(""),c=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),w=Symbol.for("lit-noChange"),T=Symbol.for("lit-nothing"),A=new WeakMap,E=r$3.createTreeWalker(r$3,129);function C(t,i){if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$1?s$1.createHTML(i):i}const P=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"":"",c=f;for(let i=0;i"===u[0]?(c=r??f,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f?s+n$2:d>=0?(o.push(a),s.slice(0,d)+e$5+s.slice(d)+h+x):s+h+(-2===d?i:x);}return [C(t,l+(t[s]||"")+(2===i?"":"")),o]};class V{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=P(t,s);if(this.el=V.createElement(f,n),E.currentNode=this.el.content,2===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=E.nextNode())&&d.length0){r.textContent=i$2?i$2.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=T;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=N(this,t,i,0),o=!c(t)||t!==this._$AH&&t!==w,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new M(i.insertBefore(l(),t),t,void 0,s??{});}return h._$AI(t),h}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */class s extends b{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=j(i,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return w}}s._$litElement$=true,s[("finalized")]=true,globalThis.litElementHydrateSupport?.({LitElement:s});const r$2=globalThis.litElementPolyfillSupport;r$2?.({LitElement:s});(globalThis.litElementVersions??=[]).push("4.0.5"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$2=t=>(e,o)=>{ void 0!==o?o.addInitializer((()=>{customElements.define(t,e);})):customElements.define(t,e);}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const o$2={attribute:true,type:String,converter:u$1,reflect:false,hasChanged:f$1},r$1=(t=o$2,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),s.set(r.name,t),"accessor"===n){const{name:o}=r;return {set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t);},init(e){return void 0!==e&&this.P(o,void 0,t),e}}}if("setter"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t);}}throw Error("Unsupported decorator location: "+n)};function n$1(t){return (e,o)=>"object"==typeof o?r$1(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,r?{...t,wrapped:true}:t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */function r(r){return n$1({...r,state:true,attribute:false})} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const e$4=(e,t,c)=>(c.configurable=true,c.enumerable=true,Reflect.decorate&&"object"!=typeof t&&Object.defineProperty(e,t,c),c); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */function e$3(e,r){return (n,s,i)=>{const o=t=>t.renderRoot?.querySelector(e)??null;return e$4(n,s,{get(){return o(this)}})}} const KEY_MAP_SLIDER = { 'a': 'decrease', 's': 'decrease', 'd': 'increase', 'w': 'increase', 'ArrowLeft': 'decrease', 'ArrowDown': 'decrease', 'ArrowRight': 'increase', 'ArrowUp': 'increase', }; const KEY_MAP_PLAYER = { ' ': 'playpause', 'Enter': 'playpause', 'a': 'prev', 's': 'prev', 'd': 'next', 'w': 'next', 'ArrowLeft': 'prev', 'ArrowDown': 'prev', 'ArrowRight': 'next', 'ArrowUp': 'next', }; /// /** * @group Web Component * @internal */ exports.PlayerComponent = class PlayerComponent extends PlayerMixin(s) { static get styles() { return i$4 `:host{display:inline-block}.Player{display:inline-block;position:relative;font-family:var(--flipnote-player-font-family,sans-serif)}.CanvasArea{cursor:pointer;position:relative;background:var(--flipnote-player-canvas-background,#fff)}.CanvasArea:focus{outline:var(--flipnote-player-focus-outline,3px solid #ffd3a6);outline-offset:2px}.PlayerCanvas{position:relative;display:block}.PlayerCanvas canvas{display:block}.Overlay{position:absolute;top:0;left:0;background:#ebf0f3;color:#4b4c53;width:100%;height:100%;display:flex;justify-content:center;align-items:center}.Overlay--error{background:#ff8b8b;color:#ca2a32}@keyframes spin{from{transform:rotateZ(0)}to{transform:rotateZ(360deg)}}.LoaderIcon{animation:spin infinite 1.2s linear}.Controls{background:var(--flipnote-player-controls-background,none)}.MuteIcon{width:28px;height:28px;cursor:pointer}.Controls__groupLeft,.Controls__groupRight,.Controls__row{display:flex;align-items:center}.Controls__groupLeft{margin-right:auto}.Controls__groupRight{margin-left:auto}.Controls__playButton{height:32px;width:32px;padding:2px}.MuteIcon{width:28px;height:28px}.LoaderIcon{width:40px;height:40px}.Controls.Controls--compact{margin:6px 0}.Controls__frameCounter{min-width:90px;font-variant-numeric:tabular-nums;cursor:pointer}.Controls__progressBar{flex:1}.Controls--compact .Controls__playButton{margin-right:12px}.Controls--compact .Controls__progressBar{flex:1}.Controls--default .Controls__playButton{margin-right:12px}.Controls--default .Controls__progressBar{margin-top:2px;margin-bottom:2px;display:block}.Controls--default .Controls__volumeBar{width:70px;margin-left:8px}.Button{border:0;padding:0;outline:0;-webkit-appearance:none;display:block;font-family:inherit;font-size:inherit;text-align:center;cursor:pointer;background:var(--flipnote-player-button-background,#ffd3a6);color:var(--flipnote-player-button-color,#f36a2d);border-radius:4px}.Button:focus{outline:3px solid var(--flipnote-player-button-background,#ffd3a6);outline-offset:2px}.Button flipnote-player-icon{display:block}`; } get width() { return this._width; } set width(value) { const oldValue = this._width; this._width = value; // wrangle plain width value to CSS pixel units if possible this._cssWidth = (!isNaN(+value)) ? `${value}px` : value; // wait for the next browser paint (when the CSS value is applied) to handle updating the canvas nextPaint(() => this.updateCanvasSize()); this.requestUpdate('width', oldValue); } get src() { return this._playerSrc; } set src(src) { const oldValue = this._playerSrc; if (this._isPlayerAvailable) this.player.load(src); this._playerSrc = src; this.requestUpdate('src', oldValue); } get autoplay() { return this.player?.autoplay; } set autoplay(value) { if (this.player) { const oldValue = this.player.autoplay; this.player.autoplay = value; this.requestUpdate('autoplay', oldValue); } } constructor() { super(); this._width = 'auto'; this._cssWidth = 'auto'; this._progress = 0; this._counter = ''; this._frameCount = 0; this._isLoading = false; this._isError = false; this._isPlaying = false; this._isMuted = false; this._volumeLevel = 0; this._isPlayerAvailable = false; this._keysDown = {}; this._counterMode = 'frame'; this.handleResize = (entries) => { this.updateCanvasSize(); }; this.handleKeyInputDown = (e) => { const isDownAlready = this._keysDown[e.key]; const action = KEY_MAP_PLAYER[e.key]; if (!action) return; switch (action) { case 'playpause': if (!isDownAlready) // Prevent key holds from triggering rapid play/pause this.togglePlay(); break; case 'prev': if (e.shiftKey) this.firstFrame(); else this.prevFrame(); break; case 'next': if (e.shiftKey) this.lastFrame(); else this.nextFrame(); break; } this._keysDown[e.key] = true; e.preventDefault(); }; this.handleKeyInputUp = (e) => { this._keysDown[e.key] = false; }; this.handlePlayToggle = (e) => { this.focus(); this.togglePlay(); }; this.handleMuteToggle = (e) => { this.focus(); this.toggleMuted(); }; this.handleProgressSliderChange = (e) => { this.focus(); this.seek(e.detail.value); }; this.handleProgressSliderInputStart = () => { this.focus(); this.startSeek(); }; this.handleProgressSliderInputEnd = () => { this.focus(); this.endSeek(); }; this.handleVolumeBarChange = (e) => { this.focus(); this.setVolume(e.detail.value); }; this.handleCounterClick = () => { this._counterMode = this._counterMode === 'frame' ? 'time' : 'frame'; this._counter = this._counterMode === 'frame' ? this.getFrameCounter() : this.getTimeCounter(); }; this._resizeObserver = new ResizeObserver(this.handleResize); } /** * @internal */ render() { return x `
${this._isLoading ? x `
` : ''} ${this._isError ? x `
Error
` : ''}
${this.renderControls()}
`; } /** * @internal */ renderControls() { if (this.controls === 'compact') { return x `
`; } else { return x `
${this._counter}
`; } } /** * @internal */ firstUpdated(changedProperties) { this.updateSettingsFromProps(); const player = new Player(this.playerCanvasWrapper, 256, 192, this.parserSettings); this._resizeObserver.observe(this); this.player = player; player.on(exports.PlayerEvent.LoadStart, () => { this._isLoading = true; }); player.on(exports.PlayerEvent.Load, () => { this.updateCanvasSize(); }); player.on(exports.PlayerEvent.Error, () => { this._isLoading = false; this._isError = true; }); player.on([exports.PlayerEvent.Load, exports.PlayerEvent.Close, exports.PlayerEvent.Progress], () => { this._isLoading = false; this._isError = false; this._progress = player.getProgress() / 100; this._counter = this._counterMode === 'frame' ? player.getFrameCounter() : player.getTimeCounter(); this._frameCount = player.frameCount; }); player.on(exports.PlayerEvent.Play, () => { this._isPlaying = true; }); player.on(exports.PlayerEvent.Pause, () => { this._isPlaying = false; }); player.on([exports.PlayerEvent.Load, exports.PlayerEvent.VolumeChange], () => { this._volumeLevel = player.volume; this._isMuted = player.muted; }); // catch any player event and dispatch it as a DOM event player.on(exports.PlayerEvent.__Any, (eventName, args) => { this.dispatchEvent(new Event(eventName)); }); if (this._playerSrc) player.load(this._playerSrc); this._isPlayerAvailable = true; } // TODO: get this to actually work so that prop updates update parser settings // /**@internal */ // async updated(changedProperties: PropertyValues) { // let hasReloaded = false; // let settingProps = ['dsiLibrary', 'cropBorder', 'bgmPredictor', 'bgmStepIndex', 'sePredictor', 'seStepIndex']; // for (let [key, value] of changedProperties) { // if ((!hasReloaded) && settingProps.includes(key as string)) { // console.log(key, 'updated'); // this.updateSettingsFromProps(); // await this.player.reload(); // hasReloaded = true; // } // } // } /** * @internal */ disconnectedCallback() { // disable resize observer this._resizeObserver.disconnect(); // clean up webgl and buffer stuff if this element is removed from DOM this.destroy(); } updateSettingsFromProps() { this.parserSettings = { dsiLibraryNote: this.dsiLibrary, borderCrop: this.cropBorder, initialBgmPredictor: this.bgmPredictor, initialBgmStepIndex: this.bgmStepIndex, initialSePredictors: this.parseListProp(this?.sePredictors), initialSeStepIndices: this.parseListProp(this?.seStepIndices), }; } parseListProp(propValue) { if (!propValue) return undefined; // split string into segments on instances of ",", check if segment is empty, if not, convert to num, else undefined return propValue.split(',').map(x => !(/^\s*$/.test(x)) ? parseInt(x) : undefined); } updateCanvasSize() { const isPlayerAvailable = this._isPlayerAvailable; const note = this.player.note; // default width is DSi note size let canvasWidth = 256; // use the Flipnote's native width if (this._width === 'auto' && isPlayerAvailable && this.player.isNoteLoaded) canvasWidth = note.imageWidth; // expand to fill the full container width else if (this._width !== 'auto') canvasWidth = this.getBoundingClientRect().width; // force flipnote aspect ratio if (isPlayerAvailable && note) this.player.resize(canvasWidth, canvasWidth * note.aspect); } }; __decorate([ n$1({ type: String }) ], exports.PlayerComponent.prototype, "controls", void 0); __decorate([ n$1({ type: Boolean }) ], exports.PlayerComponent.prototype, "dsiLibrary", void 0); __decorate([ n$1({ type: Boolean }) ], exports.PlayerComponent.prototype, "cropBorder", void 0); __decorate([ n$1({ type: Number }) ], exports.PlayerComponent.prototype, "bgmPredictor", void 0); __decorate([ n$1({ type: Number }) ], exports.PlayerComponent.prototype, "bgmStepIndex", void 0); __decorate([ n$1({ type: String }) ], exports.PlayerComponent.prototype, "sePredictors", void 0); __decorate([ n$1({ type: String }) ], exports.PlayerComponent.prototype, "seStepIndices", void 0); __decorate([ n$1({ type: String }) ], exports.PlayerComponent.prototype, "width", null); __decorate([ n$1({ type: String }) ], exports.PlayerComponent.prototype, "src", null); __decorate([ n$1({ type: Boolean }) ], exports.PlayerComponent.prototype, "autoplay", null); __decorate([ r() ], exports.PlayerComponent.prototype, "_width", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_cssWidth", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_progress", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_counter", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_frameCount", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_isLoading", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_isError", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_isPlaying", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_isMuted", void 0); __decorate([ r() ], exports.PlayerComponent.prototype, "_volumeLevel", void 0); __decorate([ e$3('#canvasWrapper') ], exports.PlayerComponent.prototype, "playerCanvasWrapper", void 0); exports.PlayerComponent = __decorate([ t$2('flipnote-player') ], exports.PlayerComponent); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const t$1={ATTRIBUTE:1,CHILD:2},e$2=t=>(...e)=>({_$litDirective$:t,values:e});let i$1 = class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i;}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}; /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const e$1=e$2(class extends i$1{constructor(t){if(super(t),t.type!==t$1.ATTRIBUTE||"class"!==t.name||t.strings?.length>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(t){return " "+Object.keys(t).filter((s=>t[s])).join(" ")+" "}update(s,[i]){if(void 0===this.st){this.st=new Set,void 0!==s.strings&&(this.nt=new Set(s.strings.join(" ").split(/\s/).filter((t=>""!==t))));for(const t in i)i[t]&&!this.nt?.has(t)&&this.st.add(t);return this.render(i)}const r=s.element.classList;for(const t of this.st)t in i||(r.remove(t),this.st.delete(t));for(const t in i){const s=!!i[t];s===this.st.has(t)||this.nt?.has(t)||(s?(r.add(t),this.st.add(t)):(r.remove(t),this.st.delete(t)));}return w}}); /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */const n="important",i=" !"+n,o$1=e$2(class extends i$1{constructor(t){if(super(t),t.type!==t$1.ATTRIBUTE||"style"!==t.name||t.strings?.length>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce(((e,r)=>{const s=t[r];return null==s?e:e+`${r=r.includes("-")?r:r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`}),"")}update(e,[r]){const{style:s}=e.element;if(void 0===this.ft)return this.ft=new Set(Object.keys(r)),this.render(r);for(const t of this.ft)null==r[t]&&(this.ft.delete(t),t.includes("-")?s.removeProperty(t):s[t]=null);for(const t in r){const e=r[t];if(null!=e){this.ft.add(t);const r="string"==typeof e&&e.endsWith(i);t.includes("-")||r?s.setProperty(t,r?e.slice(0,-11):e,r?n:""):s[t]=e;}}return w}}); const round = (value, precision = 2) => Math.round(value * 10 ** precision) / 10 ** precision; const formatPercent = (value, max, precision = 1) => round((value / max) * 100, precision) + '%'; class PartMapDirective extends i$1 { render(partInfo) { return Object.keys(partInfo) .filter((key) => partInfo[key]) .join(' '); } } const partMap = e$2(PartMapDirective); /** * @group Web Component * @internal */ let SliderComponent = class SliderComponent extends s { constructor() { super(...arguments); this.value = 0; this.step = 0.1; this.label = ''; this.orientation = 'horizontal'; // switch to horizontal this.isActive = false; this.onSliderMouseStart = (event) => { event.preventDefault(); this.isActive = true; document.addEventListener('mousemove', this.onSliderMouseMove); document.addEventListener('mouseup', this.onSliderMouseEnd); this.dispatch('inputstart'); this.onSliderInput(event.clientX, event.clientY); }; this.onSliderMouseEnd = (event) => { event.preventDefault(); document.removeEventListener('mousemove', this.onSliderMouseMove); document.removeEventListener('mouseup', this.onSliderMouseEnd); this.dispatch('inputend'); this.onSliderInput(event.clientX, event.clientY); this.isActive = false; }; this.onSliderMouseMove = (event) => { event.preventDefault(); this.onSliderInput(event.clientX, event.clientY); }; this.onSliderTouchStart = (event) => { const point = event.changedTouches[0]; event.preventDefault(); this.isActive = true; document.addEventListener('touchmove', this.onSliderTouchMove); document.addEventListener('touchend', this.onSliderTouchEnd); this.dispatch('inputstart'); this.onSliderInput(point.clientX, point.clientY); }; this.onSliderTouchEnd = (event) => { const point = event.changedTouches[0]; event.preventDefault(); document.removeEventListener('touchmove', this.onSliderTouchMove); document.removeEventListener('touchend', this.onSliderTouchEnd); this.dispatch('inputend'); this.onSliderInput(point.clientX, point.clientY); this.isActive = false; }; this.onSliderTouchMove = (event) => { const point = event.changedTouches[0]; event.preventDefault(); this.onSliderInput(point.clientX, point.clientY); }; this.onSliderInput = (x, y) => { const rect = this.sliderElement.getBoundingClientRect(); let value; if (this.orientation === 'horizontal') { const railCap = rect.height / 2; const railLength = rect.width - railCap * 2; const inputPosition = x - rect.left - railCap; const v = inputPosition / railLength; value = Math.max(0, Math.min(v, 1)); } else if (this.orientation === 'vertical') { const railCap = rect.width / 2; const railLength = rect.height - railCap * 2; const inputPosition = y - rect.top - railCap; const v = 1 - inputPosition / railLength; // y is inverted; top is the max point value = Math.max(0, Math.min(v, 1)); } this.updateValue(value); }; this.onKeyInput = (e) => { const action = KEY_MAP_SLIDER[e.key]; if (!action) return; this.dispatch('inputstart'); switch (action) { case 'decrease': if (e.shiftKey) this.updateValue(0); else this.updateValue(this.value - this.step); break; case 'increase': if (e.shiftKey) this.updateValue(1); else this.updateValue(this.value + this.step); break; } this.dispatch('inputend'); e.preventDefault(); }; this.updateValue = (value) => { value = clamp(value, 0, 1); if (this.value !== value) { this.value = value; this.dispatch('change', { value }); } }; } static get styles() { return i$4 `.Slider{touch-action:none;padding:4px 0;cursor:pointer}.Slider:focus{position:relative;z-index:1;outline:0}.Slider--vertical{height:100px;width:14px}.Slider__track{position:relative;border-radius:3px;background:var(--flipnote-player-slider-track,#ffd3a6)}.Slider:focus .Slider__track{outline:var(--flipnote-player-focus-outline,3px solid #ffd3a6);outline-offset:3px}.Slider--horizontal .Slider__track{height:4px;margin:6px 0}.Slider--vertical .Slider__track{width:4px;height:100%;margin:0 6px}.Slider__levelWrapper{position:absolute;left:0;right:0;height:6px;margin:-1px}.Slider__level{position:absolute;width:100%;left:0;height:8px;border-radius:8px;background:var(--flipnote-player-slider-level,#f36a2d)}.Slider--horizontal .Slider__level{width:100%;height:6px}.Slider--vertical .Slider__level{width:6px;height:100%;bottom:0}.Slider__handle{display:none;position:absolute;height:10px;width:10px;border-radius:5px;box-sizing:border-box;border:3px solid var(--flipnote-player-slider-handle,#f36a2d);background:var(--flipnote-player-slider-handle-fill,#fff)}.Slider--isActive .Slider__handle,.Slider:focus .Slider__handle,.Slider:hover .Slider__handle{display:block}.Slider--horizontal .Slider__handle{top:0;margin-top:-3px;margin-left:-6px}.Slider--vertical .Slider__handle{left:0;margin-bottom:-6px;margin-left:-3px}`; } render() { const percent = `${this.value * 100}%`; const mainAxis = this.orientation === 'horizontal' ? 'width' : 'height'; const side = this.orientation === 'horizontal' ? 'left' : 'bottom'; const rootClasses = { 'Slider': true, 'Slider--horizontal': this.orientation === 'horizontal', 'Slider--vertical': this.orientation === 'vertical', 'Slider--isActive': this.isActive, }; return x `
`; } dispatch(eventName, detail) { const event = new CustomEvent(eventName, { detail }); this.dispatchEvent(event); } }; __decorate([ n$1({ type: Number }) ], SliderComponent.prototype, "value", void 0); __decorate([ n$1({ type: Number }) ], SliderComponent.prototype, "step", void 0); __decorate([ n$1({ type: String }) ], SliderComponent.prototype, "label", void 0); __decorate([ n$1({ type: String }) ], SliderComponent.prototype, "orientation", void 0); __decorate([ r() ], SliderComponent.prototype, "isActive", void 0); __decorate([ e$3('.Slider__track') ], SliderComponent.prototype, "sliderElement", void 0); SliderComponent = __decorate([ t$2('flipnote-player-slider') ], SliderComponent); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */class e extends i$1{constructor(i){if(super(i),this.it=T,i.type!==t$1.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(r){if(r===T||null==r)return this._t=void 0,this.it=r;if(r===w)return r;if("string"!=typeof r)throw Error(this.constructor.directiveName+"() called with a non-string value");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */class t extends e{}t.directiveName="unsafeSVG",t.resultType=2;const o=e$2(t); var IconPlay = ""; var IconPause = ""; var IconLoader = ""; var IconVolumeOn = ""; var IconVolumeOff = ""; /** * @internal */ const patchSvg = (svgString) => svgString.replace(/]*)>/, (match, svgAttrs) => ``); /** * @internal */ const iconMap = { play: patchSvg(IconPlay), pause: patchSvg(IconPause), loader: patchSvg(IconLoader), volumeOn: patchSvg(IconVolumeOn), volumeOff: patchSvg(IconVolumeOff), }; /** * Flipnote player icon component * * @group Web Component * @internal */ let IconComponent = class IconComponent extends s { constructor() { super(...arguments); /** * Available icons: * - `play` * - `pause` * - `loader` * - `volumeOn` * - `volumeOff` */ this.icon = 'loader'; } static get styles() { return i$4 `.Icon{width:100%;height:100%;color:var(--flipnote-player-icon-color,#f36a2d)}`; } /** * @internal */ render() { return x `${o(iconMap[this.icon])}`; } }; __decorate([ n$1({ type: String }) ], IconComponent.prototype, "icon", void 0); IconComponent = __decorate([ t$2('flipnote-player-icon') ], IconComponent); var _ImageComponent_instances, _ImageComponent__src, _ImageComponent__frame, _ImageComponent_revokeUrl, _ImageComponent_dispatchLoad, _ImageComponent_dispatchError; /** * Flipnote player icon component * * @group Web Component * @internal */ exports.ImageComponent = class ImageComponent extends s { constructor() { super(...arguments); _ImageComponent_instances.add(this); _ImageComponent__src.set(this, ''); _ImageComponent__frame.set(this, 'thumb'); this.cropped = false; this._gifUrl = ''; this._imgTitle = ''; } static get styles() { return i$4 `.Image{width:inherit;height:inherit;image-rendering:-moz-crisp-edges;image-rendering:-webkit-crisp-edges;image-rendering:pixelated;image-rendering:crisp-edges;-ms-interpolation-mode:nearest-neighbor}`; } set src(src) { this.load(src); } get src() { return __classPrivateFieldGet(this, _ImageComponent__src, "f"); } set frame(frame) { __classPrivateFieldSet(this, _ImageComponent__frame, frame, "f"); if (this.note) this.loadNote(this.note); } get frame() { return __classPrivateFieldGet(this, _ImageComponent__frame, "f"); } /** * @internal */ render() { return x ``; } loadNote(note) { this.note = note; __classPrivateFieldGet(this, _ImageComponent_instances, "m", _ImageComponent_revokeUrl).call(this); const frame = __classPrivateFieldGet(this, _ImageComponent__frame, "f"); // full animated gif if (frame === 'all') { this.gif = GifImage.fromFlipnote(note); this._gifUrl = this.gif.getUrl(); } // thumbnail frame else if (frame === 'thumb') { this.gif = GifImage.fromFlipnoteFrame(note, note.thumbFrameIndex); this._gifUrl = this.gif.getUrl(); } // if frame is numeric string else if (!isNaN(+frame)) { const frameIndex = parseInt(frame); this.gif = GifImage.fromFlipnoteFrame(note, frameIndex); this._gifUrl = this.gif.getUrl(); } if (this._gifUrl) { __classPrivateFieldGet(this, _ImageComponent_instances, "m", _ImageComponent_dispatchLoad).call(this); this._imgTitle = note.getTitle(); } else { __classPrivateFieldGet(this, _ImageComponent_instances, "m", _ImageComponent_dispatchError).call(this, 'Invalid frame attribute'); } } load(src) { __classPrivateFieldSet(this, _ImageComponent__src, src, "f"); this.note = undefined; const borderCrop = this.getAttribute('cropped') === 'true'; parse(src, { borderCrop }) .then(note => this.loadNote(note)) .catch(err => __classPrivateFieldGet(this, _ImageComponent_instances, "m", _ImageComponent_dispatchError).call(this, err)); } disconnectedCallback() { __classPrivateFieldGet(this, _ImageComponent_instances, "m", _ImageComponent_revokeUrl).call(this); } }; _ImageComponent__src = new WeakMap(); _ImageComponent__frame = new WeakMap(); _ImageComponent_instances = new WeakSet(); _ImageComponent_revokeUrl = function _ImageComponent_revokeUrl() { // if there was already an image, clean up its data URL if (this.gif && this.gif.dataUrl) { this.gif.revokeUrl(); this._gifUrl = ''; } }; _ImageComponent_dispatchLoad = function _ImageComponent_dispatchLoad() { this.dispatchEvent(new Event('load')); }; _ImageComponent_dispatchError = function _ImageComponent_dispatchError(err) { this.dispatchEvent(new ErrorEvent('error', { error: err })); throw new Error(err); }; __decorate([ n$1() ], exports.ImageComponent.prototype, "src", null); __decorate([ n$1() ], exports.ImageComponent.prototype, "frame", null); __decorate([ n$1({ type: Boolean }) ], exports.ImageComponent.prototype, "cropped", void 0); __decorate([ r() ], exports.ImageComponent.prototype, "_gifUrl", void 0); __decorate([ r() ], exports.ImageComponent.prototype, "_imgTitle", void 0); exports.ImageComponent = __decorate([ t$2('flipnote-image') ], exports.ImageComponent); exports.CanvasInterface = CanvasInterface; exports.GifImage = GifImage; exports.Html5Canvas = Html5Canvas; exports.KwzParser = KwzParser; exports.Player = Player; exports.PlayerMixin = PlayerMixin; exports.PpmParser = PpmParser; exports.UniversalCanvas = UniversalCanvas; exports.WavAudio = WavAudio; exports.WebAudioPlayer = WebAudioPlayer; exports.WebglCanvas = WebglCanvas; exports.filename = index$2; exports.id = index$3; exports.loaders = index$1; exports.parse = parse; exports.parseSource = parseSource; exports.playlist = index; exports.supportedEvents = supportedEvents; exports.version = version; return exports; })({}); //# sourceMappingURL=flipnote.webcomponent.js.map