diff --git a/.vscode/settings.json b/.vscode/settings.json
index fe50924..a2b0ccb 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -21,5 +21,19 @@
"peacock.remoteColor": "aa11aa",
"deno.enable": true,
"deno.unstable": true,
- "liveServer.settings.port": 5501
+ "liveServer.settings.port": 5501,
+ "cSpell.words": [
+ "BGRA",
+ "blitting",
+ "dpng",
+ "idat",
+ "iend",
+ "ihdr",
+ "imgscr",
+ "Namee",
+ "NMAX",
+ "omggif's",
+ "plte",
+ "trns"
+ ]
}
\ No newline at end of file
diff --git a/animation/gif.ts b/animation/gif.ts
new file mode 100644
index 0000000..9684e4e
--- /dev/null
+++ b/animation/gif.ts
@@ -0,0 +1,63 @@
+import { Vector } from "../geometry/vector.ts";
+import { Frame, handleGIF } from "../processing/gif.ts";
+
+type frame = { canvas: HTMLCanvasElement } & Frame;
+export class GIFAnimation {
+ frames: frame[] = [];
+ canvas: HTMLCanvasElement;
+ ctx!: CanvasRenderingContext2D;
+ ready = false;
+
+ constructor(
+ url: string,
+ private origin: Vector,
+ private scale = 1,
+ ) {
+ this.canvas = document.createElement("canvas");
+
+ this.init(url);
+ }
+
+ async init(url: string) {
+ const res = await fetch(url);
+ const buf = new Uint8Array(await res.arrayBuffer());
+ const gif = handleGIF(buf);
+ this.frames = gif.frames;
+ this.frameTimes = this.frames.map((f) => f.delay);
+ this.totalAnimationTime = this.frameTimes.reduce(
+ (a, b) => a + b,
+ 0,
+ );
+
+ this.canvas.width = gif.w;
+ this.canvas.height = gif.h;
+ this.ctx = this.canvas.getContext("2d")!;
+ this.ready = true;
+ }
+
+ frameTimes!: number[];
+ totalAnimationTime = 0;
+ _frameCounter = 0;
+ currentFrameIndex = 0;
+
+ draw(timeSinceLastFrame: number) {
+ if (!this.ready) return;
+ this._frameCounter += timeSinceLastFrame;
+
+ const currentFrameDelay = this.frames[this.currentFrameIndex].delay * 10;
+
+ while (this._frameCounter >= currentFrameDelay) {
+ this._frameCounter -= currentFrameDelay;
+ this.currentFrameIndex = (this.currentFrameIndex + 1) %
+ this.frames.length;
+ }
+
+ const currentFrame = this.frames[this.currentFrameIndex];
+ doodler.drawImage(
+ currentFrame.canvas,
+ this.origin,
+ this.canvas.width * this.scale,
+ this.canvas.height * this.scale,
+ );
+ }
+}
diff --git a/animation/sprite.ts b/animation/sprite.ts
index 79e65ad..3595004 100644
--- a/animation/sprite.ts
+++ b/animation/sprite.ts
@@ -9,8 +9,8 @@ export class SpriteAnimation {
private cellHeight: number,
private cellCountX: number,
private cellCountY: number,
- private timing = 1,
- private scale = 1,
+ public timing = 1,
+ public scale = 1,
) {
this.image = new Image();
this.image.src = this.imageUrl;
diff --git a/bundle.js b/bundle.js
index 89ed87e..9e3505b 100644
--- a/bundle.js
+++ b/bundle.js
@@ -2,6 +2,404 @@
// deno-lint-ignore-file
// This code was bundled using `deno bundle` and it's not recommended to edit it manually
+class GifReader {
+ buf;
+ p;
+ width;
+ height;
+ globalPaletteOffset;
+ globalPaletteSize;
+ frames;
+ loopCountValue;
+ constructor(buf){
+ this.buf = buf;
+ this.p = 0;
+ this.width = 0;
+ this.height = 0;
+ this.globalPaletteOffset = null;
+ this.globalPaletteSize = null;
+ this.frames = [];
+ this.loopCountValue = null;
+ this.parseHeader();
+ this.parseFrames();
+ }
+ numFrames() {
+ return this.frames.length;
+ }
+ loopCount() {
+ return this.loopCountValue;
+ }
+ frameInfo(frameNum) {
+ if (frameNum < 0 || frameNum >= this.frames.length) {
+ throw new Error("Frame index out of range.");
+ }
+ return this.frames[frameNum];
+ }
+ decodeAndBlitFrameBGRA(frameNum, pixels) {
+ const frame = this.frameInfo(frameNum);
+ const numPixels = frame.width * frame.height;
+ const indexStream = new Uint8Array(numPixels);
+ GifReaderLZWOutputIndexStream(this.buf, frame.dataOffset, indexStream, numPixels);
+ const paletteOffset = frame.paletteOffset;
+ let trans = frame.transparentIndex;
+ if (trans === null) trans = 256;
+ const frameWidth = frame.width;
+ const frameStride = this.width - frameWidth;
+ let xLeft = frameWidth;
+ const opBeg = (frame.y * this.width + frame.x) * 4;
+ const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4;
+ let op = opBeg;
+ let scanStride = frameStride * 4;
+ if (frame.interlaced === true) {
+ scanStride += this.width * 4 * 7;
+ }
+ let interlaceSkip = 8;
+ for(let i = 0, il = indexStream.length; i < il; ++i){
+ const index = indexStream[i];
+ if (xLeft === 0) {
+ op += scanStride;
+ xLeft = frameWidth;
+ if (op >= opEnd) {
+ scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1);
+ op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1);
+ interlaceSkip >>= 1;
+ }
+ }
+ if (index === trans) {
+ op += 4;
+ } else {
+ const r = this.buf[(paletteOffset || 0) + index * 3];
+ const g = this.buf[(paletteOffset || 0) + index * 3 + 1];
+ const b = this.buf[(paletteOffset || 0) + index * 3 + 2];
+ pixels[op++] = b;
+ pixels[op++] = g;
+ pixels[op++] = r;
+ pixels[op++] = 255;
+ }
+ --xLeft;
+ }
+ }
+ decodeAndBlitFrameRGBA(frameNum, pixels) {
+ const frame = this.frameInfo(frameNum);
+ const numPixels = frame.width * frame.height;
+ const indexStream = new Uint8Array(numPixels);
+ GifReaderLZWOutputIndexStream(this.buf, frame.dataOffset, indexStream, numPixels);
+ const paletteOffset = frame.paletteOffset;
+ let trans = frame.transparentIndex;
+ if (trans === null) trans = 256;
+ const frameWidth = frame.width;
+ const frameStride = this.width - frameWidth;
+ let xLeft = frameWidth;
+ const opBeg = (frame.y * this.width + frame.x) * 4;
+ const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4;
+ let op = opBeg;
+ let scanStride = frameStride * 4;
+ if (frame.interlaced === true) {
+ scanStride += this.width * 4 * 7;
+ }
+ let interlaceSkip = 8;
+ for(let i = 0, il = indexStream.length; i < il; ++i){
+ const index = indexStream[i];
+ if (xLeft === 0) {
+ op += scanStride;
+ xLeft = frameWidth;
+ if (op >= opEnd) {
+ scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1);
+ op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1);
+ interlaceSkip >>= 1;
+ }
+ }
+ if (index === trans) {
+ op += 4;
+ } else {
+ const rI = (paletteOffset || 0) + index * 3;
+ const r = this.buf[rI];
+ const g = this.buf[rI + 1];
+ const b = this.buf[rI + 2];
+ pixels[op++] = r;
+ pixels[op++] = g;
+ pixels[op++] = b;
+ pixels[op++] = 255;
+ }
+ --xLeft;
+ }
+ }
+ parseHeader() {
+ if (this.buf[this.p++] !== 0x47 || this.buf[this.p++] !== 0x49 || this.buf[this.p++] !== 0x46 || this.buf[this.p++] !== 0x38 || (this.buf[this.p++] + 1 & 0xfd) !== 0x38 || this.buf[this.p++] !== 0x61) {
+ throw new Error("Invalid GIF 87a/89a header.");
+ }
+ }
+ parseLogicalScreenDescriptor() {}
+ parseGlobalColorTable() {}
+ parseFrames() {
+ const width = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const height = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const pf0 = this.buf[this.p++];
+ const global_palette_flag = pf0 >> 7;
+ const num_global_colors_pow2 = pf0 & 0x7;
+ const num_global_colors = 1 << num_global_colors_pow2 + 1;
+ this.buf[this.p++];
+ this.buf[this.p++];
+ let global_palette_offset = null;
+ let global_palette_size = null;
+ if (global_palette_flag) {
+ global_palette_offset = this.p;
+ global_palette_size = num_global_colors;
+ this.p += num_global_colors * 3;
+ }
+ let no_eof = true;
+ let delay = 0;
+ let transparentIndex = null;
+ let disposal = 0;
+ let loopCount = null;
+ this.width = width;
+ this.height = height;
+ while(no_eof && this.p < this.buf.length){
+ switch(this.buf[this.p++]){
+ case 0x21:
+ switch(this.buf[this.p++]){
+ case 0xff:
+ if (this.buf[this.p] !== 0x0b || this.buf[this.p + 1] == 0x4e && this.buf[this.p + 2] == 0x45 && this.buf[this.p + 3] == 0x54 && this.buf[this.p + 4] == 0x53 && this.buf[this.p + 5] == 0x43 && this.buf[this.p + 6] == 0x41 && this.buf[this.p + 7] == 0x50 && this.buf[this.p + 8] == 0x45 && this.buf[this.p + 9] == 0x32 && this.buf[this.p + 10] == 0x2e && this.buf[this.p + 11] == 0x30 && this.buf[this.p + 12] == 0x03 && this.buf[this.p + 13] == 0x01 && this.buf[this.p + 16] == 0) {
+ this.p += 14;
+ loopCount = this.buf[this.p++] | this.buf[this.p++] << 8;
+ this.p++;
+ } else {
+ this.p += 12;
+ while(true){
+ const block_size = this.buf[this.p++];
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break;
+ this.p += block_size;
+ }
+ }
+ break;
+ case 0xf9:
+ {
+ if (this.buf[this.p++] !== 0x4 || this.buf[this.p + 4] !== 0) {
+ throw new Error("Invalid graphics extension block.");
+ }
+ const pf1 = this.buf[this.p++];
+ delay = this.buf[this.p++] | this.buf[this.p++] << 8;
+ transparentIndex = this.buf[this.p++];
+ if ((pf1 & 1) === 0) transparentIndex = null;
+ disposal = pf1 >> 2 & 0x7;
+ this.p++;
+ break;
+ }
+ case 0x01:
+ case 0xfe:
+ while(true){
+ const block_size = this.buf[this.p++];
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break;
+ this.p += block_size;
+ }
+ break;
+ default:
+ throw new Error("Unknown graphic control label: 0x" + this.buf[this.p - 1].toString(16));
+ }
+ break;
+ case 0x2c:
+ {
+ const x = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const y = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const w = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const h = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const pf2 = this.buf[this.p++];
+ const local_palette_flag = pf2 >> 7;
+ const interlace_flag = pf2 >> 6 & 1;
+ const num_local_colors_pow2 = pf2 & 0x7;
+ const num_local_colors = 1 << num_local_colors_pow2 + 1;
+ let palette_offset = global_palette_offset;
+ let palette_size = global_palette_size;
+ let has_local_palette = false;
+ if (local_palette_flag) {
+ has_local_palette = true;
+ palette_offset = this.p;
+ palette_size = num_local_colors;
+ this.p += num_local_colors * 3;
+ }
+ const data_offset = this.p;
+ this.p++;
+ while(true){
+ const block_size = this.buf[this.p++];
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break;
+ this.p += block_size;
+ }
+ this.frames.push({
+ x,
+ y,
+ width: w,
+ height: h,
+ hasLocalPalette: has_local_palette,
+ paletteOffset: palette_offset,
+ paletteSize: palette_size,
+ dataOffset: data_offset,
+ dataLength: this.p - data_offset,
+ transparentIndex: transparentIndex,
+ interlaced: !!interlace_flag,
+ delay: delay,
+ disposal: disposal
+ });
+ break;
+ }
+ case 0x3b:
+ no_eof = false;
+ break;
+ default:
+ throw new Error("Unknown gif block: 0x" + this.buf[this.p - 1].toString(16));
+ }
+ }
+ }
+}
+function GifReaderLZWOutputIndexStream(codeStream, p, output, outputLength) {
+ const minCodeSize = codeStream[p++];
+ const clear_code = 1 << minCodeSize;
+ const eoi_code = clear_code + 1;
+ let nextCode = eoi_code + 1;
+ let curCodeSize = minCodeSize + 1;
+ let codeMask = (1 << curCodeSize) - 1;
+ let curShift = 0;
+ let cur = 0;
+ let op = 0;
+ let subBlockSize = codeStream[p++];
+ const codeTable = new Int32Array(4096);
+ let prevCode = null;
+ while(true){
+ while(curShift < 16){
+ if (subBlockSize === 0) break;
+ cur |= codeStream[p++] << curShift;
+ curShift += 8;
+ if (subBlockSize === 1) {
+ subBlockSize = codeStream[p++];
+ } else {
+ --subBlockSize;
+ }
+ }
+ if (curShift < curCodeSize) {
+ break;
+ }
+ const code = cur & codeMask;
+ cur >>= curCodeSize;
+ curShift -= curCodeSize;
+ if (code === clear_code) {
+ nextCode = eoi_code + 1;
+ curCodeSize = minCodeSize + 1;
+ codeMask = (1 << curCodeSize) - 1;
+ prevCode = null;
+ continue;
+ } else if (code === eoi_code) {
+ break;
+ }
+ const chaseCode = code < nextCode ? code : prevCode;
+ let chaseLength = 0;
+ let chase = chaseCode;
+ while(chase > clear_code){
+ chase = codeTable[chase] >> 8;
+ ++chaseLength;
+ }
+ const k = chase;
+ const op_end = op + chaseLength + (chaseCode !== code ? 1 : 0);
+ if (op_end > outputLength) {
+ console.log("Warning, gif stream longer than expected.");
+ return;
+ }
+ output[op++] = k;
+ op += chaseLength;
+ let b = op;
+ if (chaseCode !== code) {
+ output[op++] = k;
+ }
+ chase = chaseCode;
+ while(chaseLength--){
+ chase = codeTable[chase];
+ output[--b] = chase & 0xff;
+ chase >>= 8;
+ }
+ if (prevCode !== null && nextCode < 4096) {
+ codeTable[nextCode++] = prevCode << 8 | k;
+ if (nextCode >= codeMask + 1 && curCodeSize < 12) {
+ ++curCodeSize;
+ codeMask = codeMask << 1 | 1;
+ }
+ }
+ prevCode = code;
+ }
+ if (op !== outputLength) {
+ console.log("Warning, gif stream shorter than expected.");
+ }
+ return output;
+}
+function handleGIF(data) {
+ const framesBase64 = [];
+ const reader = new GifReader(data);
+ for(let i = 0; i < reader.numFrames(); i++){
+ const frameData = reader.frameInfo(i);
+ const canvas = document.createElement("canvas");
+ canvas.width = reader.width;
+ canvas.height = reader.height;
+ const ctx = canvas.getContext("2d");
+ const imageData = ctx.createImageData(reader.width, reader.height);
+ reader.decodeAndBlitFrameRGBA(i, imageData.data);
+ ctx.putImageData(imageData, 0, 0, frameData.x, frameData.y, frameData.width, frameData.height);
+ framesBase64.push({
+ ...frameData,
+ canvas
+ });
+ }
+ return {
+ w: reader.width,
+ h: reader.height,
+ frames: framesBase64
+ };
+}
+class GIFAnimation {
+ origin;
+ scale;
+ frames;
+ canvas;
+ ctx;
+ ready;
+ constructor(url, origin, scale = 1){
+ this.origin = origin;
+ this.scale = scale;
+ this.frames = [];
+ this.ready = false;
+ this.totalAnimationTime = 0;
+ this._frameCounter = 0;
+ this.currentFrameIndex = 0;
+ this.canvas = document.createElement("canvas");
+ this.init(url);
+ }
+ async init(url) {
+ const res = await fetch(url);
+ const buf = new Uint8Array(await res.arrayBuffer());
+ const gif = handleGIF(buf);
+ this.frames = gif.frames;
+ this.frameTimes = this.frames.map((f)=>f.delay);
+ this.totalAnimationTime = this.frameTimes.reduce((a, b)=>a + b, 0);
+ this.canvas.width = gif.w;
+ this.canvas.height = gif.h;
+ this.ctx = this.canvas.getContext("2d");
+ this.ready = true;
+ }
+ frameTimes;
+ totalAnimationTime;
+ _frameCounter;
+ currentFrameIndex;
+ draw(timeSinceLastFrame) {
+ if (!this.ready) return;
+ this._frameCounter += timeSinceLastFrame;
+ const currentFrameDelay = this.frames[this.currentFrameIndex].delay * 10;
+ while(this._frameCounter >= currentFrameDelay){
+ this._frameCounter -= currentFrameDelay;
+ this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;
+ }
+ const currentFrame = this.frames[this.currentFrameIndex];
+ doodler.drawImage(currentFrame.canvas, this.origin, this.canvas.width * this.scale, this.canvas.height * this.scale);
+ }
+}
const Constants = {
TWO_PI: Math.PI * 2
};
@@ -259,47 +657,6 @@ class OriginVector extends Vector {
return new OriginVector(origin, v);
}
}
-class SpriteAnimation {
- imageUrl;
- cellWidth;
- cellHeight;
- cellCountX;
- cellCountY;
- timing;
- scale;
- image;
- origin;
- constructor(imageUrl, cellWidth, cellHeight, cellCountX, cellCountY, timing = 1, scale = 1){
- this.imageUrl = imageUrl;
- this.cellWidth = cellWidth;
- this.cellHeight = cellHeight;
- this.cellCountX = cellCountX;
- this.cellCountY = cellCountY;
- this.timing = timing;
- this.scale = scale;
- this._frameCount = 0;
- this.image = new Image();
- this.image.src = this.imageUrl;
- this.origin = new Vector();
- }
- _frameCount;
- get frameCount() {
- return this._frameCount += this.timing;
- }
- getCell() {
- const time = Math.floor(this.frameCount);
- const x = time % this.cellCountX * this.cellWidth;
- const y = Math.floor(time / this.cellCountX) % this.cellCountY * this.cellHeight;
- return {
- x,
- y
- };
- }
- draw() {
- const { x, y } = this.getCell();
- doodler.drawSprite(this.image, new Vector(x, y), this.cellWidth, this.cellHeight, this.origin, this.cellWidth * this.scale, this.cellHeight * this.scale);
- }
-}
const easeInOut = (x)=>x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
const map = (value, x1, y1, x2, y2)=>(value - x1) * (y2 - x2) / (y1 - x1) + x2;
class Doodler {
@@ -339,18 +696,23 @@ class Doodler {
this.startDrawLoop();
}
timer;
+ lastFrameAt = 0;
startDrawLoop() {
+ this.lastFrameAt = Date.now();
this.timer = setInterval(()=>this.draw(), 1000 / this.framerate);
}
draw() {
+ const time = Date.now();
+ const frameTime = time - this.lastFrameAt;
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.bg;
this.ctx.fillRect(0, 0, this.width, this.height);
for (const [i, l] of (this.layers || []).entries()){
- l(this.ctx, i);
+ l(this.ctx, i, frameTime);
this.drawDeferred();
}
this.drawUI();
+ this.lastFrameAt = time;
}
createLayer(layer) {
this.layers.push(layer);
@@ -743,7 +1105,6 @@ class ZoomableDoodler extends Doodler {
setTimeout(()=>this.hasDoubleTapped = false, 300);
return false;
}
- console.log(this.mouse);
if (this.scale > 1) {
this.frameCounter = map(this.scale, this.maxScale, 1, 0, 59);
this.zoomDirection = -1;
@@ -874,25 +1235,16 @@ const init = (opt, zoomable, postInit)=>{
};
init({
width: 400,
- height: 400
+ height: 400,
+ framerate: 90
}, true, (ctx)=>{
ctx.imageSmoothingEnabled = false;
});
-new Vector(100, 300);
-const v = new Vector(30, 30);
-doodler.registerDraggable(v, 20);
const img = new Image();
-img.src = "./skeleton.png";
-img.hidden;
-document.body.append(img);
-new Vector(200, 200);
-const animSprite = new SpriteAnimation("./EngineSprites.png", 100, 20, 1, 5, .02, 5);
-doodler.createLayer(()=>{
- animSprite.draw();
-});
-document.addEventListener("keyup", (e)=>{
- e.preventDefault();
- if (e.key === " ") {
- doodler.unregisterDraggable(v);
- }
+img.src = "./pixel fire.gif";
+const p = new Vector();
+const gif = new GIFAnimation("./fire-joypixels.gif", p, .5);
+doodler.createLayer((c, i, t)=>{
+ gif.draw(t);
});
+requestAnimationFrame;
diff --git a/canvas.ts b/canvas.ts
index 828ecff..3ff90a5 100644
--- a/canvas.ts
+++ b/canvas.ts
@@ -27,7 +27,11 @@ export interface IDoodlerOptions {
framerate?: number;
}
-type layer = (ctx: CanvasRenderingContext2D, index: number) => void;
+type layer = (
+ ctx: CanvasRenderingContext2D,
+ index: number,
+ frameTime: number,
+) => void;
export class Doodler {
protected ctx: CanvasRenderingContext2D;
@@ -85,11 +89,15 @@ export class Doodler {
}
private timer?: number;
+ private lastFrameAt = 0;
private startDrawLoop() {
+ this.lastFrameAt = Date.now();
this.timer = setInterval(() => this.draw(), 1000 / this.framerate);
}
protected draw() {
+ const time = Date.now();
+ const frameTime = time - this.lastFrameAt;
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.bg;
this.ctx.fillRect(0, 0, this.width, this.height);
@@ -97,10 +105,12 @@ export class Doodler {
// d.point.set(this.mouseX,this.mouseY);
// }
for (const [i, l] of (this.layers || []).entries()) {
- l(this.ctx, i);
+ l(this.ctx, i, frameTime);
this.drawDeferred();
}
this.drawUI();
+
+ this.lastFrameAt = time;
}
// Layer management
@@ -215,9 +225,9 @@ export class Doodler {
this.ctx.restore();
}
- drawImage(img: HTMLImageElement, at: Vector): void;
- drawImage(img: HTMLImageElement, at: Vector, w: number, h: number): void;
- drawImage(img: HTMLImageElement, at: Vector, w?: number, h?: number) {
+ drawImage(img: CanvasImageSource, at: Vector): void;
+ drawImage(img: CanvasImageSource, at: Vector, w: number, h: number): void;
+ drawImage(img: CanvasImageSource, at: Vector, w?: number, h?: number) {
w && h
? this.ctx.drawImage(img, at.x, at.y, w, h)
: this.ctx.drawImage(img, at.x, at.y);
diff --git a/cartoon fire.png b/cartoon fire.png
new file mode 100644
index 0000000..a95aebd
Binary files /dev/null and b/cartoon fire.png differ
diff --git a/deno.jsonc b/deno.jsonc
index c24fed6..ba6983a 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -9,6 +9,11 @@
]
},
"tasks": {
- "dev" : "deno bundle --watch main.ts bundle.js"
+ "dev": "deno bundle --watch main.ts bundle.js"
+ },
+ "imports": {
+ "std": "https://deno.land/std@0.205.0/mod.ts",
+ "std/": "https://deno.land/std@0.205.0/",
+ "dpng": "https://deno.land/x/dpng@0.7.5/mod.ts"
}
}
\ No newline at end of file
diff --git a/fire-joypixels.gif b/fire-joypixels.gif
new file mode 100644
index 0000000..a70405a
Binary files /dev/null and b/fire-joypixels.gif differ
diff --git a/index.html b/index.html
index b9cdf36..095b092 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,26 @@
+
Doodler
+
+
+
\ No newline at end of file
diff --git a/main.ts b/main.ts
index 43d3e97..176c06e 100644
--- a/main.ts
+++ b/main.ts
@@ -1,13 +1,16 @@
///
+import { GIFAnimation } from "./animation/gif.ts";
import { SpriteAnimation } from "./animation/sprite.ts";
import { initializeDoodler, Vector } from "./mod.ts";
+import { handleGIF } from "./processing/gif.ts";
import { ZoomableDoodler } from "./zoomableCanvas.ts";
initializeDoodler(
{
width: 400,
height: 400,
+ framerate: 90,
},
true,
(ctx) => {
@@ -15,94 +18,15 @@ initializeDoodler(
},
);
-const movingVector = new Vector(100, 300);
-let angleMultiplier = 0;
-const v = new Vector(30, 30);
-doodler.registerDraggable(v, 20);
const img = new Image();
-img.src = "./skeleton.png";
-img.hidden;
-document.body.append(img);
+img.src = "./pixel fire.gif";
-const p = new Vector(200, 200);
+const p = new Vector();
+const gif = new GIFAnimation("./fire-joypixels.gif", p, .5);
-const animSprite = new SpriteAnimation(
- "./EngineSprites.png",
- 100,
- 20,
- 1,
- 5,
- .02,
- 5,
-);
-
-doodler.createLayer(() => {
- animSprite.draw();
- // const [gamepad] = navigator.getGamepads();
- // const deadzone = 0.04;
- // if (gamepad) {
- // const leftX = gamepad.axes[0];
- // const leftY = gamepad.axes[1];
- // p.add(
- // Math.min(Math.max(leftX - deadzone, 0), leftX + deadzone),
- // Math.min(Math.max(leftY - deadzone, 0), leftY + deadzone),
- // );
-
- // const rigthX = gamepad.axes[2];
- // const rigthY = gamepad.axes[3];
- // (doodler as ZoomableDoodler).moveOrigin({ x: -rigthX * 5, y: -rigthY * 5 });
-
- // if (gamepad.buttons[7].value) {
- // (doodler as ZoomableDoodler).scaleAt(
- // { x: 200, y: 200 },
- // 1 + (gamepad.buttons[7].value / 5),
- // );
- // }
- // if (gamepad.buttons[6].value) {
- // (doodler as ZoomableDoodler).scaleAt(
- // { x: 200, y: 200 },
- // 1 - (gamepad.buttons[6].value / 5),
- // );
- // }
- // }
- // doodler.drawImageWithOutline(img, p);
- // doodler.line(new Vector(100, 100), new Vector(200, 200))
- // doodler.dot(new Vector(300, 300))
- // doodler.fillCircle(movingVector, 6, { color: 'red' });
- // doodler.drawRect(new Vector(50, 50), movingVector.x, movingVector.y);
- // doodler.fillRect(new Vector(200, 250), 30, 10)
-
- // doodler.drawCenteredSquare(new Vector(200, 200), 40, { color: 'purple', weight: 5 })
- // doodler.drawBezier(new Vector(100, 150), movingVector, new Vector(150, 300), new Vector(100, 250))
-
- // let rotatedOrigin = new Vector(200, 200)
- // doodler.drawRotated(rotatedOrigin, Math.PI * angleMultiplier, () => {
- // doodler.drawCenteredSquare(rotatedOrigin, 30)
- // doodler.drawSprite(img, new Vector(0, 40), 80, 20, new Vector(160, 300), 80, 20)
- // })
-
- // movingVector.set((movingVector.x + 1) % 400, movingVector.y);
- // angleMultiplier += .001;
-
- // doodler.drawSprite(img, new Vector(0, 40), 80, 20, new Vector(100, 300), 80, 20)
-
- // doodler.drawScaled(1.5, () => {
- // doodler.line(p.copy().add(-8, 10), p.copy().add(8, 10), {
- // color: "grey",
- // weight: 2,
- // });
- // doodler.line(p.copy().add(-8, -10), p.copy().add(8, -10), {
- // color: "grey",
- // weight: 2,
- // });
- // doodler.line(p, p.copy().add(0, 12), { color: "brown", weight: 4 });
- // doodler.line(p, p.copy().add(0, -12), { color: "brown", weight: 4 });
- // });
+doodler.createLayer((c, i, t) => {
+ gif.draw(t);
+ // c.drawImage(img, 0, 0);
});
-document.addEventListener("keyup", (e) => {
- e.preventDefault();
- if (e.key === " ") {
- doodler.unregisterDraggable(v);
- }
-});
+requestAnimationFrame;
diff --git a/pixel fire.gif b/pixel fire.gif
new file mode 100644
index 0000000..15b8807
Binary files /dev/null and b/pixel fire.gif differ
diff --git a/processing/gif.ts b/processing/gif.ts
new file mode 100644
index 0000000..a80b8f9
--- /dev/null
+++ b/processing/gif.ts
@@ -0,0 +1,573 @@
+export type Frame = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ hasLocalPalette: boolean;
+ paletteOffset: number | null;
+ paletteSize: number | null;
+ dataOffset: number;
+ dataLength: number;
+ transparentIndex: number | null;
+ interlaced: boolean;
+ delay: number;
+ disposal: number;
+};
+
+/**
+ * @classdesc This class is a TS refactoring of 'omggif's GifReader constructor, I simply copy-pasta'd it to be able to include using a deno bundler since they currently do not work properly with npm packages. Due to this, if anything doesn't work, do NOT contact the original author for issues with this class
+ * @author original - Dean McNamee
+ * @author refactor - Emma Short
+ */
+
+export class GifReader {
+ private buf: Uint8Array;
+ private p: number;
+ public width: number;
+ public height: number;
+ private globalPaletteOffset: number | null;
+ private globalPaletteSize: number | null;
+ private frames: Frame[];
+ private loopCountValue: number | null;
+
+ constructor(buf: Uint8Array) {
+ this.buf = buf;
+ this.p = 0;
+ this.width = 0;
+ this.height = 0;
+ this.globalPaletteOffset = null;
+ this.globalPaletteSize = null;
+ this.frames = [];
+ this.loopCountValue = null;
+
+ this.parseHeader();
+ this.parseFrames();
+ }
+
+ public numFrames(): number {
+ return this.frames.length;
+ }
+
+ public loopCount(): number | null {
+ return this.loopCountValue;
+ }
+
+ public frameInfo(frameNum: number): Frame {
+ if (frameNum < 0 || frameNum >= this.frames.length) {
+ throw new Error("Frame index out of range.");
+ }
+ return this.frames[frameNum];
+ }
+
+ public decodeAndBlitFrameBGRA(
+ frameNum: number,
+ pixels: Uint8ClampedArray,
+ ): void {
+ const frame = this.frameInfo(frameNum);
+ const numPixels = frame.width * frame.height;
+ const indexStream = new Uint8Array(numPixels); // At most 8-bit indices.
+ GifReaderLZWOutputIndexStream(
+ this.buf,
+ frame.dataOffset,
+ indexStream,
+ numPixels,
+ );
+ const paletteOffset = frame.paletteOffset;
+
+ let trans = frame.transparentIndex;
+ if (trans === null) trans = 256;
+
+ // We are possibly just blitting to a portion of the entire frame.
+ // That is a subRect within the frameRect, so the additional pixels
+ // must be skipped over after we finished a scanline.
+ const frameWidth = frame.width;
+ const frameStride = this.width - frameWidth;
+ let xLeft = frameWidth; // Number of subRect pixels left in scanline.
+
+ // Output index of the top left corner of the subRect.
+ const opBeg = ((frame.y * this.width) + frame.x) * 4;
+ // Output index of what would be the left edge of the subRect, one row
+ // below it, i.e. the index at which an interlace pass should wrap.
+ const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4;
+ let op = opBeg;
+
+ let scanStride = frameStride * 4;
+
+ // Use scanStride to skip past the rows when interlacing. This is skipping
+ // 7 rows for the first two passes, then 3 then 1.
+ if (frame.interlaced === true) {
+ scanStride += this.width * 4 * 7; // Pass 1.
+ }
+
+ let interlaceSkip = 8; // Tracking the row interval in the current pass.
+
+ for (let i = 0, il = indexStream.length; i < il; ++i) {
+ const index = indexStream[i];
+
+ if (xLeft === 0) { // Beginning of new scan line
+ op += scanStride;
+ xLeft = frameWidth;
+ if (op >= opEnd) { // Catch the wrap to switch passes when interlacing.
+ scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1);
+ // interlaceSkip / 2 * 4 is interlaceSkip << 1.
+ op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1);
+ interlaceSkip >>= 1;
+ }
+ }
+
+ if (index === trans) {
+ op += 4;
+ } else {
+ const r = this.buf[(paletteOffset || 0) + index * 3];
+ const g = this.buf[(paletteOffset || 0) + index * 3 + 1];
+ const b = this.buf[(paletteOffset || 0) + index * 3 + 2];
+ pixels[op++] = b;
+ pixels[op++] = g;
+ pixels[op++] = r;
+ pixels[op++] = 255;
+ }
+ --xLeft;
+ }
+ }
+
+ public decodeAndBlitFrameRGBA(
+ frameNum: number,
+ pixels: Uint8ClampedArray,
+ ): void {
+ const frame = this.frameInfo(frameNum);
+ const numPixels = frame.width * frame.height;
+ const indexStream = new Uint8Array(numPixels); // At most 8-bit indices.
+ GifReaderLZWOutputIndexStream(
+ this.buf,
+ frame.dataOffset,
+ indexStream,
+ numPixels,
+ );
+ // debugger;
+ const paletteOffset = frame.paletteOffset;
+
+ let trans = frame.transparentIndex;
+ if (trans === null) trans = 256;
+
+ // We are possibly just blitting to a portion of the entire frame.
+ // That is a subRect within the frameRect, so the additional pixels
+ // must be skipped over after we finished a scanline.
+ const frameWidth = frame.width;
+ const frameStride = this.width - frameWidth;
+ let xLeft = frameWidth; // Number of subRect pixels left in scanline.
+
+ // Output index of the top left corner of the subRect.
+ const opBeg = ((frame.y * this.width) + frame.x) * 4;
+ // Output index of what would be the left edge of the subRect, one row
+ // below it, i.e. the index at which an interlace pass should wrap.
+ const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4;
+ let op = opBeg;
+
+ let scanStride = frameStride * 4;
+
+ // Use scanStride to skip past the rows when interlacing. This is skipping
+ // 7 rows for the first two passes, then 3 then 1.
+ if (frame.interlaced === true) {
+ scanStride += this.width * 4 * 7; // Pass 1.
+ }
+
+ let interlaceSkip = 8; // Tracking the row interval in the current pass.
+
+ for (let i = 0, il = indexStream.length; i < il; ++i) {
+ const index = indexStream[i];
+
+ if (xLeft === 0) { // Beginning of new scan line
+ op += scanStride;
+ xLeft = frameWidth;
+ if (op >= opEnd) { // Catch the wrap to switch passes when interlacing.
+ scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1);
+ // interlaceSkip / 2 * 4 is interlaceSkip << 1.
+ op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1);
+ interlaceSkip >>= 1;
+ }
+ }
+
+ if (index === trans) {
+ op += 4;
+ } else {
+ const rI = (paletteOffset || 0) + index * 3;
+ const r = this.buf[rI];
+ const g = this.buf[rI + 1];
+ const b = this.buf[rI + 2];
+ pixels[op++] = r;
+ pixels[op++] = g;
+ pixels[op++] = b;
+ pixels[op++] = 255;
+ }
+ --xLeft;
+ }
+ }
+
+ // Additional private or public methods should be implemented below
+
+ private parseHeader(): void {
+ // Parse the GIF file header
+ if (
+ this.buf[this.p++] !== 0x47 || this.buf[this.p++] !== 0x49 ||
+ this.buf[this.p++] !== 0x46 ||
+ this.buf[this.p++] !== 0x38 || (this.buf[this.p++] + 1 & 0xfd) !== 0x38 ||
+ this.buf[this.p++] !== 0x61
+ ) {
+ throw new Error("Invalid GIF 87a/89a header.");
+ }
+ }
+
+ private parseLogicalScreenDescriptor(): void {
+ // Parse the Logical Screen Descriptor block
+ }
+
+ private parseGlobalColorTable(): void {
+ // Parse the Global Color Table block if it exists
+ }
+
+ private parseFrames(): void {
+ const width = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const height = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const pf0 = this.buf[this.p++]; // .
+ const global_palette_flag = pf0 >> 7;
+ const num_global_colors_pow2 = pf0 & 0x7;
+ const num_global_colors = 1 << (num_global_colors_pow2 + 1);
+ const background = this.buf[this.p++];
+ this.buf[this.p++]; // Pixel aspect ratio (unused?).
+
+ let global_palette_offset = null;
+ let global_palette_size = null;
+
+ if (global_palette_flag) {
+ global_palette_offset = this.p;
+ global_palette_size = num_global_colors;
+ this.p += num_global_colors * 3; // Seek past palette.
+ }
+
+ let no_eof = true;
+
+ const frames = [];
+
+ let delay = 0;
+ let transparentIndex = null;
+ let disposal = 0; // 0 - No disposal specified.
+ let loopCount = null;
+
+ this.width = width;
+ this.height = height;
+
+ while (no_eof && this.p < this.buf.length) {
+ switch (this.buf[this.p++]) {
+ case 0x21: // Graphics Control Extension Block
+ switch (this.buf[this.p++]) {
+ case 0xff: // Application specific block
+ // Try if it's a Netscape block (with animation loop counter).
+ if (
+ this.buf[this.p] !== 0x0b || // 21 FF already read, check block size.
+ // NETSCAPE2.0
+ this.buf[this.p + 1] == 0x4e && this.buf[this.p + 2] == 0x45 &&
+ this.buf[this.p + 3] == 0x54 &&
+ this.buf[this.p + 4] == 0x53 &&
+ this.buf[this.p + 5] == 0x43 &&
+ this.buf[this.p + 6] == 0x41 &&
+ this.buf[this.p + 7] == 0x50 &&
+ this.buf[this.p + 8] == 0x45 &&
+ this.buf[this.p + 9] == 0x32 &&
+ this.buf[this.p + 10] == 0x2e &&
+ this.buf[this.p + 11] == 0x30 &&
+ // Sub-block
+ this.buf[this.p + 12] == 0x03 &&
+ this.buf[this.p + 13] == 0x01 && this.buf[this.p + 16] == 0
+ ) {
+ this.p += 14;
+ loopCount = this.buf[this.p++] | this.buf[this.p++] << 8;
+ this.p++; // Skip terminator.
+ } else { // We don't know what it is, just try to get past it.
+ this.p += 12;
+ while (true) { // Seek through subblocks.
+ const block_size = this.buf[this.p++];
+ // Bad block size (ex: undefined from an out of bounds read).
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break; // 0 size is terminator
+ this.p += block_size;
+ }
+ }
+ break;
+
+ case 0xf9: { // Graphics Control Extension
+ if (this.buf[this.p++] !== 0x4 || this.buf[this.p + 4] !== 0) {
+ throw new Error("Invalid graphics extension block.");
+ }
+ const pf1 = this.buf[this.p++];
+ delay = this.buf[this.p++] | this.buf[this.p++] << 8;
+ transparentIndex = this.buf[this.p++];
+ if ((pf1 & 1) === 0) transparentIndex = null;
+ disposal = pf1 >> 2 & 0x7;
+ this.p++; // Skip terminator.
+ break;
+ }
+
+ // Plain Text Extension could be present and we just want to be able
+ // to parse past it. It follows the block structure of the comment
+ // extension enough to reuse the path to skip through the blocks.
+ case 0x01: // Plain Text Extension (fallthrough to Comment Extension)
+ case 0xfe: // Comment Extension.
+ while (true) { // Seek through subblocks.
+ const block_size = this.buf[this.p++];
+ // Bad block size (ex: undefined from an out of bounds read).
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break; // 0 size is terminator
+ this.p += block_size;
+ }
+ break;
+
+ default:
+ throw new Error(
+ "Unknown graphic control label: 0x" +
+ this.buf[this.p - 1].toString(16),
+ );
+ }
+ break;
+
+ case 0x2c: { // Image Descriptor.
+ const x = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const y = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const w = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const h = this.buf[this.p++] | this.buf[this.p++] << 8;
+ const pf2 = this.buf[this.p++];
+ const local_palette_flag = pf2 >> 7;
+ const interlace_flag = pf2 >> 6 & 1;
+ const num_local_colors_pow2 = pf2 & 0x7;
+ const num_local_colors = 1 << (num_local_colors_pow2 + 1);
+ let palette_offset = global_palette_offset;
+ let palette_size = global_palette_size;
+ let has_local_palette = false;
+ if (local_palette_flag) {
+ has_local_palette = true;
+ palette_offset = this.p; // Override with local palette.
+ palette_size = num_local_colors;
+ this.p += num_local_colors * 3; // Seek past palette.
+ }
+
+ const data_offset = this.p;
+
+ this.p++; // codeSize
+ while (true) {
+ const block_size = this.buf[this.p++];
+ // Bad block size (ex: undefined from an out of bounds read).
+ if (!(block_size >= 0)) throw Error("Invalid block size");
+ if (block_size === 0) break; // 0 size is terminator
+ this.p += block_size;
+ }
+
+ this.frames.push({
+ x,
+ y,
+ width: w,
+ height: h,
+ hasLocalPalette: has_local_palette,
+ paletteOffset: palette_offset,
+ paletteSize: palette_size,
+ dataOffset: data_offset,
+ dataLength: this.p - data_offset,
+ transparentIndex: transparentIndex,
+ interlaced: !!interlace_flag,
+ delay: delay,
+ disposal: disposal,
+ });
+ break;
+ }
+ case 0x3b: // Trailer Marker (end of file).
+ no_eof = false;
+ break;
+
+ default:
+ throw new Error(
+ "Unknown gif block: 0x" + this.buf[this.p - 1].toString(16),
+ );
+ }
+ }
+ }
+
+ // private readSubBlocks(): string {
+ // // Read a series of sub-blocks
+ // return "";
+ // }
+
+ // private readBlockTerminator(): void {
+ // // Read a block terminator if necessary
+ // }
+}
+
+function GifReaderLZWOutputIndexStream(
+ codeStream: Uint8Array,
+ p: number,
+ output: Uint8Array,
+ outputLength: number,
+) {
+ const minCodeSize = codeStream[p++];
+
+ const clear_code = 1 << minCodeSize;
+ const eoi_code = clear_code + 1;
+ let nextCode = eoi_code + 1;
+
+ let curCodeSize = minCodeSize + 1; // Number of bits per code.
+ // NOTE: This shares the same name as the encoder, but has a different
+ // meaning here. Here this masks each code coming from the code stream.
+ let codeMask = (1 << curCodeSize) - 1;
+ let curShift = 0;
+ let cur = 0;
+
+ let op = 0; // Output pointer.
+
+ let subBlockSize = codeStream[p++];
+
+ const codeTable = new Int32Array(4096); // Can be signed, we only use 20 bits.
+
+ let prevCode = null; // Track code-1.
+
+ while (true) {
+ // Read up to two bytes, making sure we always 12-bits for max sized code.
+ while (curShift < 16) {
+ if (subBlockSize === 0) break; // No more data to be read.
+
+ cur |= codeStream[p++] << curShift;
+ curShift += 8;
+
+ if (subBlockSize === 1) { // Never let it get to 0 to hold logic above.
+ subBlockSize = codeStream[p++]; // Next subBlock.
+ } else {
+ --subBlockSize;
+ }
+ }
+
+ if (curShift < curCodeSize) {
+ break;
+ }
+
+ const code = cur & codeMask;
+ cur >>= curCodeSize;
+ curShift -= curCodeSize;
+
+ if (code === clear_code) {
+ // We don't actually have to clear the table. This could be a good idea
+ // for greater error checking, but we don't really do any anyway. We
+ // will just track it with next_code and overwrite old entries.
+
+ nextCode = eoi_code + 1;
+ curCodeSize = minCodeSize + 1;
+ codeMask = (1 << curCodeSize) - 1;
+
+ // Don't update prev_code ?
+ prevCode = null;
+ continue;
+ } else if (code === eoi_code) {
+ break;
+ }
+
+ // We have a similar situation as the decoder, where we want to store
+ // variable length entries (code table entries), but we want to do in a
+ // faster manner than an array of arrays. The code below stores sort of a
+ // linked list within the code table, and then "chases" through it to
+ // construct the dictionary entries. When a new entry is created, just the
+ // last byte is stored, and the rest (prefix) of the entry is only
+ // referenced by its table entry. Then the code chases through the
+ // prefixes until it reaches a single byte code. We have to chase twice,
+ // first to compute the length, and then to actually copy the data to the
+ // output (backwards, since we know the length). The alternative would be
+ // storing something in an intermediate stack, but that doesn't make any
+ // more sense. I implemented an approach where it also stored the length
+ // in the code table, although it's a bit tricky because you run out of
+ // bits (12 + 12 + 8), but I didn't measure much improvements (the table
+ // entries are generally not the long). Even when I created benchmarks for
+ // very long table entries the complexity did not seem worth it.
+ // The code table stores the prefix entry in 12 bits and then the suffix
+ // byte in 8 bits, so each entry is 20 bits.
+
+ const chaseCode: number = code < nextCode ? code : prevCode as number;
+
+ // Chase what we will output, either {CODE} or {CODE-1}.
+ let chaseLength = 0;
+ let chase = chaseCode as number;
+ while (chase > clear_code) {
+ chase = codeTable[chase] >> 8;
+ ++chaseLength;
+ }
+
+ const k = chase;
+
+ const op_end = op + chaseLength + (chaseCode !== code ? 1 : 0);
+ if (op_end > outputLength) {
+ console.log("Warning, gif stream longer than expected.");
+ return;
+ }
+
+ // Already have the first byte from the chase, might as well write it fast.
+ output[op++] = k;
+
+ op += chaseLength;
+ let b = op; // Track pointer, writing backwards.
+
+ if (chaseCode !== code) { // The case of emitting {CODE-1} + k.
+ output[op++] = k;
+ }
+
+ chase = chaseCode;
+ while (chaseLength--) {
+ chase = codeTable[chase];
+ output[--b] = chase & 0xff; // Write backwards.
+ chase >>= 8; // Pull down to the prefix code.
+ }
+
+ if (prevCode !== null && nextCode < 4096) {
+ codeTable[nextCode++] = prevCode << 8 | k;
+
+ if (nextCode >= codeMask + 1 && curCodeSize < 12) {
+ ++curCodeSize;
+ codeMask = codeMask << 1 | 1;
+ }
+ }
+
+ prevCode = code;
+ }
+
+ if (op !== outputLength) {
+ console.log("Warning, gif stream shorter than expected.");
+ }
+
+ return output;
+}
+
+export function handleGIF(
+ data: Uint8Array,
+) {
+ const framesBase64: ({ canvas: HTMLCanvasElement } & Frame)[] = [];
+ const reader = new GifReader(data);
+
+ for (let i = 0; i < reader.numFrames(); i++) {
+ const frameData = reader.frameInfo(i);
+ // const buf = new Uint8Array(frameData.width * frameData.height * 4);
+ const canvas = document.createElement("canvas");
+ canvas.width = reader.width;
+ canvas.height = reader.height;
+ const ctx = canvas.getContext("2d")!;
+ const imageData = ctx.createImageData(reader.width, reader.height);
+ reader.decodeAndBlitFrameRGBA(i, imageData.data);
+
+ ctx.putImageData(
+ imageData,
+ 0,
+ 0,
+ frameData.x,
+ frameData.y,
+ frameData.width,
+ frameData.height,
+ );
+ framesBase64.push({ ...frameData, canvas });
+ }
+ return {
+ w: reader.width,
+ h: reader.height,
+ frames: framesBase64,
+ };
+}
diff --git a/zoomableCanvas.ts b/zoomableCanvas.ts
index 35a01ce..04e2ba1 100644
--- a/zoomableCanvas.ts
+++ b/zoomableCanvas.ts
@@ -157,8 +157,6 @@ export class ZoomableDoodler extends Doodler {
// this.origin.x = 0;
// this.origin.y = 0;
- console.log(this.mouse);
-
if (this.scale > 1) {
this.frameCounter = map(this.scale, this.maxScale, 1, 0, 59);
this.zoomDirection = -1;