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
This commit is contained in:
parent
57d52dbe10
commit
93d05750bc
84
neohabitat.js
Normal file
84
neohabitat.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// adapted from populateModels.js in neohabitat
|
||||||
|
|
||||||
|
const replacements = [
|
||||||
|
[/UP/g, '"|"'],
|
||||||
|
[/DOWN/g, '"}"'],
|
||||||
|
[/LEFT/g, '"~"'],
|
||||||
|
[/RIGHT/g, '"\u007f"'],
|
||||||
|
[/SPACE/g, '" "'],
|
||||||
|
[/WEST/g, '0'],
|
||||||
|
[/SOUTH/g, '1'],
|
||||||
|
[/EAST/g, '2'],
|
||||||
|
[/NORTH/g, '3']
|
||||||
|
];
|
||||||
|
|
||||||
|
const joinReplacements = {
|
||||||
|
UP: '|',
|
||||||
|
DOWN: '}',
|
||||||
|
LEFT: '~',
|
||||||
|
RIGHT: '\u007f',
|
||||||
|
SPACE: ' ',
|
||||||
|
WEST: '0',
|
||||||
|
SOUTH: '1',
|
||||||
|
EAST: '2',
|
||||||
|
NORTH: '3'
|
||||||
|
};
|
||||||
|
|
||||||
|
const replacementJoinRegex = /((([A-Z]+\s?\+\s?)+)([A-Z]+\s?)+)/;
|
||||||
|
const stringJoinRegex = /(("([^"]|\\")*"\s*\+\s*)+"([^"]|\\")*")/g;
|
||||||
|
|
||||||
|
function templateStringJoins(data) {
|
||||||
|
if (data.search(/\+/) != -1) {
|
||||||
|
return data.replace(/(\n)/g, '').replace(stringJoinRegex,
|
||||||
|
function(origText, offset, string) {
|
||||||
|
var replacementText = [];
|
||||||
|
var splitText = origText.split('+');
|
||||||
|
for (var textLineId in splitText) {
|
||||||
|
var trimTextLine = splitText[textLineId].trim();
|
||||||
|
var quotesRemoved = trimTextLine.replace(/(^")|("$)/g, '');
|
||||||
|
replacementText.push(quotesRemoved);
|
||||||
|
}
|
||||||
|
return '"{0}"'.format(replacementText.join(''));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function templateConstantJoins(data) {
|
||||||
|
return data.replace(replacementJoinRegex, function(origText, offset, string) {
|
||||||
|
var replacementText = [];
|
||||||
|
var splitText = origText.split('+');
|
||||||
|
for (var habConstId in splitText) {
|
||||||
|
var trimHabConst = splitText[habConstId].trim();
|
||||||
|
if (trimHabConst in joinReplacements) {
|
||||||
|
replacementText.push(joinReplacements[trimHabConst]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '"{0}"'.format(replacementText.join(''));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function templateHabitatObject(data) {
|
||||||
|
var templated = templateConstantJoins(data);
|
||||||
|
for (var replacementId in replacements) {
|
||||||
|
var replacement = replacements[replacementId];
|
||||||
|
var regex = replacement[0];
|
||||||
|
var replacementText = replacement[1];
|
||||||
|
templated = templated.replace(regex, replacementText);
|
||||||
|
}
|
||||||
|
return templateStringJoins(templated);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHabitatObject(data) {
|
||||||
|
return JSON.parse(templateHabitatObject(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorsFromOrientation(orientation) {
|
||||||
|
const colorVal = (orientation & 0x78) >> 3
|
||||||
|
if (orientation & 0x80) {
|
||||||
|
return { wildcard: colorVal }
|
||||||
|
} else {
|
||||||
|
return { pattern: colorVal }
|
||||||
|
}
|
||||||
|
}
|
118
region.html
Normal file
118
region.html
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title>Inhabitor - The Habitat Inspector</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin:40px auto;
|
||||||
|
line-height:1.6;
|
||||||
|
font-size:18px;
|
||||||
|
color:#444;
|
||||||
|
padding:0 10px
|
||||||
|
}
|
||||||
|
h1,h2,h3 {
|
||||||
|
line-height:1.2
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Inhabitor - The Habitat Inspector</h1>
|
||||||
|
<div id="region" style="position: relative; width: 960px; height: 384px; overflow: hidden;">
|
||||||
|
</div>
|
||||||
|
<div id="debug"></div>
|
||||||
|
<div id="errors"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { decodeProp } from "./codec.js"
|
||||||
|
import { parse, removeComments } from "./mudparse.js"
|
||||||
|
import { translateSpace, topLeftCanvasOffset } from "./render.js"
|
||||||
|
import { docBuilder, decodeBinary, showAll, textNode, propAnimationShower, celmaskShower, celShower } from "./show.js"
|
||||||
|
import { parseHabitatObject, colorsFromOrientation } from "./neohabitat.js"
|
||||||
|
|
||||||
|
window.showErrors = () => {
|
||||||
|
document.getElementById('errors').style.display = 'block'
|
||||||
|
}
|
||||||
|
const debug = (msg, element) => {
|
||||||
|
const node = textNode(msg, "p")
|
||||||
|
if (element) {
|
||||||
|
node.addEventListener("mouseenter", () => { element.style.border = "2px solid red"; element.style.margin = "-2px" })
|
||||||
|
node.addEventListener("mouseleave", () => { element.style.border = ""; element.style.margin = "" })
|
||||||
|
}
|
||||||
|
document.getElementById("debug").appendChild(node)
|
||||||
|
}
|
||||||
|
const q = new URLSearchParams(window.location.search)
|
||||||
|
const filename = q.get("f") ?? "db/new_Downtown/Downtown_3f.json"
|
||||||
|
|
||||||
|
const onload = async () => {
|
||||||
|
const doc = docBuilder({ errorContainer: document.getElementById("errors")})
|
||||||
|
const mud = parse(await (await fetch("beta.mud", { cache: "no-cache" })).text())
|
||||||
|
const objects = parseHabitatObject(await (await fetch(filename, { cache: "no-cache" })).text())
|
||||||
|
const container = document.getElementById("region")
|
||||||
|
const sortedObjects = objects
|
||||||
|
.filter((obj) => obj.type == "item" && obj.mods && obj.mods.length > 0)
|
||||||
|
.toSorted(((a, b) => {
|
||||||
|
const ay = a.mods[0].y
|
||||||
|
const by = b.mods[0].y
|
||||||
|
const aIsBG = ay < 128
|
||||||
|
const bIsBG = by < 128
|
||||||
|
if (aIsBG != bIsBG) {
|
||||||
|
return aIsBG ? -1 : 1
|
||||||
|
} else if (aIsBG) {
|
||||||
|
return ay - by
|
||||||
|
} else {
|
||||||
|
return by - ay
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
for (const obj of sortedObjects) {
|
||||||
|
if (obj.type != "item" || !obj.mods || obj.mods.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const mod = obj.mods[0]
|
||||||
|
const classname = `class_${mod.type.toLowerCase()}`
|
||||||
|
const cls = mud.class[classname]
|
||||||
|
if (!cls) {
|
||||||
|
doc.showError(`No class named ${classname}`, filename)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const style = mod.style ?? 0
|
||||||
|
if (!cls.image[style]) {
|
||||||
|
doc.showError(`Invalid style ${mod.style} for ${classname}`, filename)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const imageId = cls.image[style].id
|
||||||
|
const image = mud.image[imageId]
|
||||||
|
if (!image) {
|
||||||
|
doc.showError(`${classname} refers to invalid image ${imageId}`, filename)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const propFilename = image.filename.replace(/^Images\//, "props/")
|
||||||
|
const prop = await decodeBinary(propFilename, decodeProp)
|
||||||
|
if (prop.error) {
|
||||||
|
doc.showError(prop.error, filename)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const colors = colorsFromOrientation(mod.orientation)
|
||||||
|
const shouldFlip = ((mod.orientation ?? 0) & 0x01) != 0
|
||||||
|
const middleOrientationBits = (mod.orientation ?? 0) & 0x06
|
||||||
|
const grState = mod.gr_state ?? 0
|
||||||
|
const [width, flipOffset] = image.arguments ?? [0,0]
|
||||||
|
const render = prop.animations.length > 0 ? propAnimationShower(prop, colors)(prop.animations[grState])
|
||||||
|
: celmaskShower(prop, colors)(prop.celmasks[grState])
|
||||||
|
const element = render.element
|
||||||
|
const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 }
|
||||||
|
const objectSpace = translateSpace(render, mod.x / 4, mod.y % 128)
|
||||||
|
const [x, y] = topLeftCanvasOffset(regionSpace, objectSpace)
|
||||||
|
element.style.position = "absolute"
|
||||||
|
element.style.left = `${x * 3}px`
|
||||||
|
element.style.top = `${y * 3}px`
|
||||||
|
debug(`${classname}: ${propFilename} ${shouldFlip} w:${width} fo:${flipOffset} o:${middleOrientationBits} [${render.minX}:${render.maxX},${render.minY}:${render.maxY}] @ ${mod.x/4},${mod.y} > ${x},${y}`, element)
|
||||||
|
container.appendChild(element)
|
||||||
|
}
|
||||||
|
// container.appendChild(textNode(JSON.stringify(objects, null, 2), "pre"))
|
||||||
|
}
|
||||||
|
onload()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
90
render.js
90
render.js
|
@ -30,9 +30,14 @@ const makeCanvas = (w, h) => {
|
||||||
const canvas = document.createElement("canvas")
|
const canvas = document.createElement("canvas")
|
||||||
canvas.width = w
|
canvas.width = w
|
||||||
canvas.height = h
|
canvas.height = h
|
||||||
|
canvas.style.imageRendering = "pixelated"
|
||||||
|
canvas.style.width = `${w * 3}px`
|
||||||
|
canvas.style.height = `${h * 3}px`
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const canvasForSpace = ({ minX, maxX, minY, maxY }) => makeCanvas((maxX - minX) * 8, maxY - minY)
|
||||||
|
|
||||||
const defaultColors = {
|
const defaultColors = {
|
||||||
wildcard: 6,
|
wildcard: 6,
|
||||||
skin: 10,
|
skin: 10,
|
||||||
|
@ -99,28 +104,50 @@ export const celsFromMask = (prop, celMask) => {
|
||||||
return cels
|
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) => {
|
export const frameFromCels = (cels, celColors = null, paintOrder = null) => {
|
||||||
if (cels.length == 0) {
|
if (cels.length == 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let minX = Number.POSITIVE_INFINITY
|
|
||||||
let minY = Number.POSITIVE_INFINITY
|
|
||||||
let maxX = Number.NEGATIVE_INFINITY
|
|
||||||
let maxY = Number.NEGATIVE_INFINITY
|
|
||||||
let xRel = 0
|
let xRel = 0
|
||||||
let yRel = 0
|
let yRel = 0
|
||||||
|
let xOrigin = null
|
||||||
|
let yOrigin = null
|
||||||
let layers = []
|
let layers = []
|
||||||
for (const [icel, cel] of cels.entries()) {
|
for (const [icel, cel] of cels.entries()) {
|
||||||
if (cel) {
|
if (cel) {
|
||||||
|
if (xOrigin == null) {
|
||||||
|
xOrigin = cel.xOffset
|
||||||
|
yOrigin = cel.yOffset - cel.height
|
||||||
|
}
|
||||||
const x = cel.xOffset + xRel
|
const x = cel.xOffset + xRel
|
||||||
const y = -(cel.yOffset + yRel)
|
const y = cel.yOffset + yRel
|
||||||
minX = Math.min(minX, x)
|
|
||||||
minY = Math.min(minY, y)
|
|
||||||
maxX = Math.max(maxX, cel.width + x)
|
|
||||||
maxY = Math.max(maxY, cel.height + y)
|
|
||||||
if (cel.bitmap) {
|
if (cel.bitmap) {
|
||||||
const colors = (Array.isArray(celColors) ? celColors[icel] : celColors) ?? {}
|
const colors = (Array.isArray(celColors) ? celColors[icel] : celColors) ?? {}
|
||||||
layers.push({ canvas: canvasFromBitmap(cel.bitmap, colors), x, y })
|
layers.push({ canvas: canvasFromBitmap(cel.bitmap, colors), minX: x, minY: y - cel.height, maxX: x + cel.width, maxY: y })
|
||||||
} else {
|
} else {
|
||||||
layers.push(null)
|
layers.push(null)
|
||||||
}
|
}
|
||||||
|
@ -139,17 +166,16 @@ export const frameFromCels = (cels, celColors = null, paintOrder = null) => {
|
||||||
layers = reordered
|
layers = reordered
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = (maxX - minX) * 8
|
const space = compositeSpaces(layers)
|
||||||
const h = maxY - minY
|
|
||||||
|
|
||||||
const canvas = makeCanvas(w, h)
|
const canvas = canvasForSpace(space)
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d")
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
if (layer && layer.canvas) {
|
if (layer && layer.canvas) {
|
||||||
ctx.drawImage(layer.canvas, (layer.x - minX) * 8, layer.y - minY)
|
drawInSpace(ctx, layer.canvas, space, layer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { canvas: canvas, xOffset: minX * 8, yOffset: minY, w: w, h: h }
|
return {...translateSpace(space, -xOrigin, -yOrigin), canvas: canvas }
|
||||||
}
|
}
|
||||||
|
|
||||||
const framesFromAnimation = (animation, frameFromState) => {
|
const framesFromAnimation = (animation, frameFromState) => {
|
||||||
|
@ -252,37 +278,23 @@ export const imageFromCanvas = (canvas) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const animate = (frames) => {
|
export const animate = (frames) => {
|
||||||
if (frames.length == 0) {
|
const space = compositeSpaces(frames)
|
||||||
return textNode("")
|
|
||||||
} else if (frames.length == 1) {
|
|
||||||
return imageFromCanvas(frames[0].canvas)
|
|
||||||
}
|
|
||||||
let minX = Number.POSITIVE_INFINITY
|
|
||||||
let minY = Number.POSITIVE_INFINITY
|
|
||||||
let maxX = Number.NEGATIVE_INFINITY
|
|
||||||
let maxY = Number.NEGATIVE_INFINITY
|
|
||||||
for (const frame of frames) {
|
|
||||||
minX = Math.min(minX, frame.xOffset)
|
|
||||||
minY = Math.min(minY, frame.yOffset)
|
|
||||||
maxX = Math.max(maxX, frame.xOffset + frame.w)
|
|
||||||
maxY = Math.max(maxY, frame.yOffset + frame.h)
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = maxX - minX
|
if (frames.length == 0) {
|
||||||
const h = maxY - minY
|
return { ...space, element: textNode("") }
|
||||||
const canvas = makeCanvas(w, h)
|
} else if (frames.length == 1) {
|
||||||
canvas.style.imageRendering = "pixelated"
|
return { ...space, element: imageFromCanvas(frames[0].canvas) }
|
||||||
canvas.style.width = `${w * 3}px`
|
}
|
||||||
canvas.style.height = `${h * 3}px`
|
const canvas = canvasForSpace(space)
|
||||||
let iframe = 0
|
let iframe = 0
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d")
|
||||||
const nextFrame = () => {
|
const nextFrame = () => {
|
||||||
const frame = frames[iframe]
|
const frame = frames[iframe]
|
||||||
ctx.clearRect(0, 0, w, h)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
ctx.drawImage(frame.canvas, frame.xOffset - minX, frame.yOffset - minY)
|
drawInSpace(ctx, frame.canvas, space, frame)
|
||||||
iframe = (iframe + 1) % frames.length
|
iframe = (iframe + 1) % frames.length
|
||||||
}
|
}
|
||||||
nextFrame()
|
nextFrame()
|
||||||
setInterval(nextFrame, 250)
|
setInterval(nextFrame, 250)
|
||||||
return canvas
|
return { ...space, element: canvas }
|
||||||
}
|
}
|
||||||
|
|
18
show.js
18
show.js
|
@ -43,18 +43,18 @@ export const docBuilder = ({ detailHref, errorContainer }) => {
|
||||||
return { linkDetail, showError }
|
return { linkDetail, showError }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showRender = (doc, container, filename, render) => {
|
||||||
|
if (Array.isArray(render)) {
|
||||||
|
render.forEach((r) => showRender(doc, container, filename, r))
|
||||||
|
} else if (render) {
|
||||||
|
container.appendChild(doc.linkDetail(render.element, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const showAll = (doc, container, filename, values, f) => {
|
export const showAll = (doc, container, filename, values, f) => {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
try {
|
try {
|
||||||
let elements = f(value)
|
showRender(doc, container, filename, f(value))
|
||||||
if (elements && !Array.isArray(elements)) {
|
|
||||||
elements = [elements]
|
|
||||||
}
|
|
||||||
if (elements) {
|
|
||||||
for (const element of elements) {
|
|
||||||
container.appendChild(doc.linkDetail(element, filename))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
doc.showError(e, filename)
|
doc.showError(e, filename)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue