Improve indicator and performance (#47)
This commit is contained in:
parent
d8a90544d7
commit
586864e4fc
Binary file not shown.
|
@ -113,8 +113,6 @@ html, body {
|
|||
|
||||
.plot {
|
||||
&:active {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
border: 1px solid @border-color-split;
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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];
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
|
||||
import { Parser } from 'mlg-converter';
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
|
Loading…
Reference in New Issue