Skip to content

Commit

Permalink
frontend(ViewPort): experimental undo/redo system
Browse files Browse the repository at this point in the history
  • Loading branch information
JackDotJS committed Jul 30, 2024
1 parent 583c7e6 commit a3e2c9f
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 26 deletions.
15 changes: 8 additions & 7 deletions src/renderer/src/components/options/CategoryInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const kblayout = await navigator.keyboard.getLayoutMap();
// TODO: action categories
// TODO: import/export keymaps
// TODO: search function
// TODO: highlight conflicting keybinds
const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStoreFunction<VincentConfig> }): JSXElement => {
const { dictionary } = useContext(StateContext);
const t = i18n.translator(() => dictionary(), i18n.resolveTemplate) as Translator;
Expand All @@ -36,7 +37,7 @@ const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStore
// console.debug(`after`, translated);

return translated;
}
};

const beginRebind = (ev: MouseEvent, index: number): void => {
if (ev.target == null) return;
Expand All @@ -56,11 +57,11 @@ const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStore
button.classList.remove(style.rebinding);
offKeyCombo(keyComboListener);
console.debug(`rebind cancelled`);
}
};

window.addEventListener(`keydown`, (ev: KeyboardEvent) => {
if (![`Escape`, `MetaLeft`, `MetaRight`].includes(ev.code)) return;
cancelFunction()
cancelFunction();
});
window.addEventListener(`blur`, cancelFunction);

Expand Down Expand Up @@ -91,7 +92,7 @@ const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStore
const oldKeymap = structuredClone(unwrap(props.newConfig.keymap));
const filtered = oldKeymap.filter((_item, filterIndex) => index !== filterIndex)
props.setNewConfig(`keymap`, filtered);
}
};

const addNewKeybind = (): void => {
props.setNewConfig(`keymap`, (currentKeymap) => [
Expand All @@ -102,15 +103,15 @@ const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStore
keyCombo: []
}
]);
}
};

const setKeybindEnabled = (enabled: boolean, index: number): void => {
props.setNewConfig(`keymap`, index, {
enabled: enabled,
action: props.newConfig.keymap[index].action,
keyCombo: props.newConfig.keymap[index].keyCombo
});
}
};

const setKeybindActionId = (ev: Event, index: number): void => {
if (ev.target == null) return;
Expand Down Expand Up @@ -171,7 +172,7 @@ const CategoryInput = (props: { newConfig: VincentConfig, setNewConfig: SetStore
</button>
<button class={style.keybindDelete} onClick={() => removeKeybind(index())}>Delete</button>
</div>
)
);
}}
</For>
<button onClick={() => addNewKeybind()}>add new</button>
Expand Down
21 changes: 17 additions & 4 deletions src/renderer/src/components/viewport/ViewPort.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,27 @@
display: none;
}

/* .canvasWrapper {
cursor: none;
} */
.canvasWrapper {
/* cursor: none; */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
}

.canvas {
.canvas, .preCanvas, .debugCanvas {
touch-action: none;
border: 1px solid black;
box-shadow: rgba(0,0,0,0.25) 3px 3px 6px;
background-image: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%);
background-size: 20px 20px;
}

.preCanvas {
border: 4px solid orange;
}

.debugCanvas {
border: 4px solid teal;
}
188 changes: 173 additions & 15 deletions src/renderer/src/components/viewport/ViewPort.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import { createSignal, JSXElement, onMount } from 'solid-js';

import style from './ViewPort.module.css';
import { subscribeEvent } from '@renderer/state/GlobalEventEmitter';

interface HistoryItem {
data: ImageData,
x: number,
y: number
}

