Skip to content

Commit

Permalink
Update experimental paletted rendering stuff
Browse files Browse the repository at this point in the history
These are changes made a while ago but not committed at the time, so I
can't remember their exact state of progress.

Paletted rendering is still experimental, so these may or may not be
production-ready changes.
  • Loading branch information
leikareipa committed Aug 11, 2022
1 parent a0ed8eb commit 9df7e6d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 69 deletions.
2 changes: 1 addition & 1 deletion distributable/rngon.cat.js

Large diffs are not rendered by default.

160 changes: 99 additions & 61 deletions js/paletted-canvas/paletted-canvas.js
Expand Up @@ -12,32 +12,25 @@ const isRunningInWebWorker = (typeof importScripts === "function");

if (!isRunningInWebWorker)
{
// Provides an ImageData-like interface for storing paletted image data.
// A wrapper interface around ImageData for storing paletted image data.
class IndexedImageData {
#palette
#width
#height
#data
data

constructor(data, width, height, palette) {
// Validate input.
{
if (!(data instanceof Uint8ClampedArray)) {
throw new Error("The data must be a Uint8ClampedArray array.");
}

if (
(typeof width !== "number") ||
(typeof height !== "number")
){
throw new Error("The width and height must be numbers.");
}
constructor(width, height) {
if (
isNaN(width) ||
isNaN(height)
){
throw new Error("This interface supports only numeric 'width' and 'height' as arguments.");
}

this.#width = width;
this.#height = height;
this.#data = data;
this.palette = (palette || [[0, 0, 0, 0]]);
this.data = new Array(width * height);
this.palette = [[0, 0, 0, 0]];
}

// To get the palette index at x as a quadruplet of 8-bit RGBA values, do "palette[x]".
Expand All @@ -58,15 +51,21 @@ class IndexedImageData {
throw new Error("The palette must be an array.");
}

if (newPalette.length < 1) {
throw new Error("A palette must consist of at least one color.");
}

if (!newPalette.every(element=>Array.isArray(element))) {
throw new Error("Each entry in the palette must be a sub-array of color channel values.");
}

newPalette.forEach(color=>{
color.length = 4;
if (typeof color[3] === "undefined") {
color[3] = 255;
}
});

newPalette = newPalette.map(color=>Uint8ClampedArray.from(color));

const palette = {
byte: newPalette,
dword: new Uint32Array(newPalette.map(color=>((color[3] << 24) | (color[2] << 16) | (color[1] << 8) | color[0]))),
Expand All @@ -86,10 +85,6 @@ class IndexedImageData {
});
}

get data() {
return this.#data;
}

get width() {
return this.#width;
}
Expand All @@ -103,72 +98,115 @@ class IndexedImageData {
}
};

class HTMLPalettedCanvasElement extends HTMLCanvasElement {
#canvasImage
#canvasContext
// A wrapper interface around CanvasRenderingContext2D for manipulating the drawing surface
// of a <canvas> element using indexed colors.
class CanvasRenderingContextIndexed {
#underlyingContext2D
#underlyingImageData
#width
#height

constructor() {
super();
}

static get observedAttributes() {
return ["width", "height"];
}
constructor(underlyingContext2D) {
if (!(underlyingContext2D instanceof CanvasRenderingContext2D)) {
throw new Error("CanvasRenderingContextIndexed requires an instance of CanvasRenderingContext2D as an argument.");
}

attributeChangedCallback(name, oldValue, newValue) {
if ((oldValue != newValue) && ["width", "height"].includes(name)) {
this.#canvasContext = super.getContext("2d");
this.#canvasImage = this.#canvasContext.createImageData(super.width, super.height);
this.#underlyingContext2D = underlyingContext2D;
this.#width = this.#underlyingContext2D.canvas.width;
this.#height = this.#underlyingContext2D.canvas.height;
this.#underlyingImageData = this.#underlyingContext2D.createImageData(this.#width, this.#height);
this.#underlyingImageData.data.fill(0);

if (
isNaN(this.#width) ||
isNaN(this.#height) ||
(this.#height < 1) ||
(this.#width < 1)
){
throw new Error("Invalid context resolution.");
}
}

createImageData(
width = this.#width,
height = this.#height
)
{
if (width instanceof ImageData) {
throw new Error("This interface supports only 'width' and 'height' as arguments.");
}

getContext(contextType = "2d") {
if (contextType !== "2d") {
throw new Error(`Only the "2d" context type is supported.`);
if (
(width !== this.#width) ||
(height !== this.#height)
){
throw new Error("This interface can only create images whose resolution matches the size of the canvas.");
}

// Emulates the interface of CanvasRenderingContext2D.
return {
createImageData: this.#createImageData.bind(this),
putImageData: this.#putImageData.bind(this),
};
return new IndexedImageData(width, height);
}

#createImageData() {
return new IndexedImageData(
new Uint8ClampedArray(super.width * super.height),
super.width,
super.height,
);
// Returns as an ImageData object the RGBA/8888 pixel data as displayed on the canvas.
getImageData() {
return this.#underlyingImageData;
}

#putImageData(image) {
if (!(image instanceof IndexedImageData)) {
putImageData(indexedImage) {
if (!(indexedImage instanceof IndexedImageData)) {
throw new Error("Only images of type IndexedImageData can be rendered.");
}

if (
!(this.#canvasImage instanceof ImageData) ||
!(this.#canvasContext instanceof CanvasRenderingContext2D)
(indexedImage.width !== this.#width) ||
(indexedImage.height !== this.#height)
){
throw new Error("Internal error: incomplete state initialization.");
throw new Error("Mismatched image resolution: images must be the size of the canvas.");
}

// Convert the paletted image into a 32-bit image on the canvas.
{
const palette = image.palette.dword;
const pixelBuffer32bit = new Uint32Array(this.#canvasImage.data.buffer);
const palette = indexedImage.palette.dword;
const pixelBuffer32bit = new Uint32Array(this.#underlyingImageData.data.buffer);

for (let i = 0; i < image.data.length; i++) {
pixelBuffer32bit[i] = palette[image.data[i]];
for (let i = 0; i < indexedImage.data.length; i++) {
pixelBuffer32bit[i] = palette[indexedImage.data[i]];
}
}

this.#canvasContext.putImageData(this.#canvasImage, 0, 0);
this.#underlyingContext2D.putImageData(this.#underlyingImageData, 0, 0);
}
}

class HTMLPalettedCanvasElement extends HTMLCanvasElement {
#underlyingContext
#indexedRenderingContext

constructor() {
super();
}

static get observedAttributes() {
return ["width", "height"];
}

attributeChangedCallback(name, oldValue, newValue) {
if ((oldValue != newValue) && ["width", "height"].includes(name)) {
this.#underlyingContext = super.getContext("2d");
this.#indexedRenderingContext = new CanvasRenderingContextIndexed(this.#underlyingContext);
}
}

getContext(contextType = "2d") {
if (contextType !== "2d") {
throw new Error("This interface only supports the '2d' context type.");
}

return this.#indexedRenderingContext;
}
};

window.IndexedImageData = IndexedImageData;
window.CanvasRenderingContextIndexed = CanvasRenderingContextIndexed;
window.HTMLPalettedCanvasElement = HTMLPalettedCanvasElement;
customElements.define("paletted-canvas", HTMLPalettedCanvasElement, {extends: "canvas"});
}
Expand Up @@ -22,8 +22,9 @@ export function generic_fill({
const usePixelShader = Rngon.internalState.usePixelShader;
const fragmentBuffer = Rngon.internalState.fragmentBuffer.data;
const depthBuffer = (Rngon.internalState.useDepthBuffer? Rngon.internalState.depthBuffer.data : null);
const pixelBufferClamped8 = Rngon.internalState.pixelBuffer.data;
const pixelBufferWidth = Rngon.internalState.pixelBuffer.width;
const pixelBufferImage = Rngon.internalState.pixelBuffer;
const pixelBufferClamped8 = pixelBufferImage.data;
const pixelBufferWidth = pixelBufferImage.width;
const material = ngon.material;
const texture = (material.texture || null);

Expand Down
Expand Up @@ -23,8 +23,9 @@ export function plain_solid_fill({
})
{
const usePalette = Rngon.internalState.usePalette;
const pixelBufferClamped8 = Rngon.internalState.pixelBuffer.data;
const pixelBufferWidth = Rngon.internalState.pixelBuffer.width;
const pixelBufferImage = Rngon.internalState.pixelBuffer;
const pixelBufferClamped8 = pixelBufferImage.data;
const pixelBufferWidth = pixelBufferImage.width;
const depthBuffer = (Rngon.internalState.useDepthBuffer? Rngon.internalState.depthBuffer.data : null);
const material = ngon.material;

Expand Down
Expand Up @@ -22,8 +22,9 @@ export function plain_textured_fill({
})
{
const usePalette = Rngon.internalState.usePalette;
const pixelBufferClamped8 = Rngon.internalState.pixelBuffer.data;
const pixelBufferWidth = Rngon.internalState.pixelBuffer.width;
const pixelBufferImage = Rngon.internalState.pixelBuffer;
const pixelBufferClamped8 = pixelBufferImage.data;
const pixelBufferWidth = pixelBufferImage.width;
const depthBuffer = (Rngon.internalState.useDepthBuffer? Rngon.internalState.depthBuffer.data : null);
const material = ngon.material;
const texture = (material.texture || null);
Expand Down
6 changes: 5 additions & 1 deletion js/retro-ngon/core/surface.js
Expand Up @@ -146,7 +146,6 @@ export function surface(canvasElement)
}
else
{
state.pixelBuffer.palette = state.palette;
renderContext.putImageData(state.pixelBuffer, 0, 0);
}
}
Expand Down Expand Up @@ -228,6 +227,11 @@ export function surface(canvasElement)
"Couldn't establish a canvas render context."
);

if (state.usePalette)
{
state.pixelBuffer.palette = state.palette;
}

// Size the canvas as per the requested render scale.
const surfaceWidth = Rngon.renderable_width_of(canvasElement, state.renderScale);
const surfaceHeight = Rngon.renderable_height_of(canvasElement, state.renderScale);
Expand Down

0 comments on commit 9df7e6d

Please sign in to comment.