ipysketch-lite 0.0.1__py3-none-any.whl
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.
- ipysketch_lite/__init__.py +69 -0
- ipysketch_lite/gen/__init__.py +196 -0
- ipysketch_lite-0.0.1.dist-info/LICENSE +1 -0
- ipysketch_lite-0.0.1.dist-info/METADATA +46 -0
- ipysketch_lite-0.0.1.dist-info/RECORD +7 -0
- ipysketch_lite-0.0.1.dist-info/WHEEL +5 -0
- ipysketch_lite-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
2
|
+
from IPython.display import HTML, display
|
|
3
|
+
from .gen import template
|
|
4
|
+
import threading
|
|
5
|
+
import base64
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from PIL import Image
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
PIL_INSTALLED = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
PIL_INSTALLED = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
18
|
+
def log_message(self, format, *args):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def do_POST(self):
|
|
22
|
+
content_length = int(self.headers["Content-Length"])
|
|
23
|
+
post_data = self.rfile.read(content_length)
|
|
24
|
+
message = post_data.decode("utf-8")
|
|
25
|
+
|
|
26
|
+
global output
|
|
27
|
+
output = message
|
|
28
|
+
|
|
29
|
+
global response_received
|
|
30
|
+
response_received.set()
|
|
31
|
+
|
|
32
|
+
self.send_response(200)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=5000):
|
|
36
|
+
server_address = ("", port)
|
|
37
|
+
httpd = server_class(server_address, handler_class)
|
|
38
|
+
server_thread = threading.Thread(target=httpd.serve_forever)
|
|
39
|
+
server_thread.start()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Sketch:
|
|
43
|
+
def __init__(self, width: int = 400, height: int = 300):
|
|
44
|
+
global response_received
|
|
45
|
+
response_received = threading.Event()
|
|
46
|
+
run()
|
|
47
|
+
|
|
48
|
+
metadata = {
|
|
49
|
+
"{width}": width,
|
|
50
|
+
"{height}": height,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
sketch_template = template
|
|
54
|
+
for key, value in metadata.items():
|
|
55
|
+
sketch_template = sketch_template.replace(key, str(value))
|
|
56
|
+
|
|
57
|
+
display(HTML(sketch_template))
|
|
58
|
+
response_received.wait()
|
|
59
|
+
|
|
60
|
+
def get_output(self) -> str:
|
|
61
|
+
return output
|
|
62
|
+
|
|
63
|
+
def get_output_array(self) -> np.ndarray:
|
|
64
|
+
if PIL_INSTALLED:
|
|
65
|
+
image_data = output.split(",")[1]
|
|
66
|
+
image = Image.open(io.BytesIO(base64.b64decode(image_data)))
|
|
67
|
+
return np.array(image)
|
|
68
|
+
else:
|
|
69
|
+
raise ImportError("PIL (Pillow) and NumPy are required to use this method.")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
template = """<style>
|
|
2
|
+
#toggleFill {
|
|
3
|
+
background-color: #f0f0f0;
|
|
4
|
+
border: 1px solid #999999;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#toggleFill.active {
|
|
8
|
+
background-color: #ffffff;
|
|
9
|
+
border: 1px solid #000000;
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
12
|
+
<canvas
|
|
13
|
+
id="canvas"
|
|
14
|
+
width="{width}"
|
|
15
|
+
height="{height}"
|
|
16
|
+
style="border: 2px solid black"
|
|
17
|
+
></canvas>
|
|
18
|
+
<button id="toggleFill" onclick="toggleFill()">bucket</button>
|
|
19
|
+
<button onclick="clearCanvas()">clear</button>
|
|
20
|
+
<input id="color" type="color" value="#000000" />
|
|
21
|
+
<script>
|
|
22
|
+
var canvas = document.getElementById("canvas");
|
|
23
|
+
var colorInput = document.getElementById("color");
|
|
24
|
+
var toggleFillButton = document.getElementById("toggleFill");
|
|
25
|
+
var ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
26
|
+
ctx.lineWidth = 4;
|
|
27
|
+
ctx.lineJoin = "round";
|
|
28
|
+
var drawing = false;
|
|
29
|
+
var mouse = { x: 0, y: 0 };
|
|
30
|
+
var fillMode = false;
|
|
31
|
+
|
|
32
|
+
function canvasUpload() {
|
|
33
|
+
fetch("http://localhost:5000", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "text/plain",
|
|
37
|
+
},
|
|
38
|
+
body: canvas.toDataURL(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
canvasUpload();
|
|
43
|
+
|
|
44
|
+
function toggleFill() {
|
|
45
|
+
fillMode = !fillMode;
|
|
46
|
+
toggleFillButton.classList.toggle("active", fillMode);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hexToRgb(hex) {
|
|
50
|
+
return {
|
|
51
|
+
r: parseInt(hex.substring(1, 3), 16),
|
|
52
|
+
g: parseInt(hex.substring(3, 5), 16),
|
|
53
|
+
b: parseInt(hex.substring(5, 7), 16),
|
|
54
|
+
a: 255,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
canvas.addEventListener("mousedown", (e) => {
|
|
59
|
+
if (fillMode) {
|
|
60
|
+
floodFill(hexToRgb(colorInput.value), e.offsetX, e.offsetY);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
mouse = { x: e.offsetX, y: e.offsetY };
|
|
65
|
+
drawing = true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
canvas.addEventListener("mousemove", (e) => {
|
|
69
|
+
if (drawing) {
|
|
70
|
+
drawLine(ctx, mouse.x, mouse.y, e.offsetX, e.offsetY);
|
|
71
|
+
mouse = { x: e.offsetX, y: e.offsetY };
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
canvas.addEventListener("mouseup", (e) => {
|
|
76
|
+
if (drawing) {
|
|
77
|
+
drawLine(ctx, mouse.x, mouse.y, e.offsetX, e.offsetY);
|
|
78
|
+
drawing = false;
|
|
79
|
+
canvasUpload();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function drawLine(context, x1, y1, x2, y2) {
|
|
84
|
+
ctx.strokeStyle = colorInput.value;
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.moveTo(x1, y1);
|
|
87
|
+
ctx.lineTo(x2, y2);
|
|
88
|
+
ctx.closePath();
|
|
89
|
+
ctx.stroke();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearCanvas() {
|
|
93
|
+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function floodFill(color, x, y) {
|
|
97
|
+
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
98
|
+
const { width, height, data } = imgData;
|
|
99
|
+
if (pixelColMatch(x, y, color, data, width)) return;
|
|
100
|
+
|
|
101
|
+
const stack = [[x, y]];
|
|
102
|
+
let baseIdx = (width * y + x) * 4;
|
|
103
|
+
const oColor = {
|
|
104
|
+
r: data[baseIdx],
|
|
105
|
+
g: data[baseIdx + 1],
|
|
106
|
+
b: data[baseIdx + 2],
|
|
107
|
+
a: data[baseIdx + 3],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
while (stack.length) {
|
|
111
|
+
var [cx, cy] = stack.pop();
|
|
112
|
+
const move = (dx, dy) => {
|
|
113
|
+
let nx = cx + dx;
|
|
114
|
+
let ny = cy + dy;
|
|
115
|
+
while (
|
|
116
|
+
ny >= 0 &&
|
|
117
|
+
ny < height &&
|
|
118
|
+
pixelColMatch(nx, ny, oColor, data, width)
|
|
119
|
+
) {
|
|
120
|
+
setPixelCol(nx, ny, color, data, width);
|
|
121
|
+
stack.push([nx, ny]);
|
|
122
|
+
ny += dy;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
move(0, 1);
|
|
126
|
+
move(0, -1);
|
|
127
|
+
move(-1, 0);
|
|
128
|
+
move(1, 0);
|
|
129
|
+
}
|
|
130
|
+
ctx.putImageData(imgData, 0, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pixelColMatch(x, y, color, data, width) {
|
|
134
|
+
var baseIdx = (width * y + x) * 4;
|
|
135
|
+
return (
|
|
136
|
+
data[baseIdx] === color.r &&
|
|
137
|
+
data[baseIdx + 1] === color.g &&
|
|
138
|
+
data[baseIdx + 2] === color.b &&
|
|
139
|
+
data[baseIdx + 3] === color.a
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setPixelCol(x, y, color, data, width) {
|
|
144
|
+
var baseIdx = (width * y + x) * 4;
|
|
145
|
+
data[baseIdx] = color.r & 0xff;
|
|
146
|
+
data[baseIdx + 1] = color.g & 0xff;
|
|
147
|
+
data[baseIdx + 2] = color.b & 0xff;
|
|
148
|
+
data[baseIdx + 3] = color.a & 0xff;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var rect = canvas.getBoundingClientRect();
|
|
152
|
+
var offset = { x: rect.left, y: rect.top };
|
|
153
|
+
var touches = [];
|
|
154
|
+
|
|
155
|
+
canvas.addEventListener("touchstart", (e) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
rect = canvas.getBoundingClientRect();
|
|
158
|
+
offset = { x: rect.left, y: rect.top };
|
|
159
|
+
touches = Array.from(e.touches);
|
|
160
|
+
|
|
161
|
+
if (fillMode) {
|
|
162
|
+
var x = Math.floor(touches[0].clientX - offset.x);
|
|
163
|
+
var y = Math.floor(touches[0].clientY - offset.y);
|
|
164
|
+
floodFill(hexToRgb(colorInput.value), x, y);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
canvas.addEventListener("touchend", (e) => {
|
|
169
|
+
canvasUpload();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
canvas.addEventListener("touchmove", (e) => {
|
|
173
|
+
if (fillMode) return;
|
|
174
|
+
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
rect = canvas.getBoundingClientRect();
|
|
177
|
+
offset = { x: rect.left, y: rect.top };
|
|
178
|
+
for (var i = 0; i < e.changedTouches.length; i++) {
|
|
179
|
+
var touch = e.changedTouches[i];
|
|
180
|
+
var previousTouch = touches.find(
|
|
181
|
+
(t) => t.identifier === touch.identifier,
|
|
182
|
+
);
|
|
183
|
+
if (previousTouch) {
|
|
184
|
+
drawLine(
|
|
185
|
+
ctx,
|
|
186
|
+
previousTouch.clientX - offset.x,
|
|
187
|
+
previousTouch.clientY - offset.y,
|
|
188
|
+
touch.clientX - offset.x,
|
|
189
|
+
touch.clientY - offset.y,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
touches.splice(i, 1, touch);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
</script>
|
|
196
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright (c) 2024 Matthew Andre Taylor
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ipysketch-lite
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Simple sketching tool for jupyter notebooks
|
|
5
|
+
Author-email: Matthew Taylor <matthew.taylor.andre@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/MatthewAndreTaylor/ipysketch_lite
|
|
7
|
+
Keywords: jupyter,sketch,drawing,canvas
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Provides-Extra: utils
|
|
11
|
+
Requires-Dist: numpy ; extra == 'utils'
|
|
12
|
+
Requires-Dist: pillow ; extra == 'utils'
|
|
13
|
+
|
|
14
|
+
# ipysketch_lite
|
|
15
|
+
|
|
16
|
+
A lite sketching utility for python notebooks, no sockets or extra dependencies 🎨
|
|
17
|
+
|
|
18
|
+
(no extra widget code)
|
|
19
|
+
|
|
20
|
+
Make sketches right in your notebook then use the sketch in your python code.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
Start drawing a quick sketch in your notebook like this
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
from ipysketch_lite import Sketch
|
|
28
|
+
|
|
29
|
+
sketch = Sketch()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then add a new cell to retrieve the sketch in python
|
|
33
|
+
|
|
34
|
+
```py
|
|
35
|
+
print(sketch.get_output())
|
|
36
|
+
|
|
37
|
+
import matplotlib.pyplot as plt
|
|
38
|
+
|
|
39
|
+
plt.imshow(sketch.get_output_array())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
Sketches get updated in cells after draw updates
|
|
45
|
+
|
|
46
|
+
This means you can continue your sketch and get the new updated outputs
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ipysketch_lite/__init__.py,sha256=-8lSU7nAsGHEhPlo1Civ8__miQjT7ZQ8kdpj9NlVskM,1876
|
|
2
|
+
ipysketch_lite/gen/__init__.py,sha256=3J5KuxXfM5WpeiTnFrEFmGq_vMmSAWMWFnK47nXD2mk,4967
|
|
3
|
+
ipysketch_lite-0.0.1.dist-info/LICENSE,sha256=-guCcX4glX_hMB9LpAtB_If_h0xpXsm0wvc55655x6M,39
|
|
4
|
+
ipysketch_lite-0.0.1.dist-info/METADATA,sha256=yQLg9Wesap4-G-8DAoLOxY1DrvfHZHtOiWNI7VK8m1M,1134
|
|
5
|
+
ipysketch_lite-0.0.1.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
|
6
|
+
ipysketch_lite-0.0.1.dist-info/top_level.txt,sha256=IGsNv82_nxTmStOol0QKT3whl6kwu6czDb-yOUCkW14,15
|
|
7
|
+
ipysketch_lite-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ipysketch_lite
|