├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── _config.yml ├── constrain-graph.js ├── constrain-mathjax.js ├── constrain-pdf.js ├── constrain-ps.js ├── constrain-reveal.js ├── constrain-slide.css ├── constrain-trees.js ├── constrain.js ├── doc ├── SRC-RR-131A.pdf └── index.html ├── examples ├── andru.css ├── audio │ ├── mixkit-ethereal-fairy-win-sound-2019.wav │ ├── slide.m4a │ └── ssh.m4a ├── constrain-demo.html ├── dragon.html ├── dragon.js ├── dragon.png ├── example-figure.js ├── fig2c.html ├── gate.html ├── ll_lr.html ├── loyd.html ├── pythagoras.html ├── reveal-demo.html ├── spiral.html ├── spiral.js ├── talk.html ├── template.html ├── text-format.html └── triangles.html ├── favicon.ico ├── fonts └── LinLibertine_R.js ├── images ├── dragon-thumbnail.png ├── loyd-thumbnail.png ├── pythagoras-thumbnail.png ├── spiral-thumbnail.png ├── tex-thumbnail.png ├── trees-thumbnail.png ├── triangle.png └── triangles-thumbnail.png ├── numeric-1.2.6.js └── tests ├── animation.html ├── constrain-test.html ├── direction.html ├── ellipse_text.html ├── graph-test1.html ├── graph-test2.html ├── graph-test3.html ├── hinttest.html ├── hvlines.html ├── insn_order2.html ├── label-text.html ├── linelabels.html ├── mathjax.html ├── polygon_test.html ├── printtest.html ├── safari-test.html ├── sqtest.html ├── text-format.html ├── text-layout.html ├── treefig.html ├── treefig2.html ├── treefig3.html └── triangles-broken.html /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | misc 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "reveal.js"] 2 | path = reveal.js 3 | url = https://github.com/andrewcmyers/reveal.js.git 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrew Myers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Constrain](https://andrewcmyers.github.io/constrain/) - a JS (ES6) library for animated, interactive web figures, based on declarative constraint solving 2 | ![Triangle image](images/triangle.png) 3 | - Responsive, animated figures embedded in web pages 4 | - Figures implemented declaratively with time-dependent constraints on graphical objects 5 | - Integrates with [Reveal.js](https://revealjs.com) presentations 6 | - [GitHub repository](https://github.com/andrewcmyers/constrain) 7 | - [Reference manual](https://andrewcmyers.github.io/constrain/doc) 8 | - [A short talk about Constrain](https://www.youtube.com/watch?v=UN_HOWSijNI) ([HTML/Reveal source](https://andrewcmyers.github.io/constrain/examples/talk.html)) 9 | 10 | ## Demos 11 | 12 | Pythagoras thumbnail [Interactive Pythagorean Theorem](https://andrewcmyers.github.io/constrain/examples/pythagoras.html) 13 | 14 | Triangles thumbnail [Interactively computing centers of a triangle](https://andrewcmyers.github.io/constrain/examples/triangles.html) 15 | 16 | Trees thumbnail [Animated trees](https://andrewcmyers.github.io/constrain/examples/ll_lr.html) 17 | 18 | Loyd thumbnail [Loyd 15-puzzle](https://andrewcmyers.github.io/constrain/examples/loyd.html) 19 | 20 | Spiral thumbnail [Using constraints to compute the Golden Ratio](https://andrewcmyers.github.io/constrain/examples/spiral.html) (Drag the diamond!) 21 | 22 | Dragon thumbnail [Dragon curve](https://andrewcmyers.github.io/constrain/examples/dragon.html) 23 | 24 | TeX thumbnail [TeX-style text formatting](https://andrewcmyers.github.io/constrain/examples/text-format.html) 25 | 26 | Cornell University course notes using Constrain for embedded figures: [CS 2112](https://www.cs.cornell.edu/courses/cs2112/2019fa/lectures/lecture.html?id=objects), 27 | [CS 4120/lexer generation](https://www.cs.cornell.edu/courses/cs4120/2023sp/notes.html?id=leximpl), 28 | [CS 4120/bottom-up parsing](https://www.cs.cornell.edu/courses/cs4120/2023sp/notes.html?id=bottomup) 29 | 30 | [Simple template page](https://andrewcmyers.github.io/constrain/examples/template.html) 31 | 32 | ## Requirements 33 | 34 | - ES6-capable web browser 35 | - Tested on Chrome, Opera, Brave, Firefox, Safari, Edge (runs best on the first three) 36 | - Does not work on Internet Explorer or Opera Mini 37 | - Numeric.js version 1.2.6 (included) 38 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | favicon: /favicon.ico 2 | -------------------------------------------------------------------------------- /constrain-graph.js: -------------------------------------------------------------------------------- 1 | Constrain.Graph = function() { 2 | 3 | const { 4 | Loss, Minus, CanvasRect, Min, Max, Times, Distance, Plus, Divide, Sqrt, 5 | Conditional, LayoutObject, Variable, evaluate, Expression, exprVariables, 6 | Global, DebugExpr, SolverCallback, Average 7 | } = Constrain 8 | 9 | // How strongly graph constraints are enforced, by default. << 1 because these are supposed to 10 | // be soft constraints, i.e., regular constraints will "give" very little to accommodate them 11 | const GRAPH_COST = 0.01 12 | 13 | // How densely laid out nodes in a graph are, relative to their size, by default. 14 | const GRAPH_SPARSITY = 1 15 | 16 | // gravity force on directed edges 17 | const GRAPH_GRAVITY = 40 18 | 19 | // 1/r^2 force pushing all nodes apart from each other 20 | const GRAPH_REPULSION = 1000 21 | 22 | // Torsional force spreading edges apart 23 | const GRAPH_BRANCH_SPREAD = 1 24 | 25 | // How much it costs to use fully squeezed dimensions 26 | const LARGE_DIM_COST = 1000000 27 | 28 | // cost of partially squeezed dimensions 29 | const DIM_COST = 100 30 | 31 | // Distance between nodes below which the repulsion force is clamped 32 | const REPULSION_CLAMP_DIST = 0.001 33 | 34 | // A NodePos computes a higher-dimensional position in which 35 | // the first two coordinates are the (x,y) position of the object 36 | // and the remaining coordinates are the "extra dimensions" specified 37 | // by the graph. 38 | class NodePos extends LayoutObject { 39 | constructor(object, graph) { 40 | super() 41 | this.obj = object 42 | this.graph = graph 43 | const n = this.graph.numExtraDims + 2 44 | if (n > 2) { 45 | this.obj.extraDims = new Array(n-2) 46 | for (let i = 2; i < n; i++) { 47 | this.obj.extraDims[i-2] = new Variable(graph.figure, "np" + i) 48 | this.obj.extraDims[i-2].hint = Math.random() - 0.5 49 | } 50 | } 51 | } 52 | toString() { return "NodePos(" + this.obj + ")" } 53 | object() { 54 | return this.obj 55 | } 56 | x() { 57 | return this.obj.x() 58 | } 59 | y() { 60 | return this.obj.y() 61 | } 62 | w() { 63 | return this.obj.w() 64 | } 65 | h() { 66 | return this.obj.h() 67 | } 68 | evaluate(valuation, doGrad) { 69 | let result = evaluate(this.obj, valuation, doGrad) 70 | const n = this.graph.numExtraDims + 2 71 | if (n == 2) return result 72 | result = result.slice(0) 73 | for (let i = 2; i < n; i++) { 74 | const extra = evaluate(this.obj.extraDims[i-2], valuation, doGrad) 75 | if (doGrad) { 76 | result[0].push(extra[0]) 77 | result[1].push(extra[1]) 78 | } else { 79 | result.push(extra) 80 | } 81 | } 82 | return result 83 | } 84 | initDiff() { 85 | this.bpDiff = new Array(this.graph.numExtraDims + 2).fill(0) 86 | } 87 | backprop(task) { 88 | const d = this.bpDiff, 89 | n = 2 + this.graph.numExtraDims 90 | if (d.length != n) { 91 | console.error("wrong number of dimensions being propagated through NodePos") 92 | } 93 | task.propagate(this.obj, d.slice(0, 2)) 94 | for (let i = 2; i < n; i++) { 95 | task.propagate(this.obj.extraDims[i-2], d[i]) 96 | } 97 | } 98 | addDependencies(task) { 99 | task.prepareBackProp(this.obj) 100 | for (let i = 0; i < this.graph.numExtraDims; i++) { 101 | task.prepareBackProp(this.obj.extraDims[i]) 102 | } 103 | } 104 | variables() { 105 | let result = this.obj.variables() 106 | for (let i = 0; i < this.graph.numExtraDims; i++) { 107 | const vs = exprVariables(this.obj.extraDims[i]) 108 | if (vs.length > 0) result = result.concat(vs) 109 | } 110 | return result 111 | } 112 | toString() { 113 | return "NodePos(" + this.obj + ")" 114 | } 115 | } 116 | 117 | var graphIndex = 0 118 | 119 | class Graph { 120 | constructor(figure) { 121 | this.figure = figure 122 | this.sparsity = GRAPH_SPARSITY 123 | this.cost = GRAPH_COST 124 | this.gravity = GRAPH_GRAVITY 125 | this.repulsion = GRAPH_REPULSION 126 | this.branchSpread = GRAPH_BRANCH_SPREAD 127 | this.horizontalLayout = false 128 | this.hintsComputed = false 129 | this.nodes = [] 130 | this.edges = [] 131 | this.numExtraDims = 0 132 | this.setEffectiveDimension(() => this.numExtraDims+2) 133 | figure.registerCallback(new SolverCallback("graph" + graphIndex, 134 | (it, x0, f0, g0, H1) => { 135 | const d = 2 + (1.0 - it/1000.0) * this.numExtraDims 136 | // console.log("graph callback seen iter " + it + " value " + f0 + " dim=" + d); 137 | this.setEffectiveDimension(() => d) 138 | return false 139 | })) 140 | } 141 | setExtraDims(d) { 142 | this.numExtraDims = d 143 | return this 144 | } 145 | // Define the effective dimensionality of points as a function f 146 | // that returns the effective dimensionality when queried. This 147 | // can be any real number at or above 2. Position coordinates 148 | // whose dimension is at least 1 full dimension too large 149 | // incur a cost multiplier of LARGE_DIM_COST. Dimensions 150 | // that are too large by x incur a cost multiplier of x/(1-x) 151 | // 152 | setEffectiveDimension(f) { 153 | this.effectiveDimensionFunction = f 154 | } 155 | // The NodePos associated with graphical object g. One is created if 156 | // none exists yet in this graph. 157 | addNode(...objs) { 158 | if (objs.length > 1) { 159 | objs.forEach(o => this.addNode(o)) 160 | return 161 | } 162 | let g = objs[0] 163 | const fig = this.figure 164 | for (let i = 0; i < this.nodes.length; i++) { 165 | if (g === this.nodes[i].object()) { 166 | return this.nodes[i] 167 | } 168 | } 169 | g = new NodePos(g, this) 170 | // nodes would like to be far apart 171 | const cr = fig.canvasRect(), 172 | sz = fig.min(cr.w(), cr.h()), 173 | n = this.numExtraDims + 2 174 | for (let i = 0; i < this.nodes.length; i++) { 175 | let g2 = this.nodes[i], 176 | dist = fig.distance(g2, g, n), 177 | // dist = fig.sq(fig.minus(g2, g)), 178 | clamped_dist = new Conditional(new Minus(dist, REPULSION_CLAMP_DIST), 179 | dist, 180 | new Average(dist, REPULSION_CLAMP_DIST)), 181 | bdist = new Min(clamped_dist, cr.w(), cr.h()), 182 | // repulsion cuts off at canvas size 183 | potential = fig.divide(this.repulsion * this.sparsity, 184 | clamped_dist 185 | // new DebugExpr("bdist(" + g + "," + g2 +")", bdist) 186 | ) 187 | // potential = new DebugExpr("potential between " + g + " and " + g2, potential) 188 | fig.costEqual(this.cost, potential, 0) 189 | } 190 | if (this.effectiveDimensionFunction) { 191 | // Add the "squeeze" loss to keep nodes inside the effective dimensionality 192 | let dimension = new Global(v => (this.effectiveDimensionFunction)(v), "dimension") 193 | for (let d = 2; d < n; d++) { 194 | const x = fig.minus(d, dimension), 195 | x2 = fig.minus(1, x); 196 | new Loss(fig, new Conditional(x, 197 | fig.times(fig.sq(fig.projection(g, d, n)), 198 | new Conditional(x2, 199 | fig.times(DIM_COST, fig.divide(x, x2)), 200 | LARGE_DIM_COST)), 201 | 0)) 202 | } 203 | } 204 | this.nodes.push(g) 205 | // but keep the node inside the figure 206 | fig.keepInside(g, cr) 207 | return g 208 | } 209 | // Add an undirected edge between objects g1 and g2, adding the objects as nodes if necessary. 210 | // Return the (straight) connector between them. 211 | edge(g1, g2) { 212 | const fig = this.figure 213 | g1 = this.addNode(g1) 214 | g2 = this.addNode(g2) 215 | fig.costEqual(this.cost, 216 | new Distance(g1, g2, this.numExtraDims + 2), 217 | new Times(new Plus(g1.w(), g1.h(), g2.w(), g2.h()), 218 | this.sparsity)) 219 | // add same-direction penalty 220 | 221 | for (let i = 0; this.branchSpread != 0 && i < this.edges.length; i++) { 222 | let [a, b] = this.edges[i] 223 | let c = g1, d = g2 224 | if (b === c) { 225 | const t = b; b = a; a = t 226 | } else if (b === d) { 227 | const t = b; b = a; a = t 228 | d = c 229 | } else if (a === d) { 230 | d = c 231 | } else if (a !== c) { 232 | continue 233 | } 234 | // now a == c, have edges a -> b and a -> d 235 | // dot product = (b - a) • (d - a) = |b-a|·|d-a|·cos(theta) 236 | let dot = new Plus(new Times(new Minus(b.x(), a.x()), 237 | new Minus(d.x(), a.x())), 238 | new Times(new Minus(b.y(), a.y()), 239 | new Minus(d.y(), a.y()))) 240 | // dot = new DebugExpr("dot", dot) 241 | let d1 = new Distance(a, b), d2 = new Distance(a, d) 242 | // d1 = new DebugExpr("d1", d1) 243 | // d2 = new DebugExpr("d2", d2) 244 | let normalization = new Times(new Max(d1, 0.001), new Max(d2, 0.001)), 245 | cos = new Divide(dot, normalization) 246 | // cos = new DebugExpr("cos", cos) 247 | fig.costEqual(this.cost * this.branchSpread * this.sparsity, 248 | new Sqrt(new Minus(1, cos)), 2) 249 | } 250 | this.edges.push([g1, g2]) 251 | return fig.connector(g1.object(), g2.object()) 252 | } 253 | // Add an directed edge between objects g1 and g2, adding the objects as nodes if necessary. 254 | // Constraints are added to order them top-to-bottom or left-to-right, depending on the graph's 255 | // horizontalLayout property. 256 | // Return the (straight) connector between the objects. 257 | dedge(g1, g2) { 258 | const fig = this.figure, 259 | result = this.edge(g1, g2) 260 | g1 = this.addNode(g1) 261 | g2 = this.addNode(g2) 262 | if (this.horizontalLayout) { 263 | fig.geq(fig.minus(g2.x0(), g1.x1()), fig.times(0.25, fig.plus(g1.w(), g2.w()))).changeCost(this.cost * this.gravity) 264 | } else { 265 | new Loss(fig, new Minus(g1.y(), g2.y())).changeCost(this.cost * this.gravity) 266 | // fig.geq(fig.minus(g2.y0(), g1.y1()), fig.times(0.25, fig.plus(g1.h(), g2.h()))).changeCost(this.cost * this.gravity) 267 | } 268 | return result 269 | } 270 | setupHints() { 271 | const graph = this 272 | if (graph.hintsComputed) return 273 | graph.hintsComputed = true 274 | if (this.nodes.length == 0) return 275 | let root = this.nodes[0], visited = [] 276 | function traverse(n, level, x, y) { 277 | if (x === undefined) x = 200 278 | if (y === undefined) y = 100 279 | if (visited.includes(n)) return 280 | // console.log("Hinting " + n + " at " + x + ", " + y) 281 | visited.push(n) 282 | let outgoing = 0 283 | graph.edges.forEach(e => { 284 | const [g1, g2] = e 285 | if (g1 == n) outgoing++ 286 | }) 287 | let kid = 0 288 | let spread = 256 >> level 289 | graph.edges.forEach(e => { 290 | let [g1, g2] = e 291 | if (g1 == n) { 292 | const n2 = g1 == n ? g2 : g1, 293 | x2 = x + ((++kid)/(outgoing + 1) - 0.5) * spread, 294 | y2 = y + 100 * graph.sparsity 295 | if (n2.x().setHint) n2.x().setHint(x2) 296 | if (n2.y().setHint) n2.y().setHint(y2) 297 | traverse(n2, level+1, x2, y2) 298 | } 299 | }) 300 | } 301 | traverse(root, 0, root.x().hint, root.y().hint) 302 | } 303 | } 304 | 305 | Constrain.Figure.prototype.graph = function() { 306 | return new Graph(this) 307 | } 308 | 309 | return Graph 310 | 311 | }() 312 | -------------------------------------------------------------------------------- /constrain-mathjax.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 3 | function svg_length(len, figure) { 4 | const s = figure.getFontSize(), v = len.valueInSpecifiedUnits 5 | switch (len.unitType) { 6 | case 0: return v 7 | case 1: return v 8 | case 2: return v 9 | case 3: return v * s 10 | case 4: return v * s / 2 11 | case 5: return v 12 | case 6: return v * (96/2.54) 13 | case 7: return v * (96/25.4) 14 | case 8: return v * 96 15 | case 9: return v * (4/3) 16 | case 10: return v * 16 17 | } 18 | } 19 | 20 | class MathJaxImage extends Constrain.Graphic { 21 | constructor(figure, input, displayMath) { 22 | super(figure) 23 | MathJax.texReset() 24 | const svg = MathJax.tex2svg(input, displayMath).childNodes[0], 25 | data = new XMLSerializer().serializeToString(svg) 26 | 27 | this.img = document.createElement('img') 28 | this.img.src = "data:image/svg+xml;base64, " + window.btoa(unescape(encodeURIComponent(data))) 29 | const w = svg_length(svg.width.baseVal, figure), 30 | h = svg_length(svg.height.baseVal, figure) 31 | 32 | figure.equal(this.x1(), figure.plus(this.x0(), w)) 33 | figure.equal(this.y1(), figure.plus(this.y0(), h)) 34 | // console.log(`MathJax figure size: ${this.img.width} x ${this.img.height}: ${data}`) 35 | } 36 | render() { 37 | const figure = this.figure, ctx = figure.ctx, valuation = figure.currentValuation 38 | const [x0, x1, y0, y1] = Constrain.evaluate([this.x0(), this.x1(), this.y0(), this.y1()], valuation) 39 | 40 | // console.log(`Rendering MathJax content at (${x0}, ${y0})`) 41 | ctx.drawImage(this.img, x0, y0, x1 - x0, y1 - y0) 42 | } 43 | } 44 | 45 | Constrain.Figure.prototype.mathJax = function(texmath) { 46 | return new MathJaxImage(this, texmath) 47 | } 48 | 49 | })() 50 | -------------------------------------------------------------------------------- /constrain-pdf.js: -------------------------------------------------------------------------------- 1 | /* Requires: Constrain */ 2 | 3 | Constrain.PDF = function() { 4 | 5 | const colorTable = { 6 | "aliceblue": "#f0f8ff", 7 | "antiquewhite": "#faebd7", 8 | "aqua": "#00ffff", 9 | "aquamarine": "#7fffd4", 10 | "azure": "#f0ffff", 11 | "beige": "#f5f5dc", 12 | "bisque": "#ffe4c4", 13 | "black": "#000000", 14 | "blanchedalmond": "#ffebcd", 15 | "blue": "#0000ff", 16 | "blueviolet": "#8a2be2", 17 | "brown": "#a52a2a", 18 | "burlywood": "#deb887", 19 | "cadetblue": "#5f9ea0", 20 | "chartreuse": "#7fff00", 21 | "chocolate": "#d2691e", 22 | "coral": "#ff7f50", 23 | "cornflowerblue": "#6495ed", 24 | "cornsilk": "#fff8dc", 25 | "crimson": "#dc143c", 26 | "cyan": "#00ffff", 27 | "darkblue": "#00008b", 28 | "darkcyan": "#008b8b", 29 | "darkgoldenrod": "#b8860b", 30 | "darkgray": "#a9a9a9", 31 | "darkgreen": "#006400", 32 | "darkkhaki": "#bdb76b", 33 | "darkmagenta": "#8b008b", 34 | "darkolivegreen": "#556b2f", 35 | "darkorange": "#ff8c00", 36 | "darkorchid": "#9932cc", 37 | "darkred": "#8b0000", 38 | "darksalmon": "#e9967a", 39 | "darkseagreen": "#8fbc8f", 40 | "darkslateblue": "#483d8b", 41 | "darkslategray": "#2f4f4f", 42 | "darkturquoise": "#00ced1", 43 | "darkviolet": "#9400d3", 44 | "deeppink": "#ff1493", 45 | "deepskyblue": "#00bfff", 46 | "dimgray": "#696969", 47 | "dodgerblue": "#1e90ff", 48 | "firebrick": "#b22222", 49 | "floralwhite": "#fffaf0", 50 | "forestgreen": "#228b22", 51 | "fuchsia": "#ff00ff", 52 | "gainsboro": "#dcdcdc", 53 | "ghostwhite": "#f8f8ff", 54 | "gold": "#ffd700", 55 | "goldenrod": "#daa520", 56 | "gray": "#808080", 57 | "green": "#008000", 58 | "greenyellow": "#adff2f", 59 | "honeydew": "#f0fff0", 60 | "hotpink": "#ff69b4", 61 | "indianred ": "#cd5c5c", 62 | "indigo": "#4b0082", 63 | "ivory": "#fffff0", 64 | "khaki": "#f0e68c", 65 | "lavender": "#e6e6fa", 66 | "lavenderblush": "#fff0f5", 67 | "lawngreen": "#7cfc00", 68 | "lemonchiffon": "#fffacd", 69 | "lightblue": "#add8e6", 70 | "lightcoral": "#f08080", 71 | "lightcyan": "#e0ffff", 72 | "lightgoldenrodyellow": "#fafad2", 73 | "lightgrey": "#d3d3d3", 74 | "lightgreen": "#90ee90", 75 | "lightpink": "#ffb6c1", 76 | "lightsalmon": "#ffa07a", 77 | "lightseagreen": "#20b2aa", 78 | "lightskyblue": "#87cefa", 79 | "lightslategray": "#778899", 80 | "lightsteelblue": "#b0c4de", 81 | "lightyellow": "#ffffe0", 82 | "lime": "#00ff00", 83 | "limegreen": "#32cd32", 84 | "linen": "#faf0e6", 85 | "magenta": "#ff00ff", 86 | "maroon": "#800000", 87 | "mediumaquamarine": "#66cdaa", 88 | "mediumblue": "#0000cd", 89 | "mediumorchid": "#ba55d3", 90 | "mediumpurple": "#9370d8", 91 | "mediumseagreen": "#3cb371", 92 | "mediumslateblue": "#7b68ee", 93 | "mediumspringgreen": "#00fa9a", 94 | "mediumturquoise": "#48d1cc", 95 | "mediumvioletred": "#c71585", 96 | "midnightblue": "#191970", 97 | "mintcream": "#f5fffa", 98 | "mistyrose": "#ffe4e1", 99 | "moccasin": "#ffe4b5", 100 | "navajowhite": "#ffdead", 101 | "navy": "#000080", 102 | "oldlace": "#fdf5e6", 103 | "olive": "#808000", 104 | "olivedrab": "#6b8e23", 105 | "orange": "#ffa500", 106 | "orangered": "#ff4500", 107 | "orchid": "#da70d6", 108 | "palegoldenrod": "#eee8aa", 109 | "palegreen": "#98fb98", 110 | "paleturquoise": "#afeeee", 111 | "palevioletred": "#d87093", 112 | "papayawhip": "#ffefd5", 113 | "peachpuff": "#ffdab9", 114 | "peru": "#cd853f", 115 | "pink": "#ffc0cb", 116 | "plum": "#dda0dd", 117 | "powderblue": "#b0e0e6", 118 | "purple": "#800080", 119 | "rebeccapurple": "#663399", 120 | "red": "#ff0000", 121 | "rosybrown": "#bc8f8f", 122 | "royalblue": "#4169e1", 123 | "saddlebrown": "#8b4513", 124 | "salmon": "#fa8072", 125 | "sandybrown": "#f4a460", 126 | "seagreen": "#2e8b57", 127 | "seashell": "#fff5ee", 128 | "sienna": "#a0522d", 129 | "silver": "#c0c0c0", 130 | "skyblue": "#87ceeb", 131 | "slateblue": "#6a5acd", 132 | "slategray": "#708090", 133 | "snow": "#fffafa", 134 | "springgreen": "#00ff7f", 135 | "steelblue": "#4682b4", 136 | "tan": "#d2b48c", 137 | "teal": "#008080", 138 | "thistle": "#d8bfd8", 139 | "tomato": "#ff6347", 140 | "turquoise": "#40e0d0", 141 | "violet": "#ee82ee", 142 | "wheat": "#f5deb3", 143 | "white": "#ffffff", 144 | "whitesmoke": "#f5f5f5", 145 | "yellow": "#ffff00", 146 | "yellowgreen": "#9acd32" 147 | } 148 | 149 | const PSFontStyles = { 150 | "Apple Chancery" : [], 151 | "Arial" : ["Regular", "Italic", "Bold", "Bold Italic"], 152 | "Bodoni" : ["Roman", "Italic", "Bold", "Bold Italic", "Poster", "Poster Compressed"], 153 | "Carta" : [], 154 | "Chicago" : [], 155 | "Clarendon" : ["Light", "Roman", "Bold"], 156 | "Cooper Black" : [], 157 | "Cooper Black Italic" : [], 158 | "Copperplate Gothic" : [], 159 | "Coronet" : [], 160 | "Eurostile" : ["Medium", "Bold", "Extended No.2", "Bold Extended No.2"], 161 | "Geneva" : [], 162 | "Gill Sans" : ["Light", "Light Italic", "Book", "Book Italic", 163 | "Bold", "Bold Italic", "Extra Bold", "Condensed", 164 | "Condensed Bold"], 165 | "Goudy" : ["Oldstyle", "Oldstyle Italic", "Bold", "Bold Italic", "Extra Bold"], 166 | "Helvetica" : ["Narrow", "Narrow Oblique", "Narrow Bold", "Narrow Bold Oblique"], 167 | "Hoefler Text" : ["Roman", "Italic", "Black", "Black Italic"], 168 | "Hoefler Ornaments" : [], 169 | "Joanna" : ["Regular", "Italic", "Bold", "Bold Italic"], 170 | "Letter Gothic" : ["Regular", "Slanted", "Bold", "Bold Slanted"], 171 | "ITC Lubalin Graph" : ["Book", "Oblique", "Demi", "Demi Oblique"], 172 | "ITC Mona Lisa Recut" : [], 173 | "Marigold" : [], 174 | "Monaco" : [], 175 | "New York" : [], 176 | "Optima" : ["Roman", "Italic", "Bold", "Bold Italic"], 177 | "Oxford" : [], 178 | "Palatino" : ["Roman", "Italic", "Bold", "Bold Italic"], 179 | "Stempel Garamond" : ["Roman", "Italic", "Bold", "Bold Italic"], 180 | "Tekton" : ["Regular"], 181 | "Times New Roman" : ["Regular", "Italic", "Bold", "Bold Italic"], 182 | "Times" : ["Roman", "Italic", "Bold", "Bold Italic"], 183 | "Wingdings" : [], 184 | } 185 | 186 | /** Maps Unicode code points to the corresponding Symbol font position. */ 187 | const UnicodeToSymbol = { 188 | 0x0020: 0x20, 0x00A0: 0x20, 0x0021: 0x21, 0x2200: 0x22, 0x0023: 0x23, 189 | 0x2203: 0x24, 0x0025: 0x25, 0x0026: 0x26, 0x220B: 0x27, 0x0028: 0x28, 190 | 0x0029: 0x29, 0x2217: 0x2A, 0x002B: 0x2B, 0x002C: 0x2C, 0x2212: 0x2D, 191 | 0x002E: 0x2E, 0x002F: 0x2F, 0x0030: 0x30, 0x0031: 0x31, 0x0032: 0x32, 192 | 0x0033: 0x33, 0x0034: 0x34, 0x0035: 0x35, 0x0036: 0x36, 0x0037: 0x37, 193 | 0x0038: 0x38, 0x0039: 0x39, 0x003A: 0x3A, 0x003B: 0x3B, 0x003C: 0x3C, 194 | 0x003D: 0x3D, 0x003E: 0x3E, 0x003F: 0x3F, 0x2245: 0x40, 0x0391: 0x41, 195 | 0x0392: 0x42, 0x03A7: 0x43, 0x0394: 0x44, 0x2206: 0x44, 0x0395: 0x45, 196 | 0x03A6: 0x46, 0x0393: 0x47, 0x0397: 0x48, 0x0399: 0x49, 0x03D1: 0x4A, 197 | 0x039A: 0x4B, 0x039B: 0x4C, 0x039C: 0x4D, 0x039D: 0x4E, 0x039F: 0x4F, 198 | 0x03A0: 0x50, 0x0398: 0x51, 0x03A1: 0x52, 0x03A3: 0x53, 0x03A4: 0x54, 199 | 0x03A5: 0x55, 0x03C2: 0x56, 0x03A9: 0x57, 0x2126: 0x57, 0x039E: 0x58, 200 | 0x03A8: 0x59, 0x0396: 0x5A, 0x005B: 0x5B, 0x2234: 0x5C, 0x005D: 0x5D, 201 | 0x22A5: 0x5E, 0x005F: 0x5F, 0xF8E5: 0x60, 0x03B1: 0x61, 0x03B2: 0x62, 202 | 0x03C7: 0x63, 0x03B4: 0x64, 0x03B5: 0x65, 0x03C6: 0x66, 0x03B3: 0x67, 203 | 0x03B7: 0x68, 0x03B9: 0x69, 0x03D5: 0x6A, 0x03BA: 0x6B, 0x03BB: 0x6C, 204 | 0x00B5: 0x6D, 0x03BC: 0x6D, 0x03BD: 0x6E, 0x03BF: 0x6F, 0x03C0: 0x70, 205 | 0x03B8: 0x71, 0x03C1: 0x72, 0x03C3: 0x73, 0x03C4: 0x74, 0x03C5: 0x75, 206 | 0x03D6: 0x76, 0x03C9: 0x77, 0x03BE: 0x78, 0x03C8: 0x79, 0x03B6: 0x7A, 207 | 0x007B: 0x7B, 0x007C: 0x7C, 0x007D: 0x7D, 0x223C: 0x7E, 0x20AC: 0xA0, 208 | 0x03D2: 0xA1, 0x2032: 0xA2, 0x2264: 0xA3, 0x2044: 0xA4, 0x2215: 0xA4, 209 | 0x221E: 0xA5, 0x0192: 0xA6, 0x2663: 0xA7, 0x2666: 0xA8, 0x2665: 0xA9, 210 | 0x2660: 0xAA, 0x2194: 0xAB, 0x2190: 0xAC, 0x2191: 0xAD, 0x2192: 0xAE, 211 | 0x2193: 0xAF, 0x00B0: 0xB0, 0x00B1: 0xB1, 0x2033: 0xB2, 0x2265: 0xB3, 212 | 0x00D7: 0xB4, 0x221D: 0xB5, 0x2202: 0xB6, 0x2022: 0xB7, 0x00F7: 0xB8, 213 | 0x2260: 0xB9, 0x2261: 0xBA, 0x2248: 0xBB, 0x2026: 0xBC, 0xF8E6: 0xBD, 214 | 0xF8E7: 0xBE, 0x21B5: 0xBF, 0x2135: 0xC0, 0x2111: 0xC1, 0x211C: 0xC2, 215 | 0x2118: 0xC3, 0x2297: 0xC4, 0x2295: 0xC5, 0x2205: 0xC6, 0x2229: 0xC7, 216 | 0x222A: 0xC8, 0x2283: 0xC9, 0x2287: 0xCA, 0x2284: 0xCB, 0x2282: 0xCC, 217 | 0x2286: 0xCD, 0x2208: 0xCE, 0x2209: 0xCF, 0x2220: 0xD0, 0x2207: 0xD1, 218 | 0xF6DA: 0xD2, 0xF6D9: 0xD3, 0xF6DB: 0xD4, 0x220F: 0xD5, 0x221A: 0xD6, 219 | 0x22C5: 0xD7, 0x00AC: 0xD8, 0x2227: 0xD9, 0x2228: 0xDA, 0x21D4: 0xDB, 220 | 0x21D0: 0xDC, 0x21D1: 0xDD, 0x21D2: 0xDE, 0x21D3: 0xDF, 0x25CA: 0xE0, 221 | 0x2329: 0xE1, 0xF8E8: 0xE2, 0xF8E9: 0xE3, 0xF8EA: 0xE4, 0x2211: 0xE5, 222 | 0xF8EB: 0xE6, 0xF8EC: 0xE7, 0xF8ED: 0xE8, 0xF8EE: 0xE9, 0xF8EF: 0xEA, 223 | 0xF8F0: 0xEB, 0xF8F1: 0xEC, 0xF8F2: 0xED, 0xF8F3: 0xEE, 0xF8F4: 0xEF, 224 | 0x232A: 0xF1, 0x222B: 0xF2, 0x2320: 0xF3, 0xF8F5: 0xF4, 0x2321: 0xF5, 225 | 0xF8F6: 0xF6, 0xF8F7: 0xF7, 0xF8F8: 0xF8, 0xF8F9: 0xF9, 0xF8FA: 0xFA, 226 | 0xF8FB: 0xFB, 0xF8FC: 0xFC, 0xF8FD: 0xFD, 0xF8FE: 0xFE 227 | } 228 | 229 | function colorToRGB(s) { 230 | let hex 231 | if (s[0] == "#") { 232 | hex = s 233 | } else { 234 | hex = colorTable[s.toLowerCase()] 235 | if (!hex) return [128, 128, 128] 236 | } 237 | let r, g, b 238 | if (hex.length == 4) { 239 | r = parseInt(hex[1], 16) * 17 240 | g = parseInt(hex[2], 16) * 17 241 | b = parseInt(hex[3], 16) * 17 242 | } else { 243 | r = parseInt(hex.substr(1, 2), 16) 244 | g = parseInt(hex.substr(3, 2), 16) 245 | b = parseInt(hex.substr(5, 2), 16) 246 | } 247 | return [r, g, b] 248 | } 249 | 250 | function override_jsPDF(jspdf, ctx, figure) { 251 | const prototype = jspdf.context2d.constructor.prototype, 252 | Point = jspdf.internal.Point 253 | 254 | // Allow recognizing special contexts 255 | prototype.printMedia = true 256 | 257 | // setLineDash() is not implemented correctly by jsPDF.Context2d 258 | prototype.setLineDash = function(pattern) { 259 | this.ctx.lineDash = pattern 260 | jspdf.setLineDashPattern(pattern.map(x => x * figure.scale)) 261 | } 262 | // getLineDash() is not implemented by jsPDF.context2d 263 | prototype.getLineDash = function() { 264 | const r = this.ctx.lineDash 265 | if (!r) return [] 266 | if (r.length % 2 == 0) return r 267 | else return r.concat(r) 268 | } 269 | 270 | // Need to use original context to make text measurements correct 271 | prototype.measureText = function(s) { 272 | ctx.font = this.font 273 | return ctx.measureText(s) 274 | } 275 | // Remove extra path segment that Context2d.closePath() adds for 276 | // some reason. 277 | prototype.closePath = function() { 278 | let pathBegin = new Point(0, 0) 279 | for (let i = this.path.length - 1; i >= 0; i--) { 280 | if (this.path[i].type === "begin") { 281 | if (typeof this.path[i + 1] === "object" && typeof this.path[i + 1].x === "number") { 282 | pathBegin = new Point(this.path[i + 1].x, this.path[i + 1].y) 283 | this.path.push({ 284 | type: "lt", 285 | x: pathBegin.x, 286 | y: pathBegin.y 287 | }) 288 | break 289 | } 290 | } 291 | } 292 | this.path.push({ type: "close" }) 293 | this.ctx.lastPoint = new Point(pathBegin.x, pathBegin.y) 294 | } 295 | prototype.rect = function(x, y, w, h) { 296 | if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) { 297 | console.error("jsPDF.context2d.rect: Invalid arguments", arguments) 298 | throw new Error("Invalid arguments passed to jsPDF.context2d.rect") 299 | } 300 | this.moveTo(x, y) 301 | this.lineTo(x + w, y) 302 | this.lineTo(x + w, y + h) 303 | this.lineTo(x, y + h) 304 | this.closePath() 305 | } 306 | prototype.clip = function(rule) { 307 | jspdf.clip(rule) 308 | } 309 | } 310 | 311 | class PrintJob { 312 | constructor(figure) { 313 | this.figure = figure 314 | this.fonts = [] 315 | this.fontFiles = [] 316 | } 317 | addFont(filename, family, variant, fontWeight, encoding) { 318 | this.fonts.push([filename, family, variant, fontWeight, encoding]) 319 | return this 320 | } 321 | addFontFile(filename, data) { 322 | this.fontFiles.push([filename, data]) 323 | return this 324 | } 325 | print() { 326 | const figure = this.figure 327 | const save_ctx = figure.ctx 328 | const orientation = (figure.width > figure.height) ? "l" : "p" 329 | const unit = "px" 330 | const output = new jspdf.jsPDF({ 331 | orientation, 332 | unit, 333 | format: [figure.width * figure.scale, figure.height * figure.scale], 334 | hot_fixes: ["px_scaling"] 335 | }) 336 | for (let [filename, data] of this.fontFiles) { 337 | output.addFileToVFS(filename, data) 338 | } 339 | for (let [filename, family, variant, fontWeight, encoding] of this.fonts) { 340 | output.addFont(filename, family, variant, fontWeight, encoding) 341 | } 342 | const pc = output.context2d 343 | override_jsPDF(output, save_ctx, figure) 344 | figure.ctx = pc 345 | 346 | figure.renderFrame(false) 347 | output.save("constrain-figure.pdf") 348 | 349 | figure.ctx = save_ctx; 350 | figure.renderFrame(false) 351 | } 352 | } 353 | 354 | 355 | class PrintButton extends Constrain.Button { 356 | constructor(figure) { 357 | super(figure) 358 | this.printJob = new PrintJob(figure) 359 | } 360 | render() { 361 | const figure = this.figure, ctx = figure.ctx, valuation = figure.currentValuation 362 | if (ctx.printMedia) return 363 | const s = this.size 364 | ctx.beginPath() 365 | const x = Constrain.evaluate(this.x(), valuation), 366 | y = Constrain.evaluate(this.y(), valuation) 367 | ctx.save() 368 | ctx.translate(x - s * 0.5, y - s*0.3) 369 | Constrain.Paths.roundedRect(ctx, 0, s, 0, s*0.8, s*0.1) 370 | if (this.pressed) 371 | ctx.fillStyle = "#888" 372 | else 373 | ctx.fillStyle = this.fillStyle 374 | ctx.fill() 375 | ctx.strokeStyle = this.strokeStyle 376 | ctx.lineWidth = s/10 377 | ctx.setLineDash([]) 378 | ctx.stroke() 379 | 380 | ctx.translate(s * 0.5, s*0.4) 381 | ctx.fillStyle = "white" 382 | ctx.beginPath() 383 | ctx.fillRect(-8, -8, 16, 16) 384 | ctx.strokeStyle = "black" 385 | ctx.lineWidth = 1 386 | ctx.stroke() 387 | ctx.beginPath(); ctx.moveTo(-4, -3); ctx.lineTo(6, -3); ctx.stroke() 388 | ctx.beginPath(); ctx.moveTo(-6, 0); ctx.lineTo(6, 0); ctx.stroke() 389 | ctx.beginPath(); ctx.moveTo(-6, 3); ctx.lineTo(4, 3); ctx.stroke() 390 | ctx.fillStyle = "#444" 391 | ctx.font = "10px Helvetica, Arial" 392 | ctx.fillText("PDF", -8, 0) 393 | ctx.restore() 394 | } 395 | activate() { 396 | this.printJob.print() 397 | } 398 | addFont(filename, family, variant, fontWeight, encoding) { 399 | this.printJob.addFont(filename, family, variant, fontWeight, encoding) 400 | return this 401 | } 402 | addFontFile(filename, data) { 403 | this.printJob.addFontFile(filename, data) 404 | return this 405 | } 406 | } 407 | 408 | function PSquote(s) { 409 | return "(" + s.replace(")", "\\)") + ")" // TODO 410 | } 411 | function mround(x) { 412 | return Math.round(x * 1000)/1000; 413 | } 414 | 415 | let PX_TO_PT = 96/72 416 | 417 | function mapStyle(fontname, style) { 418 | if (!style) { 419 | const styles = PSFontStyles[fontname] 420 | if (!styles) return undefined; 421 | if (styles.length > 0) { 422 | return styles[0] 423 | } 424 | } 425 | return style[0].toUpperCase() + style.substr(1) 426 | } 427 | 428 | return { 429 | PrintJob, 430 | PrintButton, 431 | colorTable 432 | } 433 | }() 434 | -------------------------------------------------------------------------------- /constrain-ps.js: -------------------------------------------------------------------------------- 1 | /* Requires: Constrain */ 2 | 3 | Constrain.PS = function() { 4 | 5 | const colorTable = { 6 | "aliceblue": "#f0f8ff", 7 | "antiquewhite": "#faebd7", 8 | "aqua": "#00ffff", 9 | "aquamarine": "#7fffd4", 10 | "azure": "#f0ffff", 11 | "beige": "#f5f5dc", 12 | "bisque": "#ffe4c4", 13 | "black": "#000000", 14 | "blanchedalmond": "#ffebcd", 15 | "blue": "#0000ff", 16 | "blueviolet": "#8a2be2", 17 | "brown": "#a52a2a", 18 | "burlywood": "#deb887", 19 | "cadetblue": "#5f9ea0", 20 | "chartreuse": "#7fff00", 21 | "chocolate": "#d2691e", 22 | "coral": "#ff7f50", 23 | "cornflowerblue": "#6495ed", 24 | "cornsilk": "#fff8dc", 25 | "crimson": "#dc143c", 26 | "cyan": "#00ffff", 27 | "darkblue": "#00008b", 28 | "darkcyan": "#008b8b", 29 | "darkgoldenrod": "#b8860b", 30 | "darkgray": "#a9a9a9", 31 | "darkgreen": "#006400", 32 | "darkkhaki": "#bdb76b", 33 | "darkmagenta": "#8b008b", 34 | "darkolivegreen": "#556b2f", 35 | "darkorange": "#ff8c00", 36 | "darkorchid": "#9932cc", 37 | "darkred": "#8b0000", 38 | "darksalmon": "#e9967a", 39 | "darkseagreen": "#8fbc8f", 40 | "darkslateblue": "#483d8b", 41 | "darkslategray": "#2f4f4f", 42 | "darkturquoise": "#00ced1", 43 | "darkviolet": "#9400d3", 44 | "deeppink": "#ff1493", 45 | "deepskyblue": "#00bfff", 46 | "dimgray": "#696969", 47 | "dodgerblue": "#1e90ff", 48 | "firebrick": "#b22222", 49 | "floralwhite": "#fffaf0", 50 | "forestgreen": "#228b22", 51 | "fuchsia": "#ff00ff", 52 | "gainsboro": "#dcdcdc", 53 | "ghostwhite": "#f8f8ff", 54 | "gold": "#ffd700", 55 | "goldenrod": "#daa520", 56 | "gray": "#808080", 57 | "green": "#008000", 58 | "greenyellow": "#adff2f", 59 | "honeydew": "#f0fff0", 60 | "hotpink": "#ff69b4", 61 | "indianred ": "#cd5c5c", 62 | "indigo": "#4b0082", 63 | "ivory": "#fffff0", 64 | "khaki": "#f0e68c", 65 | "lavender": "#e6e6fa", 66 | "lavenderblush": "#fff0f5", 67 | "lawngreen": "#7cfc00", 68 | "lemonchiffon": "#fffacd", 69 | "lightblue": "#add8e6", 70 | "lightcoral": "#f08080", 71 | "lightcyan": "#e0ffff", 72 | "lightgoldenrodyellow": "#fafad2", 73 | "lightgrey": "#d3d3d3", 74 | "lightgreen": "#90ee90", 75 | "lightpink": "#ffb6c1", 76 | "lightsalmon": "#ffa07a", 77 | "lightseagreen": "#20b2aa", 78 | "lightskyblue": "#87cefa", 79 | "lightslategray": "#778899", 80 | "lightsteelblue": "#b0c4de", 81 | "lightyellow": "#ffffe0", 82 | "lime": "#00ff00", 83 | "limegreen": "#32cd32", 84 | "linen": "#faf0e6", 85 | "magenta": "#ff00ff", 86 | "maroon": "#800000", 87 | "mediumaquamarine": "#66cdaa", 88 | "mediumblue": "#0000cd", 89 | "mediumorchid": "#ba55d3", 90 | "mediumpurple": "#9370d8", 91 | "mediumseagreen": "#3cb371", 92 | "mediumslateblue": "#7b68ee", 93 | "mediumspringgreen": "#00fa9a", 94 | "mediumturquoise": "#48d1cc", 95 | "mediumvioletred": "#c71585", 96 | "midnightblue": "#191970", 97 | "mintcream": "#f5fffa", 98 | "mistyrose": "#ffe4e1", 99 | "moccasin": "#ffe4b5", 100 | "navajowhite": "#ffdead", 101 | "navy": "#000080", 102 | "oldlace": "#fdf5e6", 103 | "olive": "#808000", 104 | "olivedrab": "#6b8e23", 105 | "orange": "#ffa500", 106 | "orangered": "#ff4500", 107 | "orchid": "#da70d6", 108 | "palegoldenrod": "#eee8aa", 109 | "palegreen": "#98fb98", 110 | "paleturquoise": "#afeeee", 111 | "palevioletred": "#d87093", 112 | "papayawhip": "#ffefd5", 113 | "peachpuff": "#ffdab9", 114 | "peru": "#cd853f", 115 | "pink": "#ffc0cb", 116 | "plum": "#dda0dd", 117 | "powderblue": "#b0e0e6", 118 | "purple": "#800080", 119 | "rebeccapurple": "#663399", 120 | "red": "#ff0000", 121 | "rosybrown": "#bc8f8f", 122 | "royalblue": "#4169e1", 123 | "saddlebrown": "#8b4513", 124 | "salmon": "#fa8072", 125 | "sandybrown": "#f4a460", 126 | "seagreen": "#2e8b57", 127 | "seashell": "#fff5ee", 128 | "sienna": "#a0522d", 129 | "silver": "#c0c0c0", 130 | "skyblue": "#87ceeb", 131 | "slateblue": "#6a5acd", 132 | "slategray": "#708090", 133 | "snow": "#fffafa", 134 | "springgreen": "#00ff7f", 135 | "steelblue": "#4682b4", 136 | "tan": "#d2b48c", 137 | "teal": "#008080", 138 | "thistle": "#d8bfd8", 139 | "tomato": "#ff6347", 140 | "turquoise": "#40e0d0", 141 | "violet": "#ee82ee", 142 | "wheat": "#f5deb3", 143 | "white": "#ffffff", 144 | "whitesmoke": "#f5f5f5", 145 | "yellow": "#ffff00", 146 | "yellowgreen": "#9acd32" 147 | } 148 | 149 | const PSFontStyles = { 150 | "Apple Chancery" : [], 151 | "Arial" : ["Regular", "Italic", "Bold", "Bold Italic"], 152 | "Bodoni" : ["Roman", "Italic", "Bold", "Bold Italic", "Poster", "Poster Compressed"], 153 | "Carta" : [], 154 | "Chicago" : [], 155 | "Clarendon" : ["Light", "Roman", "Bold"], 156 | "Cooper Black" : [], 157 | "Cooper Black Italic" : [], 158 | "Copperplate Gothic" : [], 159 | "Coronet" : [], 160 | "Courier" : ["Regular", "Oblique", "Bold", "Bold Oblique"], 161 | "Eurostile" : ["Medium", "Bold", "Extended No.2", "Bold Extended No.2"], 162 | "Geneva" : [], 163 | "Gill Sans" : ["Light", "Light Italic", "Book", "Book Italic", 164 | "Bold", "Bold Italic", "Extra Bold", "Condensed", 165 | "Condensed Bold"], 166 | "Goudy" : ["Oldstyle", "Oldstyle Italic", "Bold", "Bold Italic", "Extra Bold"], 167 | "Helvetica" : ["Narrow", "Narrow Oblique", "Narrow Bold", "Narrow Bold Oblique"], 168 | "Hoefler Text" : ["Roman", "Italic", "Black", "Black Italic"], 169 | "Hoefler Ornaments" : [], 170 | "Joanna" : ["Regular", "Italic", "Bold", "Bold Italic"], 171 | "Letter Gothic" : ["Regular", "Slanted", "Bold", "Bold Slanted"], 172 | "ITC Lubalin Graph" : ["Book", "Oblique", "Demi", "Demi Oblique"], 173 | "ITC Mona Lisa Recut" : [], 174 | "Marigold" : [], 175 | "Monaco" : [], 176 | "New York" : [], 177 | "Optima" : ["Roman", "Italic", "Bold", "Bold Italic"], 178 | "Oxford" : [], 179 | "Palatino" : ["Roman", "Italic", "Bold", "Bold Italic"], 180 | "Stempel Garamond" : ["Roman", "Italic", "Bold", "Bold Italic"], 181 | "Tekton" : ["Regular"], 182 | "Times New Roman" : ["Regular", "Italic", "Bold", "Bold Italic"], 183 | "Times" : ["Roman", "Italic", "Bold", "Bold Italic"], 184 | "Wingdings" : [], 185 | } 186 | 187 | const fontMap = { 188 | "Arial" : "Helvetica" 189 | } 190 | 191 | /** Maps Unicode code points to the corresponding Symbol font position. */ 192 | const UnicodeToSymbol = { 193 | 0x0020: 0x20, 0x00A0: 0x20, 0x0021: 0x21, 0x2200: 0x22, 0x0023: 0x23, 194 | 0x2203: 0x24, 0x0025: 0x25, 0x0026: 0x26, 0x220B: 0x27, 0x0028: 0x28, 195 | 0x0029: 0x29, 0x2217: 0x2A, 0x002B: 0x2B, 0x002C: 0x2C, 0x2212: 0x2D, 196 | 0x002E: 0x2E, 0x002F: 0x2F, 0x0030: 0x30, 0x0031: 0x31, 0x0032: 0x32, 197 | 0x0033: 0x33, 0x0034: 0x34, 0x0035: 0x35, 0x0036: 0x36, 0x0037: 0x37, 198 | 0x0038: 0x38, 0x0039: 0x39, 0x003A: 0x3A, 0x003B: 0x3B, 0x003C: 0x3C, 199 | 0x003D: 0x3D, 0x003E: 0x3E, 0x003F: 0x3F, 0x2245: 0x40, 0x0391: 0x41, 200 | 0x0392: 0x42, 0x03A7: 0x43, 0x0394: 0x44, 0x2206: 0x44, 0x0395: 0x45, 201 | 0x03A6: 0x46, 0x0393: 0x47, 0x0397: 0x48, 0x0399: 0x49, 0x03D1: 0x4A, 202 | 0x039A: 0x4B, 0x039B: 0x4C, 0x039C: 0x4D, 0x039D: 0x4E, 0x039F: 0x4F, 203 | 0x03A0: 0x50, 0x0398: 0x51, 0x03A1: 0x52, 0x03A3: 0x53, 0x03A4: 0x54, 204 | 0x03A5: 0x55, 0x03C2: 0x56, 0x03A9: 0x57, 0x2126: 0x57, 0x039E: 0x58, 205 | 0x03A8: 0x59, 0x0396: 0x5A, 0x005B: 0x5B, 0x2234: 0x5C, 0x005D: 0x5D, 206 | 0x22A5: 0x5E, 0x005F: 0x5F, 0xF8E5: 0x60, 0x03B1: 0x61, 0x03B2: 0x62, 207 | 0x03C7: 0x63, 0x03B4: 0x64, 0x03B5: 0x65, 0x03C6: 0x66, 0x03B3: 0x67, 208 | 0x03B7: 0x68, 0x03B9: 0x69, 0x03D5: 0x6A, 0x03BA: 0x6B, 0x03BB: 0x6C, 209 | 0x00B5: 0x6D, 0x03BC: 0x6D, 0x03BD: 0x6E, 0x03BF: 0x6F, 0x03C0: 0x70, 210 | 0x03B8: 0x71, 0x03C1: 0x72, 0x03C3: 0x73, 0x03C4: 0x74, 0x03C5: 0x75, 211 | 0x03D6: 0x76, 0x03C9: 0x77, 0x03BE: 0x78, 0x03C8: 0x79, 0x03B6: 0x7A, 212 | 0x007B: 0x7B, 0x007C: 0x7C, 0x007D: 0x7D, 0x223C: 0x7E, 0x20AC: 0xA0, 213 | 0x03D2: 0xA1, 0x2032: 0xA2, 0x2264: 0xA3, 0x2044: 0xA4, 0x2215: 0xA4, 214 | 0x221E: 0xA5, 0x0192: 0xA6, 0x2663: 0xA7, 0x2666: 0xA8, 0x2665: 0xA9, 215 | 0x2660: 0xAA, 0x2194: 0xAB, 0x2190: 0xAC, 0x2191: 0xAD, 0x2192: 0xAE, 216 | 0x2193: 0xAF, 0x00B0: 0xB0, 0x00B1: 0xB1, 0x2033: 0xB2, 0x2265: 0xB3, 217 | 0x00D7: 0xB4, 0x221D: 0xB5, 0x2202: 0xB6, 0x2022: 0xB7, 0x00F7: 0xB8, 218 | 0x2260: 0xB9, 0x2261: 0xBA, 0x2248: 0xBB, 0x2026: 0xBC, 0xF8E6: 0xBD, 219 | 0xF8E7: 0xBE, 0x21B5: 0xBF, 0x2135: 0xC0, 0x2111: 0xC1, 0x211C: 0xC2, 220 | 0x2118: 0xC3, 0x2297: 0xC4, 0x2295: 0xC5, 0x2205: 0xC6, 0x2229: 0xC7, 221 | 0x222A: 0xC8, 0x2283: 0xC9, 0x2287: 0xCA, 0x2284: 0xCB, 0x2282: 0xCC, 222 | 0x2286: 0xCD, 0x2208: 0xCE, 0x2209: 0xCF, 0x2220: 0xD0, 0x2207: 0xD1, 223 | 0xF6DA: 0xD2, 0xF6D9: 0xD3, 0xF6DB: 0xD4, 0x220F: 0xD5, 0x221A: 0xD6, 224 | 0x22C5: 0xD7, 0x00AC: 0xD8, 0x2227: 0xD9, 0x2228: 0xDA, 0x21D4: 0xDB, 225 | 0x21D0: 0xDC, 0x21D1: 0xDD, 0x21D2: 0xDE, 0x21D3: 0xDF, 0x25CA: 0xE0, 226 | 0x2329: 0xE1, 0xF8E8: 0xE2, 0xF8E9: 0xE3, 0xF8EA: 0xE4, 0x2211: 0xE5, 227 | 0xF8EB: 0xE6, 0xF8EC: 0xE7, 0xF8ED: 0xE8, 0xF8EE: 0xE9, 0xF8EF: 0xEA, 228 | 0xF8F0: 0xEB, 0xF8F1: 0xEC, 0xF8F2: 0xED, 0xF8F3: 0xEE, 0xF8F4: 0xEF, 229 | 0x232A: 0xF1, 0x222B: 0xF2, 0x2320: 0xF3, 0xF8F5: 0xF4, 0x2321: 0xF5, 230 | 0xF8F6: 0xF6, 0xF8F7: 0xF7, 0xF8F8: 0xF8, 0xF8F9: 0xF9, 0xF8FA: 0xFA, 231 | 0xF8FB: 0xFB, 0xF8FC: 0xFC, 0xF8FD: 0xFD, 0xF8FE: 0xFE 232 | } 233 | 234 | function colorToRGB(s) { 235 | let hex 236 | if (s[0] == "#") { 237 | hex = s 238 | } else { 239 | hex = colorTable[s.toLowerCase()] 240 | if (!hex) return [128, 128, 128] 241 | } 242 | let r, g, b 243 | if (hex.length == 4) { 244 | r = parseInt(hex[1], 16) * 17 245 | g = parseInt(hex[2], 16) * 17 246 | b = parseInt(hex[3], 16) * 17 247 | } else { 248 | r = parseInt(hex.substr(1, 2), 16) 249 | g = parseInt(hex.substr(3, 2), 16) 250 | b = parseInt(hex.substr(5, 2), 16) 251 | } 252 | return [r, g, b] 253 | } 254 | 255 | class PrintButton extends Constrain.Button { 256 | constructor(figure) { 257 | super(figure) 258 | } 259 | render() { 260 | const figure = this.figure, ctx = figure.ctx, valuation = figure.currentValuation 261 | if (ctx.printMedia) return 262 | const s = this.size 263 | ctx.beginPath() 264 | const x = Constrain.evaluate(this.x(), valuation), 265 | y = Constrain.evaluate(this.y(), valuation) 266 | ctx.save() 267 | ctx.translate(x - s * 0.5, y - s*0.3) 268 | Constrain.Paths.roundedRect(ctx, 0, s, 0, s*0.8, s*0.1) 269 | if (this.pressed) 270 | ctx.fillStyle = "#888" 271 | else 272 | ctx.fillStyle = this.fillStyle 273 | ctx.fill() 274 | ctx.strokeStyle = this.strokeStyle 275 | ctx.lineWidth = s/10 276 | ctx.setLineDash([]) 277 | ctx.stroke() 278 | 279 | ctx.translate(s * 0.5, s*0.4) 280 | ctx.fillStyle = "white" 281 | ctx.beginPath() 282 | ctx.fillRect(-8, -8, 16, 16) 283 | ctx.strokeStyle = "black" 284 | ctx.lineWidth = 1 285 | ctx.stroke() 286 | ctx.beginPath(); ctx.moveTo(-4, -3); ctx.lineTo(6, -3); ctx.stroke() 287 | ctx.beginPath(); ctx.moveTo(-6, 0); ctx.lineTo(6, 0); ctx.stroke() 288 | ctx.beginPath(); ctx.moveTo(-6, 3); ctx.lineTo(4, 3); ctx.stroke() 289 | ctx.fillStyle = "#444" 290 | ctx.font = "10px Helvetica, Arial" 291 | ctx.fillText("PS", -8, 0) 292 | ctx.restore() 293 | } 294 | activate() { 295 | print(this.figure) 296 | } 297 | } 298 | 299 | function PSquote(s) { 300 | return "(" + s.replace(")", "\\)") + ")" // TODO 301 | } 302 | function mround(x) { 303 | return Math.round(x * 1000)/1000; 304 | } 305 | 306 | let PX_TO_PT = 96/72 307 | 308 | function mapFontName(fontnames) { 309 | for (let i = 0; i < fontnames.length; i++) { 310 | const n = fontnames[i], 311 | f = PSFontStyles[n] 312 | if (f) return n 313 | if (fontMap[n]) return fontMap[n] 314 | } 315 | console.error("Don't know how to map font(s) " + fontnames.join(",") + " to PS fonts") 316 | return fontnames[0] 317 | } 318 | 319 | function mapStyle(fontname, style) { 320 | if (!style) { 321 | const styles = PSFontStyles[fontname] 322 | if (!styles) return undefined; 323 | if (styles.length > 0) { 324 | return styles[0] 325 | } 326 | } 327 | return style[0].toUpperCase() + style.substr(1) 328 | } 329 | 330 | function print(figure) { 331 | const save_ctx = figure.ctx 332 | const pc = new PrintContext(figure, figure.ctx) 333 | figure.ctx = pc 334 | 335 | figure.renderFrame(false) 336 | exportData(pc.getOutput(), "constrain-figure.ps", "application/postscript") 337 | 338 | figure.ctx = save_ctx; 339 | figure.render(false) 340 | } 341 | 342 | class PrintContext { 343 | constructor(figure, ctx2d) { 344 | this.figure = figure 345 | this.ctx2d = ctx2d 346 | this.output = [] 347 | const orientation = (this.figure.width > this.figure.height) ? "Landscape" : "Portrait" 348 | this.append("%!PS-Adobe-2.0") 349 | .append("%%Creator: Constrain PostScript Renderer") 350 | .append("%%Pages: 1") 351 | .append("%%PageOrder: Ascend") 352 | .append("%%Orientation: " + orientation) 353 | .append(`%%BoundingBox: 0 0 ${Math.round(figure.width)} ${Math.round(figure.height)}`) 354 | .append(`%%HiResBoundingBox: 0 0 ${figure.width} ${(figure.height)}`) 355 | .append(`%%PageSize: 0 0 ${Math.round(figure.width)} ${Math.round(figure.height)}`) 356 | .append("%%EndComments") 357 | .append("%%BeginProlog") 358 | .append("%%EndProlog") 359 | .append("%%BeginSetup") 360 | .append(`<< /PageSize [${figure.width} ${figure.height}] >> setpagedevice`) 361 | .append("%%EndSetup") 362 | .append("%%Page: 1 1") 363 | .append("/Helvetica findfont 12 scalefont setfont") 364 | this.activeFillStyle = null 365 | this.activeStrokeStyle = null 366 | this.activeLineWidth = null 367 | this.activeFont = null 368 | this.fillStyle = "black" 369 | this.strokeStyle = "black" 370 | this.lineWidth = 1 371 | this.printMedia = true 372 | // figure.font.setContextFont(this) 373 | } 374 | 375 | append(s) { 376 | this.output.push(s) 377 | this.output.push("\n") 378 | return this 379 | } 380 | updateLineWidth() { 381 | if (this.lineWidth != this.activeLineWidth) { 382 | this.activeLineWidth = this.lineWidth 383 | this.append(`${this.lineWidth} setlinewidth`) 384 | } 385 | } 386 | updateFillStyle() { 387 | if (this.fillStyle != this.activeFillStyle) { 388 | this.activeFillStyle = this.fillStyle 389 | this.activeStrokeStyle = this.fillStyle 390 | const [r, g, b] = colorToRGB(this.fillStyle) 391 | this.append(`${mround(r/255)} ${mround(g/255)} ${mround(b/255)} setrgbcolor`) 392 | } 393 | } 394 | updateStrokeStyle() { 395 | if (this.strokeStyle && this.strokeStyle != this.activeStrokeStyle) { 396 | this.activeFillStyle = this.strokeStyle 397 | this.activeStrokeStyle = this.strokeStyle 398 | const [r, g, b] = colorToRGB(this.strokeStyle) 399 | this.append(`${mround(r/255)} ${mround(g/255)} ${mround(b/255)} setrgbcolor`) 400 | } 401 | } 402 | parseFont() { 403 | let fontname = this.font, style = null, ignore, size 404 | let nostyle = fontname.match("^([1-9][.0-9]*)px ([A-Za-z- ]+(, [A-Za-z- ]+)*)$") 405 | if (nostyle) { 406 | [ignore, size, fontname] = nostyle 407 | } else { 408 | let with_style = fontname.match("^([A-Za-z]+) ([1-9][.0-9]*)px ([A-Z][A-Za-z- ]*)$") 409 | if (!with_style) return; 410 | [ignore, style, size, fontname] = with_style 411 | } 412 | style = mapStyle(fontname, style) 413 | return [fontname, size, style] 414 | } 415 | updateFont() { 416 | if (this.font && this.font != this.activeFont) { 417 | this.activeFont = this.font 418 | let [fontname, size, style] = this.parseFont() 419 | const fontnames = fontname.split(/, */) 420 | fontname = mapFontName(fontnames).replace(" ", "") 421 | if (style) { 422 | fontname += "-" + style 423 | } 424 | this.append(`/${fontname} findfont ${size} scalefont setfont`) 425 | } 426 | } 427 | 428 | pt(x, y) { 429 | return `${mround(x)} ${mround(this.figure.height - y)}` 430 | } 431 | 432 | setTransform() { 433 | } 434 | clearRect(x, y, w, h) { 435 | this.append("1 setgray") 436 | .append(`${this.pt(x,y+h)} ${w} ${h} rectfill`) 437 | .append("0 setgray") 438 | } 439 | save() { 440 | this.append("gsave") 441 | } 442 | restore() { 443 | this.append("grestore") 444 | this.activeStrokeStyle = null 445 | this.activeFillStyle = null 446 | this.activeLineWidth = null 447 | } 448 | translate(x, y) { 449 | this.append(`${mround(x)} ${mround(-y)} translate`) 450 | } 451 | beginPath() { 452 | this.append("newpath") 453 | } 454 | closePath() { 455 | this.append("closepath") 456 | } 457 | moveTo(x, y) { 458 | this.append(`${this.pt(x,y)} moveto`) 459 | } 460 | lineTo(x, y) { 461 | this.append(`${this.pt(x,y)} lineto`) 462 | } 463 | stroke() { 464 | this.updateStrokeStyle() 465 | this.updateLineWidth() 466 | this.append("gsave stroke grestore") 467 | } 468 | fill() { 469 | this.updateFillStyle() 470 | this.append("gsave fill grestore") 471 | } 472 | setLineDash(s) { 473 | this.append("[") 474 | for (let i = 0; i < s.length; i++) { 475 | this.append(` ${s[i]}`) 476 | } 477 | this.append(" ] 0 setdash") 478 | } 479 | measureText(s) { 480 | this.ctx2d.font = this.font 481 | return this.ctx2d.measureText(s) 482 | } 483 | fillText(s, x, y) { 484 | function isASCII(c) { 485 | return (c >= 32 && c < 127); 486 | } 487 | this.append(`${this.pt(x,y)} moveto`) 488 | this.updateFillStyle() 489 | let i = 0, j = i 490 | while (i < s.length) { 491 | for (j = i; j < s.length; j++) { 492 | if (!isASCII(s.charCodeAt(j))) break 493 | } 494 | if (j > i) { 495 | this.updateFont() 496 | this.append(`${PSquote(s.substring(i,j))} show`) 497 | } 498 | i = j 499 | const save_font = this.font 500 | for (j = i; j < s.length; j++) { 501 | if (isASCII(s.charCodeAt(j))) break 502 | let [fontname, size, style] = this.parseFont() 503 | this.font = `${size}px Symbol` 504 | this.updateFont() 505 | let mapped = UnicodeToSymbol[s.charCodeAt(j)] 506 | if (!mapped) mapped = 32; 507 | this.append(`<00${mapped.toString(16)}> show`) 508 | } 509 | if (j > i) { 510 | this.font = save_font 511 | i = j 512 | } 513 | } 514 | } 515 | rotate(r) { 516 | if (r != 0) { 517 | this.append(`0 ${this.figure.height} translate`) 518 | .append(`${mround(r * -57.29578)} rotate`) 519 | .append(`0 ${-this.figure.height} translate`) 520 | } 521 | } 522 | bezierCurveTo(x1, y1, x2, y2, x3, y3) { 523 | this.append(`${this.pt(x1,y1)} ${this.pt(x2,y2)} ${this.pt(x3,y3)} curveto`) 524 | } 525 | fillRect(x, y, w, h) { 526 | this.append(`${this.pt(x,y-h)} ${w} ${h} rectfill`) 527 | } 528 | rect(x, y, w, h) { 529 | this.append(`${this.pt(x,y)} moveto ${w} 0 rlineto ` + 530 | `${0} ${-h} rlineto ${-w} 0 rlineto closepath`) 531 | } 532 | clip(rule) { 533 | if (rule == "evenodd") { 534 | this.append('eoclip') 535 | } else { 536 | this.append('clip') 537 | } 538 | } 539 | getOutput() { 540 | return this.output.join("") + "\nshowpage\n%%EOF\n" 541 | } 542 | } 543 | 544 | // export string data as file of type ty. 545 | function exportData(data, filename, ty) { 546 | var blob = new Blob([data], {type: ty }); 547 | if (window.navigator.msSaveOrOpenBlob) { 548 | window.navigator.msSaveBlob(blob, filename); 549 | } else { 550 | var elem = document.createElement("a") 551 | elem.href = window.URL.createObjectURL(blob) 552 | elem.download = filename 553 | document.body.appendChild(elem); 554 | elem.click(); 555 | document.body.removeChild(elem); 556 | } 557 | } 558 | 559 | Constrain.Figure.prototype.printButton = function() { 560 | return new Constrain.PS.PrintButton(this) 561 | } 562 | 563 | return { print, PrintButton, PrintContext, colorTable } 564 | }() 565 | -------------------------------------------------------------------------------- /constrain-reveal.js: -------------------------------------------------------------------------------- 1 | // Figures can be fragments or regular components. 2 | // In the latter case, the frames of the figure happen 3 | // with the Reveal fragments. 4 | // In the former case, the entire figure happens between 5 | // Reveal fragments. 6 | var ConstrainReveal = function() { 7 | 8 | let currentFigure = null, slideFigures = [] 9 | 10 | const Figures = Constrain.Figures 11 | 12 | function newSlideHook(e) { 13 | // console.log("new slide hook: " + e.type) 14 | currentFigure = null 15 | const slide = Reveal.getCurrentSlide() 16 | canvases = slide.querySelectorAll('canvas') 17 | slideFigures.forEach(f => f.endCurrentFrame()) 18 | slideFigures = [] 19 | for (let i = 0; i < canvases.length; i++) { 20 | for (let j = 0; j < Figures.length; j++) { 21 | if (Figures[j].canvas == canvases[i]) { 22 | slideFigures.push(Figures[j]) 23 | Figures[j].start() 24 | } 25 | } 26 | } 27 | // console.log(Figures.length + " figures") 28 | // console.log(slideFigures.length + " figures on this slide") 29 | } 30 | 31 | function hasClass(elem, cls) { 32 | return elem.classList.contains(cls) 33 | } 34 | 35 | function fragmentFigure(fig) { 36 | return hasClass(fig.canvas, "fragment") 37 | } 38 | 39 | // Whether a figure is ready to be advanced 40 | function activeFigure(fig) { 41 | const ff = fragmentFigure(fig) 42 | if (!ff && !fig.isComplete()) return true 43 | if (ff && hasClass(fig.canvas, "current-fragment") && !fig.isComplete()) return true 44 | return false 45 | } 46 | 47 | // Whether a figure is ready to be rewound (must be visible and not on first frame) 48 | function rewindableFigure(fig) { 49 | const ff = fragmentFigure(fig) 50 | if (!ff && !fig.isReset()) return true 51 | if (ff && hasClass(fig.canvas, "current-fragment") && !fig.isReset()) return true 52 | return false 53 | } 54 | 55 | // Whether some fragment is ready to be advanced 56 | function activeFragment() { 57 | let r = false 58 | slideFigures.forEach(fig => { 59 | const active = (activeFigure(fig) && fragmentFigure(fig)) 60 | if (active) { console.log("a figure is active: "); console.log(fig) } 61 | r = r || active 62 | }) 63 | return r 64 | } 65 | 66 | // Advance all figures in active fragments. 67 | // Return true if any figure advanced. 68 | function advanceActiveFragments() { 69 | // console.log("advancing active fragments") 70 | let sawFragment = false, advanced = false 71 | slideFigures.forEach(f => { 72 | if (fragmentFigure(f) && activeFigure(f)) { 73 | if (f.advance()) advanced = true 74 | console.log("advanced to frame " + f.currentFrame.index) 75 | } 76 | }) 77 | return advanced 78 | } 79 | 80 | function advanceNonFragments() { 81 | slideFigures.forEach(f => { 82 | if (!fragmentFigure(f) && !f.isComplete()) { 83 | f.advance() 84 | } 85 | }) 86 | } 87 | 88 | function rewindNonFragments() { 89 | slideFigures.forEach(f => { 90 | if (!fragmentFigure(f) && !f.isReset()) { 91 | f.rewind() 92 | } 93 | }) 94 | } 95 | 96 | function completeNonFragments() { 97 | slideFigures.forEach(f => { 98 | if (!fragmentFigure(f) && !f.isComplete()) { 99 | f.reset() 100 | f.complete() 101 | } 102 | }) 103 | } 104 | 105 | function rewindableFragment() { 106 | let r = false 107 | slideFigures.forEach(fig => { 108 | const cond = (rewindableFigure(fig) && fragmentFigure(fig)) 109 | if (cond) { console.log("a figure can be rewound: "); console.log(fig) } 110 | r = r || cond 111 | }) 112 | return r 113 | } 114 | 115 | // Rewind all figures in active fragments. 116 | // Return true if any figure rewound. 117 | function rewindActiveFragments() { 118 | let rewound = false 119 | slideFigures.forEach(f => { 120 | if (fragmentFigure(f) && rewindableFigure(f)) { 121 | if (f.rewind()) rewound = true 122 | console.log("rewound to frame " + f.currentFrame.index) 123 | } 124 | }) 125 | return rewound 126 | } 127 | 128 | function completeActiveFragments() { 129 | slideFigures.forEach(f => {if (fragmentFigure(f) && activeFigure(f)) f.complete()}) 130 | } 131 | 132 | function resetActiveFragments() { 133 | slideFigures.forEach(f => {if (fragmentFigure(f) && rewindableFigure(f)) f.reset()}) 134 | } 135 | 136 | return { 137 | initialize: function(dir) { 138 | if (!dir) dir = '.' 139 | Reveal.initialize({ 140 | history: true, 141 | controls: false, 142 | progress: false, 143 | center: false, 144 | margin: 0, 145 | 146 | 147 | // More info https://github.com/hakimel/reveal.js#dependencies 148 | dependencies: [ 149 | { src: dir + '/reveal.js/plugin/markdown/marked.js' }, 150 | { src: dir + '/reveal.js/plugin/markdown/markdown.js' }, 151 | { src: dir + '/reveal.js/plugin/notes/notes.js', async: true } 152 | ], 153 | 154 | keyboard: { 155 | // override N and P to jump by whole slides 156 | 78: function() { 157 | if (!Reveal.isLastSlide()) 158 | Reveal.slide(Reveal.getIndices().h + 1, 159 | Reveal.getIndices().v, 0); 160 | }, 161 | 80: function() { 162 | if (!Reveal.isFirstSlide()) 163 | Reveal.slide(Reveal.getIndices().h - 1, 164 | Reveal.getIndices().v, 0); 165 | } 166 | }, 167 | width: 1024, 168 | height: 768 169 | }); 170 | 171 | Reveal.addEventListener( 'slidechanged', newSlideHook); 172 | Reveal.addEventListener( 'ready', newSlideHook); 173 | 174 | const reveal_next = Reveal.navigateNext, 175 | reveal_right = Reveal.navigateRight, 176 | reveal_left = Reveal.navigateLeft, 177 | reveal_prev = Reveal.navigatePrev 178 | 179 | Reveal.navigateNext = function() { 180 | if (!activeFragment()) { 181 | completeActiveFragments() 182 | advanceNonFragments() 183 | reveal_next.call(this) 184 | } else { 185 | if (!advanceActiveFragments()) reveal_next.call(this) 186 | } 187 | } 188 | Reveal.navigatePrev = function() { 189 | if (!rewindableFragment()) { 190 | reveal_prev.call(this) 191 | } else { 192 | if (!rewindActiveFragments()) reveal_prev.call(this) 193 | } 194 | } 195 | Reveal.navigateRight = Reveal.navigateNext 196 | Reveal.navigateLeft = Reveal.navigatePrev 197 | 198 | Constrain.autoResize() 199 | } 200 | } 201 | }() 202 | -------------------------------------------------------------------------------- /constrain-slide.css: -------------------------------------------------------------------------------- 1 | span.footer { font-family: Copperplate; position: fixed; bottom: 0ex; left: 1ex; font-size: 18px; z-index: 2 } 2 | em { color: #889ee5 } 3 | canvas { position: fixed; left: 0; top: 0; 4 | width: 100%; height: 100%; z-index: -1 } 5 | /* #fig1 { background-color: #eef; } */ 6 | section {width: 1024px; height:768px} 7 | kbd {font-size: 90%; color: #468} 8 | pre {background-color: #efd; color: #236} 9 | .slides {background-color: white} 10 | -------------------------------------------------------------------------------- /constrain.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/constrain.js -------------------------------------------------------------------------------- /doc/SRC-RR-131A.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/doc/SRC-RR-131A.pdf -------------------------------------------------------------------------------- /examples/andru.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Solarized Light theme for reveal.js. 3 | * Author: Achim Staebler 4 | * modified by Andrew Myers 5 | */ 6 | 7 | html * { 8 | color-profile: sRGB; 9 | rendering-intent: auto; } 10 | 11 | /********************************************* 12 | * GLOBAL STYLES 13 | *********************************************/ 14 | body { 15 | background: #f8f8f8; 16 | background-color: #f8f8f8; } 17 | 18 | .reveal { 19 | font-family: "Cronos Pro", sans-serif; 20 | font-size: 32px; 21 | font-weight: normal; 22 | color: #444; } 23 | 24 | ::selection { 25 | color: #fff; 26 | background: #d33682; 27 | text-shadow: none; } 28 | 29 | .reveal .slides > section, 30 | .reveal .slides > section > section { 31 | line-height: 1.3; 32 | font-weight: inherit; } 33 | 34 | /********************************************* 35 | * HEADERS 36 | *********************************************/ 37 | .reveal h1, 38 | .reveal h2, 39 | .reveal h3, 40 | .reveal h4, 41 | .reveal h5, 42 | .reveal h6 { 43 | margin: 0 0 20px 0; 44 | color: #485ea5; 45 | font-family: "Cronos Pro", "Open Sans", sans-serif; 46 | font-weight: bold; 47 | line-height: 1.2; 48 | letter-spacing: normal; 49 | text-transform: none; 50 | text-shadow: none; 51 | word-wrap: break-word; } 52 | 53 | .reveal h1 { 54 | font-size: 3.00em; } 55 | 56 | .reveal h2 { 57 | font-size: 2.11em; } 58 | 59 | .reveal h3 { 60 | font-size: 1.55em; } 61 | 62 | .reveal h4 { 63 | font-size: 1em; } 64 | 65 | .reveal h1 { 66 | text-shadow: none; } 67 | 68 | /********************************************* 69 | * OTHER 70 | *********************************************/ 71 | .reveal p { 72 | margin: 20px 0; 73 | line-height: 1.3; } 74 | 75 | /* Ensure certain elements are never larger than the slide itself */ 76 | .reveal img, 77 | .reveal video, 78 | .reveal iframe { 79 | max-width: 95%; 80 | max-height: 95%; } 81 | 82 | .reveal strong, 83 | .reveal b { 84 | font-weight: bold; } 85 | 86 | .reveal em { 87 | font-style: italic; } 88 | 89 | .reveal ol, 90 | .reveal dl, 91 | .reveal ul { 92 | display: inline-block; 93 | text-align: left; 94 | margin: 0 0 0 1em; } 95 | 96 | .reveal ol { 97 | list-style-type: decimal; } 98 | 99 | .reveal ul { 100 | list-style-type: disc; } 101 | 102 | .reveal ul ul { 103 | list-style-type: square; } 104 | 105 | .reveal ul ul ul { 106 | list-style-type: circle; } 107 | 108 | .reveal ul ul, 109 | .reveal ul ol, 110 | .reveal ol ol, 111 | .reveal ol ul { 112 | display: block; 113 | margin-left: 40px; } 114 | 115 | .reveal dt { 116 | font-weight: bold; } 117 | 118 | .reveal dd { 119 | margin-left: 40px; } 120 | 121 | .reveal q, 122 | .reveal blockquote { 123 | quotes: none; } 124 | 125 | .reveal blockquote { 126 | display: block; 127 | position: relative; 128 | width: 70%; 129 | margin: 20px auto; 130 | padding: 5px; 131 | font-style: italic; 132 | background: rgba(255, 255, 255, 0.05); 133 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } 134 | 135 | .reveal blockquote p:first-child, 136 | .reveal blockquote p:last-child { 137 | display: inline-block; } 138 | 139 | .reveal q { 140 | font-style: italic; } 141 | 142 | .reveal pre { 143 | display: block; 144 | position: relative; 145 | width: 90%; 146 | margin: 20px auto; 147 | text-align: left; 148 | font-size: 0.55em; 149 | font-family: monospace; 150 | line-height: 1.2em; 151 | word-wrap: break-word; 152 | box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } 153 | 154 | .reveal code { 155 | font-family: monospace; } 156 | 157 | .reveal pre code { 158 | display: block; 159 | padding: 5px; 160 | overflow: auto; 161 | max-height: 400px; 162 | word-wrap: normal; } 163 | 164 | .reveal table { 165 | margin: auto; 166 | border-collapse: collapse; 167 | border-spacing: 0; } 168 | 169 | .reveal table th { 170 | font-weight: bold; } 171 | 172 | .reveal table th, 173 | .reveal table td { 174 | text-align: left; 175 | padding: 0.2em 0.5em 0.2em 0.5em; 176 | border-bottom: 1px solid; } 177 | 178 | .reveal table th[align="center"], 179 | .reveal table td[align="center"] { 180 | text-align: center; } 181 | 182 | .reveal table th[align="right"], 183 | .reveal table td[align="right"] { 184 | text-align: right; } 185 | 186 | .reveal table tbody tr:last-child th, 187 | .reveal table tbody tr:last-child td { 188 | border-bottom: none; } 189 | 190 | .reveal sup { 191 | vertical-align: super; } 192 | 193 | .reveal sub { 194 | vertical-align: sub; } 195 | 196 | .reveal small { 197 | display: inline-block; 198 | font-size: 0.6em; 199 | line-height: 1.2em; 200 | vertical-align: top; } 201 | 202 | .reveal small * { 203 | vertical-align: top; } 204 | 205 | /********************************************* 206 | * LINKS 207 | *********************************************/ 208 | .reveal a { 209 | color: #268bd2; 210 | text-decoration: none; 211 | -webkit-transition: color .15s ease; 212 | -moz-transition: color .15s ease; 213 | transition: color .15s ease; } 214 | 215 | .reveal a:hover { 216 | color: #78b9e6; 217 | text-shadow: none; 218 | border: none; } 219 | 220 | .reveal .roll span:after { 221 | color: #fff; 222 | background: #1a6091; } 223 | 224 | /********************************************* 225 | * IMAGES 226 | *********************************************/ 227 | .reveal section img { 228 | margin: 15px 0px; 229 | background: rgba(255, 255, 255, 0.12); 230 | border: 4px solid #657b83; 231 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } 232 | 233 | .reveal section img.plain { 234 | border: 0; 235 | box-shadow: none; } 236 | 237 | .reveal a img { 238 | -webkit-transition: all .15s linear; 239 | -moz-transition: all .15s linear; 240 | transition: all .15s linear; } 241 | 242 | .reveal a:hover img { 243 | background: rgba(255, 255, 255, 0.2); 244 | border-color: #268bd2; 245 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } 246 | 247 | /********************************************* 248 | * NAVIGATION CONTROLS 249 | *********************************************/ 250 | .reveal .controls .navigate-left, 251 | .reveal .controls .navigate-left.enabled { 252 | border-right-color: #268bd2; } 253 | 254 | .reveal .controls .navigate-right, 255 | .reveal .controls .navigate-right.enabled { 256 | border-left-color: #268bd2; } 257 | 258 | .reveal .controls .navigate-up, 259 | .reveal .controls .navigate-up.enabled { 260 | border-bottom-color: #268bd2; } 261 | 262 | .reveal .controls .navigate-down, 263 | .reveal .controls .navigate-down.enabled { 264 | border-top-color: #268bd2; } 265 | 266 | .reveal .controls .navigate-left.enabled:hover { 267 | border-right-color: #78b9e6; } 268 | 269 | .reveal .controls .navigate-right.enabled:hover { 270 | border-left-color: #78b9e6; } 271 | 272 | .reveal .controls .navigate-up.enabled:hover { 273 | border-bottom-color: #78b9e6; } 274 | 275 | .reveal .controls .navigate-down.enabled:hover { 276 | border-top-color: #78b9e6; } 277 | 278 | /********************************************* 279 | * PROGRESS BAR 280 | *********************************************/ 281 | .reveal .progress { 282 | background: rgba(0, 0, 0, 0.2); } 283 | 284 | .reveal .progress span { 285 | background: #268bd2; 286 | -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 287 | -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 288 | transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } 289 | -------------------------------------------------------------------------------- /examples/audio/mixkit-ethereal-fairy-win-sound-2019.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/examples/audio/mixkit-ethereal-fairy-win-sound-2019.wav -------------------------------------------------------------------------------- /examples/audio/slide.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/examples/audio/slide.m4a -------------------------------------------------------------------------------- /examples/audio/ssh.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/examples/audio/ssh.m4a -------------------------------------------------------------------------------- /examples/constrain-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 |

