Skip to content

Commit fc2b8d3

Browse files
committed
Implement Patterns
Signed-off-by: martinRenou <[email protected]>
1 parent fe3663a commit fc2b8d3

File tree

4 files changed

+299
-23
lines changed

4 files changed

+299
-23
lines changed

examples/Patterns.ipynb

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Patterns\n",
8+
"\n",
9+
"## Create pattern from an Image object"
10+
]
11+
},
12+
{
13+
"cell_type": "code",
14+
"execution_count": null,
15+
"metadata": {},
16+
"outputs": [],
17+
"source": [
18+
"from ipycanvas import Canvas\n",
19+
"from ipywidgets import Image"
20+
]
21+
},
22+
{
23+
"cell_type": "markdown",
24+
"metadata": {},
25+
"source": [
26+
"#### The pattern source is a small image that will get repeated on the Canvas"
27+
]
28+
},
29+
{
30+
"cell_type": "code",
31+
"execution_count": null,
32+
"metadata": {},
33+
"outputs": [],
34+
"source": [
35+
"Image.from_file('pattern.png')"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": null,
41+
"metadata": {},
42+
"outputs": [],
43+
"source": [
44+
"canvas = Canvas()\n",
45+
"pattern = canvas.create_pattern(Image.from_file('pattern.png'))\n",
46+
"canvas.fill_style = pattern\n",
47+
"canvas.fill_rect(0, 0, canvas.width, canvas.height)\n",
48+
"canvas"
49+
]
50+
},
51+
{
52+
"cell_type": "markdown",
53+
"metadata": {},
54+
"source": [
55+
"## Create pattern from another Canvas"
56+
]
57+
},
58+
{
59+
"cell_type": "code",
60+
"execution_count": null,
61+
"metadata": {},
62+
"outputs": [],
63+
"source": [
64+
"from math import pi\n",
65+
"\n",
66+
"from ipycanvas import Canvas"
67+
]
68+
},
69+
{
70+
"cell_type": "code",
71+
"execution_count": null,
72+
"metadata": {},
73+
"outputs": [],
74+
"source": [
75+
"pattern_source = Canvas(width=50, height=50)\n",
76+
"\n",
77+
"pattern_source.fill_style = '#fec'\n",
78+
"pattern_source.fill_rect(0, 0, 50, 50)\n",
79+
"pattern_source.stroke_arc(0, 0, 50, 0, .5 * pi)\n",
80+
"\n",
81+
"pattern_source"
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"metadata": {},
88+
"outputs": [],
89+
"source": [
90+
"canvas2 = Canvas()\n",
91+
"pattern1 = canvas2.create_pattern(pattern_source)\n",
92+
"canvas2.fill_style = pattern1\n",
93+
"canvas2.fill_rect(0, 0, canvas2.width, canvas2.height)\n",
94+
"canvas2"
95+
]
96+
},
97+
{
98+
"cell_type": "code",
99+
"execution_count": null,
100+
"metadata": {},
101+
"outputs": [],
102+
"source": [
103+
"pattern_source = Canvas(width=50, height=50)\n",
104+
"\n",
105+
"pattern_source.fill_style = '#338ac4'\n",
106+
"pattern_source.fill_rect(0, 0, 50, 50)\n",
107+
"\n",
108+
"pattern_source.fill_style = '#3341c4'\n",
109+
"pattern_source.fill_circle(50, 50, 5)\n",
110+
"pattern_source.fill_circle(0, 0, 5)\n",
111+
"pattern_source.fill_circle(50, 0, 5)\n",
112+
"pattern_source.fill_circle(0, 50, 5)\n",
113+
"\n",
114+
"pattern_source.fill_style = '#33c4b5'\n",
115+
"pattern_source.fill_circle(25, 25, 10)\n",
116+
"pattern_source"
117+
]
118+
},
119+
{
120+
"cell_type": "code",
121+
"execution_count": null,
122+
"metadata": {},
123+
"outputs": [],
124+
"source": [
125+
"canvas = Canvas()\n",
126+
"pattern2 = canvas.create_pattern(pattern_source)\n",
127+
"canvas.fill_style = pattern2\n",
128+
"canvas.fill_rect(0, 0, canvas.width, canvas.height)\n",
129+
"canvas"
130+
]
131+
},
132+
{
133+
"cell_type": "markdown",
134+
"metadata": {},
135+
"source": [
136+
"#### You can even combine patterns"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"canvas = Canvas()\n",
146+
"\n",
147+
"canvas.fill_style = pattern1\n",
148+
"canvas.fill_rect(0, 0, canvas.width, canvas.height)\n",
149+
"\n",
150+
"canvas.fill_style = pattern2\n",
151+
"canvas.fill_circle(canvas.width / 2., canvas.height / 2., 100)\n",
152+
"\n",
153+
"canvas"
154+
]
155+
}
156+
],
157+
"metadata": {
158+
"kernelspec": {
159+
"display_name": "Python 3",
160+
"language": "python",
161+
"name": "python3"
162+
},
163+
"language_info": {
164+
"codemirror_mode": {
165+
"name": "ipython",
166+
"version": 3
167+
},
168+
"file_extension": ".py",
169+
"mimetype": "text/x-python",
170+
"name": "python",
171+
"nbconvert_exporter": "python",
172+
"pygments_lexer": "ipython3",
173+
"version": "3.9.0"
174+
}
175+
},
176+
"nbformat": 4,
177+
"nbformat_minor": 4
178+
}

