orio-ui 1.20.0 → 1.23.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -2
- package/dist/runtime/canvas.d.ts +21 -0
- package/dist/runtime/canvas.js +52 -0
- package/dist/runtime/components/Canvas/REQUIREMENTS.md +174 -0
- package/dist/runtime/components/Canvas/components/Stage.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/components/Stage.vue +150 -0
- package/dist/runtime/components/Canvas/components/Stage.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/components/ToolButton.d.vue.ts +24 -0
- package/dist/runtime/components/Canvas/components/ToolButton.vue +62 -0
- package/dist/runtime/components/Canvas/components/ToolButton.vue.d.ts +24 -0
- package/dist/runtime/components/Canvas/components/Toolbar.d.vue.ts +24 -0
- package/dist/runtime/components/Canvas/components/Toolbar.vue +48 -0
- package/dist/runtime/components/Canvas/components/Toolbar.vue.d.ts +24 -0
- package/dist/runtime/components/Canvas/composables/useCanvasHistory.d.ts +17 -0
- package/dist/runtime/components/Canvas/composables/useCanvasHistory.js +76 -0
- package/dist/runtime/components/Canvas/composables/useCanvasNodes.d.ts +13 -0
- package/dist/runtime/components/Canvas/composables/useCanvasNodes.js +60 -0
- package/dist/runtime/components/Canvas/composables/useCanvasSetup.d.ts +5 -0
- package/dist/runtime/components/Canvas/composables/useCanvasSetup.js +19 -0
- package/dist/runtime/components/Canvas/context.d.ts +47 -0
- package/dist/runtime/components/Canvas/context.js +11 -0
- package/dist/runtime/components/Canvas/index.d.vue.ts +80 -0
- package/dist/runtime/components/Canvas/index.vue +225 -0
- package/dist/runtime/components/Canvas/index.vue.d.ts +80 -0
- package/dist/runtime/components/Canvas/registry.d.ts +1 -0
- package/dist/runtime/components/Canvas/registry.js +2 -0
- package/dist/runtime/components/Canvas/tools/ColorPickerWidget.d.vue.ts +7 -0
- package/dist/runtime/components/Canvas/tools/ColorPickerWidget.vue +32 -0
- package/dist/runtime/components/Canvas/tools/ColorPickerWidget.vue.d.ts +7 -0
- package/dist/runtime/components/Canvas/tools/clearTool.d.ts +1 -0
- package/dist/runtime/components/Canvas/tools/clearTool.js +16 -0
- package/dist/runtime/components/Canvas/tools/colorPickerTool.d.ts +6 -0
- package/dist/runtime/components/Canvas/tools/colorPickerTool.js +15 -0
- package/dist/runtime/components/Canvas/tools/drawTool.d.ts +16 -0
- package/dist/runtime/components/Canvas/tools/drawTool.js +92 -0
- package/dist/runtime/components/Canvas/tools/eraseTool.d.ts +5 -0
- package/dist/runtime/components/Canvas/tools/eraseTool.js +62 -0
- package/dist/runtime/components/Canvas/tools/exportTool.d.ts +65 -0
- package/dist/runtime/components/Canvas/tools/exportTool.js +136 -0
- package/dist/runtime/components/Canvas/tools/highlightTool.d.ts +11 -0
- package/dist/runtime/components/Canvas/tools/highlightTool.js +51 -0
- package/dist/runtime/components/Canvas/tools/hitTest.d.ts +20 -0
- package/dist/runtime/components/Canvas/tools/hitTest.js +111 -0
- package/dist/runtime/components/Canvas/tools/imageTool.d.ts +18 -0
- package/dist/runtime/components/Canvas/tools/imageTool.js +163 -0
- package/dist/runtime/components/Canvas/tools/moveTool.d.ts +5 -0
- package/dist/runtime/components/Canvas/tools/moveTool.js +94 -0
- package/dist/runtime/components/Canvas/tools/redoTool.d.ts +1 -0
- package/dist/runtime/components/Canvas/tools/redoTool.js +17 -0
- package/dist/runtime/components/Canvas/tools/resizeTool.d.ts +7 -0
- package/dist/runtime/components/Canvas/tools/resizeTool.js +132 -0
- package/dist/runtime/components/Canvas/tools/rotateTool.d.ts +5 -0
- package/dist/runtime/components/Canvas/tools/rotateTool.js +109 -0
- package/dist/runtime/components/Canvas/tools/textTool.d.ts +14 -0
- package/dist/runtime/components/Canvas/tools/textTool.js +99 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Clear.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Clear.vue +12 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Clear.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Draw.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Draw.vue +12 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Draw.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Erase.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Erase.vue +12 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Erase.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Export.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Export.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Export.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Highlight.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Highlight.vue +12 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Highlight.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Image.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Image.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Image.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Move.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Move.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Move.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Redo.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Redo.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Redo.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Resize.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Resize.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Resize.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Rotate.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Rotate.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Rotate.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Text.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Text.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Text.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Transform.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Transform.vue +14 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Transform.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Undo.d.vue.ts +3 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Undo.vue +13 -0
- package/dist/runtime/components/Canvas/tools/tooltips/Undo.vue.d.ts +3 -0
- package/dist/runtime/components/Canvas/tools/transformHandles.d.ts +74 -0
- package/dist/runtime/components/Canvas/tools/transformHandles.js +191 -0
- package/dist/runtime/components/Canvas/tools/transformTool.d.ts +7 -0
- package/dist/runtime/components/Canvas/tools/transformTool.js +210 -0
- package/dist/runtime/components/Canvas/tools/undoTool.d.ts +1 -0
- package/dist/runtime/components/Canvas/tools/undoTool.js +17 -0
- package/dist/runtime/components/Canvas/types.d.ts +125 -0
- package/dist/runtime/components/Canvas/types.js +3 -0
- package/dist/runtime/components/ControlElement.vue +5 -1
- package/dist/runtime/components/DateRangePicker.vue +16 -4
- package/dist/runtime/components/Icon.vue +2 -2
- package/dist/runtime/components/LocaleSwitcher.d.vue.ts +13 -0
- package/dist/runtime/components/LocaleSwitcher.vue +43 -0
- package/dist/runtime/components/LocaleSwitcher.vue.d.ts +13 -0
- package/dist/runtime/components/Selector.vue +14 -5
- package/dist/runtime/components/Tooltip.vue +17 -7
- package/dist/runtime/components/ZoomableContainer.d.vue.ts +48 -0
- package/dist/runtime/components/ZoomableContainer.vue +238 -0
- package/dist/runtime/components/ZoomableContainer.vue.d.ts +48 -0
- package/dist/runtime/components/gallery/Carousel.vue +1 -1
- package/dist/runtime/components/gallery/CarouselPreview.d.vue.ts +31 -0
- package/dist/runtime/components/gallery/CarouselPreview.vue +64 -0
- package/dist/runtime/components/gallery/CarouselPreview.vue.d.ts +31 -0
- package/dist/runtime/components/view/Dates.vue +5 -3
- package/dist/runtime/components/view/KeyBinds.d.vue.ts +7 -0
- package/dist/runtime/components/view/KeyBinds.vue +36 -0
- package/dist/runtime/components/view/KeyBinds.vue.d.ts +7 -0
- package/dist/runtime/components/view/Text.vue +4 -4
- package/dist/runtime/composables/useInertia.d.ts +10 -0
- package/dist/runtime/composables/useInertia.js +49 -0
- package/dist/runtime/composables/usePinchZoom.d.ts +13 -0
- package/dist/runtime/composables/usePinchZoom.js +66 -0
- package/dist/runtime/composables/useValidation.js +11 -1
- package/dist/runtime/i18n/en.json +20 -0
- package/dist/runtime/i18n/index.d.ts +11 -0
- package/dist/runtime/i18n/index.js +19 -0
- package/dist/runtime/i18n/uk.json +20 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +16 -0
- package/dist/runtime/plugins/i18n.d.ts +2 -0
- package/dist/runtime/plugins/i18n.js +18 -0
- package/dist/runtime/utils/icon-registry.js +13 -1
- package/package.json +9 -4
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CanvasNode, CanvasTool, CanvasToolApi } from "./types.js";
|
|
2
|
+
import { type ExportOptions, type ExportResult } from "./tools/exportTool.js";
|
|
3
|
+
export interface CanvasProps {
|
|
4
|
+
/**
|
|
5
|
+
* Unique name for this canvas instance. Required so detached toolbars and
|
|
6
|
+
* other UI can bind to this canvas via the `canvas` prop, e.g.
|
|
7
|
+
* `<orio-canvas-toolbar canvas="editor" />` rendered anywhere in the app.
|
|
8
|
+
*/
|
|
9
|
+
name: string;
|
|
10
|
+
/** Tools available to the canvas. Empty by default — specify all tools explicitly. */
|
|
11
|
+
tools?: CanvasTool[];
|
|
12
|
+
/** Drawing surface width in CSS pixels. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Drawing surface height in CSS pixels. */
|
|
15
|
+
height?: number;
|
|
16
|
+
/** Initial active tool id. Defaults to the first tool. */
|
|
17
|
+
defaultTool?: string;
|
|
18
|
+
/** CSS background applied to the stage wrapper. */
|
|
19
|
+
background?: string;
|
|
20
|
+
/** Maximum undo history depth. */
|
|
21
|
+
maxHistory?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Called once on mount with the full tool API. Use it to seed the canvas
|
|
24
|
+
* with initial nodes (images, text, shapes). Nodes added here can be
|
|
25
|
+
* `frozen: true` to lock them from user interaction.
|
|
26
|
+
*
|
|
27
|
+
* The toolbar controls what the *user* can do — setup can add any node
|
|
28
|
+
* type regardless of which tools are registered.
|
|
29
|
+
*/
|
|
30
|
+
setup?: (api: CanvasToolApi) => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
type __VLS_Props = CanvasProps;
|
|
33
|
+
declare function getToolOptions<T extends Record<string, unknown>>(id: string): T;
|
|
34
|
+
declare function setActiveTool(id: string | null): void;
|
|
35
|
+
declare function exportCanvas(overrides?: ExportOptions): Promise<ExportResult>;
|
|
36
|
+
type __VLS_ModelProps = {
|
|
37
|
+
"nodes"?: CanvasNode[];
|
|
38
|
+
};
|
|
39
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
40
|
+
declare var __VLS_1: {};
|
|
41
|
+
type __VLS_Slots = {} & {
|
|
42
|
+
default?: (props: typeof __VLS_1) => any;
|
|
43
|
+
};
|
|
44
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
45
|
+
activeTool: import("vue").ComputedRef<any>;
|
|
46
|
+
setActiveTool: typeof setActiveTool;
|
|
47
|
+
addNode: (node: Omit<CanvasNode, "id"> & {
|
|
48
|
+
id?: string;
|
|
49
|
+
}) => CanvasNode;
|
|
50
|
+
updateNode: (id: string, patch: Partial<Omit<CanvasNode, "id">>) => void;
|
|
51
|
+
removeNode: (id: string) => void;
|
|
52
|
+
clear: () => void;
|
|
53
|
+
nodes: import("vue").ModelRef<CanvasNode<unknown>[], string, CanvasNode<unknown>[], CanvasNode<unknown>[]>;
|
|
54
|
+
getToolOptions: typeof getToolOptions;
|
|
55
|
+
undo: () => void;
|
|
56
|
+
redo: () => void;
|
|
57
|
+
canUndo: import("vue").ComputedRef<boolean>;
|
|
58
|
+
canRedo: import("vue").ComputedRef<boolean>;
|
|
59
|
+
exportCanvas: typeof exportCanvas;
|
|
60
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
61
|
+
"update:nodes": (value: CanvasNode<unknown>[]) => any;
|
|
62
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
63
|
+
"onUpdate:nodes"?: ((value: CanvasNode<unknown>[]) => any) | undefined;
|
|
64
|
+
}>, {
|
|
65
|
+
setup: (api: CanvasToolApi) => void | Promise<void>;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
tools: CanvasTool[];
|
|
69
|
+
defaultTool: string;
|
|
70
|
+
background: string;
|
|
71
|
+
maxHistory: number;
|
|
72
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
73
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
74
|
+
declare const _default: typeof __VLS_export;
|
|
75
|
+
export default _default;
|
|
76
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
77
|
+
new (): {
|
|
78
|
+
$slots: S;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const canvasRegistry: any;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CanvasTool } from "../types.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
tool: CanvasTool;
|
|
4
|
+
}
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useCanvasContext } from "../context";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
tool: { type: Object, required: true }
|
|
5
|
+
});
|
|
6
|
+
const ctx = useCanvasContext();
|
|
7
|
+
const options = ctx.getToolOptions(props.tool.id);
|
|
8
|
+
function onChange(e) {
|
|
9
|
+
const color = e.target.value;
|
|
10
|
+
options.color = color;
|
|
11
|
+
for (const targetId of options.targets ?? []) {
|
|
12
|
+
const targetOpts = ctx.getToolOptions(targetId);
|
|
13
|
+
if (targetOpts) targetOpts.color = color;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<orio-control-element>
|
|
20
|
+
<input
|
|
21
|
+
type="color"
|
|
22
|
+
:value="options.color"
|
|
23
|
+
:title="tool.label ?? 'Color'"
|
|
24
|
+
class="canvas-color-picker"
|
|
25
|
+
@input="onChange"
|
|
26
|
+
/>
|
|
27
|
+
</orio-control-element>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<style scoped>
|
|
31
|
+
.canvas-color-picker{background:none;border:1px solid var(--color-border,#ddd);border-radius:4px;cursor:pointer;height:1.75rem;padding:0;width:2rem}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CanvasTool } from "../types.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
tool: CanvasTool;
|
|
4
|
+
}
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function clearTool(): import("../types.js").CanvasTool<unknown, Record<string, unknown>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import ClearTooltip from "./tooltips/Clear.vue";
|
|
3
|
+
export function clearTool() {
|
|
4
|
+
return defineCanvasTool({
|
|
5
|
+
id: "clear",
|
|
6
|
+
label: "Clear",
|
|
7
|
+
kind: "action",
|
|
8
|
+
tooltip: ClearTooltip,
|
|
9
|
+
action(api) {
|
|
10
|
+
api.clear();
|
|
11
|
+
},
|
|
12
|
+
disabled(api) {
|
|
13
|
+
return api.nodes.value.every((n) => n.frozen);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ColorPickerToolOptions extends Record<string, unknown> {
|
|
2
|
+
color: string;
|
|
3
|
+
/** Tool ids whose `color` option should be synced. */
|
|
4
|
+
targets: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function colorPickerTool(options?: Partial<ColorPickerToolOptions>): import("../types.js").CanvasTool<never, ColorPickerToolOptions>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import ColorPickerWidget from "./ColorPickerWidget.vue";
|
|
3
|
+
export function colorPickerTool(options = {}) {
|
|
4
|
+
return defineCanvasTool({
|
|
5
|
+
id: "color-picker",
|
|
6
|
+
label: "Color",
|
|
7
|
+
kind: "widget",
|
|
8
|
+
toolbar: ColorPickerWidget,
|
|
9
|
+
defaultOptions: {
|
|
10
|
+
color: "#111111",
|
|
11
|
+
targets: [],
|
|
12
|
+
...options
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CanvasPoint } from "../types.js";
|
|
2
|
+
export interface DrawToolOptions extends Record<string, unknown> {
|
|
3
|
+
color: string;
|
|
4
|
+
size: number;
|
|
5
|
+
opacity: number;
|
|
6
|
+
/** Reserved for future brushes (eraser, marker, airbrush...). */
|
|
7
|
+
brush: "pen" | "marker";
|
|
8
|
+
}
|
|
9
|
+
export interface DrawNodeData {
|
|
10
|
+
points: CanvasPoint[];
|
|
11
|
+
color: string;
|
|
12
|
+
size: number;
|
|
13
|
+
opacity: number;
|
|
14
|
+
brush: DrawToolOptions["brush"];
|
|
15
|
+
}
|
|
16
|
+
export declare function drawTool(options?: Partial<DrawToolOptions>): import("../types.js").CanvasTool<DrawNodeData, DrawToolOptions>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import DrawTooltip from "./tooltips/Draw.vue";
|
|
3
|
+
export function drawTool(options = {}) {
|
|
4
|
+
let activeStrokeId = null;
|
|
5
|
+
return defineCanvasTool({
|
|
6
|
+
id: "draw",
|
|
7
|
+
label: "Draw",
|
|
8
|
+
icon: "pencil",
|
|
9
|
+
cursor: "crosshair",
|
|
10
|
+
tooltip: DrawTooltip,
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
color: "#111111",
|
|
13
|
+
size: 4,
|
|
14
|
+
opacity: 1,
|
|
15
|
+
brush: "pen",
|
|
16
|
+
...options
|
|
17
|
+
},
|
|
18
|
+
onPointerDown(e, api) {
|
|
19
|
+
const node = api.addNode({
|
|
20
|
+
type: "draw",
|
|
21
|
+
x: 0,
|
|
22
|
+
y: 0,
|
|
23
|
+
data: {
|
|
24
|
+
points: [{ x: e.x, y: e.y }],
|
|
25
|
+
color: api.options.color,
|
|
26
|
+
size: api.options.size,
|
|
27
|
+
opacity: api.options.opacity,
|
|
28
|
+
brush: api.options.brush
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
activeStrokeId = node.id;
|
|
32
|
+
},
|
|
33
|
+
onPointerMove(e, api) {
|
|
34
|
+
if (!activeStrokeId || e.buttons === 0) return;
|
|
35
|
+
const node = api.getNode(activeStrokeId);
|
|
36
|
+
if (!node) return;
|
|
37
|
+
node.data.points.push({ x: e.x, y: e.y });
|
|
38
|
+
api.requestRender();
|
|
39
|
+
},
|
|
40
|
+
onPointerUp(_e, api) {
|
|
41
|
+
if (!activeStrokeId) return;
|
|
42
|
+
const node = api.getNode(activeStrokeId);
|
|
43
|
+
if (node) {
|
|
44
|
+
api.updateNode(activeStrokeId, {
|
|
45
|
+
data: { ...node.data }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
activeStrokeId = null;
|
|
49
|
+
},
|
|
50
|
+
hitTest(node, point, radius) {
|
|
51
|
+
const { points, size } = node.data;
|
|
52
|
+
const r = radius + size / 2;
|
|
53
|
+
return points.some(
|
|
54
|
+
(p) => (p.x - point.x) ** 2 + (p.y - point.y) ** 2 <= r * r
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
render(ctx, node) {
|
|
58
|
+
const { points, color, size, opacity, brush } = node.data;
|
|
59
|
+
if (points.length === 0) return;
|
|
60
|
+
ctx.save();
|
|
61
|
+
ctx.globalAlpha = opacity;
|
|
62
|
+
ctx.strokeStyle = color;
|
|
63
|
+
ctx.lineWidth = size;
|
|
64
|
+
ctx.lineCap = "round";
|
|
65
|
+
ctx.lineJoin = "round";
|
|
66
|
+
if (brush === "marker") {
|
|
67
|
+
ctx.globalCompositeOperation = "multiply";
|
|
68
|
+
}
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
if (points.length === 1) {
|
|
71
|
+
const [p] = points;
|
|
72
|
+
ctx.arc(p.x, p.y, Math.max(size / 2, 0.5), 0, Math.PI * 2);
|
|
73
|
+
ctx.fillStyle = color;
|
|
74
|
+
ctx.fill();
|
|
75
|
+
ctx.restore();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
79
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
80
|
+
const p = points[i];
|
|
81
|
+
const next = points[i + 1];
|
|
82
|
+
const mx = (p.x + next.x) / 2;
|
|
83
|
+
const my = (p.y + next.y) / 2;
|
|
84
|
+
ctx.quadraticCurveTo(p.x, p.y, mx, my);
|
|
85
|
+
}
|
|
86
|
+
const last = points[points.length - 1];
|
|
87
|
+
ctx.lineTo(last.x, last.y);
|
|
88
|
+
ctx.stroke();
|
|
89
|
+
ctx.restore();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import { findTopNode, renderHighlightRect } from "./hitTest.js";
|
|
3
|
+
import EraseTooltip from "./tooltips/Erase.vue";
|
|
4
|
+
export function eraseTool(options = {}) {
|
|
5
|
+
let erasing = false;
|
|
6
|
+
let hoveredNodeId = null;
|
|
7
|
+
function eraseAt(point, api) {
|
|
8
|
+
const node = findTopNode(point, api, api.options.radius);
|
|
9
|
+
if (node) {
|
|
10
|
+
hoveredNodeId = null;
|
|
11
|
+
api.removeNode(node.id);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return defineCanvasTool({
|
|
15
|
+
id: "erase",
|
|
16
|
+
label: "Eraser",
|
|
17
|
+
icon: "eraser",
|
|
18
|
+
cursor: "crosshair",
|
|
19
|
+
tooltip: EraseTooltip,
|
|
20
|
+
defaultOptions: {
|
|
21
|
+
radius: 10,
|
|
22
|
+
...options
|
|
23
|
+
},
|
|
24
|
+
onPointerDown(e, api) {
|
|
25
|
+
erasing = true;
|
|
26
|
+
eraseAt(e, api);
|
|
27
|
+
},
|
|
28
|
+
onPointerMove(e, api) {
|
|
29
|
+
if (erasing && e.buttons !== 0) {
|
|
30
|
+
eraseAt(e, api);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const node = findTopNode(e, api, api.options.radius);
|
|
34
|
+
const newId = node?.id ?? null;
|
|
35
|
+
if (newId !== hoveredNodeId) {
|
|
36
|
+
hoveredNodeId = newId;
|
|
37
|
+
api.requestRender();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
onPointerUp() {
|
|
41
|
+
erasing = false;
|
|
42
|
+
},
|
|
43
|
+
onDeactivate(api) {
|
|
44
|
+
if (hoveredNodeId) {
|
|
45
|
+
hoveredNodeId = null;
|
|
46
|
+
api.requestRender();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
renderOverlay(ctx, api) {
|
|
50
|
+
if (!hoveredNodeId) return;
|
|
51
|
+
const node = api.getNode(hoveredNodeId);
|
|
52
|
+
if (!node) {
|
|
53
|
+
hoveredNodeId = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
renderHighlightRect(ctx, node, {
|
|
57
|
+
strokeColor: "rgba(220, 53, 69, 0.8)",
|
|
58
|
+
fillColor: "rgba(220, 53, 69, 0.08)"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CanvasNode, CanvasTool } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Image formats Canvas2D's `toBlob`/`toDataURL` natively supports across
|
|
4
|
+
* modern browsers. Anything else (e.g. AVIF, TIFF) is encoder-dependent and
|
|
5
|
+
* not guaranteed to round-trip.
|
|
6
|
+
*/
|
|
7
|
+
export type ExportFormat = "png" | "jpeg" | "webp";
|
|
8
|
+
export interface ExportResult {
|
|
9
|
+
blob: Blob;
|
|
10
|
+
dataURL: string;
|
|
11
|
+
/** Same bytes as `blob`, wrapped as a `File` with the configured filename. */
|
|
12
|
+
file: File;
|
|
13
|
+
format: ExportFormat;
|
|
14
|
+
/** Output pixel width (already multiplied by `scale`). */
|
|
15
|
+
width: number;
|
|
16
|
+
/** Output pixel height (already multiplied by `scale`). */
|
|
17
|
+
height: number;
|
|
18
|
+
filename: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ExportOptions {
|
|
21
|
+
/** Output format. Defaults to PNG. */
|
|
22
|
+
format?: ExportFormat;
|
|
23
|
+
/** Quality for lossy formats (jpeg, webp), 0–1. Ignored for PNG. */
|
|
24
|
+
quality?: number;
|
|
25
|
+
/** File name without extension. */
|
|
26
|
+
filename?: string;
|
|
27
|
+
/** Pixel scale relative to the visible canvas (1 = same size). */
|
|
28
|
+
scale?: number;
|
|
29
|
+
/**
|
|
30
|
+
* If provided, receives the export result instead of (or alongside) the
|
|
31
|
+
* download. Use to push the image into application state (e.g. a cart) or
|
|
32
|
+
* upload to a backend. Async — the canvas awaits the returned Promise.
|
|
33
|
+
*/
|
|
34
|
+
onExport?: (result: ExportResult) => void | Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Trigger a browser download. Defaults to `true` when `onExport` is not
|
|
37
|
+
* provided, `false` when it is.
|
|
38
|
+
*/
|
|
39
|
+
download?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface ExportToolOptions extends Required<Pick<ExportOptions, "format" | "quality" | "filename" | "scale">>, Pick<ExportOptions, "onExport" | "download">, Record<string, unknown> {
|
|
42
|
+
}
|
|
43
|
+
export declare const DEFAULT_EXPORT_OPTIONS: ExportToolOptions;
|
|
44
|
+
export interface CanvasSnapshot {
|
|
45
|
+
nodes: CanvasNode[];
|
|
46
|
+
tools: CanvasTool[];
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render the snapshot to an offscreen canvas and produce a Blob/dataURL/File.
|
|
52
|
+
* Pure: does not download or invoke callbacks.
|
|
53
|
+
*
|
|
54
|
+
* The offscreen canvas is sized exactly to `snapshot.width × snapshot.height`
|
|
55
|
+
* (× `scale`), so any node geometry outside the canvas is naturally clipped —
|
|
56
|
+
* the result captures only what would be visible on the live canvas.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderCanvasSnapshot(snapshot: CanvasSnapshot, options?: ExportOptions): Promise<ExportResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Render the snapshot, then optionally hand the result to `onExport` and/or
|
|
61
|
+
* trigger a browser download. Resolves with the result so callers can chain
|
|
62
|
+
* additional work (uploads, state updates).
|
|
63
|
+
*/
|
|
64
|
+
export declare function performExport(snapshot: CanvasSnapshot, options?: ExportOptions): Promise<ExportResult>;
|
|
65
|
+
export declare function exportTool(options?: Partial<ExportToolOptions>): CanvasTool<never, ExportToolOptions>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import { getNodeBounds } from "./hitTest.js";
|
|
3
|
+
import ExportTooltip from "./tooltips/Export.vue";
|
|
4
|
+
const MIME_TYPES = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpeg: "image/jpeg",
|
|
7
|
+
webp: "image/webp"
|
|
8
|
+
};
|
|
9
|
+
export const DEFAULT_EXPORT_OPTIONS = {
|
|
10
|
+
format: "png",
|
|
11
|
+
quality: 0.92,
|
|
12
|
+
filename: "canvas",
|
|
13
|
+
scale: 1
|
|
14
|
+
};
|
|
15
|
+
function renderNodesTo(target, nodes, tools) {
|
|
16
|
+
const toolMap = new Map(tools.map((t) => [t.id, t]));
|
|
17
|
+
const ordered = [...nodes].sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
|
|
18
|
+
for (const node of ordered) {
|
|
19
|
+
const renderFn = toolMap.get(node.type)?.render;
|
|
20
|
+
if (!renderFn) continue;
|
|
21
|
+
if (node.rotation) {
|
|
22
|
+
const b = getNodeBounds(node);
|
|
23
|
+
const cx = b.x + b.width / 2;
|
|
24
|
+
const cy = b.y + b.height / 2;
|
|
25
|
+
target.save();
|
|
26
|
+
target.translate(cx, cy);
|
|
27
|
+
target.rotate(node.rotation);
|
|
28
|
+
target.translate(-cx, -cy);
|
|
29
|
+
renderFn(target, node);
|
|
30
|
+
target.restore();
|
|
31
|
+
} else {
|
|
32
|
+
renderFn(target, node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function downloadBlob(blob, filename) {
|
|
37
|
+
const url = URL.createObjectURL(blob);
|
|
38
|
+
const a = document.createElement("a");
|
|
39
|
+
a.href = url;
|
|
40
|
+
a.download = filename;
|
|
41
|
+
document.body.appendChild(a);
|
|
42
|
+
a.click();
|
|
43
|
+
a.remove();
|
|
44
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
45
|
+
}
|
|
46
|
+
function resolveFormat(format) {
|
|
47
|
+
return MIME_TYPES[format] ? format : "png";
|
|
48
|
+
}
|
|
49
|
+
function sanitizeFilename(name) {
|
|
50
|
+
return (name ?? "canvas").replace(/[^A-Za-z0-9._-]/g, "_") || "canvas";
|
|
51
|
+
}
|
|
52
|
+
export async function renderCanvasSnapshot(snapshot, options = {}) {
|
|
53
|
+
const format = resolveFormat(options.format);
|
|
54
|
+
const quality = options.quality ?? DEFAULT_EXPORT_OPTIONS.quality;
|
|
55
|
+
const scale = options.scale ?? DEFAULT_EXPORT_OPTIONS.scale;
|
|
56
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[orio-canvas] invalid export option: scale must be a finite number > 0 (got ${scale})`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (format !== "png" && options.quality !== void 0 && (!Number.isFinite(quality) || quality < 0 || quality > 1)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[orio-canvas] invalid export option: quality must be a finite number in [0, 1] (got ${quality})`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const mime = MIME_TYPES[format];
|
|
67
|
+
const ext = format === "jpeg" ? "jpg" : format;
|
|
68
|
+
const filename = `${sanitizeFilename(options.filename)}.${ext}`;
|
|
69
|
+
const out = document.createElement("canvas");
|
|
70
|
+
const w = Math.max(1, Math.round(snapshot.width * scale));
|
|
71
|
+
const h = Math.max(1, Math.round(snapshot.height * scale));
|
|
72
|
+
out.width = w;
|
|
73
|
+
out.height = h;
|
|
74
|
+
const c = out.getContext("2d");
|
|
75
|
+
if (!c) {
|
|
76
|
+
throw new Error("[orio-canvas] failed to acquire 2D context for export");
|
|
77
|
+
}
|
|
78
|
+
c.scale(scale, scale);
|
|
79
|
+
if (format === "jpeg") {
|
|
80
|
+
c.fillStyle = "#ffffff";
|
|
81
|
+
c.fillRect(0, 0, snapshot.width, snapshot.height);
|
|
82
|
+
}
|
|
83
|
+
renderNodesTo(c, snapshot.nodes, snapshot.tools);
|
|
84
|
+
const encoderQuality = format === "png" ? void 0 : quality;
|
|
85
|
+
const blob = await new Promise((resolve, reject) => {
|
|
86
|
+
out.toBlob(
|
|
87
|
+
(b) => b ? resolve(b) : reject(new Error("[orio-canvas] toBlob returned null")),
|
|
88
|
+
mime,
|
|
89
|
+
encoderQuality
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
const dataURL = out.toDataURL(mime, encoderQuality);
|
|
93
|
+
const file = new File([blob], filename, { type: mime });
|
|
94
|
+
return { blob, dataURL, file, format, width: w, height: h, filename };
|
|
95
|
+
}
|
|
96
|
+
export async function performExport(snapshot, options = {}) {
|
|
97
|
+
const result = await renderCanvasSnapshot(snapshot, options);
|
|
98
|
+
if (options.onExport) {
|
|
99
|
+
await options.onExport(result);
|
|
100
|
+
}
|
|
101
|
+
const shouldDownload = options.download ?? !options.onExport;
|
|
102
|
+
if (shouldDownload) {
|
|
103
|
+
downloadBlob(result.blob, result.filename);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
export function exportTool(options = {}) {
|
|
108
|
+
return defineCanvasTool({
|
|
109
|
+
id: "export",
|
|
110
|
+
label: "Export",
|
|
111
|
+
icon: "export",
|
|
112
|
+
kind: "action",
|
|
113
|
+
tooltip: ExportTooltip,
|
|
114
|
+
defaultOptions: {
|
|
115
|
+
...DEFAULT_EXPORT_OPTIONS,
|
|
116
|
+
...options
|
|
117
|
+
},
|
|
118
|
+
disabled(api) {
|
|
119
|
+
return api.nodes.value.length === 0;
|
|
120
|
+
},
|
|
121
|
+
action(api) {
|
|
122
|
+
const { width, height } = api.size();
|
|
123
|
+
performExport(
|
|
124
|
+
{
|
|
125
|
+
nodes: api.nodes.value,
|
|
126
|
+
tools: api.getTools(),
|
|
127
|
+
width,
|
|
128
|
+
height
|
|
129
|
+
},
|
|
130
|
+
api.options
|
|
131
|
+
).catch((err) => {
|
|
132
|
+
console.error("[orio-canvas] export failed", err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface HighlightToolOptions extends Record<string, unknown> {
|
|
2
|
+
/** Hit-test radius in CSS pixels. */
|
|
3
|
+
radius: number;
|
|
4
|
+
/** Highlight stroke color. */
|
|
5
|
+
strokeColor: string;
|
|
6
|
+
/** Highlight fill color (transparent). */
|
|
7
|
+
fillColor: string;
|
|
8
|
+
/** Highlight stroke width. */
|
|
9
|
+
lineWidth: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function highlightTool(options?: Partial<HighlightToolOptions>): import("../types.js").CanvasTool<never, HighlightToolOptions>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineCanvasTool } from "../types.js";
|
|
2
|
+
import { findTopNode, renderHighlightRect } from "./hitTest.js";
|
|
3
|
+
import HighlightTooltip from "./tooltips/Highlight.vue";
|
|
4
|
+
export function highlightTool(options = {}) {
|
|
5
|
+
let highlightedNodeId = null;
|
|
6
|
+
return defineCanvasTool({
|
|
7
|
+
id: "highlight",
|
|
8
|
+
label: "Highlight",
|
|
9
|
+
icon: "highlight",
|
|
10
|
+
cursor: "default",
|
|
11
|
+
tooltip: HighlightTooltip,
|
|
12
|
+
defaultOptions: {
|
|
13
|
+
radius: 10,
|
|
14
|
+
strokeColor: "rgba(31, 122, 236, 0.8)",
|
|
15
|
+
fillColor: "rgba(31, 122, 236, 0.08)",
|
|
16
|
+
lineWidth: 2,
|
|
17
|
+
...options
|
|
18
|
+
},
|
|
19
|
+
onPointerMove(e, api) {
|
|
20
|
+
const node = findTopNode(e, api, api.options.radius);
|
|
21
|
+
const newId = node?.id ?? null;
|
|
22
|
+
if (newId !== highlightedNodeId) {
|
|
23
|
+
highlightedNodeId = newId;
|
|
24
|
+
api.requestRender();
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
onPointerDown(e, api) {
|
|
28
|
+
const node = findTopNode(e, api, api.options.radius);
|
|
29
|
+
highlightedNodeId = node?.id ?? null;
|
|
30
|
+
},
|
|
31
|
+
onDeactivate(api) {
|
|
32
|
+
if (highlightedNodeId) {
|
|
33
|
+
highlightedNodeId = null;
|
|
34
|
+
api.requestRender();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
renderOverlay(ctx, api) {
|
|
38
|
+
if (!highlightedNodeId) return;
|
|
39
|
+
const node = api.getNode(highlightedNodeId);
|
|
40
|
+
if (!node) {
|
|
41
|
+
highlightedNodeId = null;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
renderHighlightRect(ctx, node, {
|
|
45
|
+
strokeColor: api.options.strokeColor,
|
|
46
|
+
fillColor: api.options.fillColor,
|
|
47
|
+
lineWidth: api.options.lineWidth
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CanvasNode, CanvasPoint, CanvasTool, CanvasToolApi } from "../types.js";
|
|
2
|
+
export declare function defaultHitTest(node: CanvasNode, point: CanvasPoint, radius: number): boolean;
|
|
3
|
+
/** Transform a canvas-space point into the node's pre-rotation local space. */
|
|
4
|
+
export declare function toLocalPoint(node: CanvasNode, point: CanvasPoint): CanvasPoint;
|
|
5
|
+
export declare function hitTestNode(node: CanvasNode, point: CanvasPoint, radius: number, toolMap: Map<string, CanvasTool>): boolean;
|
|
6
|
+
export declare function findTopNode(point: CanvasPoint, api: CanvasToolApi, radius: number): CanvasNode | undefined;
|
|
7
|
+
/** Compute an axis-aligned bounding box for a node. */
|
|
8
|
+
export declare function getNodeBounds(node: CanvasNode): {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
export interface HighlightStyle {
|
|
15
|
+
strokeColor: string;
|
|
16
|
+
fillColor: string;
|
|
17
|
+
lineWidth: number;
|
|
18
|
+
}
|
|
19
|
+
/** Draw a dashed bounding-rect highlight around a node. */
|
|
20
|
+
export declare function renderHighlightRect(ctx: CanvasRenderingContext2D, node: CanvasNode, style?: Partial<HighlightStyle>): void;
|