Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Colors API, updated Image Editing Overlays, and more #27

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 2024-04-23

### 🧰 Added
- `@canva/preview`
- Added [asset.openColorSelector](https://www.canva.dev/docs/apps/using-color-selectors) under `@canva/preview/asset` to open a selector to pick Document, Brand, and custom colors.
- Added [/examples/color](/examples/color) to demonstrate usage of the Colors API.

### 🔧 Changed
- The HMR warning printed to the console on app run is now an info warning instead.
- `examples`
- Update [/examples/image_editing_overlay](/examples/image_editing_overlay) to reflect current recommended practices when working with overlay api.

### 🗑️ Removed
- `@canva/preview`
- Removed `AppProcessInfo.context` for selected_image_overlay surface due to stale selection, which results in wrong imageUrl passing to the overlay surface. Image url should not be requested outside of overlay code since it can be stale as users can change selection during opening overlay.
- `examples`
- Removed `OverlayLoadingIndicator` React component to [/examples/image_editing_overlay](/examples/image_editing_overlay) due to issue with cropped and flipped image.

## 2024-04-16

### 🧰 Added
Expand Down
44 changes: 44 additions & 0 deletions examples/color/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Rows, Swatch, Text } from "@canva/app-ui-kit";
import {
Anchor,
ColorSelectionEvent,
ColorSelectionScope,
openColorSelector,
} from "@canva/preview/asset";
import React from "react";
import styles from "styles/components.css";

export const App = () => {
const [color, setColor] = React.useState<string | undefined>(undefined);
const onColorSelect = async <T extends ColorSelectionScope>(
e: ColorSelectionEvent<T>
) => {
if (e.selection.type === "solid") {
setColor(e.selection.hexString);
}
};

const onRequestOpenColorSelector = (boundingRect: Anchor) => {
openColorSelector(boundingRect, {
onColorSelect: onColorSelect,
scopes: ["solid"],
});
};

return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Text>
This example demonstrates how apps can pick Brand, Document, and
custom colors in an app.
</Text>
<Swatch
fill={[color]}
onClick={(e) =>
onRequestOpenColorSelector(e.currentTarget.getBoundingClientRect())
}
/>
</Rows>
</div>
);
};
20 changes: 20 additions & 0 deletions examples/color/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "@canva/app-ui-kit/styles.css";
import { AppUiProvider } from "@canva/app-ui-kit";

const root = createRoot(document.getElementById("root")!);
function render() {
root.render(
<AppUiProvider>
<App />
</AppUiProvider>
);
}

render();

if (module.hot) {
module.hot.accept("./app", render);
}
6 changes: 6 additions & 0 deletions examples/color/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "color",
"description": "This example demonstrates how apps can pick Brand, Document, and custom colors in an app.",
"author": "Canva Pty Ltd.",
"license": "Please refer to the LICENSE.md file in the root directory"
}
2 changes: 0 additions & 2 deletions examples/image_editing_overlay/object_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as React from "react";
import styles from "styles/components.css";
import { appProcess } from "@canva/preview/platform";
import { useOverlay } from "utils/use_overlay_hook";
import { useSelection } from "utils/use_selection_hook";
import { LaunchParams } from "./app";
import type { CloseOpts } from "./overlay";

Expand All @@ -21,7 +20,6 @@ export const ObjectPanel = () => {
open,
close: closeOverlay,
} = useOverlay<"image_selection", CloseOpts>("image_selection");
const selection = useSelection("image");
const [state, setState] = React.useState<UIState>(initialState);

