├── .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 | 
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 | [Interactive Pythagorean Theorem](https://andrewcmyers.github.io/constrain/examples/pythagoras.html)
13 |
14 | [Interactively computing centers of a triangle](https://andrewcmyers.github.io/constrain/examples/triangles.html)
15 |
16 | [Animated trees](https://andrewcmyers.github.io/constrain/examples/ll_lr.html)
17 |
18 | [Loyd 15-puzzle](https://andrewcmyers.github.io/constrain/examples/loyd.html)
19 |
20 | [Using constraints to compute the Golden Ratio](https://andrewcmyers.github.io/constrain/examples/spiral.html) (Drag the diamond!)
21 |
22 | [Dragon curve](https://andrewcmyers.github.io/constrain/examples/dragon.html)
23 |
24 | [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 |
20 |
21 |
22 |
23 |
25 |
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 |
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 |
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 |
Show solving
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 |
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 |
Show solving
20 |
166 |
167 |
168 |
--------------------------------------------------------------------------------