Commit 03ed15f4 authored by Grant's avatar Grant
Browse files

no more round trips for frames

parent 094e1277
Loading
Loading
Loading
Loading
+80 −81
Original line number Diff line number Diff line
@@ -58,45 +58,34 @@ parentPort?.on("message", (msg: Message) => {
        const start = Date.now();
        const framesToGenerate = (msg.end - msg.start) / msg.step;

        // Create one canvas that persists across all frames in this worker.
        // Subsequent frames draw on the same canvas — no PNG decode needed.
        const canvas = Canvas.createCanvas(OUTPUT_SIZE[0], OUTPUT_SIZE[1]);
        const ctx = canvas.getContext("2d");
        const pixelScale = [
          OUTPUT_SIZE[0] / CANVAS_SIZE[0],
          OUTPUT_SIZE[1] / CANVAS_SIZE[1],
        ];

        let index = msg.frameStart;
        let needsInit = true;

        for (let time = msg.start; time < msg.end; time += msg.step) {
          const pixels = await getPixelsAsLog(time, time + msg.step);

          log("generating frame " + index);
          await generateFrame(index, pixels);
          parentPort?.postMessage({
            type: "progress",
            progress: (index - msg.frameStart) / framesToGenerate,
          });
          index++;
        }

        log(`Finished in ${((Date.now() - start) / 1000).toFixed(2)} seconds`);
        parentPort?.postMessage({ type: "finished" });
      });

      break;
    }
  }
});

const generateFrame = async (frameId: number, pixels: Pixel[]) => {
  const start = Date.now();

  const canvas = Canvas.createCanvas(OUTPUT_SIZE[0], OUTPUT_SIZE[1]);
  const ctx = canvas.getContext("2d");

          if (needsInit) {
            // First frame: fill white, then apply base state
            ctx.fillStyle = "#fff";
            ctx.fillRect(0, 0, canvas.width, canvas.height);

  if (
    fs.existsSync("./frames/" + getFilenameForFrameID(frameId - 1) + ".png")
  ) {
    // if a previous frame exists, use the content to prevent large queries
            const prevFilename =
              "./frames/" + frameFilename(index - 1) + ".png";
            if (fs.existsSync(prevFilename)) {
              // Load previous worker's last frame as the base
              const prevFrame = await Canvas.loadImage(
      fs.createReadStream(
        "./frames/" + getFilenameForFrameID(frameId - 1) + ".png",
      ),
                fs.createReadStream(prevFilename),
              );
              ctx.drawImage(
                prevFrame,
@@ -110,25 +99,30 @@ const generateFrame = async (frameId: number, pixels: Pixel[]) => {
                prevFrame.height,
              );
            } else {
              // No previous frame — load all existing pixels from DB
              let startTime: Date;

              if (pixels.length > 0) {
                startTime = new Date(pixels[0].date);
              } else {
                startTime = workerStart;
              }

    // no previous frame exists, guess we will have to load from the database
    const previousPixels = await query.existingPixelsBeforeTime(startTime);
    pixels = [...dbPixelsToLog(previousPixels.rows), ...pixels];
              const previousPixels =
                await query.existingPixelsBeforeTime(startTime);
              for (const p of dbPixelsToLog(previousPixels.rows)) {
                ctx.fillStyle = p.hex === "unset" ? "#FFFFFF" : "#" + p.hex;
                ctx.fillRect(
                  p.x * pixelScale[0],
                  p.y * pixelScale[1],
                  pixelScale[0],
                  pixelScale[1],
                );
              }
            }

  const pixelScale = [
    OUTPUT_SIZE[0] / CANVAS_SIZE[0],
    OUTPUT_SIZE[1] / CANVAS_SIZE[1],
  ];
            needsInit = false;
          }

  let i = 0;
          // Draw this frame's pixel changes on top of the existing canvas
          for (const pixel of pixels) {
            ctx.fillStyle = pixel.hex === "unset" ? "#FFFFFF" : "#" + pixel.hex;
            ctx.fillRect(
@@ -137,26 +131,30 @@ const generateFrame = async (frameId: number, pixels: Pixel[]) => {
              pixelScale[0],
              pixelScale[1],
            );
    i++;
          }

  let filename = getFilenameForFrameID(frameId);

  await canvas
    .encode("png")
    .then((data) =>
      fs.promises.writeFile(`./frames/${filename}.png`, data as Uint8Array),
    );
  log(
    "wrote frame " +
      frameId +
      " in " +
      ((Date.now() - start) / 1000).toFixed(2) +
      "s",
          // Encode current canvas state to PNG and write
          const data = await canvas.encode("png");
          await fs.promises.writeFile(
            `./frames/${frameFilename(index)}.png`,
            data as Uint8Array,
          );
};

const getFilenameForFrameID = (frameId: number) =>
          parentPort?.postMessage({
            type: "progress",
            progress: (index - msg.frameStart) / framesToGenerate,
          });
          index++;
        }

        log(`Finished in ${((Date.now() - start) / 1000).toFixed(2)} seconds`);
        parentPort?.postMessage({ type: "finished" });
      });
    }
  }
});

const frameFilename = (frameId: number) =>
  "0".repeat(Math.max(0, 5 - (frameId + "").length)) + frameId;

const getPixelsAsLog = async (start: number, end: number) => {
@@ -249,7 +247,8 @@ const getPixelsAsLog = async (start: number, end: number) => {
      });
    } else if (dbpixel.deletedAt) {
      // this is a deleted pixel that was in the timeframe
      const cacheKey = dbpixel.x + "," + dbpixel.y + ":" + dbpixel.createdAt.getTime();
      const cacheKey =
        dbpixel.x + "," + dbpixel.y + ":" + dbpixel.createdAt.getTime();
      pixels.push({
        x: dbpixel.x,
        y: dbpixel.y,