├── .babelrc ├── README.md ├── components ├── Editor │ ├── Code.css │ ├── Code.js │ └── Prose.js ├── Inspector │ └── Transforms.js └── Preview │ ├── Page.css │ └── Page.js ├── containers └── Root │ └── index.js ├── css └── Root.css ├── index.dev.js ├── index.html ├── index.prod.js ├── package.json ├── polyfills ├── canvas.js ├── css-parser.js ├── gl-matrix.js └── vector.js ├── server.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose", "stage-0", "react"], 3 | "plugins": ["react-hot-loader/babel"] 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What? 2 | 3 | CSS Transforms can be one of the more difficult CSS properties to understand quickly. 4 | 5 | Transform function lists are awesome and powerful, but come at the cost of immediate understanding. 6 | 7 | Here is an experiment in creating visualisation tools that might be implemented in browser DevTools one day. 8 | 9 | --- 10 | 11 | # Usage 12 | 13 | Visit: https://waapi.github.io/tool-transforms/ 14 | 15 | The value in the transform property is editable and the visualisation will update. 16 | 17 | --- 18 | 19 | # Development 20 | 21 | ``` 22 | npm install 23 | npm start 24 | open http://localhost:6756 25 | ``` 26 | -------------------------------------------------------------------------------- /components/Editor/Code.css: -------------------------------------------------------------------------------- 1 | div.editor { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | justify-content: space-between; 5 | align-items: stretch; 6 | } 7 | 8 | div.editor header { 9 | position: relative; 10 | padding: 2px 10px; 11 | /*background: hsl(0, 0%, 95%);*/ 12 | font: 200 12px/1.2 sans-serif; 13 | color: hsl(0, 0%, 20%); 14 | letter-spacing: 0.1em; 15 | } 16 | 17 | 18 | div.editor span.mode { 19 | position: absolute; 20 | top: 2px; 21 | right: 10px; 22 | } 23 | 24 | 25 | div.editor div.ReactCodeMirror { 26 | display: flex; 27 | flex-flow: row nowrap; 28 | justify-content: space-between; 29 | align-items: stretch; 30 | flex: 1 1 100%; 31 | padding: 0 10px; 32 | /*-webkit-filter: grayscale(1); 33 | filter: grayscale(1);*/ 34 | } 35 | 36 | div.editor div.ReactCodeMirror--focused { 37 | /*-webkit-filter: none; 38 | filter: none;*/ 39 | } 40 | 41 | div.editor .CodeMirror { 42 | flex: 1 1 100%; 43 | height: auto; 44 | font: 14px/1.5 SourceCodeProRegular, Menlo, Consolas, 'DejaVu Sans Mono', monospace; 45 | /*background: hsl(0, 0%, 96%);*/ 46 | } 47 | 48 | div.editor .CodeMirror-sizer { 49 | /*background: white;*/ 50 | } 51 | 52 | 53 | div.editor .CodeMirror-selected:nth-child(1) { 54 | border-top-left-radius: 3px; 55 | border-top-right-radius: 3px; 56 | } 57 | div.editor .CodeMirror-selected:nth-child(2) { 58 | border-bottom-left-radius: 3px; 59 | border-bottom-right-radius: 3px; 60 | } 61 | div.editor .CodeMirror-selected:only-child { 62 | border-radius: 2px; 63 | } 64 | 65 | 66 | div.editor .CodeMirror * { 67 | /*box-sizing: content-box;*/ 68 | } 69 | 70 | div.editor .CodeMirror { 71 | contain: strict; 72 | } 73 | div.editor .CodeMirror-scroll, 74 | div.editor .CodeMirror-selected, 75 | div.editor .CodeMirror-cursors, 76 | div.editor .CodeMirror-code, 77 | div.editor .CodeMirror-linenumber { 78 | contain: size layout style; 79 | } 80 | 81 | div.editor .CodeMirror-line { 82 | contain: content; 83 | } 84 | 85 | div.editor .CodeMirror-scroll { 86 | /*margin-bottom: 0px; 87 | margin-right: 0px; 88 | padding-bottom: 0px;*/ 89 | backface-visibility: hidden; 90 | } 91 | 92 | 93 | div.editor .CodeMirror-focused { 94 | /*background: hsl(0, 0%, 100%);*/ 95 | } 96 | 97 | div.editor .CodeMirror-gutters { 98 | background: none; 99 | } 100 | 101 | div.editor .CodeMirror-selected { 102 | background: hsl(210, 0%, 83%); 103 | } 104 | 105 | div.editor div.ReactCodeMirror--focused .CodeMirror-selected { 106 | background-color: hsla(215, 100%, 75%, 0.5); 107 | } 108 | 109 | div.editor .CodeMirror textarea { 110 | overflow: hidden; 111 | } 112 | 113 | div.editor .CodeMirror-sizer + div { 114 | visibility: hidden; 115 | } 116 | -------------------------------------------------------------------------------- /components/Editor/Code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileSystem from 'systems/FileSystem.js'; 3 | import CodeMirror from 'react-codemirror'; 4 | import 'codemirror/mode/htmlmixed/htmlmixed'; 5 | import 'codemirror/mode/css/css'; 6 | import 'codemirror/mode/javascript/javascript'; 7 | import 'codemirror/keymap/sublime'; 8 | import 'codemirror/lib/codemirror.css'; 9 | import './Code.css'; 10 | 11 | 12 | export default class CodeEditor extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | content: null, 17 | mode: null 18 | }; 19 | this.watcher = null; 20 | } 21 | 22 | componentWillMount() { 23 | if(this.props.path) 24 | { 25 | this.guessMode(); 26 | this.watcher = FileSystem.watch(this.props.path); 27 | FileSystem.read(this.props.path).then(::this.handleRead); 28 | } 29 | } 30 | componentDidMount() {} 31 | 32 | componentWillUnmount() { 33 | if(this.watcher) 34 | { 35 | FileSystem.unwatch(this.watcher); 36 | this.watcher = null; 37 | } 38 | } 39 | 40 | componentWillReceiveProps(nextProps) { 41 | if(nextProps.path !== this.props.path) 42 | { 43 | if(this.props.path) FileSystem.unwatch(this.watcher); 44 | this.watcher = nextProps.path? FileSystem.watch(nextProps.path) : null; 45 | } 46 | } 47 | 48 | componentWillUpdate(nextProps, nextState) {} 49 | componentDidUpdate(prevProps, prevState) {} 50 | 51 | guessMode() { 52 | if(this.props.path.endsWith('.html')) this.setState({ mode: 'htmlmixed' }); 53 | else if(this.props.path.endsWith('.css')) this.setState({ mode: 'css' }); 54 | else if(this.props.path.endsWith('.js')) this.setState({ mode: 'javascript' }); 55 | else this.setState({ mode: 'null' }); 56 | } 57 | 58 | handleRead(content) { 59 | this.setState({ content }); 60 | } 61 | 62 | handleEdit(newCode) { 63 | this.watcher.pause(); 64 | FileSystem.write(this.props.path, newCode).then(() => this.watcher.resume()); 65 | } 66 | 67 | render() { 68 | if(!this.props.path) return ( 69 |
70 | ); 71 | 72 | if(this.state.content == null) return ( 73 |
74 | ); 75 | 76 | return ( 77 |
78 |
79 | {this.props.path} 80 | {this.state.mode && {{ 81 | htmlmixed: 'HTML', 82 | css: 'CSS', 83 | javascript: 'JS' 84 | }[this.state.mode]}} 85 |
86 | 98 |
99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /components/Editor/Prose.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ProseEditor extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | 8 | }; 9 | } 10 | componentWillMount() {} 11 | componentDidMount() {} 12 | componentWillUnmount() {} 13 | componentWillReceiveProps(nextProps) {} 14 | componentWillUpdate(nextProps, nextState) {} 15 | componentDidUpdate(prevProps, prevState) {} 16 | render() { 17 | return ( 18 |
Editahr 4 teh proses
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/Inspector/Transforms.js: -------------------------------------------------------------------------------- 1 | // import { glMatrix, mat2, mat2d, mat3, mat4, quat, vec2, vec3, vec4 } from 'polyfills/gl-matrix.js'; 2 | import mat4 from 'gl-mat4'; 3 | import decompose from 'mat4-decompose'; 4 | mat4.decompose = decompose; 5 | import quat from 'gl-quat'; 6 | import vec3 from 'gl-vec3'; 7 | 8 | import 'polyfills/canvas.js'; 9 | import Vector from 'polyfills/vector.js'; 10 | import CSSParser from 'polyfills/css-parser.js'; 11 | import React from 'react'; 12 | 13 | /* 14 | [ 0, 1, 2, 3, [m00,m10,m20,m30, [sx, , , 15 | 4, 5, 6, 7, m01,m11,m21,m31, , sy, , 16 | 8, 9,10,11, m02,m12,m22,m32, , , sz,-1/perspective 17 | 12,13,14,15] m03,m13,m23,m33] tx, ty, tz, ] 18 | */ 19 | 20 | 21 | mat4.getCSSMatrix2d = function(out, mat) { 22 | out[0] = mat[0]; 23 | out[1] = mat[1]; 24 | out[2] = mat[4]; 25 | out[3] = mat[5]; 26 | out[4] = mat[12]; 27 | out[5] = mat[13]; 28 | return out; 29 | }; 30 | 31 | mat4.getScaling = function(out, mat) { 32 | out[0] = mat[0]; 33 | out[1] = mat[5]; 34 | out[2] = mat[10]; 35 | }; 36 | 37 | quat.getEuler = function(out, q) { 38 | var sx = 2 * (q[0] * q[3] - q[1] * q[2]); 39 | var sy = 2 * (q[1] * q[3] + q[0] * q[2]); 40 | var ys = q[3] * q[3] - q[1] * q[1]; 41 | var xz = q[0] * q[0] - q[2] * q[2]; 42 | var cx = ys - xz; 43 | var cy = Math.sqrt(sx * sx + cx * cx); 44 | 45 | if (cy > 0.00034906584331009674) 46 | { 47 | out[0] = Math.atan2(sx, cx); 48 | out[1] = Math.atan2(sy, cy); 49 | out[2] = Math.atan2(2 * (q[2] * q[3] - q[0] * q[1]), ys + xz); 50 | } 51 | 52 | else 53 | { 54 | out[0] = 0; 55 | if (sy > 0) 56 | { 57 | out[1] = Math.PI/2; 58 | out[2] = 2 * Math.atan2(q[2] + q[0], q[3] + q[1]); 59 | } 60 | else 61 | { 62 | out[1] = -Math.PI/2; 63 | out[2] = 2 * Math.atan2(q[2] - q[0], q[3] - q[1]); 64 | } 65 | } 66 | }; 67 | 68 | 69 | 70 | 71 | export default class Transforms extends React.Component { 72 | constructor(props) { 73 | super(props); 74 | 75 | var transform = null; 76 | if(props.transform) transform = CSSParser.transform(props.transform); 77 | 78 | this.state = { 79 | transform 80 | }; 81 | 82 | window.addEventListener('resize', ::this.handleResize); 83 | } 84 | 85 | componentWillMount() {} 86 | 87 | componentDidMount() { 88 | this.layout(); 89 | this.paint(); 90 | } 91 | 92 | componentWillUnmount() {} 93 | 94 | componentWillReceiveProps(nextProps) { 95 | var state = {}; 96 | if(this.props.transform !== nextProps.transform) 97 | { 98 | state.transform = nextProps.transform? CSSParser.transform(nextProps.transform) : null; 99 | } 100 | 101 | if(this.props.reference !== nextProps.reference) 102 | { 103 | state.reference = nextProps.reference || null; 104 | } 105 | 106 | this.setState(state); 107 | } 108 | 109 | componentWillUpdate(nextProps, nextState) { 110 | 111 | } 112 | 113 | componentDidUpdate(prevProps, prevState) { 114 | this.paint(); 115 | } 116 | 117 | handleResize() { 118 | this.layout(); 119 | this.paint(); 120 | } 121 | 122 | layout() { 123 | var rect = this.canvas.getBoundingClientRect(); 124 | this.canvas.width = rect.width * window.devicePixelRatio; 125 | this.canvas.height = rect.height * window.devicePixelRatio; 126 | this.context = this.canvas.getContext('2d'); 127 | } 128 | 129 | // sandwich(stack) { 130 | // return stack.reduceRight((b, a) => mat4.multiply(b, a, b), mat4.create()); 131 | 132 | /*return stack.reduce((m, op) => { 133 | switch(op[0]) 134 | { 135 | case 'translate': mat4.translate(m, m, op[1]); break; 136 | case 'scale': mat4.scale(m, m, op[1]); break; 137 | case 'rotate': mat4.rotate(m, m, op[2], op[1]); break; 138 | } 139 | return m; 140 | }, mat4.create());*/ 141 | // } 142 | 143 | paint() { 144 | var origin = this.canvas.getBoundingClientRect(); 145 | var reference = this.state.reference; 146 | var ctx = this.context; 147 | 148 | ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 149 | 150 | if(!reference || !this.state.transform) return; 151 | 152 | var styles = getComputedStyle(reference); 153 | CSSParser.base = { 154 | width: parseFloat(styles.width), 155 | height: parseFloat(styles.height), 156 | fontSize: parseFloat(styles.fontSize) 157 | }; 158 | 159 | var base = { 160 | top: reference.offsetTop, 161 | left: reference.offsetLeft, 162 | width: parseFloat(styles.width), 163 | height: parseFloat(styles.height) 164 | }; 165 | 166 | 167 | ctx.save(); 168 | 169 | if(window.devicePixelRatio !== 1) ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 170 | 171 | // var stack = []; 172 | var matrix = mat4.create(); 173 | var pos = vec3.create(); 174 | var scl = vec3.fromValues(1, 1, 1); 175 | var skew = vec3.create(); 176 | var perspective = null; // vec4.create(); 177 | var rot = quat.create(); 178 | var euler = vec3.create(); 179 | 180 | 181 | // Origin of DOM layout 182 | var boxModelOrigin = vec3.fromValues(base.left - origin.left, base.top - origin.top, 0); 183 | // stack.push(mat4.fromTranslation(mat4.create(), boxModelOrigin)); // ['translate', boxModelOrigin]); 184 | mat4.translate(matrix, matrix, boxModelOrigin); 185 | 186 | 187 | // Origin of transform 188 | var transformOrigin = vec3.fromValues(base.width * 0.5, base.height * 0.5, 0); // assuming default for now, which is center 189 | // stack.push(mat4.fromTranslation(mat4.create(), transformOrigin)); // ['translate', transformOrigin]); 190 | mat4.translate(matrix, matrix, transformOrigin); 191 | 192 | 193 | // Export components 194 | // matrix = this.sandwich(stack); 195 | // mat4.getTranslation(pos, matrix); 196 | // mat4.getScaling(scl, matrix); 197 | // mat4.getRotation(rot, matrix); 198 | mat4.decompose(matrix, pos, scl, skew, perspective, rot); 199 | quat.getEuler(euler, rot); 200 | 201 | 202 | /// Style defaults 203 | ctx.lineWidth = 1; 204 | ctx.fillStyle = 'hsl(215, 100%, 50%)'; 205 | ctx.strokeStyle = 'hsl(215, 100%, 50%)'; 206 | 207 | /// Origin widget 208 | ctx.beginPath(); 209 | ctx.arc(pos[0], pos[1], 5, 0, Math.PI*2); 210 | ctx.closePath(); 211 | ctx.stroke(); 212 | 213 | 214 | /// Draw pre-transform box 215 | var radius = CSSParser.value(styles.borderRadius); 216 | ctx.save(); 217 | ctx.beginPath(); 218 | ctx.translate(pos[0], pos[1]); 219 | ctx.roundedRect(base.width * -.5, base.height * -.5, base.width, base.height, radius); 220 | ctx.closePath(); 221 | ctx.strokeStyle = 'hsla(215, 100%, 50%, 0.1)'; 222 | ctx.stroke(); 223 | ctx.restore(); 224 | 225 | 226 | 227 | this.state.transform.map(fn => { 228 | switch(fn.name) { 229 | case 'matrix': 230 | return { name, type: 'matrix', m: fn.values }; 231 | 232 | case 'matrix3d': 233 | return { name, type: 'matrix', m: fn.values }; 234 | 235 | case 'translate': 236 | var x = CSSParser.value(fn.values[0]); 237 | var y = fn.values[1]? CSSParser.value(fn.values[1]) : 0; 238 | return { name, type: 'translate', x, y }; 239 | 240 | case 'translateX': 241 | var x = CSSParser.value(fn.values[0]); 242 | return { name, type: 'translate', x, y: 0 }; 243 | 244 | case 'translateY': 245 | var y = CSSParser.value(fn.values[0]); 246 | return { name, type: 'translate', x: 0, y }; 247 | 248 | case 'translateZ': 249 | var z = CSSParser.value(fn.values[0]); 250 | return { name, type: 'translate', x: 0, y: 0, z }; 251 | 252 | case 'translate3d': 253 | var x = CSSParser.value(fn.values[0]); 254 | var y = CSSParser.value(fn.values[1]); 255 | var z = CSSParser.value(fn.values[2]); 256 | return { name, type: 'translate', x, y, z }; 257 | 258 | case 'scale': 259 | if(fn.values[1] === undefined) 260 | { 261 | var scale = fn.values[0]; 262 | return { name, type: 'scale', x: scale, y: scale }; 263 | } 264 | else 265 | { 266 | var x = fn.values[0]; 267 | var y = fn.values[1]; 268 | return { name, type: 'scale', x, y }; 269 | } 270 | case 'scaleX': 271 | var x = fn.values[0]; 272 | var y = 1; 273 | return { name, type: 'scale', x, y }; 274 | 275 | case 'scaleY': 276 | var x = 1; 277 | var y = fn.values[0]; 278 | return { name, type: 'scale', x, y }; 279 | 280 | case 'scaleZ': 281 | var x = 1; 282 | var y = 1; 283 | var z = fn.values[0]; 284 | return { name, type: 'scale', x, y, z }; 285 | 286 | case 'scale3d': 287 | var x = fn.values[0]; 288 | var y = fn.values[1]; 289 | var z = fn.values[2]; 290 | return { name, type: 'scale', x, y, z }; 291 | 292 | case 'rotate': 293 | case 'rotateZ': 294 | var a = CSSParser.value(fn.values[0]); 295 | return { name, type: 'rotate', x: 0, y: 0, z: 1, a }; 296 | 297 | case 'rotateX': 298 | var a = CSSParser.value(fn.values[0]); 299 | return { name, type: 'rotate', x: 1, y: 0, z: 0, a }; 300 | 301 | case 'rotateY': 302 | var a = CSSParser.value(fn.values[0]); 303 | return { name, type: 'rotate', x: 0, y: 1, z: 0, a }; 304 | 305 | case 'rotate3d': 306 | var x = CSSParser.value(fn.values[0]); 307 | var y = CSSParser.value(fn.values[1]); 308 | var z = CSSParser.value(fn.values[2]); 309 | var a = CSSParser.value(fn.values[3]); 310 | return { name, type: 'rotate', x, y, z, a }; 311 | 312 | case 'skew': 313 | var x = CSSParser.value(fn.values[0]); 314 | var y = fn.values[1]? CSSParser.value(fn.values[1]) : 0; 315 | return { name, type: 'skew', x, y }; 316 | 317 | case 'skewX': 318 | var x = CSSParser.value(fn.values[0]); 319 | return { name, type: 'skew', x, y: 0 }; 320 | 321 | case 'skewY': 322 | var y = CSSParser.value(fn.values[0]); 323 | return { name, type: 'skew', x: 0, y }; 324 | 325 | case 'perspective': 326 | var v = CSSParser.value(fn.values[0]); 327 | return { name, type: 'perspective', v }; 328 | 329 | default: return null; // Invalid fn 330 | } 331 | 332 | return null; 333 | }).forEach(fn => { 334 | if(!fn) return; 335 | 336 | ctx.save(); 337 | 338 | switch(fn.type) { 339 | case 'translate': 340 | var a = vec3.clone(pos); 341 | var v = vec3.fromValues(fn.x, fn.y, 0); 342 | vec3.multiply(v, v, scl); 343 | vec3.transformQuat(v, v, rot); 344 | var b = vec3.add(vec3.create(), pos, v); 345 | var delta = vec3.subtract(vec3.create(), b, a); 346 | var dir = vec3.normalize(vec3.create(), delta); 347 | 348 | /*var a = new Vector(pos[0], pos[1]).clone; 349 | var v = new Vector(fn.x, fn.y).scl(scl).rot(rot); 350 | var b = pos.clone.add(v); 351 | var delta = b.clone.sub(a); 352 | var dist = delta.mag; 353 | var dir = delta.clone.div(dist); 354 | var angle = delta.angle;*/ 355 | 356 | ctx.save(); 357 | ctx.beginPath(); 358 | 359 | ctx.translate(b[0], b[1]); 360 | ctx.rotate(euler[2]); 361 | ctx.rect(-3, -3, 6, 6); 362 | ctx.closePath(); 363 | ctx.fill(); 364 | ctx.restore(); 365 | 366 | var offset = vec3.scale(vec3.create(), dir, 6); 367 | vec3.add(a, a, offset); 368 | vec3.subtract(b, b, offset); 369 | 370 | // var offset = dir.clone.scl(6); 371 | // a.add(offset); 372 | // b.sub(offset); 373 | 374 | ctx.beginPath(); 375 | 376 | ctx.dashedLine( 377 | Math.round(a[0]) - .5, Math.round(a[1]) - .5, 378 | Math.round(b[0]) - .5, Math.round(b[1]) - .5, 379 | 5, 10 380 | ); 381 | 382 | ctx.closePath(); 383 | 384 | ctx.stroke(); 385 | 386 | break; 387 | case 'scale': 388 | ctx.save(); 389 | ctx.beginPath(); 390 | 391 | ctx.translate(pos[0], pos[1]); 392 | ctx.rotate(euler[2]); 393 | 394 | if(fn.x !== 1) 395 | { 396 | // ctx.moveTo(-18, 4); 397 | // ctx.lineTo(-18, -4); 398 | // 399 | // ctx.moveTo(18, 4); 400 | // ctx.lineTo(18, -4); 401 | 402 | ctx.clearRect(-18, -3, 14, 6); 403 | ctx.clearRect(4, -3, 14, 6); 404 | 405 | if(fn.x > 1) 406 | { 407 | ctx.moveTo(-6, 0); 408 | ctx.lineTo(-12, 0); 409 | 410 | ctx.moveTo(-13, 2); 411 | ctx.lineTo(-16, 0); 412 | ctx.lineTo(-13, -2); 413 | 414 | ctx.moveTo(6, 0); 415 | ctx.lineTo(12, 0); 416 | 417 | ctx.moveTo(13, 2); 418 | ctx.lineTo(16, 0); 419 | ctx.lineTo(13, -2); 420 | } 421 | else 422 | { 423 | ctx.moveTo(-10, 0); 424 | ctx.lineTo(-16, 0); 425 | 426 | ctx.moveTo(-9, 2); 427 | ctx.lineTo(-6, 0); 428 | ctx.lineTo(-9, -2); 429 | 430 | ctx.moveTo(10, 0); 431 | ctx.lineTo(16, 0); 432 | 433 | ctx.moveTo(9, 2); 434 | ctx.lineTo(6, 0); 435 | ctx.lineTo(9, -2); 436 | } 437 | } 438 | 439 | if(fn.y !== 1) 440 | { 441 | // ctx.moveTo(4, -18); 442 | // ctx.lineTo(-4, -18); 443 | // 444 | // ctx.moveTo(4, 18); 445 | // ctx.lineTo(-4, 18); 446 | 447 | ctx.clearRect(-3, -18, 6, 14); 448 | ctx.clearRect(-3, 4, 6, 14); 449 | 450 | if(fn.y > 1) 451 | { 452 | ctx.moveTo(0, -6); 453 | ctx.lineTo(0, -12); 454 | 455 | ctx.moveTo(2, -13); 456 | ctx.lineTo(0, -16); 457 | ctx.lineTo(-2, -13); 458 | 459 | ctx.moveTo(0, 6); 460 | ctx.lineTo(0, 12); 461 | 462 | ctx.moveTo(2, 13); 463 | ctx.lineTo(0, 16); 464 | ctx.lineTo(-2, 13); 465 | 466 | } 467 | else 468 | { 469 | ctx.moveTo(0, -10); 470 | ctx.lineTo(0, -16); 471 | 472 | ctx.moveTo(2, -9); 473 | ctx.lineTo(0, -6); 474 | ctx.lineTo(-2, -9); 475 | 476 | ctx.moveTo(0, 10); 477 | ctx.lineTo(0, 16); 478 | 479 | ctx.moveTo(2, 9); 480 | ctx.lineTo(0, 6); 481 | ctx.lineTo(-2, 9); 482 | } 483 | } 484 | 485 | ctx.stroke(); 486 | ctx.restore(); 487 | 488 | 489 | // ctx.beginPath(); 490 | // ctx.arc(pos[0], pos[1], 1, 0, Math.PI*2); 491 | // ctx.closePath(); 492 | // ctx.fill(); 493 | break; 494 | case 'rotate': 495 | ctx.save(); 496 | ctx.beginPath(); 497 | ctx.translate(pos[0], pos[1]); 498 | ctx.rotate(euler[2]); 499 | 500 | if(Math.abs(fn.a) < (Math.PI/180 * 15)) 501 | { 502 | var start = Math.PI/2; 503 | var end = Math.PI/2 + fn.a; 504 | 505 | ctx.arc(0, 0, 40, start, end, fn.a < 0); 506 | } 507 | 508 | else 509 | { 510 | ctx.clearRect(-1, 33, 3, 47 - 33); 511 | ctx.moveTo(0, 35); 512 | ctx.lineTo(0, 45); 513 | 514 | ctx.moveTo(fn.a > 0? -2 : 2, 40); 515 | var start = Math.PI/2 + (Math.PI/180 * (fn.a > 0? 3 : -3)); 516 | var end = Math.PI/2 + (fn.a + (Math.PI/180 * (fn.a > 0? -5 : 5))); 517 | 518 | ctx.arc(0, 0, 40, start, end, fn.a < 0); 519 | 520 | ctx.rotate(fn.a); 521 | 522 | 523 | if(fn.a > 0) 524 | { 525 | ctx.moveTo(5, 35); 526 | ctx.lineTo(0, 40); 527 | ctx.lineTo(5, 45); 528 | } 529 | else if(fn.a < 0) 530 | { 531 | ctx.moveTo(-5, 35); 532 | ctx.lineTo(0, 40); 533 | ctx.lineTo(-5, 45); 534 | } 535 | } 536 | 537 | // for(var iter = 2; iter < 96; iter += iter) 538 | // { 539 | // ctx.rotate(fn.a/iter); 540 | // ctx.moveTo(0, 37); 541 | // ctx.lineTo(0, 38); 542 | // ctx.moveTo(0, 42); 543 | // ctx.lineTo(0, 43); 544 | // } 545 | 546 | // ctx.rotate(fn.a/2); 547 | // 548 | // ctx.moveTo(0, 37); 549 | // ctx.lineTo(0, 38); 550 | // ctx.moveTo(0, 42); 551 | // ctx.lineTo(0, 43); 552 | 553 | ctx.stroke(); 554 | ctx.restore(); 555 | break; 556 | case 'skew': break; 557 | case 'matrix': break; 558 | case 'perspective': break; 559 | } 560 | 561 | ctx.restore(); 562 | 563 | switch(fn.type) { 564 | case 'translate': 565 | // Apply transform 566 | var t = vec3.fromValues(fn.x, fn.y, 0); 567 | // stack.push(mat4.fromTranslation(mat4.create(), t)); // ['translate', t]); 568 | mat4.translate(matrix, matrix, vec3.fromValues(fn.x, fn.y, 0)); 569 | 570 | // Export components 571 | // matrix = this.sandwich(stack); 572 | // mat4.getTranslation(pos, matrix); 573 | // mat4.getScaling(scl, matrix); 574 | // mat4.getRotation(rot, matrix); 575 | mat4.decompose(matrix, pos, scl, skew, perspective, rot); 576 | quat.getEuler(euler, rot); 577 | 578 | 579 | // var v = new Vector(fn.x, fn.y).scl(scl).rot(rot); 580 | // pos.add(v); 581 | break; 582 | case 'scale': 583 | // Apply transform 584 | var s = vec3.fromValues(fn.x, fn.y, fn.z || 1); 585 | // stack.push(mat4.fromScaling(mat4.create(), s)); // ['scale', s]); 586 | mat4.scale(matrix, matrix, s); 587 | 588 | // Export components 589 | // matrix = this.sandwich(stack); 590 | // mat4.getTranslation(pos, matrix); 591 | // mat4.getScaling(scl, matrix); 592 | // mat4.getRotation(rot, matrix); 593 | mat4.decompose(matrix, pos, scl, skew, perspective, rot); 594 | quat.getEuler(euler, rot); 595 | 596 | // scl.scl(fn.x, fn.y); 597 | break; 598 | case 'rotate': 599 | // Apply transform 600 | var axis = vec3.fromValues(fn.x, fn.y, fn.z); 601 | var angle = fn.a; 602 | // stack.push(mat4.fromRotation(mat4.create(), angle, axis)); // ['rotate', axis, angle]); 603 | mat4.rotate(matrix, matrix, angle, axis); 604 | 605 | // Export components 606 | // matrix = this.sandwich(stack); 607 | // mat4.getTranslation(pos, matrix); 608 | // mat4.getScaling(scl, matrix); 609 | // mat4.getRotation(rot, matrix); 610 | mat4.decompose(matrix, pos, scl, skew, perspective, rot); 611 | quat.getEuler(euler, rot); 612 | 613 | 614 | // if(fn.z === 1) rot += fn.a; 615 | break; 616 | case 'skew': 617 | // ctx.transform(1, Math.tan(fn.y), Math.tan(fn.x), 1, 0, 0); 618 | break; 619 | case 'matrix': 620 | // if(fn.m.length === 6) ctx.transform.apply(ctx, fn.m); 621 | // else ctx.transform.apply(ctx, fn.m.filter((c, i) => i < 12 && i%4 < 2)) 622 | break; 623 | case 'perspective': 624 | // ctx.transform() is only a transform matrix, need to figure out how to apply a projection matrix 625 | break; 626 | } 627 | }); 628 | 629 | if(reference instanceof HTMLElement) 630 | { 631 | var radius = CSSParser.value(styles.borderRadius); 632 | 633 | ctx.save(); 634 | // ctx.translate(pos[0], pos[1]); 635 | // ctx.rotate(euler[2]); 636 | ctx.resetTransform(); 637 | var cssMatrix = mat4.getCSSMatrix2d([], matrix); 638 | ctx.setTransform.apply(ctx, cssMatrix); 639 | // ctx.setTransform(cssMatrix[0], cssMatrix[1], cssMatrix[4], cssMatrix[5], cssMatrix[12], cssMatrix[13]); 640 | 641 | var w = base.width; 642 | var h = base.height; 643 | 644 | ctx.beginPath(); 645 | ctx.roundedRect(w * -.5, h * -.5, w, h, radius); 646 | ctx.closePath(); 647 | ctx.stroke(); 648 | 649 | // ctx.beginPath(); 650 | // ctx.arc(w * .5, h * .5, 3, 0, Math.PI*2); 651 | // ctx.closePath(); 652 | // ctx.stroke(); 653 | // 654 | // ctx.beginPath(); 655 | // ctx.arc(w * -.5, h * .5, 3, 0, Math.PI*2); 656 | // ctx.closePath(); 657 | // ctx.stroke(); 658 | // 659 | // ctx.beginPath(); 660 | // ctx.arc(w * -.5, h * -.5, 3, 0, Math.PI*2); 661 | // ctx.closePath(); 662 | // ctx.stroke(); 663 | // 664 | // ctx.beginPath(); 665 | // ctx.arc(w * .5, h * -.5, 3, 0, Math.PI*2); 666 | // ctx.closePath(); 667 | // ctx.stroke(); 668 | 669 | ctx.restore(); 670 | } 671 | 672 | else 673 | { 674 | ctx.beginPath(); 675 | ctx.rect(0, 0, base.width, base.height); 676 | ctx.closePath(); 677 | ctx.stroke(); 678 | } 679 | 680 | 681 | ctx.restore(); 682 | } 683 | 684 | render() { 685 | return ( 686 | this.canvas = c} 688 | className="overlay" 689 | /> 690 | ); 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /components/Preview/Page.css: -------------------------------------------------------------------------------- 1 | 2 | div.preview { 3 | position: relative; 4 | display: flex; 5 | flex-flow: column nowrap; 6 | justify-content: space-between; 7 | align-items: stretch; 8 | } 9 | 10 | div.preview iframe.preview { 11 | width: 100%; 12 | height: 100%; 13 | border: none; 14 | /*border-radius: 3px;*/ 15 | } 16 | -------------------------------------------------------------------------------- /components/Preview/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileSystem from 'systems/FileSystem.js'; 3 | import './Page.css'; 4 | 5 | export default class Preview extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.watcher = null; 9 | this.tid = null; 10 | this.watchedDependencies = {}; 11 | } 12 | 13 | componentWillMount() {} 14 | componentDidMount() { 15 | if(this.props.path) 16 | { 17 | this.watcher = FileSystem.watch(this.props.path); 18 | this.watcher.onUpdate = ::this.handleUpdate; 19 | this.tid = setInterval(::this.checkDependencies, 1000); 20 | } 21 | } 22 | 23 | componentWillUnmount() { 24 | if(this.tid) 25 | { 26 | clearInterval(this.tid); 27 | this.tid = null; 28 | } 29 | 30 | if(this.watcher) 31 | { 32 | FileSystem.unwatch(this.watcher); 33 | this.watcher = null; 34 | } 35 | } 36 | 37 | componentWillReceiveProps(nextProps) { 38 | if(nextProps.path !== this.props.path) 39 | { 40 | if(this.props.path) FileSystem.unwatch(this.watcher); 41 | this.watcher = nextProps.path? FileSystem.watch(nextProps.path) : null; 42 | } 43 | } 44 | 45 | componentWillUpdate(nextProps, nextState) {} 46 | componentDidUpdate(prevProps, prevState) {} 47 | 48 | checkDependencies() { 49 | if(!(this.props.path && this._iframe && this._iframe.contentWindow)) return; 50 | 51 | // Select all s 52 | var elements = this._iframe.contentWindow.document.querySelectorAll('link[href]'); 53 | 54 | // Filter down to local files 55 | var files = Array.prototype.slice.call(elements) 56 | .map(element => new URL(element.href)) 57 | .filter(url => url.origin == location.origin && url.pathname.startsWith('/live/')) 58 | .map(url => url.pathname.replace('/live', '')); 59 | 60 | // Check for added dependencies 61 | files 62 | .filter(path => !(path in this.watchedDependencies)) 63 | .forEach(path => { 64 | console.log('Added dependency:', path); 65 | var watcher = FileSystem.watch(path); 66 | watcher.on = (event) => console.log(event); 67 | watcher.onUpdate = ::this.handleUpdateDependency; 68 | this.watchedDependencies[path] = watcher; 69 | }); 70 | 71 | // Check for removed dependencies 72 | Object.keys(this.watchedDependencies) 73 | .filter(path => files.indexOf(path) === -1) 74 | .forEach(path => { 75 | console.log('Removed dependency:', path); 76 | var watcher = this.watchedDependencies[path]; 77 | FileSystem.unwatch(watcher); 78 | delete this.watchedDependencies[path]; 79 | }); 80 | } 81 | 82 | handleUpdate(event) { 83 | if(this._iframe) 84 | { 85 | // this._iframe.contentWindow.location.reload(); 86 | 87 | fs.read(event.path).then((html) => { 88 | if(html.indexOf) 89 | html = '' + html; 90 | this._iframe.contentDocument.open(); 91 | this._iframe.contentDocument.write(''); 92 | this._iframe.contentDocument.write(html); 93 | this._iframe.contentDocument.close(); 94 | }); 95 | } 96 | } 97 | 98 | handleUpdateDependency(event) { 99 | var path = event.path.slice(1); 100 | 101 | // Select responsible 102 | var element = this._iframe.contentWindow.document.querySelector(`link[href^="${path}"]`); 103 | element.href = path + '?' + (new Date()).toString(); 104 | 105 | // console.log('Dependency was updated:', element.href); 106 | } 107 | 108 | render() { 109 | if(!this.props.path) return ( 110 | null 111 | ); 112 | 113 | return ( 114 |
115 | {/*
116 | {location.origin + '/live' + this.props.path} 117 |
*/} 118 | 126 |
127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /containers/Root/index.js: -------------------------------------------------------------------------------- 1 | import 'css/Root.css'; 2 | import React from 'react'; 3 | import TransformsInspector from 'components/inspector/Transforms.js'; 4 | import AutosizeInput from 'react-input-autosize'; 5 | 6 | 7 | 8 | export default class Root extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | transform: 13 | // 'rotate(45deg) scaleY(0.5) rotate(-45deg)', 14 | 'rotate(50deg) translateY(50%) rotate(-75deg) translateY(-60%) rotate(30deg) scale(0.5)', 15 | // 'rotate(50deg) translateY(-100%) scale(0.5) rotate(30deg) translateY(100%) scaleY(0.4) translate(4em, 4em) rotate(-30deg)', 16 | reference: null 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this.setState({ 22 | reference: this.foo 23 | }); 24 | } 25 | 26 | handleInput(event) { 27 | this.setState({ transform: event.target.value }); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 36 |
this.foo = c} 38 | className="foo" 39 | style={{ transform: this.state.transform }} 40 | /> 41 | 45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /css/Root.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | .app-root { 12 | display: flex; 13 | flex-flow: column nowrap; 14 | justify-content: center; 15 | align-items: center; 16 | width: 100%; 17 | height: 100%; 18 | background: hsl(0, 0%, 90%); 19 | } 20 | 21 | nav.controls { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | padding: 20px 10px; 27 | border-bottom: 1px solid hsl(0, 0%, 80%); 28 | background: hsl(0, 0%, 100%); 29 | font: 12px/1.2 monospace; 30 | color: hsl(210, 15%, 20%) 31 | } 32 | 33 | span.selector { color: hsl(0, 0%, 10%); } 34 | span.property { color: hsl(0, 100%, 39%); } 35 | span.value { } 36 | 37 | nav.controls input { 38 | border: 1px solid transparent; 39 | padding: 4px 2px; 40 | margin: -4px -2px; 41 | border-radius: 3px; 42 | font: 12px/1.2 monospace; 43 | background: none; 44 | color: currentColor; 45 | } 46 | 47 | nav.controls input:focus { 48 | color: black; 49 | border: 1px solid hsl(0, 0%, 70%); 50 | background: white; 51 | box-shadow: 0 2px 1px -1px hsla(0, 0%, 10%, 0.05); 52 | outline: none; 53 | } 54 | 55 | 56 | .app-root canvas.overlay { 57 | position: absolute; 58 | top: 0; 59 | left: 0; 60 | right: 0; 61 | bottom: 0; 62 | display: block; 63 | width: 100%; 64 | height: 100%; 65 | pointer-events: none; 66 | } 67 | 68 | 69 | div.foo { 70 | width: 180px; 71 | height: 300px; 72 | border-radius: 4px; 73 | background: white; 74 | box-shadow: 0 2px 3px 1px hsla(0, 0%, 0%, 0.2); 75 | } 76 | -------------------------------------------------------------------------------- /index.dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Root from './containers/Root'; 5 | 6 | 7 | render( 8 | 9 | 10 | , 11 | document.body 12 | ); 13 | 14 | 15 | if(module.hot) 16 | { 17 | module.hot.accept('./containers/Root', () => { 18 | var NewRoot = require('./containers/Root').default; 19 | render( 20 | 21 | 22 | , 23 | document.body 24 | ); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Animations API Tool – Editor 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.prod.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Root from './containers/Root'; 4 | 5 | 6 | render( 7 | , 8 | document.body 9 | ); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waapi-transforms", 3 | "version": "0.0.0", 4 | "description": "Web Animations API Transforms Visualisation Experiment", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "build": "NODE_ENV=production webpack -p" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/waapi/transforms.git" 13 | }, 14 | "keywords": [ 15 | "waapi", 16 | "web animations", 17 | "animations", 18 | "transforms", 19 | "sandbox", 20 | "tool" 21 | ], 22 | "license": "MIT", 23 | "dependencies": { 24 | "classnames": "^2.1.2", 25 | "gl-mat4": "^1.1.4", 26 | "gl-quat": "^1.0.0", 27 | "gl-vec3": "^1.0.3", 28 | "localforage": "^1.4.2", 29 | "mat4-decompose": "^1.0.4", 30 | "react": "^15.0.1", 31 | "react-codemirror": "^0.2.6", 32 | "react-dom": "^15.0.1", 33 | "react-hot-loader": "^3.0.0-beta.1", 34 | "react-input-autosize": "^1.0.0", 35 | "react-notification-center": "^1.2.1" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.3.17", 39 | "babel-core": "^6.3.17", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015-loose": "^6.1.3", 42 | "babel-preset-react": "6.3.13", 43 | "babel-preset-stage-0": "^6.3.13", 44 | "file-loader": "^0.8.5", 45 | "node-libs-browser": "^0.5.2", 46 | "raw-loader": "^0.5.1", 47 | "style-loader": "^0.12.3", 48 | "url-loader": "^0.5.7", 49 | "webpack": "^1.9.11", 50 | "webpack-dev-server": "^1.9.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /polyfills/canvas.js: -------------------------------------------------------------------------------- 1 | CanvasRenderingContext2D.prototype.dashedLine = function (x0, y0, x1, y1, dash, gap) { 2 | if(dash == undefined) dash = 2; 3 | if(gap == undefined) gap = 2; 4 | var dashgap = dash + gap; 5 | 6 | var dx = x1 - x0; 7 | var dy = y1 - y0; 8 | var dist = Math.sqrt(dx * dx + dy * dy); 9 | var nx = dx / dist; 10 | var ny = dy / dist; 11 | var dashX = nx * dash; 12 | var dashY = ny * dash; 13 | var gapX = nx * gap; 14 | var gapY = ny * gap; 15 | 16 | var approx = dist / dashgap; 17 | var total = Math.floor(approx); 18 | var expose = approx - total; 19 | 20 | x0 += nx * dashgap * expose * 0.5; 21 | y0 += ny * dashgap * expose * 0.5; 22 | 23 | x0 -= gapX * 0.5; 24 | y0 -= gapY * 0.5; 25 | 26 | while (total-->0) 27 | { 28 | x0 += gapX; 29 | y0 += gapY; 30 | this.moveTo(x0, y0); 31 | x0 += dashX; 32 | y0 += dashY; 33 | this.lineTo(x0, y0); 34 | } 35 | }; 36 | 37 | CanvasRenderingContext2D.prototype.roundedRect = function (x, y, w, h, r) { 38 | this.moveTo(x + r, y); 39 | this.arcTo(x + w, y, x + w, y + r, r); 40 | this.arcTo(x + w, y + h, x + w - r, y + h, r); 41 | this.arcTo(x, y + h, x, y + h - r, r); 42 | this.arcTo(x, y, x + r, y, r); 43 | }; 44 | -------------------------------------------------------------------------------- /polyfills/css-parser.js: -------------------------------------------------------------------------------- 1 | 2 | class CSSParser { 3 | static transform(string) { 4 | if(!string) return null; 5 | 6 | var number = '[+-]?(?:\\d+\\.\\d+|\\.?\\d+)(?:e\\d+)?'; 7 | 8 | var length = `${number}(px|em|ex|rem|vw|vh|vmin|vmax|in|cm|mm|pt|pc)`; 9 | var percent = `${number}(%)`; 10 | var dimension = `${number}(%|px|em|ex|rem|vw|vh|vmin|vmax|in|cm|mm|pt|pc)`; 11 | var angle = `${number}(deg|rad|turn)`; 12 | 13 | var translate = new RegExp(`\\s*(${dimension}|0)\\s*(?:,\\s*(${dimension}|0)\\s*)?`); 14 | var translateXYZ = new RegExp(`\\s*(${dimension}|0)\\s*`); 15 | var translate3d = new RegExp(`\\s*(${dimension}|0)\\s*,\\s*(${dimension}|0)\\s*,\\s*(${dimension}|0)\\s*`); 16 | 17 | var rotate = new RegExp(`\\s*(${angle}|0)\\s*`); 18 | var rotate3d = new RegExp(`\\s*${number}\\s*,\\s*${number}\\s*,\\s*${number}\\s*,\\s*(${angle}|0)\\s*`); 19 | 20 | var skew = new RegExp(`\\s*(${angle}|0)\\s*(?:,\\s*(${angle}|0)\\s*)?`); 21 | var skewXY = new RegExp(`\\s*(${angle}|0)\\s*`); 22 | 23 | var perspective = new RegExp(`\\s*(${length}|0)\\s*`); 24 | 25 | var functions = /\b((?:matrix|translate|scale|rotate|skew|perspective)(?:X|Y|Z|3d)?)\((.*?)\)/g; 26 | 27 | var results = []; 28 | var result; 29 | while(result = functions.exec(string)) 30 | { 31 | var name = result[1]; 32 | var args = result[2]; 33 | var values; 34 | switch(name) { 35 | case 'matrix': values = args.split(','); if(values.length !== 6) return null; break; 36 | case 'matrix3d': values = args.split(','); if(values.length !== 16) return null; break; 37 | case 'translate': values = args.match(translate); break; 38 | case 'translateX': 39 | case 'translateY': 40 | case 'translateZ': values = args.match(translateXYZ); break; 41 | case 'translate3d': values = args.match(translate3d); break; 42 | case 'scale': 43 | case 'scaleX': 44 | case 'scaleY': 45 | case 'scaleZ': 46 | case 'scale3d': values = args.split(','); break; 47 | case 'rotate': 48 | case 'rotateX': 49 | case 'rotateY': 50 | case 'rotateZ': values = args.match(rotate); break; 51 | case 'rotate3d': values = args.match(rotate3d); break; 52 | case 'skew': values = args.match(skew); break; 53 | case 'skewX': 54 | case 'skewY': values = args.match(skewXY); break; 55 | case 'perspective': values = args.match(perspective); break; 56 | default: return null; // An invalid or unimplemented transform function? (E.g. skewZ, perspectiveX) 57 | } 58 | 59 | // Parse values into floats 60 | if(/^(matrix|scale)/.test(name)) // unit-less 61 | { 62 | values = values.map(value => parseFloat(value)); 63 | } 64 | 65 | else // Some functions have tuples of [value, unit] 66 | { 67 | values = (values => { 68 | var temp = []; 69 | for(var iter = 0, total = values.length; iter < total; iter += 2) 70 | { 71 | var value = values[iter]; 72 | var unit = values[iter + 1]; 73 | if(typeof value === 'string') 74 | { 75 | var pair = [parseFloat(value), unit]; 76 | temp.push(pair); 77 | } 78 | else return temp; 79 | } 80 | return temp; 81 | })(values.slice(1)); 82 | 83 | switch(name) { 84 | case 'translate': 85 | values[0].push('width'); 86 | if(values[1]) values[1].push('height'); 87 | break; 88 | case 'translateX': values[0].push('width'); break; 89 | case 'translateY': values[0].push('height'); break; 90 | case 'translate3d': 91 | values[0].push('width'); 92 | values[1].push('height'); 93 | break; 94 | } 95 | } 96 | 97 | results.push({ 98 | name, 99 | values 100 | }); 101 | } 102 | 103 | return results; 104 | } 105 | 106 | static value(input) { 107 | if(!input || !input.length) return null; 108 | var cardinal = input[2] || 'width'; 109 | 110 | if(typeof input === 'string') 111 | { 112 | input = input.match(/([+-]?\d+?(?:\.\d+)?)(px|em|rem|ex|%|vw|vh|vmin|vmax|in|cm|mm|pt|pc|deg|rad|turn)?/); 113 | var value = parseFloat(input[1]); 114 | var unit = input[2]; 115 | } 116 | 117 | else 118 | { 119 | var [value, unit] = input; 120 | } 121 | 122 | switch (unit) { 123 | case 'em': return value * CSSParser.base.fontSize; 124 | case 'rem': return value * parseFloat(getComputedStyle(document.documentElement).fontSize); 125 | case 'ch': return value * CSSParser.base.fontSize * 0.5; 126 | case 'ex': return value * CSSParser.base.fontSize * 0.45; 127 | case '%': return value * CSSParser.base[cardinal] / 100; 128 | case 'vw': return value * window.innerWidth / 100; 129 | case 'vh': return value * window.innerHeight / 100; 130 | case 'vmin': return value * Math.min(window.innerWidth, window.innerHeight) / 100; 131 | case 'vmax': return value * Math.max(window.innerWidth, window.innerHeight) / 100; 132 | case 'in': return value * 72; 133 | case 'cm': return value / 2.54 * 96; 134 | case 'mm': return value / 2.54 * 96 / 10; 135 | case 'pt': return value * 96 / 72; 136 | case 'pc': return value * CSSParser.base[cardinal]; 137 | case 'deg': return value * 0.017453292519943295; 138 | case 'rad': return value; 139 | case 'turn': return value * 6.283185307179586; 140 | default: return value; 141 | } 142 | } 143 | } 144 | 145 | CSSParser.base = { width: 1, height: 1, fontSize: 16 }; 146 | 147 | export default CSSParser; 148 | -------------------------------------------------------------------------------- /polyfills/vector.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Vector { 3 | constructor(a, b) { 4 | if(a instanceof Vector) { this.x = a.x; this.y = a.y } 5 | else { this.x = a || 0; this.y = b || 0 } 6 | } 7 | 8 | add(a, b) { 9 | if(a instanceof Vector) { this.x += a.x; this.y += a.y } 10 | else { this.x += a; this.y += b } 11 | return this; 12 | } 13 | 14 | sub(a, b) { 15 | if(a instanceof Vector) { this.x -= a.x; this.y -= a.y } 16 | else { this.x -= a; this.y -= b } 17 | return this; 18 | } 19 | 20 | scl(a, b) { 21 | if(a instanceof Vector) { this.x *= a.x; this.y *= a.y } 22 | else if(typeof a === 'number' && typeof b === 'number') { this.x *= a; this.y *= b } 23 | else { this.x *= a; this.y *= a } 24 | return this; 25 | } 26 | 27 | div(a, b) { 28 | if(a instanceof Vector) { this.x /= a.x; this.y /= a.y } 29 | else if(typeof a === 'number' && typeof b === 'number') { this.x /= a; this.y /= b } 30 | else { this.x /= a; this.y /= a } 31 | return this; 32 | } 33 | 34 | rot(a) { 35 | var sin = Math.sin(a), cos = Math.cos(a); 36 | var x = this.x * cos - this.y * sin; 37 | var y = this.x * sin + this.y * cos; 38 | this.x = x; this.y = y; 39 | return this; 40 | } 41 | 42 | get mag() { return Math.sqrt(this.x * this.x + this.y * this.y) } 43 | get clone() { return new Vector(this) } 44 | get perp() { return new Vector(this.y, this.x) } 45 | get angle() { return Math.atan2(this.y, this.x) } 46 | } 47 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | config.devtool = 'eval'; 6 | config.entry.splice(config.entry.indexOf('./index.prod'), 1, './index.dev'); 7 | config.entry.unshift( 8 | 'webpack-dev-server/client?http://localhost:6756', 9 | 'webpack/hot/only-dev-server', 10 | 'react-hot-loader/patch' 11 | ); 12 | config.plugins.push( 13 | new webpack.HotModuleReplacementPlugin() 14 | ); 15 | 16 | new WebpackDevServer(webpack(config), { 17 | publicPath: config.output.publicPath, 18 | hot: true, 19 | historyApiFallback: true, 20 | stats: { 21 | colors: true 22 | } 23 | }).listen(6756, 'localhost', function (err) { 24 | if(err) console.log(err); 25 | 26 | console.log('Listening at localhost:6756'); 27 | }); 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | './index.prod' 7 | ], 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'bundle.js', 11 | publicPath: '/' 12 | }, 13 | plugins: [ 14 | new webpack.DefinePlugin({ 15 | 'process.env': { 16 | NODE_ENV: JSON.stringify('production') 17 | } 18 | }), 19 | new webpack.optimize.UglifyJsPlugin() 20 | ], 21 | resolve: { 22 | root: [ 23 | __dirname 24 | ], 25 | alias: { 26 | 'react': path.join(__dirname, 'node_modules', 'react') 27 | }, 28 | extensions: ['', '.js'] 29 | }, 30 | resolveLoader: { 31 | 'fallback': path.join(__dirname, 'node_modules') 32 | }, 33 | watchOptions: { 34 | poll: true, 35 | ignore: /node_modules/ 36 | }, 37 | module: { 38 | loaders: [{ 39 | test: /\.js$/, 40 | loaders: ['babel'], 41 | exclude: /node_modules/, 42 | include: __dirname 43 | }, { 44 | test: /\.css?$/, 45 | loaders: ['style', 'raw'], 46 | include: __dirname 47 | }, { 48 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/, 49 | loader: ['file'], 50 | include: __dirname 51 | }, { 52 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$|\.wav$|\.mp3$/, 53 | loader: ['url'], 54 | include: __dirname 55 | }] 56 | } 57 | }; 58 | --------------------------------------------------------------------------------