34/**
5* Dither the image into a smaller palette
6* Very fast dithering
7* Use twoRowSierra for more accuracy
15let i = 0;
16// pixels is an array of pixels with r, g, b values
17// width is the width of the image in pixels
18while (i < (pixels.length)) {
19const newC = findClosestColor(pixels[i], palette);
5253/**
54* Dither the image into monochrome
55* Uses Floyd-Steinberg matrix
56*/
58let i = 0;
59// pixels is an array of pixels with r, g, b values
60// width is the width of the image in pixels
61while (i < (pixels.length)) {
62// We shall use "black" and "white" as our quantized palette
6import {
7createCanvas,
8loadImage,
9} from "https://deno.land/x/canvas@v1.4.2/mod.ts";
10import { Image } from "./structures/image.ts";
1112export async function getPixels(path: string) {
13const data = /https?:\/\/.+/.test(path)
14? await getImageFromWeb(path)
15: await getImageFromLocal(path);
16const image = await loadImage(data.data);
1718const canvas = createCanvas(image.width(), image.height());
1920const ctx = canvas.getContext("2d");
2122ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
23const d = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
24return new Image(d, canvas.width, canvas.height)
25}
2627async function getImageFromWeb(path: string) {
28const res = await fetch(path);
29if (res.ok) {
34mediaType,
35};
36} else throw new Error("Unable to load image.");
37}
3839async function getImageFromLocal(path: string) {
40try {
41const data = await Deno.readFile(path);
48};
49} catch (_e) {
50throw new Error("Unable to load image.");
51}
52}
monkerecolor.ts2 matches
2import { findClosestColor } from "./closest.ts";
34/** Recolor the image without dithering */
5export function recolor(
6pixels: Color[],
9let i = 0;
10// pixels is an array of pixels with r, g, b values
11// width is the width of the image in pixels
12while (i < (pixels.length)) {
13const newC = findClosestColor(pixels[i], palette);
34/**
5* Dither the image into a smaller palette
6* Uses a quick, two-row dither.
7* Creates a column pattern. Use Floyd-Steinberg
16const twoW = width << 2;
17// pixels is an array of pixels with r, g, b values
18// width is the width of the image in pixels
19while (i >= 0) {
20const newC = findClosestColor(pixels[i], palette);
7576/**
77* Dither the image into a smaller palette
78* Starts from the mid point of the image
79* I have no idea why I did this
80*/
133}
134// pixels is an array of pixels with r, g, b values
135// width is the width of the image in pixels
136while (i > 0) {
137const newC = findClosestColor(pixels[i], palette);
184185/**
186* Dither the image into monochrome
187* Uses the same matrix as above
188*/
192193// pixels is an array of pixels with r, g, b values
194// width is the width of the image in pixels
195while (i >= 0) {
196// We shall use "black" and "white" as our quantized palette
24export type BlurType = "box";
2526export interface ImageData {
27data: Uint8ClampedArray;
28width: number;
3334/**
35* Image with width, height, and pixel data
36* All methods mutate the image itself
37*/
38export class Image implements ImageData {
39pixels: Color[];
40width: number;
41height: number;
42/** Canvas ImageData always returns RGBA values */
43channels = 4;
44/** We are only gonna work with sRGB images */
45colorSpace: "srgb" = "srgb";
46constructor(pixels: Uint8ClampedArray, width: number, height?: number) {
67}
68}
69/** Blur the image. Currently only supports box blur. */
70blur(method: BlurType) {
71switch (method) {
79}
80}
81/** Recolor the image with dithering */
82dither(
83palette: Color[],
110}
111}
112/** Make the image grayscale */
113grayscale(): void {
114let i = 0;
118}
119}
120/** Invert colors in the image */
121invert(): void {
122let i = 0;
126}
127}
128/** Apply a function on every pixel in the image */
129map(fn: (c: Color) => Color): void {
130let i = 0;
134}
135}
136/** Recolor the image using just black and white */
137monochrome(
138dither = false,
145} else this.recolor([new Color("#000000"), new Color("#ffffff")]);
146}
147/** Recolor the image without dithering */
148recolor(palette: Color[]) {
149recolor(this.pixels, palette);
150}
151/** Convert to an ImageData object */
152toImageData(): ImageData {
153return {
154data: this.data,
174return data;
175}
176/** Get a Uint8ClampedArray of the grayscale image with RGBA values */
177get grayscaleData(): Uint8ClampedArray {
178const data = new Uint8ClampedArray(this.pixels.length);
1# monke
23An image-related module in TypeScript.
45Check https://deno.land/x/monke/mod.ts for documentation.
78Supports:
9- Recoloring an image with a different palette
10- Recoloring an image with dithering
11- Quantizing a palette
12- Image filters (blur, grayscale, invert, etc)
1314~~Provides a class `Color` for general color-related stuff.~~
7export * from "./dither/mod.ts";
89export * from "./structures/image.ts";
1011// For backwards compat. TODO: Remove soon
ditheringMaybeindex.html1 match
6<title>React Hono Val Town Starter</title>
7<link rel="stylesheet" href="/public/style.css">
8<link rel="icon" href="/public/favicon.svg" sizes="any" type="image/svg+xml">
9</head>
10<body>
OpenTownieImageUpload.tsx47 matches
2import React, { useRef, useState } from "https://esm.sh/react@18.2.0?dev";
34// Maximum number of images that can be uploaded
5export const PROMPT_IMAGE_LIMIT = 5;
67interface ImageUploadProps {
8images: (string | null)[];
9setImages: (images: (string | null)[]) => void;
10processFiles: (files: File[]) => void;
11}
1213export function ImageUpload({ images, setImages, processFiles }: ImageUploadProps) {
14const fileInputRef = useRef<HTMLInputElement>(null);
1521};
2223// Handle removing an image
24const removeImage = (index: number) => {
25const newImages = [...images];
26newImages.splice(index, 1);
27setImages(newImages);
28};
2930// Check if we've reached the image limit
31const isAtLimit = images.filter(Boolean).length >= PROMPT_IMAGE_LIMIT;
3233return (
34<div className="w-full">
35{/* Image previews */}
36{images.length > 0 && (
37<div className="flex flex-wrap gap-2 mb-2">
38{images.map((image, index) => (
39<div key={index} className="relative">
40{image ? (
41<div className="relative group">
42<img
43src={image}
44alt={`Uploaded ${index + 1}`}
45className="h-16 w-16 object-cover rounded border border-gray-300"
47<button
48className="absolute top-0 right-0 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
49onClick={() => removeImage(index)}
50title="Remove image"
51>
52×
68ref={fileInputRef}
69onChange={handleFileChange}
70accept="image/*"
71multiple
72className="hidden"
8081// Process files utility function - moved from the component to be reusable
82export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
83// Filter for image files only
84const imageFiles = files.filter(file => file.type.startsWith('image/'));
85
86// Limit the number of images
87const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
88
89if (filesToProcess.length === 0) return;
9091// Add null placeholders for loading state
92const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
93setImages(newImages);
9495// Process each file
96const processedImages = await Promise.all(
97filesToProcess.map(async (file) => {
98return await readFileAsDataURL(file);
100);
101102// Replace null placeholders with actual images
103const updatedImages = [...images];
104processedImages.forEach((dataUrl, index) => {
105updatedImages[images.length + index] = dataUrl;
106});
107
108setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
109};
110115reader.onload = () => {
116const result = reader.result as string;
117console.log("Image loaded, size:", result.length, "bytes");
118resolve(result);
119};
123};
124125// Component to display images in messages
126export function ImagePreview({ images }: { images: string[] }) {
127const [expandedImage, setExpandedImage] = useState<string | null>(null);
128129if (!images || images.length === 0) return null;
130131return (
132<div className="mt-2">
133<div className="flex flex-wrap gap-2">
134{images.map((image, index) => (
135<div key={index} className="relative">
136<img
137src={image}
138alt={`Image ${index + 1}`}
139className="max-h-32 max-w-32 object-contain rounded cursor-pointer"
140onClick={() => setExpandedImage(image)}
141/>
142</div>
144</div>
145146{/* Modal for expanded image */}
147{expandedImage && (
148<div
149className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
150onClick={() => setExpandedImage(null)}
151>
152<div className="max-w-[90%] max-h-[90%]">
153<img
154src={expandedImage}
155alt="Expanded view"
156className="max-w-full max-h-full object-contain"
OpenTownieChat.tsx8 matches
9import { ChatInput } from "./ChatInput.tsx";
10import { ApiKeyWarning } from "./ApiKeyWarning.tsx";
11import { processFiles } from "./ImageUpload.tsx";
1213export function Chat({
25const [soundEnabled, setSoundEnabled] = useLocalStorage<boolean>("soundEnabled", true);
26const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
27const [images, setImages] = useState<(string | null)[]>([]);
28const [isDragging, setIsDragging] = useState(false);
2959bearerToken,
60selectedFiles,
61images,
62soundEnabled,
63});
94
95if (e.dataTransfer.files && !running) {
96processFiles(Array.from(e.dataTransfer.files), images, setImages);
97}
98};
124
125if (e.dataTransfer?.files && !running) {
126processFiles(Array.from(e.dataTransfer.files), images, setImages);
127}
128};
141document.removeEventListener('drop', handleDocumentDrop);
142};
143}, [images, running, setImages]);
144145return (
184handleSubmit={handleSubmit}
185running={running}
186images={images}
187setImages={setImages}
188isDragging={isDragging}
189/>