Constrain demo (this should be hidden)

16 | 17 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/dragon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/dragon.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementsByTagName("canvas")[0] 2 | 3 | Constrain.fullWindowCanvas(canvas) 4 | 5 | const figure = new Constrain.Figure(canvas) 6 | 7 | with (figure) { 8 | const m = margin() 9 | const max_levels = 15 10 | 11 | function dragon_points(p0, p1, level, sign) { 12 | if (level == 0) return [p0] 13 | const p2 = relative(0.5, 0.5 * sign, p0, p1) 14 | let result = dragon_points(p0, p2, level-1, 1) 15 | result = result.concat(dragon_points(p2, p1, level-1, -1)) 16 | return result 17 | } 18 | 19 | const ll = m.ll(), lr = m.lr() 20 | const p0 = point(), p1 = point() 21 | const w = 0.7 22 | equal(p0.y(), p1.y(), m.y()) 23 | equal(p0.x(), plus(times(w, ll.x()), times(1 - w, lr.x()))) 24 | equal(p1.x(), plus(times(1 - w, ll.x()), times(w, lr.x()))) 25 | 26 | for (let level = max_levels - 1; level >= 0; level--) { 27 | const pts = dragon_points(p0, p1, level, -1) 28 | const opacity = 0.5 + 0.5/(max_levels - level) 29 | const f = 255*(max_levels - level)/max_levels 30 | const strokeStyle = (level == max_levels - 1) 31 | ? '#fff' 32 | : `rgb(${f}, ${f}, ${255-f/2})` 33 | const c = connector(p0, ...pts, p1, p1) 34 | .setOpacity(opacity) 35 | .setLineWidth(times(0.05, distance(p0,p1), Math.pow(0.7071, level))) 36 | .setStrokeStyle(strokeStyle) 37 | } 38 | } 39 | 40 | Constrain.autoResize() 41 | Constrain.setupTouchListeners() 42 | figure.start() 43 | -------------------------------------------------------------------------------- /examples/dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/examples/dragon.png -------------------------------------------------------------------------------- /examples/example-figure.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementsByTagName("canvas")[0] 2 | 3 | Constrain.fullWindowCanvas(canvas) 4 | 5 | const figure = new Constrain.Figure(canvas) 6 | 7 | with (figure) { 8 | var border = rectangle("blue", "white") 9 | 10 | align("center", "center", border, canvasRect()) 11 | border.addText("Constrain") 12 | .setFontName("Helvetica, Arial") 13 | .setTextStyle("white") 14 | .setFontSize(36) 15 | border.setH(150) 16 | border.setW(300) 17 | 18 | } 19 | Constrain.autoResize() 20 | figure.start() 21 | -------------------------------------------------------------------------------- /examples/fig2c.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 71 | 72 |

