Skip to content

Commit

Permalink
refactor the ui components
Browse files Browse the repository at this point in the history
  • Loading branch information
went2 committed Jul 7, 2023
1 parent 0618684 commit 135d46b
Show file tree
Hide file tree
Showing 28 changed files with 371 additions and 100 deletions.
4 changes: 3 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
<title>Pixel Editor</title>
</head>
<body>
<div id="app"></div>
<div id="app">
<h1>Pixel Editor</h1>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"@types/node": "^20.4.0",
"typescript": "^5.0.2",
"vite": "^4.4.0"
}
Expand Down
39 changes: 20 additions & 19 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import {
ToolSelect,
ColorSelect,
SaveButton,
LoadButton,
UndoButton,
PixelEditor,
} from "./components";
import { SaveButton, LoadButton, UndoButton, PixelEditor } from "./components";

import SetSizeButtons from "./components/uiControls/top";
import ToolBar from "./components/uiControls/left";
import RightControls from "./components/uiControls/right";
import BottomControls from "./components/uiControls/bottom";

import { draw, fill, rectangle, pick } from "./utils/drawHelpers";

import { EditorState } from "./types";
import Picture from "./models/Picture";
import { EditorState, ActionType } from "./types";
import Picture from "./models/picture";
import { historyUpdateState } from "./models/reducers";

const initialState: EditorState = {
currentTool: "draw",
currentSize: 64,
size: 128,
tool: "draw",
color: "#000000",
picture: Picture.empty(60, 30, "#f0f0f0"),
picture: Picture.empty(128, 128, "#f0f0f000"),
done: [],
doneAt: 0,
};
Expand All @@ -29,13 +30,12 @@ const baseTools = {
};

// UI components
const baseControls = [
ToolSelect,
ColorSelect,
SaveButton,
LoadButton,
UndoButton,
];
const baseControls = {
LeftControls: ToolBar,
TopControls: SetSizeButtons,
RightControls,
BottomControls,
};