const ViewPort = (): JSXElement => {
const [ brushSize, setBrushSize ] = createSignal(10);
const [ cursorVisible, setCursorVisible ] = createSignal(false);
const [ drawing, setDrawing ] = createSignal(false);
const [ brushColor, setBrushColor ] = createSignal(`#000000`);
const [ eraserMode, setEraserMode ] = createSignal(false);
const [ historyStep, setHistoryStep ] = createSignal(0);

let lastPosX = 0;
let lastPosY = 0;
let lastSize = brushSize();

let cursorElem!: HTMLDivElement;
const bbox = {
top: 0,
left: 0,
right: 0,
bottom: 0
};

const history: HistoryItem[] = [];

let canvasElem!: HTMLCanvasElement;
let preCanvasElem!: HTMLCanvasElement;
let debugCanvasElem!: HTMLCanvasElement;

let cursorElem!: HTMLDivElement;
let canvasWrapperElem!: HTMLDivElement;
let widthElem!: HTMLInputElement;
let heightElem!: HTMLInputElement;
Expand All @@ -29,12 +49,20 @@ const ViewPort = (): JSXElement => {
const valAsNumber = validateNumber(value);
canvasElem.width = valAsNumber;
canvasElem.style.width = valAsNumber + `px`;
preCanvasElem.width = valAsNumber;
preCanvasElem.style.width = valAsNumber + `px`;
canvasElem.width = valAsNumber;
canvasElem.style.width = valAsNumber + `px`;
};

const setHeight = (value: string): void => {
const valAsNumber = validateNumber(value);
canvasElem.height = valAsNumber;
canvasElem.style.height = valAsNumber + `px`;
preCanvasElem.height = valAsNumber;
preCanvasElem.style.height = valAsNumber + `px`;
canvasElem.height = valAsNumber;
canvasElem.style.height = valAsNumber + `px`;
};

const updateCursor = (ev: PointerEvent): void => {
Expand All @@ -57,7 +85,20 @@ const ViewPort = (): JSXElement => {
}

if (drawing()) {
const ctx = canvasElem.getContext(`2d`);
// update bounding box data

const brushMargin = ((curSize / 2) + 1);
const maxTop = (curPosY - brushMargin);
const maxLeft = (curPosX - brushMargin);
const maxRight = (curPosX + brushMargin);
const maxBottom = (curPosY + brushMargin);

if (maxTop < bbox.top) bbox.top = maxTop;
if (maxLeft < bbox.left) bbox.left = maxLeft;
if (maxRight > bbox.right) bbox.right = maxRight;
if (maxBottom > bbox.bottom) bbox.bottom = maxBottom;

const ctx = preCanvasElem.getContext(`2d`);

if (ctx != null) {
if (eraserMode()) {
Expand Down Expand Up @@ -97,34 +138,139 @@ const ViewPort = (): JSXElement => {
lastSize = curSize;
};

const startDrawing = (ev: PointerEvent): void => {
const canvasBox = canvasElem.getBoundingClientRect();
bbox.top = ev.pageY - canvasBox.top;
bbox.left = ev.pageX - canvasBox.left;
bbox.right = ev.pageX - canvasBox.left;
bbox.bottom = ev.pageY - canvasBox.top;

setDrawing(true);
updateCursor(ev);
};

const finishDrawing = (): void => {
if (!drawing()) return;
setDrawing(false);

const ctxMain = canvasElem.getContext(`2d`);
const ctxPre = preCanvasElem.getContext(`2d`);
const ctxDebug = debugCanvasElem.getContext(`2d`);

if (ctxMain == null || ctxPre == null || ctxDebug == null) return;

// clamp bounding box to canvas bounds
bbox.top = Math.min(canvasElem.height, Math.max(bbox.top, 0));
bbox.left = Math.min(canvasElem.width, Math.max(bbox.left, 0));
bbox.right = Math.min(canvasElem.width, Math.max(bbox.right, 0));
bbox.bottom = Math.min(canvasElem.height, Math.max(bbox.bottom, 0));

const bboxWidth = Math.abs(bbox.right - bbox.left);
const bboxHeight = Math.abs(bbox.bottom - bbox.top);

const beforeData = ctxMain.getImageData(
bbox.left,
bbox.top,
bboxWidth,
bboxHeight
);

ctxMain.drawImage(preCanvasElem, 0, 0);

// const afterData = ctxMain.getImageData(
// bbox.left,
// bbox.top,
// bboxWidth,
// bboxHeight
// );

if (bboxWidth !== 0 && bboxHeight !== 0) {
ctxDebug.clearRect(0, 0, debugCanvasElem.width, debugCanvasElem.height);

addHistoryStep(beforeData, bbox.left, bbox.top);
// history.push({
// data: afterData,
// x: bbox.left,
// y: bbox.top
// });

ctxDebug.putImageData(beforeData, 0, 0);

ctxDebug.beginPath();
ctxDebug.lineWidth = 1;
ctxDebug.strokeStyle = `#FF0000`;
ctxDebug.rect(
0,
0,
bboxWidth,
bboxHeight
);
ctxDebug.stroke();
}

ctxPre.clearRect(0, 0, preCanvasElem.width, preCanvasElem.height);
};

const addHistoryStep = (data: ImageData, x: number, y: number): void => {
if (history.length > (historyStep() + 1)) {
console.debug(`overwriting history`);
history.splice(historyStep() + 1);
}

setHistoryStep((old) => old + 1);

history.push({ data, x, y });
};

onMount(() => {
widthElem.value = canvasElem.width.toString();
heightElem.value = canvasElem.height.toString();

canvasElem.addEventListener(`pointerdown`, (ev) => {
setDrawing(true);
updateCursor(ev);
subscribeEvent(`generic.undo`, null, () => {
// if ((historyStep() - 1) < 0) return;

setHistoryStep((old) => old - 1);

const newData = history[historyStep()];

const ctx = canvasElem.getContext(`2d`);

if (ctx != null) {
ctx.putImageData(newData.data, newData.x, newData.y);
}
});

subscribeEvent(`generic.redo`, null, () => {
// if ((historyStep() + 1) === history.length) return;

setHistoryStep((old) => old + 1);

const newData = history[historyStep()];

const ctx = canvasElem.getContext(`2d`);

if (ctx != null) {
ctx.putImageData(newData.data, newData.x, newData.y);
}
});

canvasElem.addEventListener(`pointerdown`, startDrawing);

window.addEventListener(`pointermove`, (ev) => {
updateCursor(ev);
});

window.addEventListener(`pointerup`, () => {
setDrawing(false);
});
window.addEventListener(`pointerup`, finishDrawing);

window.addEventListener(`pointerout`, (ev: PointerEvent) => {
if (ev.pointerType === `pen`) setDrawing(false);
if (ev.pointerType === `pen`) finishDrawing();
});

window.addEventListener(`pointerleave`, (ev: PointerEvent) => {
if (ev.pointerType === `pen`) setDrawing(false);
if (ev.pointerType === `pen`) finishDrawing();
});

window.addEventListener(`pointercancel`, () => {
setDrawing(false);
});
window.addEventListener(`pointercancel`, finishDrawing);
});

return (
Expand Down Expand Up @@ -154,21 +300,33 @@ const ViewPort = (): JSXElement => {
</div>
<div
class={style.canvasWrapper}
onPointerEnter={() => setCursorVisible(true)}
onPointerLeave={() => setCursorVisible(false)}
ref={canvasWrapperElem}
>
<div
class={style.brushCursor}
classList={{ [style.cursorVisible]: cursorVisible() }}
ref={cursorElem}
/>
<canvas
width={600}
height={400}
class={style.preCanvas}
ref={preCanvasElem}
/>
<canvas
width={600}
height={400}
class={style.canvas}
onPointerEnter={() => setCursorVisible(true)}
onPointerLeave={() => setCursorVisible(false)}
ref={canvasElem}
/>
<canvas
width={600}
height={400}
class={style.debugCanvas}
ref={debugCanvasElem}
/>
</div>
</div>
);
Expand Down

0 comments on commit a3e2c9f

Please sign in to comment.