UNPKG

rpi-led-matrix-painter

Version:

Add drawing facilities to rpi-led-matrix. Manage sections of the board independently, draw shapes, text, and images.

451 lines (409 loc) 23.9 kB
import * as matrix from "wesleytabaka-rpi-led-matrix"; import { Canvas } from "./canvas"; import { CanvasSection } from "./canvassection"; import { DrawMode } from "./drawmode"; import { DrawModeOption } from "./drawmodeoption"; import { PaintingInstruction } from "./paintinginstruction"; import { Point } from "./point"; import { EffectType } from "./effecttype"; import { Effect } from "."; import { Image } from "./image"; import Jimp from "jimp"; import { PaintingInstructionCache } from "./paintinginstructioncache"; export class Painter { private canvas: Canvas; private matrix: matrix.LedMatrixInstance; private fontCache: matrix.FontInstance[]; private imageCache: Image[]; private paintingInstructionCache: PaintingInstructionCache; // Handles effect updates private startTime: Date; private currentTime: Date; private duration: number; // milliseconds constructor(matrixOptions: matrix.MatrixOptions, runtimeOptions: matrix.RuntimeOptions){ this.canvas = new Canvas(matrixOptions, runtimeOptions); // May come in handy for display size, etc. this.matrix = new matrix.LedMatrix(matrixOptions, runtimeOptions); this.fontCache = [] as matrix.FontInstance[]; this.imageCache = [] as Image[]; this.paintingInstructionCache = {} as PaintingInstructionCache; this.startTime = new Date(); this.currentTime = new Date(); this.duration = this.currentTime.getTime() - this.startTime.getTime(); } private tick(): void { this.currentTime = new Date(); this.duration = this.currentTime.getTime() - this.startTime.getTime(); } public clear(): void { this.matrix.clear(); } public resetClock(): void { // Use to start effects from "zero". this.startTime = new Date(); this.tick(); } public getCanvas(): Canvas{ return this.canvas; }; public getFontInstance(name: string, path: string): matrix.FontInstance { let cachedFont = this.fontCache.find((font: matrix.FontInstance) => { return font.name() == name && font.path() == path; }); if(cachedFont == undefined){ // Push font cachedFont = new matrix.Font(name, path); this.fontCache.push(cachedFont); } return cachedFont; } public getImageInstance(imagePath: string): Promise<Image> { return new Promise<Image>((resolve, reject) => { let cachedImage = this.imageCache.find((image: Image) => { return image.path == imagePath; }); if(cachedImage != undefined){ resolve(cachedImage); } if(cachedImage == undefined){ let content: number[][]; Jimp.read(imagePath) .then((res: Jimp) => { let width = res.getWidth(); let height = res.getHeight(); content = Array(width); for(let x = 1; x <= width; x++){ content[x - 1] = Array(height); for(let y = 1; y <= width; y++){ content[x - 1][y - 1] = (res.getPixelColor(x, y) >>> 0); // Convert to unsigned 32-bit int. } } cachedImage = { path: imagePath, width: width, height: height, content: content } as Image; this.imageCache.push(cachedImage); resolve(cachedImage); }, (rej: any) => { reject(rej); console.log("Error reading image."); throw Error("Error reading image."); }); } }); } protected fillBlankCanvasSections(): void { // TODO will probably have to come back here to fix multiple boards let width = this.matrix.width(); let height = this.matrix.height(); let map: boolean[][] = [[]]; for(let y = 0; y < height; y++){ map[y] = Array(width); map[y].fill(false, 0, width); } let sections: CanvasSection[] = this.getCanvas().getCanvasSections(); // TODO There's probably a better implementation for this, but for now... // Loop through sections and mark all pixels that are touched. this.matrix.fgColor(0x000000); sections.forEach(section => { for(let y = section.y; y < section.y + section.height; y++){ map[y].fill(true, section.x, section.x + section.width); } }); for(let y = 0; y < height; y++){ for(let x = 0; x < width; x++){ if(!map[y][x]){ this.matrix.setPixel(x, y); } } } } private getPaintingInstructionSize(paintingInstruction: PaintingInstruction): {width: number, height: number} { switch (paintingInstruction.drawMode){ case DrawMode.POLYGON: { let bounds = (paintingInstruction.points as Point[]) .map((value) => {return {minx: value.x, maxx: value.x, miny: value.y, maxy: value.y};}) .reduce((prev, curr) => { return {minx: Math.min(prev.minx, curr.minx), maxx: Math.max(prev.maxx, curr.maxx), miny: Math.min(prev.miny, curr.miny), maxy: Math.max(prev.maxy, curr.maxy)}; }); return {width: (bounds.maxx - bounds.minx + 1), height: (bounds.maxy - bounds.miny + 1)}; } case DrawMode.PIXEL: { let bounds = (paintingInstruction.points as Point[]) .map((value) => {return {minx: value.x, maxx: value.x, miny: value.y, maxy: value.y};}) .reduce((prev, curr) => { return {minx: Math.min(prev.minx, curr.minx), maxx: Math.max(prev.maxx, curr.maxx), miny: Math.min(prev.miny, curr.miny), maxy: Math.max(prev.maxy, curr.maxy)}; }); return {width: (bounds.maxx - bounds.minx + 1), height: (bounds.maxy - bounds.miny + 1)}; } case DrawMode.TEXT: { let text = (paintingInstruction.text as string); let font = this.getFontInstance((((paintingInstruction as PaintingInstruction).drawModeOptions as DrawModeOption).font as string), (((paintingInstruction as PaintingInstruction).drawModeOptions as DrawModeOption).fontPath as string)); let textwidth = font.stringWidth(text); let textheight = font.height(); return {width: textwidth, height: textheight}; } case DrawMode.IMAGE: { this.getImageInstance(paintingInstruction.imagePath as string) .then((res) => { return {width: res.width, height: res.height}; }, (rej) => { return {width: 0, height: 0}; }); } default: { return {width: paintingInstruction?.width as number, height: paintingInstruction?.height as number}; } } } private applyEffects(paintingInstruction: PaintingInstruction, canvasSection: CanvasSection): PaintingInstruction | null { // Updates a given PaintingInstruction to transpose over time. let newPaintingInstruction = paintingInstruction; let dimensions: {width: number, height: number} = this.getPaintingInstructionSize(newPaintingInstruction); let delta_x: number = 0; let delta_y: number = 0; let color: number = paintingInstruction.color; let draw: boolean = true; newPaintingInstruction.drawModeOptions?.effects?.forEach((effect) => { switch(effect.effectType){ case EffectType.SCROLLLEFT: { if((this.paintingInstructionCache[paintingInstruction.id].points as Point).x + dimensions.width < canvasSection.x){ delta_x = canvasSection.width; // Wrap text to right edge. } else { delta_x = canvasSection.width - ((this.duration / effect.effectOptions.rate) % (dimensions.width + canvasSection.width)); // rate expressed as ms/pixel. } break; } case EffectType.SCROLLRIGHT: { if((this.paintingInstructionCache[paintingInstruction.id].points as Point).x > (canvasSection.x + canvasSection.width)){ delta_x = (dimensions.width); // Wrap text to left edge. } else { delta_x = ((this.duration / effect.effectOptions.rate) % (dimensions.width + canvasSection.width)) - dimensions.width; // rate expressed as ms/pixel. } break; } case EffectType.SCROLLUP: { if((this.paintingInstructionCache[paintingInstruction.id].points as Point).y < (canvasSection.y - canvasSection.height)){ delta_y = (canvasSection.height); // Wrap text to left edge. } else { delta_y = canvasSection.height + ((this.duration / effect.effectOptions.rate) % (dimensions.height + canvasSection.height)); // rate expressed as ms/pixel. } break; } case EffectType.SCROLLDOWN: { if((this.paintingInstructionCache[paintingInstruction.id].points as Point).y > (canvasSection.y + canvasSection.height)){ delta_y = 0 - (dimensions.height); // Wrap text to left edge. } else { delta_y = ((this.duration / effect.effectOptions.rate) % (dimensions.height + canvasSection.height)) - dimensions.height; // rate expressed as ms/pixel. } break; } case EffectType.BLINK: { if(Math.floor(this.duration / effect.effectOptions.rate) % 2 == 1){ draw = false; } break; } case EffectType.PULSE: { let r = (Math.floor(((color & 0xFF0000) >>> 0) * ((effect.effectOptions.rate - (this.duration % effect.effectOptions.rate)) / effect.effectOptions.rate)) >>> 16); let g = (Math.floor(((color & 0x00FF00) >>> 0) * ((effect.effectOptions.rate - (this.duration % effect.effectOptions.rate)) / effect.effectOptions.rate)) >>> 8); let b = Math.floor(((color & 0x0000FF) >>> 0) * ((effect.effectOptions.rate - (this.duration % effect.effectOptions.rate)) / effect.effectOptions.rate)); newPaintingInstruction.color = (r << 16) + (g << 8) + b; break; } } }); // Apply delta to all Points if(paintingInstruction.drawMode == DrawMode.POLYGON || paintingInstruction.drawMode == DrawMode.PIXEL){ (newPaintingInstruction.points as Point[]).forEach((point) => { point.x += delta_x; point.y += delta_y; }); } else { (newPaintingInstruction.points as Point).x += delta_x; (newPaintingInstruction.points as Point).y += delta_y; } return draw ? newPaintingInstruction : null; } public paint(): void { // How can I crop the CanvasSection if there's overflow? // How about before we draw each CanvasSection we do a fill with black on the section? It would work for the next section being drawn... // In other words, if there are empty portions of the canvas, we should fill them in with black as well. Some clever maths will help. this.tick(); this.matrix.clear(); let instructionPromises: Promise<PaintingInstruction | null>[] = [] as Promise<PaintingInstruction | null>[]; this.getCanvas().getCanvasSections().sort((a, b) => {return a.z - b.z;}).forEach((canvasSection: CanvasSection) => { // Blank out the CanvasSection. this.matrix.fgColor(0x000000); this.matrix.fill(canvasSection.x, canvasSection.y, canvasSection.x + canvasSection.width - 1, canvasSection.y + canvasSection.height - 1); // this.matrix.sync(); // TODO Use promise.all() so we know everything's been drawn to the screen. canvasSection.get().representation.sort((a, b) => {return a.layer - b.layer}).forEach(paintingInstruction => { let dereferencedPaintingInstruction: PaintingInstruction = JSON.parse(JSON.stringify(paintingInstruction)); instructionPromises.push(new Promise((resolve, reject) => { if(this.paintingInstructionCache[dereferencedPaintingInstruction.id] == undefined){ this.paintingInstructionCache[dereferencedPaintingInstruction.id] = dereferencedPaintingInstruction; } // Do stuff here. switch (dereferencedPaintingInstruction.drawMode){ case DrawMode.LINE: { let newPaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection); let x0 = (newPaintingInstruction?.points as Point[])[0].x + canvasSection.x; let y0 = (newPaintingInstruction?.points as Point[])[0].y + canvasSection.y; let x1 = (newPaintingInstruction?.points as Point[])[1].x + canvasSection.x; let y1 = (newPaintingInstruction?.points as Point[])[1].y + canvasSection.y; let color = newPaintingInstruction?.color; this.matrix.fgColor(color!); this.matrix.drawLine(x0, y0, x1, y1); resolve(newPaintingInstruction); break; } case DrawMode.RECTANGLE: { let newPaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection); let x = (newPaintingInstruction?.points as Point).x + canvasSection.x; let y = (newPaintingInstruction?.points as Point).y + canvasSection.y; let width = newPaintingInstruction?.width as number; let height = newPaintingInstruction?.height as number; let color = newPaintingInstruction?.color; let fill = newPaintingInstruction?.drawModeOptions?.fill || false; this.matrix.fgColor(color!); if(fill){ this.matrix.drawFilledRect(x, y, width, height); } else { this.matrix.drawRect(x, y, width, height); } resolve(newPaintingInstruction); break; } case DrawMode.CIRCLE: { let newPaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection); let x = (newPaintingInstruction?.points as Point).x + canvasSection.x; let y = (newPaintingInstruction?.points as Point).y + canvasSection.y; let r = (newPaintingInstruction?.width as number) / 2; let color = newPaintingInstruction?.color; let fill = newPaintingInstruction?.drawModeOptions?.fill || false; this.matrix.fgColor(color!); if(fill){ this.matrix.drawFilledCircle(x, y, r); } else { this.matrix.drawCircle(x, y, r); } resolve(newPaintingInstruction); break; } case DrawMode.ELLIPSE: { console.error("Not implemented."); resolve(dereferencedPaintingInstruction); break; } case DrawMode.POLYGON: { let newPaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection); let color = newPaintingInstruction?.color; let fill = newPaintingInstruction?.drawModeOptions?.fill || false; let coordinateArray: number[] = []; (newPaintingInstruction?.points as Point[]).forEach((point: Point) => { coordinateArray.push(point.x + canvasSection.x); coordinateArray.push(point.y + canvasSection.y); }); this.matrix.fgColor(color!); if(fill){ this.matrix.drawFilledPolygon(coordinateArray); } else { this.matrix.drawPolygon(coordinateArray); } resolve(newPaintingInstruction); break; } case DrawMode.PIXEL: { let newPaintingInstruction: PaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection) as PaintingInstruction; this.matrix.fgColor(newPaintingInstruction.color); if(newPaintingInstruction != null){ (newPaintingInstruction.points as Point[]).forEach((point: Point) => { this.matrix.setPixel(point.x + canvasSection.x, point.y + canvasSection.y); }); } resolve(newPaintingInstruction); break; } case DrawMode.TEXT: { let text = (dereferencedPaintingInstruction.text as string); let x = (dereferencedPaintingInstruction.points as Point).x + canvasSection.x; let y = (dereferencedPaintingInstruction.points as Point).y + canvasSection.y; let color = (dereferencedPaintingInstruction.color); let font = this.getFontInstance((((dereferencedPaintingInstruction as PaintingInstruction).drawModeOptions as DrawModeOption).font as string), (((dereferencedPaintingInstruction as PaintingInstruction).drawModeOptions as DrawModeOption).fontPath as string)); let textwidth = font.stringWidth(text); let textheight = font.height(); let draw: boolean = true; let newPaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection); if(newPaintingInstruction != null){ color = newPaintingInstruction.color; x = (newPaintingInstruction?.points as Point).x + canvasSection.x; y = (newPaintingInstruction?.points as Point).y + canvasSection.y; } else { draw = false; } if(draw){ this.matrix.font(font); this.matrix.fgColor(color!); let running_x = x; for(let c = 0; c < text.length; c++){ this.matrix.drawText(text.charAt(c), running_x, y); running_x += font.stringWidth(text.charAt(c)); } } resolve(newPaintingInstruction); break; } case DrawMode.IMAGE: { // Loop through image points and use SetPixel. let newPaintingInstruction: PaintingInstruction = this.applyEffects(dereferencedPaintingInstruction, canvasSection) as PaintingInstruction; this.getImageInstance(dereferencedPaintingInstruction.imagePath!) .then((res: Image) => { let imageInstance: Image = res; let x = (newPaintingInstruction.points as Point).x + canvasSection.x; let y = (newPaintingInstruction.points as Point).y + canvasSection.y; for(let img_y = 0; img_y < imageInstance.height; img_y++){ for(let img_x = 0; img_x < imageInstance.width; img_x++){ if((imageInstance.content[img_x][img_y] & 0x000000FF) != 0){ // Alpha 0 is not drawn. this.matrix.fgColor(imageInstance.content[img_x][img_y] >>> 8); this.matrix.setPixel(x + img_x, y + img_y); } } } // this.matrix.sync(); }, (rej) => { console.log(rej); }); resolve(newPaintingInstruction); break; } // case DrawMode.BUFFER: { // this.matrix.drawBuffer(paintingInstruction.buffer!, paintingInstruction.width!, paintingInstruction.height!); // TODO better definition. // } } })); }); // Debugging... // this.matrix.fgColor(0x00FF00); // Draw bound on CanvasSection // this.matrix.setPixel(canvasSection.x, canvasSection.y); // this.matrix.setPixel(canvasSection.x + canvasSection.width - 1, canvasSection.y); // this.matrix.setPixel(canvasSection.x + canvasSection.width - 1, canvasSection.y + canvasSection.height - 1); // this.matrix.setPixel(canvasSection.x, canvasSection.y + canvasSection.height - 1); }); Promise.all(instructionPromises) .then((res) => { this.fillBlankCanvasSections(); this.matrix.sync(); }, (rej) => { this.fillBlankCanvasSections(); this.matrix.sync(); }); } }