From fdee88e59181818df4c21a169e043db6c3f34845 Mon Sep 17 00:00:00 2001 From: Tobias Ganzhorn Date: Fri, 11 Jun 2021 08:57:16 +0200 Subject: [PATCH] Proper lcd view now --- package.json | 4 +- src/App.tsx | 21 +-- src/components/LCDView.tsx | 334 ++++++++++++++++++++++++++++--------- 3 files changed, 266 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index e9f1917..c2934f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lcd-simulator-electron", - "version": "0.1.0", + "version": "0.2.0", "private": true, "author": "Tobias Ganzhorn", "description": "Simple LCD-Simulator for the Mikrocontroller-Labor-Board.", @@ -76,7 +76,7 @@ "type": "development" }, "win": { - "target": "portable", + "target": "nsis", "icon": "./public/ms-icon-310x310.png" }, "linux": { diff --git a/src/App.tsx b/src/App.tsx index e1567c3..468f715 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,13 +12,13 @@ import { isDisplayClearCommand } from './classes/CommandParser'; import { DebugCommandView } from './components/DebugCommandView'; -import { LCDView, LCDBuffer } from './components/LCDView'; import { Navbar, Container, Jumbotron, Tabs, Tab, Button } from 'react-bootstrap'; import { DisplayCommandView } from './components/DisplayCommandView'; import "./App.css"; import "./Fonts.css"; import "./bootstrap.min.css"; import { useToasts } from 'react-toast-notifications'; +import { LCDView, LCDManager } from './components/LCDView'; function App() { const [serial, setSerial] = useState(); @@ -26,7 +26,7 @@ function App() { const [debugCommands, setDebugCommands] = useState<(DebugNumberCommand | DebugTextCommand)[]>([]); const [commands, setCommands] = useState([]); const [connected, setConnected] = useState(false); - const lcdRef = useRef<[LCDBuffer, React.Dispatch>]>(); + const lcdRef = useRef(); const { addToast } = useToasts(); @@ -72,23 +72,23 @@ function App() { commandParser.onNewCommand = (command: LCDCommand) => { if (!lcdRef.current) return; - const [buffer, setBuffer] = lcdRef.current; + const lcdManager = lcdRef.current; if (isDebugNumberCommand(command) || isDebugTextCommand(command)) { setDebugCommands(state => state.concat([command])); } else { setCommands(state => state.concat([command])); } if (isDisplayCharCommand(command)) { - setBuffer(buffer.insertText(command.text)); + lcdManager.insertText(command.text); } if (isDisplayTextCommand(command)) { - setBuffer(buffer.insertTextAt(command.text, command.row, command.column)); + lcdManager.insertTextAt(command.text, command.row, command.column); } if (isDisplaySetCursorCommand(command)) { - setBuffer(buffer.setCursor(command.row, command.column)); + lcdManager.setCursor(command.row, command.column); } if (isDisplayClearCommand(command)) { - setBuffer(buffer.clearLines()); + lcdManager.clearLines(); } }; @@ -127,10 +127,11 @@ function App() { const clearAll = () => { if (!lcdRef.current) return; - const [buffer, setBuffer] = lcdRef.current; + const lcdManager = lcdRef.current; setCommands([]); setDebugCommands([]); - setBuffer(new LCDBuffer(buffer.rows, buffer.columns)); + lcdManager.commandsReceived = 0; + lcdManager.clearLines(); } return ( @@ -169,7 +170,7 @@ function App() { <>

No device connected!

- Please connect to a the microcontroller by pressing the "Open COM Port" button. + Please connect to a microcontroller by pressing the "Open COM Port" button.

