├── Intertwined ├── __init__.py ├── Thread.py ├── Segment.py └── Canvas.py ├── Examples ├── braid.gif ├── braid.png ├── knots.png ├── node.png ├── crochet.png ├── vd_cp1.png ├── vd_cp2.png ├── vd_cp1_cp2.png ├── vd_cp_none.png ├── vd_draw_cp.png ├── vd_draw_all.png ├── vd_draw_depth.png ├── vd_draw_grid.png ├── vd_draw_points.png ├── vd_stretch_grid.png ├── crochet.py ├── node.py ├── braid.py ├── visualdoc.py └── knots.py ├── LICENSE ├── .gitignore └── README.md /Intertwined/__init__.py: -------------------------------------------------------------------------------- 1 | from .Canvas import * 2 | 3 | -------------------------------------------------------------------------------- /Examples/braid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/braid.gif -------------------------------------------------------------------------------- /Examples/braid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/braid.png -------------------------------------------------------------------------------- /Examples/knots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/knots.png -------------------------------------------------------------------------------- /Examples/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/node.png -------------------------------------------------------------------------------- /Examples/crochet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/crochet.png -------------------------------------------------------------------------------- /Examples/vd_cp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_cp1.png -------------------------------------------------------------------------------- /Examples/vd_cp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_cp2.png -------------------------------------------------------------------------------- /Examples/vd_cp1_cp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_cp1_cp2.png -------------------------------------------------------------------------------- /Examples/vd_cp_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_cp_none.png -------------------------------------------------------------------------------- /Examples/vd_draw_cp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_draw_cp.png -------------------------------------------------------------------------------- /Examples/vd_draw_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_draw_all.png -------------------------------------------------------------------------------- /Examples/vd_draw_depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_draw_depth.png -------------------------------------------------------------------------------- /Examples/vd_draw_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_draw_grid.png -------------------------------------------------------------------------------- /Examples/vd_draw_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_draw_points.png -------------------------------------------------------------------------------- /Examples/vd_stretch_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/Intertwined/main/Examples/vd_stretch_grid.png -------------------------------------------------------------------------------- /Examples/crochet.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-09-06 4 | 5 | # https://www.hardybarn.co.uk/adobe-illustrator-how-to-knitting-illustrations/ 6 | # https://math.hws.edu/eck/cs424/notes2013/canvas/bezier.html 7 | 8 | import sys 9 | sys.path.append('..') 10 | 11 | from Intertwined import * 12 | 13 | cv = Canvas(47, 25) 14 | 15 | v_offset = 6 16 | 17 | for row in range(3): 18 | 19 | t = cv.create_thread(o = (1, 10 + (v_offset*row))) 20 | 21 | for col in range(3): 22 | 23 | cv.arc_rel(t, 8, 0, 1, cp1=(2, 2), cp2=(-2, 2)) 24 | cv.arc_rel(t, -2, -8, 1, cp2=(-2, 2)) 25 | cv.arc_rel(t, 8, 0, 0, cp2=(-2,-2)) 26 | cv.arc_rel(t, -2, 8, 1, cp2=(-2,-2)) 27 | 28 | cv.arc_rel(t, 8, 0, 1, cp2=(-2, 2)) 29 | 30 | cv.draw() 31 | 32 | cv.surface.write_to_png("crochet.png") 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nicolas Seriot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Examples/node.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-09-06 4 | 5 | # https://ucsdnews.ucsd.edu/archive/newsrel/science/12-07NYTTopScienceStories2007.html 6 | # https://ucsdnews.ucsd.edu/archive/graphics/images/2007/10-07PNAS4BIG.jpg 7 | 8 | import sys 9 | sys.path.append('..') 10 | 11 | from Intertwined import * 12 | 13 | def draw(): 14 | 15 | cv = Canvas(16, 19) 16 | 17 | #cv.draw_grid() 18 | 19 | t = cv.create_thread(o = (1,1), draw_ends = False) 20 | 21 | cv.arc_to(t, 9, 4, 1, cp1=(2,-3), cp2=(-3,-3)) 22 | cv.arc_to(t, 13, 12, 0, cp2=(1, -3)) 23 | cv.arc_to(t, 7, 15, 1, cp2=(3, 0)) 24 | cv.arc_to(t, 1, 12, 0, cp2=(1, 3)) 25 | cv.arc_to(t, 5, 4, 1, cp2=(-3, 3)) 26 | cv.arc_to(t, 13, 1, 0, cp2=(-2, -3)) 27 | cv.arc_to(t, 9, 8, 1, cp2=(3,-3)) 28 | cv.arc_to(t, 5, 12, 0, cp2=(3, -3)) 29 | cv.arc_to(t, 7, 18, 1, cp2=(-3, 0)) 30 | cv.arc_to(t, 9, 12, 0, cp2=(3, 3)) 31 | cv.arc_to(t, 5, 8, 1, cp2=(3, 3)) 32 | cv.arc_to(t, 1, 1, 0, cp2=(-2, 3)) 33 | 34 | # col 1 | 5 | 7 | 9 | 13 35 | 36 | # row 1 | 4 | 8 | 12 | 15 | 18 37 | 38 | cv.draw(draw_cp=True, draw_depth=True) 39 | 40 | cv.surface.write_to_png("node.png") 41 | 42 | draw() 43 | -------------------------------------------------------------------------------- /Examples/braid.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-09-06 4 | 5 | import sys 6 | sys.path.append('..') 7 | 8 | from Intertwined import * 9 | 10 | def draw_1(): 11 | 12 | cv = Canvas(7, 15, col_size=20, row_size=30) 13 | 14 | #cv.c.set_antialias(cairo.ANTIALIAS_NONE) 15 | 16 | cv.draw_grid() 17 | 18 | t1 = cv.create_thread(o = (1,0)) 19 | 20 | t2 = cv.create_thread(o = (3,0)) 21 | 22 | t3 = cv.create_thread(o = (5,0)) 23 | 24 | cv.arc_rel(t1, 0, 1, 1, cp2=(0,-1)) 25 | cv.arc_rel(t2, 0, 1, 1, cp2=(0,-1)) 26 | cv.arc_rel(t3, 0, 1, 1, cp2=(0,-1)) 27 | 28 | R = 2 29 | for i in range(R): 30 | 31 | cv.arc_rel(t1, 4, 2, 1, cp2=(0, -1)) 32 | cv.arc_rel(t1, -4, 4, 4, cp2=(0, -1)) 33 | 34 | cv.arc_rel(t2, -2, 2, 0, cp2=(0, -1)) 35 | cv.arc_rel(t2, 4, 2, 3, cp2=(0, -1)) 36 | 37 | cp2x = 4/3. if i != (R-1) else 0 38 | cv.arc_rel(t2, -2, 2, 5, cp2=(cp2x, -1)) 39 | 40 | cv.arc_rel(t3, -4, 4, 2, cp2=(0, -1)) 41 | cv.arc_rel(t3, 4, 2, 4, cp2=(0, -1)) 42 | 43 | cv.arc_rel(t1, 0, 1, 1, cp2=(0, -1)) 44 | cv.arc_rel(t2, 0, 1, 5, cp2=(0, -1)) 45 | cv.arc_rel(t3, 0, 1, 4, cp2=(0, -1)) 46 | 47 | cv.draw() 48 | 49 | cv.surface.write_to_png("braid.png") 50 | 51 | draw_1() 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Examples/visualdoc.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-09-06 4 | 5 | import sys 6 | sys.path.append('..') 7 | 8 | from Intertwined import * 9 | 10 | def cp_none(): 11 | 12 | cv = Canvas(10, 8, col_size=20, row_size=20) 13 | t = cv.create_thread(o = (1,2)) 14 | cv.arc_rel(t, 5, -1, 1) 15 | cv.arc_rel(t, 2, 5, 1) 16 | cv.draw(draw_grid=True, draw_cp=True) 17 | cv.surface.write_to_png("vd_cp_none.png") 18 | 19 | def cp_cp1(): 20 | 21 | cv = Canvas(10, 8, col_size=20, row_size=20) 22 | t = cv.create_thread(o = (1,2)) 23 | cv.arc_rel(t, 5, -1, 1, cp1=(1,-2)) 24 | cv.arc_rel(t, 2, 5, 1) 25 | cv.draw(draw_grid=True, draw_cp=True) 26 | cv.surface.write_to_png("vd_cp1.png") 27 | 28 | def cp_cp2(): 29 | 30 | cv = Canvas(10, 8, col_size=20, row_size=20) 31 | t = cv.create_thread(o = (1,2)) 32 | cv.arc_rel(t, 5, -1, 1, cp2=(0,-2)) 33 | cv.arc_rel(t, 2, 5, 1) 34 | cv.draw(draw_grid=True, draw_cp=True) 35 | cv.surface.write_to_png("vd_cp2.png") 36 | 37 | def cp_cp1_cp2(): 38 | 39 | cv = Canvas(10, 8, col_size=20, row_size=20) 40 | t = cv.create_thread(o = (1,2)) 41 | cv.arc_rel(t, 5, -1, 1, cp1=(1,-2), cp2=(0,-2)) 42 | cv.arc_rel(t, 2, 5, 1) 43 | cv.draw(draw_grid=True, draw_cp=True) 44 | cv.surface.write_to_png("vd_cp1_cp2.png") 45 | 46 | def fill_canvas(cv): 47 | 48 | t = cv.create_thread(o = (3,1)) 49 | 50 | cv.arc_rel(t, 2, 5, 1, cp1=(-1,1), cp2=(-2,-1)) 51 | cv.arc_rel(t, 2, -3, 1, cp2=(2,1)) 52 | cv.arc_rel(t, -6, 2, 0, cp2=(0,-1)) 53 | 54 | return cv 55 | 56 | def draw_grid(): 57 | 58 | cv = Canvas(10, 8, col_size=20, row_size=20) 59 | cv = fill_canvas(cv) 60 | cv.draw(draw_grid=True) 61 | cv.surface.write_to_png("vd_draw_grid.png") 62 | 63 | def stretch_grid(): 64 | 65 | cv = Canvas(10, 8, col_size=20, row_size=30) 66 | cv = fill_canvas(cv) 67 | cv.draw(draw_grid=True) 68 | cv.surface.write_to_png("vd_stretch_grid.png") 69 | 70 | def draw_control_points(): 71 | 72 | cv = Canvas(10, 8, col_size=20, row_size=20) 73 | cv = fill_canvas(cv) 74 | cv.draw(draw_cp=True) 75 | cv.surface.write_to_png("vd_draw_cp.png") 76 | 77 | def draw_points(): 78 | 79 | cv = Canvas(10, 8, col_size=20, row_size=20) 80 | cv = fill_canvas(cv) 81 | cv.draw(draw_points=True) 82 | cv.surface.write_to_png("vd_draw_points.png") 83 | 84 | def draw_depth(): 85 | 86 | cv = Canvas(10, 8, col_size=20, row_size=20) 87 | cv = fill_canvas(cv) 88 | cv.draw(draw_depth=True) 89 | cv.surface.write_to_png("vd_draw_depth.png") 90 | 91 | def draw_all(): 92 | 93 | cv = Canvas(10, 8, col_size=20, row_size=20) 94 | cv = fill_canvas(cv) 95 | cv.draw(draw_grid=True, draw_points=True, draw_cp=True, draw_depth=True) 96 | cv.surface.write_to_png("vd_draw_all.png") 97 | 98 | if __name__ == "__main__": 99 | 100 | draw_grid() 101 | 102 | stretch_grid() 103 | 104 | draw_control_points() 105 | 106 | draw_points() 107 | 108 | draw_depth() 109 | 110 | draw_all() 111 | 112 | cp_none() 113 | cp_cp1() 114 | cp_cp2() 115 | cp_cp1_cp2() -------------------------------------------------------------------------------- /Intertwined/Thread.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-07-03 4 | 5 | import cairo 6 | 7 | from .Segment import * 8 | 9 | class Thread: 10 | 11 | # knows only context (pixels) coordinates, not canvas ones 12 | 13 | EXT_C = (0,0,0) 14 | INT_C = (1,1,1) 15 | 16 | EXT_W = 16 17 | INT_W = 12 18 | 19 | MAX_DEPTH = 100 20 | VISUAL_DEBUG = True 21 | 22 | draw_ends = True 23 | 24 | def __init__(self, c, o, ext_color, int_color, ext_width, int_width, draw_ends): 25 | 26 | self.EXT_C = ext_color 27 | self.INT_C = int_color 28 | 29 | self.EXT_W = ext_width 30 | self.INT_W = int_width 31 | 32 | self.c = c 33 | self.segments = [] 34 | self.segments_for_depth = [] 35 | self.draw_ends = draw_ends 36 | 37 | for i in range(Thread.MAX_DEPTH): 38 | self.segments_for_depth.append([]) 39 | 40 | self.pos = o 41 | 42 | def arc_rel(self, p, z, cp1=None, cp2=None, cp1_length=None): 43 | assert(z < Thread.MAX_DEPTH) 44 | self.arc_to(self.pos[0] + p[0], self.pos[1] + p[1], z, cp1, cp2) 45 | 46 | def arc_to(self, x, y, z, cp1=None, cp2=None): 47 | s = Segment(self.pos, (x,y), z, cp1, cp2) 48 | self.segments.append(s) 49 | self.segments_for_depth[z].append(s) 50 | self.pos = (x, y) 51 | 52 | def compute_cps(self): 53 | 54 | pcp2 = None # previous segment cp2 55 | 56 | for s in self.segments: 57 | if not s.cp1: 58 | s.cp1 = s.compute_cp1(pcp2) 59 | if not s.cp2: 60 | s.cp2 = s.compute_cp2() 61 | pcp2 = s.cp2 62 | 63 | def draw_segment_end(self, p): 64 | 65 | self.c.save() 66 | 67 | line_width = (self.EXT_W - self.INT_W) / 2. 68 | 69 | if self.c.get_antialias() == cairo.ANTIALIAS_DEFAULT: 70 | line_width = line_width - 0.5 71 | 72 | self.c.set_line_width(line_width) 73 | 74 | self.c.set_source_rgb(*self.INT_C) 75 | self.c.arc(p[0], p[1], self.EXT_W / 2. - 2, 0, 2*math.pi) 76 | self.c.fill_preserve() 77 | self.c.set_source_rgb(*self.EXT_C) 78 | self.c.stroke() 79 | 80 | self.c.restore() 81 | 82 | def draw_segments_of_depth(self, z): 83 | 84 | if len(self.segments) == 0: 85 | print("-- %s has no segments to draw for depth %d" % (self, z)) 86 | return 87 | 88 | s0 = self.segments[0] 89 | sn = self.segments[-1] 90 | 91 | if self.draw_ends: 92 | if s0.z == z: 93 | self.draw_segment_end(s0.o) 94 | 95 | if sn.z == z: 96 | self.draw_segment_end(sn.p) 97 | 98 | for s in self.segments_for_depth[z]: 99 | 100 | # hack: draw several times to soften external antialiasing on segment ends 101 | aa =self.c.get_antialias() == cairo.ANTIALIAS_DEFAULT 102 | ext_repeat = 2 if aa else 1 103 | int_repeat = 16 if aa else 1 104 | 105 | for i in range(ext_repeat): 106 | s.draw(self.c, self.EXT_W, self.EXT_C, cairo.LINE_CAP_BUTT) 107 | 108 | for i in range(int_repeat): 109 | s.draw(self.c, self.INT_W, self.INT_C, cairo.LINE_CAP_BUTT) 110 | -------------------------------------------------------------------------------- /Intertwined/Segment.py: -------------------------------------------------------------------------------- 1 | import cairo 2 | import math 3 | 4 | class Segment: 5 | 6 | def __init__(self, o, p, z, cp1=None, cp2=None): 7 | 8 | if cp2 == None: 9 | pass 10 | 11 | self.o = o # origin 12 | self.p = p # point 13 | self.z = z # z-depth 14 | self.cp1 = (o[0] + cp1[0], o[1] + cp1[1]) if cp1 else None 15 | self.cp2 = (p[0] + cp2[0], p[1] + cp2[1]) if cp2 else None 16 | 17 | def norm(self, p1, p2): 18 | return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2) 19 | 20 | def compute_cp2(self): 21 | 22 | # from p towards cp1, same length as o_cp1 23 | 24 | v = ((self.cp1[0] - self.p[0]), (self.cp1[1] - self.p[1])) # from cp1 to p 25 | #v = ((self.o[0] - self.p[0]), (self.o[1] - self.p[1])) # another way to compute cp2 26 | 27 | length = self.norm(self.o, self.cp1) 28 | 29 | if length < 0.0001: # eg. for 1st segment, when cp1 is not set 30 | length = self.norm(self.o, self.p) / 2. # pure heuristic.. 31 | 32 | v_norm = math.sqrt(v[0]**2 + v[1]**2) 33 | 34 | ratio = v_norm / length 35 | 36 | (dx, dy) = (v[0]/ratio, v[1]/ratio) 37 | 38 | return (self.p[0] + dx, self.p[1] + dy) 39 | 40 | def compute_cp1(self, pcp2): 41 | 42 | # symetric to previous segement cp2, aka pcp2, around segment origin 43 | 44 | if pcp2: 45 | dx = (self.o[0] - pcp2[0]) 46 | dy = (self.o[1] - pcp2[1]) 47 | else: 48 | dx = 0 49 | dy = 0 50 | 51 | return (self.o[0] + dx, self.o[1] + dy) 52 | 53 | def draw(self, ctx, width, color, line_cap): 54 | 55 | ctx.save() 56 | 57 | ctx.set_line_cap(line_cap) 58 | 59 | ctx.set_line_width(width) 60 | ctx.set_source_rgb(*color) 61 | 62 | ctx.move_to(*self.o) 63 | 64 | ctx.curve_to(self.cp1[0], self.cp1[1], self.cp2[0], self.cp2[1], *self.p) 65 | ctx.stroke() 66 | 67 | ctx.restore() 68 | 69 | def debug_draw_lines(self, ctx): 70 | 71 | ctx.save() 72 | 73 | ctx.set_antialias(cairo.ANTIALIAS_DEFAULT) 74 | ctx.set_line_width(1) 75 | ctx.set_source_rgb(0,0,0) 76 | 77 | ctx.move_to(*self.o) 78 | ctx.line_to(*self.cp1) 79 | 80 | ctx.move_to(*self.cp2) 81 | ctx.line_to(*self.p) 82 | 83 | #print("----- o cp1 cp2 p", self.o, self.cp1, self.cp2, self.p) 84 | 85 | ctx.stroke() 86 | 87 | ctx.restore() 88 | 89 | def debug_write_point(self, ctx, p, s): 90 | 91 | ctx.save() 92 | 93 | ctx.set_antialias(cairo.ANTIALIAS_NONE) 94 | 95 | ctx.set_line_width(1) 96 | ctx.set_source_rgb(1,1,1) 97 | ctx.rectangle(p[0]-8, p[1]-8, 16, 16) 98 | ctx.fill_preserve() 99 | ctx.set_source_rgb(0,0,0) 100 | ctx.stroke() 101 | 102 | ctx.select_font_face("Courier New") 103 | ctx.set_font_size(14) 104 | 105 | fo = cairo.FontOptions() 106 | fo.set_antialias(cairo.ANTIALIAS_NONE) 107 | ctx.set_font_options(fo) 108 | 109 | ctx.move_to(p[0]-5, p[1]+4) 110 | ctx.show_text(s) 111 | 112 | ctx.restore() 113 | 114 | def debug_draw_point(self, ctx, p, color): 115 | 116 | ctx.save() 117 | 118 | ctx.set_source_rgb(*color) 119 | ctx.arc(p[0], p[1], 5, 0, 2*math.pi) 120 | ctx.fill() 121 | ctx.arc(p[0], p[1], 5, 0, 2*math.pi) 122 | ctx.set_source_rgb(0,0,0) 123 | ctx.stroke() 124 | 125 | ctx.restore() 126 | -------------------------------------------------------------------------------- /Examples/knots.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-09-06 4 | 5 | # https://www.hardybarn.co.uk/adobe-illustrator-how-to-knitting-illustrations/ 6 | # https://math.hws.edu/eck/cs424/notes2013/canvas/bezier.html 7 | 8 | import sys 9 | sys.path.append('..') 10 | 11 | from Intertwined import * 12 | 13 | import math 14 | 15 | c1 = (170/255., 170/255., 249/255.) 16 | c2 = (243/255., 174/255., 234/255.) 17 | c3 = (251/255., 231/255., 142/255.) 18 | 19 | def draw_knot_1(cv): 20 | 21 | t1 = cv.create_thread(o = (4,0), 22 | int_color = c1) 23 | 24 | cv.arc_rel(t1, -1, 9, 0, cp2=(1,-2), cp1=(0,2)) 25 | 26 | cv.arc_rel(t1, -2, 6, 1, cp2=(-2, -2)) 27 | cv.arc_rel(t1, 8, 0, 0, cp2=(-2, 2)) 28 | cv.arc_rel(t1, -2, -6, 1, cp2=(1, 2)) 29 | cv.arc_rel(t1, -1, -9, 0, cp2=(0, 2)) 30 | 31 | t2 = cv.create_thread(o = (4,20), 32 | ext_color = (0,0,0), 33 | int_color = c2, 34 | ext_width = 24, 35 | int_width = 18) 36 | 37 | cv.arc_rel(t2, -1, -9, 1, cp2=(1,2), cp1=(0,-2)) 38 | 39 | cv.arc_rel(t2, -2, -6, 0, cp2=(-2, 2)) 40 | cv.arc_rel(t2, 8, 0, 1, cp2=(-2, -2)) 41 | cv.arc_rel(t2, -2, 6, 0, cp2=(1, -2)) 42 | cv.arc_rel(t2, -1, 9, 1, cp2=(0, -2)) 43 | 44 | def draw_knot_2(cv): 45 | 46 | t1 = cv.create_thread(o = (4,0), 47 | int_color = c1) 48 | 49 | cv.arc_rel(t1, -1, 9, 1, cp2=(1,-2), cp1=(0,2)) 50 | 51 | cv.arc_rel(t1, -2, 6, 1, cp2=(-2, -2)) 52 | cv.arc_rel(t1, 8, 0, 1, cp2=(-2, 2)) 53 | cv.arc_rel(t1, -2, -6, 3, cp2=(1, 2)) 54 | cv.arc_rel(t1, -1, -9, 1, cp2=(0, 2)) 55 | 56 | t2 = cv.create_thread(o = (1,20), 57 | ext_color = (0,0,0), 58 | int_color = c2, 59 | ext_width = 24, 60 | int_width = 18) 61 | 62 | cv.arc_rel(t2, 3, -6, 0, cp2=(-1,2), cp1=(0,-2)) 63 | 64 | cv.arc_rel(t2, 5, -9, 2, cp2=(2, 2)) 65 | cv.arc_rel(t2, -8, -0, 1, cp2=(2, -2)) 66 | cv.arc_rel(t2, 3, 7, 0, cp2=(-2, -3)) 67 | cv.arc_rel(t2, 5, 8, 1, cp2=(0, -2)) 68 | 69 | cv.draw() 70 | 71 | def draw_knot_3(cv): 72 | 73 | sq = 2.2 74 | 75 | t1 = cv.create_thread(o = (4,0), 76 | int_color = c3) 77 | 78 | cv.arc_rel(t1, 4, 4, 1, cp1=(sq,0), cp2=(0,-sq)) 79 | 80 | cv.arc_rel(t1, -4, 4, 1, cp2=(sq, 0)) 81 | cv.arc_rel(t1, -4, -4, 1, cp2=(0, sq)) 82 | cv.arc_rel(t1, 4, -4, 1, cp2=(-sq, 0)) 83 | 84 | t2 = cv.create_thread(o = (0,18), 85 | ext_color = (0,0,0), 86 | int_color = c2, 87 | ext_width = 24, 88 | int_width = 18) 89 | 90 | cv.arc_rel(t2, 4, -3, 0, cp1=(0,-2), cp2=(-2,0)) 91 | 92 | cv.arc_rel(t2, 4, 2, 2, cp2=(0,-2)) 93 | cv.arc_rel(t2, -5, 0, 1, cp2=(1,2)) 94 | cv.arc_rel(t2, 0, -6, 1, cp2=(-1,2)) 95 | cv.arc_rel(t2, 5, 1, 0, cp2=(0,-2)) 96 | cv.arc_rel(t2, -4, 1, 2, cp2=(2,0)) 97 | cv.arc_rel(t2, -3, -3, 0, cp2=(0,2)) 98 | cv.arc_rel(t2, 4, -4, 0, cp2=(-2,-2)) 99 | cv.arc_rel(t2, 0, 14, 1, cp2=(0,-2)) 100 | 101 | cv.draw() 102 | 103 | def draw_knots(): 104 | 105 | cv = Canvas(38, 21) 106 | 107 | #cv.draw_grid() 108 | 109 | draw_knot_1(cv) 110 | 111 | cv.draw() 112 | cv.clear_context() 113 | 114 | cv.c.translate(280, 0) 115 | 116 | draw_knot_2(cv) 117 | 118 | cv.draw() 119 | cv.clear_context() 120 | 121 | cv.c.translate(280*2, 0) 122 | 123 | draw_knot_3(cv) 124 | 125 | cv.surface.write_to_png("knots.png") 126 | 127 | draw_knots() 128 | -------------------------------------------------------------------------------- /Intertwined/Canvas.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # Nicolas Seriot 3 | # 2022-08-30 4 | 5 | import cairo 6 | 7 | from .Thread import * 8 | 9 | def hexcolor(s): 10 | s = s.lstrip("#") 11 | return tuple(int(s[i:i+2], 16) / 255. for i in (0, 2, 4)) 12 | 13 | class Canvas: 14 | 15 | MARGIN = 50 16 | 17 | DEFAULT_PALETTE = [(170/255., 170/255., 249/255.), 18 | (243/255., 174/255., 234/255.), 19 | (251/255., 231/255., 142/255.)] 20 | 21 | def clear_context(self): 22 | self.c = cairo.Context(self.surface) 23 | self.c.translate(self.MARGIN, self.MARGIN) 24 | self.threads = [] 25 | 26 | def __init__(self, NB_COLS, NB_ROWS, background_color=(1,1,1), col_size=20, row_size=20, palette=DEFAULT_PALETTE): 27 | 28 | self.palette = palette 29 | self.threads = [] 30 | 31 | self.RS = row_size 32 | self.CS = col_size 33 | 34 | self.NB_ROWS = NB_ROWS 35 | self.NB_COLS = NB_COLS 36 | 37 | W = self.NB_COLS * self.CS + self.MARGIN*2 38 | H = self.NB_ROWS * self.RS + self.MARGIN*2 39 | 40 | self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, W, H) 41 | self.c = cairo.Context(self.surface) 42 | 43 | self.c.set_source_rgb(*background_color) 44 | self.c.paint() 45 | 46 | self.c.translate(self.MARGIN, self.MARGIN) 47 | 48 | def draw(self, draw_grid=False, draw_points=False, draw_cp=False, draw_depth=False): 49 | 50 | if draw_grid: 51 | self.draw_grid() 52 | 53 | for t in self.threads: 54 | t.compute_cps() 55 | 56 | Z=Thread.MAX_DEPTH-1 # limit drawing to some depth 57 | for z in range(Z+1): 58 | for t in self.threads: 59 | t.draw_segments_of_depth(z) 60 | 61 | for t in self.threads: 62 | for z in range(Z+1): 63 | for s in t.segments_for_depth[z]: 64 | if draw_points: 65 | s.debug_draw_point(self.c, s.o, (1,1,1)) 66 | s.debug_draw_point(self.c, s.p, (1,1,1)) 67 | if draw_cp: 68 | s.debug_draw_lines(self.c) 69 | s.debug_draw_point(self.c, s.o, (1,1,1)) 70 | s.debug_draw_point(self.c, s.p, (1,1,1)) 71 | s.debug_draw_point(self.c, s.cp1, (1,0,0)) 72 | s.debug_draw_point(self.c, s.cp2, (0,1,0)) 73 | for s in t.segments: 74 | if draw_depth and z <= Z: 75 | s.debug_write_point(self.c, s.o, "%d" % s.z) 76 | 77 | def draw_grid(self, color=(0,0,0)): 78 | 79 | self.c.save() 80 | 81 | self.c.set_line_width(1) 82 | 83 | self.c.set_source_rgb(*color) 84 | 85 | for i in range(self.NB_ROWS+1): 86 | self.c.move_to(0, i*self.RS) 87 | self.c.rel_line_to(self.NB_COLS * self.CS, 0) 88 | 89 | for i in range(self.NB_COLS+1): 90 | self.c.move_to(i*self.CS, 0) 91 | self.c.rel_line_to(0, self.NB_ROWS * self.RS) 92 | 93 | self.c.stroke() 94 | 95 | self.c.restore() 96 | 97 | def canvas_to_surface(self, p, add_half_offset=False): 98 | if not p: 99 | return None 100 | 101 | x = p[0] * self.CS + (self.CS/2. if add_half_offset else 0) 102 | y = p[1] * self.RS + (self.RS/2. if add_half_offset else 0) 103 | return (x, y) 104 | 105 | def create_thread(self, o, ext_color=(0,0,0), int_color=None, ext_width=24, int_width=18, draw_ends=True): 106 | 107 | if not ext_color: 108 | ext_color = (0,0,0) 109 | 110 | if not int_color: 111 | int_color = self.palette[len(self.threads) % len(self.palette)] 112 | 113 | t = Thread(self.c, 114 | self.canvas_to_surface(o, add_half_offset=True), 115 | ext_color, int_color, ext_width, int_width, 116 | draw_ends) 117 | 118 | self.threads.append(t) 119 | return t 120 | 121 | def arc_to(self, t, col, row, z, cp1=None, cp2=None): 122 | p_s = self.canvas_to_surface((col, row), add_half_offset=True) 123 | cp2_s = self.canvas_to_surface(cp2) 124 | cp1_s = self.canvas_to_surface(cp1) 125 | t.arc_to(p_s[0], p_s[1], z, cp1_s, cp2_s) 126 | 127 | def arc_rel(self, t, col, row, z, cp1=None, cp2=None): 128 | p_s = self.canvas_to_surface((col, row)) 129 | cp2_s = self.canvas_to_surface(cp2) 130 | cp1_s = self.canvas_to_surface(cp1) 131 | t.arc_rel(p_s, z, cp1_s, cp2_s) 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intertwined 2 | __Draw intertwined threads, knots and knitting figures with PyCairo__ 3 | 4 | __Overall Idea__ 5 | 6 | Threads (paths) are made of segments (Bezier curves) with their own z-depth. 7 | 8 | Segments are drawn in z-depth order, so that threads look Intertwined. 9 | 10 | Control points are either set explicitely by the caller, or computed according to specific rules. 11 | 12 | Here are the drawing steps, starting from lowest z-depth, here for `Braid.py`. 13 | 14 | ![braid.png](Examples/braid.png)![braid.gif](Examples/braid.gif) 15 | 16 | __Architecture__ 17 | 18 | There are three classes, stacked in a layer model. 19 | 20 | Canvas - the surface where to draw 21 | Thread - a specific thread, attached to a canvas 22 | Segment - a portion of a thread, with its own z-depth 23 | 24 | __API__ 25 | 26 | The caller only interacts with Canvas, which exposes methods to create and move threads. 27 | 28 | cv = Canvas(...) 29 | t = cv.create_thread(...) 30 | cv.arc_to(t, ...) 31 | cv.arc_rel(t, ...) 32 | cv.draw(...) 33 | 34 | Whenever a thread is moved, a segment is added to the thread. 35 | 36 | Segments are [cubic Bezier curves](https://math.hws.edu/eck/cs424/notes2013/canvas/bezier.html), defined by two endpoints and two control points. 37 | 38 | __Control points logic__ 39 | 40 | When not set by the caller, control points are calculated as follows: 41 | 42 | * `cp1`: symetric to previous segment cp2 around segment origin 43 | * `cp2`: from segment end towards segment cp1, same length as origin to cp1 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 |
No control points are set explicitely on 1st segment s1.
Control point s1.cp2 is computed as half s.p_s.o.
Control point s2.cp1 (red) is computed according to previous segment s1.cp2 (green).
Control point s2.cp2 is computed as s2.p_s2.cp1 with samed length as s2.cp1.
t = cv.create_thread(o = (1,2))
 50 | cv.arc_rel(t, 5, -1, 1)
 51 | cv.arc_rel(t, 2, 5, 1)
