├── LICENSE
├── README.md
├── demo
├── autodemo.html
├── autodemo.js
├── interactivedemo.html
├── interactivedemo.js
├── minimaldemo.html
├── minimaldemo.js
├── p5.min.js
├── quicksettings.min.js
├── tileinfo.js
├── touchdemo.html
└── touchdemo.js
├── images
├── jusi.png
├── params.png
├── shape.png
└── topo.png
├── lib
├── package.json
└── tactile.js
├── spirals
├── README.md
├── assets
│ ├── frag1.txt
│ ├── helveticaneue.otf
│ └── vert1.txt
├── earcut.js
├── fit-curve.js
├── index.html
└── spirals.js
└── src
├── README.md
├── preamble.inc
├── tactile.inc
└── tiling_data.inc
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2018, Craig S. Kaplan
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TactileJS
2 | TactileJS is a Javascript library for representing, manipulating, and drawing tilings of the plane. The tilings all belong to a special class called the _isohedral tilings_. Every isohedral tiling is formed from repeated copies of a single shape, and the pattern of repetition is fairly simple. At the same time, isohedral tilings are very expressive (they form the basis for a lot of the tessellations created by M.C. Escher) and efficient to compute.
3 |
4 | I created the first versions of Tactile in the late 1990s, while working on my [PhD][phd]. This Javascript library is a port of the analogous [C++ library][tactile] I created as a modern upgrade of the original Tactile. The core library is completely self-contained; I also provide a demo based on Tactile, [`P5.js`][p5js], and [`QuickSettings`][quickset] (which are included in `demo/` for convenience). Of course, the goal is not simply to use the demo page as-is, but rather to explore new applications of the library in an interactive web-based context.
5 |
6 | The rest of the documentation below is a translation into Javascript of the corresponding documentation on the [Tactile][tactile] library page. You can also peruse the source code in `demo/`, which uses the library to develop a couple of simple interactive and non-interactive drawing programs. You can also [try the demo][demo] online.
7 |
8 | ## A crash course on isohedral tilings
9 |
10 | In order to understand how to use Tactile, it might first be helpful to become acquainted with the Isohedral tilings. The ultimate reference on the subject is the book _Tilings and Patterns_ by Grünbaum and Shephard. You could also have a look at my book, [_Introductory Tiling Theory for Computer Graphics_][mybook], which is much slimmer and written more from a computer science perspective. If you want a quick and free introduction, you could look through Chapters 2 and 4 of [my PhD thesis][phd].
11 |
12 | Every isohedral tiling is made from repeated copies of a single shape called the _prototile_, which repeats in a very orderly way to fill the plane. We can describe the prototile's shape by breaking it into _tiling edges_, the shared boundaries between adjacent tiles, which connect at _tiling vertices_, the points where three or more tiles meet.
13 |
14 |

