Skip to content

Commit

Permalink
Merge pull request #24 from Pettor/feature/save-image
Browse files Browse the repository at this point in the history
feat: add save image button to drawer
  • Loading branch information
Pettor authored Feb 28, 2024
2 parents f9e6195 + 22783ea commit dcf4510
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 48 deletions.
1 change: 1 addition & 0 deletions src/components/library/editor/Editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const commonProps = {
onSwitch: () => console.log("Switched"),
},
onNewImage: () => {},
onSaveImage: () => {},
},
ErrorComponent: () => <div>Error</div>,
LoaderComponent: () => <div>Loading</div>,
Expand Down
19 changes: 8 additions & 11 deletions src/components/library/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ComponentProps } from "react";
import type { ReactElement } from "react";
import { Provider } from "jotai/react";
import { imageAtom } from "./atoms/ImageAtoms";
import { imageUrlAtom } from "./atoms/ImageUrlAtoms";
import type { ContentProps } from "./content/Content";
Expand All @@ -17,16 +16,14 @@ interface ImageEditorWithImageProps extends ContentProps, WithImageProps {
function Editor({ url, image, ...contentProps }: ImageEditorWithImageProps): ReactElement {
return (
<ErrorBoundary fallback={<h1>Internal error</h1>}>
<Provider>
<HydrateAtoms
atomValues={[
[imageUrlAtom, url],
[imageAtom, image],
]}
>
<Content {...contentProps} />
</HydrateAtoms>
</Provider>
<HydrateAtoms
atomValues={[
[imageUrlAtom, url],
[imageAtom, image],
]}
>
<Content {...contentProps} />
</HydrateAtoms>
</ErrorBoundary>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/components/library/editor/drawer/AppDrawer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const commonProps = {
},
onClose: () => {},
onNewImage: () => {},
onSaveImage: () => {},
} satisfies ComponentProps;

export const App = {
Expand Down
11 changes: 9 additions & 2 deletions src/components/library/editor/drawer/AppDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef, type ReactElement } from "react";
import { FolderPlusIcon } from "@heroicons/react/24/solid";
import { FolderPlusIcon, ArrowDownTrayIcon } from "@heroicons/react/24/solid";
import { useOnClickOutside } from "usehooks-ts";
import type { ThemeSwitchProps } from "../../theme/ThemeSwitch";
import { ThemeSwitch } from "../../theme/ThemeSwitch";
Expand All @@ -10,9 +10,10 @@ export interface AppDrawerProps {
themeSwitchProps: ThemeSwitchProps;
onClose: () => void;
onNewImage: () => void;
onSaveImage: () => void;
}

export function AppDrawer({ open, themeSwitchProps, onClose, onNewImage }: AppDrawerProps): ReactElement {
export function AppDrawer({ open, themeSwitchProps, onClose, onNewImage, onSaveImage }: AppDrawerProps): ReactElement {
const menuRef = useRef<HTMLUListElement>(null);
useOnClickOutside(menuRef, onClose);

Expand All @@ -35,6 +36,12 @@ export function AppDrawer({ open, themeSwitchProps, onClose, onNewImage }: AppDr
New Image
</a>
</li>
<li className="mt-4" onClick={onSaveImage}>
<a>
<ArrowDownTrayIcon className="h-6 w-6" />
Save Image
</a>
</li>
<div className="flex flex-1" />
<div className="flex items-center flex-row justify-end">
<ThemeSwitch {...themeSwitchProps} />
Expand Down
36 changes: 18 additions & 18 deletions src/components/library/editor/stage/StageComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ export function StageComponent(): ReactElement {
return (
<div ref={ref} className="flex-1 overflow-hidden rounded-lg border-4 border-base-100">
<Stage width={containerWidth} height={containerHeight} options={stageOptions}>
<StagedFilters
blur={{ blur }}
adjustement={{ brightness, contrast, saturation, red, green, blue }}
pixelate={{ enabled: pixelate > 0, size: pixelate }}
<StagedViewport
ref={registerViewport}
worldWidth={imageWidth}
worldHeight={imageHeight}
screenWidth={containerWidth}
screenHeight={containerHeight}
lock={lock}
zoom={zoom}
maxZoom={maxZoom}
minZoom={minZoom}
onZoomed={setZoom}
rotated={isRotated}
>
<StagedViewport
ref={registerViewport}
worldWidth={imageWidth}
worldHeight={imageHeight}
screenWidth={containerWidth}
screenHeight={containerHeight}
lock={lock}
zoom={zoom}
maxZoom={maxZoom}
minZoom={minZoom}
onZoomed={setZoom}
rotated={isRotated}
<StagedFilters
blur={{ blur }}
adjustement={{ brightness, contrast, saturation, red, green, blue }}
pixelate={{ enabled: pixelate > 0, size: pixelate }}
>
<StagedImage
imageUrl={imageUrl}
Expand All @@ -44,8 +44,8 @@ export function StageComponent(): ReactElement {
scale={scale}
rotation={rotation}
/>
</StagedViewport>
</StagedFilters>
</StagedFilters>
</StagedViewport>
</Stage>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ViewportComponent = PixiComponent<ViewportComponentProps, ViewportE
const { app, ...viewportProps } = props;
const { worldWidth, worldHeight, onZoomed, maxZoom, minZoom } = viewportProps;

const viewport = new ViewportExtended({
const viewport = new ViewportExtended(app, {
ticker: ticker,
events: app.renderer.events,
...viewportProps,
Expand Down
6 changes: 4 additions & 2 deletions src/components/library/editor/viewport/ViewportExtended.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { IViewportOptions } from "pixi-viewport";
import { Viewport } from "pixi-viewport";
import type { Application, ICanvas } from "pixi.js";

// this is a custom class that extends pixi-viewport's Viewport class
// it's used to fix an issue with releasing the DOM element when the viewport is unmounted
export class ViewportExtended extends Viewport {
public app: Application<ICanvas>;
private renderedDOMElement?: HTMLElement;

constructor(options: IViewportOptions) {
constructor(app: Application<ICanvas>, options: IViewportOptions) {
super(options);

this.app = app;
this.lockDOMElement();
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/views/editor/EditorView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const commonProps = {
mode: "light",
onSwitch: () => console.log("Switched"),
},
onNewImage: () => {},
onNewImage: () => console.log("onNewImage"),
onSaveImage: () => console.log("onSaveImage"),
},
ErrorComponent: () => <div>Error</div>,
LoaderComponent: () => <div>Loading</div>,
Expand Down
14 changes: 14 additions & 0 deletions src/libs/functions/CreateImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ViewportExtended } from "~/components/library/editor/viewport/ViewportExtended";

export async function createImage(viewport: ViewportExtended): Promise<HTMLImageElement> {
const app = viewport.app;

const generatedImage = app.renderer.generateTexture(viewport, {
region: app.screen,
resolution: 2,
});

const image = app.renderer.plugins.extract.image(generatedImage, "image/png", 1.0);
generatedImage.destroy();
return image;
}
10 changes: 10 additions & 0 deletions src/pages/editor/EditorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactElement } from "react";
import { Provider } from "jotai";

export interface EditorContextProps {
children: ReactElement;
}

export function EditorContext({ children }: EditorContextProps): ReactElement {
return <Provider>{children}</Provider>;
}
15 changes: 3 additions & 12 deletions src/pages/editor/EditorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ReactElement } from "react";
import { useNavigate } from "react-router-dom";
import { useEditorPage } from "./UseEditorPage";
import { ImageEditor } from "~/components/library/editor/Editor";
import { useThemeSwitcher } from "~/components/library/theme/UseThemeSwitcher";
import { ErrorView } from "~/components/views/error/ErrorView";
import { LoadingView } from "~/components/views/loading/LoadingView";

Expand All @@ -10,20 +9,12 @@ export interface EditorPageProps {
}

export function EditorPage({ url }: EditorPageProps): ReactElement {
const navigate = useNavigate();
const themeSwitchProps = useThemeSwitcher();

function handleOnNewImage(): void {
navigate(`/`);
}
const appDrawerProps = useEditorPage();

return (
<ImageEditor
url={url}
appdrawerProps={{
onNewImage: handleOnNewImage,
themeSwitchProps,
}}
appdrawerProps={appDrawerProps}
LoaderComponent={() => <LoadingView />}
ErrorComponent={() => <ErrorView onBack={() => console.log("onBack")} />}
/>
Expand Down
7 changes: 6 additions & 1 deletion src/pages/editor/EditorRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactElement } from "react";
import { useAtomValue } from "jotai";
import { EditorContext } from "./EditorContext";
import { EditorPage } from "./EditorPage";
import { ErrorView } from "~/components/views/error/ErrorView";
import { useErrorView } from "~/components/views/error/UseErrorView";
Expand All @@ -13,7 +14,11 @@ export function Component(): ReactElement {
return <ErrorView {...errorViewProps} message="File somehow went missing 🤔" />;
}

return <EditorPage url={fileUrl} />;
return (
<EditorContext>
<EditorPage url={fileUrl} />
</EditorContext>
);
}
Component.displayName = "EditorPage";

Expand Down
37 changes: 37 additions & 0 deletions src/pages/editor/UseEditorPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useAtomValue } from "jotai";
import { useNavigate } from "react-router-dom";
import { viewportAtom } from "~/components/library/editor/atoms/viewport/ViewportAtoms";
import type { AppDrawerProps } from "~/components/library/editor/drawer/AppDrawer";
import { useThemeSwitcher } from "~/components/library/theme/UseThemeSwitcher";
import { createImage } from "~/libs/functions/CreateImage";

export function useEditorPage(): Omit<AppDrawerProps, "onClose" | "open"> {
const navigate = useNavigate();
const themeSwitchProps = useThemeSwitcher();
const viewport = useAtomValue(viewportAtom);

function handleOnNewImage(): void {
navigate(`/`);
}

async function handleOnSaveImage(): Promise<void> {
if (!viewport) {
console.error("Viewport is not available");
return;
}

const imagesrc = await createImage(viewport);
const createEl = document.createElement("a");

createEl.href = imagesrc.src;
createEl.download = "image.png";
createEl.click();
createEl.remove();
}

return {
onNewImage: handleOnNewImage,
onSaveImage: handleOnSaveImage,
themeSwitchProps,
};
}

0 comments on commit dcf4510

Please sign in to comment.