├── 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 | 12 | 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 | --------------------------------------------------------------------------------