Improve indicator and performance (#47)

This commit is contained in:
Piotr Rogowski 2021-04-07 17:31:46 +02:00 committed by GitHub
parent d8a90544d7
commit 586864e4fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 67 deletions

BIN
public/logs/longest.mlg Normal file

Binary file not shown.

View File

@ -113,8 +113,6 @@ html, body {
.plot {
&:active {
cursor: move;
cursor: grab;
}
border: 1px solid @border-color-split;
}

View File

@ -26,6 +26,7 @@ import {
isReplace,
} from '../../utils/keyboard/shortcuts';
import LandscapeNotice from './LandscapeNotice';
import { colorHsl } from '../../utils/number';
type CellsType = boolean[][];
type DataType = number[][];
@ -35,7 +36,6 @@ enum Operations {
DEC,
REPLACE,
}
type HslType = [number, number, number];
const { useBreakpoint } = Grid;
@ -175,23 +175,6 @@ const Map = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const colorHsl = (min: number, max: number, value: number): HslType => {
const saturation = 60;
const lightness = 40;
const coldDeg = 220;
const hotDeg = 0;
const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
let hue = remap(value, min, max, coldDeg, hotDeg);
// fallback to cold temp
if (Number.isNaN(hue)) {
hue = coldDeg;
}
return [hue, saturation, lightness];
};
const min = Math.min(...data.map((row) => Math.min(...row)));
const max = Math.max(...data.map((row) => Math.max(...row)));

View File

@ -25,6 +25,7 @@ import {
isIncrement,
isReplace,
} from '../../utils/keyboard/shortcuts';
import { colorHsl } from '../../utils/number';
type AxisType = 'x' | 'y';
type CellsType = boolean[][];
@ -35,7 +36,6 @@ enum Operations {
DEC,
REPLACE,
}
type HslType = [number, number, number];
const Table = ({
name,
@ -193,23 +193,6 @@ const Table = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const colorHsl = (min: number, max: number, value: number): HslType => {
const saturation = 60;
const lightness = 40;
const coldDeg = 220;
const hotDeg = 0;
const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
let hue = remap(value, min, max, coldDeg, hotDeg);
// fallback to cold temp
if (Number.isNaN(hue)) {
hue = coldDeg;
}
return [hue, saturation, lightness];
};
const renderRow = (axis: AxisType, input: number[]) => input
.map((value, index) => {
const [hue, sat, light] = colorHsl(Math.min(...input), Math.max(...input), value);

View File

@ -1,3 +1,5 @@
/* eslint-disable no-bitwise */
import {
useCallback,
useEffect,
@ -8,9 +10,19 @@ import {
TouchEvent,
Touch,
} from 'react';
import {
isDown,
isLeft,
isRight,
isUp,
} from '../../utils/keyboard/shortcuts';
import {
colorHsl,
msToTime,
} from '../../utils/number';
export interface LogEntry {
[id: string]: number
[id: string]: number | string,
}
enum Colors {
@ -22,6 +34,7 @@ enum Colors {
BLUE = '#2fe3ff',
GREY = '#334455',
WHITE = '#fff',
BG = '#222629',
}
const Canvas = ({
@ -53,14 +66,34 @@ const Canvas = ({
const plot = useCallback(() => {
const canvas = canvasRef.current!;
const fieldsToPlot = [
{ name: 'RPM', scale: 0.1 },
{ name: 'TPS', scale: 5 },
{ name: 'AFR Target', scale: 2 },
{ name: 'AFR', scale: 2 },
{ name: 'MAP', scale: 5 },
];
const ctx = canvas.getContext('2d')!;
const lastEntry = data[data.length - 1];
const maxTime = lastEntry.Time / (zoom < 1 ? 1 : zoom);
const xScale = canvas.width / maxTime;
const maxTime = (lastEntry.Time as number) / (zoom < 1 ? 1 : zoom);
const areaWidth = canvas.width;
const areaHeight = canvas.height - 30; // leave some space in the bottom
const xScale = areaWidth / maxTime;
const firstEntry = data[0];
const scaledWidth = canvas.width * zoom / 1;
const scaledWidth = areaWidth * zoom / 1;
const start = pan;
setRightBoundary(-(scaledWidth - canvas.width));
// TODO: adjust this based on FPS / preference
const resolution = Math.round(data.length / 1000 / zoom) || 1; // 1..x where 1 is max
setRightBoundary(-(scaledWidth - areaWidth));
const hsl = (fieldIndex: number) => {
const [hue] = colorHsl(0, fieldsToPlot.length - 1, fieldIndex);
return `hsl(${hue}, 80%, 50%)`;
};
// basic settings
ctx.font = '14px Arial';
ctx.lineWidth = Math.max(1.25, areaHeight / 400);
if (zoom < 1) {
setZoom(1);
@ -72,17 +105,39 @@ const Canvas = ({
return;
}
const plotEntry = (field: string, yScale: number, color: string) => {
const drawText = (left: number, top: number, text: string, color: string, textAlign = 'left') => {
ctx.textAlign = textAlign as any;
ctx.fillStyle = Colors.BG;
ctx.fillText(text, left + 2, top + 2);
ctx.fillStyle = color;
ctx.fillText(text, left, top);
};
const drawMarker = (left: number) => {
// TODO
};
const plotField = (field: string, yScale: number, color: string) => {
ctx.strokeStyle = color;
ctx.beginPath();
// initial value
ctx.moveTo(start + firstEntry.Time, canvas.height - (firstEntry[field] * yScale));
ctx.moveTo(start, areaHeight - (firstEntry[field] as number * yScale));
// TODO: slice array according to the visible part
let index = 0;
data.forEach((entry) => {
const time = entry.Time * xScale; // scale time to max width
const value = canvas.height - (entry[field] * yScale); // scale the value
index++;
if (index % resolution !== 0) {
return;
}
// draw marker on top of the record
if (entry.type === 'marker') {
return;
}
const time = (entry.Time as number) * xScale; // scale time to max width
const value = areaHeight - (entry[field] as number * yScale); // scale the value
ctx.lineTo(start + time, value);
});
@ -90,26 +145,51 @@ const Canvas = ({
ctx.stroke();
};
const plotIndicator = () => {
const drawIndicator = () => {
ctx.setLineDash([5]);
ctx.strokeStyle = Colors.WHITE;
ctx.beginPath();
// switch to time
let index = Math.round(indicatorPos * (data.length - 1) / areaWidth);
if (index < 0) {
index = 0;
}
ctx.moveTo(indicatorPos, 0);
let left = indicatorPos + 10;
let textAlign = 'left';
if (indicatorPos > areaWidth / 2) {
// flip text to the left side of the indicator
textAlign = 'right';
left = indicatorPos - 10;
}
let top = 0;
fieldsToPlot.forEach(({ name }, fieldIndex) => {
top += 20;
drawText(left, top, `${name}: ${data[index][name]}`, hsl(fieldIndex), textAlign);
});
// draw Time
drawText(
left,
areaHeight + 20,
msToTime(Math.round(data[index].Time as number * 1000)),
Colors.GREY, textAlign,
);
ctx.lineTo(indicatorPos, canvas.height);
ctx.stroke();
ctx.setLineDash([]);
};
// clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = Math.max(1.25, canvas.height / 400);
plotIndicator();
plotEntry('RPM', 0.16, Colors.RED);
plotEntry('TPS', 20, Colors.BLUE);
plotEntry('AFR Target', 4, Colors.YELLOW);
plotEntry('AFR', 4, Colors.GREEN);
plotEntry('MAP', 5, Colors.GREY);
fieldsToPlot.forEach(({ name, scale }, fieldIndex) => plotField(name, scale, hsl(fieldIndex)));
drawIndicator();
}, [data, zoom, pan, rightBoundary, indicatorPos]);
const onWheel = (e: WheelEvent) => {
@ -145,9 +225,36 @@ const Canvas = ({
setPreviousTouch(touch);
};
const keyboardListener = useCallback((e: KeyboardEvent) => {
if (isUp(e)) {
setZoom((current) => current + 0.1);
}
if (isDown(e)) {
setZoom((current) => {
if (current < 1) {
setPan(0);
return 1;
}
return current - 0.1;
});
}
if (isLeft(e)) {
setPan((current) => checkPan(current, current + 20));
}
if (isRight(e)) {
setPan((current) => checkPan(current, current - 20));
}
}, [checkPan]);
useEffect(() => {
plot();
}, [plot, width, height]);
document.addEventListener('keydown', keyboardListener);
// TODO: crate custom hook
return () => {
document.removeEventListener('keydown', keyboardListener);
};
}, [plot, width, height, keyboardListener]);
return (
<canvas

View File

@ -176,7 +176,7 @@ const TopBar = () => {
<Typography.Text keyboard>P</Typography.Text>
</>
}>
<Button icon={<SearchOutlined />} />
<Button icon={<SearchOutlined />} ref={searchInput as any} />
</Tooltip>
<Dropdown
overlay={shareMenu}

View File

@ -78,5 +78,5 @@ export const loadAll = async () => {
});
};
export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) => fetchWithProgress('./logs/long.mlg', onProgress, signal)
export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) => fetchWithProgress('./logs/longest.mlg', onProgress, signal)
.then((response) => response);

View File

@ -9,20 +9,22 @@ enum Keys {
SIDEBAR = '\\',
ESCAPE = 'Escape',
REPLACE = '=',
UP = 'ArrowUp',
DOWN = 'ArrowDown',
LEFT = 'ArrowLeft',
RIGHT = 'ArrowRight',
}
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
export const isCommand = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.COMMAND;
export const isToggleSidebar = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.SIDEBAR;
export const isIncrement = (e: KeyEvent) => e.key === Keys.INCREMENT;
export const isDecrement = (e: KeyEvent) => e.key === Keys.DECREMENT;
export const isReplace = (e: KeyEvent) => e.key === Keys.REPLACE;
export const isEscape = (e: KeyEvent) => e.key === Keys.ESCAPE;
export const isUp = (e: KeyEvent) => e.key === Keys.UP;
export const isDown = (e: KeyEvent) => e.key === Keys.DOWN;
export const isLeft = (e: KeyEvent) => e.key === Keys.LEFT;
export const isRight = (e: KeyEvent) => e.key === Keys.RIGHT;
export const useDigits = (e: KeyEvent): [boolean, number] => [digits.includes(Number(e.key)), Number(e.key)];

View File

@ -7,7 +7,7 @@ export const formatBytes = (bytes: number, decimals = 2): string => {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / k**i).toFixed(dm)) } ${ sizes[i]}`;
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
};
export const leftPad = (n: number, z = 2) => (`00${n}`).slice(-z);
@ -23,3 +23,22 @@ export const msToTime = (input: number) => {
return `${leftPad(hrs)}:${leftPad(mins)}:${leftPad(secs)}.${ms}`;
};
export const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
export type HslType = [number, number, number];
export const colorHsl = (min: number, max: number, value: number): HslType => {
const saturation = 60;
const lightness = 40;
const coldDeg = 220;
const hotDeg = 0;
let hue = remap(value, min, max, coldDeg, hotDeg);
// fallback to cold temp
if (Number.isNaN(hue)) {
hue = coldDeg;
}
return [hue, saturation, lightness];
};

View File

@ -1,4 +1,5 @@
/* eslint-disable no-bitwise */
import { Parser } from 'mlg-converter';
// eslint-disable-next-line no-restricted-globals