Skip to content

Commit

Permalink
feat: add a basic editor UI
Browse files Browse the repository at this point in the history
  • Loading branch information
vimcaw committed May 18, 2021
1 parent 901a05d commit 1c0d134
Show file tree
Hide file tree
Showing 22 changed files with 1,018 additions and 26 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"react/jsx-props-no-spreading": "off",
"consistent-return": "off",
"no-param-reassign": "off",
"i18next/no-literal-string": [
"warn",
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,22 @@
},
"dependencies": {
"@adobe/react-spectrum": "^3.10.0",
"@inlet/react-pixi": "^6.5.2",
"@reduxjs/toolkit": "^1.5.1",
"@spectrum-icons/illustrations": "^3.2.1",
"@spectrum-icons/workflow": "^3.2.0",
"@types/lodash": "^4.14.169",
"i18next": "^20.2.3",
"i18next-browser-languagedetector": "^6.1.1",
"lodash": "^4.17.21",
"pixi-viewport": "^4.31.0",
"pixi.js": "^6.0.4",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-hook-form": "^7.6.0",
"react-i18next": "^11.8.15",
"react-redux": "^7.2.4"
"react-redux": "^7.2.4",
"react-use": "^17.2.4"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",
Expand All @@ -48,6 +52,7 @@
"prettier": "2.3.0",
"sass": "^1.32.13",
"typescript": "^4.1.2",
"vite": "^2.3.0"
"vite": "^2.3.0",
"vite-aliases": "^0.6.3"
}
}
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useHasActiveDocument } from './store';
import { useHasActiveDocument } from '@store';
import Home from './Home';
import Editor from './Editor';

Expand Down
29 changes: 29 additions & 0 deletions src/Canvas/Background.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Graphics } from '@inlet/react-pixi';
import { useActiveDocumentSize } from '@store';

const GRID_SIZE = 20;
const GRID_COLOR = 0xc8c8c8;
const BACKGROUND_COLOR = 0xffffff;

export default function Background() {
const activeDocumentSize = useActiveDocumentSize();

if (!activeDocumentSize) return null;

return (
<Graphics
draw={g => {
g.clear();
g.beginFill(BACKGROUND_COLOR);
g.drawRect(0, 0, activeDocumentSize.width, activeDocumentSize.height);
g.beginFill(GRID_COLOR);
let flag = true;
for (let x = 0; x < activeDocumentSize.width; x += GRID_SIZE, flag = !flag) {
for (let y = flag ? 0 : GRID_SIZE; y < activeDocumentSize.height; y += GRID_SIZE * 2) {
g.drawRect(x, y, GRID_SIZE, GRID_SIZE);
}
}
}}
/>
);
}
95 changes: 95 additions & 0 deletions src/Canvas/Viewport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { PixiComponent, useApp } from '@inlet/react-pixi';
import { Viewport as PixiViewport } from 'pixi-viewport';
import { Application } from 'pixi.js';
import {
useCallback,
createContext,
useState,
RefCallback,
PropsWithChildren,
useEffect,
useContext,
} from 'react';

const ViewportContext = createContext<PixiViewport | null>(null);

export function useViewport(): PixiViewport {
return useContext(ViewportContext) as PixiViewport;
}

export interface ViewportProps {
scale?: number;
onZoomed?: (scale: number) => void;
}

const PixiViewportComponent = PixiComponent<
{
app: Application;
scale?: number;
},
PixiViewport
>('Viewport', {
create: ({ app }) => {
const viewport = new PixiViewport({
screenWidth: app.renderer.width / app.renderer.resolution,
screenHeight: app.renderer.height / app.renderer.resolution,
interaction: app.renderer.plugins.interaction,
});

// 激活插件
viewport.drag().pinch().wheel();

// 设置 viewport 默认中心和缩放
viewport.moveCenter(0, 0);
viewport.setZoom(1, true);

function update() {
if (viewport.dirty) {
app.renderer.render(viewport);
viewport.dirty = false;
}
requestAnimationFrame(() => update());
}

update();

return viewport;
},
applyProps: (viewport, _, { scale }) => {
if (scale && scale.toFixed(2) !== viewport.scaled.toFixed(2)) {
viewport.setZoom(scale, true);
}
},
});