73 | You will need the Linux Libertine font on your system to see this figure 74 | properly in the browser. Drag the diamond handle to reposition figure 75 | elements. 76 |

77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/gate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 |

Gate design

13 | 14 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /examples/ll_lr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 22 | 23 | 24 | 25 | 26 |
27 |

Animated tree demo

28 |

29 | There are four cases for rebalancing in the AVL tree data structure. Shown 30 | are the two cases in which insertion causes a node (z) to become unbalanced because 31 | the left subtree becomes too tall; the cases when the right 32 | subtree becomes too tall are symmetrical. By 33 | clicking on the button, you can see the rebalancing that is performed in each case. 34 | In the LL case, a single tree rotation is used; in the LR case, there are two tree rotations. 35 |

36 | 37 | 38 | 110 |

111 |

112 | This diagram is computed in real time in JavaScript, using 113 | Constrain, 114 | a constraint-based framework for creating animated diagrams. 115 |

116 |
117 |
118 |
119 | 
120 | 124 | -------------------------------------------------------------------------------- /examples/loyd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 |

12 | This is the well-known 15-puzzle, designed by Samuel Loyd. Most tiles 13 | are already in the right place. Click on tiles to move them and 14 | finish the job. 15 |

16 |
17 | 18 | 19 | 22 | 26 | 111 |
112 |