s1.cp1 is set, s1.cp2 is calculated accordingly.
Orientation propagates to s2.cp1.
t = cv.create_thread(o = (1,2))
 57 | cv.arc_rel(t, 5, -1, 1, cp1=(1,-2))
 58 | cv.arc_rel(t, 2, 5, 1)
 59 | 
s1.cp2 is set, s2.cp1 is calculated accordingly.
t = cv.create_thread(o = (1,2))
 65 | cv.arc_rel(t, 5, -1, 1, cp2=(0,-2))
 66 | cv.arc_rel(t, 2, 5, 1)
Both control points are set for s1.
t = cv.create_thread(o = (1,2))
 72 | cv.arc_rel(t, 5, -1, 1, cp1=(1,-2), cp2=(0,-2))
 73 | cv.arc_rel(t, 2, 5, 1)
77 | 78 | ---- 79 | 80 | __Sample Drawings__ 81 | 82 | `Crochet.py` 83 | 84 | ![crochet.png](Examples/crochet.png) 85 | 86 | ```Python 87 | from Intertwined import * 88 | 89 | cv = Canvas(47, 25) 90 | 91 | v_offset = 6 92 | 93 | for row in range(3): 94 | 95 | t = cv.create_thread(o = (1, 10 + (v_offset*row))) 96 | 97 | for col in range(3): 98 | 99 | cv.arc_rel(t, 8, 0, 1, cp1=(2, 2), cp2=(-2, 2)) 100 | cv.arc_rel(t, -2, -8, 1, cp2=(-2, 2)) 101 | cv.arc_rel(t, 8, 0, 0, cp2=(-2,-2)) 102 | cv.arc_rel(t, -2, 8, 1, cp2=(-2,-2)) 103 | 104 | cv.arc_rel(t, 8, 0, 1, cp2=(-2, 2)) 105 | 106 | cv.draw() 107 | 108 | cv.surface.write_to_png("crochet.png") 109 | ``` 110 | 111 | ---- 112 | 113 | `Knots.py` 114 | 115 | ![knots.png](Examples/knots.png) 116 | 117 | ---- 118 | 119 | `Node.py` 120 | 121 | ![knots.png](Examples/node.png) 122 | 123 | ---- 124 | 125 | __Drawing Options__ 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 161 | 162 | 163 |
Draw Grid
cv.draw(draw_grid=True)
Stretch Grid
cv = Canvas(10, 8, col_size=20, row_size=30)
Draw Points
cv.draw(draw_points=True)
Draw Control Points
cv.draw(draw_cp=True)
Draw Depth
cv.draw(draw_depth=True)
Draw All
cv.draw(draw_grid=True,
157 |     draw_points=True,
158 |     draw_cp=True,
159 |     draw_depth=True)
160 | 
164 | --------------------------------------------------------------------------------