function startPixelEditor({
state = initialState,
Expand All @@ -45,7 +45,8 @@ function startPixelEditor({
const app = new PixelEditor(state, {
tools,
controls,
dispatch(action: { undo: boolean; picture: Picture }) {
dispatch(action: ActionType) {
// TODO: add 'clear' action reducer
state = historyUpdateState(state, action);
app.syncState(state);
},
Expand Down
104 changes: 84 additions & 20 deletions src/components/PictureCanvas.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,106 @@
import elt from "../utils/createElement";
import { Position } from "../types";
import Picture from "../models/Picture";
import Picture from "../models/picture";
import { drawPicture } from "../utils/drawHelpers";

// A component holds canvas that only knows current picture
// It adds mouse and touch events handlers when constructs
// A canvas component that only knows current picture
class PictureCanvas {
public dom: HTMLCanvasElement;
public dom: HTMLElement;
public canvas: HTMLCanvasElement;
private background: HTMLCanvasElement;
public picture: Picture;
private scale: number = 10;
private WIDTH = 384;
private HEIGHT = 384;
private _size = 64;

constructor(
picture: Picture,
pointerDown: (...args: any[]) => void,
scale?: number
size?: number
) {
this.dom = elt("canvas", {
if (size) this._size = size;
this.picture = picture;

this.background = elt("canvas", {
width: this.WIDTH,
height: this.HEIGHT,
className: "canvas",
}) as HTMLCanvasElement;

this.drawGrid(0, 0, this.WIDTH, this.HEIGHT, this.WIDTH / this._size);
this.canvas = elt("canvas", {
onmousedown: (event: MouseEvent) => this.mouse(event, pointerDown),
ontouchstart: (event: TouchEvent) => this.touch(event, pointerDown),
width: this.WIDTH,
height: this.HEIGHT,
className: "canvas",
}) as HTMLCanvasElement;
if (scale) this.scale = scale;
this.picture = picture;

this.dom = elt(
"div",
{
id: "canvas-container",
},
this.background,
this.canvas
);

this.syncState(picture);
}

public syncState(picture: Picture) {
if (this.picture == picture) return;

this.picture = picture;
drawPicture(this.picture, this.dom, this.scale);
drawPicture(this.picture, this.canvas, this.scale);
}

public mouse(downEvent: MouseEvent, onDown: (...args: any[]) => any) {
if (downEvent.button != 0) return;
let pos = this.pointerPosition(downEvent, this.dom);
let pos = this.pointerPosition(downEvent, this.canvas);
let onMove = onDown(pos);
if (!onMove) return;
let move = (moveEvent: MouseEvent) => {
if (moveEvent.buttons == 0) {
this.dom.removeEventListener("mousemove", move);
this.canvas.removeEventListener("mousemove", move);
} else {
let newPos = this.pointerPosition(moveEvent, this.dom);
let newPos = this.pointerPosition(moveEvent, this.canvas);
if (newPos.x == pos.x && newPos.y == pos.y) return;
pos = newPos;
onMove(newPos);
}
};
this.dom.addEventListener("mousemove", move);
this.canvas.addEventListener("mousemove", move);
}

public touch(startEvent: TouchEvent, onDown: (...args: any[]) => any) {
let pos = this.pointerPosition(startEvent.changedTouches[0], this.dom);
let pos = this.pointerPosition(startEvent.changedTouches[0], this.canvas);
let onMove = onDown(pos);
startEvent.preventDefault();
if (!onMove) return;
let move = (moveEvent: TouchEvent) => {
let newPos = this.pointerPosition(moveEvent.changedTouches[0], this.dom);
let newPos = this.pointerPosition(
moveEvent.changedTouches[0],
this.canvas
);
if (newPos.x == pos.x && newPos.y == pos.y) return;
pos = newPos;
onMove(newPos);
};
let end = () => {
this.dom.removeEventListener("touchmove", move);
this.dom.removeEventListener("touchend", end);
this.canvas.removeEventListener("touchmove", move);
this.canvas.removeEventListener("touchend", end);
};
this.dom.addEventListener("touchmove", move);
this.dom.addEventListener("touchend", end);
this.canvas.addEventListener("touchmove", move);
this.canvas.addEventListener("touchend", end);
}

public get size() {
return this._size;
}

public get scale() {
return this.WIDTH / this._size;
}

private pointerPosition(
Expand All @@ -77,6 +113,34 @@ class PictureCanvas {
y: Math.floor((downEvent.clientY - rect.top) / this.scale),
};
}

private drawGrid(
startX: number,
startY: number,
endX: number,
endY: number,
gridCellSize: number
) {
const ctx = <CanvasRenderingContext2D>this.background.getContext("2d");
ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT);
ctx.beginPath();
ctx.lineWidth = 0.5;
for (let x = startX; x <= endX; x += gridCellSize) {
ctx.moveTo(x, startY);
ctx.lineTo(x, endY);
}

for (let y = startY; y <= endY; y += gridCellSize) {
ctx.moveTo(startX, y);
ctx.lineTo(endX, y);
}

ctx.strokeStyle = "#00000035";
ctx.stroke();
ctx.closePath();
}
}

export default PictureCanvas;
31 changes: 20 additions & 11 deletions src/components/PixelEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,38 @@ import elt from "../utils/createElement";
import PictureCanvas from "./PictureCanvas";
import { UIComponent, EditorState, EditorConfig, Position } from "../types";

// whole app holds the state and all UI components
class PixelEditor implements UIComponent {
public state: EditorState;
public canvas: PictureCanvas;
public controls: any[];
public controls: UIComponent[];
public dom: HTMLElement;

constructor(state: EditorState, config: EditorConfig) {
const { tools, controls, dispatch } = config;
this.state = state;

this.canvas = new PictureCanvas(state.picture, (pos: Position) => {
const tool = tools[this.state.tool];
const onMove = tool(pos, this.state, dispatch);
if (onMove) return (pos: Position) => onMove(pos, this.state);
});
this.canvas = new PictureCanvas(
state.picture,
(pos: Position) => {
const tool = tools[this.state.tool];
const onMove = tool(pos, this.state, dispatch);
if (onMove) return (pos: Position) => onMove(pos, this.state);
},
state.size
);

this.controls = Object.keys(controls).map(
(key) => new controls[key](state, config)
);

this.controls = controls.map((Control) => new Control(state, config));
this.dom = elt(
"div",
{},
this.canvas.dom,
elt("br"),
...this.controls.reduce((a, c) => a.concat(" ", c.dom), [])
{
className: "app-container",
},
...this.controls.map((controlUI) => controlUI.dom),
this.canvas.dom
);
}

Expand Down
10 changes: 5 additions & 5 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ColorSelect from "./ColorSelect";
import SaveButton from "./SaveButton";
import ToolSelect from "./ToolSelect";
import LoadButton from "./LoadButton";
import UndoButton from "./UndoButton";
import ColorSelect from "./uiControls/left/ColorSelect";
import SaveButton from "./uiControls/bottom/SaveButton";
import ToolSelect from "./uiControls/left/ToolSelect";
import LoadButton from "./uiControls/bottom/LoadButton";
import UndoButton from "./uiControls/right/UndoButton";
import PictureCanvas from "./PictureCanvas";
import PixelEditor from "./PixelEditor";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import elt from "../utils/createElement";
import { EditorConfig, UIComponent } from "../types";
import { startLoad } from "../utils/fileLoader";
import elt from "../../../utils/createElement";
import { EditorConfig, UIComponent } from "../../../types";
import { startLoad } from "../../../utils/fileLoader";

class LoadButton implements UIComponent {
public dom: HTMLElement;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import elt from "../utils/createElement";
import { EditorState, UIComponent } from "../types";
import Picture from "../models/Picture";
import { drawPicture } from "../utils/drawHelpers";
import elt from "@/utils/createElement";
import { EditorState, UIComponent } from "@/types";
import Picture from "@/models/picture";
import { drawPicture } from "@/utils/drawHelpers";

class SaveButton implements UIComponent {
public picture: Picture;
Expand Down
20 changes: 20 additions & 0 deletions src/components/uiControls/bottom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { EditorState, UIComponent, EditorConfig } from "@/types";
import elt from "@/utils/createElement";
import SaveButton from "./SaveButton";

class BottomControls implements UIComponent {
public dom: HTMLElement;

constructor(state: EditorState, config: EditorConfig) {
this.dom = elt(
"div",
{
className: "bottom-controls",
},
new SaveButton(state).dom
);
}
public syncState(): void {}
}

export default BottomControls;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import elt from "../utils/createElement";
import { UIComponent, EditorState } from "../types";
import elt from "../../../utils/createElement";
import { UIComponent, EditorState } from "../../../types";

class ColorSelect implements UIComponent {
public input: HTMLInputElement;
Expand All @@ -13,6 +13,7 @@ class ColorSelect implements UIComponent {
value: state.color,
onchange: () => dispatch({ color: this.input.value }),
}) as HTMLInputElement;

this.dom = elt("label", null, "🎨 Color: ", this.input);
}
public syncState(state: EditorState) {
Expand Down
Loading

0 comments on commit 135d46b

Please sign in to comment.