113 | Built with Constrain 114 |

115 |
116 |
117 | 
118 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /examples/pythagoras.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 19 | 20 | 21 |

Interactive Pythagorean Theorem

22 | 23 |
24 | 25 |

26 | The Pythagorean Theorem says that for any right triangle 27 | with perpendicular sides \(a\) and \(b\), and long side (hypoteneuse) \(c\), 28 | the equation \(a^2 + b^2 = c^2\) holds. We can see that this is true by 29 | considering the following diagram. You can drag the diamond to change the shape 30 | of the right triangles shown in yellow. 31 |

32 | 33 | 34 | 35 |

36 | The big squares have the same size and the 4 right triangles that each big square 37 | contains also have exactly the same size, so the remaining space in each big square 38 | must have the same area. That means the areas of the pink square (\(a^2\)) and the green square 39 | (\(b^2\)) must add up to the area of the blue square (\(c^2\)). 40 |

41 | 42 |

Built using Constrain

43 | 44 | 137 |
138 | 139 | 140 | -------------------------------------------------------------------------------- /examples/reveal-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Constrain/Reveal Integration Demo 8 | 9 | 10 | 11 | 12 | 14 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | Constrain/Reveal integration 32 |
33 |
34 |

Constrain: A Constraint-Based System for Animated Figures

