forked from maxnet/pico-webserver
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2764302
commit 60d05c4
Showing
19 changed files
with
588 additions
and
195 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { ofetch } from 'ofetch'; | ||
import { GifReader } from 'omggif'; | ||
import { LOCALSTORAGE_SCALE_KEY } from '../../consts.ts'; | ||
import { imageDatasToBlob } from './imageDatasToBlob.ts'; | ||
import { scaleImageData } from './scaleImageData.ts'; | ||
|
||
export const getScaledGif = async (url: string): Promise<Blob> => { | ||
const scale = parseInt(localStorage.getItem(LOCALSTORAGE_SCALE_KEY) || '1', 10); | ||
|
||
const gifBlob = await ofetch<Blob>(url); | ||
const gifBuffer = await gifBlob.arrayBuffer(); | ||
const intArray = new Uint8Array(gifBuffer); | ||
const reader = new GifReader(intArray as Buffer); | ||
|
||
const info = reader.frameInfo(0); | ||
const fps = Math.round(100 / info.delay); | ||
|
||
const frames: ImageData[] = new Array(reader.numFrames()).fill(0).map((_, k) => { | ||
const image = new ImageData(info.width, info.height); | ||
reader.decodeAndBlitFrameRGBA(k, image.data as any); | ||
return scaleImageData(scale, image); | ||
}); | ||
|
||
return imageDatasToBlob(frames, fps); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
export const getScaledCanvas = ( | ||
imageSource: HTMLImageElement | HTMLCanvasElement, | ||
scaleFactor: number | ||
): HTMLCanvasElement => { | ||
// Create a new canvas for the scaled output | ||
const scaledCanvas = document.createElement("canvas"); | ||
const scaledWidth = imageSource.width * scaleFactor; | ||
const scaledHeight = imageSource.height * scaleFactor; | ||
|
||
scaledCanvas.width = scaledWidth; | ||
scaledCanvas.height = scaledHeight; | ||
|
||
const scaledContext = scaledCanvas.getContext("2d"); | ||
if (!scaledContext) { | ||
throw new Error("Failed to get 2D context from scaled canvas."); | ||
} | ||
|
||
// Disable image smoothing for nearest-neighbor scaling | ||
scaledContext.imageSmoothingEnabled = false; | ||
|
||
// Scale the source canvas and draw to the new canvas | ||
scaledContext.drawImage( | ||
imageSource, | ||
0, 0, imageSource.width, imageSource.height, | ||
0, 0, scaledWidth, scaledHeight | ||
); | ||
|
||
return scaledCanvas; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import chunk from 'chunk'; | ||
import { GifWriter } from 'omggif'; | ||
|
||
export interface GifFrameData { | ||
palette: number[], | ||
pixels: number[], | ||
} | ||
|
||
export const imageDatasToBlob = (frames: ImageData[], fps: number): Blob => { | ||
const buf: number[] = []; | ||
const gifWriter: GifWriter = new GifWriter(buf, frames[0].width, frames[0].height, { loop: 0xffff }); | ||
|
||
for (const frame of frames) { | ||
const { | ||
palette, | ||
pixels, | ||
} = chunk(frame.data, 4).reduce((acc: GifFrameData, [r, g, b]): GifFrameData => { | ||
// eslint-disable-next-line no-bitwise | ||
const color: number = (r << 16) + (g << 8) + b; | ||
let colorIndex: number = acc.palette.findIndex((c) => c === color); | ||
if (colorIndex === -1) { | ||
colorIndex = acc.palette.length; | ||
acc.palette.push(color); | ||
} | ||
|
||
acc.pixels.push(colorIndex); | ||
|
||
return acc; | ||
}, { pixels: [], palette: [] }); | ||
|
||
gifWriter.addFrame(0, 0, frame.width, frame.height, pixels, { | ||
delay: Math.round(100 / fps), | ||
palette, | ||
}); | ||
} | ||
|
||
const bufferSize = gifWriter.end(); | ||
|
||
const file = new Blob( | ||
[new Uint8Array(buf.slice(0, bufferSize)).buffer], | ||
{ type: 'image/gif' }, | ||
); | ||
|
||
return file; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { getScaledCanvas } from './getScaledcanvas.ts'; | ||
|
||
export const scaleImageData = (scale: number, sourceImageData: ImageData): ImageData => { | ||
const canvas = document.createElement('canvas'); | ||
canvas.width = sourceImageData.width; | ||
canvas.height = sourceImageData.height; | ||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | ||
|
||
ctx.putImageData(sourceImageData, 0, 0); | ||
const result = getScaledCanvas(canvas, scale); | ||
const resultCtx = result.getContext('2d') as CanvasRenderingContext2D; | ||
return resultCtx.getImageData(0, 0, result.width, result.height); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,75 @@ | ||
import { updateButtons } from './buttons.ts'; | ||
import { downloadImage } from '../saveImage.ts'; | ||
import { updateSelectionOrder } from './selectionOrder.ts'; | ||
|
||
const gallery = document.getElementById("gallery") as HTMLDivElement; | ||
|
||
export const addImageDataToGallery = (imageData: ImageData, timestamp: number): boolean => { | ||
if (imageData.height * imageData.width > 1) { | ||
const canvas = document.createElement('canvas') as HTMLCanvasElement; | ||
canvas.width = imageData.width; | ||
canvas.height = imageData.height; | ||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | ||
ctx.putImageData(imageData, 0, 0) | ||
const createGalleryItem = (imgSrc: string, timestamp: number, isFinal: boolean) => { | ||
const imageContainer = document.createElement('div'); | ||
imageContainer.classList.add('gallery-image'); | ||
imageContainer.dataset.timestamp = timestamp.toString(10); | ||
|
||
const imageContainer = document.createElement("label"); | ||
imageContainer.classList.add("gallery-image"); | ||
if (isFinal) { | ||
imageContainer.classList.add('final'); | ||
} | ||
|
||
const img = new Image(); | ||
img.src = canvas.toDataURL(); | ||
imageContainer.appendChild(img); | ||
const img = new Image(); | ||
img.src = imgSrc; | ||
|
||
imageContainer.dataset.timestamp = timestamp.toString(10); | ||
const label = document.createElement('label'); | ||
label.appendChild(img); | ||
|
||
const input = document.createElement("input"); | ||
input.setAttribute("type", "checkbox"); | ||
imageContainer.appendChild(label); | ||
|
||
input.addEventListener("change", function() { | ||
if (input.checked) { | ||
imageContainer.classList.add('marked-for-action'); | ||
} else { | ||
imageContainer.classList.remove('marked-for-action'); | ||
} | ||
const input = document.createElement('input'); | ||
input.setAttribute("type", "checkbox"); | ||
|
||
updateButtons(); | ||
}); | ||
input.addEventListener("change", function() { | ||
if (input.checked) { | ||
imageContainer.classList.add('marked-for-action'); | ||
} else { | ||
imageContainer.classList.remove('marked-for-action'); | ||
} | ||
|
||
imageContainer.appendChild(input); | ||
updateButtons(); | ||
updateSelectionOrder(imageContainer); | ||
}); | ||
|
||
const btn = document.createElement("button"); | ||
btn.innerHTML = "<span>Save</span>"; | ||
btn.addEventListener("click", function () { | ||
downloadImage(img); | ||
}); | ||
imageContainer.appendChild(btn); | ||
label.appendChild(input); | ||
|
||
gallery.appendChild(imageContainer); | ||
updateButtons(); | ||
const btn = document.createElement("button"); | ||
btn.innerHTML = "<span>Save</span>"; | ||
btn.addEventListener("click", function () { | ||
downloadImage(img, isFinal); | ||
}); | ||
imageContainer.appendChild(btn); | ||
|
||
return true; | ||
} | ||
gallery.appendChild(imageContainer); | ||
updateButtons(); | ||
} | ||
|
||
export const addImageDataToGallery = async (imageData: ImageData, timestamp: number): Promise<void> => ( | ||
new Promise((resolve) => { | ||
|
||
if (imageData.height * imageData.width > 1) { | ||
const canvas = document.createElement('canvas') as HTMLCanvasElement; | ||
canvas.width = imageData.width; | ||
canvas.height = imageData.height; | ||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | ||
ctx.putImageData(imageData, 0, 0) | ||
canvas.toBlob((blob) => { | ||
if (blob) { | ||
const url = URL.createObjectURL(blob); | ||
createGalleryItem(url, timestamp, false); | ||
} | ||
|
||
resolve(); | ||
}); | ||
} | ||
}) | ||
); | ||
|
||
return false; | ||
export const addBlobToGallery = async (blob: Blob, timestamp: number): Promise<void> => { | ||
const url = URL.createObjectURL(blob); | ||
createGalleryItem(url, timestamp, true); | ||
} |
Oops, something went wrong.