inhabitor/render.js
Jeremy Penner 93d05750bc Implement test region display
Refactor rendering to more consistently work in "habitat-space" where
the origin is at the bottom-left and y increases when moving upwards
2024-01-06 00:03:16 -05:00

301 lines
9.8 KiB
JavaScript

// C64 RGB values taken from https://www.c64-wiki.com/wiki/Color
const c64Colors = [
0x000000, 0xffffff, 0x880000, 0xaaffee, 0xcc44cc, 0x00cc55,
0x0000aa, 0xeeee77, 0xdd8855, 0x664400, 0xff7777, 0x333333,
0x777777, 0xaaff66, 0x0088ff, 0xbbbbbb
]
// from paint.m:447
const celPatterns = [
[0x00, 0x00, 0x00, 0x00],
[0xaa, 0xaa, 0xaa, 0xaa],
[0xff, 0xff, 0xff, 0xff],
[0xe2, 0xe2, 0xe2, 0xe2],
[0x8b, 0xbe, 0x0f, 0xcc],
[0xee, 0x00, 0xee, 0x00],
[0xf0, 0xf0, 0x0f, 0x0f],
[0x22, 0x88, 0x22, 0x88],
[0x32, 0x88, 0x23, 0x88],
[0x00, 0x28, 0x3b, 0x0c],
[0x33, 0xcc, 0x33, 0xcc],
[0x08, 0x80, 0x0c, 0x80],
[0x3f, 0x3f, 0xf3, 0xf3],
[0xaa, 0x3f, 0xaa, 0xf3],
[0xaa, 0x00, 0xaa, 0x00],
[0x55, 0x55, 0x55, 0x55]
]
const makeCanvas = (w, h) => {
const canvas = document.createElement("canvas")
canvas.width = w
canvas.height = h
canvas.style.imageRendering = "pixelated"
canvas.style.width = `${w * 3}px`
canvas.style.height = `${h * 3}px`
return canvas
}
export const canvasForSpace = ({ minX, maxX, minY, maxY }) => makeCanvas((maxX - minX) * 8, maxY - minY)
const defaultColors = {
wildcard: 6,
skin: 10,
pattern: 15
}
export const canvasFromBitmap = (bitmap, colors = {}) => {
if (bitmap.length == 0 || bitmap[0].length == 0) {
return null
}
const { wildcard, pattern, skin } = { ...defaultColors, ...colors }
const patternColors = [6, wildcard, 0, skin]
const h = bitmap.length
const w = bitmap[0].length * 2
const canvas = makeCanvas(w, h)
const ctx = canvas.getContext("2d")
const img = ctx.createImageData(w, h)
const putpixel = (x, y, r, g, b, a) => {
const i = (x * 8) + (y * w * 4)
img.data[i] = r
img.data[i + 1] = g
img.data[i + 2] = b
img.data[i + 3] = a
img.data[i + 4] = r
img.data[i + 5] = g
img.data[i + 6] = b
img.data[i + 7] = a
}
for (let y = 0; y < bitmap.length; y ++) {
const line = bitmap[y]
// TODO: What is pattern 255?
const patbyte = celPatterns[pattern < 0 || pattern > 15 ? 15 : pattern][y % 4]
for (let x = 0; x < line.length; x ++) {
const pixel = line[x]
let color = null
if (pixel == 0) { // transparent
putpixel(x, y, 0, 0, 0, 0)
} else if (pixel == 1) { // wild
const shift = (x % 4) * 2
color = patternColors[(patbyte & (0xc0 >> shift)) >> (6 - shift)]
} else {
color = patternColors[pixel]
}
if (color != null) {
const rgb = c64Colors[color]
putpixel(x, y, (rgb & 0xff0000) >> 16, (rgb & 0xff00) >> 8, rgb & 0xff, 0xff)
}
}
}
ctx.putImageData(img, 0, 0)
return canvas
}
export const celsFromMask = (prop, celMask) => {
const cels = []
for (let icel = 0; icel < 8; icel ++) {
const celbit = 0x80 >> icel
if ((celMask & celbit) != 0) {
cels.push(prop.cels[icel])
}
}
return cels
}
// canvas coordinate spaces have the top-left corner at 0,0, x increasing to the right, y increasing down.
// habitat coordinate spaces have the object origin at 0,0, x increasing to the right, y increasing _up_.
// In addition, 1 unit horizontally in habitat coordinate space corresponds to 8 pixels horizontally in canvas space.
export const translateSpace = ({ minX, maxX, minY, maxY }, dx, dy) => {
return { minX: minX + dx, maxX: maxX + dx, minY: minY + dy, maxY: maxY + dy }
}
export const compositeSpaces = (spaces) => {
return { minX: Math.min(...spaces.map((f) => f ? f.minX : Math.min())),
maxX: Math.max(...spaces.map((f) => f ? f.maxX : Math.max())),
minY: Math.min(...spaces.map((f) => f ? f.minY : Math.min())),
maxY: Math.max(...spaces.map((f) => f ? f.maxY : Math.max())) }
}
export const topLeftCanvasOffset = (outerSpace, innerSpace) => {
return [(innerSpace.minX - outerSpace.minX) * 8, outerSpace.maxY - innerSpace.maxY]
}
export const drawInSpace = (ctx, canvas, ctxSpace, canvasSpace) => {
const [x, y] = topLeftCanvasOffset(ctxSpace, canvasSpace)
ctx.drawImage(canvas, x, y)
}
// Habitat's coordinate space consistently has y=0 for the bottom, and increasing y means going up
export const frameFromCels = (cels, celColors = null, paintOrder = null) => {
if (cels.length == 0) {
return null
}
let xRel = 0
let yRel = 0
let xOrigin = null
let yOrigin = null
let layers = []
for (const [icel, cel] of cels.entries()) {
if (cel) {
if (xOrigin == null) {
xOrigin = cel.xOffset
yOrigin = cel.yOffset - cel.height
}
const x = cel.xOffset + xRel
const y = cel.yOffset + yRel
if (cel.bitmap) {
const colors = (Array.isArray(celColors) ? celColors[icel] : celColors) ?? {}
layers.push({ canvas: canvasFromBitmap(cel.bitmap, colors), minX: x, minY: y - cel.height, maxX: x + cel.width, maxY: y })
} else {
layers.push(null)
}
xRel += cel.xRel
yRel += cel.yRel
} else {
layers.push(null)
}
}
if (paintOrder) {
const reordered = []
for (const ilayer of paintOrder) {
reordered.push(layers[ilayer])
}
layers = reordered
}
const space = compositeSpaces(layers)
const canvas = canvasForSpace(space)
const ctx = canvas.getContext("2d")
for (const layer of layers) {
if (layer && layer.canvas) {
drawInSpace(ctx, layer.canvas, space, layer)
}
}
return {...translateSpace(space, -xOrigin, -yOrigin), canvas: canvas }
}
const framesFromAnimation = (animation, frameFromState) => {
const frames = []
for (let istate = animation.startState; istate <= animation.endState; istate ++) {
const frame = frameFromState(istate)
if (frame != null) {
frames.push(frame)
}
}
return frames
}
export const framesFromPropAnimation = (animation, prop, colors = null) => {
const frameFromState = (istate) =>
frameFromCels(celsFromMask(prop, prop.celmasks[istate]), colors)
return framesFromAnimation(animation, frameFromState)
}
export const framesFromLimbAnimation = (animation, limb, colors = null) => {
const frameFromState = (istate) => {
const iframe = limb.frames[istate]
if (iframe >= 0) {
return frameFromCels([limb.cels[iframe]], colors)
} else {
return null
}
}
return framesFromAnimation(animation, frameFromState)
}
const actionOrientations = {
"stand_back": "back",
"walk_front": "front",
"walk_back": "back",
"stand_front": "front",
"sit_front": "front"
}
export const framesFromAction = (action, body, limbColors = null) => {
const frames = []
const chore = body.choreography[body.actions[action]]
const animations = []
const orientation = actionOrientations[action] ?? "side"
const limbOrder = orientation == "front" ? body.frontFacingLimbOrder :
orientation == "back" ? body.backFacingLimbOrder :
null // side animations are always displayed in standard limb order
for (const limb of body.limbs) {
if (limb.animations.length > 0) {
animations.push({ ...limb.animations[0] })
} else {
animations.push({ startState: 0, endState: 0 })
}
}
for (const override of chore) {
const ilimb = override.limb
const newAnim = body.limbs[ilimb].animations[override.animation]
animations[ilimb].startState = newAnim.startState
animations[ilimb].endState = newAnim.endState
}
while (true) {
const cels = []
// const celColors = []
let restartedCount = 0
for (const [ilimb, limb] of body.limbs.entries()) {
const animation = animations[ilimb]
if (animation.current == undefined) {
animation.current = animation.startState
} else {
animation.current ++
if (animation.current > animation.endState) {
animation.current = animation.startState
restartedCount ++
}
}
const istate = limb.frames[animation.current]
if (istate >= 0) {
cels.push(limb.cels[istate])
} else {
cels.push(null)
}
// limb.pattern is not a pattern index, it's a LIMB pattern index
// celColors.push({ pattern: limb.pattern })
}
if (restartedCount == animations.length) {
break
}
frames.push(frameFromCels(cels, null, limbOrder))
}
return frames
}
export const imageFromCanvas = (canvas) => {
const img = document.createElement("img")
img.src = canvas.toDataURL()
img.width = canvas.width * 3
img.height = canvas.height * 3
img.style.imageRendering = "pixelated"
return img
}
export const animate = (frames) => {
const space = compositeSpaces(frames)
if (frames.length == 0) {
return { ...space, element: textNode("") }
} else if (frames.length == 1) {
return { ...space, element: imageFromCanvas(frames[0].canvas) }
}
const canvas = canvasForSpace(space)
let iframe = 0
const ctx = canvas.getContext("2d")
const nextFrame = () => {
const frame = frames[iframe]
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawInSpace(ctx, frame.canvas, space, frame)
iframe = (iframe + 1) % frames.length
}
nextFrame()
setInterval(nextFrame, 250)
return { ...space, element: canvas }
}