diff --git a/src/components/LCDView.tsx b/src/components/LCDView.tsx index 07df808..fbce50f 100644 --- a/src/components/LCDView.tsx +++ b/src/components/LCDView.tsx @@ -1,109 +1,165 @@ -import { forwardRef, ForwardRefExoticComponent, useRef, useState, useImperativeHandle, useEffect } from "react"; +import { forwardRef, ForwardRefExoticComponent, useEffect, useImperativeHandle, useRef, useState } from "react"; import { Button } from "react-bootstrap"; -export class LCDBuffer { - rows: number; - columns: number; - cursorRow: number; - cursorColumn: number; - commandsReceived: number; - - lines: string[]; - - constructor(rows: number, columns: number, lines?: string[], cursorRow?: number, cursorColumn?: number, commandsReceived?: number) { - this.rows = rows; - this.columns = columns; - - this.cursorColumn = cursorColumn ?? 0; - this.cursorRow = cursorRow ?? 0; - - this.commandsReceived = commandsReceived ?? 0; - - this.lines = lines ?? new Array(rows).fill(" ".repeat(columns)); +class LCDFrameBuffer extends ImageData { + constructor(width: number, height: number, data?: Uint8ClampedArray) { + super(data ?? new Uint8ClampedArray(width * height * 4), width, height); } - insertTextAt(text: string, row: number, column: number) { - if (row > this.rows - 1) return this; - const line = this.lines[row]; - const length = Math.min(text.length + column, this.columns - column); - this.lines[row] = line.substring(0, column) + text.substring(0, length) + line.substring(column + length, line.length); - this.cursorRow = row; - this.cursorColumn = Math.min(column + text.length, this.columns - 1); - - this.commandsReceived++; + setRGB(rgba: [number, number, number, number], x: number, y: number) { + if (x > this.width - 1 || y > this.height - 1 || x < 0 || y < 0) return; + const index = (y * this.width + x) * 4; + this.data[index] = rgba[0]; + this.data[index + 1] = rgba[1]; + this.data[index + 2] = rgba[2]; + this.data[index + 3] = rgba[3]; + } - return new LCDBuffer(this.rows, this.columns, this.lines, this.cursorRow, this.cursorColumn, this.commandsReceived); + getRGB(x: number, y: number): [number, number, number, number] { + const index = (y * this.width + x) * 4; + return [this.data[index], this.data[index + 1], this.data[index + 2], this.data[index + 3]]; } - insertText(text: string) { - this.insertTextAt(text, this.cursorRow, this.cursorColumn); + setBlockData(blockData: LCDFrameBuffer, dx: number, dy: number) { + for (let x = dx; x < blockData.width + dx; x++) { + for (let y = dy; y < blockData.height + dy; y++) { + this.setRGB(blockData.getRGB(x - dx, y - dy), x, y); + } + } + } +} - this.commandsReceived++; +class LCDCharacter extends LCDFrameBuffer { + constructor(data: Uint8ClampedArray) { + super(5, 7, data); + } +} - return new LCDBuffer(this.rows, this.columns, this.lines, this.cursorRow, this.cursorColumn, this.commandsReceived); +export class LCDManager { + readonly width: number; + readonly height: number; + readonly context: CanvasRenderingContext2D; + readonly lcdFrameBuffer: LCDFrameBuffer; + + cursorRow: number = 0; + cursorColumn: number = 0; + dirty: boolean = false; + raf: number = -1; + commandsReceived: number = 0; + onReceiveCommand: () => void = () => {}; + + constructor(width: number, height: number, context: CanvasRenderingContext2D) { + this.width = width; + this.height = height; + this.context = context; + + this.lcdFrameBuffer = new LCDFrameBuffer(width, height); } setCursor(row: number | null, column: number | null) { this.cursorRow = row ?? this.cursorRow; this.cursorColumn = column ?? this.cursorColumn; + this.onReceiveCommand(); + this.commandsReceived++; + } + + insertTextAt(text: string, row: number, column: number) { + const rowIndex = row * 8 + 1; + for (let i = 0; i < text.length; i++) { + const columnIndex = (column + i) * 6 + 2; + this.lcdFrameBuffer.setBlockData(generateChar(text[i]), columnIndex, rowIndex) + } + this.cursorRow = row; + this.cursorColumn = column + text.length; + + this.dirty = true; + + this.onReceiveCommand(); this.commandsReceived++; + } - return new LCDBuffer(this.rows, this.columns, this.lines, this.cursorRow, this.cursorColumn, this.commandsReceived); + insertText(text: string) { + this.insertTextAt(text, this.cursorRow, this.cursorColumn); } clearLines() { - this.lines = new Array(this.rows).fill(" ".repeat(this.columns)); + for (let i = 0; i < this.lcdFrameBuffer.data.length; i++) { + this.lcdFrameBuffer.data[i] = 0; + } - this.commandsReceived++; + this.dirty = true; - return new LCDBuffer(this.rows, this.columns, this.lines, this.cursorRow, this.cursorColumn, this.commandsReceived); + this.onReceiveCommand(); + this.commandsReceived++; } clearRow(row: number) { - this.lines[row] = " ".repeat(this.columns); + this.dirty = true; + this.onReceiveCommand(); this.commandsReceived++; + } + + startTicker() { + this.draw(); + this.raf = window.requestAnimationFrame(() => this.startTicker()); + } - return new LCDBuffer(this.rows, this.columns, this.lines, this.cursorRow, this.cursorColumn, this.commandsReceived); + stopTicker() { + window.cancelAnimationFrame(this.raf); + this.raf = -1; } - getLines() { - return this.lines; + draw() { + if (!this.dirty) return; + this.context.putImageData(this.lcdFrameBuffer, 0, 0); + this.context.stroke(); + this.dirty = false; } } export const LCDView: ForwardRefExoticComponent<{}> = forwardRef((props, ref) => { - const [size, setSize] = useState<[number, number]>([21, 8]); - const [buffer, setBuffer] = useState(new LCDBuffer(size[1], size[0])); - const [fontSize, setFontSize] = useState(16); + const canvasRef = useRef(null); + const [lcdManager, setLcdManager] = useState(); + const [size, setSize] = useState(3); const lightRef = useRef(null); - - - useImperativeHandle(ref, () => ([buffer, setBuffer])); + const lightRaf = useRef(-1); useEffect(() => { - if (!lightRef.current) return; + if (!canvasRef.current) return; - if (buffer.commandsReceived === 0) return; + const context = canvasRef.current.getContext('2d'); - lightRef.current.style.backgroundColor = "#5DFF00"; - lightRef.current.style.boxShadow = "0 0 20px #5DFF00"; + if (!context) return; - const raf = window.setTimeout(() => { + const lcdManagerRef = new LCDManager(128, 64, context); + lcdManagerRef.startTicker(); + + lcdManagerRef.onReceiveCommand = () => { if (!lightRef.current) return; - lightRef.current.style.backgroundColor = "#aaa"; - lightRef.current.style.boxShadow = "0 0 0 #aaa"; - }, 300); + + lightRef.current.style.backgroundColor = "#5DFF00"; + lightRef.current.style.boxShadow = "0 0 20px #5DFF00"; + + lightRaf.current = window.setTimeout(() => { + if (!lightRef.current) return; + lightRef.current.style.backgroundColor = "#aaa"; + lightRef.current.style.boxShadow = "0 0 0 #aaa"; + }, 300); + }; + + setLcdManager(lcdManagerRef); return () => { - window.clearTimeout(raf); + if (!lcdManager) return; + lcdManager.stopTicker(); + window.clearTimeout(lightRaf.current); + lcdManager.onReceiveCommand = () => {}; } - }, [buffer]); + }, []); - useEffect(() => { - setBuffer(new LCDBuffer(size[1], size[0])); - }, [size]); + useImperativeHandle(ref, () => lcdManager); return (
@@ -113,32 +169,148 @@ export const LCDView: ForwardRefExoticComponent<{}> = forwardRef((props, ref) =>
- CMD: {buffer.commandsReceived} + CMD: {lcdManager?.commandsReceived ?? 0}
-
- +
- -
-
- setFontSize(size => Math.min(48, size + 1))}>➕ - setFontSize(size => Math.max(8, size - 1))}>➖ +
+
+ setSize(size => Math.min(5, size + 0.1))}>➕ + setSize(size => Math.max(2, size - 0.1))}>➖
- { - buffer.getLines().map((line, index) => ( -
{line}
- )) - } + +
) -}); \ No newline at end of file +}); + +const font5x7 = new Uint8ClampedArray([ + 0x00, 0x00, 0x00, 0x00, 0x00,// (space) + 0x00, 0x00, 0x5F, 0x00, 0x00,// ! + 0x00, 0x07, 0x00, 0x07, 0x00,// " + 0x14, 0x7F, 0x14, 0x7F, 0x14,// # + 0x24, 0x2A, 0x7F, 0x2A, 0x12,// $ + 0x23, 0x13, 0x08, 0x64, 0x62,// % + 0x36, 0x49, 0x55, 0x22, 0x50,// & + 0x00, 0x05, 0x03, 0x00, 0x00,// ' + 0x00, 0x1C, 0x22, 0x41, 0x00,// ( + 0x00, 0x41, 0x22, 0x1C, 0x00,// ) + 0x08, 0x2A, 0x1C, 0x2A, 0x08,// * + 0x08, 0x08, 0x3E, 0x08, 0x08,// + + 0x00, 0x50, 0x30, 0x00, 0x00,// , + 0x08, 0x08, 0x08, 0x08, 0x08,// - + 0x00, 0x60, 0x60, 0x00, 0x00,// . + 0x20, 0x10, 0x08, 0x04, 0x02,// / + 0x3E, 0x51, 0x49, 0x45, 0x3E,// 0 + 0x00, 0x42, 0x7F, 0x40, 0x00,// 1 + 0x42, 0x61, 0x51, 0x49, 0x46,// 2 + 0x21, 0x41, 0x45, 0x4B, 0x31,// 3 + 0x18, 0x14, 0x12, 0x7F, 0x10,// 4 + 0x27, 0x45, 0x45, 0x45, 0x39,// 5 + 0x3C, 0x4A, 0x49, 0x49, 0x30,// 6 + 0x01, 0x71, 0x09, 0x05, 0x03,// 7 + 0x36, 0x49, 0x49, 0x49, 0x36,// 8 + 0x06, 0x49, 0x49, 0x29, 0x1E,// 9 + 0x00, 0x36, 0x36, 0x00, 0x00,// : + 0x00, 0x56, 0x36, 0x00, 0x00,// ; + 0x00, 0x08, 0x14, 0x22, 0x41,// < + 0x14, 0x14, 0x14, 0x14, 0x14,// = + 0x41, 0x22, 0x14, 0x08, 0x00,// > + 0x02, 0x01, 0x51, 0x09, 0x06,// ? + 0x32, 0x49, 0x79, 0x41, 0x3E,// @ + 0x7E, 0x11, 0x11, 0x11, 0x7E,// A + 0x7F, 0x49, 0x49, 0x49, 0x36,// B + 0x3E, 0x41, 0x41, 0x41, 0x22,// C + 0x7F, 0x41, 0x41, 0x22, 0x1C,// D + 0x7F, 0x49, 0x49, 0x49, 0x41,// E + 0x7F, 0x09, 0x09, 0x01, 0x01,// F + 0x3E, 0x41, 0x41, 0x51, 0x32,// G + 0x7F, 0x08, 0x08, 0x08, 0x7F,// H + 0x00, 0x41, 0x7F, 0x41, 0x00,// I + 0x20, 0x40, 0x41, 0x3F, 0x01,// J + 0x7F, 0x08, 0x14, 0x22, 0x41,// K + 0x7F, 0x40, 0x40, 0x40, 0x40,// L + 0x7F, 0x02, 0x04, 0x02, 0x7F,// M + 0x7F, 0x04, 0x08, 0x10, 0x7F,// N + 0x3E, 0x41, 0x41, 0x41, 0x3E,// O + 0x7F, 0x09, 0x09, 0x09, 0x06,// P + 0x3E, 0x41, 0x51, 0x21, 0x5E,// Q + 0x7F, 0x09, 0x19, 0x29, 0x46,// R + 0x46, 0x49, 0x49, 0x49, 0x31,// S + 0x01, 0x01, 0x7F, 0x01, 0x01,// T + 0x3F, 0x40, 0x40, 0x40, 0x3F,// U + 0x1F, 0x20, 0x40, 0x20, 0x1F,// V + 0x7F, 0x20, 0x18, 0x20, 0x7F,// W + 0x63, 0x14, 0x08, 0x14, 0x63,// X + 0x03, 0x04, 0x78, 0x04, 0x03,// Y + 0x61, 0x51, 0x49, 0x45, 0x43,// Z + 0x00, 0x00, 0x7F, 0x41, 0x41,// [ + 0x02, 0x04, 0x08, 0x10, 0x20,// "\" + 0x41, 0x41, 0x7F, 0x00, 0x00,// ] + 0x04, 0x02, 0x01, 0x02, 0x04,// ^ + 0x40, 0x40, 0x40, 0x40, 0x40,// _ + 0x00, 0x01, 0x02, 0x04, 0x00,// ` + 0x20, 0x54, 0x54, 0x54, 0x78,// a + 0x7F, 0x48, 0x44, 0x44, 0x38,// b + 0x38, 0x44, 0x44, 0x44, 0x20,// c + 0x38, 0x44, 0x44, 0x48, 0x7F,// d + 0x38, 0x54, 0x54, 0x54, 0x18,// e + 0x08, 0x7E, 0x09, 0x01, 0x02,// f + 0x08, 0x14, 0x54, 0x54, 0x3C,// g + 0x7F, 0x08, 0x04, 0x04, 0x78,// h + 0x00, 0x44, 0x7D, 0x40, 0x00,// i + 0x20, 0x40, 0x44, 0x3D, 0x00,// j + 0x00, 0x7F, 0x10, 0x28, 0x44,// k + 0x00, 0x41, 0x7F, 0x40, 0x00,// l + 0x7C, 0x04, 0x18, 0x04, 0x78,// m + 0x7C, 0x08, 0x04, 0x04, 0x78,// n + 0x38, 0x44, 0x44, 0x44, 0x38,// o + 0x7C, 0x14, 0x14, 0x14, 0x08,// p + 0x08, 0x14, 0x14, 0x18, 0x7C,// q + 0x7C, 0x08, 0x04, 0x04, 0x08,// r + 0x48, 0x54, 0x54, 0x54, 0x20,// s + 0x04, 0x3F, 0x44, 0x40, 0x20,// t + 0x3C, 0x40, 0x40, 0x20, 0x7C,// u + 0x1C, 0x20, 0x40, 0x20, 0x1C,// v + 0x3C, 0x40, 0x30, 0x40, 0x3C,// w + 0x44, 0x28, 0x10, 0x28, 0x44,// x + 0x0C, 0x50, 0x50, 0x50, 0x3C,// y + 0x44, 0x64, 0x54, 0x4C, 0x44,// z + 0x00, 0x08, 0x36, 0x41, 0x00,// { + 0x00, 0x00, 0x7F, 0x00, 0x00,// | + 0x00, 0x41, 0x36, 0x08, 0x00,// } + 0x08, 0x08, 0x2A, 0x1C, 0x08,// -> + 0x08, 0x1C, 0x2A, 0x08, 0x08 // <- +]); + +const generateChar = (charcode: number | string) => { + if (typeof charcode === "string") charcode = charcode.charCodeAt(0); + charcode = Math.max(0, charcode - 32); + let index = 0; + const tmp = new Uint8ClampedArray(5 * 7 * 4); + for (let j = 0; j < 8; j++) { + for (let i = charcode * 5; i < charcode * 5 + 5; i++) { + const val = (font5x7[i] >> j) & 0x01; + tmp[index++] = 0; + tmp[index++] = 0; + tmp[index++] = 0; + tmp[index++] = val * 170; + } + } + return new LCDCharacter(tmp); +} \ No newline at end of file