export default function Viewport({ scale, onZoomed, children }: PropsWithChildren<ViewportProps>) {
const app = useApp();
const [viewportInstance, setViewportInstance] = useState<PixiViewport | null>(null);
const viewportRef = useCallback<RefCallback<unknown>>(currentViewportInstance => {
if (currentViewportInstance) {
setViewportInstance(currentViewportInstance as PixiViewport);
}
}, []);

useEffect(() => {
if (viewportInstance && typeof onZoomed === 'function') {
const onViewportZoomed = (e: any) => {
requestAnimationFrame(() => {
onZoomed(e.viewport.scaled);
});
};
viewportInstance.on('zoomed', onViewportZoomed);
return () => {
viewportInstance.off('zoomed', onViewportZoomed);
};
}
}, [onZoomed, viewportInstance]);

return (
<PixiViewportComponent ref={viewportRef} app={app} scale={scale}>
<ViewportContext.Provider value={viewportInstance}>
{viewportInstance && children}
</ViewportContext.Provider>
</PixiViewportComponent>
);
}
45 changes: 45 additions & 0 deletions src/Canvas/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useMeasure } from 'react-use';
import { Stage } from '@inlet/react-pixi';
import { View } from '@adobe/react-spectrum';
import { ReactReduxContext } from 'react-redux';
import Viewport from './Viewport';
import Background from './Background';

export default function Canvas() {
const [ref, canvasContainerRect] = useMeasure();

return (
<View
flexGrow={1}
overflow="hidden"
ref={instance => {
const node = instance?.UNSAFE_getDOMNode();
if (node) ref(node);
}}
>
{canvasContainerRect.width > 0 && canvasContainerRect.height > 0 && (
<ReactReduxContext.Consumer>
{value => (
<Stage
width={canvasContainerRect.width}
height={canvasContainerRect.height}
raf={false}
renderOnComponentChange
options={{
backgroundColor: 0x080808,
autoDensity: true,
resolution: window.devicePixelRatio,
}}
>
<ReactReduxContext.Provider value={value}>
<Viewport>
<Background />
</Viewport>
</ReactReduxContext.Provider>
</Stage>
)}
</ReactReduxContext.Consumer>
)}
</View>
);
}
20 changes: 20 additions & 0 deletions src/DocumentWindow/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Flex, View } from '@adobe/react-spectrum';
import { useActiveDocument } from '@store';
import Canvas from '@Canvas';

export default function DocumentWindow() {
const activeDocument = useActiveDocument()!;

return (
<Flex flexGrow={1} width={0} direction="column">
<View paddingX="size-200" paddingY="size-100">
{activeDocument.name}
</View>
<Canvas />
<View paddingX="size-100" paddingY="size-50">
{/* eslint-disable-next-line i18next/no-literal-string */}
{activeDocument.size.width}×{activeDocument.size.height}
</View>
</Flex>
);
}
12 changes: 6 additions & 6 deletions src/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Flex, View } from '@adobe/react-spectrum';
import { useActiveDocument } from './store';
import { Flex } from '@adobe/react-spectrum';
import DocumentWindow from './DocumentWindow';
import Panels from './Panels';

export default function Editor() {
const activeDocument = useActiveDocument()!;

return (
<Flex>
<View>{activeDocument.name}</View>
<Flex width="100vw" height="100vh">
<DocumentWindow />
<Panels />
</Flex>
);
}
4 changes: 2 additions & 2 deletions src/CreateDocument.tsx → src/Home/CreateDocument.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Flex, ButtonGroup, Button } from '@adobe/react-spectrum';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Form, TextField, NumberField } from './components/Form';
import { createNewDocument } from './store';
import { Form, TextField, NumberField } from '@components/Form';
import { createNewDocument } from '@store';