examples/pattern.png

217 Bytes
Loading

ipycanvas/canvas.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,33 @@ def __init__(self, value):
7474
super(Path2D, self).__init__()
7575

7676

77+
class Pattern(Widget):
78+
"""Create a Pattern.
79+
80+
Args:
81+
image (Canvas or MultiCanvas or ipywidgets.Image): The source to be used as the pattern's image
82+
repetition (str): A string indicating how to repeat the pattern's image, can be "repeat" (both directions), "repeat-x" (horizontal only), "repeat-y" (vertical only), "no-repeat" (neither direction)
83+
"""
84+
85+
_model_module = Unicode(module_name).tag(sync=True)
86+
_model_module_version = Unicode(module_version).tag(sync=True)
87+
88+
_model_name = Unicode('PatternModel').tag(sync=True)
89+
90+
image = Union((Instance(Image), Instance('ipycanvas.Canvas'), Instance('ipycanvas.MultiCanvas')), allow_none=False, read_only=True).tag(sync=True, **widget_serialization)
91+
repetition = Enum(['repeat', 'repeat-x', 'repeat-y', 'no-repeat'], allow_none=False, read_only=True).tag(sync=True)
92+
93+
def __init__(self, image, repetition='repeat'):
94+
"""Create a Pattern object given the image and the type of repetition."""
95+
self.set_trait('image', image)
96+
self.set_trait('repetition', repetition)
97+
98+
super(Pattern, self).__init__()
99+
100+
def _ipython_display_(self, *args, **kwargs):
101+
return self.image._ipython_display_(*args, **kwargs)
102+
103+
77104
class _CanvasGradient(Widget):
78105
_model_module = Unicode(module_name).tag(sync=True)
79106
_model_module_version = Unicode(module_version).tag(sync=True)
@@ -226,11 +253,11 @@ class Canvas(_CanvasBase):
226253
_model_name = Unicode('CanvasModel').tag(sync=True)
227254
_view_name = Unicode('CanvasView').tag(sync=True)
228255

229-
#: (valid HTML color) The color for filling rectangles and paths. Default to ``'black'``.
230-
fill_style = Union((Color(), Instance(_CanvasGradient)), default_value='black')
256+
#: (valid HTML color or Gradient or Pattern) The color for filling rectangles and paths. Default to ``'black'``.
257+
fill_style = Union((Color(), Instance(_CanvasGradient), Instance(Pattern)), default_value='black')
231258

232-
#: (valid HTML color) The color for rectangles and paths stroke. Default to ``'black'``.
233-
stroke_style = Color('black')
259+
#: (valid HTML color or Gradient or Pattern) The color for rectangles and paths stroke. Default to ``'black'``.
260+
stroke_style = Union((Color(), Instance(_CanvasGradient), Instance(Pattern)), default_value='black')
234261

