|
| 1 | +const Struct = require('awestruct') |
| 2 | + |
| 3 | +const t = Struct.types |
| 4 | + |
| 5 | +module.exports = SLPEncoder |
| 6 | + |
| 7 | +// SLP commands |
| 8 | +const SLP_END_OF_ROW = 0x0f |
| 9 | +const SLP_COLOR_LIST = 0x00 |
| 10 | +const SLP_COLOR_LIST_EX = 0x02 |
| 11 | +const SLP_COLOR_LIST_PLAYER = 0x06 |
| 12 | +const SLP_SKIP = 0x01 |
| 13 | +const SLP_SKIP_EX = 0x03 |
| 14 | +const SLP_FILL = 0x07 |
| 15 | +const SLP_FILL_PLAYER = 0x0a |
| 16 | +const SLP_SHADOW = 0x0b |
| 17 | +const SLP_EXTENDED = 0x0e |
| 18 | +const SLP_EX_OUTLINE1 = 0x40 |
| 19 | +const SLP_EX_FILL_OUTLINE1 = 0x50 |
| 20 | +const SLP_EX_OUTLINE2 = 0x60 |
| 21 | +const SLP_EX_FILL_OUTLINE2 = 0x70 |
| 22 | +const SLP_LINE_EMPTY = 0x8000 |
| 23 | + |
| 24 | +// Render commands |
| 25 | +const RENDER_NEXTLINE = 0x00 |
| 26 | +const RENDER_COLOR = 0x01 |
| 27 | +const RENDER_SKIP = 0x02 |
| 28 | +const RENDER_PLAYER_COLOR = 0x03 |
| 29 | +const RENDER_SHADOW = 0x04 |
| 30 | +const RENDER_OUTLINE = 0x05 |
| 31 | +const RENDER_FILL = 0x06 |
| 32 | +const RENDER_PLAYER_FILL = 0x07 |
| 33 | + |
| 34 | +// SLP Header |
| 35 | +const headerStruct = Struct({ |
| 36 | + version: t.string(4), |
| 37 | + numFrames: t.int32, |
| 38 | + comment: t.string(24), |
| 39 | + |
| 40 | + frames: t.array('numFrames', Struct({ |
| 41 | + cmdTableOffset: t.uint32, |
| 42 | + outlineTableOffset: t.uint32, |
| 43 | + paletteOffset: t.uint32, |
| 44 | + properties: t.uint32, |
| 45 | + |
| 46 | + width: t.int32, |
| 47 | + height: t.int32, |
| 48 | + hotspot: Struct({ |
| 49 | + x: t.int32, |
| 50 | + y: t.int32 |
| 51 | + }) |
| 52 | + })) |
| 53 | +}) |
| 54 | + |
| 55 | +function last (arr) { |
| 56 | + return arr[arr.length - 1] |
| 57 | +} |
| 58 | + |
| 59 | +function padSlice (str, len, padding = '\0') { |
| 60 | + str = str.slice(0, len) |
| 61 | + while (str.length < len) str += padding |
| 62 | + return str |
| 63 | +} |
| 64 | + |
| 65 | +function SLPEncoder (options = {}) { |
| 66 | + if (!(this instanceof SLPEncoder)) return new SLPEncoder(options) |
| 67 | + |
| 68 | + if (!options.palette) { |
| 69 | + throw new Error('SLPEncoder: `palette` option is required') |
| 70 | + } |
| 71 | + |
| 72 | + this.version = options.version || '1.00' |
| 73 | + this.comment = options.comment || '' |
| 74 | + this.palette = options.palette |
| 75 | + this.frames = [] |
| 76 | + |
| 77 | + this.colorIndices = {} |
| 78 | + for (let i = 0; i < this.palette.length; i++) { |
| 79 | + const [r, g, b] = this.palette[i] |
| 80 | + const c = (r << 16) + (g << 8) + b |
| 81 | + this.colorIndices[c] = i |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +function pixelsToRenderCommands (palette, { width, height, data }) { |
| 86 | + const commands = [] |
| 87 | + |
| 88 | + let prevCommand |
| 89 | + let prevArg |
| 90 | + function push (command, arg) { |
| 91 | + prevCommand = command |
| 92 | + prevArg = arg |
| 93 | + commands.push({ command, arg }) |
| 94 | + } |
| 95 | + |
| 96 | + for (let i = 0; i < data.length; i += 4) { |
| 97 | + if (i > 0 && (i / 4) % width === 0) { |
| 98 | + push(RENDER_NEXTLINE) |
| 99 | + } |
| 100 | + // transparent pixel |
| 101 | + if (data[i + 3] === 0) { |
| 102 | + if (prevCommand === RENDER_SKIP) { |
| 103 | + last(commands).arg++ |
| 104 | + } else { |
| 105 | + push(RENDER_SKIP, 1) |
| 106 | + } |
| 107 | + continue |
| 108 | + } |
| 109 | + |
| 110 | + const r = data[i] |
| 111 | + const g = data[i + 1] |
| 112 | + const b = data[i + 2] |
| 113 | + const c = (r << 16) + (g << 8) + b |
| 114 | + const index = palette[c] |
| 115 | + if (!index) { |
| 116 | + throw new Error(`[${r} ${g} ${b}] is not in palette`) |
| 117 | + } |
| 118 | + if (prevCommand === RENDER_FILL && index === prevArg.color) { |
| 119 | + prevArg.pxCount++ |
| 120 | + } else if (prevCommand === RENDER_COLOR && index === prevArg) { |
| 121 | + commands.pop() |
| 122 | + push(RENDER_FILL, { |
| 123 | + pxCount: 2, |
| 124 | + color: index |
| 125 | + }) |
| 126 | + } else { |
| 127 | + push(RENDER_COLOR, index) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + return commands |
| 132 | +} |
| 133 | + |
| 134 | +function renderCommandsToSlpFrame ({ width, height, commands, baseOffset }) { |
| 135 | + const buffer = Buffer.alloc( |
| 136 | + (height * 4) + // outlines |
| 137 | + (height * 4) + // cmd table offsets |
| 138 | + // space for cmd table. each render command takes at most 2 bytes |
| 139 | + (commands.length * 2) |
| 140 | + ) |
| 141 | + const outlines = [ |
| 142 | + { left: 0, right: 0 } |
| 143 | + ] |
| 144 | + let offset = (height * 4) + (height * 4) |
| 145 | + const offsets = [ |
| 146 | + offset |
| 147 | + ] |
| 148 | + |
| 149 | + |
| 150 | + let x = 0 |
| 151 | + let y = 0 |
| 152 | + for (let i = 0; i < commands.length; i++) { |
| 153 | + const { command, arg } = commands[i] |
| 154 | + if (command === RENDER_NEXTLINE) { |
| 155 | + buffer[offset++] = SLP_END_OF_ROW |
| 156 | + offsets.push(offset) |
| 157 | + y++ |
| 158 | + x = 0 |
| 159 | + outlines[y] = { left: 0, right: 0 } |
| 160 | + } else if (command === RENDER_COLOR) { |
| 161 | + let end = i |
| 162 | + while (commands[end].command === RENDER_COLOR) { |
| 163 | + end++ |
| 164 | + } |
| 165 | + buffer[offset++] = SLP_COLOR_LIST | ((end - i) << 2) |
| 166 | + for (; i < end; i++) { |
| 167 | + buffer[offset++] = commands[i].arg |
| 168 | + x++ |
| 169 | + } |
| 170 | + i-- |
| 171 | + } else if (command === RENDER_FILL) { |
| 172 | + if (arg.pxCount < 16) { |
| 173 | + buffer[offset++] = SLP_FILL | (arg.pxCount << 4) |
| 174 | + } else { |
| 175 | + buffer[offset++] = SLP_FILL |
| 176 | + buffer[offset++] = arg.pxCount |
| 177 | + } |
| 178 | + buffer[offset++] = arg.color |
| 179 | + x += arg.pxCount |
| 180 | + } else if (command === RENDER_SKIP) { |
| 181 | + if (x === 0) { |
| 182 | + outlines[y].left = arg |
| 183 | + } else if (x + arg === width) { |
| 184 | + outlines[y].right = arg |
| 185 | + } else if (arg >= 64) { |
| 186 | + buffer[offset++] = SLP_SKIP |
| 187 | + buffer[offset++] = arg |
| 188 | + } else { |
| 189 | + buffer[offset++] = SLP_SKIP | (arg << 2) |
| 190 | + } |
| 191 | + x += arg |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + buffer[offset++] = SLP_END_OF_ROW |
| 196 | + |
| 197 | + // Flush outlines |
| 198 | + for (let i = 0; i < outlines.length; i++) { |
| 199 | + buffer.writeUInt16LE(outlines[i].left, i * 4) |
| 200 | + buffer.writeUInt16LE(outlines[i].right, i * 4 + 2) |
| 201 | + } |
| 202 | + for (let i = 0; i < offsets.length; i++) { |
| 203 | + buffer.writeUInt32LE(baseOffset + offsets[i], (height * 4) + (i * 4)) |
| 204 | + } |
| 205 | + |
| 206 | + return buffer.slice(0, offset) |
| 207 | +} |
| 208 | + |
| 209 | +SLPEncoder.prototype.addFrame = function ({ width, height, data, hotspot }) { |
| 210 | + const commands = pixelsToRenderCommands(this.colorIndices, { width, height, data }) |
| 211 | + |
| 212 | + this.frames.push({ |
| 213 | + cmdTableOffset: 0, |
| 214 | + outlineTableOffset: 0, |
| 215 | + paletteOffset: 0, |
| 216 | + properties: 0, |
| 217 | + width, |
| 218 | + height, |
| 219 | + hotspot: hotspot || { x: 0, y: 0 }, |
| 220 | + data, |
| 221 | + commands |
| 222 | + }) |
| 223 | + |
| 224 | + return commands |
| 225 | +} |
| 226 | + |
| 227 | +SLPEncoder.prototype.encode = function () { |
| 228 | + const header = { |
| 229 | + version: padSlice(this.version, 4), |
| 230 | + numFrames: this.frames.length, |
| 231 | + comment: padSlice(this.comment, 24), |
| 232 | + frames: this.frames |
| 233 | + } |
| 234 | + |
| 235 | + let offset = headerStruct.size(header) |
| 236 | + const frameBuffers = this.frames.map((frame) => { |
| 237 | + frame.outlineTableOffset = offset |
| 238 | + frame.cmdTableOffset = offset + frame.height * 4 |
| 239 | + const buffer = renderCommandsToSlpFrame({ |
| 240 | + width: frame.width, |
| 241 | + height: frame.height, |
| 242 | + commands: frame.commands, |
| 243 | + baseOffset: offset |
| 244 | + }) |
| 245 | + offset += buffer.length |
| 246 | + return buffer |
| 247 | + }) |
| 248 | + |
| 249 | + return Buffer.concat([ |
| 250 | + headerStruct.encode(header), |
| 251 | + ...frameBuffers |
| 252 | + ]) |
| 253 | +} |
0 commit comments