@gradio/imageeditor 0.1.0

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.
@@ -0,0 +1,247 @@
1
+ import {
2
+ Container,
3
+ Graphics,
4
+ Color,
5
+ type ColorSource,
6
+ type IRenderer,
7
+ RenderTexture,
8
+ Sprite
9
+ } from "pixi.js";
10
+
11
+ import type { LayerScene } from "../layers/utils";
12
+ import { type Command } from "../utils/commands";
13
+ import { make_graphics } from "../utils/pixi";
14
+
15
+ export interface DrawCommand extends Command {
16
+ /**
17
+ * sets the initial state to begin drawing
18
+ * @param options the options for drawing
19
+ * @returns
20
+ */
21
+ start: (options: DrawOptions) => void;
22
+ /**
23
+ * continues drawing, smoothly interpolating between points where necessary
24
+ * @param options the options for drawing
25
+ * @returns
26
+ */
27
+ continue: (options: Points) => void;
28
+ /**
29
+ * stops drawing
30
+ * @returns
31
+ */
32
+ stop: () => void;
33
+ /**
34
+ * Whether or not the user is currently drawing.
35
+ */
36
+ drawing: boolean;
37
+ }
38
+
39
+ /**
40
+ * Coordinates for a draw or erase path.
41
+ */
42
+ interface Points {
43
+ /**
44
+ * The x coordinate of the path.
45
+ */
46
+ x: number;
47
+ /**
48
+ * The y coordinate of the path.
49
+ */
50
+ y: number;
51
+ }
52
+
53
+ /**
54
+ * Options for drawing a path.
55
+ */
56
+ interface DrawOptions extends Points {
57
+ /**
58
+ * The size of the brush.
59
+ */
60
+ size: number;
61
+ /**
62
+ * The color of the brush.
63
+ */
64
+ color?: ColorSource;
65
+ /**
66
+ * The opacity of the brush.
67
+ */
68
+ opacity: number;
69
+ /**
70
+ * Whether or not to set the initial texture.
71
+ */
72
+ set_initial_texture?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Draws a circle on the given graphics object.
77
+ * @param graphics the graphics object to draw on
78
+ * @param x the x coordinate of the circle
79
+ * @param y the y coordinate of the circle
80
+ * @param brush_color the color of the circle
81
+ * @param brush_size the radius of the circle
82
+ */
83
+ function drawCircle(
84
+ graphics: Graphics,
85
+ x: number,
86
+ y: number,
87
+ brush_color: ColorSource = new Color("black"),
88
+ brush_size: number
89
+ ): void {
90
+ const color = new Color(brush_color);
91
+ graphics.beginFill(color);
92
+ graphics.drawCircle(x, y, brush_size);
93
+ graphics.endFill();
94
+ }
95
+
96
+ /**
97
+ * Interpolates between two points.
98
+ * @param point1 the first point
99
+ * @param point2 the second point
100
+ * @returns an array of points between the two points
101
+ */
102
+ function interpolate(
103
+ point1: { x: number; y: number },
104
+ point2: { x: number; y: number }
105
+ ): Points[] {
106
+ let points: Points[] = [];
107
+ const dx = point2.x - point1.x;
108
+ const dy = point2.y - point1.y;
109
+ const distance = Math.sqrt(dx * dx + dy * dy);
110
+ const steps = Math.ceil(distance / 2);
111
+ const stepX = dx / steps;
112
+ const stepY = dy / steps;
113
+
114
+ for (let j = 0; j < steps; j++) {
115
+ const x = point1.x + j * stepX;
116
+ const y = point1.y + j * stepY;
117
+ points.push({ x, y });
118
+ }
119
+
120
+ return points;
121
+ }
122
+
123
+ export function draw_path(
124
+ renderer: IRenderer,
125
+ stage: Container,
126
+ layer: LayerScene,
127
+ mode: "draw" | "erase"
128
+ ): DrawCommand {
129
+ const paths: Points[] = [];
130
+ let initial_path: DrawOptions;
131
+ let graphics: Graphics;
132
+ let InitialTexture: RenderTexture;
133
+
134
+ let has_drawn = false;
135
+ let id = 0;
136
+
137
+ return {
138
+ drawing: false,
139
+ start: function ({
140
+ x,
141
+ y,
142
+ size,
143
+ color = new Color("black"),
144
+ opacity,
145
+ set_initial_texture = true
146
+ }: DrawOptions) {
147
+ if (set_initial_texture) {
148
+ InitialTexture = RenderTexture.create({
149
+ width: layer.draw_texture.width,
150
+ height: layer.draw_texture.height
151
+ });
152
+ renderer.render(layer.composite, {
153
+ renderTexture: InitialTexture
154
+ });
155
+ }
156
+ initial_path = { x, y, size, color, opacity };
157
+ paths.push({ x, y });
158
+ graphics = make_graphics(id++);
159
+
160
+ drawCircle(graphics, x, y, color, size);
161
+
162
+ renderer.render(graphics, {
163
+ renderTexture:
164
+ mode === "draw" ? layer.draw_texture : layer.erase_texture,
165
+ clear: false
166
+ });
167
+
168
+ this.drawing = true;
169
+ },
170
+ continue: function ({ x, y }: Points) {
171
+ const last_point = paths[paths.length - 1];
172
+ const new_points = interpolate(last_point, { x, y });
173
+
174
+ for (let i = 0; i < new_points.length; i++) {
175
+ const { x, y } = new_points[i];
176
+ drawCircle(graphics, x, y, initial_path.color, initial_path.size);
177
+ paths.push({ x, y });
178
+ }
179
+
180
+ renderer.render(graphics, {
181
+ renderTexture:
182
+ mode === "draw" ? layer.draw_texture : layer.erase_texture,
183
+ clear: false
184
+ });
185
+ graphics.clear();
186
+ },
187
+ stop: function () {
188
+ const current_sketch = RenderTexture.create({
189
+ width: layer.draw_texture.width,
190
+ height: layer.draw_texture.height
191
+ });
192
+
193
+ renderer.render(layer.composite, {
194
+ renderTexture: current_sketch
195
+ });
196
+
197
+ renderer.render(new Sprite(current_sketch), {
198
+ renderTexture: layer.draw_texture
199
+ });
200
+
201
+ const clear_graphics = new Graphics()
202
+ .beginFill(0x000000, 0) // Use a fill color with 0 alpha for transparency
203
+ .drawRect(0, 0, layer.erase_texture.width, layer.erase_texture.height)
204
+ .endFill();
205
+
206
+ renderer.render(clear_graphics, {
207
+ renderTexture: layer.erase_texture,
208
+ clear: true
209
+ });
210
+
211
+ has_drawn = true;
212
+ this.drawing = false;
213
+ },
214
+ execute: function () {
215
+ if (!has_drawn) {
216
+ for (let i = 1; i < paths.length; i++) {
217
+ const { x, y } = paths[i];
218
+ drawCircle(graphics, x, y, initial_path.color, initial_path.size);
219
+ }
220
+
221
+ renderer.render(graphics, {
222
+ renderTexture:
223
+ mode === "draw" ? layer.draw_texture : layer.erase_texture,
224
+ clear: false
225
+ });
226
+
227
+ this.stop!();
228
+ }
229
+ },
230
+ undo: function () {
231
+ const clear_graphics = new Graphics()
232
+ .beginFill(0x000000, 0)
233
+ .drawRect(0, 0, layer.erase_texture.width, layer.erase_texture.height)
234
+ .endFill();
235
+ renderer.render(new Sprite(InitialTexture), {
236
+ renderTexture: layer.draw_texture
237
+ });
238
+ renderer.render(clear_graphics, {
239
+ renderTexture: layer.erase_texture,
240
+ clear: true
241
+ });
242
+
243
+ this.stop!();
244
+ has_drawn = false;
245
+ }
246
+ };
247
+ }
@@ -0,0 +1,210 @@
1
+ import {
2
+ Graphics,
3
+ type IRenderer,
4
+ type Container,
5
+ Sprite,
6
+ RenderTexture
7
+ } from "pixi.js";
8
+ import { type Command } from "../utils/commands";
9
+ import { spring } from "svelte/motion";
10
+ import type { Writable } from "svelte/store";
11
+
12
+ export interface CropCommand extends Command {
13
+ start: (
14
+ width: number,
15
+ height: number,
16
+ previous_crop: [number, number, number, number],
17
+ set_previous?: boolean
18
+ ) => void;
19
+ stop: () => number;
20
+ continue: (crop_size: [number, number, number, number]) => void;
21
+ }
22
+
23
+ export function crop_canvas(
24
+ renderer: IRenderer,
25
+ mask_container: Container,
26
+ crop: Writable<[number, number, number, number]>,
27
+ current_opacity = 0
28
+ ): CropCommand {
29
+ let text: RenderTexture;
30
+ let sprite: Sprite;
31
+ const mask_graphics = new Graphics();
32
+ let previous_crop: [number, number, number, number];
33
+ let final_crop: [number, number, number, number];
34
+ let width: number;
35
+ let height: number;
36
+ let alpha_spring = spring(current_opacity, {
37
+ stiffness: 0.1,
38
+ damping: 0.5
39
+ });
40
+
41
+ let spring_value = current_opacity;
42
+ alpha_spring.subscribe((value) => {
43
+ if (!final_crop) return;
44
+ spring_value = value;
45
+ crop_mask(width, height, final_crop, true);
46
+ });
47
+
48
+ function crop_mask(
49
+ width: number,
50
+ height: number,
51
+ crop_size: [number, number, number, number],
52
+ preview: boolean
53
+ ): void {
54
+ mask_graphics.clear();
55
+ if (preview) {
56
+ mask_graphics.beginFill(0xffffff, spring_value);
57
+ mask_graphics.drawRect(0, 0, width, height);
58
+ mask_graphics.endFill();
59
+ }
60
+ mask_graphics.beginFill(0xffffff, 1);
61
+ mask_graphics.drawRect(...crop_size);
62
+ mask_graphics.endFill();
63
+
64
+ renderer.render(mask_graphics, {
65
+ renderTexture: text
66
+ });
67
+ }
68
+
69
+ let clean = true;
70
+ let stopped = false;
71
+
72
+ return {
73
+ start: (
74
+ _width: number,
75
+ _height: number,
76
+ _previous_crop: [number, number, number, number],
77
+ set_previous = true
78
+ ) => {
79
+ clean = false;
80
+ text = RenderTexture.create({
81
+ width: _width,
82
+ height: _height
83
+ });
84
+
85
+ crop_mask(_width, _height, _previous_crop, true);
86
+ sprite = new Sprite(text);
87
+ mask_container.mask = sprite;
88
+ width = _width;
89
+ height = _height;
90
+ if (set_previous)
91
+ previous_crop = JSON.parse(JSON.stringify(_previous_crop));
92
+ },
93
+ continue: (crop_size: [number, number, number, number]) => {
94
+ final_crop = JSON.parse(JSON.stringify(crop_size));
95
+ if (spring_value === 0.2) {
96
+ crop_mask(width, height, final_crop, true);
97
+ } else {
98
+ alpha_spring.set(0.2);
99
+ }
100
+ },
101
+
102
+ undo() {
103
+ this.start(width, height, previous_crop, false);
104
+ crop.set([
105
+ previous_crop[0] / width,
106
+ previous_crop[1] / height,
107
+ previous_crop[2] / width,
108
+ previous_crop[3] / height
109
+ ]);
110
+ clean = true;
111
+ },
112
+ stop() {
113
+ stopped = true;
114
+ return spring_value;
115
+ },
116
+ execute() {
117
+ if (clean) {
118
+ this.start(width, height, final_crop, false);
119
+
120
+ crop.set([
121
+ final_crop[0] / width,
122
+ final_crop[1] / height,
123
+ final_crop[2] / width,
124
+ final_crop[3] / height
125
+ ]);
126
+ clean = true;
127
+ } else {
128
+ if (!stopped) {
129
+ alpha_spring.set(0);
130
+ }
131
+
132
+ crop.set([
133
+ final_crop[0] / width,
134
+ final_crop[1] / height,
135
+ final_crop[2] / width,
136
+ final_crop[3] / height
137
+ ]);
138
+
139
+ clean = true;
140
+ }
141
+ }
142
+ };
143
+ }
144
+
145
+ export function resize_and_reposition(
146
+ original_width: number,
147
+ original_height: number,
148
+ anchor: "t" | "r" | "l" | "b" | "tl" | "tr" | "bl" | "br" | "c",
149
+ aspect_ratio: number,
150
+ max_width: number,
151
+ max_height: number
152
+ ): {
153
+ new_width: number;
154
+ new_height: number;
155
+ x_offset: number;
156
+ y_offset: number;
157
+ } {
158
+ let new_width = original_width;
159
+ let new_height = original_height;
160
+
161
+ // Calculate new dimensions based on aspect ratio
162
+ if (anchor.includes("t") || anchor.includes("b") || anchor == "c") {
163
+ new_width = original_height * aspect_ratio;
164
+ }
165
+ if (anchor.includes("l") || anchor.includes("r") || anchor == "c") {
166
+ new_height = original_width / aspect_ratio;
167
+ }
168
+
169
+ // Ensure aspect ratio is maintained
170
+ new_height = new_height || new_width / aspect_ratio;
171
+ new_width = new_width || new_height * aspect_ratio;
172
+
173
+ // Apply max width/height constraints and adjust dimensions
174
+ if (new_width > max_width) {
175
+ new_width = max_width;
176
+ new_height = new_width / aspect_ratio;
177
+ }
178
+ if (new_height > max_height) {
179
+ new_height = max_height;
180
+ new_width = new_height * aspect_ratio;
181
+ }
182
+
183
+ // Calculate offsets to ensure anchor position is maintained
184
+ let x_offset = 0;
185
+ let y_offset = 0;
186
+ if (anchor.includes("r")) {
187
+ x_offset = original_width - new_width;
188
+ } else if (anchor.includes("l")) {
189
+ x_offset = 0;
190
+ } else {
191
+ // Center or top/bottom center
192
+ x_offset = (original_width - new_width) / 2;
193
+ }
194
+
195
+ if (anchor.includes("b")) {
196
+ y_offset = original_height - new_height;
197
+ } else if (anchor.includes("t")) {
198
+ y_offset = 0;
199
+ } else {
200
+ // Center or left/right center
201
+ y_offset = (original_height - new_height) / 2;
202
+ }
203
+
204
+ return {
205
+ new_width: new_width,
206
+ new_height: new_height,
207
+ x_offset: x_offset,
208
+ y_offset: y_offset
209
+ };
210
+ }
@@ -0,0 +1,5 @@
1
+ export { default as Tools } from "./Tools.svelte";
2
+ export { default as Brush } from "./Brush.svelte";
3
+ export { default as Crop } from "./Crop.svelte";
4
+ export { default as Sources } from "./Sources.svelte";
5
+ export * from "./types";
@@ -0,0 +1,114 @@
1
+ import {
2
+ type Container,
3
+ type IRenderer,
4
+ Sprite,
5
+ Texture,
6
+ DisplayObject,
7
+ type ColorSource,
8
+ Color,
9
+ RenderTexture
10
+ } from "pixi.js";
11
+
12
+ import { type Command } from "../utils/commands";
13
+ import { make_graphics } from "../utils/pixi";
14
+
15
+ interface BgImageCommand extends Command {
16
+ /**
17
+ * Initial setup for the bg command
18
+ * @returns
19
+ */
20
+ start: () => Promise<[number, number]>;
21
+ }
22
+
23
+ /**
24
+ * Adds a background image to the canvas.
25
+ * @param container The container to add the image to.
26
+ * @param renderer The renderer to use for the image.
27
+ * @param background The background image to add.
28
+ * @param resize The function to resize the canvas.
29
+ * @returns A command that can be used to undo the action.
30
+ */
31
+
32
+ export function add_bg_image(
33
+ container: Container,
34
+ renderer: IRenderer,
35
+ background: Blob | File,
36
+ resize: (width: number, height: number) => void
37
+ ): BgImageCommand {
38
+ let sprite: Sprite & DisplayObject;
39
+ return {
40
+ async start() {
41
+ container.removeChildren();
42
+ const img = await createImageBitmap(background);
43
+ const bitmap_texture = Texture.from(img);
44
+ sprite = new Sprite(bitmap_texture) as Sprite & DisplayObject;
45
+ return [sprite.width, sprite.height];
46
+ },
47
+ async execute() {
48
+ // renderer.resize(sprite.width, sprite.height);
49
+ resize(sprite.width, sprite.height);
50
+
51
+ sprite.zIndex = 0;
52
+ container.addChild(sprite);
53
+ },
54
+ undo() {
55
+ container.removeChildAt(0);
56
+ }
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Command that sets a background
62
+ */
63
+ interface BgColorCommand extends Command {
64
+ /**
65
+ * Initial setup for the bg command
66
+ * @returns
67
+ */
68
+ start: () => [number, number];
69
+ }
70
+
71
+ /**
72
+ * Adds a background color to the canvas.
73
+ * @param container The container to add the image to.
74
+ * @param renderer The renderer to use for the image.
75
+ * @param color The background color to add.
76
+ * @param width The width of the background.
77
+ * @param height The height of the background.
78
+ * @param resize The function to resize the canvas.
79
+ * @returns A command that can be used to undo the action.
80
+ */
81
+ export function add_bg_color(
82
+ container: Container,
83
+ renderer: IRenderer,
84
+ color: ColorSource,
85
+ width: number,
86
+ height: number,
87
+ resize: (width: number, height: number) => void
88
+ ): BgColorCommand {
89
+ let sprite: Sprite & DisplayObject;
90
+ return {
91
+ start() {
92
+ container.removeChildren();
93
+ const graphics = make_graphics(1);
94
+ const texture = RenderTexture.create({
95
+ width,
96
+ height
97
+ });
98
+ graphics.beginFill(new Color(color));
99
+ graphics.drawRect(0, 0, width, height);
100
+ graphics.endFill();
101
+ renderer.render(graphics, { renderTexture: texture });
102
+ sprite = new Sprite(texture) as Sprite & DisplayObject;
103
+ return [sprite.width, sprite.height];
104
+ },
105
+ async execute() {
106
+ resize(sprite.width, sprite.height);
107
+ sprite.zIndex = 1;
108
+ container.addChild(sprite);
109
+ },
110
+ undo() {
111
+ container.removeChildren();
112
+ }
113
+ };
114
+ }
@@ -0,0 +1 @@
1
+ export type tool = "draw" | "erase" | "crop" | "bg";