const openOverlay = async () => {
Expand Down
135 changes: 86 additions & 49 deletions examples/image_editing_overlay/overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from "react";
import { AppProcessInfo, CloseParams } from "sdk/preview/platform";
import { LaunchParams } from "./app";
import { upload } from "@canva/asset";
import { getTemporaryUrl, upload } from "@canva/asset";
import { useSelection } from "utils/use_selection_hook";
import { appProcess } from "@canva/preview/platform";
import { OverlayLoadingIndicator } from "./overlay_loading_indicator";
import { SelectionEvent } from "@canva/design";

// App can extend CloseParams type to send extra data when closing the overlay
// For example:
Expand All @@ -21,53 +21,71 @@ type UIState = {

export const Overlay = (props: OverlayProps) => {
const { context: appContext } = props;
const selection = useSelection("image");

const canvasRef = React.useRef<HTMLCanvasElement>(null);
const isDragginRef = React.useRef<boolean>();
const selection = useSelection("image");
const uiStateRef = React.useRef<UIState>({
brushSize: 7,
});
const [isLoading, setIsLoading] = React.useState(false);

React.useEffect(() => {
if (!selection || selection.count !== 1) {
return;
}

if (
!appContext.launchParams ||
appContext.surface !== "selected_image_overlay"
) {
return;
return void abort();
}

const uiState = appContext.launchParams;
const selectedImageUrl = appContext.context.imageUrl;

// set initial ui state
const uiState = appContext.launchParams;
uiStateRef.current = uiState;

// set up canvas
const canvas = canvasRef.current;
if (!canvas) {
throw new Error("no canvas");
return void abort();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("failed to create context 2d");
return void abort();
}

// load image
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// load and draw image to canvas
let img = new Image();
img.onload = () => {
let cssScale = 1;
const drawImage = () => {
// Set the canvas dimensions to match the original image dimensions to maintain image quality,
// when saving the output image back to the design using canvas.toDataUrl()
cssScale = window.innerWidth / img.width;
canvas.width = img.width;
canvas.height = img.height;
canvas.style.transform = `scale(${cssScale})`;
canvas.style.transformOrigin = "0 0";
context.drawImage(img, 0, 0, canvas.width, canvas.height);
};
img.onload = drawImage;
img.crossOrigin = "anonymous";
img.src = selectedImageUrl;
(async () => {
const selectedImageUrl = await loadOriginalImage(selection);
if (!selectedImageUrl) {
return void abort();
}
img.src = selectedImageUrl;
})();

window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
if (img.complete) {
context.drawImage(img, 0, 0, canvas.width, canvas.height);
drawImage();
}
});

Expand All @@ -77,12 +95,13 @@ export const Overlay = (props: OverlayProps) => {

canvas.addEventListener("pointermove", (e) => {
if (isDragginRef.current) {
const mousePos = getCanvasMousePosition(canvas, e);
context.fillStyle = "white";
context.beginPath();
context.arc(
e.clientX,
e.clientY,
uiStateRef.current.brushSize,
mousePos.x,
mousePos.y,
uiStateRef.current.brushSize * (1 / cssScale),
0,
Math.PI * 2
);
Expand All @@ -94,51 +113,69 @@ export const Overlay = (props: OverlayProps) => {
isDragginRef.current = false;
});

// set up message handler
return void appProcess.registerOnMessage((_, message) => {
if (!message) {
return;
}
const { brushSize } = message as UIState;
uiStateRef.current = {
...uiStateRef.current,
brushSize: brushSize,
};
});
}, []);

React.useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !selection || selection.count !== 1) {
return;
}

return void appProcess.current.setOnDispose<CloseOpts>(
async ({ reason }) => {
if (reason === "aborted") {
// abort if image has not loaded or receive `aborted` signal
if (reason === "aborted" || !img.complete) {
return;
}
setIsLoading(true);
const dataUrl = canvas.toDataURL();
const draft = await selection.read();
const queueImage = await upload({
type: "IMAGE",
mimeType: "image/png",
url: canvas.toDataURL(),
thumbnailUrl: canvas.toDataURL(),
url: dataUrl,
thumbnailUrl: dataUrl,
width: canvas.width,
height: canvas.height,
});
draft.contents[0].ref = queueImage.ref;
await draft.save();
setIsLoading(false);
}
);
}, [selection]);

return (
<>
<canvas ref={canvasRef} />
{isLoading && <OverlayLoadingIndicator />}
</>
);
React.useEffect(() => {
// set up message handler
return void appProcess.registerOnMessage((_, message) => {
if (!message) {
return;
}
const { brushSize } = message as UIState;
uiStateRef.current = {
...uiStateRef.current,
brushSize: brushSize,
};
});
}, []);

return <canvas ref={canvasRef} />;
};

const abort = () => appProcess.current.requestClose({ reason: "aborted" });

const loadOriginalImage = async (selection: SelectionEvent<"image">) => {
if (selection.count !== 1) {
return;
}
const draft = await selection.read();
const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: draft.contents[0].ref,
});
return url;
};

// get the mouse position relative to the canvas
const getCanvasMousePosition = (
canvas: HTMLCanvasElement,
event: PointerEvent
) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY,
};
};
11 changes: 0 additions & 11 deletions examples/image_editing_overlay/overlay_loading_indicator.css

This file was deleted.

16 changes: 0 additions & 16 deletions examples/image_editing_overlay/overlay_loading_indicator.tsx

This file was deleted.

11 changes: 9 additions & 2 deletions package-lock.json

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

Loading
Loading