35 |
36 |
37 |

Constrain

38 |
    39 |
  • JavaScript/ES package for animated figures
  • 40 |
  • Embeds figures into regular web pages
  • 41 |
  • Integrates into Reveal presentations with Constrain/Reveal
  • 42 |
  • Declarative constraint-based layout
  • 43 |
  • Figure can have multiple frames
  • 44 |
  • Graphical content can constrain against HTML content
  • 45 |
46 | 47 | 81 |
82 |
83 |

Rendering capabilities

84 |
    85 |
  • Several graphical objects are supported: 86 | rectangles, lines, circles 87 |
  • Automatic connectors support diagrams
  • 88 |
  • Full power of JavaScript 2D canvas rendering
  • 89 |
  • Constraint-based ⇒ responsive to display changes, font availability
  • 90 | 91 |
92 | 93 | 142 |
143 |
144 |

Extensible

145 |
    146 |
  • Constrain is a JavaScript (ES6) library 147 |
  • General-purpose constraint solver ⇒ can add/modify graphical objects 148 |
149 | 150 | 215 |
216 |

Automatic Graph Layout

217 | 218 | 241 |
242 |

Programming with Constrain

243 |
    244 |
  • Web page can contain multiple Figures, each tied to a canvas.
    245 |
    <canvas id=canv1></canvas>
    246 | fig = new Figure("canv1")
    247 |
  • 248 |
  • Graphical objects created declaratively using builder pattern:
    249 |
    250 | with (fig) {
    251 |   s = square().setFillStyle("yellow").setLineWidth(3)
    252 |
  • Graphical objects introduce variables for their position: 253 |
      254 |
    • .x() : x position (min/max: .x0(), .x1()
    • 255 |
    • .y() : y position (min/max: .y0(), .y1()
    • 256 |
    • .w() : width, .h() : height
    • 257 |
    258 |
  • 259 |
  • Variable values are determined by solving constraints: 260 |
    261 |   equal(s.w(), 150)    // w = 150
    262 |   equal(s.x0(), s.w()) // x0 = w
    263 |   equal(s.y0(), times(2, s.h())) // y0 = 2*h
    264 |
  • 265 | 266 | 267 | 291 |
292 |
293 |

More information

294 | 295 | 302 |
303 |
304 | 305 | 306 | 307 | 308 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /examples/spiral.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/spiral.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementsByTagName("canvas")[0] 2 | 3 | Constrain.fullWindowCanvas(canvas) 4 | 5 | const figure = new Constrain.Figure(canvas) 6 | 7 | // Bezier constant for a circular arc: (4/3) tan(theta/4) 8 | function circularArcConstant(degrees) { 9 | return (4/3) * Math.tan(degrees * Math.PI / 180 / 4) 10 | } 11 | 12 | const bezier_k = circularArcConstant(90) 13 | 14 | with (figure) { 15 | var border = rectangle("gray", "white") 16 | h = handle("yellow", 300, 300), 17 | m = margin() 18 | 19 | equal(border, m) 20 | align("left", "top", h, border) 21 | 22 | class SpiralPiece extends Constrain.Square { 23 | constructor(figure, style, side) { 24 | super(figure, style, "white", 1, 300, 300, 300) 25 | this.side = side 26 | } 27 | render() { 28 | super.render() 29 | let [x0, x1, y0, y1] = Constrain.evaluate([this.x0(), this.x1(), this.y0(), this.y1()]) 30 | let ax = x0, ay = y0, bx = x0, by = y1, cx = x1, cy = y1, dx = x1, dy = y0, s = this.side 31 | while (s--) { 32 | const tx = ax, ty = ay 33 | ax = bx; ay = by; bx = cx; by = cy; cx = dx; cy = dy; dx = tx; dy = ty 34 | } 35 | const ctx = this.figure.ctx, f = 0.8, 36 | k2 = bezier_k*f, k1 = 1 - k2, 37 | k4 = bezier_k/f, k3 = 1 - k4 38 | ctx.beginPath() 39 | ctx.moveTo(ax, ay) 40 | ctx.bezierCurveTo(ax*k1 + bx*k2, ay*k1 + by*k2, 41 | cx*k3 + bx*k4, cy*k3 + by*k4, 42 | cx, cy) 43 | ctx.strokeStyle = "yellow" 44 | ctx.lineWidth = scale*Math.sqrt(x1-x0)/5 45 | ctx.stroke() 46 | } 47 | } 48 | var leftE = border.x0(), rightE = border.x1(), topE = border.y0(), bottomE = border.y1(), steps = 16 49 | for (let i = 0; i < steps; i++) { 50 | let b2 = new SpiralPiece(figure, Constrain.rgbStyle(Math.sqrt((i+1)/steps)*255,0,0), i%4) 51 | let le = leftE, re = rightE, te = topE, be = bottomE 52 | 53 | if (i%4 == 0) le = b2.x1() 54 | else equal(b2.x1(), rightE) 55 | if (i%4 == 1) be = b2.y0() 56 | else equal(b2.y0(), topE) 57 | if (i%4 == 2) re = b2.x0() 58 | else equal(b2.x0(), leftE) 59 | if (i%4 == 3) te = b2.y1() 60 | else equal(b2.y1(), bottomE) 61 | leftE = le 62 | rightE = re 63 | topE = te 64 | bottomE = be 65 | } 66 | 67 | let c = nearZero(minus(border.w(), canvasRect().x())) 68 | let success = updateValuation() 69 | 70 | let mw = border.w().solutionValue, mh = border.h().solutionValue 71 | let phi = mw/mh 72 | t = label("The Golden Ratio is approximately " + phi, 30, "Palatino", "yellow") 73 | align("center", "none", t, border) 74 | align("none", "top", t, m) 75 | 76 | figure.removeConstraints(c) 77 | } 78 | Constrain.autoResize() 79 | Constrain.setupTouchListeners() 80 | figure.start() 81 | -------------------------------------------------------------------------------- /examples/talk.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | A Short talk about Constrain 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | Constrain: Constraint-Based Animated Figures 30 |
31 |
32 |

Constrain:

Constraint-Based Animated Figures
33 | Andrew Myers

34 | 35 | 36 | 37 | 133 |
134 |
135 |

Constrain

136 | 137 | 138 | 139 | 181 | 182 |
183 |
184 |

Constrain

185 |
    186 |
  • Declarative constraint-based description
  • 187 |
  • Figure can have multiple key frames
  • 188 |
  • Attributes can be animated within a frame
  • 189 |
  • Graphical content can constrain against HTML content
  • 190 |
191 | 192 | 226 |
227 |
228 |

Rendering capabilities

229 |
    230 |
  • Standard graphical objects: 231 | rectangles, lines, circles 232 |
  • Automatic connectors support diagrams
  • 233 |
  • Automatic text layout
  • 234 |
  • Constraint-based ⇒ responsive to display changes, font availability
  • 235 | 236 |
237 | 238 | 301 |
302 |

Programming with Constrain

303 |
    304 |
  • Web page can hold multiple Figures, each tied to a canvas.
    305 |
    <canvas id=canv1></canvas>
    306 | fig = new Figure("canv1")
    307 |
  • 308 |
  • Graphical objects created declaratively using builder pattern:
    309 |
    310 | with (fig) {
    311 |   s = square().setFillStyle("yellow").setLineWidth(3)
    312 |
  • Graphical objects introduce variables for their position: 313 |
      314 |
    • .x() : x position (min/max: .x0(), .x1()
    • 315 |
    • .y() : y position (min/max: .y0(), .y1()
    • 316 |
    • .w() : width, .h() : height
    • 317 |
    318 |
  • 319 |
  • Variable values determined by solving constraints: 320 |
    321 |   equal(s.w(), 150)    // w = 150
    322 |   equal(s.x0(), s.w()) // x0 = w
    323 |   equal(s.y0(), times(2, s.h())) // y0 = 2*h
    324 |
  • 325 |
  • Convenience methods for alignment constraints, etc. 326 |
    327 |   let t = ellipse().setFillStyle("#8f8")
    328 |   align("abut", "top bottom", s, hspace(100), t)
    329 |   align("right", "none", t, canvasRect()) // t.x1 == canvas.x1
    330 |             
  • 331 | 332 | 333 | 363 |
364 |
365 |

Examples

366 | 372 |
373 |

Automatic Graph Layout

374 | 375 | 398 |
399 |
400 |

Extensible

401 |
    402 |
  • Full power of JavaScript 2D canvas rendering
  • 403 |
  • Can add/modify graphical objects 404 |
405 | 406 | 472 |
473 |
474 |

Implementation

475 |
    476 |
  • Everything done online in JavaScript (~6k LoC ECMAScript 6)
  • 477 |
  • Constraint declarations generate a cost function 478 |
      479 |
    • Less fragile for overconstrained problems 480 |
    • Can optimize placement with costs 481 |
  • 482 |
  • Constraints “solved” by minimizing cost 483 |
      484 |
    • Derivatives computed with backpropagation 485 |
    • Gradient descent is way too slow! 486 |
    • Uses second-order algorithm: BFGS 487 |
    • Constraints automatically partitioned into 488 | components, simplified using substitution 489 |
    490 |
  • Animations solved incrementally from previous solution, with 491 | adaptive tweening to increase frame rate
  • 492 |
  • Text layout: dynamic programming ala TeX
  • 493 |
494 |
495 |

For more information

496 | 497 | 504 | 505 | 570 |
571 |
572 | 573 | 574 | 575 | 579 | 580 | 581 | -------------------------------------------------------------------------------- /examples/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/text-format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 |

Text Layout Algorithms in Constrain 18 | (drag the red diamond!) 19 |

20 |

21 | Explanation: TeX uses dynamic programming to minimize the sum of cubes 22 | of unused space on each line. Constrain does this minimization in real time 23 | inside an HTML canvas. It doesn't implement all the features of TeX, like allowing 24 | overfull boxes or automatic hyphenation. 25 |

26 | 27 | 69 | 70 | 74 | 75 | -------------------------------------------------------------------------------- /examples/triangles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 |

Triangle demo (this should be hidden)

16 | 17 | 18 | 19 | 20 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/favicon.ico -------------------------------------------------------------------------------- /images/dragon-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/dragon-thumbnail.png -------------------------------------------------------------------------------- /images/loyd-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/loyd-thumbnail.png -------------------------------------------------------------------------------- /images/pythagoras-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/pythagoras-thumbnail.png -------------------------------------------------------------------------------- /images/spiral-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/spiral-thumbnail.png -------------------------------------------------------------------------------- /images/tex-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/tex-thumbnail.png -------------------------------------------------------------------------------- /images/trees-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/trees-thumbnail.png -------------------------------------------------------------------------------- /images/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/triangle.png -------------------------------------------------------------------------------- /images/triangles-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewcmyers/constrain/a58ddb8e7ace8b2450f206f1c8ff374268c5babe/images/triangles-thumbnail.png -------------------------------------------------------------------------------- /tests/animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 25 | 26 | -------------------------------------------------------------------------------- /tests/constrain-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 |

Constrain Test Page

15 | 16 | 108 | 109 | 113 | 114 | -------------------------------------------------------------------------------- /tests/direction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 |

Direction Test

15 | 16 | 49 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /tests/ellipse_text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |

12 | The word "frontier" should be visible in the top of the ellipse. 13 |

14 | 15 | 24 | -------------------------------------------------------------------------------- /tests/graph-test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |

Graph Layout Test Page

16 | 17 | 85 | 86 | 89 | 90 | -------------------------------------------------------------------------------- /tests/graph-test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Constrain/Reveal Integration Demo 8 | 9 | 10 | 11 | 12 | 14 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | Constrain/Reveal integration 32 |
33 |

Automatic Graph Layout

34 | 35 | 58 |
59 |
60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/graph-test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 |

Graph test 3

12 | 13 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/hinttest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/hvlines.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 26 |

27 | The "plus sign" should fit exactly inside the square. 28 |

29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/insn_order2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 23 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/label-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 |

Label formatting test

18 | 19 | 40 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /tests/linelabels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 |

Label formatting test

18 | 19 | 36 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /tests/mathjax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 35 | 36 | 45 | 46 | 47 | 48 |

MathJax support for Constrain

49 | 50 |

This page shows that it is easy in Constrain to add TeX math formulas, formatted 52 | by MathJax, to a diagram.

53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/polygon_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/printtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 |

Print Test

16 | 17 |

Label

18 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/safari-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 |

Graph Layout Test Page

15 | 16 | 38 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /tests/sqtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/text-format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /tests/text-layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 |

Constrain Test Page

15 | 16 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /tests/treefig.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 81 | 82 | -------------------------------------------------------------------------------- /tests/treefig2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 50 | -------------------------------------------------------------------------------- /tests/treefig3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 50 | -------------------------------------------------------------------------------- /tests/triangles-broken.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 |

Triangle demo (this should be hidden)

16 | 17 | 18 | 19 | 20 | 166 | 167 | 168 | --------------------------------------------------------------------------------