Skip to content

Commit

Permalink
Render to an ImageData object
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop committed Aug 4, 2017
1 parent d9e2ef8 commit 73879fa
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 22 deletions.
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ Genie Engine .SLP graphic file reader in Node.js

```javascript
let fs = require('fs')
let SLP = require('slp')
let { Png } = require('png')

let slp = SLP(fs.readFileSync('my-file.slp'))
let frame = slp.renderFrame(0, { player: 7, palette: mainPalette })
// Load a Palette file using the `jascpal` module
let Palette = require('jascpal')
let mainPalette = Palette(fs.readFileSync('palette.pal'))

let png = new Png(frame.buffer, frame.width, frame.height, 'rgba')
require('fs').writeFile('my-file.png', png.encode())
// Load an SLP file and render a frame
let SLP = require('genie-slp')
let slp = SLP(fs.readFileSync('my-file.slp'))
let frame = slp.renderFrame(0, mainPalette, { player: 7 })

// Render the returned ImageData object to a PNG file
let { PNG } = require('pngjs')
let png = new PNG({
width: frame.width,
height: frame.height
})
png.data = Buffer.from(frame.data.buffer)
png.pack().pipe(fs.createWriteStream('my-file.png'))
```

## API
Expand All @@ -25,8 +35,17 @@ require('fs').writeFile('my-file.png', png.encode())

Creates an SLP graphic from a buffer.

### SLP#renderFrame(frameIndex, { palette, player }) → { buffer, width, height }
### SLP#renderFrame(frameIndex, palette, { player }) → ImageData

Renders a frame to an `[ r, g, b, a ]` ImageData object.

**Parameters**

- `frameIndex` - The SLP frame ID to render.
- `palette` - A colour palette: an array of `[ r, g, b ]` colour arrays, probably from the [jascpal](https://github.com/goto-bus-stop/jascpal) module.
- `options` - Optionally, an object with properties:
- `player` - Player colour (1-8) to use for player-specific parts. Defaults to 1 (blue).
- `drawOutline` - Whether to draw an outline (used when units are behind buildings, etc). Defaults to false.

Renders a frame to an `[ r, g, b, a ]` pixel Buffer. Takes a `frameIndex`, and an options object with
a `palette` (an array of `[ r, g, b ]` colour arrays) and a `player` (ID of the player colour to use, 1-8).
Returns an object with the `buffer`, the `width` of the frame, and the `height` of the frame.
In the browser, returns an ImageData object that can be drawn to a Canvas.
In node, returns a plain object with the `data` as a Uint8ClampedArray, the `width` of the frame, and the `height` of the frame (like the ImageData API).
38 changes: 26 additions & 12 deletions src/SLP.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Struct, { types as t } from 'awestruct'
import createImageData from './createImageData'

// SLP commands
const SLP_END_OF_ROW = 0x0f
Expand Down Expand Up @@ -50,6 +51,17 @@ let headerStruct = Struct({

const getPlayerColor = (pal, idx, player) => pal[idx + 16 * player]

/**
* Noncompliant `Array.fill` polyfill that does everything this module needs.
*/
function polyFill (value, start, end) {
if (!end) end = this.length
if (!start) start = 0
for (var i = start; i < end; i++) {
this[i] = value
}
}

/**
* @param {Buffer} buf
*/
Expand Down Expand Up @@ -202,7 +214,9 @@ SLP.prototype.renderFrame = function (frameIdx, palette, { player, drawOutline }

const frame = this.getFrame(frameIdx)
const outlines = frame.outlines
const pixels = Buffer(frame.width * frame.height * 4)
const imageData = createImageData(frame.width, frame.height)
const pixels = imageData.data
const fill = (pixels.fill || polyFill).bind(pixels)
let idx = 0
let y = 0

Expand All @@ -217,19 +231,19 @@ SLP.prototype.renderFrame = function (frameIdx, palette, { player, drawOutline }
if (skip === SLP_LINE_EMPTY) {
skip = frame.width
}
pixels.fill(255, 0, skip * 4)
fill(255, 0, skip * 4)
idx = skip * 4

frame.commands.forEach(({ command, arg }) => {
let i, color
switch (command) {
case RENDER_SKIP:
pixels.fill(255, idx, idx + arg * 4)
fill(255, idx, idx + arg * 4)
idx += arg * 4
break
case RENDER_NEXTLINE:
// fill up the rest of this line
pixels.fill(255, idx, idx + outlines[y].right * 4)
fill(255, idx, idx + outlines[y].right * 4)
idx += outlines[y].right * 4
y++
if (y < frame.height) {
Expand All @@ -239,35 +253,35 @@ SLP.prototype.renderFrame = function (frameIdx, palette, { player, drawOutline }
skip = frame.width
}
// fill the start of this line until the first pixel
pixels.fill(255, idx, idx + skip * 4)
fill(255, idx, idx + skip * 4)
idx += skip * 4
}
break
case RENDER_COLOR:
pushColor(palette[arg], 0)
pushColor(palette[arg], 255)
break
case RENDER_FILL:
i = arg.pxCount
color = palette[arg.color]
while (i--) pushColor(color, 0)
while (i--) pushColor(color, 255)
break
case RENDER_OUTLINE:
pushColor([ 0, 0, 0 ], drawOutline ? 0 : 255)
pushColor([ 0, 0, 0 ], drawOutline ? 255 : 0)
break
case RENDER_PLAYER_COLOR:
pushColor(getPlayerColor(palette, arg, player), 0)
pushColor(getPlayerColor(palette, arg, player), 255)
break
case RENDER_PLAYER_FILL:
i = arg.pxCount
color = getPlayerColor(palette, arg.color, player)
while (i--) pushColor(color, 0)
while (i--) pushColor(color, 255)
break
case RENDER_SHADOW:
i = arg
while (i--) pushColor([ 255, 0, 0 ], 0)
while (i--) pushColor([ 255, 0, 0 ], 255)
break
}
})

return { buffer: pixels, width: frame.width, height: frame.height }
return imageData
}
36 changes: 36 additions & 0 deletions src/createImageData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function supportsImageData () {
try {
const img = new window.ImageData(1, 1)
return img.width === 1 && img.height === 1
} catch (err) {
return false
}
}

function createImageDataSimple (width, height) {
return new ImageData(width, height)
}

function createImageDataCanvas (canvas, width, height) {
return canvas.getContext('2d')
.createImageData(width, height)
}

function createImageDataObject (width, height) {
return {
data: new Uint8ClampedArray(width * height * 4).fill(0),
width,
height
}
}

const hasWindow = typeof window !== 'undefined'
if (hasWindow && supportsImageData()) {
module.exports = createImageDataSimple
} else if (hasWindow && typeof document !== 'undefined' && document.createElement) {
const canvas = document.createElement('canvas')
module.exports = (width, height) =>
createImageDataCanvas(canvas, width, height)
} else {
module.exports = createImageDataObject
}

0 comments on commit 73879fa

Please sign in to comment.