15 |
16 | There are 93 "tiling types", different ways that tiles can relate to each other. Of these, 12 are boring for reasons I won't talk about here; this library lets you manipulate the other 81 types.
17 |
18 | For each isohedral tiling type, there are constraints on the legal relationships between the tiling vertices. Those constraints can be encoded in a set of _parameters_, which are just real numbers. Some tiling types have zero parameters (their tiling vertices must form a fixed shape, like a square or a hexagon); others have as many as six free parameters.
19 |
20 | 
21 |
22 | ## Loading the library
23 |
24 | Tactile is delivered as an ES6 module, which you should access from your Javascript code using an `import` statement. For example:
25 |
26 | ```Javascript
27 | import { EdgeShape, numTypes, tilingTypes, IsohedralTiling }
28 | from './tactile.js';
29 | ```
30 |
31 | See the `demo/` folder for more examples.
32 |
33 | ## Constructing a tiling
34 |
35 | The class `IsohedralTiling` can be used to describe a specific tiling and its prototile. It has a single constructor that takes the desired tiling type as an argument. The tiling type is expressed as an integer representing a legal isohedral type. These are all numbers between 1 and 93 (inclusive), but there are some holes—for example, there is no Type 19. The array `tilingTypes`, with length `numTypes` (fixed at 81) contains the legal types:
36 |
37 | ```Javascript
38 | // Suppose you wanted to loop over all the tiling types...
39 | for( let idx = 0; idx < numTypes; ++idx ) {
40 | // Create a new tiling of the given type, with default shape.
41 | let a_tiling = new IsohedralTiling( tilingTypes[ idx ] );
42 | // Do something with this tiling type
43 | }
44 | ```
45 |
46 | ## Controlling parameters
47 |
48 | You can get and set the parameters that control the positions of the tiling vertices through an array of floating point numbers:
49 |
50 | ```Javascript
51 | // Get the current values of the parameters.
52 | let params = a_tiling.getParameters();
53 | if( a_tiling.numParameters() > 1 ) {
54 | params[ 1 ] += 1.0;
55 | // Send the parameters back to the tiling
56 | a_tiling.setParameters( params );
57 | }
58 | ```
59 |
60 | Setting the parameters causes a lot of internal data to be recomputed (efficiently, but still), which is why all parameters should be set together in one function call.
61 |
62 | ## Prototile shape
63 |
64 | As discussed above, a prototile's outline can be thought of as a sequence of tiling edges running between consecutive tiling vertices. Of course, in order to tile the plane, some of those edges must be transformed copies of others, so that a tile can interlock with its neighbours. In most tiling types, then, there are fewer distinct _edge shapes_ than there are edges, sometimes as few as a single path repeated all the way around the tile. Furthermore, some edge shapes can have internal symmetries forced upon it by the tiling:
65 |
66 | 
67 |
68 | * Some edges must look the same after a 180° rotation, like a letter S. We call these **S** edges.
69 | * Some edges must look the same after reflecting across their length, like a letter U. We call these **U** edges.
70 | * Some edges must look the same after both rotation _and_ reflection. Only a straight line has this property, so we call these **I** edges.
71 | * All other edges can be a path of any shape. We call these **J** edges.
72 |
73 | Tactile assumes that an edge shape lives in a canonical coordinate system, as a path that starts at (0,0) and ends at (1,0). The library will then tell you the transformations you need to perform in order to map those canonical edges into position around the prototile's boundary. You can access this information by looping over the tiling edges using a C++ iterator:
74 |
75 | ```Javascript
76 | // Iterate over the tiling's edges, getting information about each edge
77 | for( let i of a_tiling.shape() ) {
78 | // Determine which canonical edge shape to use for this tiling edge.
79 | // Multiple edges will have the same ID, indicating that the edge shape is
80 | // reused multiple times.
81 | const id = i.id;
82 | // Get a transformation matrix (as an array of six numbers, representing
83 | // the first two rows of a 3x3 matrix) that moves the edge from canonical
84 | // position into its correct place around the tile boundary.
85 | const T = i.T;
86 | // Get the intrinsic shape constraints on this edge shape: Is it
87 | // J, U, S, or I? Here, it's your responsibility to draw a path that
88 | // actually has those symmetries.
89 | const shape = i.shape;
90 | // When edges interlock, one copy must be parameterized backwards
91 | // to fit with the rest of the prototile outline. This boolean
92 | // tells you whether you need to do that.
93 | const rev = i.rev;
94 |
95 | // Do something with the information above...
96 | }
97 | ```
98 |
99 | 
100 |
101 | Javascript doesn't include functions for linear algebra. For what it's worth, TactileJS includes a function `mul` which understands how to multiply 2D transformation matrices together (as might be offered to you in a shape iterator's `T` field above), and how to transform a point (represented as a Javascript object with fields `x` and `y`) using a transformation matrix.
102 |
103 | Occasionally, it's annoying to have to worry about the **U** or **S** symmetries of edges yourself. Tactile offers an alternative way to describe the tile's outline that includes extra steps that account for these symmetries. In this case, the transformation matrices build in scaling operations that map a path from (0,0) to (1,0) to, say, each half of an **S** edge separately. The correct approach here is to iterate over a tile's `parts()` rather than its `shape()`:
104 |
105 | ```C++
106 | // Iterate over the tiling's edges, getting information about each edge
107 | for( let i of a_tiling.parts() ) {
108 | // As above.
109 | const id = i.id;
110 | // As above for J and I edges. For U and S edges, include a scaling
111 | // operation to map to each half of the tiling edge in turn.
112 | const T = i.T;
113 | // As above
114 | const shape = i.shape;
115 | // As above
116 | const rev = i.rev;
117 | // For J and I edges, this is always false. For U and S edges, this
118 | // will be false for the first half of the edge and true for the second.
119 | const second = i.second;
120 |
121 | // Do something with the information above...
122 | }
123 | ```
124 |
125 | When drawing a prototile's outline using `parts()`, a **U** edge's midpoint might lie anywhere on the perpendicular bisector of the line joining two tiling vertices. For that reason, you are permitted to make an exception and have the underlying canonical path end at (1,_y_) for any _y_ value.
126 |
127 | Note that there's nothing in the description above that knows how paths are represented. That's a deliberate design decision that keeps the library lightweight and adaptable to different sorts of curves. It's up to you to maintain a set of canonical edge shapes that you can transform and string together to get the final tile outline.
128 |
129 | ## Laying out tiles
130 |
131 | The core operation of tiling is to fill a region of the plane with copies of the prototile. Tactile offers a simple iterator-based approach for doing this:
132 |
133 | ```Javascript
134 | // Fill a rectangle given its bounds (xmin, ymin, xmax, ymax)
135 | for( let i of a_tiling.fillRegionBounds( 0.0, 0.0, 8.0, 5.0 ) ) {
136 | // Get the 3x3 matrix corresponding to one of the transformed
137 | // tiles in the filled region.
138 | const T = i.T;
139 | // Use a simple colouring algorithm to pick a colour for this tile
140 | // so that adjacent tiles aren't the same colour. The resulting
141 | // value col will be 0, 1, or 2, which you should map to your
142 | // three favourite colours.
143 | const col = a_tiling.getColour( i.t1, i.t2, i.aspect );
144 | }
145 | ```
146 |
147 | There is an alternative form `fillRegionQuad()` that takes four points as arguments instead of bounds.
148 |
149 | The region filling algorithm isn't perfect. It's difficult to compute exactly which tiles are needed to fill a given rectangle, at least with high efficiency. It's possible you'll generate tiles that are completely outside the window, or leave unfilled fringes at the edge of the window. The easiest remedy is to fill a larger region than you need and ignore the extra tiles. In the future I may work on improving the algorithm, perhaps by including an option that performs the extra computation when requested.
150 |
151 | ## Other versions
152 |
153 | * [Tactile][tactile], my original C++ version of this library.
154 | * [Tactile-rs][tactilers], a Rust port by Antoine Büsch
155 | * [Tactile-python][tactilepy], a Python port by David Braun
156 |
157 | ## In closing
158 |
159 | I hope you find this library to be useful. If you are using Tactile for research, for fun, or for commercial products, I would appreciate it if you'd let me know. I'd be happy to list projects based on Tactile here, and it helps my research agenda to be able to say that the library is getting used. Thank you.
160 |
161 | [phd]: https://cs.uwaterloo.ca/~csk/other/phd/
162 | [p5js]: https://p5js.org/
163 | [quickset]: https://github.com/bit101/quicksettings
164 | [tactile]: https://github.com/isohedral/tactile
165 | [demo]: http://isohedral.ca/software/tactile/
166 | [mybook]: https://www.amazon.com/Introductory-Computer-Graphics-Synthesis-Animation/dp/1608450171
167 | [tactilers]: https://github.com/abusch/tactile-rs
168 | [tactilepy]: https://github.com/DBraun/tactile-python
169 |
--------------------------------------------------------------------------------
/demo/autodemo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/autodemo.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2018 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | import { mul, matchSeg, EdgeShape, numTypes, tilingTypes, IsohedralTiling }
10 | from '../lib/tactile.js';
11 |
12 | let sktch = function( p5c )
13 | {
14 | let cur_tiling = null;
15 | let next_tiling = null;
16 | let last_change = 0.0;
17 |
18 | function sub( V, W ) { return { x: V.x-W.x, y: V.y-W.y }; };
19 | function dot( V, W ) { return V.x*W.x + V.y*W.y; };
20 | function len( V ) { return p5c.sqrt( dot( V, V ) ); }
21 |
22 | function inv( T ) {
23 | const det = T[0]*T[4] - T[1]*T[3];
24 | return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det,
25 | -T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det];
26 | };
27 |
28 | function createRandomTiling()
29 | {
30 | const tp = tilingTypes[ Math.floor( 81 * p5c.random() ) ];
31 |
32 | let tiling = new IsohedralTiling( tp );
33 | let ps = tiling.getParameters();
34 | for( let i = 0; i < ps.length; ++i ) {
35 | ps[i] += p5c.random() * 0.3 - 0.15;
36 | }
37 | tiling.setParameters( ps );
38 |
39 | let edges = [];
40 | for( let i = 0; i < tiling.numEdgeShapes(); ++i ) {
41 | let ej = [];
42 | const shp = tiling.getEdgeShape( i );
43 | if( shp == EdgeShape.I ) {
44 | // Pass
45 | } else if( shp == EdgeShape.J ) {
46 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
47 | ej.push( { x: Math.random()*0.6 + 0.4, y : Math.random() - 0.5 } );
48 | } else if( shp == EdgeShape.S ) {
49 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
50 | ej.push( { x: 1.0 - ej[0].x, y: -ej[0].y } );
51 | } else if( shp == EdgeShape.U ) {
52 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
53 | ej.push( { x: 1.0 - ej[0].x, y: ej[0].y } );
54 | }
55 |
56 | edges.push( ej );
57 | }
58 |
59 | let cols = [];
60 | for( let i = 0; i < 3; ++i ) {
61 | cols.push( [
62 | Math.floor( Math.random() * 255.0 ),
63 | Math.floor( Math.random() * 255.0 ),
64 | Math.floor( Math.random() * 255.0 ) ] );
65 | }
66 |
67 | const dtheta = Math.random() * p5c.TWO_PI;
68 | const dv = Math.random() * 0.05;
69 |
70 | return {
71 | tiling: tiling,
72 | edges: edges,
73 | cols: cols,
74 |
75 | tx: Math.random() * 10.0,
76 | ty: Math.random() * 10.0,
77 | theta: Math.random() * p5c.TWO_PI,
78 | sc: Math.random() * 20.0 + 4.0,
79 |
80 | dx: dv * Math.cos( dtheta ),
81 | dy: dv * Math.sin( dtheta )
82 | };
83 | }
84 |
85 | function samp( O, V, W, a, b )
86 | {
87 | return {
88 | x: O.x + a * V.x + b * W.x,
89 | y: O.y + a * V.y + b * W.y };
90 | }
91 |
92 | function tvertex( T, p )
93 | {
94 | const P = mul( T, p );
95 | p5c.vertex( P.x, P.y );
96 | }
97 |
98 | function tbezier( T, ax, ay, bx, by, cx, cy )
99 | {
100 | const A = mul( T, { x: ax, y: ay } );
101 | const B = mul( T, { x: bx, y: by } );
102 | const C = mul( T, { x: cx, y: cy } );
103 | p5c.bezierVertex( A.x, A.y, B.x, B.y, C.x, C.y );
104 | }
105 |
106 | function drawTiling( T, alpha )
107 | {
108 | const c = Math.cos( T.theta );
109 | const s = Math.sin( T.theta );
110 |
111 | const O = { x: T.tx, y: T.ty };
112 | const V = { x: c, y: s };
113 | const W = { x: -s, y: c };
114 |
115 | const t1l = len( T.tiling.getT1() );
116 | const t2l = len( T.tiling.getT2() );
117 | const marg = 1.5 * p5c.sqrt( t1l*t1l + t2l*t2l );
118 |
119 | const pts = [
120 | samp( O, V, W, -marg, -marg ),
121 | samp( O, V, W, T.sc + marg, -marg ),
122 | samp( O, V, W, T.sc + marg, T.sc * (p5c.height/p5c.width) + marg ),
123 | samp( O, V, W, -marg, T.sc * (p5c.height/p5c.width) + marg ),
124 | ];
125 |
126 | const M = mul(
127 | [ p5c.width, 0.0, 0.0, 0.0, p5c.width, 0.0 ],
128 | inv( matchSeg( O, samp( O, V, W, T.sc, 0.0 ) ) ) );
129 |
130 | p5c.stroke( 0, alpha );
131 | p5c.strokeWeight( 1.0 );
132 | p5c.strokeJoin( p5c.ROUND );
133 | p5c.strokeCap( p5c.ROUND );
134 |
135 | for( let i of T.tiling.fillRegionQuad( pts[0], pts[1], pts[2], pts[3] ) ) {
136 | const TT = i.T;
137 | const CT = mul( M, TT );
138 |
139 | const col = T.cols[ T.tiling.getColour( i.t1, i.t2, i.aspect ) ];
140 | p5c.fill( col[0], col[1], col[2], alpha );
141 |
142 | p5c.beginShape();
143 | tvertex( CT, T.tiling.getVertex( 0 ) );
144 |
145 | for( let si of T.tiling.shape() ) {
146 | const S = mul( CT, si.T );
147 | if( si.shape == EdgeShape.I ) {
148 | tvertex( S, { x: si.rev ? 0.0 : 1.0, y: 0.0 } );
149 | } else {
150 | const ej = T.edges[si.id];
151 | if( si.rev ) {
152 | tbezier( S, ej[1].x, ej[1].y, ej[0].x, ej[0].y, 0.0, 0.0 );
153 | } else {
154 | tbezier( S, ej[0].x, ej[0].y, ej[1].x, ej[1].y, 1.0, 0.0 );
155 | }
156 | }
157 | }
158 | p5c.endShape( p5c.CLOSE );
159 | }
160 | }
161 |
162 | p5c.setup = function()
163 | {
164 | const clientWidth = document.getElementById('sktch').clientWidth;
165 | const clientHeight = document.getElementById('sktch').clientHeight;
166 |
167 | let canvas = p5c.createCanvas( clientWidth, clientHeight );
168 | canvas.parent( "sktch" );
169 |
170 | cur_tiling = createRandomTiling();
171 | next_tiling = createRandomTiling();
172 | }
173 |
174 | p5c.draw = function()
175 | {
176 | p5c.background( 255 );
177 |
178 | const cur_time = p5c.millis();
179 | let delta = cur_time - last_change;
180 |
181 | if( delta > 6000 ) {
182 | cur_tiling = next_tiling;
183 | next_tiling = createRandomTiling();
184 | last_change = cur_time;
185 | delta = 0.0;
186 | }
187 |
188 | drawTiling( cur_tiling, 255 );
189 | cur_tiling.tx += cur_tiling.dx;
190 | cur_tiling.ty += cur_tiling.dy;
191 |
192 | if( delta > 5000 ) {
193 | drawTiling( next_tiling, p5c.map( delta, 5000, 6000, 0, 255 ) );
194 | next_tiling.tx += next_tiling.dx;
195 | next_tiling.ty += next_tiling.dy;
196 | }
197 | }
198 |
199 | p5c.mousePressed = function()
200 | {
201 | cur_tiling = createRandomTiling();
202 | }
203 | };
204 |
205 | let myp5 = new p5( sktch, 'sketch0' );
206 |
--------------------------------------------------------------------------------
/demo/interactivedemo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/interactivedemo.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2018 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | // A port of the Tactile C++ demo program to P5.js.
10 |
11 | import { makeBox, EditableTiling } from './tileinfo.js';
12 | import { mul, EdgeShape, numTypes, tilingTypes, IsohedralTiling }
13 | from '../lib/tactile.js';
14 |
15 | let sktch = function( p5c )
16 | {
17 | const editor_box = makeBox( 10, 350, 200, 240 );
18 | const phys_unit = 60;
19 |
20 | let QS = null;
21 | let vals = null;
22 |
23 | let editor_pane = null;
24 | let show_controls = true;
25 | let zoom = 1.0;
26 |
27 | let the_type = null;
28 | let tiling = null;
29 |
30 | let dragging = null;
31 |
32 | const COLS = [
33 | [ 25, 52, 65 ],
34 | [ 62, 96, 111 ],
35 | [ 145, 170, 157 ],
36 | [ 209, 219, 189 ],
37 | [ 252, 255, 245 ],
38 | [ 219, 188, 209 ] ];
39 |
40 | function setTilingType()
41 | {
42 | const tp = tilingTypes[ the_type ];
43 | tiling.setType( tp );
44 |
45 | let title = "Tiling: IH";
46 | if( tp < 10 ) {
47 | title += "0";
48 | }
49 | title += tp;
50 |
51 | // I'd like to say this: QS.setTitle( title );
52 | // QuickSettings doesn't include a public API for setting the
53 | // title of a panel, so reach into the guts and twiddle the
54 | // data directly.
55 | QS._titleBar.textContent = title;
56 |
57 | const np = tiling.numParams();
58 | let vals = {};
59 | for( let idx = 0; idx < 6; ++idx ) {
60 | if( idx < np ) {
61 | QS.showControl( "v" + idx );
62 | vals["v"+idx] = tiling.getParam( idx );
63 | } else {
64 | QS.hideControl( "v" + idx );
65 | }
66 | }
67 | QS.setValuesFromJSON( vals );
68 | }
69 |
70 | function nextTilingType()
71 | {
72 | if( the_type < (numTypes-1) ) {
73 | the_type++;
74 | setTilingType();
75 | }
76 | }
77 |
78 | function prevTilingType()
79 | {
80 | if( the_type > 0 ) {
81 | the_type--;
82 | setTilingType();
83 | }
84 | }
85 |
86 | function drawTiling()
87 | {
88 | const asp = p5c.width / p5c.height;
89 | const h = 6.0 * zoom;
90 | const w = asp * h * zoom;
91 | const sc = p5c.height / (2*h);
92 | const M = mul(
93 | [1, 0, p5c.width/2.0, 0, 1, p5c.height/2.0],
94 | [sc, 0, 0, 0, -sc, 0] );
95 |
96 | p5c.stroke( COLS[0][0], COLS[0][1], COLS[0][2] );
97 | p5c.strokeWeight( 1.0 );
98 |
99 | const proto = tiling.getPrototile();
100 |
101 | for( let i of proto.fillRegionBounds(-w-2.0, -h-2.0, w+2.0, h+2.0) ) {
102 | const TT = i.T;
103 | const T = mul( M, TT );
104 |
105 | const col = COLS[ proto.getColour( i.t1, i.t2, i.aspect ) + 1 ];
106 | p5c.fill( col[0], col[1], col[2] );
107 |
108 | p5c.beginShape();
109 | for( let v of tiling.getTileShape() ) {
110 | const P = mul( T, v );
111 | p5c.vertex( P.x, P.y );
112 | }
113 | p5c.endShape( p5c.CLOSE );
114 | }
115 | }
116 |
117 | function drawEditor()
118 | {
119 | let pg = editor_pane;
120 | pg.clear();
121 |
122 | pg.fill( 252, 255, 254, 220 );
123 | pg.noStroke();
124 | pg.rect( 0, 0, editor_box.w, editor_box.h );
125 |
126 | pg.strokeWeight( 2.0 );
127 | pg.fill( COLS[3][0], COLS[3][1], COLS[3][2] );
128 |
129 | const ET = tiling.getEditorTransform();
130 | const proto = tiling.getPrototile();
131 |
132 | pg.beginShape();
133 | for( let v of tiling.getTileShape() ) {
134 | const P = mul( ET, v );
135 | pg.vertex( P.x, P.y );
136 | }
137 | pg.endShape( p5c.CLOSE );
138 |
139 | pg.noFill();
140 |
141 | // Draw edges
142 | for( let i of proto.parts() ) {
143 | if( i.shape == EdgeShape.I ) {
144 | pg.stroke( 158 );
145 | } else {
146 | pg.stroke( 0 );
147 | }
148 |
149 | const M = mul( ET, i.T );
150 | pg.beginShape();
151 | for( let v of tiling.getEdgeShape( i.id ) ) {
152 | const P = mul( M, v );
153 | pg.vertex( P.x, P.y );
154 | }
155 | pg.endShape();
156 | }
157 |
158 | // Draw tiling vertices
159 | pg.noStroke();
160 | pg.fill( 158 );
161 | for( let v of proto.vertices() ) {
162 | const pt = mul( ET, v );
163 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
164 | }
165 |
166 | // Draw editable vertices
167 | for( let i of proto.parts() ) {
168 | const shp = i.shape;
169 | const id = i.id;
170 | const ej = tiling.getEdgeShape( id );
171 | const T = mul( ET, i.T );
172 |
173 | for( let idx = 1; idx < ej.length - 1; ++idx ) {
174 | pg.fill( 0 );
175 | const pt = mul( T, ej[idx] );
176 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
177 | }
178 |
179 | if( shp == EdgeShape.I || shp == EdgeShape.J ) {
180 | continue;
181 | }
182 |
183 | // Draw symmetry points for U and S edges.
184 | if( !i.second ) {
185 | if( shp == EdgeShape.U ) {
186 | pg.fill( COLS[2][0], COLS[2][1], COLS[2][2] );
187 | } else {
188 | pg.fill( COLS[5][0], COLS[5][1], COLS[5][2] );
189 | }
190 | const pt = mul( T, ej[ej.length-1] );
191 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
192 | }
193 | }
194 |
195 | p5c.image( pg, editor_box.x, editor_box.y );
196 |
197 | p5c.strokeWeight( 3.0 );
198 | p5c.stroke( 25, 52, 65, 220 );
199 | p5c.noFill();
200 | p5c.rect( editor_box.x, editor_box.y, editor_box.w, editor_box.h );
201 | }
202 |
203 | function slide()
204 | {
205 | let params = []
206 | vals = QS.getValuesAsJSON();
207 | for( let idx = 0; idx < tiling.numParams(); ++idx ) {
208 | params.push( vals[ "v" + idx ] );
209 | }
210 | tiling.setParams( params );
211 | p5c.loop();
212 | }
213 |
214 | p5c.mouseDragged = function()
215 | {
216 | if( dragging ) {
217 | const npt =
218 | { x: p5c.mouseX - editor_box.x, y: p5c.mouseY - editor_box.y };
219 | tiling.moveEdit( npt );
220 | p5c.loop();
221 | return false;
222 | }
223 | }
224 |
225 | p5c.mousePressed = function()
226 | {
227 | dragging = false;
228 | if( !show_controls ) {
229 | return;
230 | }
231 |
232 | const pt = {
233 | x: p5c.mouseX - editor_box.x, y: p5c.mouseY - editor_box.y };
234 |
235 | if( (pt.x < 0) || (pt.x > editor_box.w) ) {
236 | return;
237 | }
238 | if( (pt.y < 0) || (pt.y > editor_box.h) ) {
239 | return;
240 | }
241 |
242 | if( tiling.startEdit( pt, p5c.keyIsDown( p5c.SHIFT ) ) ) {
243 | dragging = true;
244 | p5c.loop();
245 | } else {
246 | tiling.calcEditorTransform();
247 | p5c.loop();
248 | }
249 | }
250 |
251 | p5c.mouseReleased = function()
252 | {
253 | tiling.finishEdit();
254 | dragging = false;
255 | }
256 |
257 | p5c.keyPressed = function()
258 | {
259 | if( p5c.keyCode === p5c.RIGHT_ARROW ) {
260 | nextTilingType();
261 | p5c.loop();
262 | } else if( p5c.keyCode === p5c.LEFT_ARROW ) {
263 | prevTilingType();
264 | p5c.loop();
265 | } else if( p5c.key == ' ' ) {
266 | show_controls = !show_controls;
267 | if( show_controls ) {
268 | QS.expand();
269 | } else {
270 | QS.collapse();
271 | }
272 | p5c.loop();
273 | } else if( p5c.key == ',' || p5c.key == '<' ) {
274 | zoom /= 0.9;
275 | p5c.loop();
276 | } else if( p5c.key == '.' || p5c.key == '>' ) {
277 | zoom *= 0.9;
278 | p5c.loop();
279 | }
280 | }
281 |
282 | p5c.setup = function()
283 | {
284 | let canvas = p5c.createCanvas( 800, 600 );
285 | canvas.parent( "sktch" );
286 |
287 | tiling = new EditableTiling( editor_box.w, editor_box.h, phys_unit );
288 |
289 | let res = document.getElementById( "sktch" ).getBoundingClientRect();
290 | QS = QuickSettings.create(
291 | res.left + window.scrollX + 10, res.top + window.scrollY + 10,
292 | "Tiling: IH01" );
293 | for( let idx = 0; idx < 6; ++idx ) {
294 | QS.addRange( "v" + idx, 0, 2, 1, 0.0001, null );
295 | QS.hideControl( "v" + idx );
296 | }
297 |
298 | editor_pane = p5c.createGraphics( editor_box.w, editor_box.h );
299 |
300 | QS.setGlobalChangeHandler( slide );
301 | the_type = 0;
302 | setTilingType();
303 | }
304 |
305 | p5c.draw = function()
306 | {
307 | p5c.background( 255 );
308 |
309 | drawTiling();
310 |
311 | if( show_controls ) {
312 | drawEditor();
313 | }
314 |
315 | p5c.noLoop();
316 | }
317 | }
318 |
319 | let myp5 = new p5( sktch, 'sketch0' );
320 |
--------------------------------------------------------------------------------
/demo/minimaldemo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
A randomly generated isohedral tiling.
9 | Reload the page to generate another one.
10 |
11 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/minimaldemo.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2020 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | // A minimal demo that constructs a random tiling with random edge
10 | // shapes and then draws it, using no external library dependencies
11 | // other than Tactile itself.
12 |
13 | 'use strict'
14 |
15 | import { mul, EdgeShape, tilingTypes, IsohedralTiling }
16 | from '../lib/tactile.js';
17 |
18 | function drawRandomTiling()
19 | {
20 | var canvas = document.getElementById( 'canvas' );
21 | var ctx = canvas.getContext( '2d' );
22 |
23 | const { tiling, edges } = makeRandomTiling();
24 |
25 | // Make some random colours.
26 | let cols = [];
27 | for( let i = 0; i < 3; ++i ) {
28 | cols.push( 'rgb(' +
29 | Math.floor( Math.random() * 255.0 ) + ',' +
30 | Math.floor( Math.random() * 255.0 ) + ',' +
31 | Math.floor( Math.random() * 255.0 ) + ')' );
32 | }
33 |
34 | ctx.lineWidth = 1.0;
35 | ctx.strokeStyle = '#000';
36 |
37 | // Define a world-to-screen transformation matrix that scales by 50x.
38 | const ST = [ 50.0, 0.0, 0.0,
39 | 0.0, 50.0, 0.0 ];
40 |
41 | for( let i of tiling.fillRegionBounds( -2, -2, 12, 12 ) ) {
42 | const T = mul( ST, i.T );
43 | ctx.fillStyle = cols[ tiling.getColour( i.t1, i.t2, i.aspect ) ];
44 |
45 | let start = true;
46 | ctx.beginPath();
47 |
48 | for( let si of tiling.shape() ) {
49 | const S = mul( T, si.T );
50 | let seg = [ mul( S, { x: 0.0, y: 0.0 } ) ];
51 |
52 | if( si.shape != EdgeShape.I ) {
53 | const ej = edges[ si.id ];
54 | seg.push( mul( S, ej[0] ) );
55 | seg.push( mul( S, ej[1] ) );
56 | }
57 |
58 | seg.push( mul( S, { x: 1.0, y: 0.0 } ) );
59 |
60 | if( si.rev ) {
61 | seg = seg.reverse();
62 | }
63 |
64 | if( start ) {
65 | start = false;
66 | ctx.moveTo( seg[0].x, seg[0].y );
67 | }
68 |
69 | if( seg.length == 2 ) {
70 | ctx.lineTo( seg[1].x, seg[1].y );
71 | } else {
72 | ctx.bezierCurveTo(
73 | seg[1].x, seg[1].y,
74 | seg[2].x, seg[2].y,
75 | seg[3].x, seg[3].y );
76 | }
77 | }
78 |
79 | ctx.closePath();
80 | ctx.fill();
81 | ctx.stroke();
82 | }
83 | }
84 |
85 | function makeRandomTiling()
86 | {
87 | // Construct a tiling
88 | const tp = tilingTypes[ Math.floor( 81 * Math.random() ) ];
89 | let tiling = new IsohedralTiling( tp );
90 |
91 | // Randomize the tiling vertex parameters
92 | let ps = tiling.getParameters();
93 | for( let i = 0; i < ps.length; ++i ) {
94 | ps[i] += Math.random() * 0.1 - 0.05;
95 | }
96 | tiling.setParameters( ps );
97 |
98 | // Make some random edge shapes. Note that here, we sidestep the
99 | // potential complexity of using .shape() vs. .parts() by checking
100 | // ahead of time what the intrinsic edge shape is and building
101 | // Bezier control points that have all necessary symmetries.
102 |
103 | let edges = [];
104 | for( let i = 0; i < tiling.numEdgeShapes(); ++i ) {
105 | let ej = [];
106 | const shp = tiling.getEdgeShape( i );
107 | if( shp == EdgeShape.I ) {
108 | // Pass
109 | } else if( shp == EdgeShape.J ) {
110 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
111 | ej.push( { x: Math.random()*0.6 + 0.4, y : Math.random() - 0.5 } );
112 | } else if( shp == EdgeShape.S ) {
113 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
114 | ej.push( { x: 1.0 - ej[0].x, y: -ej[0].y } );
115 | } else if( shp == EdgeShape.U ) {
116 | ej.push( { x: Math.random()*0.6, y : Math.random() - 0.5 } );
117 | ej.push( { x: 1.0 - ej[0].x, y: ej[0].y } );
118 | }
119 |
120 | edges.push( ej );
121 | }
122 |
123 | return { tiling: tiling, edges: edges }
124 | }
125 |
126 | drawRandomTiling();
127 |
--------------------------------------------------------------------------------
/demo/quicksettings.min.js:
--------------------------------------------------------------------------------
1 | !function(){function a(){return"qs_"+ ++i}function b(a,b){var c=d("div",null,"qs_label",b);return c.innerHTML=a,c}function c(a,b,c,e){var f=d("input",b,c,e);return f.type=a,f}function d(a,b,c,d){var e=document.createElement(a);if(e)return e.id=b,c&&(e.className=c),d&&d.appendChild(e),e}function e(){return-1!=navigator.userAgent.indexOf("rv:11")||-1!=navigator.userAgent.indexOf("MSIE")}function f(){var a=navigator.userAgent.toLowerCase();return!(a.indexOf("chrome")>-1||a.indexOf("firefox")>-1||a.indexOf("epiphany")>-1)&&a.indexOf("safari/")>-1}function g(){return navigator.userAgent.toLowerCase().indexOf("edge")>-1}function h(){var a=document.createElement("style");a.innerText=k,document.head.appendChild(a),j=!0}var i=0,j=!1,k=".qs_main{background-color:#dddddd;text-align:left;position:absolute;width:200px;font:12px sans-serif;box-shadow:5px 5px 8px rgba(0,0,0,0.35);user-select:none;-webkit-user-select:none;color:#000000;border:none}.qs_content{background-color:#cccccc;overflow-y:auto}.qs_title_bar{background-color:#eeeeee;user-select:none;-webkit-user-select:none;cursor:pointer;padding:5px;font-weight:bold;border:none;color:#000000}.qs_container{margin:5px;padding:5px;background-color:#eeeeee;border:none;position:relative}.qs_container_selected{border:none;background-color:#ffffff}.qs_range{-webkit-appearance:none;-moz-appearance:none;width:100%;height:17px;padding:0;margin:0;background-color:transparent;border:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.qs_range:focus{outline:none;border:none}.qs_range::-webkit-slider-runnable-track{width:100%;height:15px;cursor:pointer;background:#cccccc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.qs_range:focus::-webkit-slider-runnable-track{background:#cccccc}.qs_range::-webkit-slider-thumb{-webkit-appearance:none;height:15px;width:15px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;background:#999999;cursor:pointer;margin-top:0}.qs_range::-moz-range-track{width:100%;height:15px;cursor:pointer;background:#cccccc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.qs_range::-moz-range-thumb{height:15px;width:15px;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;background:#999999;cursor:pointer}.qs_range::-ms-track{width:100%;height:15px;cursor:pointer;visibility:hidden;background:transparent}.qs_range::-ms-thumb{height:15px;width:15px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;background:#999999;cursor:pointer;border:none}.qs_range::-ms-fill-lower{background:#cccccc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.qs_range:focus::-ms-fill-lower{background:#cccccc}.qs_range::-ms-fill-upper{background:#cccccc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.qs_range:focus::-ms-fill-upper{background:#cccccc}.qs_button{background-color:#f6f6f6;color:#000000;height:30px;border:1px solid #aaaaaa;font:12px sans-serif}.qs_button:active{background-color:#ffffff;border:1px solid #aaaaaa}.qs_button:focus{border:1px solid #aaaaaa;outline:none}.qs_checkbox{cursor:pointer;display:inline}.qs_checkbox input{position:absolute;left:-99999px}.qs_checkbox span{height:16px;width:100%;display:block;text-indent:20px;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAALklEQVQ4T2OcOXPmfwYKACPIgLS0NLKMmDVrFsOoAaNhMJoOGBioFwZkZUWoJgApdFaxjUM1YwAAAABJRU5ErkJggg==') no-repeat}.qs_checkbox input:checked+span{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAvElEQVQ4T63Tyw2EIBAA0OFKBxBL40wDRovAUACcKc1IB1zZDAkG18GYZTmSmafzgTnnMgwchoDWGlJKheGcP3JtnPceCqCUAmttSZznuYtgchsXQrgC+77DNE0kUpPbmBOoJaBOIVQylnqWgAAeKhDve/AN+EaklJBzhhgjWRoJVGTbNjiOowAIret6a+4jYIwpX8aDwLIs74C2D0IIYIyVP6Gm898m9kbVm85ljHUTf16k4VUefkwDrxk+zoUEwCt0GbUAAAAASUVORK5CYII=') no-repeat}.qs_checkbox_label{position:absolute;top:7px;left:30px}.qs_label{margin-bottom:3px;user-select:none;-webkit-user-select:none;cursor:default;font:12px sans-serif}.qs_text_input{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%;padding:0 0 0 5px;height:24px;border:1px inset #ffffff;background-color:#ffffff;color:#000000;font-size:12px}.qs_text_input:focus{outline:none;background:#ffffff;border:1px inset #ffffff}.qs_select{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAp0lEQVRIS+2SsQ3FIAwF7RVYhA5mgQFhFuhYhJKWL0eKxI8SGylKZ0p4+OBsHGNM+HChAiS7qkgyBKrovaLeOxhjbgtxZ+cFtgelFMg5QwgBvPd/EO5sDbKAlBLUWo/8CjmL075zDmKMj6rEKbpCqBL9aqc4ZUQAhVbInBMQUXz5Vg/WfxOktXZsWWtZLds9uIqlqaH1NFV3jdhSJA47E1CAaE8ViYp+wGiWMZ/T+cgAAAAASUVORK5CYII=') no-repeat right #f6f6f6;-webkit-appearance:none;-moz-appearance:none;appearance:none;color:#000000;width:100%;height:24px;border:1px solid #aaaaaa;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;padding:0 5px;-moz-outline:none;font-size:14px}.qs_select option{font-size:14px}.qs_select::-ms-expand{display:none}.qs_select:focus{outline:none}.qs_number{height:24px}.qs_image{width:100%}.qs_progress{width:100%;height:15px;background-color:#cccccc;border:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.qs_progress_value{height:100%;background-color:#999999}.qs_textarea{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;resize:vertical;width:100%;padding:3px 5px;border:1px inset #ffffff;background-color:#ffffff;color:#000000;font-size:12px}.qs_textarea:focus{outline:none;background:#ffffff;border:1px inset #ffffff}.qs_color{position:absolute;left:-999999px}.qs_color_label{width:100%;height:20px;display:block;border:1px solid #aaaaaa;cursor:pointer;padding:0 0 0 5px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.qs_file_chooser{position:absolute;left:-999999px}.qs_file_chooser_label{background-color:#f6f6f6;color:#000000;height:30px;border:1px solid #aaaaaa;font:12px sans-serif;width:100%;display:block;cursor:pointer;padding:7px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",l={_version:"3.0.2",_topZ:1,_panel:null,_titleBar:null,_content:null,_startX:0,_startY:0,_hidden:!1,_collapsed:!1,_controls:null,_keyCode:-1,_draggable:!0,_collapsible:!0,_globalChangeHandler:null,useExtStyleSheet:function(){j=!0},create:function(a,b,c,d){var e=Object.create(this);return e._init(a,b,c,d),e},destroy:function(){this._panel.parentElement&&this._panel.parentElement.removeChild(this._panel);for(var a in this)this[a]=null},_init:function(a,b,c,d){j||h(),this._bindHandlers(),this._createPanel(a,b,d),this._createTitleBar(c||"QuickSettings"),this._createContent()},_bindHandlers:function(){this._startDrag=this._startDrag.bind(this),this._drag=this._drag.bind(this),this._endDrag=this._endDrag.bind(this),this._doubleClickTitle=this._doubleClickTitle.bind(this),this._onKeyUp=this._onKeyUp.bind(this)},getValuesAsJSON:function(a){var b={};for(var c in this._controls)this._controls[c].getValue&&(b[c]=this._controls[c].getValue());return a&&(b=JSON.stringify(b)),b},setValuesFromJSON:function(a){"string"==typeof a&&(a=JSON.parse(a));for(var b in a)this._controls[b]&&this._controls[b].setValue&&this._controls[b].setValue(a[b]);return this},saveInLocalStorage:function(a){return this._localStorageName=a,this._readFromLocalStorage(a),this},clearLocalStorage:function(a){return localStorage.removeItem(a),this},_saveInLocalStorage:function(a){localStorage.setItem(a,this.getValuesAsJSON(!0))},_readFromLocalStorage:function(a){var b=localStorage.getItem(a);b&&this.setValuesFromJSON(b)},_createPanel:function(a,b,c){this._panel=d("div",null,"qs_main",c||document.body),this._panel.style.zIndex=++l._topZ,this.setPosition(a||0,b||0),this._controls={}},_createTitleBar:function(a){this._titleBar=d("div",null,"qs_title_bar",this._panel),this._titleBar.textContent=a,this._titleBar.addEventListener("mousedown",this._startDrag),this._titleBar.addEventListener("dblclick",this._doubleClickTitle)},_createContent:function(){this._content=d("div",null,"qs_content",this._panel)},_createContainer:function(){var a=d("div",null,"qs_container");return a.addEventListener("focus",function(){this.className+=" qs_container_selected"},!0),a.addEventListener("blur",function(){var a=this.className.indexOf(" qs_container_selected");a>-1&&(this.className=this.className.substr(0,a))},!0),this._content.appendChild(a),a},setPosition:function(a,b){return this._panel.style.left=a+"px",this._panel.style.top=Math.max(b,0)+"px",this},setSize:function(a,b){return this._panel.style.width=a+"px",this._content.style.width=a+"px",this._content.style.height=b-this._titleBar.offsetHeight+"px",this},setWidth:function(a){return this._panel.style.width=a+"px",this._content.style.width=a+"px",this},setHeight:function(a){return this._content.style.height=a-this._titleBar.offsetHeight+"px",this},setDraggable:function(a){return this._draggable=a,this._draggable||this._collapsible?this._titleBar.style.cursor="pointer":this._titleBar.style.cursor="default",this},_startDrag:function(a){this._draggable&&(this._panel.style.zIndex=++l._topZ,document.addEventListener("mousemove",this._drag),document.addEventListener("mouseup",this._endDrag),this._startX=a.clientX,this._startY=a.clientY),a.preventDefault()},_drag:function(a){var b=parseInt(this._panel.style.left),c=parseInt(this._panel.style.top),d=a.clientX,e=a.clientY;this.setPosition(b+d-this._startX,c+e-this._startY),this._startX=d,this._startY=e,a.preventDefault()},_endDrag:function(a){document.removeEventListener("mousemove",this._drag),document.removeEventListener("mouseup",this._endDrag),a.preventDefault()},setGlobalChangeHandler:function(a){return this._globalChangeHandler=a,this},_callGCH:function(a){this._localStorageName&&this._saveInLocalStorage(this._localStorageName),this._globalChangeHandler&&this._globalChangeHandler(a)},hide:function(){return this._panel.style.visibility="hidden",this._hidden=!0,this},show:function(){return this._panel.style.visibility="visible",this._panel.style.zIndex=++l._topZ,this._hidden=!1,this},toggleVisibility:function(){return this._hidden?this.show():this.hide(),this},setCollapsible:function(a){return this._collapsible=a,this._draggable||this._collapsible?this._titleBar.style.cursor="pointer":this._titleBar.style.cursor="default",this},collapse:function(){return this._panel.removeChild(this._content),this._collapsed=!0,this},expand:function(){return this._panel.appendChild(this._content),this._collapsed=!1,this},toggleCollapsed:function(){return this._collapsed?this.expand():this.collapse(),this},setKey:function(a){return this._keyCode=a.toUpperCase().charCodeAt(0),document.addEventListener("keyup",this._onKeyUp),this},_onKeyUp:function(a){a.keyCode===this._keyCode&&["INPUT","SELECT","TEXTAREA"].indexOf(a.target.tagName)<0&&this.toggleVisibility()},_doubleClickTitle:function(){this._collapsible&&this.toggleCollapsed()},removeControl:function(a){if(this._controls[a])var b=this._controls[a].container;return b&&b.parentElement&&b.parentElement.removeChild(b),this._controls[a]=null,this},enableControl:function(a){return this._controls[a]&&(this._controls[a].control.disabled=!1),this},disableControl:function(a){return this._controls[a]&&(this._controls[a].control.disabled=!0),this},hideControl:function(a){return this._controls[a]&&(this._controls[a].container.style.display="none"),this},showControl:function(a){return this._controls[a]&&(this._controls[a].container.style.display="block"),this},overrideStyle:function(a,b,c){return this._controls[a]&&(this._controls[a].control.style[b]=c),this},hideTitle:function(a){var b=this._controls[a].label;return b&&(b.style.display="none"),this},showTitle:function(a){var b=this._controls[a].label;return b&&(b.style.display="block"),this},hideAllTitles:function(){for(var a in this._controls){var b=this._controls[a].label;b&&(b.style.display="none")}return this},showAllTitles:function(){for(var a in this._controls){var b=this._controls[a].label;b&&(b.style.display="block")}return this},getValue:function(a){return this._controls[a].getValue()},setValue:function(a,b){return this._controls[a].setValue(b),this._callGCH(a),this},addBoolean:function(b,e,f){var g=this._createContainer(),h=a(),i=d("label",null,"qs_checkbox_label",g);i.textContent=b,i.setAttribute("for",h);var j=d("label",null,"qs_checkbox",g);j.setAttribute("for",h);var k=c("checkbox",h,null,j);k.checked=e;d("span",null,null,j);this._controls[b]={container:g,control:k,getValue:function(){return this.control.checked},setValue:function(a){this.control.checked=a,f&&f(a)}};var l=this;return k.addEventListener("change",function(){f&&f(k.checked),l._callGCH(b)}),this},bindBoolean:function(a,b,c){return this.addBoolean(a,b,function(b){c[a]=b})},addButton:function(b,d){var e=this._createContainer(),f=c("button",a(),"qs_button",e);f.value=b,this._controls[b]={container:e,control:f};var g=this;return f.addEventListener("click",function(){d&&d(f),g._callGCH(b)}),this},addColor:function(h,i,j){if(f()||g()||e())return this.addText(h,i,j);var k=this._createContainer(),l=b(""+h+": "+i,k),m=a(),n=c("color",m,"qs_color",k);n.value=i||"#ff0000";var o=d("label",null,"qs_color_label",k);o.setAttribute("for",m),o.style.backgroundColor=n.value,this._controls[h]={container:k,control:n,colorLabel:o,label:l,title:h,getValue:function(){return this.control.value},setValue:function(a){this.control.value=a,this.colorLabel.style.backgroundColor=n.value,this.label.innerHTML=""+this.title+": "+this.control.value,j&&j(a)}};var p=this;return n.addEventListener("input",function(){l.innerHTML=""+h+": "+n.value,o.style.backgroundColor=n.value,j&&j(n.value),p._callGCH(h)}),this},bindColor:function(a,b,c){return this.addColor(a,b,function(b){c[a]=b})},addDate:function(d,f,g){function h(a){if(a instanceof Date){var b=a.getFullYear(),c=a.getMonth()+1;c<10&&(c="0"+c);var d=a.getDate();return d<10&&(d="0"+d),b+"-"+c+"-"+d}return a}var i=h(f);if(e())return this.addText(d,i,g);var j=this._createContainer(),k=b(""+d+"",j),l=c("date",a(),"qs_text_input",j);l.value=i||"",this._controls[d]={container:j,control:l,label:k,getValue:function(){return this.control.value},setValue:function(a){var b=h(a);this.control.value=b||"",g&&g(b)}};var m=this;return l.addEventListener("input",function(){g&&g(l.value),m._callGCH(d)}),this},bindDate:function(a,b,c){return this.addDate(a,b,function(b){c[a]=b})},addDropDown:function(a,c,e){for(var f=this._createContainer(),g=b(""+a+"",f),h=d("select",null,"qs_select",f),i=0;i"+a+"",d);return d.appendChild(c),this._controls[a]={container:d,label:e},this},addFileChooser:function(e,f,g,h){var i=this._createContainer(),j=b(""+e+"",i),k=a(),l=c("file",k,"qs_file_chooser",i);g&&(l.accept=g);var m=d("label",null,"qs_file_chooser_label",i);m.setAttribute("for",k),m.textContent=f||"Choose a file...",this._controls[e]={container:i,control:l,label:j,getValue:function(){return this.control.files[0]}};var n=this;return l.addEventListener("change",function(){l.files&&l.files.length&&(m.textContent=l.files[0].name,h&&h(l.files[0]),n._callGCH(e))}),this},addHTML:function(a,c){var e=this._createContainer(),f=b(""+a+": ",e),g=d("div",null,null,e);return g.innerHTML=c,this._controls[a]={container:e,label:f,control:g,getValue:function(){return this.control.innerHTML},setValue:function(a){this.control.innerHTML=a}},this},addImage:function(a,c,e){var f=this._createContainer(),g=b(""+a+"",f);return img=d("img",null,"qs_image",f),img.src=c,this._controls[a]={container:f,control:img,label:g,getValue:function(){return this.control.src},setValue:function(a){this.control.src=a,e&&img.addEventListener("load",function b(){img.removeEventListener("load",b),e(a)})}},this},addRange:function(a,b,c,d,e,f){return this._addNumber("range",a,b,c,d,e,f)},addNumber:function(a,b,c,d,e,f){return this._addNumber("number",a,b,c,d,e,f)},_addNumber:function(d,f,g,h,i,j,k){var l=this._createContainer(),m=b("",l),n="range"===d?"qs_range":"qs_text_input qs_number",o=c(d,a(),n,l);o.min=g||0,o.max=h||100,o.step=j||1,o.value=i||0,m.innerHTML=""+f+": "+o.value,this._controls[f]={container:l,control:o,label:m,title:f,callback:k,getValue:function(){return parseFloat(this.control.value)},setValue:function(a){this.control.value=a,this.label.innerHTML=""+this.title+": "+this.control.value,k&&k(parseFloat(a))}};var p="input";"range"===d&&e()&&(p="change");var q=this;return o.addEventListener(p,function(){m.innerHTML=""+f+": "+o.value,k&&k(parseFloat(o.value)),q._callGCH(f)}),this},bindRange:function(a,b,c,d,e,f){return this.addRange(a,b,c,d,e,function(b){f[a]=b})},bindNumber:function(a,b,c,d,e,f){return this.addNumber(a,b,c,d,e,function(b){f[a]=b})},setRangeParameters:function(a,b,c,d){return this.setNumberParameters(a,b,c,d)},setNumberParameters:function(a,b,c,d){var e=this._controls[a],f=e.control.value;return e.control.min=b,e.control.max=c,e.control.step=d,e.control.value!==f&&e.callback&&e.callback(e.control.value),this},addPassword:function(a,b,c){return this._addText("password",a,b,c)},bindPassword:function(a,b,c){return this.addPassword(a,b,function(b){c[a]=b})},addProgressBar:function(a,c,e,f){var g=this._createContainer(),h=b("",g),i=d("div",null,"qs_progress",g),j=d("div",null,"qs_progress_value",i);return j.style.width=e/c*100+"%",h.innerHTML="numbers"===f?""+a+": "+e+" / "+c:"percent"===f?""+a+": "+Math.round(e/c*100)+"%":""+a+"",this._controls[a]={container:g,control:i,valueDiv:j,valueDisplay:f,label:h,value:e,max:c,title:a,getValue:function(){return this.value},setValue:function(a){this.value=Math.max(0,Math.min(a,this.max)),this.valueDiv.style.width=this.value/this.max*100+"%","numbers"===this.valueDisplay?this.label.innerHTML=""+this.title+": "+this.value+" / "+this.max:"percent"===this.valueDisplay&&(this.label.innerHTML=""+this.title+": "+Math.round(this.value/this.max*100)+"%")}},this},setProgressMax:function(a,b){var c=this._controls[a];return c.max=b,c.value=Math.min(c.value,c.max),c.valueDiv.style.width=c.value/c.max*100+"%","numbers"===c.valueDisplay?c.label.innerHTML=""+c.title+": "+c.value+" / "+c.max:"percent"===c.valueDisplay?c.label.innerHTML=""+c.title+": "+Math.round(c.value/c.max*100)+"%":c.label.innerHTML=""+c.title+"",this},addText:function(a,b,c){return this._addText("text",a,b,c)},_addText:function(e,f,g,h){var i,j=this._createContainer(),k=b(""+f+"",j);"textarea"===e?(i=d("textarea",a(),"qs_textarea",j),i.rows=5):i=c(e,a(),"qs_text_input",j),i.value=g||"",this._controls[f]={container:j,control:i,label:k,getValue:function(){return this.control.value},setValue:function(a){this.control.value=a,h&&h(a)}};var l=this;return i.addEventListener("input",function(){h&&h(i.value),l._callGCH(f)}),this},bindText:function(a,b,c){return this.addText(a,b,function(b){c[a]=b})},addTextArea:function(a,b,c){return this._addText("textarea",a,b,c)},setTextAreaRows:function(a,b){return this._controls[a].control.rows=b,this},bindTextArea:function(a,b,c){return this.addTextArea(a,b,function(b){c[a]=b})},addTime:function(d,f,g){var h;if(f instanceof Date){var i=f.getHours();i<10&&(i="0"+i);var j=f.getMinutes();j<10&&(j="0"+j);var k=f.getSeconds();k<10&&(k="0"+k),h=i+":"+j+":"+k}else h=f;if(e())return this.addText(d,h,g);var l=this._createContainer(),m=b(""+d+"",l),n=c("time",a(),"qs_text_input",l);n.value=h||"",this._controls[d]={container:l,control:n,label:m,getValue:function(){return this.control.value},setValue:function(a){var b;if(a instanceof Date){var c=a.getHours();c<10&&(c="0"+c);var d=a.getMinutes();d<10&&(d="0"+d);var e=a.getSeconds();e<10&&(e="0"+e),b=c+":"+d+":"+e}else b=a;this.control.value=b||"",g&&g(b)}};var o=this;return n.addEventListener("input",function(){g&&g(n.value),o._callGCH(d)}),this},bindTime:function(a,b,c){return this.addTime(a,b,function(b){c[a]=b})}};"object"==typeof exports&&"object"==typeof module?module.exports=l:"function"==typeof define&&define.amd?define(l):window.QuickSettings=l}();
--------------------------------------------------------------------------------
/demo/tileinfo.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2020 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | 'use strict';
10 |
11 | import { mul, matchSeg, EdgeShape, numTypes, tilingTypes, IsohedralTiling }
12 | from '../lib/tactile.js';
13 |
14 | // A collection of utilities and classes that are generally useful when
15 | // displaying and manipulating isohedral tilings interactively.
16 |
17 | function sub( V, W )
18 | {
19 | return { x: V.x-W.x, y: V.y-W.y };
20 | }
21 |
22 | function dot( V, W )
23 | {
24 | return V.x*W.x + V.y*W.y;
25 | }
26 |
27 | function len( V )
28 | {
29 | return Math.sqrt( dot( V, V ) );
30 | }
31 |
32 | function ptdist( V, W )
33 | {
34 | return len( sub( V, W ) );
35 | }
36 |
37 | function normalize( V )
38 | {
39 | const l = len( V );
40 | return { x: V.x / l, y: V.y / l };
41 | }
42 |
43 | // 2D affine matrix inverse
44 | function inv( T )
45 | {
46 | const det = T[0]*T[4] - T[1]*T[3];
47 | return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det,
48 | -T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det];
49 | }
50 |
51 | // Shortest distance from point P to line segment AB.
52 | function distToSeg( P, A, B )
53 | {
54 | const qmp = sub( B, A );
55 | const t = dot( sub( P, A ), qmp ) / dot( qmp, qmp );
56 | if( (t >= 0.0) && (t <= 1.0) ) {
57 | return len( sub( P, { x: A.x + t*qmp.x, y : A.y + t*qmp.y } ) );
58 | } else if( t < 0.0 ) {
59 | return len( sub( P, A ) );
60 | } else {
61 | return len( sub( P, B ) );
62 | }
63 | }
64 |
65 | function makeBox( x, y, w, h )
66 | {
67 | return { x: x, y: y, w: w, h: h };
68 | }
69 |
70 | function hitBox( x, y, B )
71 | {
72 | return (x >= B.x) && (x <= (B.x+B.w)) && (y >= B.y) && (y <= (B.y+B.h));
73 | }
74 |
75 | class EditableTiling
76 | {
77 | constructor( ew, eh, phys_unit )
78 | {
79 | this.edit_w = ew;
80 | this.edit_h = eh;
81 | this.phys_unit = phys_unit;
82 | this.tiling = null;
83 |
84 | this.u_constrain = false;
85 | }
86 |
87 | setType( tp )
88 | {
89 | this.the_type = tp;
90 | this.tiling = new IsohedralTiling( tp );
91 | this.params = this.tiling.getParameters();
92 |
93 | this.edges = [];
94 | for( let idx = 0; idx < this.tiling.numEdgeShapes(); ++idx ) {
95 | this.edges.push( [{ x: 0, y: 0 }, { x: 1, y: 0 }] );
96 | }
97 |
98 | this.cacheTileShape();
99 | this.calcEditorTransform();
100 | }
101 |
102 | getPrototile()
103 | {
104 | return this.tiling;
105 | }
106 |
107 | getEdgeShape( idx )
108 | {
109 | return this.edges[ idx ];
110 | }
111 |
112 | cacheTileShape()
113 | {
114 | this.tile_shape = [];
115 |
116 | for( let i of this.tiling.parts() ) {
117 | const ej = this.edges[i.id];
118 | let cur = i.rev ? (ej.length-2) : 1;
119 | const inc = i.rev ? -1 : 1;
120 |
121 | for( let idx = 0; idx < ej.length - 1; ++idx ) {
122 | this.tile_shape.push( mul( i.T, ej[cur] ) );
123 | cur += inc;
124 | }
125 | }
126 | }
127 |
128 | getTileShape()
129 | {
130 | return this.tile_shape;
131 | }
132 |
133 | calcEditorTransform()
134 | {
135 | console.log( this );
136 |
137 | let xmin = 1e7;
138 | let xmax = -1e7;
139 | let ymin = 1e7;
140 | let ymax = -1e7;
141 |
142 | for( let v of this.tile_shape ) {
143 | xmin = Math.min( xmin, v.x );
144 | xmax = Math.max( xmax, v.x );
145 | ymin = Math.min( ymin, v.y );
146 | ymax = Math.max( ymax, v.y );
147 | }
148 |
149 | const sc = Math.min(
150 | (this.edit_w-50) / (xmax-xmin), (this.edit_h-50) / (ymax-ymin) );
151 |
152 | this.editor_T = mul(
153 | [sc, 0, 0.5*this.edit_w, 0, -sc, 0.5*this.edit_h],
154 | [1, 0, -0.5*(xmin+xmax), 0, 1, -0.5*(ymin+ymax)] );
155 | }
156 |
157 | getEditorTransform()
158 | {
159 | return this.editor_T;
160 | }
161 |
162 | setEditorTransform( T )
163 | {
164 | this.editor_T = T;
165 | }
166 |
167 | getParam( idx )
168 | {
169 | return this.params[ idx ];
170 | }
171 |
172 | setParam( idx, v )
173 | {
174 | this.params[ idx ] = v;
175 | this.tiling.setParameters( params );
176 | this.cacheTileShape();
177 | }
178 |
179 | setParams( vs )
180 | {
181 | this.tiling.setParameters( vs );
182 | this.cacheTileShape();
183 | }
184 |
185 | numParams()
186 | {
187 | return this.tiling.numParameters();
188 | }
189 |
190 | startEdit( pt, do_del = false )
191 | {
192 | for( let i of this.tiling.parts() ) {
193 | const shp = i.shape;
194 |
195 | // No interaction possible with an I edge.
196 | if( shp == EdgeShape.I ) {
197 | continue;
198 | }
199 |
200 | const id = i.id;
201 | let ej = this.edges[id];
202 | const T = mul( this.editor_T, i.T );
203 | let P = mul( T, ej[0] );
204 |
205 | for( let idx = 1; idx < ej.length; ++idx ) {
206 | let Q = mul( T, ej[idx] );
207 | if( ptdist( Q, pt ) < 0.5 * this.phys_unit ) {
208 | this.u_constrain = false;
209 | if( idx == (ej.length-1) ) {
210 | if( shp == EdgeShape.U && !i.second ) {
211 | this.u_constrain = true;
212 | } else {
213 | break;
214 | }
215 | } else if( do_del ) {
216 | ej.splice( idx, 1 );
217 | this.cacheTileShape();
218 | return false;
219 | }
220 |
221 | this.drag_edge_shape = id;
222 | this.drag_vertex = idx;
223 | this.drag_T = inv( T );
224 | this.down_motion = pt;
225 |
226 | // Set timer for deletion. But not on a U vertex.
227 | if( !this.u_constrain ) {
228 | this.delete_timer = setTimeout(
229 | this.deleteVertex, 1000 );
230 | }
231 |
232 | return true;
233 | }
234 |
235 | // Check segment
236 | if( distToSeg( pt, P, Q ) < 20 ) {
237 | this.drag_edge_shape = id;
238 | this.drag_vertex = idx;
239 | this.drag_T = inv( T );
240 | this.down_motion = pt;
241 | // Don't set timer -- can't delete new vertex.
242 |
243 | ej.splice( idx, 0, mul( this.drag_T, pt ) );
244 | this.cacheTileShape();
245 |
246 | return true;
247 | }
248 |
249 | P = Q;
250 | }
251 | }
252 |
253 | return false;
254 | }
255 |
256 | moveEdit( pt )
257 | {
258 | const npt = mul( this.drag_T, pt );
259 |
260 | if( this.u_constrain ) {
261 | npt.x = 1.0;
262 | }
263 |
264 | const d = ptdist( pt, this.down_motion );
265 | if( d > 10 ) {
266 | // You've moved far enough, so don't delete.
267 | if( this.delete_timer != null ) {
268 | clearTimeout( this.delete_timer );
269 | this.delete_timer = null;
270 | }
271 | }
272 |
273 | this.edges[this.drag_edge_shape][this.drag_vertex] = npt;
274 | this.cacheTileShape();
275 | }
276 |
277 | finishEdit()
278 | {
279 | this.u_constrain = false;
280 |
281 | if( this.delete_timer ) {
282 | clearTimeout( this.delete_timer );
283 | this.delete_timer = null;
284 | }
285 | }
286 | };
287 |
288 | export {
289 | sub,
290 | dot,
291 | len,
292 | ptdist,
293 | normalize,
294 | inv,
295 | distToSeg,
296 | makeBox,
297 | hitBox,
298 | EditableTiling
299 | };
300 |
--------------------------------------------------------------------------------
/demo/touchdemo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/touchdemo.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2018 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | // Here's a slightly fancier demo, intended to work well on mouse
10 | // and touch interfaces. It's a bit messier -- hopefully I'll clean
11 | // up the code in the future.
12 |
13 | import { mul, matchSeg, EdgeShape, numTypes, tilingTypes, IsohedralTiling }
14 | from '../lib/tactile.js';
15 |
16 | let sktch = function( p5c )
17 | {
18 | let the_type = null;
19 | let params = null;
20 | let tiling = null;
21 | let edges = null;
22 | let tile_shape = null;
23 |
24 | let phys_unit; // Ideally, about a centimeter
25 | let edit_button_box = null;
26 | let save_button_box = null;
27 | let prev_box = null;
28 | let next_box = null;
29 | let navigator_box = null;
30 | let edit_box = null;
31 | let slide_w = null;
32 | let slide_h = null;
33 |
34 | const Mode = {
35 | NONE : 0,
36 | MOVE_VERTEX : 1,
37 | ADJ_TILE : 2,
38 | ADJ_TV : 3,
39 | ADJ_TILING : 4
40 | };
41 |
42 | let tiling_T = null;
43 | let tiling_iT = null;
44 |
45 | let tiling_T_down = null;
46 |
47 | let mode = Mode.NONE;
48 | let drag_tv = null;
49 | let drag_tv_offs = null;
50 |
51 | let editor_T;
52 | let editor_T_down;
53 | let drag_edge_shape = -1;
54 | let drag_vertex = -1;
55 | let drag_T = null;
56 | let u_constrain = false;
57 |
58 | let down_motion = null;
59 | let delete_timer = null;
60 |
61 | let editor_pane = null;
62 | let show_controls = false;
63 |
64 | let msgs = [];
65 | let DEBUG = true;
66 | function dbg( s ) {
67 | if( DEBUG ) {
68 | msgs.push( s );
69 | loop();
70 | }
71 | }
72 |
73 | const COLS = [
74 | [ 25, 52, 65 ],
75 | [ 62, 96, 111 ],
76 | [ 145, 170, 157 ],
77 | [ 209, 219, 189 ],
78 | [ 252, 255, 245 ],
79 | [ 219, 188, 209 ] ];
80 |
81 | function sub( V, W ) { return { x: V.x-W.x, y: V.y-W.y }; }
82 | function dot( V, W ) { return V.x*W.x + V.y*W.y; }
83 | function len( V ) { return Math.sqrt( dot( V, V ) ); }
84 | function ptdist( V, W ) { return len( sub( V, W ) ); }
85 | function inv( T ) {
86 | const det = T[0]*T[4] - T[1]*T[3];
87 | return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det,
88 | -T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det];
89 | }
90 | function normalize( V ) {
91 | const l = len( V );
92 | return { x: V.x / l, y: V.y / l };
93 | }
94 |
95 | function makeBox( x, y, w, h )
96 | {
97 | return { x: x, y: y, w: w, h: h };
98 | }
99 |
100 | function hitBox( x, y, B )
101 | {
102 | return (x >= B.x) && (x <= (B.x+B.w)) && (y >= B.y) && (y <= (B.y+B.h));
103 | }
104 |
105 | let fake_serial = 123456;
106 | let all_touch_ids = [];
107 | let my_touches = {};
108 | let num_touches = 0;
109 | let max_touches = 1;
110 |
111 | function addTouch( x, y, id )
112 | {
113 | if( num_touches < max_touches ) {
114 | my_touches[id] = {
115 | down: { x: x, y: y },
116 | prev: { x: x, y: y },
117 | pos: { x: x, y: y },
118 | id: id,
119 | t: p5c.millis() };
120 | ++num_touches;
121 | doTouchStarted( id );
122 | }
123 | }
124 |
125 | p5c.touchStarted = function()
126 | {
127 | if( p5c.touches.length == 0 ) {
128 | addTouch( p5c.mouseX, p5c.mouseY, fake_serial );
129 | ++fake_serial;
130 | } else {
131 | all_touch_ids = [];
132 | for( let tch of p5c.touches ) {
133 | all_touch_ids.push( tch.id );
134 |
135 | if( !(tch.id in my_touches) ) {
136 | addTouch( tch.x, tch.y, tch.id );
137 | }
138 | }
139 | }
140 |
141 | return false;
142 | }
143 |
144 | p5c.touchMoved = function()
145 | {
146 | if( num_touches > 0 ) {
147 | if( p5c.touches.length == 0 ) {
148 | for( let k in my_touches ) {
149 | let tch = my_touches[k];
150 |
151 | tch.prev = tch.pos;
152 | tch.pos = { x: p5c.mouseX, y: p5c.mouseY };
153 | }
154 | } else {
155 | for( let tch of p5c.touches ) {
156 | if( tch.id in my_touches ) {
157 | let atch = my_touches[ tch.id ];
158 | atch.prev = atch.pos;
159 | atch.pos = { x: tch.x, y: tch.y };
160 | }
161 | }
162 | }
163 |
164 | doTouchMoved();
165 | }
166 | return false;
167 | }
168 |
169 | p5c.touchEnded = function()
170 | {
171 | // If we're on a mouse device, touches will be empty and this should
172 | // work regardless.
173 |
174 | let new_ids = [];
175 |
176 | for( let k in my_touches ) {
177 | my_touches[k].present = false;
178 | }
179 |
180 | for( let tch of p5c.touches ) {
181 | const id = tch.id;
182 | new_ids.push( id );
183 | if( id in my_touches ) {
184 | my_touches[id].present = true;
185 | }
186 | }
187 |
188 | for( let k in my_touches ) {
189 | if( !my_touches[k].present ) {
190 | // This one is going away.
191 | doTouchEnded( k );
192 | delete my_touches[ k ];
193 | --num_touches;
194 | }
195 | }
196 |
197 | u_constrain = false;
198 |
199 | return false;
200 | }
201 |
202 | function cacheTileShape()
203 | {
204 | tile_shape = [];
205 |
206 | for( let i of tiling.parts() ) {
207 | const ej = edges[i.id];
208 | let cur = i.rev ? (ej.length-2) : 1;
209 | const inc = i.rev ? -1 : 1;
210 |
211 | for( let idx = 0; idx < ej.length - 1; ++idx ) {
212 | tile_shape.push( mul( i.T, ej[cur] ) );
213 | cur += inc;
214 | }
215 | }
216 | }
217 |
218 | function setTilingType()
219 | {
220 | const tp = tilingTypes[ the_type ];
221 | tiling.reset( tp );
222 | params = tiling.getParameters();
223 |
224 | edges = [];
225 | for( let idx = 0; idx < tiling.numEdgeShapes(); ++idx ) {
226 | let ej = [{ x: 0, y: 0 }, { x: 1, y: 0 }];
227 | edges.push( ej );
228 | }
229 |
230 | cacheTileShape();
231 | calcEditorTransform();
232 | }
233 |
234 | function nextTilingType()
235 | {
236 | if( the_type < (numTypes-1) ) {
237 | the_type++;
238 | setTilingType();
239 | }
240 | }
241 |
242 | function prevTilingType()
243 | {
244 | if( the_type > 0 ) {
245 | the_type--;
246 | setTilingType();
247 | }
248 | }
249 |
250 | function getTilingRect()
251 | {
252 | const ww = window.innerWidth;
253 | const hh = window.innerHeight;
254 |
255 | const t1l = len( tiling.getT1() );
256 | const t2l = len( tiling.getT2() );
257 |
258 | const margin = Math.sqrt( t1l*t1l + t2l*t2l );
259 |
260 | const pts = [
261 | mul( tiling_iT, { x: 0, y: hh } ),
262 | mul( tiling_iT, { x: ww, y: hh } ),
263 | mul( tiling_iT, { x: ww, y: 0 } ),
264 | mul( tiling_iT, { x: 0, y: 0 } ) ];
265 |
266 | const v = normalize( sub( pts[1], pts[0] ) );
267 | const w = normalize( sub( pts[3], pts[0] ) );
268 |
269 | return [
270 | { x: pts[0].x+margin*(-v.x-w.x), y: pts[0].y+margin*(-v.y-w.y) },
271 | { x: pts[1].x+margin*(v.x-w.x), y: pts[1].y+margin*(v.y-w.y) },
272 | { x: pts[2].x+margin*(v.x+w.x), y: pts[2].y+margin*(v.y+w.y) },
273 | { x: pts[3].x+margin*(-v.x+w.x), y: pts[3].y+margin*(-v.y+w.y) } ];
274 | }
275 |
276 | function drawTiling()
277 | {
278 | p5c.stroke( COLS[0][0], COLS[0][1], COLS[0][2] );
279 | p5c.strokeWeight( 1.0 );
280 |
281 | const bx = getTilingRect();
282 | for( let i of tiling.fillRegionQuad( bx[0], bx[1], bx[2], bx[3] ) ) {
283 | const TT = i.T;
284 | const T = mul( tiling_T, TT );
285 |
286 | const col = COLS[ tiling.getColour( i.t1, i.t2, i.aspect ) + 1 ];
287 | p5c.fill( col[0], col[1], col[2] );
288 |
289 | p5c.beginShape();
290 | for( let v of tile_shape ) {
291 | const P = mul( T, v );
292 | p5c.vertex( P.x, P.y );
293 | }
294 | p5c.endShape( p5c.CLOSE );
295 | }
296 | }
297 |
298 | function calcEditorTransform()
299 | {
300 | let xmin = 1e7;
301 | let xmax = -1e7;
302 | let ymin = 1e7;
303 | let ymax = -1e7;
304 |
305 | for( let v of tile_shape ) {
306 | xmin = Math.min( xmin, v.x );
307 | xmax = Math.max( xmax, v.x );
308 | ymin = Math.min( ymin, v.y );
309 | ymax = Math.max( ymax, v.y );
310 | }
311 |
312 | const ww = edit_box.w - 5 * phys_unit;
313 |
314 | const sc = Math.min( (ww-50) / (xmax-xmin), (edit_box.h-50) / (ymax-ymin) );
315 |
316 | editor_T = mul(
317 | [sc, 0, 0.5*ww+25, 0, -sc, 0.5*edit_box.h],
318 | [1, 0, -0.5*(xmin+xmax), 0, 1, -0.5*(ymin+ymax)] );
319 | }
320 |
321 | function distToSeg( P, A, B )
322 | {
323 | const qmp = sub( B, A );
324 | const t = dot( sub( P, A ), qmp ) / dot( qmp, qmp );
325 | if( (t >= 0.0) && (t <= 1.0) ) {
326 | return len( sub( P, { x: A.x + t*qmp.x, y : A.y + t*qmp.y } ) );
327 | } else if( t < 0.0 ) {
328 | return len( sub( P, A ) );
329 | } else {
330 | return len( sub( P, B ) );
331 | }
332 | }
333 |
334 | function drawEditor()
335 | {
336 | let pg = editor_pane;
337 | pg.clear();
338 |
339 | pg.fill( 252, 255, 254, 220 );
340 | pg.noStroke();
341 | pg.rect( 0, 0, edit_box.w, edit_box.h );
342 |
343 | pg.strokeWeight( 2.0 );
344 | pg.fill( COLS[3][0], COLS[3][1], COLS[3][2] );
345 |
346 | pg.beginShape();
347 | for( let v of tile_shape ) {
348 | const P = mul( editor_T, v );
349 | pg.vertex( P.x, P.y );
350 | }
351 | pg.endShape( p5c.CLOSE );
352 |
353 | pg.noFill();
354 |
355 | // Draw edges
356 | for( let i of tiling.parts() ) {
357 | if( i.shape == EdgeShape.I ) {
358 | pg.stroke( 158 );
359 | } else {
360 | pg.stroke( 0 );
361 | }
362 |
363 | const M = mul( editor_T, i.T );
364 | pg.beginShape();
365 | for( let v of edges[i.id] ) {
366 | const P = mul( M, v );
367 | pg.vertex( P.x, P.y );
368 | }
369 | pg.endShape();
370 | }
371 |
372 | // Draw tiling vertices
373 | pg.noStroke();
374 | pg.fill( 158 );
375 | for( let v of tiling.vertices() ) {
376 | const pt = mul( editor_T, v );
377 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
378 | }
379 |
380 | // Draw editable vertices
381 | for( let i of tiling.parts() ) {
382 | const shp = i.shape;
383 | const id = i.id;
384 | const ej = edges[id];
385 | const T = mul( editor_T, i.T );
386 |
387 | for( let idx = 1; idx < ej.length - 1; ++idx ) {
388 | pg.fill( 0 );
389 | const pt = mul( T, ej[idx] );
390 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
391 | }
392 |
393 | if( shp == EdgeShape.I || shp == EdgeShape.J ) {
394 | continue;
395 | }
396 |
397 | // Draw symmetry points for U and S edges.
398 | if( !i.second ) {
399 | if( shp == EdgeShape.U ) {
400 | pg.fill( COLS[2][0], COLS[2][1], COLS[2][2] );
401 | } else {
402 | pg.fill( COLS[5][0], COLS[5][1], COLS[5][2] );
403 | }
404 | const pt = mul( T, ej[ej.length-1] );
405 | pg.ellipse( pt.x, pt.y, 10.0, 10.0 );
406 | }
407 | }
408 |
409 | // Draw sliders
410 | const params = tiling.getParameters();
411 | let yy = 25;
412 | const xx = edit_box.w - 25 - slide_w;
413 | pg.textSize( slide_h * 0.75 );
414 |
415 | for( let i = 0; i < params.length; ++i ) {
416 | pg.fill( 200 );
417 | pg.stroke( 60 );
418 | pg.strokeWeight( 0.5 );
419 | pg.rect( xx, yy, slide_w, slide_h );
420 |
421 | pg.fill( 60 );
422 | pg.noStroke();
423 | pg.rect( p5c.map( params[i],
424 | 0, 2, xx, xx+slide_w-slide_h ), yy, slide_h, slide_h );
425 |
426 | pg.text( "v" + i, xx - slide_h, yy + slide_h * 0.75 );
427 |
428 | yy += slide_h + 10;
429 | }
430 |
431 | p5c.image( pg, edit_box.x, edit_box.y );
432 |
433 | p5c.strokeWeight( 3.0 );
434 | p5c.stroke( 25, 52, 65, 220 );
435 | p5c.noFill();
436 | p5c.rect( edit_box.x, edit_box.y, edit_box.w, edit_box.h );
437 | }
438 |
439 | function deleteVertex()
440 | {
441 | edges[drag_edge_shape].splice( drag_vertex, 1 );
442 | mode = Mode.NONE;
443 | cacheTileShape();
444 | p5c.loop();
445 | }
446 |
447 | function saveSVG()
448 | {
449 | const xmlns = "http://www.w3.org/2000/svg";
450 | const svgElement = getTilingSVG( xmlns );
451 | const s = new XMLSerializer();
452 | const svgFile = s.serializeToString( svgElement ).split( '\n' );
453 | p5c.save( svgFile, "tiling", "svg" );
454 | }
455 |
456 | function getTilingSVG( namespace )
457 | {
458 | let svgElement = document.createElementNS( namespace,'svg' );
459 | svgElement.setAttribute( 'xmlns:xlink','http://www.w3.org/1999/xlink' );
460 | svgElement.setAttribute( 'height', window.innerHeight );
461 | svgElement.setAttribute( 'width', window.innerWidth );
462 |
463 | let tileSVG = getTileShapeSVG( namespace );
464 | svgElement.appendChild( tileSVG );
465 |
466 | const bx = getTilingRect();
467 | for ( let i of tiling.fillRegionQuad( bx[0], bx[1], bx[2], bx[3] ) ) {
468 | const T = mul( tiling_T, i.T );
469 | const svg_T = [ T[0], T[3], T[1], T[4], T[2], T[5] ].map( t => +t.toFixed(3) );
470 |
471 | const col = COLS[ tiling.getColour( i.t1, i.t2, i.aspect ) + 1 ];
472 |
473 | let tile = document.createElementNS( namespace, 'use' );
474 | tile.setAttribute( 'xlink:href', '#tile-shape' );
475 | tile.setAttribute( 'fill', `rgb(${col[0]},${col[1]},${col[2]})` );
476 | tile.setAttribute( 'transform', `matrix(${svg_T})` );
477 | svgElement.appendChild( tile );
478 | }
479 |
480 | return svgElement;
481 | }
482 |
483 | function getTileShapeSVG( namespace )
484 | {
485 | let defs = document.createElementNS( namespace, 'defs' );
486 | let symbol = document.createElementNS( namespace, 'symbol' );
487 | let polygon = document.createElementNS( namespace, 'polygon' );
488 |
489 | let points = tile_shape.map( v => `${+v.x.toFixed(3)},${+v.y.toFixed(3)}` );
490 |
491 | polygon.setAttribute( 'points', points.join(' ') );
492 | polygon.setAttribute( 'stroke', 'black' );
493 | polygon.setAttribute( 'vector-effect', 'non-scaling-stroke' );
494 |
495 | symbol.setAttribute( 'id', 'tile-shape' );
496 | symbol.setAttribute( 'overflow', 'visible' );
497 | symbol.appendChild( polygon );
498 | defs.appendChild( symbol );
499 |
500 | return defs;
501 | }
502 |
503 | function doTouchStarted( id )
504 | {
505 | // First, check if this touch is intended to initiate an
506 | // instantaneous action.
507 |
508 | if( mode == Mode.NONE ) {
509 | if( hitBox( p5c.mouseX, p5c.mouseY, edit_button_box ) ) {
510 | show_controls = !show_controls;
511 | p5c.loop();
512 | return false;
513 | }
514 |
515 | if( hitBox( p5c.mouseX, p5c.mouseY, save_button_box ) ) {
516 | saveSVG();
517 | p5c.loop();
518 | return false;
519 | }
520 |
521 | if( hitBox( p5c.mouseX, p5c.mouseY, prev_box ) ) {
522 | prevTilingType();
523 | p5c.loop();
524 | return false;
525 | }
526 |
527 | if( hitBox( p5c.mouseX, p5c.mouseY, next_box ) ) {
528 | nextTilingType();
529 | p5c.loop();
530 | return false;
531 | }
532 | }
533 |
534 | // If not, we assume that it might be the start of a new gesture.
535 |
536 | if( show_controls ) {
537 | const pt =
538 | { x: p5c.mouseX - edit_box.x, y: p5c.mouseY - edit_box.y };
539 |
540 | if( (pt.x < 0) || (pt.x > edit_box.w) ) {
541 | return false;
542 | }
543 | if( (pt.y < 0) || (pt.y > edit_box.h) ) {
544 | return false;
545 | }
546 |
547 | // Check for a sliding gesture on one of the tiling vertex
548 | // parameter sliders.
549 | const params = tiling.getParameters();
550 | let yy = 25;
551 | const xx = edit_box.w - 25 - slide_w;
552 |
553 | for( let i = 0; i < params.length; ++i ) {
554 | const x = p5c.map( params[i], 0, 2, xx, xx+slide_w-slide_h );
555 |
556 | if( hitBox( pt.x, pt.y, makeBox( x, yy, slide_h, slide_h ) ) ) {
557 | mode = Mode.ADJ_TV;
558 | max_touches = 1;
559 | drag_tv = i;
560 | drag_tv_offs = pt.x - x;
561 | return false;
562 | }
563 |
564 | yy += slide_h + 10;
565 | }
566 |
567 | // Nothing yet. OK, try the geometric features of the tiling.
568 | for( let i of tiling.parts() ) {
569 | const shp = i.shape;
570 |
571 | // No interaction possible with an I edge.
572 | if( shp == EdgeShape.I ) {
573 | continue;
574 | }
575 |
576 | const id = i.id;
577 | let ej = edges[id];
578 | const T = mul( editor_T, i.T );
579 | let P = mul( T, ej[0] );
580 |
581 | for( let idx = 1; idx < ej.length; ++idx ) {
582 | let Q = mul( T, ej[idx] );
583 | if( ptdist( Q, pt ) < 0.5 * phys_unit ) {
584 | u_constrain = false;
585 | if( idx == (ej.length-1) ) {
586 | if( shp == EdgeShape.U && !i.second ) {
587 | u_constrain = true;
588 | } else {
589 | break;
590 | }
591 | }
592 |
593 | mode = Mode.MOVE_VERTEX;
594 | max_touches = 1;
595 | drag_edge_shape = id;
596 | drag_vertex = idx;
597 | drag_T = inv( T );
598 | down_motion = pt;
599 |
600 | // Set timer for deletion. But not on a U vertex.
601 | if( !u_constrain ) {
602 | delete_timer = setTimeout( deleteVertex, 1000 );
603 | }
604 |
605 | p5c.loop();
606 | return false;
607 | }
608 |
609 | // Check segment
610 | if( distToSeg( pt, P, Q ) < 20 ) {
611 | mode = Mode.MOVE_VERTEX;
612 | max_touches = 1;
613 | drag_edge_shape = id;
614 | drag_vertex = idx;
615 | drag_T = inv( T );
616 | down_motion = pt;
617 | // Don't set timer -- can't delete new vertex.
618 |
619 | ej.splice( idx, 0, mul( drag_T, pt ) );
620 | cacheTileShape();
621 | p5c.loop();
622 | return false;
623 | }
624 |
625 | P = Q;
626 | }
627 | }
628 |
629 | mode = Mode.ADJ_TILE;
630 | editor_T_down = editor_T;
631 | max_touches = 2;
632 | } else {
633 | mode = Mode.ADJ_TILING;
634 | tiling_T_down = tiling_T;
635 | max_touches = 2;
636 | }
637 |
638 | return false;
639 | }
640 |
641 | function getTouchRigid()
642 | {
643 | const ks = Object.keys( my_touches );
644 |
645 | if( num_touches == 1 ) {
646 | // Just translation.
647 | const tch = my_touches[ks[0]];
648 | const dx = tch.pos.x - tch.down.x;
649 | const dy = tch.pos.y - tch.down.y;
650 |
651 | return [ 1.0, 0.0, dx, 0.0, 1.0, dy ];
652 | } else {
653 | // Full rigid.
654 | const tch1 = my_touches[ks[0]];
655 | const tch2 = my_touches[ks[1]];
656 |
657 | const P1 = tch1.down;
658 | const P2 = tch1.pos;
659 | const Q1 = tch2.down;
660 | const Q2 = tch2.pos;
661 |
662 | const M1 = matchSeg( P1, Q1 );
663 | const M2 = matchSeg( P2, Q2 );
664 | const M = mul( M2, inv( M1 ) );
665 |
666 | return M;
667 | }
668 | }
669 |
670 | function doTouchMoved()
671 | {
672 | if( mode == Mode.ADJ_TILING ) {
673 | const M = getTouchRigid();
674 | tiling_T = mul( M, tiling_T_down );
675 | tiling_iT = inv( tiling_T );
676 | p5c.loop();
677 | return false;
678 | } else if( mode == Mode.ADJ_TILE ) {
679 | const M = getTouchRigid();
680 | editor_T = mul( M, editor_T_down );
681 | p5c.loop();
682 | return false;
683 | } else if( mode == Mode.ADJ_TV ) {
684 | // FIXME -- it would be better if this mode and Mode.MOVE_VERTEX
685 | // used my_touches instead of mouseX and mouseY. Oh well.
686 |
687 | const params = tiling.getParameters();
688 | let yy = 25 + 30*drag_tv;
689 | const xx = edit_box.w - 25 - 5*phys_unit;
690 |
691 | const t = p5c.map(
692 | p5c.mouseX-edit_box.x-drag_tv_offs, xx,
693 | xx+5*phys_unit-20, 0, 2 );
694 | params[drag_tv] = t;
695 | tiling.setParameters( params );
696 | cacheTileShape();
697 | p5c.loop();
698 | } else if( mode == Mode.MOVE_VERTEX ) {
699 | const pt =
700 | { x: p5c.mouseX - edit_box.x, y: p5c.mouseY - edit_box.y };
701 | const npt = mul( drag_T, pt );
702 |
703 | if( u_constrain ) {
704 | npt.x = 1.0;
705 | }
706 | const d = p5c.dist( pt.x, pt.y, down_motion.x, down_motion.y );
707 | if( d > 10 ) {
708 | // You've moved far enough, so don't delete.
709 | if( delete_timer ) {
710 | clearTimeout( delete_timer );
711 | delete_timer = null;
712 | }
713 | }
714 |
715 | edges[drag_edge_shape][drag_vertex] = npt;
716 | cacheTileShape();
717 | p5c.loop();
718 | }
719 |
720 | return false;
721 | }
722 |
723 | function doTouchEnded( id )
724 | {
725 | if( delete_timer ) {
726 | clearTimeout( delete_timer );
727 | delete_timer = null;
728 | }
729 | mode = Mode.NONE;
730 | }
731 |
732 | function setupInterface()
733 | {
734 | let w = window.innerWidth;
735 | let h = window.innerHeight;
736 |
737 | // Any way to fix this for different devices?
738 | phys_unit = 60;
739 |
740 | edit_button_box = makeBox(
741 | 0.25 * phys_unit, 0.25 * phys_unit, phys_unit, phys_unit );
742 | save_button_box = makeBox(
743 | 1.5 * phys_unit, 0.25 * phys_unit, phys_unit, phys_unit );
744 | navigator_box = makeBox(
745 | w - 5.25 * phys_unit, 0.25 * phys_unit, 5 * phys_unit, phys_unit );
746 | prev_box = makeBox(
747 | navigator_box.x, navigator_box.y, phys_unit, phys_unit );
748 | next_box = makeBox(
749 | navigator_box.x + navigator_box.w - phys_unit, navigator_box.y,
750 | phys_unit, phys_unit );
751 |
752 | edit_box = makeBox(
753 | 0.25*phys_unit, 1.5*phys_unit,
754 | Math.min( 800, 0.8*w ), Math.min( 600, 0.8*h ) );
755 |
756 | slide_w = 5 * phys_unit;
757 | slide_h = 0.7 * phys_unit;
758 |
759 | editor_pane = p5c.createGraphics( edit_box.w, edit_box.h );
760 | }
761 |
762 | p5c.setup = function()
763 | {
764 | let w = window.innerWidth;
765 | let h = window.innerHeight;
766 |
767 | let canvas = p5c.createCanvas( w, h );
768 | canvas.parent( "sktch" );
769 |
770 | const asp = w / h;
771 | const hh = 6.0;
772 | const ww = asp * hh;
773 | const sc = h / (2*hh);
774 |
775 | tiling_T = mul(
776 | [1, 0, p5c.width/2.0, 0, 1, p5c.height/2.0],
777 | [sc, 0, 0, 0, -sc, 0] );
778 | tiling_iT = inv( tiling_T );
779 |
780 | setupInterface();
781 |
782 | the_type = 0;
783 |
784 | let parms = p5c.getURLParams();
785 | if( 't' in parms ) {
786 | let tt = p5c.int( parms.t );
787 | for( let i = 0; i < tilingTypes.length; ++i ) {
788 | if( tilingTypes[i] == tt ) {
789 | the_type = i;
790 | break;
791 | }
792 | }
793 | }
794 |
795 | const tp = tilingTypes[ the_type ];
796 | tiling = new IsohedralTiling( tp );
797 |
798 | setTilingType();
799 | }
800 |
801 | p5c.windowResized = function()
802 | {
803 | p5c.resizeCanvas( window.innerWidth, window.innerHeight );
804 | setupInterface();
805 | p5c.loop();
806 | }
807 |
808 | function drawIcon( drf, B )
809 | {
810 | p5c.push();
811 | p5c.translate( B.x, B.y + B.h );
812 | p5c.scale( B.w / 200.0 );
813 | p5c.scale( 1.0, -1.0 );
814 | drf();
815 | p5c.pop();
816 | }
817 |
818 | p5c.draw = function()
819 | {
820 | p5c.background( 255 );
821 |
822 | drawTiling();
823 |
824 | drawIcon( drawEditIcon, edit_button_box );
825 | drawIcon( drawSaveIcon, save_button_box );
826 |
827 | p5c.fill( 252, 255, 254, 220 );
828 | p5c.stroke( 0 );
829 | p5c.strokeWeight( 4 );
830 | p5c.rect( navigator_box.x, navigator_box.y,
831 | navigator_box.w, navigator_box.h, 5 );
832 |
833 | const tt = tilingTypes[ the_type ];
834 | const name = ((tt<10)?"IH0":"IH") + tilingTypes[ the_type ];
835 | p5c.textAlign( p5c.CENTER );
836 | p5c.textSize( 0.75 * phys_unit );
837 | p5c.fill( 0 );
838 | p5c.noStroke();
839 | p5c.text( name, navigator_box.x + 0.5*navigator_box.w,
840 | navigator_box.y + 0.75*navigator_box.h );
841 |
842 | p5c.fill( (the_type > 0) ? 0 : 200 );
843 | drawIcon( () => p5c.triangle( 35, 100, 165, 30, 165, 170 ), prev_box );
844 | p5c.fill( (the_type < 80) ? 0 : 200 );
845 | drawIcon( () => p5c.triangle( 165, 100, 35, 30, 35, 170 ), next_box );
846 |
847 | if( show_controls ) {
848 | drawEditor();
849 | }
850 |
851 | p5c.fill( 255 );
852 | p5c.noStroke();
853 | p5c.textSize( 24 );
854 | p5c.textAlign( p5c.LEFT );
855 | let c = 0;
856 | c += 32;
857 | for( let i = Math.max( 0, msgs.length - 10 ); i < msgs.length; ++i ) {
858 | p5c.text( msgs[i], 25, 200+c );
859 | c = c + 32;
860 | }
861 |
862 | p5c.noLoop();
863 | }
864 |
865 | function drawSaveIcon()
866 | {
867 | drawIconBackground();
868 |
869 | p5c.fill( 0, 0, 0 );
870 | p5c.beginShape();
871 | p5c.vertex( 133.75, 161.5 );
872 | p5c.vertex( 51.25, 161.5 );
873 | p5c.bezierVertex( 43.6172, 161.5, 37.5, 155.313, 37.5, 147.75 );
874 | p5c.vertex( 37.5, 51.5 );
875 | p5c.bezierVertex( 37.5, 43.9375, 43.6172, 37.75, 51.25, 37.75 );
876 | p5c.vertex( 147.5, 37.75 );
877 | p5c.bezierVertex( 155.063, 37.75, 161.25, 43.9375, 161.25, 51.5 );
878 | p5c.vertex( 161.25, 134.0 );
879 | p5c.beginContour();
880 | p5c.vertex( 99.375, 51.5 );
881 | p5c.bezierVertex( 87.9609, 51.5, 78.75, 60.7109, 78.75, 72.125 );
882 | p5c.bezierVertex( 78.75, 83.5391, 87.9609, 92.75, 99.375, 92.75 );
883 | p5c.bezierVertex( 110.789, 92.75, 120.0, 83.5391, 120.0, 72.125 );
884 | p5c.bezierVertex( 120.0, 60.7109, 110.789, 51.5, 99.375, 51.5 );
885 | p5c.endContour();
886 | p5c.beginContour();
887 | p5c.vertex( 120.0, 120.25 );
888 | p5c.vertex( 51.25, 120.25 );
889 | p5c.vertex( 51.25, 147.75 );
890 | p5c.vertex( 120.0, 147.75 );
891 | p5c.endContour();
892 | p5c.endShape( p5c.CLOSE );
893 |
894 | drawIconOutline();
895 | }
896 |
897 | function drawEditIcon()
898 | {
899 | drawIconBackground();
900 |
901 | p5c.fill( 0, 0, 0 );
902 | p5c.beginShape();
903 | p5c.vertex( 119.539, 148.27 );
904 | p5c.vertex( 82.0313, 109.57 );
905 | p5c.bezierVertex( 87.8008, 103.59, 93.8594, 97.9297, 99.0508, 91.5508 );
906 | p5c.vertex( 132.051, 125.602 );
907 | p5c.bezierVertex( 132.93, 126.512, 134.648, 126.281, 135.898, 125.09 );
908 | p5c.vertex( 136.301, 124.711 );
909 | p5c.bezierVertex( 137.551, 123.52, 137.852, 121.82, 136.969, 120.91 );
910 | p5c.vertex( 103.16, 86.0313 );
911 | p5c.bezierVertex( 104.309, 84.3086, 105.391, 82.5195, 106.371, 80.6484 );
912 | p5c.vertex( 146.738, 122.301 );
913 | p5c.vertex( 119.539, 148.27 );
914 | p5c.endShape( p5c.CLOSE );
915 | p5c.fill( 0, 0, 0 );
916 | p5c.beginShape();
917 | p5c.vertex( 79.6211, 61.7383 );
918 | p5c.bezierVertex( 78.7383, 60.8281, 77.0117, 61.0586, 75.7695, 62.25 );
919 | p5c.vertex( 75.3711, 62.6289 );
920 | p5c.bezierVertex( 74.1211, 63.8203, 73.8203, 65.5195, 74.6992, 66.4297 );
921 | p5c.vertex( 112.578, 105.512 );
922 | p5c.bezierVertex( 107.738, 112.25, 102.48, 118.711, 95.75, 123.73 );
923 | p5c.vertex( 51.0586, 77.6289 );
924 | p5c.vertex( 78.2617, 51.6484 );
925 | p5c.vertex( 120.059, 94.7813 );
926 | p5c.bezierVertex( 118.891, 96.4609, 117.719, 98.1484, 116.539, 99.8516 );
927 | p5c.vertex( 79.6211, 61.7383 );
928 | p5c.endShape( p5c.CLOSE );
929 | p5c.fill( 0, 0, 0 );
930 | p5c.beginShape();
931 | p5c.vertex( 151.391, 127.102 );
932 | p5c.vertex( 124.191, 153.078 );
933 | p5c.vertex( 131.961, 161.102 );
934 | p5c.bezierVertex( 136.391, 165.672, 145.07, 164.512, 151.359, 158.512 );
935 | p5c.vertex( 155.801, 154.27 );
936 | p5c.bezierVertex( 162.078, 148.27, 163.59, 139.699, 159.16, 135.129 );
937 | p5c.vertex( 151.391, 127.102 );
938 | p5c.endShape( p5c.CLOSE );
939 | p5c.fill( 0, 0, 0 );
940 | p5c.beginShape();
941 | p5c.vertex( 37.6016, 41.3789 );
942 | p5c.vertex( 46.4102, 72.8203 );
943 | p5c.vertex( 60.0117, 59.8281 );
944 | p5c.vertex( 73.6094, 46.8398 );
945 | p5c.vertex( 42.3008, 36.8906 );
946 | p5c.bezierVertex( 39.9609, 36.1484, 36.9414, 39.0313, 37.6016, 41.3789 );
947 | p5c.endShape( p5c.CLOSE );
948 |
949 | drawIconOutline();
950 | }
951 |
952 | function drawIconBackground()
953 | {
954 | p5c.fill( 252, 255, 254, 220 );
955 | p5c.beginShape();
956 | p5c.vertex( 180.0, 7.94141 );
957 | p5c.vertex( 19.2188, 7.94141 );
958 | p5c.bezierVertex( 12.6211, 7.94141, 7.21875, 13.3398, 7.21875, 19.9414 );
959 | p5c.vertex( 7.21875, 180.73 );
960 | p5c.bezierVertex( 7.21875, 187.328, 12.6211, 192.73, 19.2188, 192.73 );
961 | p5c.vertex( 180.0, 192.73 );
962 | p5c.bezierVertex( 186.602, 192.73, 192.0, 187.328, 192.0, 180.73 );
963 | p5c.vertex( 192.0, 19.9414 );
964 | p5c.bezierVertex( 192.0, 13.3398, 186.602, 7.94141, 180.0, 7.94141 );
965 | p5c.endShape( p5c.CLOSE );
966 | }
967 |
968 | function drawIconOutline()
969 | {
970 | p5c.fill( 0, 0, 0 );
971 | p5c.beginShape();
972 | p5c.vertex( 85.75, 15.2109 );
973 | p5c.vertex( 177.18, 15.2109 );
974 | p5c.bezierVertex( 181.371, 15.2109, 184.789, 18.6211, 184.789, 22.8203 );
975 | p5c.vertex( 184.789, 177.18 );
976 | p5c.bezierVertex( 184.789, 181.371, 181.379, 184.789, 177.18, 184.789 );
977 | p5c.vertex( 84.4453, 184.789 );
978 | p5c.vertex( 84.4453, 200.0 );
979 | p5c.vertex( 177.18, 200.0 );
980 | p5c.bezierVertex( 189.762, 200.0, 200.0, 189.762, 200.0, 177.18 );
981 | p5c.vertex( 200.0, 22.8203 );
982 | p5c.bezierVertex( 200.0, 10.2383, 189.762, 0.0, 177.18, 0.0 );
983 | p5c.vertex( 84.0117, 0.0 );
984 | p5c.vertex( 85.75, 15.2109 );
985 | p5c.endShape( p5c.CLOSE );
986 | p5c.fill( 0, 0, 0 );
987 | p5c.beginShape();
988 | p5c.vertex( 114.25, 184.789 );
989 | p5c.vertex( 22.8203, 184.789 );
990 | p5c.bezierVertex( 18.6289, 184.789, 15.2109, 181.379, 15.2109, 177.18 );
991 | p5c.vertex( 15.2109, 22.8203 );
992 | p5c.bezierVertex( 15.2109, 18.6289, 18.6211, 15.2109, 22.8203, 15.2109 );
993 | p5c.vertex( 115.555, 15.2109 );
994 | p5c.vertex( 115.555, 0.0 );
995 | p5c.vertex( 22.8203, 0.0 );
996 | p5c.bezierVertex( 10.2383, 0.0, 0.0, 10.2383, 0.0, 22.8203 );
997 | p5c.vertex( 0.0, 177.18 );
998 | p5c.bezierVertex( 0.0, 189.762, 10.2383, 200.0, 22.8203, 200.0 );
999 | p5c.vertex( 115.988, 200.0 );
1000 | p5c.vertex( 114.25, 184.789 );
1001 | p5c.endShape( p5c.CLOSE );
1002 | }
1003 | };
1004 |
1005 | let myp5 = new p5( sktch, 'sketch0' );
1006 |
--------------------------------------------------------------------------------
/images/jusi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isohedral/tactile-js/fd58ec372a4398e3dfc22663d120da186790834e/images/jusi.png
--------------------------------------------------------------------------------
/images/params.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isohedral/tactile-js/fd58ec372a4398e3dfc22663d120da186790834e/images/params.png
--------------------------------------------------------------------------------
/images/shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isohedral/tactile-js/fd58ec372a4398e3dfc22663d120da186790834e/images/shape.png
--------------------------------------------------------------------------------
/images/topo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isohedral/tactile-js/fd58ec372a4398e3dfc22663d120da186790834e/images/topo.png
--------------------------------------------------------------------------------
/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tactile-js",
3 | "version": "1.0.0",
4 | "description": "TactileJS is a Javascript library for representing, manipulating, and drawing tilings of the plane.",
5 | "main": "tactile.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/isohedral/tactile-js.git"
12 | },
13 | "keywords": [
14 | "tiles",
15 | "tiling",
16 | "geometry",
17 | "isohedral"
18 | ],
19 | "author": "Craig S. Kaplan",
20 | "license": "SEE LICENSE IN LICENSE",
21 | "bugs": {
22 | "url": "https://github.com/isohedral/tactile-js/issues"
23 | },
24 | "homepage": "https://github.com/isohedral/tactile-js#readme",
25 | "type": "module"
26 | }
27 |
--------------------------------------------------------------------------------
/spirals/README.md:
--------------------------------------------------------------------------------
1 | This folder contains the source code for an interactive Javascript tool that allows you to draw spiral tilings. See the blog post at http://isohedral.ca/escher-like-spiral-tilings/ for more information, including a link to a runnable version.
2 |
3 | If you want to run this code yourself from source, you'll need to run a web server. Because Chrome is afraid of cross-site scripting attacks, this script won't run via a `file:` URL. It suffices to run Python's web server by executing "`python -m http.server`" from the command line while in the main directory.
4 |
--------------------------------------------------------------------------------
/spirals/assets/frag1.txt:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform sampler2D tex;
4 | uniform vec2 res;
5 | // uniform vec4 Mv;
6 | uniform mat3 M;
7 | uniform bool mob;
8 |
9 | varying vec2 uv;
10 |
11 | void main()
12 | {
13 | vec2 asp = vec2( 1.0, res.y / res.x );
14 |
15 | vec2 cen = 0.5 * asp;
16 | vec2 v = uv * asp;
17 |
18 | vec2 p = v - cen;
19 |
20 | if( mob ) {
21 | float dia = length( res );
22 | float c = res.x / dia;
23 | float s = res.y / dia;
24 |
25 | mat3 rot = mat3( c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0 );
26 | p = (rot*vec3(p,1.0)).xy;
27 |
28 | p = (1.5*p+vec2(0.5,0.0));
29 | float l = (p.x-1.0)*(p.x-1.0) + p.y*p.y;
30 | p = vec2( (p.x-p.x*p.x-p.y*p.y)/l, p.y/l );
31 | }
32 |
33 | vec2 merc = vec2( log( length( p ) ), atan( p.y, p.x ) );
34 |
35 | vec4 col = texture2D( tex, (M * vec3( merc, 1.0 )).xy );
36 | gl_FragColor = col;
37 | }
38 |
--------------------------------------------------------------------------------
/spirals/assets/helveticaneue.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isohedral/tactile-js/fd58ec372a4398e3dfc22663d120da186790834e/spirals/assets/helveticaneue.otf
--------------------------------------------------------------------------------
/spirals/assets/vert1.txt:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute vec3 aPosition;
4 |
5 | uniform bool fullscreen;
6 |
7 | varying vec2 uv;
8 |
9 | void main() {
10 | uv = aPosition.xy;
11 |
12 | vec4 pos = vec4(aPosition, 1.0);
13 | if( fullscreen ) {
14 | pos = vec4( 2.0*pos.x-1.0, 2.0*pos.y-1.0, pos.z, 1.0 );
15 | }
16 | pos.y = -pos.y;
17 |
18 | gl_Position = pos;
19 | }
20 |
--------------------------------------------------------------------------------
/spirals/earcut.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Earcut implementation taken from
4 | // https://github.com/mapbox/earcut
5 | // Minor change: added export statement at the end.
6 |
7 | function earcut(data, holeIndices, dim) {
8 |
9 | dim = dim || 2;
10 |
11 | var hasHoles = holeIndices && holeIndices.length,
12 | outerLen = hasHoles ? holeIndices[0] * dim : data.length,
13 | outerNode = linkedList(data, 0, outerLen, dim, true),
14 | triangles = [];
15 |
16 | if (!outerNode || outerNode.next === outerNode.prev) return triangles;
17 |
18 | var minX, minY, maxX, maxY, x, y, invSize;
19 |
20 | if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
21 |
22 | // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
23 | if (data.length > 80 * dim) {
24 | minX = maxX = data[0];
25 | minY = maxY = data[1];
26 |
27 | for (var i = dim; i < outerLen; i += dim) {
28 | x = data[i];
29 | y = data[i + 1];
30 | if (x < minX) minX = x;
31 | if (y < minY) minY = y;
32 | if (x > maxX) maxX = x;
33 | if (y > maxY) maxY = y;
34 | }
35 |
36 | // minX, minY and invSize are later used to transform coords into integers for z-order calculation
37 | invSize = Math.max(maxX - minX, maxY - minY);
38 | invSize = invSize !== 0 ? 1 / invSize : 0;
39 | }
40 |
41 | earcutLinked(outerNode, triangles, dim, minX, minY, invSize);
42 |
43 | return triangles;
44 | }
45 |
46 | // create a circular doubly linked list from polygon points in the specified winding order
47 | function linkedList(data, start, end, dim, clockwise) {
48 | var i, last;
49 |
50 | if (clockwise === (signedArea(data, start, end, dim) > 0)) {
51 | for (i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
52 | } else {
53 | for (i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
54 | }
55 |
56 | if (last && equals(last, last.next)) {
57 | removeNode(last);
58 | last = last.next;
59 | }
60 |
61 | return last;
62 | }
63 |
64 | // eliminate colinear or duplicate points
65 | function filterPoints(start, end) {
66 | if (!start) return start;
67 | if (!end) end = start;
68 |
69 | var p = start,
70 | again;
71 | do {
72 | again = false;
73 |
74 | if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
75 | removeNode(p);
76 | p = end = p.prev;
77 | if (p === p.next) break;
78 | again = true;
79 |
80 | } else {
81 | p = p.next;
82 | }
83 | } while (again || p !== end);
84 |
85 | return end;
86 | }
87 |
88 | // main ear slicing loop which triangulates a polygon (given as a linked list)
89 | function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
90 | if (!ear) return;
91 |
92 | // interlink polygon nodes in z-order
93 | if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
94 |
95 | var stop = ear,
96 | prev, next;
97 |
98 | // iterate through ears, slicing them one by one
99 | while (ear.prev !== ear.next) {
100 | prev = ear.prev;
101 | next = ear.next;
102 |
103 | if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
104 | // cut off the triangle
105 | triangles.push(prev.i / dim);
106 | triangles.push(ear.i / dim);
107 | triangles.push(next.i / dim);
108 |
109 | removeNode(ear);
110 |
111 | // skipping the next vertex leads to less sliver triangles
112 | ear = next.next;
113 | stop = next.next;
114 |
115 | continue;
116 | }
117 |
118 | ear = next;
119 |
120 | // if we looped through the whole remaining polygon and can't find any more ears
121 | if (ear === stop) {
122 | // try filtering points and slicing again
123 | if (!pass) {
124 | earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
125 |
126 | // if this didn't work, try curing all small self-intersections locally
127 | } else if (pass === 1) {
128 | ear = cureLocalIntersections(filterPoints(ear), triangles, dim);
129 | earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
130 |
131 | // as a last resort, try splitting the remaining polygon into two
132 | } else if (pass === 2) {
133 | splitEarcut(ear, triangles, dim, minX, minY, invSize);
134 | }
135 |
136 | break;
137 | }
138 | }
139 | }
140 |
141 | // check whether a polygon node forms a valid ear with adjacent nodes
142 | function isEar(ear) {
143 | var a = ear.prev,
144 | b = ear,
145 | c = ear.next;
146 |
147 | if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
148 |
149 | // now make sure we don't have other points inside the potential ear
150 | var p = ear.next.next;
151 |
152 | while (p !== ear.prev) {
153 | if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
154 | area(p.prev, p, p.next) >= 0) return false;
155 | p = p.next;
156 | }
157 |
158 | return true;
159 | }
160 |
161 | function isEarHashed(ear, minX, minY, invSize) {
162 | var a = ear.prev,
163 | b = ear,
164 | c = ear.next;
165 |
166 | if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
167 |
168 | // triangle bbox; min & max are calculated like this for speed
169 | var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
170 | minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
171 | maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
172 | maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
173 |
174 | // z-order range for the current triangle bbox;
175 | var minZ = zOrder(minTX, minTY, minX, minY, invSize),
176 | maxZ = zOrder(maxTX, maxTY, minX, minY, invSize);
177 |
178 | var p = ear.prevZ,
179 | n = ear.nextZ;
180 |
181 | // look for points inside the triangle in both directions
182 | while (p && p.z >= minZ && n && n.z <= maxZ) {
183 | if (p !== ear.prev && p !== ear.next &&
184 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
185 | area(p.prev, p, p.next) >= 0) return false;
186 | p = p.prevZ;
187 |
188 | if (n !== ear.prev && n !== ear.next &&
189 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
190 | area(n.prev, n, n.next) >= 0) return false;
191 | n = n.nextZ;
192 | }
193 |
194 | // look for remaining points in decreasing z-order
195 | while (p && p.z >= minZ) {
196 | if (p !== ear.prev && p !== ear.next &&
197 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
198 | area(p.prev, p, p.next) >= 0) return false;
199 | p = p.prevZ;
200 | }
201 |
202 | // look for remaining points in increasing z-order
203 | while (n && n.z <= maxZ) {
204 | if (n !== ear.prev && n !== ear.next &&
205 | pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
206 | area(n.prev, n, n.next) >= 0) return false;
207 | n = n.nextZ;
208 | }
209 |
210 | return true;
211 | }
212 |
213 | // go through all polygon nodes and cure small local self-intersections
214 | function cureLocalIntersections(start, triangles, dim) {
215 | var p = start;
216 | do {
217 | var a = p.prev,
218 | b = p.next.next;
219 |
220 | if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
221 |
222 | triangles.push(a.i / dim);
223 | triangles.push(p.i / dim);
224 | triangles.push(b.i / dim);
225 |
226 | // remove two nodes involved
227 | removeNode(p);
228 | removeNode(p.next);
229 |
230 | p = start = b;
231 | }
232 | p = p.next;
233 | } while (p !== start);
234 |
235 | return filterPoints(p);
236 | }
237 |
238 | // try splitting polygon into two and triangulate them independently
239 | function splitEarcut(start, triangles, dim, minX, minY, invSize) {
240 | // look for a valid diagonal that divides the polygon into two
241 | var a = start;
242 | do {
243 | var b = a.next.next;
244 | while (b !== a.prev) {
245 | if (a.i !== b.i && isValidDiagonal(a, b)) {
246 | // split the polygon in two by the diagonal
247 | var c = splitPolygon(a, b);
248 |
249 | // filter colinear points around the cuts
250 | a = filterPoints(a, a.next);
251 | c = filterPoints(c, c.next);
252 |
253 | // run earcut on each half
254 | earcutLinked(a, triangles, dim, minX, minY, invSize);
255 | earcutLinked(c, triangles, dim, minX, minY, invSize);
256 | return;
257 | }
258 | b = b.next;
259 | }
260 | a = a.next;
261 | } while (a !== start);
262 | }
263 |
264 | // link every hole into the outer loop, producing a single-ring polygon without holes
265 | function eliminateHoles(data, holeIndices, outerNode, dim) {
266 | var queue = [],
267 | i, len, start, end, list;
268 |
269 | for (i = 0, len = holeIndices.length; i < len; i++) {
270 | start = holeIndices[i] * dim;
271 | end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
272 | list = linkedList(data, start, end, dim, false);
273 | if (list === list.next) list.steiner = true;
274 | queue.push(getLeftmost(list));
275 | }
276 |
277 | queue.sort(compareX);
278 |
279 | // process holes from left to right
280 | for (i = 0; i < queue.length; i++) {
281 | eliminateHole(queue[i], outerNode);
282 | outerNode = filterPoints(outerNode, outerNode.next);
283 | }
284 |
285 | return outerNode;
286 | }
287 |
288 | function compareX(a, b) {
289 | return a.x - b.x;
290 | }
291 |
292 | // find a bridge between vertices that connects hole with an outer ring and and link it
293 | function eliminateHole(hole, outerNode) {
294 | outerNode = findHoleBridge(hole, outerNode);
295 | if (outerNode) {
296 | var b = splitPolygon(outerNode, hole);
297 |
298 | // filter collinear points around the cuts
299 | filterPoints(outerNode, outerNode.next);
300 | filterPoints(b, b.next);
301 | }
302 | }
303 |
304 | // David Eberly's algorithm for finding a bridge between hole and outer polygon
305 | function findHoleBridge(hole, outerNode) {
306 | var p = outerNode,
307 | hx = hole.x,
308 | hy = hole.y,
309 | qx = -Infinity,
310 | m;
311 |
312 | // find a segment intersected by a ray from the hole's leftmost point to the left;
313 | // segment's endpoint with lesser x will be potential connection point
314 | do {
315 | if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
316 | var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
317 | if (x <= hx && x > qx) {
318 | qx = x;
319 | if (x === hx) {
320 | if (hy === p.y) return p;
321 | if (hy === p.next.y) return p.next;
322 | }
323 | m = p.x < p.next.x ? p : p.next;
324 | }
325 | }
326 | p = p.next;
327 | } while (p !== outerNode);
328 |
329 | if (!m) return null;
330 |
331 | if (hx === qx) return m; // hole touches outer segment; pick leftmost endpoint
332 |
333 | // look for points inside the triangle of hole point, segment intersection and endpoint;
334 | // if there are no points found, we have a valid connection;
335 | // otherwise choose the point of the minimum angle with the ray as connection point
336 |
337 | var stop = m,
338 | mx = m.x,
339 | my = m.y,
340 | tanMin = Infinity,
341 | tan;
342 |
343 | p = m;
344 |
345 | do {
346 | if (hx >= p.x && p.x >= mx && hx !== p.x &&
347 | pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
348 |
349 | tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
350 |
351 | if (locallyInside(p, hole) &&
352 | (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
353 | m = p;
354 | tanMin = tan;
355 | }
356 | }
357 |
358 | p = p.next;
359 | } while (p !== stop);
360 |
361 | return m;
362 | }
363 |
364 | // whether sector in vertex m contains sector in vertex p in the same coordinates
365 | function sectorContainsSector(m, p) {
366 | return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
367 | }
368 |
369 | // interlink polygon nodes in z-order
370 | function indexCurve(start, minX, minY, invSize) {
371 | var p = start;
372 | do {
373 | if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, invSize);
374 | p.prevZ = p.prev;
375 | p.nextZ = p.next;
376 | p = p.next;
377 | } while (p !== start);
378 |
379 | p.prevZ.nextZ = null;
380 | p.prevZ = null;
381 |
382 | sortLinked(p);
383 | }
384 |
385 | // Simon Tatham's linked list merge sort algorithm
386 | // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
387 | function sortLinked(list) {
388 | var i, p, q, e, tail, numMerges, pSize, qSize,
389 | inSize = 1;
390 |
391 | do {
392 | p = list;
393 | list = null;
394 | tail = null;
395 | numMerges = 0;
396 |
397 | while (p) {
398 | numMerges++;
399 | q = p;
400 | pSize = 0;
401 | for (i = 0; i < inSize; i++) {
402 | pSize++;
403 | q = q.nextZ;
404 | if (!q) break;
405 | }
406 | qSize = inSize;
407 |
408 | while (pSize > 0 || (qSize > 0 && q)) {
409 |
410 | if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
411 | e = p;
412 | p = p.nextZ;
413 | pSize--;
414 | } else {
415 | e = q;
416 | q = q.nextZ;
417 | qSize--;
418 | }
419 |
420 | if (tail) tail.nextZ = e;
421 | else list = e;
422 |
423 | e.prevZ = tail;
424 | tail = e;
425 | }
426 |
427 | p = q;
428 | }
429 |
430 | tail.nextZ = null;
431 | inSize *= 2;
432 |
433 | } while (numMerges > 1);
434 |
435 | return list;
436 | }
437 |
438 | // z-order of a point given coords and inverse of the longer side of data bbox
439 | function zOrder(x, y, minX, minY, invSize) {
440 | // coords are transformed into non-negative 15-bit integer range
441 | x = 32767 * (x - minX) * invSize;
442 | y = 32767 * (y - minY) * invSize;
443 |
444 | x = (x | (x << 8)) & 0x00FF00FF;
445 | x = (x | (x << 4)) & 0x0F0F0F0F;
446 | x = (x | (x << 2)) & 0x33333333;
447 | x = (x | (x << 1)) & 0x55555555;
448 |
449 | y = (y | (y << 8)) & 0x00FF00FF;
450 | y = (y | (y << 4)) & 0x0F0F0F0F;
451 | y = (y | (y << 2)) & 0x33333333;
452 | y = (y | (y << 1)) & 0x55555555;
453 |
454 | return x | (y << 1);
455 | }
456 |
457 | // find the leftmost node of a polygon ring
458 | function getLeftmost(start) {
459 | var p = start,
460 | leftmost = start;
461 | do {
462 | if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
463 | p = p.next;
464 | } while (p !== start);
465 |
466 | return leftmost;
467 | }
468 |
469 | // check if a point lies within a convex triangle
470 | function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
471 | return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
472 | (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
473 | (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
474 | }
475 |
476 | // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
477 | function isValidDiagonal(a, b) {
478 | return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
479 | (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
480 | (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
481 | equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
482 | }
483 |
484 | // signed area of a triangle
485 | function area(p, q, r) {
486 | return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
487 | }
488 |
489 | // check if two points are equal
490 | function equals(p1, p2) {
491 | return p1.x === p2.x && p1.y === p2.y;
492 | }
493 |
494 | // check if two segments intersect
495 | function intersects(p1, q1, p2, q2) {
496 | var o1 = sign(area(p1, q1, p2));
497 | var o2 = sign(area(p1, q1, q2));
498 | var o3 = sign(area(p2, q2, p1));
499 | var o4 = sign(area(p2, q2, q1));
500 |
501 | if (o1 !== o2 && o3 !== o4) return true; // general case
502 |
503 | if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
504 | if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
505 | if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
506 | if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
507 |
508 | return false;
509 | }
510 |
511 | // for collinear points p, q, r, check if point q lies on segment pr
512 | function onSegment(p, q, r) {
513 | return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
514 | }
515 |
516 | function sign(num) {
517 | return num > 0 ? 1 : num < 0 ? -1 : 0;
518 | }
519 |
520 | // check if a polygon diagonal intersects any polygon segments
521 | function intersectsPolygon(a, b) {
522 | var p = a;
523 | do {
524 | if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
525 | intersects(p, p.next, a, b)) return true;
526 | p = p.next;
527 | } while (p !== a);
528 |
529 | return false;
530 | }
531 |
532 | // check if a polygon diagonal is locally inside the polygon
533 | function locallyInside(a, b) {
534 | return area(a.prev, a, a.next) < 0 ?
535 | area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
536 | area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
537 | }
538 |
539 | // check if the middle point of a polygon diagonal is inside the polygon
540 | function middleInside(a, b) {
541 | var p = a,
542 | inside = false,
543 | px = (a.x + b.x) / 2,
544 | py = (a.y + b.y) / 2;
545 | do {
546 | if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
547 | (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
548 | inside = !inside;
549 | p = p.next;
550 | } while (p !== a);
551 |
552 | return inside;
553 | }
554 |
555 | // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
556 | // if one belongs to the outer ring and another to a hole, it merges it into a single ring
557 | function splitPolygon(a, b) {
558 | var a2 = new Node(a.i, a.x, a.y),
559 | b2 = new Node(b.i, b.x, b.y),
560 | an = a.next,
561 | bp = b.prev;
562 |
563 | a.next = b;
564 | b.prev = a;
565 |
566 | a2.next = an;
567 | an.prev = a2;
568 |
569 | b2.next = a2;
570 | a2.prev = b2;
571 |
572 | bp.next = b2;
573 | b2.prev = bp;
574 |
575 | return b2;
576 | }
577 |
578 | // create a node and optionally link it with previous one (in a circular doubly linked list)
579 | function insertNode(i, x, y, last) {
580 | var p = new Node(i, x, y);
581 |
582 | if (!last) {
583 | p.prev = p;
584 | p.next = p;
585 |
586 | } else {
587 | p.next = last.next;
588 | p.prev = last;
589 | last.next.prev = p;
590 | last.next = p;
591 | }
592 | return p;
593 | }
594 |
595 | function removeNode(p) {
596 | p.next.prev = p.prev;
597 | p.prev.next = p.next;
598 |
599 | if (p.prevZ) p.prevZ.nextZ = p.nextZ;
600 | if (p.nextZ) p.nextZ.prevZ = p.prevZ;
601 | }
602 |
603 | function Node(i, x, y) {
604 | // vertex index in coordinates array
605 | this.i = i;
606 |
607 | // vertex coordinates
608 | this.x = x;
609 | this.y = y;
610 |
611 | // previous and next vertex nodes in a polygon ring
612 | this.prev = null;
613 | this.next = null;
614 |
615 | // z-order curve value
616 | this.z = null;
617 |
618 | // previous and next nodes in z-order
619 | this.prevZ = null;
620 | this.nextZ = null;
621 |
622 | // indicates whether this is a steiner point
623 | this.steiner = false;
624 | }
625 |
626 | // return a percentage difference between the polygon area and its triangulation area;
627 | // used to verify correctness of triangulation
628 | earcut.deviation = function (data, holeIndices, dim, triangles) {
629 | var hasHoles = holeIndices && holeIndices.length;
630 | var outerLen = hasHoles ? holeIndices[0] * dim : data.length;
631 |
632 | var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
633 | if (hasHoles) {
634 | for (var i = 0, len = holeIndices.length; i < len; i++) {
635 | var start = holeIndices[i] * dim;
636 | var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
637 | polygonArea -= Math.abs(signedArea(data, start, end, dim));
638 | }
639 | }
640 |
641 | var trianglesArea = 0;
642 | for (i = 0; i < triangles.length; i += 3) {
643 | var a = triangles[i] * dim;
644 | var b = triangles[i + 1] * dim;
645 | var c = triangles[i + 2] * dim;
646 | trianglesArea += Math.abs(
647 | (data[a] - data[c]) * (data[b + 1] - data[a + 1]) -
648 | (data[a] - data[b]) * (data[c + 1] - data[a + 1]));
649 | }
650 |
651 | return polygonArea === 0 && trianglesArea === 0 ? 0 :
652 | Math.abs((trianglesArea - polygonArea) / polygonArea);
653 | };
654 |
655 | function signedArea(data, start, end, dim) {
656 | var sum = 0;
657 | for (var i = start, j = end - dim; i < end; i += dim) {
658 | sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
659 | j = i;
660 | }
661 | return sum;
662 | }
663 |
664 | // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
665 | earcut.flatten = function (data) {
666 | var dim = data[0][0].length,
667 | result = {vertices: [], holes: [], dimensions: dim},
668 | holeIndex = 0;
669 |
670 | for (var i = 0; i < data.length; i++) {
671 | for (var j = 0; j < data[i].length; j++) {
672 | for (var d = 0; d < dim; d++) result.vertices.push(data[i][j][d]);
673 | }
674 | if (i > 0) {
675 | holeIndex += data[i - 1].length;
676 | result.holes.push(holeIndex);
677 | }
678 | }
679 | return result;
680 | };
681 |
682 | export { earcut };
683 |
--------------------------------------------------------------------------------
/spirals/fit-curve.js:
--------------------------------------------------------------------------------
1 | // ==ClosureCompiler==
2 | // @output_file_name fit-curve.min.js
3 | // @compilation_level SIMPLE_OPTIMIZATIONS
4 | // ==/ClosureCompiler==
5 |
6 | 'use strict';
7 |
8 | /**
9 | * @preserve JavaScript implementation of
10 | * Algorithm for Automatically Fitting Digitized Curves
11 | * by Philip J. Schneider
12 | * "Graphics Gems", Academic Press, 1990
13 | *
14 | * The MIT License (MIT)
15 | *
16 | * https://github.com/soswow/fit-curves
17 | */
18 |
19 | /**
20 | * Fit one or more Bezier curves to a set of points.
21 | *
22 | * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
23 | * @param {Number} maxError - Tolerance, squared error between points and fitted curve
24 | * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
25 | */
26 | function fitCurve(points, maxError, progressCallback) {
27 | if (!Array.isArray(points)) {
28 | throw new TypeError("First argument should be an array");
29 | }
30 | points.forEach((point) => {
31 | if(!Array.isArray(point) || point.some(item => typeof item !== 'number')
32 | || point.length !== points[0].length) {
33 | throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers.");
34 | }
35 | });
36 |
37 | // Remove duplicate points
38 | points = points.filter((point, i) =>
39 | i === 0 || !point.every((val, j) => val === points[i-1][j])
40 | );
41 |
42 | if (points.length < 2) {
43 | return [];
44 | }
45 |
46 | const len = points.length;
47 | const leftTangent = createTangent(points[1], points[0]);
48 | const rightTangent = createTangent(points[len - 2], points[len - 1]);
49 |
50 | return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback);
51 | }
52 |
53 | /**
54 | * Fit a Bezier curve to a (sub)set of digitized points.
55 | * Your code should not call this function directly. Use {@link fitCurve} instead.
56 | *
57 | * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
58 | * @param {Array} leftTangent - Unit tangent vector at start point
59 | * @param {Array} rightTangent - Unit tangent vector at end point
60 | * @param {Number} error - Tolerance, squared error between points and fitted curve
61 | * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
62 | */
63 | function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
64 | const MaxIterations = 20; //Max times to try iterating (to find an acceptable curve)
65 |
66 | var bezCurve, //Control points of fitted Bezier curve
67 | u, //Parameter values for point
68 | uPrime, //Improved parameter values
69 | maxError, prevErr, //Maximum fitting error
70 | splitPoint, prevSplit, //Point to split point set at if we need more than one curve
71 | centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint
72 | beziers, //Array of fitted Bezier curves if we need more than one curve
73 | dist, i;
74 |
75 | //console.log('fitCubic, ', points.length);
76 |
77 | //Use heuristic if region only has two points in it
78 | if (points.length === 2) {
79 | dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
80 | bezCurve = [
81 | points[0],
82 | maths.addArrays(points[0], maths.mulItems(leftTangent, dist)),
83 | maths.addArrays(points[1], maths.mulItems(rightTangent, dist)),
84 | points[1]
85 | ];
86 | return [bezCurve];
87 | }
88 |
89 |
90 | //Parameterize points, and attempt to fit curve
91 | u = chordLengthParameterize(points);
92 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback)
93 |
94 | if (maxError < error) {
95 | return [bezCurve];
96 | }
97 | //If error not too large, try some reparameterization and iteration
98 | if (maxError < (error*error)) {
99 |
100 | uPrime = u;
101 | prevErr = maxError;
102 | prevSplit = splitPoint;
103 |
104 | for (i = 0; i < MaxIterations; i++) {
105 |
106 | uPrime = reparameterize(bezCurve, points, uPrime);
107 | [bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback);
108 |
109 | if (maxError < error) {
110 | return [bezCurve];
111 | }
112 | //If the development of the fitted curve grinds to a halt,
113 | //we abort this attempt (and try a shorter curve):
114 | else if(splitPoint === prevSplit) {
115 | let errChange = maxError/prevErr;
116 | if((errChange > .9999) && (errChange < 1.0001)) {
117 | break;
118 | }
119 | }
120 |
121 | prevErr = maxError;
122 | prevSplit = splitPoint;
123 | }
124 | }
125 |
126 | //Fitting failed -- split at max error point and fit recursively
127 | beziers = [];
128 |
129 | //To create a smooth transition from one curve segment to the next, we
130 | //calculate the line between the points directly before and after the
131 | //center, and use that as the tangent both to and from the center point.
132 | centerVector = maths.subtract(points[splitPoint-1], points[splitPoint+1]);
133 | //However, this won't work if they're the same point, because the line we
134 | //want to use as a tangent would be 0. Instead, we calculate the line from
135 | //that "double-point" to the center point, and use its tangent.
136 | if(centerVector.every(val => val === 0)) {
137 | //[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
138 | centerVector = maths.subtract(points[splitPoint-1], points[splitPoint]);
139 | [centerVector[0],centerVector[1]] = [-centerVector[1],centerVector[0]];
140 | }
141 | toCenterTangent = maths.normalize(centerVector);
142 | //To and from need to point in opposite directions:
143 | fromCenterTangent = maths.mulItems(toCenterTangent, -1);
144 |
145 | /*
146 | Note: An alternative to this "divide and conquer" recursion could be to always
147 | let new curve segments start by trying to go all the way to the end,
148 | instead of only to the end of the current subdivided polyline.
149 | That might let many segments fit a few points more, reducing the number of total segments.
150 | However, a few tests have shown that the segment reduction is insignificant
151 | (240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
152 | and the results take twice as many steps and milliseconds to finish,
153 | without looking any better than what we already have.
154 | */
155 | beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback));
156 | beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback));
157 | return beziers;
158 | };
159 |
160 | function generateAndReport(points, paramsOrig, paramsPrime, leftTangent, rightTangent, progressCallback) {
161 | var bezCurve, maxError, splitPoint;
162 |
163 | bezCurve = generateBezier(points, paramsPrime, leftTangent, rightTangent, progressCallback);
164 | //Find max deviation of points to fitted curve.
165 | //Here we always use the original parameters (from chordLengthParameterize()),
166 | //because we need to compare the current curve to the actual source polyline,
167 | //and not the currently iterated parameters which reparameterize() & generateBezier() use,
168 | //as those have probably drifted far away and may no longer be in ascending order.
169 | [maxError, splitPoint] = computeMaxError(points, bezCurve, paramsOrig);
170 |
171 | if(progressCallback) {
172 | progressCallback({
173 | bez: bezCurve,
174 | points: points,
175 | params: paramsOrig,
176 | maxErr: maxError,
177 | maxPoint: splitPoint,
178 | });
179 | }
180 |
181 | return [bezCurve, maxError, splitPoint];
182 | }
183 |
184 | /**
185 | * Use least-squares method to find Bezier control points for region.
186 | *
187 | * @param {Array>} points - Array of digitized points
188 | * @param {Array} parameters - Parameter values for region
189 | * @param {Array} leftTangent - Unit tangent vector at start point
190 | * @param {Array} rightTangent - Unit tangent vector at end point
191 | * @returns {Array>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y]
192 | */
193 | function generateBezier(points, parameters, leftTangent, rightTangent) {
194 | var bezCurve, //Bezier curve ctl pts
195 | A, a, //Precomputed rhs for eqn
196 | C, X, //Matrices C & X
197 | det_C0_C1, det_C0_X, det_X_C1, //Determinants of matrices
198 | alpha_l, alpha_r, //Alpha values, left and right
199 |
200 | epsilon, segLength,
201 | i, len, tmp, u, ux,
202 | firstPoint = points[0],
203 | lastPoint = points[points.length-1];
204 |
205 | bezCurve = [firstPoint, null, null, lastPoint];
206 | //console.log('gb', parameters.length);
207 |
208 | //Compute the A's
209 | A = maths.zeros_Xx2x2(parameters.length);
210 | for (i = 0, len = parameters.length; i < len; i++) {
211 | u = parameters[i];
212 | ux = 1 - u;
213 | a = A[i];
214 |
215 | a[0] = maths.mulItems(leftTangent, 3 * u * (ux*ux));
216 | a[1] = maths.mulItems(rightTangent, 3 * ux * (u*u));
217 | }
218 |
219 | //Create the C and X matrices
220 | C = [[0,0], [0,0]];
221 | X = [0,0];
222 | for (i = 0, len = points.length; i < len; i++) {
223 | u = parameters[i];
224 | a = A[i];
225 |
226 | C[0][0] += maths.dot(a[0], a[0]);
227 | C[0][1] += maths.dot(a[0], a[1]);
228 | C[1][0] += maths.dot(a[0], a[1]);
229 | C[1][1] += maths.dot(a[1], a[1]);
230 |
231 | tmp = maths.subtract(points[i], bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u));
232 |
233 | X[0] += maths.dot(a[0], tmp);
234 | X[1] += maths.dot(a[1], tmp);
235 | }
236 |
237 | //Compute the determinants of C and X
238 | det_C0_C1 = (C[0][0] * C[1][1]) - (C[1][0] * C[0][1]);
239 | det_C0_X = (C[0][0] * X[1] ) - (C[1][0] * X[0] );
240 | det_X_C1 = (X[0] * C[1][1]) - (X[1] * C[0][1]);
241 |
242 | //Finally, derive alpha values
243 | alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
244 | alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
245 |
246 | //If alpha negative, use the Wu/Barsky heuristic (see text).
247 | //If alpha is 0, you get coincident control points that lead to
248 | //divide by zero in any subsequent NewtonRaphsonRootFind() call.
249 | segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint));
250 | epsilon = 1.0e-6 * segLength;
251 | if (alpha_l < epsilon || alpha_r < epsilon) {
252 | //Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
253 | bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, segLength / 3.0));
254 | bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, segLength / 3.0));
255 | } else {
256 | //First and last control points of the Bezier curve are
257 | //positioned exactly at the first and last data points
258 | //Control points 1 and 2 are positioned an alpha distance out
259 | //on the tangent vectors, left and right, respectively
260 | bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, alpha_l));
261 | bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, alpha_r));
262 | }
263 |
264 | return bezCurve;
265 | };
266 |
267 | /**
268 | * Given set of points and their parameterization, try to find a better parameterization.
269 | *
270 | * @param {Array>} bezier - Current fitted curve
271 | * @param {Array>} points - Array of digitized points
272 | * @param {Array} parameters - Current parameter values
273 | * @returns {Array} New parameter values
274 | */
275 | function reparameterize(bezier, points, parameters) {
276 | /*
277 | var j, len, point, results, u;
278 | results = [];
279 | for (j = 0, len = points.length; j < len; j++) {
280 | point = points[j], u = parameters[j];
281 | results.push(newtonRaphsonRootFind(bezier, point, u));
282 | }
283 | return results;
284 | //*/
285 | return parameters.map((p, i) => newtonRaphsonRootFind(bezier, points[i], p));
286 | };
287 |
288 | /**
289 | * Use Newton-Raphson iteration to find better root.
290 | *
291 | * @param {Array>} bez - Current fitted curve
292 | * @param {Array} point - Digitized point
293 | * @param {Number} u - Parameter value for "P"
294 | * @returns {Number} New u
295 | */
296 | function newtonRaphsonRootFind(bez, point, u) {
297 | /*
298 | Newton's root finding algorithm calculates f(x)=0 by reiterating
299 | x_n+1 = x_n - f(x_n)/f'(x_n)
300 | We are trying to find curve parameter u for some point p that minimizes
301 | the distance from that point to the curve. Distance point to curve is d=q(u)-p.
302 | At minimum distance the point is perpendicular to the curve.
303 | We are solving
304 | f = q(u)-p * q'(u) = 0
305 | with
306 | f' = q'(u) * q'(u) + q(u)-p * q''(u)
307 | gives
308 | u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
309 | */
310 |
311 | var d = maths.subtract(bezier.q(bez, u), point),
312 | qprime = bezier.qprime(bez, u),
313 | numerator = maths.mulMatrix(d, qprime),
314 | denominator = maths.sum(maths.squareItems(qprime)) + 2 * maths.mulMatrix(d, bezier.qprimeprime(bez, u));
315 |
316 | if (denominator === 0) {
317 | return u;
318 | } else {
319 | return u - (numerator/denominator);
320 | }
321 | };
322 |
323 | /**
324 | * Assign parameter values to digitized points using relative distances between points.
325 | *
326 | * @param {Array>} points - Array of digitized points
327 | * @returns {Array} Parameter values
328 | */
329 | function chordLengthParameterize(points) {
330 | var u = [], currU, prevU, prevP;
331 |
332 | points.forEach((p, i) => {
333 | currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP))
334 | : 0;
335 | u.push(currU);
336 |
337 | prevU = currU;
338 | prevP = p;
339 | })
340 | u = u.map(x => x/prevU);
341 |
342 | return u;
343 | };
344 |
345 | /**
346 | * Find the maximum squared distance of digitized points to fitted curve.
347 | *
348 | * @param {Array>} points - Array of digitized points
349 | * @param {Array>} bez - Fitted curve
350 | * @param {Array} parameters - Parameterization of points
351 | * @returns {Array} Maximum error (squared) and point of max error
352 | */
353 | function computeMaxError(points, bez, parameters) {
354 | var dist, //Current error
355 | maxDist, //Maximum error
356 | splitPoint, //Point of maximum error
357 | v, //Vector from point to curve
358 | i, count, point, t;
359 |
360 | maxDist = 0;
361 | splitPoint = points.length / 2;
362 |
363 | const t_distMap = mapTtoRelativeDistances(bez, 10);
364 |
365 | for (i = 0, count = points.length; i < count; i++) {
366 | point = points[i];
367 | //Find 't' for a point on the bez curve that's as close to 'point' as possible:
368 | t = find_t(bez, parameters[i], t_distMap, 10);
369 |
370 | v = maths.subtract(bezier.q(bez, t), point);
371 | dist = v[0]*v[0] + v[1]*v[1];
372 |
373 | if (dist > maxDist) {
374 | maxDist = dist;
375 | splitPoint = i;
376 | }
377 | }
378 |
379 | return [maxDist, splitPoint];
380 | };
381 |
382 | //Sample 't's and map them to relative distances along the curve:
383 | var mapTtoRelativeDistances = function (bez, B_parts) {
384 | var B_t_curr;
385 | var B_t_dist = [0];
386 | var B_t_prev = bez[0];
387 | var sumLen = 0;
388 |
389 | for (var i=1; i<=B_parts; i++) {
390 | B_t_curr = bezier.q(bez, i/B_parts);
391 |
392 | sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev));
393 |
394 | B_t_dist.push(sumLen);
395 | B_t_prev = B_t_curr;
396 | }
397 |
398 | //Normalize B_length to the same interval as the parameter distances; 0 to 1:
399 | B_t_dist = B_t_dist.map(x => x/sumLen);
400 | return B_t_dist;
401 | };
402 |
403 | function find_t(bez, param, t_distMap, B_parts) {
404 | if(param < 0) { return 0; }
405 | if(param > 1) { return 1; }
406 |
407 | /*
408 | 'param' is a value between 0 and 1 telling us the relative position
409 | of a point on the source polyline (linearly from the start (0) to the end (1)).
410 | To see if a given curve - 'bez' - is a close approximation of the polyline,
411 | we compare such a poly-point to the point on the curve that's the same
412 | relative distance along the curve's length.
413 | But finding that curve-point takes a little work:
414 | There is a function "B(t)" to find points along a curve from the parametric parameter 't'
415 | (also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
416 | http://pomax.github.io/bezierinfo/#explanation),
417 | but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
418 | So, we sample some points along the curve using a handful of values for 't'.
419 | Then, we calculate the length between those samples via plain euclidean distance;
420 | B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve.
421 | Thus, for a given relative distance ('param'), we can now find an upper and lower value
422 | for the corresponding 't' by searching through those sampled distances.
423 | Finally, we just use linear interpolation to find a better value for the exact 't'.
424 | More info:
425 | http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
426 | http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
427 | http://steve.hollasch.net/cgindex/curves/cbezarclen.html
428 | https://github.com/retuxx/tinyspline
429 | */
430 | var lenMax, lenMin, tMax, tMin, t;
431 |
432 | //Find the two t-s that the current param distance lies between,
433 | //and then interpolate a somewhat accurate value for the exact t:
434 | for(var i = 1; i <= B_parts; i++) {
435 |
436 | if(param <= t_distMap[i]) {
437 | tMin = (i-1) / B_parts;
438 | tMax = i / B_parts;
439 | lenMin = t_distMap[i-1];
440 | lenMax = t_distMap[i];
441 |
442 | t = (param-lenMin)/(lenMax-lenMin) * (tMax-tMin) + tMin;
443 | break;
444 | }
445 | }
446 | return t;
447 | }
448 |
449 | /**
450 | * Creates a vector of length 1 which shows the direction from B to A
451 | */
452 | function createTangent(pointA, pointB) {
453 | return maths.normalize(maths.subtract(pointA, pointB));
454 | }
455 |
456 | /*
457 | Simplified versions of what we need from math.js
458 | Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates).
459 | */
460 | class maths {
461 | //zeros = logAndRun(math.zeros);
462 | static zeros_Xx2x2(x) {
463 | var zs = [];
464 | while(x--) { zs.push([0,0]); }
465 | return zs
466 | }
467 |
468 | //multiply = logAndRun(math.multiply);
469 | static mulItems(items, multiplier) {
470 | return items.map(x => x*multiplier);
471 | }
472 | static mulMatrix(m1, m2) {
473 | //https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29
474 | //Simplified to only handle 1-dimensional matrices (i.e. arrays) of equal length:
475 | return m1.reduce((sum,x1,i) => sum + (x1*m2[i]), 0);
476 | }
477 |
478 | //Only used to subract to points (or at least arrays):
479 | // subtract = logAndRun(math.subtract);
480 | static subtract(arr1, arr2) {
481 | return arr1.map((x1, i) => x1 - arr2[i]);
482 | }
483 |
484 | //add = logAndRun(math.add);
485 | static addArrays(arr1, arr2) {
486 | return arr1.map((x1, i) => x1 + arr2[i]);
487 | }
488 | static addItems(items, addition) {
489 | return items.map(x => x+addition);
490 | }
491 |
492 | //var sum = logAndRun(math.sum);
493 | static sum(items) {
494 | return items.reduce((sum,x) => sum + x);
495 | }
496 |
497 | //chain = math.chain;
498 |
499 | //Only used on two arrays. The dot product is equal to the matrix product in this case:
500 | // dot = logAndRun(math.dot);
501 | static dot(m1, m2) {
502 | return maths.mulMatrix(m1, m2);
503 | }
504 |
505 | //https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm
506 | // var norm = logAndRun(math.norm);
507 | static vectorLen(v) {
508 | return Math.hypot(...v);
509 | }
510 |
511 | //math.divide = logAndRun(math.divide);
512 | static divItems(items, divisor) {
513 | return items.map(x => x/divisor);
514 | }
515 |
516 | //var dotPow = logAndRun(math.dotPow);
517 | static squareItems(items) {
518 | return items.map(x => x*x);
519 | }
520 |
521 | static normalize(v) {
522 | return this.divItems(v, this.vectorLen(v));
523 | }
524 |
525 | //Math.pow = logAndRun(Math.pow);
526 | }
527 |
528 |
529 | class bezier {
530 | //Evaluates cubic bezier at t, return point
531 | static q(ctrlPoly, t) {
532 | var tx = 1.0 - t;
533 | var pA = maths.mulItems( ctrlPoly[0], tx * tx * tx ),
534 | pB = maths.mulItems( ctrlPoly[1], 3 * tx * tx * t ),
535 | pC = maths.mulItems( ctrlPoly[2], 3 * tx * t * t ),
536 | pD = maths.mulItems( ctrlPoly[3], t * t * t );
537 | return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD));
538 | }
539 |
540 | //Evaluates cubic bezier first derivative at t, return point
541 | static qprime(ctrlPoly, t) {
542 | var tx = 1.0 - t;
543 | var pA = maths.mulItems( maths.subtract(ctrlPoly[1], ctrlPoly[0]), 3 * tx * tx ),
544 | pB = maths.mulItems( maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t ),
545 | pC = maths.mulItems( maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t );
546 | return maths.addArrays(maths.addArrays(pA, pB), pC);
547 | }
548 |
549 | //Evaluates cubic bezier second derivative at t, return point
550 | static qprimeprime(ctrlPoly, t) {
551 | return maths.addArrays(maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)), ctrlPoly[0]), 6 * (1.0 - t) ),
552 | maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)), ctrlPoly[1]), 6 * t ));
553 | }
554 | }
555 |
556 | // module.exports = fitCurve;
557 | export { fitCurve };
558 |
--------------------------------------------------------------------------------
/spirals/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spirals/spirals.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2019 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | 'use strict';
10 |
11 | import { fitCurve } from './fit-curve.js';
12 | import { earcut } from './earcut.js'
13 | import { mul, matchSeg, EdgeShape, numTypes, tilingTypes, IsohedralTiling }
14 | from '../lib/tactile.js';
15 |
16 | function sktch( p5c )
17 | {
18 | let the_type = null;
19 | let params = null;
20 | let tiling = null;
21 | let edges = null;
22 | let tile_shape = null;
23 | let triangles = null;
24 |
25 | let colouring = null;
26 | let uniform_colouring = null;
27 | let min_colouring = null;
28 |
29 | let phys_unit; // Ideally, about a centimeter
30 | let edit_box = null;
31 |
32 | const MODE_NONE = 9000;
33 | const MODE_MOVE_VERTEX = 9001;
34 | const MODE_ADJ_TILE = 9002;
35 | const MODE_ADJ_TV = 9003;
36 | const MODE_ADJ_TILING = 9004;
37 |
38 | let spiral_A = 1;
39 | let spiral_B = 5;
40 | let tiling_V = { x: 0.0, y: 0.0 };
41 | let tiling_T = null;
42 | let tiling_iT = null;
43 |
44 | let tiling_V_down = null;
45 |
46 | let mode = MODE_NONE;
47 | let drag_tv = null;
48 | let drag_tv_offs = null;
49 |
50 | let editor_T;
51 | let editor_T_down;
52 | let drag_edge_shape = -1;
53 | let drag_vertex = -1;
54 | let drag_T = null;
55 | let u_constrain = false;
56 |
57 | let down_motion = null;
58 | let delete_timer = null;
59 | let animating = false;
60 | let fullscreen = false;
61 | let colour = false;
62 |
63 | let fnt = null;
64 |
65 | let msgs = [];
66 | let DEBUG = true;
67 | function dbg( s ) {
68 | if( DEBUG ) {
69 | msgs.push( s );
70 | p5c.loop();
71 | }
72 | }
73 |
74 | let WIDTH = null;
75 | let HEIGHT = null;
76 | const FBO_DIM = 256;
77 | let fbo = null;
78 | let fbo_M = null;
79 |
80 | let ih_slider = null;
81 | let ih_label = null;
82 |
83 | let A_slider = null;
84 | let A_label = null;
85 | let B_slider = null;
86 | let B_label = null;
87 | let do_mobius = false;
88 |
89 | let tv_sliders = null;
90 |
91 | let help_button = null;
92 | let fullscreen_button = null;
93 | let colour_button = null;
94 | let animate_button = null;
95 | let save_button = null;
96 |
97 | const COLS = [
98 | [ 25, 52, 65 ],
99 | [ 62, 96, 111 ],
100 | [ 145, 170, 157 ],
101 | [ 209, 219, 189 ],
102 | [ 252, 255, 245 ],
103 | [ 219, 188, 209 ] ];
104 |
105 | const XMLNS = "http://www.w3.org/2000/svg";
106 | const XLINK = "http://www.w3.org/1999/xlink";
107 |
108 | let shad1;
109 |
110 | class Permutation {
111 | static rank( p ) {
112 | let identity = Object.keys( p );
113 | let product = p.slice();
114 | let rank = 1;
115 | while ( product.join() !== identity.join() ) {
116 | product = this.mult( product, p );
117 | rank++;
118 | }
119 | return rank;
120 | }
121 |
122 | static pow( p, exp ) {
123 | let product = p.slice();
124 | for ( let i = 0; i < exp - 1; i++ ) {
125 | product = this.mult( product, p );
126 | }
127 | return product;
128 | }
129 |
130 | static mult( p1, p2 ) {
131 | if ( p1.length !== p2.length ) {
132 | return [ ];
133 | }
134 | return p1.map( x => p2[ x ] );
135 | }
136 |
137 | static evaluate( p, start, num_times ) {
138 | let val = p[ start ];
139 | for ( let idx = 0; idx < num_times; ++idx ) {
140 | val = p[ val ];
141 | }
142 | return val;
143 | }
144 | }
145 |
146 | class Colouring {
147 | constructor( tiling, cols, init, p1, p2 ) {
148 | this.tiling = tiling;
149 | this.cols = cols;
150 | this.init = init;
151 | this.p1 = p1;
152 | this.p1rank = Permutation.rank( p1 );
153 | this.p2 = p2;
154 | this.p2rank = Permutation.rank( p2 );
155 | }
156 |
157 | getColour( a, b, asp ) {
158 | /*
159 | const nc = this.cols.length;
160 | let mt = function( a ) {
161 | let _mt = a % nc;
162 | return _mt < 0 ? _mt + nc : _mt;
163 | };
164 | c = Permutation.evaluate( this.p1, c, mt( a ) );
165 | c = Permutation.evaluate( this.p2, c, mt( b ) );
166 | */
167 |
168 | let c = this.init[ asp ];
169 | const r1 = this.p1rank;
170 | const r2 = this.p2rank;
171 |
172 | c = Permutation.evaluate( this.p1, c, ((a%r1)+r1)%r1 );
173 | c = Permutation.evaluate( this.p2, c, ((b%r2)+r2)%r2 );
174 |
175 | return this.cols[ c ];
176 | }
177 | }
178 |
179 | class UniformColouring extends Colouring {
180 | constructor( tiling, col ) {
181 | const nasps = tiling.numAspects();
182 | const init = new Array( nasps ).fill( 0 );
183 | const p = [ 0 ];
184 | super( tiling, [ col ], init, p, p );
185 | }
186 | }
187 |
188 | class MinColouring extends Colouring {
189 | constructor( tiling, cols ) {
190 | const clrg = tiling.ttd.colouring;
191 | const init = clrg.slice( 0, tiling.numAspects() );
192 | const p1 = clrg.slice( 12, 15 );
193 | const p2 = clrg.slice( 15, 18 );
194 | super( tiling, cols, init, p1, p2 );
195 | }
196 | }
197 |
198 | function sub( V, W ) { return { x: V.x-W.x, y: V.y-W.y }; }
199 | function dot( V, W ) { return V.x*W.x + V.y*W.y; }
200 | function len( V ) { return Math.sqrt( dot( V, V ) ); }
201 | function ptdist( V, W ) { return len( sub( V, W ) ); }
202 | function inv( T ) {
203 | const det = T[0]*T[4] - T[1]*T[3];
204 | return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det,
205 | -T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det];
206 | }
207 | function normalize( V ) {
208 | const l = len( V );
209 | return { x: V.x / l, y: V.y / l };
210 | }
211 |
212 | function makeBox( x, y, w, h )
213 | {
214 | return { x: x, y: y, w: w, h: h };
215 | }
216 |
217 | function hitBox( x, y, B )
218 | {
219 | return (x >= B.x) && (x <= (B.x+B.w)) && (y >= B.y) && (y <= (B.y+B.h));
220 | }
221 |
222 | let fake_serial = 123456;
223 | let all_touch_ids = [];
224 | let my_touches = {};
225 | let num_touches = 0;
226 | let max_touches = 1;
227 |
228 | function addTouch( x, y, id )
229 | {
230 | if( num_touches < max_touches ) {
231 | my_touches[id] = {
232 | down: { x: x, y: y },
233 | prev: { x: x, y: y },
234 | pos: { x: x, y: y },
235 | id: id,
236 | t: p5c.millis() };
237 | ++num_touches;
238 | doTouchStarted( id );
239 | }
240 | }
241 |
242 | p5c.touchStarted = function()
243 | {
244 | if( p5c.touches.length == 0 ) {
245 | addTouch( p5c.mouseX, p5c.mouseY, fake_serial );
246 | ++fake_serial;
247 | } else {
248 | all_touch_ids = [];
249 | for( let tch of p5c.touches ) {
250 | all_touch_ids.push( tch.id );
251 |
252 | if( !(tch.id in my_touches) ) {
253 | addTouch( tch.x, tch.y, tch.id );
254 | }
255 | }
256 | }
257 | }
258 |
259 | p5c.touchMoved = function()
260 | {
261 | if( num_touches > 0 ) {
262 | if( p5c.touches.length == 0 ) {
263 | for( let k in my_touches ) {
264 | let tch = my_touches[k];
265 |
266 | tch.prev = tch.pos;
267 | tch.pos = { x: p5c.mouseX, y: p5c.mouseY };
268 | }
269 | } else {
270 | for( let tch of p5c.touches ) {
271 | if( tch.id in my_touches ) {
272 | let atch = my_touches[ tch.id ];
273 | atch.prev = atch.pos;
274 | atch.pos = { x: tch.x, y: tch.y };
275 | }
276 | }
277 | }
278 |
279 | return doTouchMoved();
280 | }
281 | }
282 |
283 | p5c.touchEnded = function()
284 | {
285 | // If we're on a mouse device, touches will be empty and this should
286 | // work regardless.
287 |
288 | let new_ids = [];
289 |
290 | for( let k in my_touches ) {
291 | my_touches[k].present = false;
292 | }
293 |
294 | for( let tch of p5c.touches ) {
295 | const id = tch.id;
296 | new_ids.push( id );
297 | if( id in my_touches ) {
298 | my_touches[id].present = true;
299 | }
300 | }
301 |
302 | for( let k in my_touches ) {
303 | if( !my_touches[k].present ) {
304 | // This one is going away.
305 | doTouchEnded( k );
306 | delete my_touches[ k ];
307 | --num_touches;
308 | }
309 | }
310 |
311 | u_constrain = false;
312 | }
313 |
314 | function cacheTileShape()
315 | {
316 | tile_shape = [];
317 | let blah = [];
318 |
319 | for( let i of tiling.parts() ) {
320 | const ej = edges[i.id];
321 | let cur = i.rev ? (ej.length-2) : 1;
322 | const inc = i.rev ? -1 : 1;
323 |
324 | for( let idx = 0; idx < ej.length - 1; ++idx ) {
325 | const { x, y } = mul( i.T, ej[cur] );
326 | tile_shape.push( { x : x, y : y } );
327 | blah.push( x );
328 | blah.push( y );
329 | cur += inc;
330 | }
331 | }
332 |
333 | triangles = earcut( blah );
334 |
335 | drawTranslationalUnit();
336 | }
337 |
338 | function setTilingType()
339 | {
340 | const tp = tilingTypes[ the_type ];
341 | tiling.reset( tp );
342 | params = tiling.getParameters();
343 |
344 | uniform_colouring = new UniformColouring( tiling, COLS[ 4 ] );
345 | min_colouring = new MinColouring( tiling, COLS.slice( 1, 4 ) );
346 |
347 | edges = [];
348 | for( let idx = 0; idx < tiling.numEdgeShapes(); ++idx ) {
349 | let ej = [{ x: 0, y: 0 }, { x: 1, y: 0 }];
350 | edges.push( ej );
351 | }
352 |
353 | cacheTileShape();
354 | calcEditorTransform();
355 |
356 | if( tv_sliders != null ) {
357 | for( let s of tv_sliders ) {
358 | s.remove();
359 | }
360 | tv_sliders = null;
361 | }
362 |
363 | let yy = 50;
364 | tv_sliders = [];
365 |
366 | for( let i = 0; i < params.length; ++i ) {
367 | let sl = p5c.createSlider( 0.0, 500.0, params[i] * 250.0 );
368 | sl.position( WIDTH/2 + 20, yy );
369 | sl.style( "width", "" + (WIDTH/2-100) + "px" );
370 | sl.input( parameterChanged );
371 | yy += 30;
372 | tv_sliders.push( sl );
373 | }
374 | }
375 |
376 | function parameterChanged()
377 | {
378 | if( tv_sliders != null ) {
379 | const params = tv_sliders.map( sl => sl.value() / 250.0 );
380 | tiling.setParameters( params );
381 | cacheTileShape();
382 | p5c.loop();
383 | }
384 | }
385 |
386 | function tilingTypeChanged()
387 | {
388 | the_type = p5c.int( ih_slider.value() );
389 | const tt = tilingTypes[ the_type ];
390 | const name = ((tt<10)?"IH0":"IH") + tt;
391 | ih_label.html( name );
392 |
393 | setTilingType();
394 |
395 | p5c.loop();
396 | }
397 |
398 | function spiralChanged()
399 | {
400 | spiral_A = p5c.int( A_slider.value() );
401 | spiral_B = p5c.int( B_slider.value() );
402 | calculateTilingTransform();
403 |
404 | A_label.html( "A: " + spiral_A );
405 | B_label.html( "B: " + spiral_B );
406 |
407 | p5c.loop();
408 | }
409 |
410 | function getTilingRect( t1, t2 )
411 | {
412 | const t1l = len( tiling.getT1() );
413 | const t2l = len( tiling.getT2() );
414 |
415 | const margin = Math.sqrt( t1l*t1l + t2l*t2l );
416 |
417 | const det = t1.x*t2.y - t2.x*t1.y;
418 |
419 | const pts = [
420 | { x: 0, y: 0 },
421 | (det < 0.0) ? t2 : t1,
422 | { x: t1.x + t2.x, y: t1.y + t2.y },
423 | (det < 0.0) ? t1 : t2 ];
424 |
425 | const v = normalize( sub( pts[1], pts[0] ) );
426 | const w = normalize( sub( pts[3], pts[0] ) );
427 |
428 | return [
429 | { x: pts[0].x+margin*(-v.x-w.x), y: pts[0].y+margin*(-v.y-w.y) },
430 | { x: pts[1].x+margin*(v.x-w.x), y: pts[1].y+margin*(v.y-w.y) },
431 | { x: pts[2].x+margin*(v.x+w.x), y: pts[2].y+margin*(v.y+w.y) },
432 | { x: pts[3].x+margin*(-v.x+w.x), y: pts[3].y+margin*(-v.y+w.y) } ];
433 | }
434 |
435 | function scaleVec( v, a )
436 | {
437 | return { x: v.x * a, y: v.y * a };
438 | }
439 |
440 | function drawTranslationalUnit()
441 | {
442 | if( fbo == null ) {
443 | fbo = p5c.createGraphics( FBO_DIM, FBO_DIM );
444 | }
445 |
446 | fbo.background( 255, 0, 0 );
447 |
448 | colouring = colour ? min_colouring : uniform_colouring;
449 |
450 | const r1 = Permutation.rank( colouring.p1 );
451 | const r2 = Permutation.rank( colouring.p2 );
452 |
453 | const t1 = scaleVec( tiling.getT1(), r1 );
454 | const t2 = scaleVec( tiling.getT2(), r2 );
455 |
456 | const det = (t1.x*t2.y - t2.x*t1.y);
457 | fbo_M = [ t2.y / det, -t1.y / det, -t2.x / det, t1.x / det ];
458 | const M = fbo_M;
459 |
460 | const est_sc = Math.sqrt( Math.abs( det / (r1 * r2) ) );
461 | // console.log( est_sc );
462 |
463 | fbo.push();
464 | fbo.applyMatrix( M[0], M[1], M[2], M[3], 0.0, 0.0 );
465 | const bx = getTilingRect( t1, t2 );
466 |
467 | for( let i of tiling.fillRegionQuad( bx[0], bx[1], bx[2], bx[3] ) ) {
468 | const TT = i.T;
469 | let tshape = [];
470 | for( let v of tile_shape ) {
471 | let P = mul( TT, v );
472 | P.x *= FBO_DIM;
473 | P.y *= FBO_DIM;
474 | tshape.push( P );
475 | }
476 |
477 | const col = colouring.getColour( i.t1, i.t2, i.aspect );
478 | fbo.fill( col[0], col[1], col[2] );
479 | fbo.stroke( col[0], col[1], col[2] );
480 | fbo.strokeWeight( est_sc );
481 |
482 | for( let idx = 0; idx < triangles.length; idx += 3 ) {
483 | fbo.triangle(
484 | tshape[triangles[idx]].x, tshape[triangles[idx]].y,
485 | tshape[triangles[idx+1]].x, tshape[triangles[idx+1]].y,
486 | tshape[triangles[idx+2]].x, tshape[triangles[idx+2]].y );
487 | }
488 |
489 | fbo.stroke( COLS[0][0], COLS[0][1], COLS[0][2] );
490 | fbo.strokeWeight( 20 * est_sc );
491 | fbo.strokeJoin( p5c.ROUND );
492 | fbo.noFill();
493 |
494 | for( let idx = 0; idx < tile_shape.length; ++idx ) {
495 | const P = tshape[idx];
496 | const Q = tshape[(idx+1)%tile_shape.length];
497 |
498 | fbo.line( P.x, P.y, Q.x, Q.y );
499 | }
500 | }
501 |
502 | fbo.pop();
503 |
504 | /*
505 | fbo.noFill();
506 | fbo.stroke( 255, 0, 0 );
507 | fbo.strokeWeight( 1 );
508 | fbo.rect( 0, 0, FBO_DIM, FBO_DIM );
509 | */
510 |
511 | calculateTilingTransform();
512 | }
513 |
514 | function calculateTilingTransform()
515 | {
516 | const t1 = tiling.getT1();
517 | const t2 = tiling.getT2();
518 | const pA = Permutation.pow( colouring.p1, spiral_A );
519 | const pB = Permutation.pow( colouring.p2, spiral_B );
520 | const rv = Permutation.rank( Permutation.mult( pA, pB ) );
521 |
522 | let v = {
523 | x: spiral_A * t1.x + spiral_B * t2.x,
524 | y: spiral_A * t1.y + spiral_B * t2.y };
525 |
526 | v = scaleVec( v, rv );
527 |
528 | tiling_T = mul(
529 | matchSeg( {x:0.0,y:0.0}, {x:0.0,y:p5c.TWO_PI} ),
530 | inv( matchSeg( {x:0.0,y:0.0}, v ) ) );
531 |
532 | tiling_T[2] = tiling_V.x;
533 | tiling_T[5] = tiling_V.y;
534 |
535 | tiling_iT = inv( tiling_T );
536 | }
537 |
538 | function drawSpiral()
539 | {
540 | p5c.noStroke();
541 | p5c.shader( shad1 );
542 |
543 | shad1.setUniform( "res", [WIDTH/2, HEIGHT/2] );
544 | shad1.setUniform( "tex", fbo );
545 | shad1.setUniform( "mob", do_mobius );
546 | shad1.setUniform( "fullscreen", fullscreen );
547 |
548 | const M = [fbo_M[0], fbo_M[2], 0.0, fbo_M[1], fbo_M[3], 0.0];
549 | const T = mul( M, tiling_iT );
550 | shad1.setUniform( "M", [T[0],T[3],0.0,T[1],T[4],0.0,T[2],T[5],1.0] );
551 |
552 | // rect(WIDTH/2,HEIGHT/2,WIDTH/2,HEIGHT/2);
553 | // It turns out that it basically doesn't matter what you put here,
554 | // as long as you send some geometry into the pipeline. Processing
555 | // will feed WebGL a rectangle from (0,0) to (1,1) with matching
556 | // texture coordinates, and the shader will see that in the context
557 | // of a viewport covering (-1,-1) to (1,1). So do all the real work
558 | // in the shader, I guess.
559 | p5c.rect(0, 1, 2, 3);
560 | }
561 |
562 | function drawTiling()
563 | {
564 | const M = fbo_M;
565 | const MM = [M[0], M[2], 0.0, M[1], M[3], 0.0];
566 |
567 | let nit = mul( MM, tiling_iT );
568 | const asp = WIDTH / HEIGHT;
569 |
570 | function vtex( sx, sy, px, py )
571 | {
572 | let P = mul( nit, { x: px, y: py } );
573 | p5c.vertex( sx, sy, 1, P.x, P.y );
574 | }
575 |
576 | p5c.texture( fbo );
577 |
578 | p5c.beginShape();
579 | vtex( 0, HEIGHT/2, 0, 0 );
580 | vtex( WIDTH/2, HEIGHT/2, p5c.TWO_PI * asp, 0 );
581 | vtex( WIDTH/2, HEIGHT, p5c.TWO_PI * asp, p5c.TWO_PI );
582 | vtex( 0, HEIGHT, 0, p5c.TWO_PI );
583 | p5c.endShape( p5c.CLOSE );
584 | }
585 |
586 | function calcEditorTransform()
587 | {
588 | let xmin = 1e7;
589 | let xmax = -1e7;
590 | let ymin = 1e7;
591 | let ymax = -1e7;
592 |
593 | for( let v of tile_shape ) {
594 | xmin = Math.min( xmin, v.x );
595 | xmax = Math.max( xmax, v.x );
596 | ymin = Math.min( ymin, v.y );
597 | ymax = Math.max( ymax, v.y );
598 | }
599 |
600 | const sc = Math.min(
601 | edit_box.w / (xmax-xmin), edit_box.h / (ymax-ymin) );
602 |
603 | editor_T = mul(
604 | [sc, 0, edit_box.x + 0.5*edit_box.w, 0,
605 | -sc, edit_box.y + 0.5*edit_box.h],
606 | [1, 0, -0.5*(xmin+xmax), 0, 1, -0.5*(ymin+ymax)] );
607 | }
608 |
609 | function distToSeg( P, A, B )
610 | {
611 | const qmp = sub( B, A );
612 | const t = dot( sub( P, A ), qmp ) / dot( qmp, qmp );
613 | if( (t >= 0.0) && (t <= 1.0) ) {
614 | return len( sub( P, { x: A.x + t*qmp.x, y : A.y + t*qmp.y } ) );
615 | } else if( t < 0.0 ) {
616 | return len( sub( P, A ) );
617 | } else {
618 | return len( sub( P, B ) );
619 | }
620 | }
621 |
622 | function drawEditor()
623 | {
624 | p5c.noStroke();
625 | p5c.fill( 252, 255, 254, 220 );
626 | p5c.rect( 0, 0, WIDTH/2, HEIGHT/2 );
627 |
628 | p5c.fill( COLS[3][0], COLS[3][1], COLS[3][2] );
629 |
630 | let tshape = [];
631 |
632 | for( let v of tile_shape ) {
633 | tshape.push( mul( editor_T, v ) );
634 | }
635 |
636 | for( let i = 0; i < triangles.length; i += 3 ) {
637 | p5c.triangle(
638 | tshape[triangles[i]].x, tshape[triangles[i]].y,
639 | tshape[triangles[i+1]].x, tshape[triangles[i+1]].y,
640 | tshape[triangles[i+2]].x, tshape[triangles[i+2]].y );
641 | }
642 |
643 | p5c.strokeWeight( 2.0 );
644 | p5c.noFill();
645 |
646 | // Draw edges
647 | for( let i of tiling.parts() ) {
648 | if( i.shape == EdgeShape.I ) {
649 | p5c.stroke( 158 );
650 | } else {
651 | p5c.stroke( 0 );
652 | }
653 |
654 | const M = mul( editor_T, i.T );
655 | let prev = null;
656 | for( let v of edges[i.id] ) {
657 | const P = mul( M, v );
658 | if( prev != null ) {
659 | p5c.line( prev.x, prev.y, P.x, P.y );
660 | }
661 | prev = P;
662 | }
663 | }
664 |
665 | // Draw tiling vertices
666 | p5c.noStroke();
667 | p5c.fill( 158 );
668 | for( let v of tiling.vertices() ) {
669 | const pt = mul( editor_T, v );
670 | p5c.ellipse( pt.x, pt.y, 10.0, 10.0 );
671 | }
672 |
673 | // Draw editable vertices
674 | for( let i of tiling.parts() ) {
675 | const shp = i.shape;
676 | const id = i.id;
677 | const ej = edges[id];
678 | const T = mul( editor_T, i.T );
679 |
680 | for( let idx = 1; idx < ej.length - 1; ++idx ) {
681 | p5c.fill( 0 );
682 | const pt = mul( T, ej[idx] );
683 | p5c.ellipse( pt.x, pt.y, 10.0, 10.0 );
684 | }
685 |
686 | if( shp == EdgeShape.I || shp == EdgeShape.J ) {
687 | continue;
688 | }
689 |
690 | // Draw symmetry points for U and S edges.
691 | if( !i.second ) {
692 | if( shp == EdgeShape.U ) {
693 | p5c.fill( COLS[2][0], COLS[2][1], COLS[2][2] );
694 | } else {
695 | p5c.fill( COLS[5][0], COLS[5][1], COLS[5][2] );
696 | }
697 | const pt = mul( T, ej[ej.length-1] );
698 | p5c.ellipse( pt.x, pt.y, 10.0, 10.0 );
699 | }
700 | }
701 | }
702 |
703 | function deleteVertex()
704 | {
705 | edges[drag_edge_shape].splice( drag_vertex, 1 );
706 | mode = MODE_NONE;
707 | cacheTileShape();
708 | p5c.loop();
709 | }
710 |
711 | function doTouchStarted( id )
712 | {
713 | for( let b of [help_button, fullscreen_button, colour_button, animate_button, save_button] ) {
714 | const pos = b.position();
715 | const sz = b.size();
716 | const r = makeBox( pos.x, pos.y, sz.width, sz.height );
717 | if( hitBox( p5c.mouseX, p5c.mouseY, r ) ) {
718 | return false;
719 | }
720 | }
721 |
722 | if( fullscreen || hitBox( p5c.mouseX, p5c.mouseY,
723 | makeBox( WIDTH/2, HEIGHT/2, WIDTH/2, HEIGHT/2 ) ) ) {
724 | do_mobius = !do_mobius;
725 | p5c.loop();
726 | } else if( hitBox( p5c.mouseX, p5c.mouseY,
727 | makeBox( 0, 0, WIDTH/2, HEIGHT/2 ) ) ) {
728 | const pt = { x: p5c.mouseX, y: p5c.mouseY };
729 |
730 | // Nothing yet. OK, try the geometric features of the tiling.
731 | for( let i of tiling.parts() ) {
732 | const shp = i.shape;
733 |
734 | // No interaction possible with an I edge.
735 | if( shp == EdgeShape.I ) {
736 | continue;
737 | }
738 |
739 | const id = i.id;
740 | let ej = edges[id];
741 | const T = mul( editor_T, i.T );
742 | let P = mul( T, ej[0] );
743 |
744 | for( let idx = 1; idx < ej.length; ++idx ) {
745 | let Q = mul( T, ej[idx] );
746 | if( ptdist( Q, pt ) < 0.5 * phys_unit ) {
747 | u_constrain = false;
748 | if( idx == (ej.length-1) ) {
749 | if( shp == EdgeShape.U && !i.second ) {
750 | u_constrain = true;
751 | } else {
752 | break;
753 | }
754 | }
755 |
756 | mode = MODE_MOVE_VERTEX;
757 | max_touches = 1;
758 | drag_edge_shape = id;
759 | drag_vertex = idx;
760 | drag_T = inv( T );
761 | down_motion = pt;
762 |
763 | // Set timer for deletion. But not on a U vertex.
764 | if( !u_constrain ) {
765 | delete_timer = setTimeout( deleteVertex, 1000 );
766 | }
767 |
768 | p5c.loop();
769 | return false;
770 | }
771 |
772 | // Check segment
773 | if( distToSeg( pt, P, Q ) < 20 ) {
774 | mode = MODE_MOVE_VERTEX;
775 | max_touches = 1;
776 | drag_edge_shape = id;
777 | drag_vertex = idx;
778 | drag_T = inv( T );
779 | down_motion = pt;
780 | // Don't set timer -- can't delete new vertex.
781 |
782 | ej.splice( idx, 0, mul( drag_T, pt ) );
783 | cacheTileShape();
784 | p5c.loop();
785 | return false;
786 | }
787 |
788 | P = Q;
789 | }
790 | }
791 |
792 | mode = MODE_ADJ_TILE;
793 | editor_T_down = editor_T;
794 | max_touches = 2;
795 | } else if( hitBox( p5c.mouseX, p5c.mouseY,
796 | makeBox( 0, HEIGHT/2, WIDTH/2, HEIGHT/2 ) ) ) {
797 | mode = MODE_ADJ_TILING;
798 | tiling_V_down = { x: tiling_V.x, y: tiling_V.y };
799 | max_touches = 1;
800 | }
801 |
802 | return false;
803 | }
804 |
805 | function getTouchRigid()
806 | {
807 | const ks = Object.keys( my_touches );
808 |
809 | if( num_touches == 1 ) {
810 | // Just translation.
811 | const tch = my_touches[ks[0]];
812 | const dx = tch.pos.x - tch.down.x;
813 | const dy = tch.pos.y - tch.down.y;
814 |
815 | return [ 1.0, 0.0, dx, 0.0, 1.0, dy ];
816 | } else {
817 | // Full rigid.
818 | const tch1 = my_touches[ks[0]];
819 | const tch2 = my_touches[ks[1]];
820 |
821 | const P1 = tch1.down;
822 | const P2 = tch1.pos;
823 | const Q1 = tch2.down;
824 | const Q2 = tch2.pos;
825 |
826 | const M1 = matchSeg( P1, Q1 );
827 | const M2 = matchSeg( P2, Q2 );
828 | const M = mul( M2, inv( M1 ) );
829 |
830 | return M;
831 | }
832 | }
833 |
834 | function doTouchMoved()
835 | {
836 | if( mode == MODE_ADJ_TILING ) {
837 | const M = getTouchRigid();
838 | const sc = p5c.TWO_PI / (HEIGHT/2);
839 | tiling_V.x = tiling_V_down.x + M[2] * sc;
840 | tiling_V.y = tiling_V_down.y + M[5] * sc;
841 | calculateTilingTransform();
842 |
843 | p5c.loop();
844 | return false;
845 | } else if( mode == MODE_ADJ_TILE ) {
846 | const M = getTouchRigid();
847 | editor_T = mul( M, editor_T_down );
848 | p5c.loop();
849 | return false;
850 | } else if( mode == MODE_MOVE_VERTEX ) {
851 | // const pt = { x: mouseX - edit_box.x, y: mouseY - edit_box.y };
852 | const pt = { x: p5c.mouseX, y: p5c.mouseY };
853 | const npt = mul( drag_T, pt );
854 |
855 | if( u_constrain ) {
856 | npt.x = 1.0;
857 | }
858 | const d = p5c.dist( pt.x, pt.y, down_motion.x, down_motion.y );
859 | if( d > 10 ) {
860 | // You've moved far enough, so don't delete.
861 | if( delete_timer ) {
862 | clearTimeout( delete_timer );
863 | delete_timer = null;
864 | }
865 | }
866 |
867 | edges[drag_edge_shape][drag_vertex] = npt;
868 | cacheTileShape();
869 | p5c.loop();
870 | return false;
871 | }
872 |
873 | return true;
874 | }
875 |
876 | function doTouchEnded( id )
877 | {
878 | if( delete_timer ) {
879 | clearTimeout( delete_timer );
880 | delete_timer = null;
881 | }
882 | mode = MODE_NONE;
883 | }
884 |
885 | p5c.preload = function()
886 | {
887 | fnt = p5c.loadFont( 'assets/helveticaneue.otf' );
888 | shad1 = p5c.loadShader( "assets/vert1.txt", "assets/frag1.txt" );
889 | }
890 |
891 | function setLabelStyle( lab )
892 | {
893 | lab.style( "font-family", "sans-serif" );
894 | lab.style( "font-size", "24px" );
895 | lab.style( "font-weight", "bold" );
896 | lab.style( "text-align", "center" );
897 | }
898 |
899 | function setupInterface()
900 | {
901 | const w = WIDTH;
902 | const h = HEIGHT;
903 |
904 | // Any way to fix this for different devices?
905 | phys_unit = 60;
906 |
907 | if( ih_slider == null ) {
908 | ih_slider = p5c.createSlider( 0, 80, 0, 1 );
909 | ih_slider.input( tilingTypeChanged );
910 | }
911 | ih_slider.position( WIDTH/2 + 20, 15 );
912 | ih_slider.style( "width", "" + (WIDTH/2-100) + "px" );
913 |
914 | if( ih_label == null ) {
915 | ih_label = p5c.createSpan( "IH01" );
916 | }
917 | ih_label.position( WIDTH - 70, 10 );
918 | setLabelStyle( ih_label );
919 |
920 | if( A_slider == null ) {
921 | A_slider = p5c.createSlider( 0, 20, 1, 1 );
922 | A_slider.input( spiralChanged );
923 | }
924 | A_slider.position( WIDTH/2 + 20, HEIGHT/2 - 80 );
925 | A_slider.style( "width", "" + (WIDTH/2-100) + "px" );
926 |
927 | if( A_label == null ) {
928 | A_label = p5c.createSpan( "A: 1" );
929 | }
930 | A_label.position( WIDTH - 70, HEIGHT/2 - 85 );
931 | setLabelStyle( A_label );
932 |
933 | if( B_slider == null ) {
934 | B_slider = p5c.createSlider( 0, 20, 5, 1 );
935 | B_slider.input( spiralChanged );
936 | }
937 | B_slider.position( WIDTH/2 + 20, HEIGHT/2 - 50 );
938 | B_slider.style( "width", "" + (WIDTH/2-100) + "px" );
939 |
940 | if( B_label == null ) {
941 | B_label = p5c.createSpan( "B: 5" );
942 | }
943 | B_label.position( WIDTH - 70, HEIGHT/2 - 55 );
944 | setLabelStyle( B_label );
945 |
946 | edit_box = makeBox( 150, 50, WIDTH/2-200, HEIGHT/2-100 );
947 |
948 | if( tv_sliders != null ) {
949 | let yy = 50;
950 | for( let sl of tv_sliders ) {
951 | sl.position( WIDTH/2 + 20, yy );
952 | sl.style( "width", "" + (WIDTH/2-100) + "px" );
953 | yy += 30;
954 | }
955 | }
956 |
957 | if( help_button == null ) {
958 | help_button = p5c.createButton( "Help!" );
959 | help_button.mousePressed( doHelp );
960 | }
961 | help_button.size( 90, 30 );
962 | help_button.position( 10, 130 );
963 |
964 | if( fullscreen_button == null ) {
965 | fullscreen_button = p5c.createButton( "Fullscreen" );
966 | fullscreen_button.mousePressed( toggleFullscreen );
967 | }
968 | fullscreen_button.size( 90, 30 );
969 | fullscreen_button.position( 10, 10 );
970 |
971 | if ( colour_button == null ) {
972 | colour_button = p5c.createButton( "Colour" );
973 | colour_button.mousePressed( toggleColour );
974 | }
975 | colour_button.size( 90, 30 );
976 | colour_button.position( 10, 50 );
977 |
978 | if( animate_button == null ) {
979 | animate_button = p5c.createButton( "Animate" );
980 | animate_button.mousePressed( toggleAnimation );
981 | }
982 | animate_button.size( 90, 30 );
983 | animate_button.position( 10, 90 );
984 |
985 | if( save_button == null ) {
986 | save_button = p5c.createButton( "Save" );
987 | save_button.mousePressed( doSave );
988 | }
989 | save_button.size( 90, 30 );
990 | save_button.position( 10, 170 );
991 | }
992 |
993 | function doSave()
994 | {
995 | const getSvgFile = ( s, svg ) =>
996 | s.serializeToString( svg ).split( '\n' );
997 |
998 | const svg = getSpiralTilingSVG();
999 | const svgFile = getSvgFile( new XMLSerializer(), svg );
1000 | p5c.save( svgFile, "spiral", "svg" );
1001 | }
1002 |
1003 | function getSpiralTilingSVG()
1004 | {
1005 | colouring = colour ? min_colouring : uniform_colouring;
1006 |
1007 | /*
1008 | const r1 = Permutation.rank( colouring.p1 );
1009 | const r2 = Permutation.rank( colouring.p2 );
1010 | */
1011 | const r1 = colouring.p1rank;
1012 | const r2 = colouring.p2rank;
1013 |
1014 | const t1 = tiling.getT1();
1015 | const t2 = tiling.getT2();
1016 |
1017 | let svgElement = document.createElementNS( XMLNS, 'svg' );
1018 | let g = document.createElementNS( XMLNS, 'g' );
1019 |
1020 | svgElement.setAttribute( 'xmlns:xlink', XLINK );
1021 | svgElement.setAttribute( 'height', HEIGHT );
1022 | svgElement.setAttribute( 'width', WIDTH );
1023 |
1024 | svgElement.appendChild( getSpiralUnitSVG() );
1025 |
1026 | // TODO(nikihasrati): Fix small tilings in centre.
1027 |
1028 | let i_i = spiral_A === 0 ? -spiral_B : -8 * r1 * spiral_A;
1029 | let i_f = spiral_A === 0 ? spiral_B : 5 * r1 * spiral_A;
1030 | let j_i = spiral_B === 0 ? -spiral_A : 1;
1031 | let j_f = spiral_B === 0 ? spiral_A : r2 * spiral_B;
1032 |
1033 | for ( let i = i_i; i <= i_f; i++ ) {
1034 | for ( let j = j_i; j <= j_f; j++ ) {
1035 | let unit = document.createElementNS( XMLNS, 'use' );
1036 | unit.setAttribute( 'xlink:href', '#spiral-unit' );
1037 |
1038 | let v = { x: i*t1.x + j*t2.x, y: i*t1.y + j*t2.y };
1039 | let vp = mul( tiling_T, v );
1040 | let s = Math.exp( vp.x );
1041 | let r = p5c.degrees( vp.y );
1042 |
1043 | unit.setAttribute( 'transform', `scale(${s} ${s}) rotate(${r})` );
1044 | g.appendChild( unit );
1045 | }
1046 | }
1047 |
1048 | const s = HEIGHT / p5c.TWO_PI;
1049 | const tx = WIDTH / 2;
1050 | const ty = HEIGHT / 2;
1051 | g.setAttribute( 'transform', `translate(${tx}, ${ty}) scale(${s}, ${s})` );
1052 | svgElement.appendChild( g );
1053 |
1054 | return svgElement;
1055 | }
1056 |
1057 | // Return an SVG definition of a spiral translation unit.
1058 | function getSpiralUnitSVG()
1059 | {
1060 | let defs = document.createElementNS( XMLNS, 'defs' );
1061 | let symbol = document.createElementNS( XMLNS, 'symbol' );
1062 | let g = document.createElementNS( XMLNS, 'g' );
1063 |
1064 | symbol.setAttribute('id', 'spiral-unit');
1065 | symbol.setAttribute('overflow', 'visible');
1066 |
1067 | for ( let i = 0; i < tiling.numAspects(); i++ ) {
1068 | let T = tiling.getAspectTransform( i );
1069 | let tile = getSpiralSVG( T );
1070 | // TODO(nikihasrati): Add colouring when colour is toggled on.
1071 | tile.setAttribute( 'fill', 'none' );
1072 | g.appendChild( tile );
1073 | }
1074 |
1075 | symbol.appendChild( g );
1076 | defs.appendChild( symbol );
1077 |
1078 | return defs;
1079 | }
1080 |
1081 | // Return an SVG path representing the spiral tiling aspect with transformation T.
1082 | function getSpiralSVG( T )
1083 | {
1084 | // Return the spiral coordinates of point v.
1085 | function spiral( v ) {
1086 | return {
1087 | x: +( Math.exp(v.x) * Math.cos(v.y) ),
1088 | y: +( Math.exp(v.x) * Math.sin(v.y) )}
1089 | }
1090 |
1091 | // Return the point that divides the line segment from v1 to v2 into ratio m:n.
1092 | function section( v1, v2, m, n ) {
1093 | return {
1094 | x: ( m*v1.x + n*v2.x ) / ( m + n ),
1095 | y: ( m*v1.y + n*v2.y ) / ( m + n )
1096 | }
1097 | }
1098 |
1099 | // Return sample points from tiling edge.
1100 | function sample_edge( v1, v2, n ) {
1101 | let pts = [];
1102 | for ( let i = 0; i <= n; i++ ) {
1103 | let p = spiral( section( v1, v2, n - i, i ) );
1104 | pts.push( [ p.x, p.y ] );
1105 | }
1106 | return pts;
1107 | }
1108 |
1109 | // Apply the aspect transformation to the prototile.
1110 | let vs = [ ...tile_shape, tile_shape[0] ];
1111 | vs = vs.map( v => mul( T, v ) );
1112 |
1113 | // Make bezier curves to represent each edge of the spiral tile.
1114 | let curves = [];
1115 | for (let i = 0; i < vs.length - 1; i++ ) {
1116 | let v1 = mul( tiling_T, vs[ i ] );
1117 | let v2 = mul( tiling_T, vs[ i+1 ] );
1118 | let edge_curves = sample_edge( v1, v2, 32 );
1119 | let bezierCurves = fitCurve( edge_curves, 50 );
1120 | curves.push(...bezierCurves);
1121 | }
1122 |
1123 | // Create SVG string representation of bezier curves.
1124 | let d = [`M ${curves[0][0][0]} ${curves[0][0][1]}`];
1125 | for ( let c of curves ) {
1126 | d.push(`C ${c[1][0]} ${c[1][1]}, ${c[2][0]} ${c[2][1]}, ${c[3][0]} ${c[3][1]}`)
1127 | }
1128 |
1129 | let path = document.createElementNS( XMLNS, 'path' );
1130 | path.setAttribute('d', d.join(' '));
1131 | path.setAttribute('stroke', 'black');
1132 | path.setAttribute('fill', 'none');
1133 | path.setAttribute('vector-effect', 'non-scaling-stroke');
1134 |
1135 | return path;
1136 | }
1137 |
1138 | function doHelp()
1139 | {
1140 | window.open( "https://isohedral.ca" );
1141 | }
1142 |
1143 | function toggleFullscreen()
1144 | {
1145 | fullscreen = !fullscreen;
1146 | let elts = [
1147 | ih_slider, ih_label, A_slider, A_label, B_slider, B_label,
1148 | help_button, fullscreen_button, colour_button, animate_button, save_button ].concat(
1149 | tv_sliders );
1150 |
1151 | for( let elt of elts ) {
1152 | if( elt != null ) {
1153 | if( fullscreen ) {
1154 | elt.hide();
1155 | } else {
1156 | elt.show();
1157 | }
1158 | }
1159 | }
1160 |
1161 | fullscreen_button.show();
1162 | if( fullscreen ) {
1163 | fullscreen_button.html( "Fullscreen Off" );
1164 | } else {
1165 | fullscreen_button.html( "Fullscreen" );
1166 | }
1167 |
1168 | p5c.loop();
1169 | return false;
1170 | }
1171 |
1172 | function toggleColour()
1173 | {
1174 | colour = !colour;
1175 |
1176 | drawTranslationalUnit();
1177 | p5c.loop();
1178 | }
1179 |
1180 | function toggleAnimation()
1181 | {
1182 | animating = !animating;
1183 | if( animating ) {
1184 | p5c.loop();
1185 | }
1186 | }
1187 |
1188 | p5c.setup = function()
1189 | {
1190 | WIDTH = window.innerWidth;
1191 | HEIGHT = window.innerHeight;
1192 |
1193 | let canvas = p5c.createCanvas( WIDTH, HEIGHT, p5c.WEBGL );
1194 | canvas.parent( "sktch" );
1195 |
1196 | p5c.textFont( fnt );
1197 |
1198 | const asp = WIDTH / HEIGHT;
1199 | const hh = 6.0;
1200 | const ww = asp * hh;
1201 | const sc = HEIGHT / (2*hh);
1202 |
1203 | setupInterface();
1204 |
1205 | the_type = 0;
1206 | const tp = tilingTypes[ the_type ];
1207 | tiling = new IsohedralTiling( tp );
1208 |
1209 | setTilingType();
1210 |
1211 | p5c.textureWrap( p5c.REPEAT );
1212 | p5c.textureMode( p5c.NORMAL );
1213 | }
1214 |
1215 | p5c.windowResized = function()
1216 | {
1217 | WIDTH = window.innerWidth;
1218 | HEIGHT = window.innerHeight;
1219 |
1220 | p5c.resizeCanvas( WIDTH, HEIGHT );
1221 | setupInterface();
1222 | calculateTilingTransform();
1223 | p5c.loop();
1224 | }
1225 |
1226 | p5c.draw = function()
1227 | {
1228 | p5c.background( 255 );
1229 | p5c.push();
1230 | p5c.translate( -p5c.width/2, -p5c.height/2 );
1231 |
1232 | if( animating ) {
1233 | const t1 = mul( tiling_T, tiling.getT1() );
1234 | t1.x -= tiling_T[2];
1235 | t1.y -= tiling_T[5];
1236 | const l = len( t1 );
1237 | calculateTilingTransform();
1238 | tiling_V.x -= 0.01 * t1.x / l;
1239 | tiling_V.y -= 0.01 * t1.y / l;
1240 | }
1241 |
1242 | // FIXME -- why doesn't this work if the spiral comes after the tiling?
1243 | if( !fullscreen ) {
1244 | drawEditor();
1245 | }
1246 |
1247 | drawSpiral();
1248 |
1249 | if( !fullscreen ) {
1250 | drawTiling();
1251 | }
1252 | p5c.pop();
1253 |
1254 | /*
1255 | fill( 255 );
1256 | noStroke();
1257 | textSize( 24 );
1258 | textAlign( LEFT );
1259 | let c = 0;
1260 | c += 32;
1261 | for( let i = Math.max( 0, msgs.length - 10 ); i < msgs.length; ++i ) {
1262 | text( msgs[i], 25, 200+c );
1263 | c = c + 32;
1264 | }
1265 | */
1266 |
1267 | if( !animating ) {
1268 | p5c.noLoop();
1269 | }
1270 | }
1271 | };
1272 |
1273 | let myp5 = new p5( sktch, 'sketch0' );
1274 |
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | The final library is constructed simply by concatenating the three
2 | files found here, in the order
3 |
4 | * `preamble.inc`
5 | * `tiling_data.inc`
6 | * `tactile.inc`
7 |
8 | The file `tiling_data.inc` is generated automatically by a separate
9 | program (not included here). This directory can safely be ignored by just about everyone—the main file to use is in the `lib/` folder.
10 |
--------------------------------------------------------------------------------
/src/preamble.inc:
--------------------------------------------------------------------------------
1 | /*
2 | * Tactile-JS
3 | * Copyright 2018 Craig S. Kaplan, csk@uwaterloo.ca
4 | *
5 | * Distributed under the terms of the 3-clause BSD license. See the
6 | * file "LICENSE" for more information.
7 | */
8 |
9 | 'use strict'
10 |
11 | const EdgeShape = {
12 | J : 10001,
13 | U : 10002,
14 | S : 10003,
15 | I : 10004,
16 | };
17 |
18 | const numTypes = 81;
19 |
20 | function mul( A, B )
21 | {
22 | if( B.hasOwnProperty( 'x' ) ) {
23 | // Matrix * Point
24 | return {
25 | x : A[0]*B.x + A[1]*B.y + A[2],
26 | y : A[3]*B.x + A[4]*B.y + A[5] };
27 | } else {
28 | // Matrix * Matrix
29 | return [A[0]*B[0] + A[1]*B[3],
30 | A[0]*B[1] + A[1]*B[4],
31 | A[0]*B[2] + A[1]*B[5] + A[2],
32 |
33 | A[3]*B[0] + A[4]*B[3],
34 | A[3]*B[1] + A[4]*B[4],
35 | A[3]*B[2] + A[4]*B[5] + A[5]];
36 | }
37 | };
38 |
39 | function matchSeg( p, q )
40 | {
41 | return [q.x-p.x, p.y-q.y, p.x, q.y-p.y, q.x-p.x, p.y];
42 | };
43 |
--------------------------------------------------------------------------------
/src/tactile.inc:
--------------------------------------------------------------------------------
1 | function makePoint( coeffs, offs, params )
2 | {
3 | let ret = { x : 0.0, y : 0.0 }
4 |
5 | for( let i = 0; i < params.length; ++i ) {
6 | ret.x += coeffs[offs+i] * params[i];
7 | ret.y += coeffs[offs+params.length+i] * params[i];
8 | }
9 |
10 | return ret;
11 | };
12 |
13 | function makeMatrix( coeffs, offs, params )
14 | {
15 | let ret = []
16 |
17 | for( let row = 0; row < 2; ++row ) {
18 | for( let col = 0; col < 3; ++col ) {
19 | let val = 0.0;
20 | for( let idx = 0; idx < params.length; ++idx ) {
21 | val += coeffs[offs+idx] * params[idx];
22 | }
23 | ret.push( val );
24 | offs += params.length;
25 | }
26 | }
27 |
28 | return ret;
29 | };
30 |
31 | const M_orients = [
32 | [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], // IDENTITY
33 | [-1.0, 0.0, 1.0, 0.0, -1.0, 0.0], // ROT
34 | [-1.0, 0.0, 1.0, 0.0, 1.0, 0.0], // FLIP
35 | [1.0, 0.0, 0.0, 0.0, -1.0, 0.0] // ROFL
36 | ];
37 |
38 | const TSPI_U = [
39 | [0.5, 0.0, 0.0, 0.0, 0.5, 0.0],
40 | [-0.5, 0.0, 1.0, 0.0, 0.5, 0.0]
41 | ];
42 |
43 | const TSPI_S = [
44 | [0.5, 0.0, 0.0, 0.0, 0.5, 0.0],
45 | [-0.5, 0.0, 1.0, 0.0, -0.5, 0.0]
46 | ];
47 |
48 | class IsohedralTiling
49 | {
50 | constructor( tp )
51 | {
52 | this.reset( tp );
53 | }
54 |
55 | reset( tp )
56 | {
57 | this.tiling_type = tp;
58 | this.ttd = tiling_type_data[ tp ];
59 | this.parameters = this.ttd.default_params.slice( 0 );
60 | this.parameters.push( 1.0 );
61 | this.recompute();
62 | }
63 |
64 | recompute()
65 | {
66 | const ntv = this.numVertices();
67 | const np = this.numParameters();
68 | const na = this.numAspects();
69 |
70 | // Recompute tiling vertex locations.
71 | this.verts = [];
72 | for( let idx = 0; idx < ntv; ++idx ) {
73 | this.verts.push( makePoint( this.ttd.vertex_coeffs,
74 | idx * (2 * (np + 1)), this.parameters ) );
75 | }
76 |
77 | // Recompute edge transforms and reversals from orientation information.
78 | this.reversals = [];
79 | this.edges = []
80 | for( let idx = 0; idx < ntv; ++idx ) {
81 | const fl = this.ttd.edge_orientations[2*idx];
82 | const ro = this.ttd.edge_orientations[2*idx+1];
83 | this.reversals.push( fl != ro );
84 | this.edges.push(
85 | mul( matchSeg( this.verts[idx], this.verts[(idx+1)%ntv] ),
86 | M_orients[2*fl+ro] ) );
87 | }
88 |
89 | // Recompute aspect xforms.
90 | this.aspects = []
91 | for( let idx = 0; idx < na; ++idx ) {
92 | this.aspects.push(
93 | makeMatrix( this.ttd.aspect_coeffs, 6*(np+1)*idx,
94 | this.parameters ) );
95 | }
96 |
97 | // Recompute translation vectors.
98 | this.t1 = makePoint(
99 | this.ttd.translation_coeffs, 0, this.parameters );
100 | this.t2 = makePoint(
101 | this.ttd.translation_coeffs, 2*(np+1), this.parameters );
102 | }
103 |
104 | getTilingType()
105 | {
106 | return this.tiling_type;
107 | }
108 |
109 | numParameters()
110 | {
111 | return this.ttd.num_params;
112 | }
113 |
114 | setParameters( arr )
115 | {
116 | if( arr.length == (this.parameters.length-1) ) {
117 | this.parameters = arr.slice( 0 );
118 | this.parameters.push( 1.0 );
119 | this.recompute();
120 | }
121 | }
122 |
123 | getParameters()
124 | {
125 | return this.parameters.slice( 0, -1 );
126 | }
127 |
128 | numEdgeShapes()
129 | {
130 | return this.ttd.num_edge_shapes;
131 | }
132 |
133 | getEdgeShape( idx )
134 | {
135 | return this.ttd.edge_shapes[ idx ];
136 | }
137 |
138 | * shape()
139 | {
140 | for( let idx = 0; idx < this.numVertices(); ++idx ) {
141 | const an_id = this.ttd.edge_shape_ids[idx];
142 |
143 | yield {
144 | T : this.edges[idx],
145 | id : an_id,
146 | shape : this.ttd.edge_shapes[ an_id ],
147 | rev : this.reversals[ idx ]
148 | };
149 | }
150 | }
151 |
152 | * parts()
153 | {
154 | for( let idx = 0; idx < this.numVertices(); ++idx ) {
155 | const an_id = this.ttd.edge_shape_ids[idx];
156 | const shp = this.ttd.edge_shapes[an_id];
157 |
158 | if( (shp == EdgeShape.J) || (shp == EdgeShape.I) ) {
159 | yield {
160 | T : this.edges[idx],
161 | id : an_id,
162 | shape : shp,
163 | rev : this.reversals[ idx ],
164 | second : false
165 | };
166 | } else {
167 | const indices = this.reversals[idx] ? [1,0] : [0,1];
168 | const Ms = (shp == EdgeShape.U) ? TSPI_U : TSPI_S;
169 |
170 | yield {
171 | T : mul( this.edges[idx], Ms[indices[0]] ),
172 | id : an_id,
173 | shape : shp,
174 | rev : false,
175 | second : false
176 | };
177 |
178 | yield {
179 | T : mul( this.edges[idx], Ms[indices[1]] ),
180 | id : an_id,
181 | shape : shp,
182 | rev : true,
183 | second : true
184 | };
185 | }
186 | }
187 | }
188 |
189 | numVertices()
190 | {
191 | return this.ttd.num_vertices;
192 | }
193 |
194 | getVertex( idx )
195 | {
196 | return { ...this.verts[ idx ] };
197 | }
198 |
199 | vertices()
200 | {
201 | return this.verts.map( v => ({ ...v }) );
202 | }
203 |
204 | numAspects()
205 | {
206 | return this.ttd.num_aspects;
207 | }
208 |
209 | getAspectTransform( idx )
210 | {
211 | return [ ...this.aspects[ idx ] ];
212 | }
213 |
214 | getT1()
215 | {
216 | return { ...this.t1 };
217 | }
218 |
219 | getT2()
220 | {
221 | return { ...this.t2 };
222 | }
223 |
224 | * fillRegionBounds( xmin, ymin, xmax, ymax )
225 | {
226 | yield* this.fillRegionQuad(
227 | { x : xmin, y : ymin },
228 | { x : xmax, y : ymin },
229 | { x : xmax, y : ymax },
230 | { x : xmin, y : ymax } );
231 | }
232 |
233 | * fillRegionQuad( A, B, C, D )
234 | {
235 | const t1 = this.getT1();
236 | const t2 = this.getT2();
237 | const ttd = this.ttd;
238 | const aspects = this.aspects;
239 |
240 | let last_y;
241 |
242 | function bc( M, p ) {
243 | return {
244 | x : M[0]*p.x + M[1]*p.y,
245 | y : M[2]*p.x + M[3]*p.y };
246 | };
247 |
248 | function sampleAtHeight( P, Q, y )
249 | {
250 | const t = (y-P.y)/(Q.y-P.y);
251 | return { x : (1.0-t)*P.x + t*Q.x, y : y };
252 | }
253 |
254 | function* doFill( A, B, C, D, do_top )
255 | {
256 | let x1 = A.x;
257 | const dx1 = (D.x-A.x)/(D.y-A.y);
258 | let x2 = B.x;
259 | const dx2 = (C.x-B.x)/(C.y-B.y);
260 | const ymin = A.y;
261 | let ymax = C.y;
262 |
263 | if( do_top ) {
264 | ymax = ymax + 1.0;
265 | }
266 |
267 | let y = Math.floor( ymin );
268 | if( last_y ) {
269 | y = Math.max( last_y, y );
270 | }
271 |
272 | while( y < ymax ) {
273 | const yi = Math.trunc( y );
274 | let x = Math.floor( x1 );
275 | while( x < (x2 + 1e-7) ) {
276 | const xi = Math.trunc( x );
277 |
278 | for( let asp = 0; asp < ttd.num_aspects; ++asp ) {
279 | let M = aspects[ asp ].slice( 0 );
280 | M[2] += xi*t1.x + yi*t2.x;
281 | M[5] += xi*t1.y + yi*t2.y;
282 |
283 | yield {
284 | T : M,
285 | t1 : xi,
286 | t2 : yi,
287 | aspect : asp
288 | };
289 | }
290 |
291 | x += 1.0;
292 | }
293 | x1 += dx1;
294 | x2 += dx2;
295 | y += 1.0;
296 | }
297 |
298 | last_y = y;
299 | }
300 |
301 | function* fillFixX( A, B, C, D, do_top )
302 | {
303 | if( A.x > B.x ) {
304 | yield* doFill( B, A, D, C, do_top );
305 | } else {
306 | yield* doFill( A, B, C, D, do_top );
307 | }
308 | }
309 |
310 | function* fillFixY( A, B, C, D, do_top )
311 | {
312 | if( A.y > C.y ) {
313 | yield* doFill( C, D, A, B, do_top );
314 | } else {
315 | yield* doFill( A, B, C, D, do_top );
316 | }
317 | }
318 |
319 | const det = 1.0 / (t1.x*t2.y-t2.x*t1.y);
320 | const Mbc = [ t2.y * det, -t2.x * det, -t1.y * det, t1.x * det ];
321 |
322 | let pts = [ bc( Mbc, A ), bc( Mbc, B ), bc( Mbc, C ), bc( Mbc, D ) ];
323 |
324 | if( det < 0.0 ) {
325 | let tmp = pts[1];
326 | pts[1] = pts[3];
327 | pts[3] = tmp;
328 | }
329 |
330 | if( Math.abs( pts[0].y - pts[1].y ) < 1e-7 ) {
331 | yield* fillFixY( pts[0], pts[1], pts[2], pts[3], true );
332 | } else if( Math.abs( pts[1].y - pts[2].y ) < 1e-7 ) {
333 | yield* fillFixY( pts[1], pts[2], pts[3], pts[0], true );
334 | } else {
335 | let lowest = 0;
336 | for( let idx = 1; idx < 4; ++idx ) {
337 | if( pts[idx].y < pts[lowest].y ) {
338 | lowest = idx;
339 | }
340 | }
341 |
342 | let bottom = pts[lowest];
343 | let left = pts[(lowest+1)%4];
344 | let top = pts[(lowest+2)%4];
345 | let right = pts[(lowest+3)%4];
346 |
347 | if( left.x > right.x ) {
348 | let tmp = left;
349 | left = right;
350 | right = tmp;
351 | }
352 |
353 | if( left.y < right.y ) {
354 | const r1 = sampleAtHeight( bottom, right, left.y );
355 | const l2 = sampleAtHeight( left, top, right.y );
356 | yield* fillFixX( bottom, bottom, r1, left, false );
357 | yield* fillFixX( left, r1, right, l2, false );
358 | yield* fillFixX( l2, right, top, top, true );
359 | } else {
360 | const l1 = sampleAtHeight( bottom, left, right.y );
361 | const r2 = sampleAtHeight( right, top, left.y );
362 | yield* fillFixX( bottom, bottom, right, l1, false );
363 | yield* fillFixX( l1, right, r2, left, false );
364 | yield* fillFixX( left, r2, top, top, true );
365 | }
366 | }
367 | }
368 |
369 | getColour( a, b, asp )
370 | {
371 | const clrg = this.ttd.colouring;
372 | const nc = clrg[18];
373 |
374 | let mt1 = a % nc;
375 | if( mt1 < 0 ) {
376 | mt1 += nc;
377 | }
378 | let mt2 = b % nc;
379 | if( mt2 < 0 ) {
380 | mt2 += nc;
381 | }
382 | let col = clrg[asp];
383 |
384 | for( let idx = 0; idx < mt1; ++idx ) {
385 | col = clrg[12+col];
386 | }
387 | for( let idx = 0; idx < mt2; ++idx ) {
388 | col = clrg[15+col];
389 | }
390 |
391 | return col;
392 | }
393 | };
394 |
395 | export
396 | {
397 | EdgeShape,
398 |
399 | numTypes,
400 | tilingTypes,
401 |
402 | makePoint,
403 | makeMatrix,
404 | mul,
405 | matchSeg,
406 |
407 | IsohedralTiling
408 | };
409 |
--------------------------------------------------------------------------------