Skip to content

Commit

Permalink
/history.gif
Browse files Browse the repository at this point in the history
  • Loading branch information
Chetan Padia committed Oct 10, 2024
1 parent 80ef32d commit 569e074
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 31 deletions.
2 changes: 1 addition & 1 deletion database/rules.bolt
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ type CurrentTimestamp extends Number {

createOnly(value) {
prior(value) == null && value != null
}
}
4 changes: 4 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
{
"source": "/image.png",
"function": "imagePng"
},
{
"source": "/history.gif",
"function": "historyGif"
}
],
"headers": [
Expand Down
2 changes: 2 additions & 0 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
},
"main": "dist/functions/src/index.js",
"dependencies": {
"canvas": "^2.11.2",
"firebase-admin": "^8.13.0",
"firebase-functions": "^3.21.0",
"modern-gif": "^2.0.3",
"pngjs": "^3.3.3"
},
"devDependencies": {
Expand Down
78 changes: 78 additions & 0 deletions functions/src/encodeGif.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ColorHistoryItem } from "../../database/.types";
import { createCanvas } from "canvas";
import { Palette } from "./palette";
import { encode as encodeGif } from "modern-gif";

export async function encodeHistoryAsGif(
width: number,
height: number,
history: ColorHistoryItem[],
options: {
speed: number;
lastFrameDelay: number;
minFrameDelay: number;
maxFrameDelay: number;
scale: number;
} = {
speed: 1,
lastFrameDelay: 1000,
minFrameDelay: 0,
maxFrameDelay: Infinity,
scale: 1,
}
): Promise<ArrayBuffer> {
if (history.length === 0) {
throw new Error("No data to encode");
}

const canvas = createCanvas(width * options.scale, height * options.scale);
const ctx = canvas.getContext("2d");

const frames: { delay: number; data: Uint8ClampedArray }[] = [];

for (let i = 0; i < history.length; i++) {
const { x, y, value, timestamp } = history[i];

const color = Palette[value];
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
ctx.fillRect(
x * options.scale,
y * options.scale,
options.scale,
options.scale
);

const data = ctx.getImageData(
0,
0,
width * options.scale,
height * options.scale
).data;

let delay =
i < history.length - 1
? (history[i + 1].timestamp - timestamp) / options.speed
: options.lastFrameDelay;

if (delay < options.minFrameDelay) {
const previousFrame = frames.pop();
if (previousFrame) {
delay += previousFrame.delay;
}
}

frames.push({ data, delay });
}

for (const frame of frames) {
if (frame.delay > options.maxFrameDelay) {
frame.delay = options.maxFrameDelay;
}
}

return await encodeGif({
width: width * options.scale,
height: height * options.scale,
frames,
});
}
71 changes: 52 additions & 19 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import * as functions from 'firebase-functions';
import { PNG } from 'pngjs';
import { Canvas, Square } from '../../database/.types';
import { Color as PaletteColor, Palette } from './palette';
import * as functions from "firebase-functions";
import { PNG } from "pngjs";
import { Canvas, Square } from "../../database/.types";
import { Color as PaletteColor, Palette } from "./palette";

import * as admin from 'firebase-admin';
import * as admin from "firebase-admin";
import { encodeHistoryAsGif } from "./encodeGif";
admin.initializeApp();

const IMAGE_ID = 'the-one-and-only';
const IMAGE_ID = "the-one-and-only";
const database = admin.database();

function zeroPad(n: number|string, padding: number): string {
return ('0'.repeat(padding) + n).slice(-padding);
function zeroPad(n: number | string, padding: number): string {
return ("0".repeat(padding) + n).slice(-padding);
}

function colorIndexFrom(depth: Canvas['depth'], canvas: Canvas['canvas'], x: number, y: number): number|undefined {
function colorIndexFrom(
depth: Canvas["depth"],
canvas: Canvas["canvas"],
x: number,
y: number
): number | undefined {
const xPath = zeroPad(x.toString(2), depth);
const yPath = zeroPad(y.toString(2), depth);
let square: Square<any>|number|undefined = canvas;
for (let i=0; i<depth; i++) {
square = square !== undefined && square[xPath[i] + yPath[i]] || undefined;
let square: Square<any> | number | undefined = canvas;
for (let i = 0; i < depth; i++) {
square = (square !== undefined && square[xPath[i] + yPath[i]]) || undefined;
}
return square as number;
}
Expand All @@ -34,14 +40,14 @@ function setColor(png: PNG, x: number, y: number, color: PaletteColor) {
export const imagePng = functions.https.onRequest(async (request, response) => {
try {
const [depthSnapshot, canvasSnapshot] = await Promise.all([
database.ref('canvas').child(IMAGE_ID).child('depth').once('value'),
database.ref('canvas').child(IMAGE_ID).child('canvas').once('value'),
database.ref("canvas").child(IMAGE_ID).child("depth").once("value"),
database.ref("canvas").child(IMAGE_ID).child("canvas").once("value"),
]);
const depth = depthSnapshot.val() as Canvas['depth'];
const canvas = canvasSnapshot.val() as Canvas['canvas'];
const depth = depthSnapshot.val() as Canvas["depth"];
const canvas = canvasSnapshot.val() as Canvas["canvas"];

const size = Math.pow(2, depth);
const image = new PNG({width: size, height: size});
const image = new PNG({ width: size, height: size });
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const colorIndex = colorIndexFrom(depth, canvas, x, y);
Expand All @@ -51,8 +57,35 @@ export const imagePng = functions.https.onRequest(async (request, response) => {
}
}
const packedImage = image.pack();
packedImage.pipe(response).type('image/png')
packedImage.pipe(response).type("image/png");
} catch (error) {
response.status(500).send(error)
response.status(500).send(error);
}
});

export const historyGif = functions.https.onRequest(
async (request, response) => {
try {
const [depthSnapshot, historySnapshot] = await Promise.all([
database.ref("canvas").child(IMAGE_ID).child("depth").once("value"),
database.ref("canvas").child(IMAGE_ID).child("history").once("value"),
]);
const depth = depthSnapshot.val() as Canvas["depth"];
const history = Object.values(
historySnapshot.val() as Canvas["history"]
).sort((a, b) => a.timestamp - b.timestamp);

const size = Math.pow(2, depth);
const imageData = await encodeHistoryAsGif(size, size, history, {
speed: 10000,
lastFrameDelay: 5000,
minFrameDelay: 1,
maxFrameDelay: 500,
scale: 1,
});
response.type("image/gif").end(imageData, "binary");
} catch (error) {
response.status(500).send(error);
}
}
);
1 change: 0 additions & 1 deletion functions/src/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const Palette: Color[] = [
{r: 0x22, g: 0x22, b: 0x22, a: 0xFF },
{r: 0xFF, g: 0xA7, b: 0xD1, a: 0xFF },
{r: 0xE5, g: 0x00, b: 0x00, a: 0xFF },

{r: 0xE5, g: 0x95, b: 0x00, a: 0xFF },
{r: 0xA0, g: 0x6A, b: 0x42, a: 0xFF },
{r: 0xE5, g: 0xD9, b: 0x00, a: 0xFF },
Expand Down
Loading

0 comments on commit 569e074

Please sign in to comment.