235262
#: (float) Transparency level. Default to ``1.0``.
236263
global_alpha = Float(1.0)
@@ -373,6 +400,16 @@ def create_radial_gradient(self, x0, y0, r0, x1, y1, r1, color_stops):
373400
"""
374401
return RadialGradient(x0, y0, r0, x1, y1, r1, color_stops)
375402

403+
# Pattern method
404+
def create_pattern(self, image, repetition='repeat'):
405+
"""Create a Pattern.
406+
407+
Args:
408+
image (Canvas or MultiCanvas or ipywidgets.Image): The source to be used as the pattern's image
409+
repetition (str): A string indicating how to repeat the pattern's image, can be "repeat" (both directions), "repeat-x" (horizontal only), "repeat-y" (vertical only), "no-repeat" (neither direction)
410+
"""
411+
return Pattern(image, repetition)
412+
376413
# Rectangles methods
377414
def fill_rect(self, x, y, width, height=None):
378415
"""Draw a filled rectangle of size ``(width, height)`` at the ``(x, y)`` position."""

src/widget.ts

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ function deserializeImageData(dataview: DataView | null) {
3838
return new Uint8ClampedArray(dataview.buffer);
3939
}
4040

41+
async function createImageFromWidget(image: DOMWidgetModel): Promise<HTMLImageElement> {
42+
// Create the image manually instead of creating an ImageView
43+
let url: string;
44+
const format = image.get('format');
45+
const value = image.get('value');
46+
if (format !== 'url') {
47+
const blob = new Blob([value], {type: `image/${format}`});
48+
url = URL.createObjectURL(blob);
49+
} else {
50+
url = (new TextDecoder('utf-8')).decode(value.buffer);
51+
}
52+
53+
const img = new Image();
54+
return new Promise((resolve) => {
55+
img.onload = () => {
56+
resolve(img);
57+
};
58+
img.src = url;
59+
});
60+
}
61+
62+
4163
const COMMANDS = [
4264
'fillRect', 'strokeRect', 'fillRects', 'strokeRects', 'clearRect', 'fillArc',
4365
'fillCircle', 'strokeArc', 'strokeCircle', 'fillArcs', 'strokeArcs',
@@ -76,6 +98,62 @@ class Path2DModel extends WidgetModel {
7698
}
7799

78100

101+
export
102+
class PatternModel extends WidgetModel {
103+
defaults() {
104+
return {...super.defaults(),
105+
_model_name: PatternModel.model_name,
106+
_model_module: PatternModel.model_module,
107+
_model_module_version: PatternModel.model_module_version,
108+
image: '',
109+
repetition: 'repeat',
110+
};
111+
}
112+
113+
async initialize(attributes: any, options: any) {
114+
super.initialize(attributes, options);
115+
116+
const image = this.get('image');
117+
let patternSource: HTMLCanvasElement | HTMLImageElement | undefined = undefined;
118+
119+
if (image instanceof CanvasModel || image instanceof MultiCanvasModel) {
120+
patternSource = image.canvas;
121+
}
122+
123+
if (image.get('_model_name') == 'ImageModel') {
124+
const img = await createImageFromWidget(image);
125+
patternSource = img;
126+
}
127+
128+
if (patternSource == undefined) {
129+
throw "Could not understand the souce for the pattern";
130+
}
131+
132+
const pattern = PatternModel.ctx.createPattern(patternSource, this.get('repetition'));
133+
134+
if (pattern == null) {
135+
throw "Could not initialize pattern object";
136+
}
137+
138+
this.value = pattern;
139+
}
140+
141+
static serializers: ISerializers = {
142+
...WidgetModel.serializers,
143+
image: { deserialize: (unpack_models as any) },
144+
}
145+
146+
value: CanvasPattern;
147+
148+
static model_name = 'PatternModel';
149+
static model_module = MODULE_NAME;
150+
static model_module_version = MODULE_VERSION;
151+
152+
// Global context for creating the gradients
153+
static ctx: CanvasRenderingContext2D = getContext(document.createElement('canvas'));
154+
}
155+
156+
79157
class GradientModel extends WidgetModel {
80158
defaults() {
81159
return {...super.defaults(),
@@ -435,25 +513,8 @@ class CanvasModel extends DOMWidgetModel {
435513
}
436514

437515
if (image.get('_model_name') == 'ImageModel') {
438-
// Create the image manually instead of creating an ImageView
439-
let url: string;
440-
const format = image.get('format');
441-
const value = image.get('value');
442-
if (format !== 'url') {
443-
const blob = new Blob([value], {type: `image/${format}`});
444-
url = URL.createObjectURL(blob);
445-
} else {
446-
url = (new TextDecoder('utf-8')).decode(value.buffer);
447-
}
448-
449-
const img = new Image();
450-
return new Promise((resolve) => {
451-
img.onload = () => {
452-
this._drawImage(img, x, y, width, height);
453-
resolve();
454-
};
455-
img.src = url;
456-
});
516+
const img = await createImageFromWidget(image);
517+
this._drawImage(img, x, y, width, height);
457518
}
458519
}
459520

0 commit comments

Comments
 (0)