export default function CreateDocument({ onCancel }: { onCancel?: () => void }) {
const { t } = useTranslation();
Expand Down
File renamed without changes.
23 changes: 23 additions & 0 deletions src/Panels/LayersPanel/Layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EntityId } from '@reduxjs/toolkit';
import { Flex, View, ActionButton, Text } from '@adobe/react-spectrum';
import Visibility from '@spectrum-icons/workflow/Visibility';
import VisibilityOff from '@spectrum-icons/workflow/VisibilityOff';
import { useDispatch } from 'react-redux';
import { useLayerById, switchLayerVisible } from '@store';

export default function Layer({ id }: { id: EntityId }) {
const layer = useLayerById(id);
const dispatch = useDispatch();

if (!layer) return null;

return (
<Flex alignItems="center">
<ActionButton isQuiet onPress={() => dispatch(switchLayerVisible(id))}>
{layer.visible ? <Visibility /> : <VisibilityOff />}
</ActionButton>
<View backgroundColor="gray-200" width="size-600" height="size-600" />
<Text marginStart="size-200">{layer.name}</Text>
</Flex>
);
}
16 changes: 16 additions & 0 deletions src/Panels/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Flex, View } from '@adobe/react-spectrum';
import { useActiveLayerIds } from '@store';
import Layer from './LayersPanel/Layer';

export default function Panels() {
const activeLayerIds = useActiveLayerIds();
return (
<Flex flexShrink={0} direction="column" width="size-5000">
<View padding="size-200">
{activeLayerIds?.map(layerId => (
<Layer key={layerId} id={layerId} />
))}
</View>
</Flex>
);
}
2 changes: 1 addition & 1 deletion src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { mapKeys, mapValues } from 'lodash';
import en from './lang/en';
import en from '@lang/en';

const languages = import.meta.globEager('./lang/*.ts') as Record<string, { default: typeof en }>;

Expand Down
1 change: 1 addition & 0 deletions src/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export default {
height: 'Height',
create: 'Create',
cancel: 'Cancel',
layer: 'Layer',
};
1 change: 1 addition & 0 deletions src/lang/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export default {
height: '高度',
create: '新建',
cancel: '取消',
layer: '图层',
} as Translation;
5 changes: 2 additions & 3 deletions src/store/entries/document.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createEntityAdapter, EntityId, EntityState, nanoid } from '@reduxjs/toolkit';

import { History } from './history';
import layer, { LayerEntry } from './layer';
import { getInitialLayer, LayerEntry } from './layer';

export interface Size {
width: number;
Expand Down Expand Up @@ -33,7 +32,7 @@ export function getInitialDocument(data: Pick<DocumentState, 'name' | 'size'>):
present: {
state: {
...data,
layers: layer.getInitialState(),
layers: getInitialLayer(),
},
description: 'newFile',
},
Expand Down
2 changes: 2 additions & 0 deletions src/store/entries/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export interface History<State = any> {
present: HistoryItem<State>;
future: HistoryItem<State>[];
}

export const selectState = (rootState: History) => rootState.present.state;
19 changes: 18 additions & 1 deletion src/store/entries/layer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';
import { createEntityAdapter, EntityState, nanoid } from '@reduxjs/toolkit';
import i18next from 'i18next';

export interface Layer {
name: string;
visible: boolean;
opacity: number;
}

const layerAdapter = createEntityAdapter<Layer>();

export type LayerEntry = EntityState<Layer>;

export default layerAdapter;

export function getInitialLayer(): LayerEntry {
const id = nanoid();
return {
ids: [id],
entities: {
[id]: {
name: `${i18next.t('layer')} 1`,
visible: true,
opacity: 1,
},
},
};
}
Loading

0 comments on commit 1c0d134

Please sign in to comment.