Repository: metafizzy/zdog Branch: master Commit: dde8684be686 Files: 67 Total size: 188.3 KB Directory structure: gitextract_1ppqe373/ ├── .eslintrc.js ├── .github/ │ ├── contributing.md │ └── issue_template.md ├── .gitignore ├── README.md ├── bower.json ├── demos/ │ ├── .eslintrc.js │ ├── box-cross/ │ │ ├── box-cross.js │ │ └── index.html │ ├── dot-cube/ │ │ ├── dot-cube.js │ │ └── index.html │ ├── fullscreen/ │ │ ├── fullscreen.js │ │ └── index.html │ ├── hello-world-canvas/ │ │ ├── hello-world-canvas.js │ │ └── index.html │ ├── hello-world-svg/ │ │ ├── hello-world-svg.js │ │ └── index.html │ ├── hemisphere-cone-ball/ │ │ ├── hemisphere-cone-ball.js │ │ └── index.html │ ├── houses/ │ │ ├── houses.js │ │ └── index.html │ ├── kid-kit/ │ │ ├── index.html │ │ └── kid-kit.js │ ├── kirby-parasol/ │ │ ├── index.html │ │ └── kirby-parasol.js │ ├── no-illo-canvas/ │ │ ├── index.html │ │ └── no-illo-canvas.js │ ├── no-illo-svg/ │ │ ├── index.html │ │ └── no-illo-svg.js │ ├── path-commands/ │ │ ├── index.html │ │ └── path-commands.js │ ├── resize/ │ │ ├── index.html │ │ └── resize.js │ ├── shade-and-shades/ │ │ ├── index.html │ │ └── shade-and-shades.js │ ├── shapes/ │ │ ├── index.html │ │ └── shapes.js │ ├── solids/ │ │ ├── index.html │ │ └── solids.js │ ├── strutter/ │ │ ├── index.html │ │ └── strutter.js │ └── zdog-logo/ │ ├── index.html │ └── zdog-logo.js ├── dist/ │ └── zdog.dist.js ├── js/ │ ├── anchor.js │ ├── boilerplate.js │ ├── box.js │ ├── canvas-renderer.js │ ├── cone.js │ ├── cylinder.js │ ├── dragger.js │ ├── ellipse.js │ ├── group.js │ ├── hemisphere.js │ ├── illustration.js │ ├── index.js │ ├── path-command.js │ ├── polygon.js │ ├── rect.js │ ├── rounded-rect.js │ ├── shape.js │ ├── svg-renderer.js │ └── vector.js ├── package.json └── tasks/ ├── .eslintrc.js ├── bundle.js └── version.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ /* eslint-env node */ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/browser', env: { browser: true, commonjs: true, }, parserOptions: { ecmaVersion: 5, }, rules: { 'no-var': 'off', 'max-params': [ 'error', { max: 5, } ], }, }; ================================================ FILE: .github/contributing.md ================================================ ## Feature requests **Add 👍 reaction** to issues for features you would like to see added to Zdog. Do not add +1 comments — [they will be deleted](https://metafizzy.co/blog/use-github-reactions-delete-plus-1-comments/). ## Reduced test cases required All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/). Create one by forking this any one of the [Zdog demos on CodePen](https://codepen.io/desandro/pens/tags/?grid_type=list&selected_tag=zdog-v1-docs&sort_col=created_at&sort_order=asc). **CodePen** + [Hello world canvas](https://codepen.io/desandro/pen/YbrLaO) + [Hello world SVG](https://codepen.io/desandro/pen/Bewxme) + [resize fullscreen](https://codepen.io/desandro/pen/dEJxaV) + [Strutter](https://codepen.io/desandro/pen/xNPaoP) **Test cases** + A reduced test case clearly demonstrates the bug or issue. + It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug. + A link to your production site is **not** a reduced test case. Providing a reduced test case is the best way to get your issue addressed. They help you point out the problem. They help me verify and debug the problem. They help others understand the problem. Without a reduced test case, your issue may be closed. ## Pull requests Contributions are welcome! + **For typos and one-line fixes,** send those right in. + **For larger features,** open an issue before starting any significant work. Let's discuss to see how your feature fits within Zdog's vision. + **Follow the code style.** Spaces in brackets, semicolons, trailing commas. + **Do not edit `dist/` files.** Make your edits to source files in `js/` and `css/`. + **Do not run `make` to update `dist/` files.** I'll take care of this when I create a new release. ================================================ FILE: .github/issue_template.md ================================================ **Test case:** https://codepen.io/desandro/pen/YbrLaO ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ bower_components/ ================================================ FILE: README.md ================================================ # Zdog _Round, flat, designer-friendly pseudo-3D engine_ View complete documentation and live demos at [zzz.dog](https://zzz.dog). ## Install ### Download + [zdog.dist.min.js](https://unpkg.com/zdog@1/dist/zdog.dist.min.js) minified, or + [zdog.dist.js](https://unpkg.com/zdog@1/dist/zdog.dist.js) un-minified ### CDN Link directly to Zdog JS on [unpkg](https://unpkg.com). ``` html ``` ### Package managers npm: `npm install zdog` Bower: `bower install zdog` ## Hello world demo Create 3D models with Zdog by adding shapes. See [Getting started](https://zzz.dog/getting-started) for a walk-through of this demo. ``` js let isSpinning = true; let illo = new Zdog.Illustration({ element: '.zdog-canvas', zoom: 4, dragRotate: true, // stop spinning when drag starts onDragStart: function() { isSpinning = false; }, }); // circle new Zdog.Ellipse({ addTo: illo, diameter: 20, translate: { z: 10 }, stroke: 5, color: '#636', }); // square new Zdog.Rect({ addTo: illo, width: 20, height: 20, translate: { z: -10 }, stroke: 3, color: '#E62', fill: true, }); function animate() { illo.rotate.y += isSpinning ? 0.03 : 0; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ``` ## About Zdog Hi, [Dave here](https://desandro.com). I wanted to make a video game. I needed a 3D engine, but most engines were too powerful and complex for me. I made Zdog so I could design and display simple 3D models without a lot of overhead. Zdog is directly inspired by [Dogz](https://en.wikipedia.org/wiki/Petz), a virtual pet game by P.F. Magic released in 1995. It used flat 2D circle sprites to render the Dogz’ models, but in a 3D scene. [See Dogz playthrough video here.](https://www.youtube.com/watch?v=6lKSn_cHw5k) Dogz were fully animated in real time, running, flopping, scratching (on Windows 3.1!). It was remarkable. Zdog uses the same principle. It renders all shapes using the 2D drawing APIs in either `` or ``. Spheres are actually dots. Toruses are actually circles. Capsules are actually thick lines. It’s a simple, but effective trick. The underlying 3D math comes from [Rotating 3D Shapes](https://www.khanacademy.org/computing/computer-programming/programming-games-visualizations/programming-3d-shapes/a/rotating-3d-shapes) by [Peter Collingridge](https://petercollingridge.appspot.com/3D-tutorial/rotating-objects). Zdog is pronounced "Zee-dog" in American parlance or "Zed-dog" in British. ### Beta! Zdog v1 is a beta-release, of sorts. This is my first time creating a 3D engine, so I probably got some stuff wrong. Expect lots of changes for v2. Provide input and select new features on the [Zdog issue tracker on GitHub](https://github.com/metafizzy/zdog/issues). ### More Zdog resources Other people's stuff: + [Zfont](https://jaames.github.io/zfont/) - Text plugin for Zdog + [vue-zdog](https://github.com/AlexandreBonaventure/vue-zdog) - Vue wrapper for Zdog + [zDogPy](https://github.com/gferreira/zdogpy) - Python port of Zdog for DrawBot + [Made with Zdog CodePen Collection](https://codepen.io/collection/DzdGMe/) + [Made with Zdog on Twitter](https://twitter.com/i/moments/1135000612356206592) My stuff: + [Zdog demos on CodePen](https://github.com/metafizzy/zdog-demos), source code at [zdog-demos](https://github.com/metafizzy/zdog-demos) + [zdog-docs](https://github.com/metafizzy/zdog-docs) - Docs site source code --- Licensed MIT. Made by Metafizzy 🌈🐻 ================================================ FILE: bower.json ================================================ { "name": "zdog", "description": "Round, flat, designer-friendly pseudo-3D engine", "main": "js/index.js", "authors": [ "David DeSandro" ], "license": "MIT", "keywords": [ "3D", "canvas", "svg" ], "homepage": "https://zzz.dog", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests", "package.json", "demo", "demos" ] } ================================================ FILE: demos/.eslintrc.js ================================================ /* eslint-env node */ module.exports = { extends: '../.eslintrc.js', globals: { Zdog: 'readonly', }, rules: { 'key-spacing': 'off', 'max-lines': 'off', }, }; ================================================ FILE: demos/box-cross/box-cross.js ================================================ // ----- setup ----- // var sceneSize = 9; var isSpinning = true; var TAU = Zdog.TAU; // colors var yellow = '#ED0'; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; var initRotate = { x: ( 35/360 ) * TAU, y: TAU/8 }; var illo = new Zdog.Illustration({ element: '.illo', rotate: initRotate, resize: 'fullscreen', dragRotate: true, onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // var model = new Zdog.Anchor({ addTo: illo, }); function addBox( options ) { var boxOptions = { addTo: model, stroke: false, topFace: yellow, rearFace: gold, leftFace: orange, rightFace: orange, frontFace: garnet, bottomFace: eggplant, }; Zdog.extend( boxOptions, options ); new Zdog.Box( boxOptions ); } // top addBox({ bottomFace: false, translate: { y: -1 }, }); // bottom addBox({ topFace: false, translate: { y: 1 }, }); // front addBox({ rearFace: false, translate: { z: 1 }, }); // back addBox({ frontFace: false, translate: { z: -1 }, }); // left addBox({ rightFace: false, translate: { x: -1 }, }); // right addBox({ leftFace: false, translate: { x: 1 }, }); var dot = new Zdog.Shape({ addTo: model, translate: { y: -2 }, stroke: 1, color: gold, }); dot.copy({ translate: { y: 2 }, color: gold, }); dot.copy({ translate: { x: -2 }, color: yellow, }); dot.copy({ translate: { x: 2 }, color: garnet, }); dot.copy({ translate: { z: -2 }, color: orange, }); dot.copy({ translate: { z: 2 }, color: eggplant, }); // ----- animate ----- // var ticker = 0; var cycleCount = 150; function animate() { spin(); illo.updateRenderGraph(); requestAnimationFrame( animate ); } function spin() { if ( !isSpinning ) { return; } var progress = ticker/cycleCount; var turn = Math.floor( progress % 4 ); var theta = Zdog.easeInOut( progress % 1, 3 ) * TAU; if ( turn == 0 || turn == 2 ) { model.rotate.y = theta; } else if ( turn == 1 ) { model.rotate.x = theta; } else if ( turn == 3 ) { model.rotate.z = theta; } ticker++; } animate(); ================================================ FILE: demos/box-cross/index.html ================================================ box cross ================================================ FILE: demos/dot-cube/dot-cube.js ================================================ // ----- setup ----- // var sceneSize = 24; var TAU = Zdog.TAU; var illo = new Zdog.Illustration({ element: '.illo', rotate: { x: TAU * -35/360, y: TAU * 1/8 }, dragRotate: true, resize: 'fullscreen', onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // var cube = new Zdog.Anchor({ addTo: illo, scale: 4, }); var oneUnit = new Zdog.Vector({ x: 1, y: 1 }); var side = new Zdog.Anchor({ addTo: cube, translate: { z: 1 }, }); var dot = new Zdog.Shape({ addTo: side, translate: oneUnit.copy(), stroke: 1, color: 'white', }); dot.copy({ translate: { x: -1, y: 1 } }); dot.copy({ translate: { x: 1, y: -1 } }); dot.copy({ translate: { x: -1, y: -1 } }); // more dots dot.copy({ translate: { x: 1 } }); dot.copy({ translate: { x: -1 } }); dot.copy({ translate: { y: -1 } }); dot.copy({ translate: { y: 1 } }); side.copyGraph({ translate: { z: -1 }, }); var midDot = dot.copy({ addTo: cube, }); midDot.copy({ translate: { x: -1, y: 1 } }); midDot.copy({ translate: { x: 1, y: -1 } }); midDot.copy({ translate: { x: -1, y: -1 } }); // ----- animate ----- // var keyframes = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: TAU/4 }, { x: -TAU/4, y: 0, z: TAU/4 }, { x: -TAU/4, y: 0, z: TAU/2 }, ]; var ticker = 0; var cycleCount = 75; var turnLimit = keyframes.length - 1; function animate() { var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 4 ); var turn = Math.floor( progress % turnLimit ); var keyA = keyframes[ turn ]; var keyB = keyframes[ turn + 1 ]; cube.rotate.x = Zdog.lerp( keyA.x, keyB.x, tween ); cube.rotate.y = Zdog.lerp( keyA.y, keyB.y, tween ); cube.rotate.z = Zdog.lerp( keyA.z, keyB.z, tween ); ticker++; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/dot-cube/index.html ================================================ dot cube ================================================ FILE: demos/fullscreen/fullscreen.js ================================================ // ----- setup ----- // var isSpinning = true; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; var illo = new Zdog.Illustration({ element: '.illo', zoom: 4, resize: 'fullscreen', dragRotate: true, onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.min( width, height ) / 50; }, }); // ----- model ----- // new Zdog.Rect({ width: 20, height: 20, addTo: illo, translate: { z: -10 }, stroke: 2, color: garnet, }); new Zdog.Ellipse({ diameter: 16, addTo: illo, translate: { z: 10 }, stroke: 4, color: eggplant, }); new Zdog.Shape({ path: [ { x: 0, z: 1 }, { x: -1, z: -1 }, { x: 1, z: -1 }, ], scale: { x: 5, z: 5 }, addTo: illo, stroke: 2, fill: true, color: gold, }); new Zdog.Shape({ translate: { x: 10, y: -5 }, addTo: illo, stroke: 7, color: orange, }); // ----- animate ----- // function animate() { illo.rotate.y += isSpinning ? 0.03 : 0; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/fullscreen/index.html ================================================ fullscreen ================================================ FILE: demos/hello-world-canvas/hello-world-canvas.js ================================================ // ----- variables ----- // var isSpinning = true; // ----- model ----- // var illo = new Zdog.Illustration({ element: '.illo', zoom: 5, dragRotate: true, onDragStart: function() { isSpinning = false; }, }); // circle new Zdog.Ellipse({ addTo: illo, diameter: 20, translate: { z: 10 }, stroke: 5, color: '#636', }); // square new Zdog.Rect({ addTo: illo, width: 20, height: 20, translate: { z: -10 }, stroke: 3, color: '#E62', fill: true, }); // ----- animate ----- // function animate() { illo.rotate.y += isSpinning ? 0.03 : 0; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/hello-world-canvas/index.html ================================================ Hello world canvas

Hello world canvas

================================================ FILE: demos/hello-world-svg/hello-world-svg.js ================================================ // ----- variables ----- // var isSpinning = true; // ----- model ----- // var illo = new Zdog.Illustration({ element: '.illo', zoom: 5, dragRotate: true, onDragStart: function() { isSpinning = false; }, }); // circle new Zdog.Ellipse({ addTo: illo, diameter: 20, translate: { z: 10 }, stroke: 5, color: '#636', }); // square new Zdog.Rect({ addTo: illo, width: 20, height: 20, translate: { z: -10 }, stroke: 3, color: '#E62', fill: true, }); // ----- animate ----- // function animate() { illo.rotate.y += isSpinning ? 0.03 : 0; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/hello-world-svg/index.html ================================================ Hello world SVG

Hello world SVG

================================================ FILE: demos/hemisphere-cone-ball/hemisphere-cone-ball.js ================================================ // ----- setup ----- // var sceneSize = 48; var isSpinning = true; var TAU = Zdog.TAU; var illo = new Zdog.Illustration({ element: '.illo', dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // colors var yellow = '#ED0'; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; // ----- model ----- // var hemi = new Zdog.Hemisphere({ addTo: illo, diameter: 13, translate: { y: -16 }, rotate: { x: -TAU/4 }, color: garnet, backface: eggplant, stroke: false, }); var cone = new Zdog.Cone({ addTo: illo, diameter: 13, length: 6.5, translate: { y: 16 }, rotate: { x: TAU/4 }, color: garnet, backface: eggplant, stroke: false, }); var colorWheel = [ eggplant, garnet, orange, gold, yellow ]; [ true, false ].forEach( function( isHemi ) { var shape = isHemi ? hemi : cone; for ( var i = 0; i < 5; i++ ) { var rotor1 = new Zdog.Anchor({ addTo: illo, rotate: { y: TAU/5 * i }, }); var rotor2 = new Zdog.Anchor({ addTo: rotor1, rotate: { x: TAU/6 }, }); shape.copy({ addTo: rotor2, color: colorWheel[i], backface: colorWheel[ ( i + 7 ) % 5 ], }); } } ); // ----- animate ----- // var keyframes = [ { x: TAU * 0, y: TAU * 0 }, { x: TAU/2, y: TAU/2 }, { x: TAU * 1, y: TAU * 1 }, ]; var ticker = 0; var cycleCount = 180; var turnLimit = keyframes.length - 1; function animate() { spin(); illo.updateRenderGraph(); requestAnimationFrame( animate ); } function spin() { if ( !isSpinning ) { return; } var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 3 ); var turn = Math.floor( progress % turnLimit ); var keyA = keyframes[ turn ]; var keyB = keyframes[ turn + 1 ]; var thetaX = Zdog.lerp( keyA.x, keyB.x, tween ); illo.rotate.x = Math.cos( thetaX ) * TAU/12; illo.rotate.y = Zdog.lerp( keyA.y, keyB.y, tween ); ticker++; } animate(); ================================================ FILE: demos/hemisphere-cone-ball/index.html ================================================ hemisphere-cone-ball ================================================ FILE: demos/houses/houses.js ================================================ // ----- setup ----- // var sceneSize = 56; var isSpinning = true; var TAU = Zdog.TAU; var offWhite = '#FED'; var yellow = '#ED0'; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; // enable fill, disable stroke for all defaults [ Zdog.Rect, Zdog.Shape, Zdog.Ellipse ].forEach( function( Item ) { Item.defaults.fill = true; Item.defaults.stroke = false; } ); var initRotate = { y: TAU/8 }; var turnRatio = 1 / Math.sin( TAU/8 ); var illo = new Zdog.Illustration({ element: '.illo', rotate: initRotate, // stretch looks circular at 1/8 turn scale: { x: turnRatio, z: turnRatio }, dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // var house = new Zdog.Anchor({ addTo: illo, translate: { x: -2, y: 2, z: 8 }, }); var frontGroup = new Zdog.Group({ addTo: house, translate: { z: 5 }, }); // front wall new Zdog.Rect({ addTo: frontGroup, width: 14, height: 14, color: garnet, }); var frontWindow = new Zdog.Rect({ addTo: frontGroup, width: 2, height: 4, translate: { x: -4, y: -3 }, color: eggplant, }); frontWindow.copy({ translate: { y: -3 }, }); frontWindow.copy({ translate: { x: 4, y: -3 }, }); frontWindow.copy({ translate: { x: -4, y: 3 }, }); // door new Zdog.Shape({ addTo: frontGroup, path: [ { x: -2, y: 3 }, { x: -2, y: -1 }, { arc: [ { x: -2, y: -3 }, { x: 0, y: -3 }, ] }, { arc: [ { x: 2, y: -3 }, { x: 2, y: -1 }, ] }, { x: 2, y: 3 }, ], translate: { x: 2, y: 4 }, color: eggplant, }); // backWall var backGroup = frontGroup.copyGraph({ translate: { z: -5 }, rotate: { y: TAU/2 }, }); backGroup.children.forEach( function( child, i ) { // orange windows, yellow wall child.color = i ? orange : yellow; } ); var rightGroup = new Zdog.Group({ addTo: house, translate: { x: 7 }, rotate: { y: -TAU/4 }, }); // right wall new Zdog.Shape({ addTo: rightGroup, path: [ { x: -5, y: 7 }, { x: -5, y: -7 }, { x: 0, y: -12 }, { x: 5, y: -7 }, { x: 5, y: 7 }, ], width: 10, height: 14, color: offWhite, }); var sideWindow = frontWindow.copy({ addTo: rightGroup, translate: { x: -2, y: -3 }, color: gold, }); sideWindow.copy({ translate: { x: 2, y: -3 }, }); sideWindow.copy({ translate: { x: 2, y: 3 }, }); sideWindow.copy({ translate: { x: -2, y: 3 }, }); // porthole new Zdog.Ellipse({ addTo: rightGroup, width: 2, height: 2, translate: { y: -8 }, color: gold, }); var leftGroup = rightGroup.copyGraph({ translate: { x: -7 }, rotate: { y: TAU/4 }, }); leftGroup.children.forEach( function( child, i ) { // eggplant windows, yellow wall child.color = i ? eggplant : orange; } ); // front roof var frontRoof = new Zdog.Shape({ addTo: house, path: [ { x: -7, y: -7, z: 5 }, { x: -7, y: -12, z: 0 }, { x: 7, y: -12, z: 0 }, { x: 7, y: -7, z: 5 }, ], color: eggplant, }); frontRoof.copy({ scale: { z: -1 }, color: garnet, }); // floor new Zdog.Rect({ addTo: house, width: 14, height: 10, translate: { y: 7 }, rotate: { x: TAU/4 }, color: eggplant, }); house.copyGraph({ translate: house.translate.copy().multiply( -1 ), }); // ----- animate ----- // var ticker = 0; var cycleCount = 240; function animate() { if ( isSpinning ) { var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 3 ); illo.rotate.y = tween * TAU + initRotate.y; ticker++; } illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/houses/index.html ================================================ houses
================================================ FILE: demos/kid-kit/index.html ================================================ Kid Kit

Kid Kit

================================================ FILE: demos/kid-kit/kid-kit.js ================================================ // ----- setup ----- // var offWhite = '#FED'; var gold = '#EA0'; var garnet = '#C25'; var eggplant = '#636'; var illo = new Zdog.Illustration({ element: '.illo', zoom: 4, dragRotate: true, }); // ----- model ----- // // body center new Zdog.Shape({ path: [ { x: -3, y: 10 }, { x: 0, y: 14 }, { x: 3, y: 10 }, ], addTo: illo, color: offWhite, stroke: 13, }); // head circle new Zdog.Shape({ addTo: illo, translate: { y: -12 }, color: gold, stroke: 32, }); // nose var nose = new Zdog.Anchor({ addTo: illo, translate: { y: -7, z: 17 }, }); new Zdog.Shape({ path: [ { x: -1 }, { x: 1 }, ], addTo: nose, color: eggplant, stroke: 3, }); new Zdog.Shape({ path: [ { y: 0 }, { y: 1 }, ], addTo: nose, color: eggplant, stroke: 3, }); // snout new Zdog.Shape({ path: [ { x: -2, y: -5, z: 11 }, { x: 2, y: -5, z: 11 }, { x: 2, y: -3, z: 7 }, { x: -2, y: -3, z: 7 }, ], addTo: illo, color: gold, stroke: 12, }); // eyes var eye = new Zdog.Shape({ path: [ { y: -12 }, { y: -9 }, ], addTo: illo, translate: { x: -8, z: 11 }, color: eggplant, stroke: 4, }); eye.copy({ translate: { x: 8, z: 11 }, }); // ears var frontEarZ = 4; var topEarY = -30; var earColor = gold; var earAnchor = new Zdog.Anchor({ addTo: illo, translate: { y: topEarY, z: frontEarZ }, }); var earA = { x: 14, y: 12, z: -4 }; var earB = { x: 14, y: 0, z: 0 }; var earC = { x: 7, y: 11, z: -14 }; var earD = { x: 10, y: 0, z: 0 }; var earE = { x: 3, y: 5, z: 0 }; // outer ear new Zdog.Shape({ path: [ earA, earB, earC ], addTo: earAnchor, color: earColor, fill: true, stroke: 4, }); new Zdog.Shape({ path: [ earB, earC, earD ], addTo: earAnchor, color: earColor, fill: true, stroke: 4, }); new Zdog.Shape({ path: [ earC, earD, earE ], addTo: earAnchor, color: earColor, fill: true, stroke: 4, }); // inner ear var innerEarXShift = 4; new Zdog.Shape({ path: [ { x: earA.x - innerEarXShift, y: earA.y - 3 }, { x: earD.x, y: earD.y + 5 }, { x: earE.x + innerEarXShift, y: earE.y + 2 }, ], addTo: earAnchor, color: offWhite, fill: true, stroke: 3, }); earAnchor.copyGraph({ scale: { x: -1 }, }); // var whiskerX0 = 10*xSide; // var whiskerX1 = 17*xSide; // var whiskerY0 = -6+yShift; // var whiskerY1 = -2+yShift; // whiskers var whisker = new Zdog.Shape({ path: [ { x: 10, y: -6 }, { x: 10, y: -2 }, { x: 17, y: -2 }, ], addTo: illo, translate: { z: 6 }, fill: true, color: gold, stroke: 3, }); whisker.copy({ translate: { y: -6, z: 6 }, }); whisker.copy({ scale: { x: -1 }, }); whisker.copy({ scale: { x: -1 }, translate: { y: -6, z: 6 }, }); // arms var armAnchor = new Zdog.Anchor({ addTo: illo, }); // shoulder new Zdog.Shape({ path: [ { x: 11, y: 6, z: -2 }, { x: 12, y: 9, z: -2.5 }, ], addTo: armAnchor, closed: true, color: eggplant, stroke: 8, }); // forearm new Zdog.Shape({ path: [ { x: 12, y: 12, z: -2.5 }, { x: 12, y: 15, z: -2 }, ], addTo: armAnchor, color: gold, stroke: 8, }); // hand new Zdog.Shape({ path: [ { x: 11, y: 18, z: -1 } ], addTo: armAnchor, color: eggplant, stroke: 10, }); armAnchor.copyGraph({ scale: { x: -1 }, }); // legs var leg = new Zdog.Shape({ path: [ { y: 20 }, { y: 27 }, ], addTo: illo, translate: { x: -6 }, color: eggplant, stroke: 8, }); leg.copy({ translate: { x: 6 }, }); var cloakX0 = 8; var cloakX1 = 5; var cloakY0 = 4; var cloakY1 = 6; var cloakY2 = 13; var cloakY3 = 21; var cloakZ0 = 0; var cloakZ1 = 6; var cloakZ2 = 8; var cloakSide = new Zdog.Anchor({ addTo: illo, }); // top straps var topCloakStrap = new Zdog.Shape({ path: [ { x: cloakX0, y: cloakY0, z: cloakZ0 }, { x: cloakX0, y: cloakY1, z: cloakZ1 }, { x: cloakX1, y: cloakY1, z: cloakZ1 }, ], addTo: cloakSide, fill: true, color: garnet, stroke: 4, }); topCloakStrap.copy({ scale: { x: -1 }, }); var vNeckY = ( cloakY1 + cloakY2 )/2; var vNeckZ = ( cloakZ2 + cloakZ1 )/2; new Zdog.Shape({ path: [ { x: -cloakX0, y: cloakY1, z: cloakZ1 }, { x: -cloakX1, y: cloakY1, z: cloakZ1 }, { x: 0, y: vNeckY, z: vNeckZ }, { x: cloakX1, y: cloakY1, z: cloakZ1 }, { x: cloakX0, y: cloakY1, z: cloakZ1 }, { x: cloakX0, y: cloakY2, z: cloakZ2 }, { x: -cloakX0, y: cloakY2, z: cloakZ2 }, ], addTo: cloakSide, fill: true, color: garnet, stroke: 4, }); new Zdog.Shape({ path: [ { x: -cloakX0, y: cloakY2 }, { x: cloakX0, y: cloakY2 }, { x: cloakX0, y: cloakY3 }, { x: -cloakX0, y: cloakY3 }, ], addTo: cloakSide, translate: { z: cloakZ2 }, fill: true, color: garnet, stroke: 4, }); cloakSide.copyGraph({ scale: { z: -1 }, }); // ----- animate ----- // function animate() { illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/kirby-parasol/index.html ================================================ kirby parasol ================================================ FILE: demos/kirby-parasol/kirby-parasol.js ================================================ // ----- setup ----- // var sceneSize = 80; var isSpinning = true; var TAU = Zdog.TAU; // colors var pink = '#F8B'; var blush = '#F5A'; var black = '#333'; var shoe = '#D03'; var red = '#E10'; var yellow = '#FD0'; var illo = new Zdog.Illustration({ element: '.illo', dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // var body = new Zdog.Shape({ stroke: 22, translate: { y: 11 }, rotate: { x: 0.3, z: 0.1 }, addTo: illo, color: pink, }); var face = new Zdog.Anchor({ translate: { z: 10.5 }, addTo: body, }); [ -1, 1 ].forEach( function( xSide ) { var eyeGroup = new Zdog.Group({ addTo: face, translate: { x: 2.4 * xSide, y: -2 }, rotate: { x: -0.1 }, }); // eye new Zdog.Ellipse({ width: 1.4, height: 5.5, addTo: eyeGroup, stroke: 1, color: black, fill: true, }); // eye highlight new Zdog.Ellipse({ width: 1, height: 2, addTo: eyeGroup, translate: { y: -1.5, z: 0.5 }, stroke: 0.5, color: '#FFF', fill: true, }); // cheek holder var cheekHolder = new Zdog.Anchor({ addTo: body, rotate: { y: 0.6 * xSide }, }); new Zdog.Ellipse({ width: 2.5, height: 1, translate: { y: 1, z: 10.5 }, addTo: cheekHolder, color: blush, stroke: 1, }); } ); // mouth new Zdog.Shape({ path: [ { x: 0, y: 0 }, { bezier: [ { x: 1.1, y: 0 }, { x: 1.1, y: 0.2 }, { x: 1.1, y: 0.5 }, ] }, { bezier: [ { x: 1.1, y: 1.1 }, { x: 0.2, y: 1.8 }, { x: 0, y: 1.8 }, ] }, { bezier: [ { x: -0.2, y: 1.8 }, { x: -1.1, y: 1.1 }, { x: -1.1, y: 0.5 }, ] }, { bezier: [ { x: -1.1, y: 0.2 }, { x: -1.1, y: 0 }, { x: 0, y: 0 }, ] }, ], addTo: face, translate: { y: 2, z: -0.5 }, stroke: 1, color: shoe, fill: true, }); var rightArm = new Zdog.Shape({ path: [ { y: 0 }, { y: -7 }, ], addTo: body, translate: { x: -6, y: -4, z: 0 }, color: pink, stroke: 7, }); // left arm rightArm.copy({ path: [ { x: 0 }, { x: 6 }, ], translate: { x: 6, y: -2, z: 0 }, }); // right foot var rightFoot = new Zdog.Shape({ path: [ { x: 0, y: -2 }, { arc: [ { x: 2, y: -2 }, { x: 2, y: 0 }, ] }, { arc: [ { x: 2, y: 5 }, { x: 0, y: 5 }, ] }, { arc: [ { x: -2, y: 5 }, { x: -2, y: 0 }, ] }, { arc: [ { x: -2, y: -2 }, { x: 0, y: -2 }, ] }, ], addTo: body, translate: { x: -1, y: 9, z: -9 }, rotate: { z: 0.2 }, stroke: 6, color: shoe, fill: true, closed: false, }); rightFoot.copy({ translate: { x: 9.5, y: 6, z: -6 }, rotate: { z: -1.1, y: 0.8 }, }); // ----- umbrella ----- // // umbrella rod var umbrella = new Zdog.Shape({ path: [ { y: 0 }, { y: 22 }, ], addTo: rightArm, translate: { y: -33, z: 2 }, rotate: { y: 0.5 }, color: yellow, stroke: 1, }); // star var starPath = ( function() { var path = []; var starRadiusA = 3; var starRadiusB = 1.7; for ( var i = 0; i < 10; i++ ) { var radius = i % 2 ? starRadiusA : starRadiusB; var angle = TAU * i/10 + TAU/4; var point = { x: Math.cos( angle ) * radius, y: Math.sin( angle ) * radius, }; path.push( point ); } return path; } )(); // star shape var star = new Zdog.Shape({ path: starPath, addTo: umbrella, translate: { y: -4.5 }, stroke: 2, color: yellow, fill: true, }); // umbrella handle new Zdog.Shape({ path: [ { z: 0, y: 0 }, { z: 0, y: 1 }, { arc: [ { z: 0, y: 4 }, { z: 3, y: 4 }, ] }, { arc: [ { z: 6, y: 4 }, { z: 6, y: 1 }, ] }, ], addTo: umbrella, translate: { y: 23 }, stroke: 2, color: '#37F', closed: false, }); // umbrella shield panels ( function() { var umbPanelX = 14 * Math.sin( TAU/24 ); var umbPanelZ = 14 * Math.cos( TAU/24 ); for ( var i = 0; i < 12; i++ ) { var colorSide = Math.floor( i/2 ) % 2; new Zdog.Shape({ path: [ { x: 0, y: 0, z: 0 }, { arc: [ { x: -umbPanelX, y: 0, z: umbPanelZ }, { x: -umbPanelX, y: 14, z: umbPanelZ }, ] }, { x: umbPanelX, y: 14, z: umbPanelZ }, { arc: [ { x: umbPanelX, y: 0, z: umbPanelZ }, { x: 0, y: 0, z: 0 }, ] }, ], addTo: umbrella, rotate: { y: TAU/12 * i }, stroke: 1, color: colorSide ? red : 'white', fill: true, }); } } )(); // floater stars ( function() { for ( var i = 0; i < 6; i++ ) { var starHolder = new Zdog.Anchor({ addTo: umbrella, translate: { y: 10 }, rotate: { y: TAU/6 * i + TAU/24 }, }); star.copy({ addTo: starHolder, translate: { z: 28 }, }); } } )(); // ----- animate ----- // function animate() { illo.rotate.y += isSpinning ? -0.03 : 0; illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/no-illo-canvas/index.html ================================================ No Illo canvas

No Illo canvas

================================================ FILE: demos/no-illo-canvas/no-illo-canvas.js ================================================ // ----- setup ----- // // get canvas element and its context var canvas = document.querySelector('.zdog-canvas'); var ctx = canvas.getContext('2d'); // get canvas size var canvasWidth = canvas.width; var canvasHeight = canvas.height; // illustration variables var zoom = 5; var isSpinning = true; var TAU = Zdog.TAU; // ----- model ----- // var scene = new Zdog.Anchor(); // circle new Zdog.Ellipse({ addTo: scene, diameter: 20, translate: { z: 10 }, stroke: 5, color: '#636', }); // square new Zdog.Rect({ addTo: scene, width: 20, height: 20, translate: { z: -10 }, stroke: 3, color: '#E62', fill: true, }); // ----- animate ----- // function animate() { scene.rotate.y += isSpinning ? 0.03 : 0; scene.updateGraph(); render(); requestAnimationFrame( animate ); } function render() { // clear canvas ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); ctx.save(); // center canvas & zoom ctx.translate( canvasWidth/2, canvasHeight/2 ); ctx.scale( zoom, zoom ); // set lineJoin and lineCap to round ctx.lineJoin = 'round'; ctx.lineCap = 'round'; // render scene graph scene.renderGraphCanvas( ctx ); ctx.restore(); } animate(); // ----- drag ----- // var dragStartRX, dragStartRY; var minSize = Math.min( canvasWidth, canvasHeight ); // add drag-rotatation with Dragger new Zdog.Dragger({ startElement: canvas, onDragStart: function() { isSpinning = false; dragStartRX = scene.rotate.x; dragStartRY = scene.rotate.y; }, onDragMove: function( pointer, moveX, moveY ) { scene.rotate.x = dragStartRX - ( moveY/minSize * TAU ); scene.rotate.y = dragStartRY - ( moveX/minSize * TAU ); }, }); ================================================ FILE: demos/no-illo-svg/index.html ================================================ No Illo SVG

No Illo SVG

================================================ FILE: demos/no-illo-svg/no-illo-svg.js ================================================ // ----- setup ----- // // svg element var svg = document.querySelector('svg'); // set size var zoom = 5; var svgWidth = svg.getAttribute('width'); var svgHeight = svg.getAttribute('height'); // set viewBox for zoom & centering var viewWidth = svgWidth/zoom; var viewHeight = svgHeight/zoom; svg.setAttribute( 'viewBox', -viewWidth/2 + ' ' + -viewHeight/2 + ' ' + viewWidth + ' ' + viewHeight ); // rendering variable var isSpinning = true; var TAU = Zdog.TAU; var scene = new Zdog.Anchor(); // ----- model ----- // // circle new Zdog.Ellipse({ addTo: scene, diameter: 20, translate: { z: 10 }, stroke: 5, color: '#636', }); // square new Zdog.Rect({ addTo: scene, width: 20, height: 20, translate: { z: -10 }, stroke: 3, color: '#E62', fill: true, }); // ----- animate ----- // function animate() { scene.rotate.y += isSpinning ? 0.03 : 0; scene.updateGraph(); render(); requestAnimationFrame( animate ); } function render() { empty( svg ); scene.renderGraphSvg( svg ); } animate(); function empty( element ) { while ( element.firstChild ) { element.removeChild( element.firstChild ); } } // ----- drag ----- // var dragStartRX, dragStartRY; var minSize = Math.min( svgWidth, svgHeight ); // add drag-rotatation with Dragger new Zdog.Dragger({ startElement: svg, onDragStart: function() { isSpinning = false; dragStartRX = scene.rotate.x; dragStartRY = scene.rotate.y; }, onDragMove: function( pointer, moveX, moveY ) { scene.rotate.x = dragStartRX - ( moveY/minSize * TAU ); scene.rotate.y = dragStartRY - ( moveX/minSize * TAU ); }, }); ================================================ FILE: demos/path-commands/index.html ================================================ Path commands

Path commands

================================================ FILE: demos/path-commands/path-commands.js ================================================ // ----- variables ----- // var eggplant = '#636'; // ----- model ----- // var illo = new Zdog.Illustration({ element: '.illo', zoom: 5, dragRotate: true, }); // lines new Zdog.Shape({ addTo: illo, path: [ { x: -6, y: -6 }, { x: 6, y: -6 }, { x: -6, y: 6 }, { x: 6, y: 6 }, ], translate: { x: -12, y: -12 }, closed: false, color: eggplant, stroke: 2, }); // move new Zdog.Shape({ addTo: illo, path: [ { x: -6, y: -6 }, { x: 6, y: -6 }, { move: { x: -6, y: 6 } }, { x: 6, y: 6 }, ], translate: { x: 12, y: -12 }, closed: false, color: eggplant, stroke: 2, }); // arc new Zdog.Shape({ addTo: illo, path: [ { x: -6, y: -6 }, // start { arc: [ { x: 2, y: -6 }, // corner { x: 2, y: 2 }, // end point ] }, { arc: [ // start next arc from last end point { x: 2, y: 6 }, // corner { x: 6, y: 6 }, // end point ] }, ], translate: { x: -12, y: 12 }, closed: false, color: eggplant, stroke: 2, }); // bezier new Zdog.Shape({ addTo: illo, path: [ { x: -6, y: -6 }, // start { bezier: [ { x: 2, y: -6 }, // start control point { x: 2, y: 6 }, // end control point { x: 6, y: 6 }, // end control point ] }, ], translate: { x: 12, y: 12 }, closed: false, color: eggplant, stroke: 2, }); // ----- animate ----- // function animate() { illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/resize/index.html ================================================ resize ================================================ FILE: demos/resize/resize.js ================================================ // ----- setup ----- // var zoom = 4; var isSpinning = true; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; var model = new Zdog.Anchor(); var canvasIllo = new Zdog.Illustration({ element: 'canvas', zoom: zoom, resize: true, dragRotate: model, onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.min( width, height ) / 50; }, }); var svgIllo = new Zdog.Illustration({ element: 'svg', zoom: zoom, resize: true, dragRotate: model, onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.min( width, height ) / 50; }, }); // HACK set initial zoom for SVG svgIllo.setSize( svgIllo.width, svgIllo.height ); // ----- model ----- // new Zdog.Rect({ width: 20, height: 20, addTo: model, translate: { z: -10 }, stroke: 2, color: garnet, }); new Zdog.Ellipse({ diameter: 16, addTo: model, translate: { z: 10 }, stroke: 4, color: eggplant, }); new Zdog.Shape({ path: [ { x: 0, z: 1 }, { x: -1, z: -1 }, { x: 1, z: -1 }, ], scale: { x: 5, z: 5 }, addTo: model, stroke: 2, fill: true, color: gold, }); new Zdog.Shape({ translate: { x: 10, y: -5 }, addTo: model, stroke: 7, color: orange, }); model.copyGraph({ addTo: svgIllo, }); // ----- animate ----- // function animate() { model.rotate.y += isSpinning ? 0.03 : 0; model.updateGraph(); svgIllo.renderGraph( model ); canvasIllo.renderGraph( model ); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/shade-and-shades/index.html ================================================ shade & shades ================================================ FILE: demos/shade-and-shades/shade-and-shades.js ================================================ // ----- setup ----- // var sceneSize = 96; var orange = '#E62'; var eggplant = '#636'; // shape defaults Zdog.Shape.defaults.closed = false; [ Zdog.Shape, Zdog.Ellipse ].forEach( function( ShapeClass ) { ShapeClass.defaults.stroke = 3; ShapeClass.defaults.color = orange; } ); var isSpinning = true; var TAU = Zdog.TAU; var initialRotate = { y: -TAU/8 }; var illo = new Zdog.Illustration({ element: '.illo', rotate: initialRotate, dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // // cap top [ 0, 1, 2, 3, 4 ].forEach( function( i ) { new Zdog.Shape({ path: [ { x: -20, y: 4 }, { x: -20, y: 0 }, { arc: [ { x: -20, y: -20 }, { x: 0, y: -20 }, ] }, ], rotate: { y: TAU/6 * i - TAU/12 }, addTo: illo, }); } ); // cap back new Zdog.Ellipse({ addTo: illo, diameter: 40, quarters: 2, translate: { y: 4 }, rotate: { x: TAU/4, z: -TAU/4 }, }); // cap back to brim bottom connect var brimConnector = new Zdog.Shape({ path: [ { x: -20, z: 0 }, { arc: [ { x: -20, z: 6 }, { x: -16, z: 12 }, ] }, ], addTo: illo, translate: { y: 4 }, }); brimConnector.copy({ scale: { x: -1 }, }); // brim back arch new Zdog.Ellipse({ addTo: illo, diameter: 32, quarters: 2, translate: { y: 4, z: 12 }, rotate: { z: -TAU/4 }, }); var brimTip = new Zdog.Vector({ x: 0, y: -12, z: 34 }); var brimEdge = brimTip.copy(); brimEdge.x = -14; // brim top line new Zdog.Shape({ addTo: illo, path: [ { x: 0, y: -12, z: 12 }, brimTip, ], }); var brimBridge = new Zdog.Shape({ addTo: illo, path: [ { x: -16, y: 4, z: 12 }, { x: -16, y: 4, z: 18 }, { bezier: [ { x: -16, y: 4, z: 30 }, brimEdge, brimTip, ] }, ], }); brimBridge.copy({ scale: { x: -1 }, }); // glasses front top var glassFront = new Zdog.Shape({ addTo: illo, path: [ { x: -16 }, { x: 16 }, ], translate: { y: 8, z: 12 }, color: eggplant, }); // glass lens var glassLens = new Zdog.Shape({ addTo: glassFront, path: [ { x: -1, y: -1 }, { x: 1, y: -1 }, { x: 1, y: 0 }, { arc: [ { x: 1, y: 1 }, { x: 0, y: 1 }, ] }, { arc: [ { x: -1, y: 1 }, { x: -1, y: 0 }, ] }, ], closed: true, scale: 5, translate: { x: -8, y: 5 }, color: eggplant, fill: true, }); glassLens.copy({ translate: { x: 8, y: 5 }, }); // glasses arm var glassesArm = new Zdog.Shape({ addTo: illo, path: [ { x: 12, y: 0 }, { x: -1, y: 0 }, { arc: [ { x: -12, y: 0 }, { x: -12, y: 8 }, ] }, ], rotate: { y: TAU/4 }, translate: { x: -16, y: 8 }, color: eggplant, // only see one arm at time backface: false, }); glassesArm.copy({ scale: { x: -1 }, rotate: { y: -TAU/4 }, translate: { x: 16, y: 8 }, }); // ----- animate ----- // var ticker = 0; var cycleCount = 150; function animate() { if ( isSpinning ) { var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 4 ); illo.rotate.y = tween * TAU + initialRotate.y; ticker++; } illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/shapes/index.html ================================================ shapes ================================================ FILE: demos/shapes/shapes.js ================================================ // ----- setup ----- // var sceneSize = 24; var isSpinning = true; var TAU = Zdog.TAU; var offWhite = '#FED'; var gold = '#EA0'; var orange = '#E62'; var garnet = '#C25'; var eggplant = '#636'; var illo = new Zdog.Illustration({ element: '.illo', dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // new Zdog.Rect({ addTo: illo, width: 4, height: 4, translate: { x: -4, y: -4, z: 4 }, stroke: 1, color: orange, }); new Zdog.RoundedRect({ addTo: illo, width: 4, height: 4, cornerRadius: 1, translate: { x: -4, y: 4, z: -4 }, stroke: 1, color: eggplant, }); new Zdog.Ellipse({ addTo: illo, diameter: 4, translate: { x: 4, y: 4, z: 4 }, stroke: 1, color: garnet, }); new Zdog.Polygon({ addTo: illo, sides: 3, radius: 2.5, translate: { x: 4, y: -4, z: -4 }, stroke: 1, color: orange, }); new Zdog.Shape({ addTo: illo, path: [ { x: -1 }, { x: 1 }, { move: { y: -1 } }, { y: 1 }, { move: { z: -1 } }, { z: 1 }, ], scale: 1.25, stroke: 1, color: offWhite, }); new Zdog.Hemisphere({ addTo: illo, diameter: 5, translate: { x: -4, y: -4, z: -4 }, color: garnet, backface: gold, stroke: false, }); new Zdog.Cylinder({ addTo: illo, diameter: 5, length: 4, translate: { x: -4, y: 4, z: 4 }, color: gold, backface: offWhite, stroke: false, }); new Zdog.Cone({ addTo: illo, diameter: 5, length: 4, translate: { x: 4, y: -4, z: 4 }, color: eggplant, backface: garnet, stroke: false, }); new Zdog.Box({ addTo: illo, width: 5, height: 5, depth: 5, translate: { x: 4, y: 4, z: -4 }, color: orange, topFace: gold, leftFace: garnet, rightFace: garnet, bottomFace: eggplant, stroke: false, }); // ----- animate ----- // var ticker = 0; var cycleCount = 360; function animate() { if ( isSpinning ) { var progress = ticker/cycleCount; var theta = Zdog.easeInOut( progress % 1, 3 ) * TAU; illo.rotate.y = theta * 2; illo.rotate.x = Math.sin( theta ) * 0.5; ticker++; } illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/solids/index.html ================================================ solids ================================================ FILE: demos/solids/solids.js ================================================ // ----- setup ----- // var illoElem = document.querySelector('.illo'); var sceneSize = 96; var TAU = Zdog.TAU; var ROOT3 = Math.sqrt( 3 ); var ROOT5 = Math.sqrt( 5 ); var PHI = ( 1 + ROOT5 )/2; var isSpinning = true; var viewRotation = new Zdog.Vector(); var displaySize; // colors var eggplant = '#636'; var garnet = '#C25'; var orange = '#E62'; var gold = '#EA0'; var yellow = '#ED0'; var illo = new Zdog.Illustration({ element: illoElem, scale: 8, resize: 'fullscreen', onResize: function( width, height ) { displaySize = Math.min( width, height ); this.zoom = Math.floor( displaySize/sceneSize ); }, }); var solids = []; // ----- hourglass ----- // ( function() { var hourglass = new Zdog.Anchor({ addTo: illo, translate: { x: 0, y: -4 }, }); solids.push( hourglass ); var hemi = new Zdog.Hemisphere({ diameter: 2, translate: { z: -1 }, addTo: hourglass, color: garnet, backface: orange, stroke: false, }); hemi.copy({ translate: { z: 1 }, rotate: { y: TAU/2 }, color: eggplant, backface: gold, }); } )(); // ----- sphere ----- // ( function() { var sphere = new Zdog.Anchor({ addTo: illo, translate: { x: -4, y: -4 }, }); solids.push( sphere ); var hemi = new Zdog.Hemisphere({ diameter: 2, addTo: sphere, color: orange, backface: eggplant, stroke: false, }); hemi.copy({ rotate: { y: TAU/2 }, color: eggplant, backface: orange, }); } )(); // ----- cylinder ----- // var cylinder = new Zdog.Cylinder({ diameter: 2, length: 2, addTo: illo, translate: { x: 4, y: -4 }, // rotate: { x: TAU/4 }, color: gold, backface: garnet, stroke: false, }); solids.push( cylinder ); // ----- cone ----- // var cone = new Zdog.Anchor({ addTo: illo, translate: { x: -4, y: 0 }, }); solids.push( cone ); new Zdog.Cone({ diameter: 2, length: 2, addTo: cone, translate: { z: 1 }, rotate: { y: TAU/2 }, color: garnet, backface: gold, stroke: false, }); // ----- tetrahedron ----- // ( function() { var tetrahedron = new Zdog.Anchor({ addTo: illo, translate: { x: 0, y: 0 }, scale: 2.5, }); var radius = 0.5; var inradius = Math.cos( TAU/6 ) * radius; var height = radius + inradius; solids.push( tetrahedron ); var triangle = new Zdog.Polygon({ sides: 3, radius: radius, addTo: tetrahedron, translate: { y: height/2 }, fill: true, stroke: false, color: eggplant, // backface: false, }); for ( var i = 0; i < 3; i++ ) { var rotor1 = new Zdog.Anchor({ addTo: tetrahedron, rotate: { y: TAU/3 * -i }, }); var rotor2 = new Zdog.Anchor({ addTo: rotor1, translate: { z: inradius, y: height/2 }, rotate: { x: Math.acos( 1/3 ) * -1 + TAU/4 }, }); triangle.copy({ addTo: rotor2, translate: { y: -inradius }, color: [ gold, garnet, orange ][i], }); } triangle.rotate.set({ x: -TAU/4, z: -TAU/2 }); } )(); // ----- octahedron ----- // ( function() { var octahedron = new Zdog.Anchor({ addTo: illo, translate: { x: -4, y: 4 }, scale: 1.75, }); solids.push( octahedron ); var colorWheel = [ eggplant, garnet, orange, gold, yellow ]; // radius of triangle with side length = 1 var radius = ROOT3/2 * 2/3; var height = radius * 3/2; var tilt = Math.asin( 0.5/height ); [ -1, 1 ].forEach( function( ySide ) { for ( var i = 0; i < 4; i++ ) { var rotor = new Zdog.Anchor({ addTo: octahedron, rotate: { y: TAU/4 * ( i + 1.5 ) * -1 }, }); var anchor = new Zdog.Anchor({ addTo: rotor, translate: { z: 0.5 }, rotate: { x: tilt * ySide }, // scale: { y: -ySide }, }); new Zdog.Polygon({ sides: 3, radius: radius, addTo: anchor, translate: { y: -radius/2 * ySide }, scale: { y: ySide }, stroke: false, fill: true, color: colorWheel[ i + 0.5 + 0.5 * ySide ], backface: false, }); } } ); } )(); // ----- cube ----- // var cube = new Zdog.Box({ addTo: illo, width: 2, height: 2, depth: 2, translate: { x: 4, y: 0 }, topFace: yellow, frontFace: gold, leftFace: orange, rightFace: orange, rearFace: garnet, bottomFace: eggplant, stroke: false, }); solids.push( cube ); // ----- dodecahedron ----- // ( function() { var dodecahedron = new Zdog.Anchor({ addTo: illo, translate: { x: 0, y: 4 }, scale: 0.75, }); solids.push( dodecahedron ); // https://en.wikipedia.org/wiki/Regular_dodecahedron#Dimensions var midradius = ( PHI * PHI )/2; // top & bottom faces var face = new Zdog.Polygon({ sides: 5, radius: 1, addTo: dodecahedron, translate: { y: -midradius }, rotate: { x: TAU/4 }, fill: true, stroke: false, color: yellow, // backface: false, }); face.copy({ translate: { y: midradius }, rotate: { x: -TAU/4 }, color: eggplant, }); [ -1, 1 ].forEach( function( ySide ) { var colorWheel = { '-1': [ eggplant, garnet, gold, orange, garnet ], 1: [ yellow, gold, garnet, orange, gold ], }[ ySide ]; for ( var i = 0; i < 5; i++ ) { var rotor1 = new Zdog.Anchor({ addTo: dodecahedron, rotate: { y: TAU/5 * i }, }); var rotor2 = new Zdog.Anchor({ addTo: rotor1, rotate: { x: TAU/4 * ySide - Math.atan( 2 ) }, }); face.copy({ addTo: rotor2, translate: { z: midradius }, rotate: { z: TAU/2 }, color: colorWheel[i], }); } } ); } )(); // ----- isocahedron ----- // ( function() { var isocahedron = new Zdog.Anchor({ addTo: illo, translate: { x: 4, y: 4 }, scale: 1.2, }); solids.push( isocahedron ); // geometry // radius of triangle with side length = 1 var faceRadius = ROOT3/2 * 2/3; var faceHeight = faceRadius * 3/2; var capApothem = 0.5 / Math.tan( TAU/10 ); var capRadius = 0.5 / Math.sin( TAU/10 ); var capTilt = Math.asin( capApothem/faceHeight ); var capSagitta = capRadius - capApothem; var sideTilt = Math.asin( capSagitta/faceHeight ); var sideHeight = Math.sqrt( faceHeight * faceHeight - capSagitta * capSagitta ); // var colorWheel = [ eggplant, garnet, orange, gold, yellow ]; [ -1, 1 ].forEach( function( ySide ) { var capColors = { '-1': [ garnet, gold, yellow, gold, orange ], 1: [ gold, garnet, eggplant, garnet, orange ], }[ ySide ]; var sideColors = { '-1': [ garnet, gold, yellow, orange, garnet ], 1: [ gold, garnet, eggplant, orange, orange ], }[ ySide ]; for ( var i = 0; i < 5; i++ ) { var rotor = new Zdog.Anchor({ addTo: isocahedron, rotate: { y: TAU/5 * -i }, translate: { y: sideHeight/2 * ySide }, }); var capRotateX = -capTilt; var isYPos = ySide > 0; capRotateX += isYPos ? TAU/2 : 0; var capAnchor = new Zdog.Anchor({ addTo: rotor, translate: { z: capApothem * ySide }, rotate: { x: capRotateX }, }); // cap face var face = new Zdog.Polygon({ sides: 3, radius: faceRadius, addTo: capAnchor, translate: { y: -faceRadius/2 }, stroke: false, fill: true, color: capColors[i], // backface: false, }); var sideRotateX = -sideTilt; sideRotateX += isYPos ? 0 : TAU/2; var sideAnchor = capAnchor.copy({ rotate: { x: sideRotateX }, }); face.copy({ addTo: sideAnchor, translate: { y: -faceRadius/2 }, rotate: { y: TAU/2 }, color: sideColors[i], }); } } ); } )(); // ----- animate ----- // var keyframes = [ { x: 0, y: 0 }, { x: 0, y: TAU }, { x: TAU, y: TAU }, ]; var ticker = 0; var cycleCount = 180; var turnLimit = keyframes.length - 1; function animate() { update(); illo.renderGraph(); requestAnimationFrame( animate ); } animate(); function update() { if ( isSpinning ) { var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 4 ); var turn = Math.floor( progress % turnLimit ); var keyA = keyframes[ turn ]; var keyB = keyframes[ turn + 1 ]; viewRotation.x = Zdog.lerp( keyA.x, keyB.x, tween ); viewRotation.y = Zdog.lerp( keyA.y, keyB.y, tween ); ticker++; } solids.forEach( function( solid ) { solid.rotate.set( viewRotation ); } ); illo.updateGraph(); } // ----- inputs ----- // var dragStartRX, dragStartRY; new Zdog.Dragger({ startElement: illoElem, onDragStart: function() { isSpinning = false; dragStartRX = viewRotation.x; dragStartRY = viewRotation.y; }, onDragMove: function( pointer, moveX, moveY ) { viewRotation.x = dragStartRX - ( moveY/displaySize * TAU ); viewRotation.y = dragStartRY - ( moveX/displaySize * TAU ); }, }); ================================================ FILE: demos/strutter/index.html ================================================ strutter ================================================ FILE: demos/strutter/strutter.js ================================================ // ----- setup ----- // var sceneSize = 48; var isSpinning = true; var TAU = Zdog.TAU; // colors var gold = '#EA0'; var orange = '#C25'; var eggplant = '#636'; var midnight = '#424'; var illo = new Zdog.Illustration({ element: '.illo', rotate: { y: -TAU/8 }, translate: { y: 4 }, dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) / sceneSize ); }, }); // ----- model ----- // var hipX = 3; new Zdog.Shape({ addTo: illo, path: [ { x: -1 }, { x: 1 } ], scale: hipX, color: eggplant, stroke: 4, }); var rightLeg = new Zdog.Shape({ addTo: illo, path: [ { y: 0 }, { y: 12 } ], translate: { x: -hipX }, rotate: { x: TAU/4 }, color: eggplant, stroke: 4, }); // foot new Zdog.RoundedRect({ addTo: rightLeg, width: 2, height: 4, cornerRadius: 1, translate: { y: 14, z: 2 }, rotate: { x: TAU/4 }, color: orange, fill: true, stroke: 4, }); var plantAngle = -TAU/32 * 3; var leftLeg = rightLeg.copyGraph({ translate: { x: hipX }, rotate: { x: plantAngle }, color: midnight, }); leftLeg.children[0].rotate.set({ x: TAU/4 - plantAngle }); // chest new Zdog.Shape({ addTo: illo, path: [ { x: -1 }, { x: 1 } ], scale: 1.5, translate: { y: -5.5, z: -3 }, color: orange, stroke: 9, fill: true, }); var armSize = 6; [ true, false ].forEach( function( isRight ) { var xSide = isRight ? -1 : 1; var upperArm = new Zdog.Shape({ addTo: illo, path: [ { x: 0 }, { x: armSize } ], scale: { x: xSide }, translate: { x: 4.5 * xSide, y: -8, z: -4 }, rotate: isRight ? { y: TAU/8, z: -TAU/16 } : { y: TAU/8 }, color: eggplant, stroke: 4, }); var forearm = new Zdog.Shape({ addTo: upperArm, path: [ { x: 0 }, { x: armSize - 2 } ], translate: { x: armSize }, rotate: isRight ? { z: TAU/16 * 3, y: TAU/4 } : { z: -TAU/4, x: -TAU/32 * 2, y: TAU/8 }, color: orange, stroke: 4, }); // hand new Zdog.Shape({ addTo: forearm, translate: { x: armSize, z: 1 }, stroke: 6, color: gold, }); } ); var head = new Zdog.Anchor({ addTo: illo, translate: { y: -12, z: -10 }, rotate: { x: TAU/8 }, }); // face new Zdog.Hemisphere({ addTo: head, diameter: 12, color: gold, backface: orange, rotate: { x: -TAU/4 }, stroke: false, }); var eye = new Zdog.Ellipse({ addTo: head, diameter: 2, quarters: 2, translate: { x: -2, y: 1.5, z: 5 }, rotate: { z: -TAU/4 }, color: eggplant, stroke: 0.5, backface: false, }); eye.copy({ translate: { x: 2, y: 1.5, z: 5 }, rotate: { z: -TAU/4 }, }); // smile new Zdog.Ellipse({ addTo: head, diameter: 3, quarters: 2, translate: { y: 3, z: 4.5 }, rotate: { z: TAU/4 }, closed: true, color: '#FED', stroke: 0.5, fill: true, backface: false, }); new Zdog.Hemisphere({ addTo: head, diameter: 12, color: orange, backface: gold, rotate: { x: TAU/4 }, stroke: false, }); var brim = new Zdog.Anchor({ addTo: head, scale: 5.5, translate: { y: -0.5, z: 6 }, }); new Zdog.Shape({ addTo: brim, path: [ { x: 0, z: 0 }, { arc: [ { x: -1, z: 0 }, { x: -1, z: -1 }, ] }, { x: -1, z: 0 }, ], color: eggplant, fill: true, }); new Zdog.Shape({ addTo: brim, path: [ { x: -1, z: 0 }, { arc: [ { x: -1, z: 1 }, { x: 0, z: 1 }, ] }, { x: 0, z: 0 }, ], color: eggplant, fill: true, }); brim.copyGraph({ scale: brim.scale.copy().multiply({ x: -1 }), }); // ----- animate ----- // var ticker = 0; var cycleCount = 150; function animate() { if ( isSpinning ) { var progress = ticker/cycleCount; illo.rotate.y = Zdog.easeInOut( progress % 1, 4 ) * TAU - TAU/8; ticker++; } illo.updateRenderGraph(); requestAnimationFrame( animate ); } animate(); ================================================ FILE: demos/zdog-logo/index.html ================================================ zdog logo ================================================ FILE: demos/zdog-logo/zdog-logo.js ================================================ // ----- setup ----- // var sceneSize = 100; var isSpinning = true; var TAU = Zdog.TAU; var initRotate = { x: 20/360 * TAU, y: -50/360 * TAU }; var orange = '#E62'; var gold = '#EA0'; var eggplant = '#636'; var depth = 20; var lineWidth = 8; var illo = new Zdog.Illustration({ element: '.illo', rotate: initRotate, dragRotate: true, resize: 'fullscreen', onDragStart: function() { isSpinning = false; }, onResize: function( width, height ) { this.zoom = Math.floor( Math.min( width, height ) * 2/sceneSize ) / 2; }, }); // ----- model ----- // var bigGroup = new Zdog.Group({ addTo: illo, }); var backGroup = new Zdog.Group({ addTo: bigGroup, updateSort: true, }); // top var topSide = new Zdog.Rect({ addTo: backGroup, width: 40, height: depth, translate: { y: -20 }, rotate: { x: TAU/4 }, fill: true, stroke: lineWidth, color: orange, }); topSide.copy({ translate: { y: 20 }, rotate: { x: -TAU/4 }, }); var endCap = new Zdog.Rect({ addTo: backGroup, width: depth, height: 8, translate: { x: -20, y: -16 }, rotate: { y: TAU/4 }, fill: true, color: orange, stroke: lineWidth, backface: false, }); endCap.copy({ translate: { x: 20, y: 16 }, rotate: { y: -TAU/4 }, }); var cornerCap = endCap.copy({ height: 10, translate: { x: -20, y: 15 }, }); cornerCap.copy({ translate: { x: 20, y: -15 }, rotate: { y: -TAU/4 }, }); var underside = new Zdog.Rect({ addTo: backGroup, width: 30, height: depth, translate: { x: -5, y: -12 }, rotate: { x: -TAU/4 }, stroke: lineWidth, fill: true, color: orange, }); underside.copy({ translate: { x: 5, y: 12 }, rotate: { x: TAU/4 }, }); var slopeW = 30; var slopeH = 22; var slopeAngle = Math.atan( slopeH/slopeW ); var slope = new Zdog.Rect({ addTo: backGroup, width: Math.sqrt( slopeH * slopeH + slopeW * slopeW ), height: depth, translate: { x: -5, y: -1 }, rotate: { x: TAU/4, y: slopeAngle }, stroke: lineWidth, fill: true, color: orange, backface: false, }); slope.copy({ translate: { x: 5, y: 1 }, rotate: { x: -TAU/4, y: -slopeAngle }, }); // tail new Zdog.Ellipse({ addTo: backGroup, diameter: 32, quarters: 1, closed: false, translate: { x: 22, y: -4 }, rotate: { z: TAU/4 }, color: orange, stroke: lineWidth, }); // tongue var tongueAnchor = new Zdog.Anchor({ addTo: backGroup, translate: { x: -6, y: -7 }, rotate: { y: TAU/4 }, }); var tongueH = 12; var tongueS = 5; var tongueTip = tongueH + tongueS; new Zdog.Shape({ addTo: tongueAnchor, path: [ { x: -tongueS, y: 0 }, { x: tongueS, y: 0 }, { x: tongueS, y: tongueH }, { arc: [ { x: tongueS, y: tongueTip }, { x: 0, y: tongueTip }, ] }, { arc: [ { x: -tongueS, y: tongueTip }, { x: -tongueS, y: tongueH }, ] }, ], rotate: { x: TAU/4 - Math.atan( 16/22 ) }, fill: true, stroke: 4, color: eggplant, }); var foreGroup = new Zdog.Group({ addTo: bigGroup, updateSort: true, }); var zFace = new Zdog.Shape({ addTo: foreGroup, path: [ { x: -20, y: -20 }, { x: 20, y: -20 }, { x: 20, y: -10 }, { x: -10, y: 12 }, { x: 20, y: 12 }, { x: 20, y: 20 }, { x: -20, y: 20 }, { x: -20, y: 10 }, { x: 10, y: -12 }, { x: -20, y: -12 }, ], translate: { z: depth/2 }, fill: true, color: gold, stroke: lineWidth, backface: false, }); zFace.copy({ scale: { x: -1 }, translate: { z: -depth/2 }, rotate: { y: TAU/2 }, }); // nose var semiCircle = new Zdog.Ellipse({ addTo: backGroup, quarters: 2, scale: 8, translate: { x: -26, y: -20 }, rotate: { y: TAU/4, z: TAU/4 }, fill: true, stroke: 5, color: eggplant, closed: true, // backface: false, }); // ears // group & extra shape are hacks var earGroup = new Zdog.Group({ addTo: illo, }); var ear = semiCircle.copy({ addTo: earGroup, quarters: 2, scale: 24, rotate: { z: -TAU/16, x: TAU/16 }, translate: { x: 10, y: -14, z: depth }, }); new Zdog.Shape({ visible: false, addTo: ear, translate: { z: 0.5, x: -0.5 }, }); earGroup.copyGraph({ scale: { z: -1 }, }); // ----- animate ----- // var keyframes = [ { y: 0 + initRotate.y, z: 0 }, { y: TAU + initRotate.y, z: 0 }, { y: TAU + initRotate.y, z: TAU }, ]; var ticker = 0; var cycleCount = 180; var turnLimit = keyframes.length - 1; function animate() { spin(); illo.updateRenderGraph(); requestAnimationFrame( animate ); } function spin() { if ( !isSpinning ) { return; } var progress = ticker/cycleCount; var tween = Zdog.easeInOut( progress % 1, 4 ); var turn = Math.floor( progress % turnLimit ); var keyA = keyframes[ turn ]; var keyB = keyframes[ turn + 1 ]; illo.rotate.y = Zdog.lerp( keyA.y, keyB.y, tween ); illo.rotate.z = Zdog.lerp( keyA.z, keyB.z, tween ); ticker++; } animate(); ================================================ FILE: dist/zdog.dist.js ================================================ /*! * Zdog v1.1.3 * Round, flat, designer-friendly pseudo-3D engine * Licensed MIT * https://zzz.dog * Copyright 2020 Metafizzy */ /** * Boilerplate & utils */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog = factory(); } }( this, function factory() { var Zdog = {}; Zdog.TAU = Math.PI * 2; Zdog.extend = function( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; }; Zdog.lerp = function( a, b, alpha ) { return ( b - a ) * alpha + a; }; Zdog.modulo = function( num, div ) { return ( ( num % div ) + div ) % div; }; var powerMultipliers = { 2: function( a ) { return a * a; }, 3: function( a ) { return a * a * a; }, 4: function( a ) { return a * a * a * a; }, 5: function( a ) { return a * a * a * a * a; }, }; Zdog.easeInOut = function( alpha, power ) { if ( power == 1 ) { return alpha; } alpha = Math.max( 0, Math.min( 1, alpha ) ); var isFirstHalf = alpha < 0.5; var slope = isFirstHalf ? alpha : 1 - alpha; slope /= 0.5; // make easing steeper with more multiples var powerMultiplier = powerMultipliers[ power ] || powerMultipliers[2]; var curve = powerMultiplier( slope ); curve /= 2; return isFirstHalf ? curve : 1 - curve; }; return Zdog; } ) ); /** * CanvasRenderer */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.CanvasRenderer = factory(); } }( this, function factory() { var CanvasRenderer = { isCanvas: true }; CanvasRenderer.begin = function( ctx ) { ctx.beginPath(); }; CanvasRenderer.move = function( ctx, elem, point ) { ctx.moveTo( point.x, point.y ); }; CanvasRenderer.line = function( ctx, elem, point ) { ctx.lineTo( point.x, point.y ); }; CanvasRenderer.bezier = function( ctx, elem, cp0, cp1, end ) { ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y ); }; CanvasRenderer.closePath = function( ctx ) { ctx.closePath(); }; CanvasRenderer.setPath = function() {}; CanvasRenderer.renderPath = function( ctx, elem, pathCommands, isClosed ) { this.begin( ctx, elem ); pathCommands.forEach( function( command ) { command.render( ctx, elem, CanvasRenderer ); } ); if ( isClosed ) { this.closePath( ctx, elem ); } }; CanvasRenderer.stroke = function( ctx, elem, isStroke, color, lineWidth ) { if ( !isStroke ) { return; } ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.stroke(); }; CanvasRenderer.fill = function( ctx, elem, isFill, color ) { if ( !isFill ) { return; } ctx.fillStyle = color; ctx.fill(); }; CanvasRenderer.end = function() {}; return CanvasRenderer; } ) ); /** * SvgRenderer */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.SvgRenderer = factory(); } }( this, function factory() { var SvgRenderer = { isSvg: true }; // round path coordinates to 3 decimals var round = SvgRenderer.round = function( num ) { return Math.round( num * 1000 ) / 1000; }; function getPointString( point ) { return round( point.x ) + ',' + round( point.y ) + ' '; } SvgRenderer.begin = function() {}; SvgRenderer.move = function( svg, elem, point ) { return 'M' + getPointString( point ); }; SvgRenderer.line = function( svg, elem, point ) { return 'L' + getPointString( point ); }; SvgRenderer.bezier = function( svg, elem, cp0, cp1, end ) { return 'C' + getPointString( cp0 ) + getPointString( cp1 ) + getPointString( end ); }; SvgRenderer.closePath = function( /* elem */) { return 'Z'; }; SvgRenderer.setPath = function( svg, elem, pathValue ) { elem.setAttribute( 'd', pathValue ); }; SvgRenderer.renderPath = function( svg, elem, pathCommands, isClosed ) { var pathValue = ''; pathCommands.forEach( function( command ) { pathValue += command.render( svg, elem, SvgRenderer ); } ); if ( isClosed ) { pathValue += this.closePath( svg, elem ); } this.setPath( svg, elem, pathValue ); }; SvgRenderer.stroke = function( svg, elem, isStroke, color, lineWidth ) { if ( !isStroke ) { return; } elem.setAttribute( 'stroke', color ); elem.setAttribute( 'stroke-width', lineWidth ); }; SvgRenderer.fill = function( svg, elem, isFill, color ) { var fillColor = isFill ? color : 'none'; elem.setAttribute( 'fill', fillColor ); }; SvgRenderer.end = function( svg, elem ) { svg.appendChild( elem ); }; return SvgRenderer; } ) ); /** * Vector */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate') ); } else { // browser global var Zdog = root.Zdog; Zdog.Vector = factory( Zdog ); } }( this, function factory( utils ) { function Vector( position ) { this.set( position ); } var TAU = utils.TAU; // 'pos' = 'position' Vector.prototype.set = function( pos ) { this.x = pos && pos.x || 0; this.y = pos && pos.y || 0; this.z = pos && pos.z || 0; return this; }; // set coordinates without sanitizing // vec.write({ y: 2 }) only sets y coord Vector.prototype.write = function( pos ) { if ( !pos ) { return this; } this.x = pos.x != undefined ? pos.x : this.x; this.y = pos.y != undefined ? pos.y : this.y; this.z = pos.z != undefined ? pos.z : this.z; return this; }; Vector.prototype.rotate = function( rotation ) { if ( !rotation ) { return; } this.rotateZ( rotation.z ); this.rotateY( rotation.y ); this.rotateX( rotation.x ); return this; }; Vector.prototype.rotateZ = function( angle ) { rotateProperty( this, angle, 'x', 'y' ); }; Vector.prototype.rotateX = function( angle ) { rotateProperty( this, angle, 'y', 'z' ); }; Vector.prototype.rotateY = function( angle ) { rotateProperty( this, angle, 'x', 'z' ); }; function rotateProperty( vec, angle, propA, propB ) { if ( !angle || angle % TAU === 0 ) { return; } var cos = Math.cos( angle ); var sin = Math.sin( angle ); var a = vec[ propA ]; var b = vec[ propB ]; vec[ propA ] = a * cos - b * sin; vec[ propB ] = b * cos + a * sin; } Vector.prototype.isSame = function( pos ) { if ( !pos ) { return false; } return this.x === pos.x && this.y === pos.y && this.z === pos.z; }; Vector.prototype.add = function( pos ) { if ( !pos ) { return this; } this.x += pos.x || 0; this.y += pos.y || 0; this.z += pos.z || 0; return this; }; Vector.prototype.subtract = function( pos ) { if ( !pos ) { return this; } this.x -= pos.x || 0; this.y -= pos.y || 0; this.z -= pos.z || 0; return this; }; Vector.prototype.multiply = function( pos ) { if ( pos == undefined ) { return this; } // multiple all values by same number if ( typeof pos == 'number' ) { this.x *= pos; this.y *= pos; this.z *= pos; } else { // multiply object this.x *= pos.x != undefined ? pos.x : 1; this.y *= pos.y != undefined ? pos.y : 1; this.z *= pos.z != undefined ? pos.z : 1; } return this; }; Vector.prototype.transform = function( translation, rotation, scale ) { this.multiply( scale ); this.rotate( rotation ); this.add( translation ); return this; }; Vector.prototype.lerp = function( pos, alpha ) { this.x = utils.lerp( this.x, pos.x || 0, alpha ); this.y = utils.lerp( this.y, pos.y || 0, alpha ); this.z = utils.lerp( this.z, pos.z || 0, alpha ); return this; }; Vector.prototype.magnitude = function() { var sum = this.x * this.x + this.y * this.y + this.z * this.z; return getMagnitudeSqrt( sum ); }; function getMagnitudeSqrt( sum ) { // PERF: check if sum ~= 1 and skip sqrt if ( Math.abs( sum - 1 ) < 0.00000001 ) { return 1; } return Math.sqrt( sum ); } Vector.prototype.magnitude2d = function() { var sum = this.x * this.x + this.y * this.y; return getMagnitudeSqrt( sum ); }; Vector.prototype.copy = function() { return new Vector( this ); }; return Vector; } ) ); /** * Anchor */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./canvas-renderer'), require('./svg-renderer') ); } else { // browser global var Zdog = root.Zdog; Zdog.Anchor = factory( Zdog, Zdog.Vector, Zdog.CanvasRenderer, Zdog.SvgRenderer ); } }( this, function factory( utils, Vector, CanvasRenderer, SvgRenderer ) { var TAU = utils.TAU; var onePoint = { x: 1, y: 1, z: 1 }; function Anchor( options ) { this.create( options || {} ); } Anchor.prototype.create = function( options ) { this.children = []; // set defaults & options utils.extend( this, this.constructor.defaults ); this.setOptions( options ); // transform this.translate = new Vector( options.translate ); this.rotate = new Vector( options.rotate ); this.scale = new Vector( onePoint ).multiply( this.scale ); // origin this.origin = new Vector(); this.renderOrigin = new Vector(); if ( this.addTo ) { this.addTo.addChild( this ); } }; Anchor.defaults = {}; Anchor.optionKeys = Object.keys( Anchor.defaults ).concat([ 'rotate', 'translate', 'scale', 'addTo', ]); Anchor.prototype.setOptions = function( options ) { var optionKeys = this.constructor.optionKeys; for ( var key in options ) { if ( optionKeys.indexOf( key ) != -1 ) { this[ key ] = options[ key ]; } } }; Anchor.prototype.addChild = function( shape ) { if ( this.children.indexOf( shape ) != -1 ) { return; } shape.remove(); // remove previous parent shape.addTo = this; // keep parent reference this.children.push( shape ); }; Anchor.prototype.removeChild = function( shape ) { var index = this.children.indexOf( shape ); if ( index != -1 ) { this.children.splice( index, 1 ); } }; Anchor.prototype.remove = function() { if ( this.addTo ) { this.addTo.removeChild( this ); } }; // ----- update ----- // Anchor.prototype.update = function() { // update self this.reset(); // update children this.children.forEach( function( child ) { child.update(); } ); this.transform( this.translate, this.rotate, this.scale ); }; Anchor.prototype.reset = function() { this.renderOrigin.set( this.origin ); }; Anchor.prototype.transform = function( translation, rotation, scale ) { this.renderOrigin.transform( translation, rotation, scale ); // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); } ); }; Anchor.prototype.updateGraph = function() { this.update(); this.updateFlatGraph(); this.flatGraph.forEach( function( item ) { item.updateSortValue(); } ); // z-sort this.flatGraph.sort( Anchor.shapeSorter ); }; Anchor.shapeSorter = function( a, b ) { return a.sortValue - b.sortValue; }; // custom getter to check for flatGraph before using it Object.defineProperty( Anchor.prototype, 'flatGraph', { get: function() { if ( !this._flatGraph ) { this.updateFlatGraph(); } return this._flatGraph; }, set: function( graph ) { this._flatGraph = graph; }, } ); Anchor.prototype.updateFlatGraph = function() { this.flatGraph = this.getFlatGraph(); }; // return Array of self & all child graph items Anchor.prototype.getFlatGraph = function() { var flatGraph = [ this ]; return this.addChildFlatGraph( flatGraph ); }; Anchor.prototype.addChildFlatGraph = function( flatGraph ) { this.children.forEach( function( child ) { var childFlatGraph = child.getFlatGraph(); Array.prototype.push.apply( flatGraph, childFlatGraph ); } ); return flatGraph; }; Anchor.prototype.updateSortValue = function() { this.sortValue = this.renderOrigin.z; }; // ----- render ----- // Anchor.prototype.render = function() {}; // TODO refactor out CanvasRenderer so its not a dependency within anchor.js Anchor.prototype.renderGraphCanvas = function( ctx ) { if ( !ctx ) { throw new Error( 'ctx is ' + ctx + '. ' + 'Canvas context required for render. Check .renderGraphCanvas( ctx ).' ); } this.flatGraph.forEach( function( item ) { item.render( ctx, CanvasRenderer ); } ); }; Anchor.prototype.renderGraphSvg = function( svg ) { if ( !svg ) { throw new Error( 'svg is ' + svg + '. ' + 'SVG required for render. Check .renderGraphSvg( svg ).' ); } this.flatGraph.forEach( function( item ) { item.render( svg, SvgRenderer ); } ); }; // ----- misc ----- // Anchor.prototype.copy = function( options ) { // copy options var itemOptions = {}; var optionKeys = this.constructor.optionKeys; optionKeys.forEach( function( key ) { itemOptions[ key ] = this[ key ]; }, this ); // add set options utils.extend( itemOptions, options ); var ItemClass = this.constructor; return new ItemClass( itemOptions ); }; Anchor.prototype.copyGraph = function( options ) { var clone = this.copy( options ); this.children.forEach( function( child ) { child.copyGraph({ addTo: clone, }); } ); return clone; }; Anchor.prototype.normalizeRotate = function() { this.rotate.x = utils.modulo( this.rotate.x, TAU ); this.rotate.y = utils.modulo( this.rotate.y, TAU ); this.rotate.z = utils.modulo( this.rotate.z, TAU ); }; // ----- subclass ----- // function getSubclass( Super ) { return function( defaults ) { // create constructor function Item( options ) { this.create( options || {} ); } Item.prototype = Object.create( Super.prototype ); Item.prototype.constructor = Item; Item.defaults = utils.extend( {}, Super.defaults ); utils.extend( Item.defaults, defaults ); // create optionKeys Item.optionKeys = Super.optionKeys.slice( 0 ); // add defaults keys to optionKeys, dedupe Object.keys( Item.defaults ).forEach( function( key ) { if ( !Item.optionKeys.indexOf( key ) != 1 ) { Item.optionKeys.push( key ); } } ); Item.subclass = getSubclass( Item ); return Item; }; } Anchor.subclass = getSubclass( Anchor ); return Anchor; } ) ); /** * Dragger */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.Dragger = factory(); } }( this, function factory() { // quick & dirty drag event stuff // messes up if multiple pointers/touches // check for browser window #85 var hasWindow = typeof window != 'undefined'; // event support, default to mouse events var downEvent = 'mousedown'; var moveEvent = 'mousemove'; var upEvent = 'mouseup'; if ( hasWindow ) { if ( window.PointerEvent ) { // PointerEvent, Chrome downEvent = 'pointerdown'; moveEvent = 'pointermove'; upEvent = 'pointerup'; } else if ( 'ontouchstart' in window ) { // Touch Events, iOS Safari downEvent = 'touchstart'; moveEvent = 'touchmove'; upEvent = 'touchend'; } } function noop() {} function Dragger( options ) { this.create( options || {} ); } Dragger.prototype.create = function( options ) { this.onDragStart = options.onDragStart || noop; this.onDragMove = options.onDragMove || noop; this.onDragEnd = options.onDragEnd || noop; this.bindDrag( options.startElement ); }; Dragger.prototype.bindDrag = function( element ) { element = this.getQueryElement( element ); if ( !element ) { return; } // disable browser gestures #53 element.style.touchAction = 'none'; element.addEventListener( downEvent, this ); }; Dragger.prototype.getQueryElement = function( element ) { if ( typeof element == 'string' ) { // with string, query selector element = document.querySelector( element ); } return element; }; Dragger.prototype.handleEvent = function( event ) { var method = this[ 'on' + event.type ]; if ( method ) { method.call( this, event ); } }; Dragger.prototype.onmousedown = Dragger.prototype.onpointerdown = function( event ) { this.dragStart( event, event ); }; Dragger.prototype.ontouchstart = function( event ) { this.dragStart( event, event.changedTouches[0] ); }; Dragger.prototype.dragStart = function( event, pointer ) { event.preventDefault(); this.dragStartX = pointer.pageX; this.dragStartY = pointer.pageY; if ( hasWindow ) { window.addEventListener( moveEvent, this ); window.addEventListener( upEvent, this ); } this.onDragStart( pointer ); }; Dragger.prototype.ontouchmove = function( event ) { // HACK, moved touch may not be first this.dragMove( event, event.changedTouches[0] ); }; Dragger.prototype.onmousemove = Dragger.prototype.onpointermove = function( event ) { this.dragMove( event, event ); }; Dragger.prototype.dragMove = function( event, pointer ) { event.preventDefault(); var moveX = pointer.pageX - this.dragStartX; var moveY = pointer.pageY - this.dragStartY; this.onDragMove( pointer, moveX, moveY ); }; Dragger.prototype.onmouseup = Dragger.prototype.onpointerup = Dragger.prototype.ontouchend = Dragger.prototype.dragEnd = function( /* event */) { window.removeEventListener( moveEvent, this ); window.removeEventListener( upEvent, this ); this.onDragEnd(); }; return Dragger; } ) ); /** * Illustration */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./anchor'), require('./dragger') ); } else { // browser global var Zdog = root.Zdog; Zdog.Illustration = factory( Zdog, Zdog.Anchor, Zdog.Dragger ); } }( this, function factory( utils, Anchor, Dragger ) { function noop() {} var TAU = utils.TAU; var Illustration = Anchor.subclass({ element: undefined, centered: true, zoom: 1, dragRotate: false, resize: false, onPrerender: noop, onDragStart: noop, onDragMove: noop, onDragEnd: noop, onResize: noop, }); utils.extend( Illustration.prototype, Dragger.prototype ); Illustration.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); Dragger.prototype.create.call( this, options ); this.setElement( this.element ); this.setDragRotate( this.dragRotate ); this.setResize( this.resize ); }; Illustration.prototype.setElement = function( element ) { element = this.getQueryElement( element ); if ( !element ) { throw new Error( 'Zdog.Illustration element required. Set to ' + element ); } var nodeName = element.nodeName.toLowerCase(); if ( nodeName == 'canvas' ) { this.setCanvas( element ); } else if ( nodeName == 'svg' ) { this.setSvg( element ); } }; Illustration.prototype.setSize = function( width, height ) { width = Math.round( width ); height = Math.round( height ); if ( this.isCanvas ) { this.setSizeCanvas( width, height ); } else if ( this.isSvg ) { this.setSizeSvg( width, height ); } }; Illustration.prototype.setResize = function( resize ) { this.resize = resize; // create resize event listener if ( !this.resizeListener ) { this.resizeListener = this.onWindowResize.bind( this ); } // add/remove event listener if ( resize ) { window.addEventListener( 'resize', this.resizeListener ); this.onWindowResize(); } else { window.removeEventListener( 'resize', this.resizeListener ); } }; // TODO debounce this? Illustration.prototype.onWindowResize = function() { this.setMeasuredSize(); this.onResize( this.width, this.height ); }; Illustration.prototype.setMeasuredSize = function() { var width, height; var isFullscreen = this.resize == 'fullscreen'; if ( isFullscreen ) { width = window.innerWidth; height = window.innerHeight; } else { var rect = this.element.getBoundingClientRect(); width = rect.width; height = rect.height; } this.setSize( width, height ); }; // ----- render ----- // Illustration.prototype.renderGraph = function( item ) { if ( this.isCanvas ) { this.renderGraphCanvas( item ); } else if ( this.isSvg ) { this.renderGraphSvg( item ); } }; // combo method Illustration.prototype.updateRenderGraph = function( item ) { this.updateGraph(); this.renderGraph( item ); }; // ----- canvas ----- // Illustration.prototype.setCanvas = function( element ) { this.element = element; this.isCanvas = true; // update related properties this.ctx = this.element.getContext('2d'); // set initial size this.setSizeCanvas( element.width, element.height ); }; Illustration.prototype.setSizeCanvas = function( width, height ) { this.width = width; this.height = height; // up-rez for hi-DPI devices var pixelRatio = this.pixelRatio = window.devicePixelRatio || 1; this.element.width = this.canvasWidth = width * pixelRatio; this.element.height = this.canvasHeight = height * pixelRatio; var needsHighPixelRatioSizing = pixelRatio > 1 && !this.resize; if ( needsHighPixelRatioSizing ) { this.element.style.width = width + 'px'; this.element.style.height = height + 'px'; } }; Illustration.prototype.renderGraphCanvas = function( item ) { item = item || this; this.prerenderCanvas(); Anchor.prototype.renderGraphCanvas.call( item, this.ctx ); this.postrenderCanvas(); }; Illustration.prototype.prerenderCanvas = function() { var ctx = this.ctx; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.clearRect( 0, 0, this.canvasWidth, this.canvasHeight ); ctx.save(); if ( this.centered ) { var centerX = this.width / 2 * this.pixelRatio; var centerY = this.height / 2 * this.pixelRatio; ctx.translate( centerX, centerY ); } var scale = this.pixelRatio * this.zoom; ctx.scale( scale, scale ); this.onPrerender( ctx ); }; Illustration.prototype.postrenderCanvas = function() { this.ctx.restore(); }; // ----- svg ----- // Illustration.prototype.setSvg = function( element ) { this.element = element; this.isSvg = true; this.pixelRatio = 1; // set initial size from width & height attributes var width = element.getAttribute('width'); var height = element.getAttribute('height'); this.setSizeSvg( width, height ); }; Illustration.prototype.setSizeSvg = function( width, height ) { this.width = width; this.height = height; var viewWidth = width / this.zoom; var viewHeight = height / this.zoom; var viewX = this.centered ? -viewWidth/2 : 0; var viewY = this.centered ? -viewHeight/2 : 0; this.element.setAttribute( 'viewBox', viewX + ' ' + viewY + ' ' + viewWidth + ' ' + viewHeight ); if ( this.resize ) { // remove size attributes, let size be determined by viewbox this.element.removeAttribute('width'); this.element.removeAttribute('height'); } else { this.element.setAttribute( 'width', width ); this.element.setAttribute( 'height', height ); } }; Illustration.prototype.renderGraphSvg = function( item ) { item = item || this; empty( this.element ); this.onPrerender( this.element ); Anchor.prototype.renderGraphSvg.call( item, this.element ); }; function empty( element ) { while ( element.firstChild ) { element.removeChild( element.firstChild ); } } // ----- drag ----- // Illustration.prototype.setDragRotate = function( item ) { if ( !item ) { return; } else if ( item === true ) { /* eslint consistent-this: "off" */ item = this; } this.dragRotate = item; this.bindDrag( this.element ); }; Illustration.prototype.dragStart = function( /* event, pointer */) { this.dragStartRX = this.dragRotate.rotate.x; this.dragStartRY = this.dragRotate.rotate.y; Dragger.prototype.dragStart.apply( this, arguments ); }; Illustration.prototype.dragMove = function( event, pointer ) { var moveX = pointer.pageX - this.dragStartX; var moveY = pointer.pageY - this.dragStartY; var displaySize = Math.min( this.width, this.height ); var moveRY = moveX/displaySize * TAU; var moveRX = moveY/displaySize * TAU; this.dragRotate.rotate.x = this.dragStartRX - moveRX; this.dragRotate.rotate.y = this.dragStartRY - moveRY; Dragger.prototype.dragMove.apply( this, arguments ); }; return Illustration; } ) ); /** * PathCommand */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./vector') ); } else { // browser global var Zdog = root.Zdog; Zdog.PathCommand = factory( Zdog.Vector ); } }( this, function factory( Vector ) { function PathCommand( method, points, previousPoint ) { this.method = method; this.points = points.map( mapVectorPoint ); this.renderPoints = points.map( mapNewVector ); this.previousPoint = previousPoint; this.endRenderPoint = this.renderPoints[ this.renderPoints.length - 1 ]; // arc actions come with previous point & corner point // but require bezier control points if ( method == 'arc' ) { this.controlPoints = [ new Vector(), new Vector() ]; } } function mapVectorPoint( point ) { if ( point instanceof Vector ) { return point; } else { return new Vector( point ); } } function mapNewVector( point ) { return new Vector( point ); } PathCommand.prototype.reset = function() { // reset renderPoints back to orignal points position var points = this.points; this.renderPoints.forEach( function( renderPoint, i ) { var point = points[i]; renderPoint.set( point ); } ); }; PathCommand.prototype.transform = function( translation, rotation, scale ) { this.renderPoints.forEach( function( renderPoint ) { renderPoint.transform( translation, rotation, scale ); } ); }; PathCommand.prototype.render = function( ctx, elem, renderer ) { return this[ this.method ]( ctx, elem, renderer ); }; PathCommand.prototype.move = function( ctx, elem, renderer ) { return renderer.move( ctx, elem, this.renderPoints[0] ); }; PathCommand.prototype.line = function( ctx, elem, renderer ) { return renderer.line( ctx, elem, this.renderPoints[0] ); }; PathCommand.prototype.bezier = function( ctx, elem, renderer ) { var cp0 = this.renderPoints[0]; var cp1 = this.renderPoints[1]; var end = this.renderPoints[2]; return renderer.bezier( ctx, elem, cp0, cp1, end ); }; var arcHandleLength = 9/16; PathCommand.prototype.arc = function( ctx, elem, renderer ) { var prev = this.previousPoint; var corner = this.renderPoints[0]; var end = this.renderPoints[1]; var cp0 = this.controlPoints[0]; var cp1 = this.controlPoints[1]; cp0.set( prev ).lerp( corner, arcHandleLength ); cp1.set( end ).lerp( corner, arcHandleLength ); return renderer.bezier( ctx, elem, cp0, cp1, end ); }; return PathCommand; } ) ); /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./path-command'), require('./anchor') ); } else { // browser global var Zdog = root.Zdog; Zdog.Shape = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor ); } }( this, function factory( utils, Vector, PathCommand, Anchor ) { var Shape = Anchor.subclass({ stroke: 1, fill: false, color: '#333', closed: true, visible: true, path: [ {} ], front: { z: 1 }, backface: true, }); Shape.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); // front this.front = new Vector( options.front || this.front ); this.renderFront = new Vector( this.front ); this.renderNormal = new Vector(); }; var actionNames = [ 'move', 'line', 'bezier', 'arc', ]; Shape.prototype.updatePath = function() { this.setPath(); this.updatePathCommands(); }; // place holder for Ellipse, Rect, etc. Shape.prototype.setPath = function() {}; // parse path into PathCommands Shape.prototype.updatePathCommands = function() { var previousPoint; this.pathCommands = this.path.map( function( pathPart, i ) { // pathPart can be just vector coordinates -> { x, y, z } // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] } var keys = Object.keys( pathPart ); var method = keys[0]; var points = pathPart[ method ]; // default to line if no instruction var isInstruction = keys.length == 1 && actionNames.indexOf( method ) != -1; if ( !isInstruction ) { method = 'line'; points = pathPart; } // munge single-point methods like line & move without arrays var isLineOrMove = method == 'line' || method == 'move'; var isPointsArray = Array.isArray( points ); if ( isLineOrMove && !isPointsArray ) { points = [ points ]; } // first action is always move method = i === 0 ? 'move' : method; // arcs require previous last point var command = new PathCommand( method, points, previousPoint ); // update previousLastPoint previousPoint = command.endRenderPoint; return command; } ); }; // ----- update ----- // Shape.prototype.reset = function() { this.renderOrigin.set( this.origin ); this.renderFront.set( this.front ); // reset command render points this.pathCommands.forEach( function( command ) { command.reset(); } ); }; Shape.prototype.transform = function( translation, rotation, scale ) { // calculate render points backface visibility & cone/hemisphere shapes this.renderOrigin.transform( translation, rotation, scale ); this.renderFront.transform( translation, rotation, scale ); this.renderNormal.set( this.renderOrigin ).subtract( this.renderFront ); // transform points this.pathCommands.forEach( function( command ) { command.transform( translation, rotation, scale ); } ); // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); } ); }; Shape.prototype.updateSortValue = function() { // sort by average z of all points // def not geometrically correct, but works for me var pointCount = this.pathCommands.length; var firstPoint = this.pathCommands[0].endRenderPoint; var lastPoint = this.pathCommands[ pointCount - 1 ].endRenderPoint; // ignore the final point if self closing shape var isSelfClosing = pointCount > 2 && firstPoint.isSame( lastPoint ); if ( isSelfClosing ) { pointCount -= 1; } var sortValueTotal = 0; for ( var i = 0; i < pointCount; i++ ) { sortValueTotal += this.pathCommands[i].endRenderPoint.z; } this.sortValue = sortValueTotal/pointCount; }; // ----- render ----- // Shape.prototype.render = function( ctx, renderer ) { var length = this.pathCommands.length; if ( !this.visible || !length ) { return; } // do not render if hiding backface this.isFacingBack = this.renderNormal.z > 0; if ( !this.backface && this.isFacingBack ) { return; } if ( !renderer ) { throw new Error( 'Zdog renderer required. Set to ' + renderer ); } // render dot or path var isDot = length == 1; if ( renderer.isCanvas && isDot ) { this.renderCanvasDot( ctx, renderer ); } else { this.renderPath( ctx, renderer ); } }; var TAU = utils.TAU; // Safari does not render lines with no size, have to render circle instead Shape.prototype.renderCanvasDot = function( ctx ) { var lineWidth = this.getLineWidth(); if ( !lineWidth ) { return; } ctx.fillStyle = this.getRenderColor(); var point = this.pathCommands[0].endRenderPoint; ctx.beginPath(); var radius = lineWidth/2; ctx.arc( point.x, point.y, radius, 0, TAU ); ctx.fill(); }; Shape.prototype.getLineWidth = function() { if ( !this.stroke ) { return 0; } if ( this.stroke == true ) { return 1; } return this.stroke; }; Shape.prototype.getRenderColor = function() { // use backface color if applicable var isBackfaceColor = typeof this.backface == 'string' && this.isFacingBack; var color = isBackfaceColor ? this.backface : this.color; return color; }; Shape.prototype.renderPath = function( ctx, renderer ) { var elem = this.getRenderElement( ctx, renderer ); var isTwoPoints = this.pathCommands.length == 2 && this.pathCommands[1].method == 'line'; var isClosed = !isTwoPoints && this.closed; var color = this.getRenderColor(); renderer.renderPath( ctx, elem, this.pathCommands, isClosed ); renderer.stroke( ctx, elem, this.stroke, color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Shape.prototype.getRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.svgElement ) { // create svgElement this.svgElement = document.createElementNS( svgURI, 'path' ); this.svgElement.setAttribute( 'stroke-linecap', 'round' ); this.svgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.svgElement; }; return Shape; } ) ); /** * Group */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./anchor') ); } else { // browser global var Zdog = root.Zdog; Zdog.Group = factory( Zdog.Anchor ); } }( this, function factory( Anchor ) { var Group = Anchor.subclass({ updateSort: false, visible: true, }); // ----- update ----- // Group.prototype.updateSortValue = function() { var sortValueTotal = 0; this.flatGraph.forEach( function( item ) { item.updateSortValue(); sortValueTotal += item.sortValue; } ); // average sort value of all points // def not geometrically correct, but works for me this.sortValue = sortValueTotal / this.flatGraph.length; if ( this.updateSort ) { this.flatGraph.sort( Anchor.shapeSorter ); } }; // ----- render ----- // Group.prototype.render = function( ctx, renderer ) { if ( !this.visible ) { return; } this.flatGraph.forEach( function( item ) { item.render( ctx, renderer ); } ); }; // actual group flatGraph only used inside group Group.prototype.updateFlatGraph = function() { // do not include self var flatGraph = []; this.flatGraph = this.addChildFlatGraph( flatGraph ); }; // do not include children, group handles rendering & sorting internally Group.prototype.getFlatGraph = function() { return [ this ]; }; return Group; } ) ); /** * Rect */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Rect = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var Rect = Shape.subclass({ width: 1, height: 1, }); Rect.prototype.setPath = function() { var x = this.width / 2; var y = this.height / 2; /* eslint key-spacing: "off" */ this.path = [ { x: -x, y: -y }, { x: x, y: -y }, { x: x, y: y }, { x: -x, y: y }, ]; }; return Rect; } ) ); /** * RoundedRect */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.RoundedRect = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var RoundedRect = Shape.subclass({ width: 1, height: 1, cornerRadius: 0.25, closed: false, }); RoundedRect.prototype.setPath = function() { /* eslint id-length: [ "error", { "min": 2, "exceptions": [ "x", "y" ] }], key-spacing: "off" */ var xA = this.width / 2; var yA = this.height / 2; var shortSide = Math.min( xA, yA ); var cornerRadius = Math.min( this.cornerRadius, shortSide ); var xB = xA - cornerRadius; var yB = yA - cornerRadius; var path = [ // top right corner { x: xB, y: -yA }, { arc: [ { x: xA, y: -yA }, { x: xA, y: -yB }, ] }, ]; // bottom right corner if ( yB ) { path.push({ x: xA, y: yB }); } path.push({ arc: [ { x: xA, y: yA }, { x: xB, y: yA }, ] }); // bottom left corner if ( xB ) { path.push({ x: -xB, y: yA }); } path.push({ arc: [ { x: -xA, y: yA }, { x: -xA, y: yB }, ] }); // top left corner if ( yB ) { path.push({ x: -xA, y: -yB }); } path.push({ arc: [ { x: -xA, y: -yA }, { x: -xB, y: -yA }, ] }); // back to top right corner if ( xB ) { path.push({ x: xB, y: -yA }); } this.path = path; }; return RoundedRect; } ) ); /** * Ellipse */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Ellipse = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var Ellipse = Shape.subclass({ diameter: 1, width: undefined, height: undefined, quarters: 4, closed: false, }); Ellipse.prototype.setPath = function() { var width = this.width != undefined ? this.width : this.diameter; var height = this.height != undefined ? this.height : this.diameter; var x = width/2; var y = height/2; this.path = [ { x: 0, y: -y }, { arc: [ // top right { x: x, y: -y }, { x: x, y: 0 }, ] }, ]; // bottom right if ( this.quarters > 1 ) { this.path.push({ arc: [ { x: x, y: y }, { x: 0, y: y }, ] }); } // bottom left if ( this.quarters > 2 ) { this.path.push({ arc: [ { x: -x, y: y }, { x: -x, y: 0 }, ] }); } // top left if ( this.quarters > 3 ) { this.path.push({ arc: [ { x: -x, y: -y }, { x: 0, y: -y }, ] }); } }; return Ellipse; } ) ); /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Polygon = factory( Zdog, Zdog.Shape ); } }( this, function factory( utils, Shape ) { var Polygon = Shape.subclass({ sides: 3, radius: 0.5, }); var TAU = utils.TAU; Polygon.prototype.setPath = function() { this.path = []; for ( var i = 0; i < this.sides; i++ ) { var theta = i / this.sides * TAU - TAU/4; var x = Math.cos( theta ) * this.radius; var y = Math.sin( theta ) * this.radius; this.path.push({ x: x, y: y }); } }; return Polygon; } ) ); /** * Hemisphere composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./anchor'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Hemisphere = factory( Zdog, Zdog.Vector, Zdog.Anchor, Zdog.Ellipse ); } }( this, function factory( utils, Vector, Anchor, Ellipse ) { var Hemisphere = Ellipse.subclass({ fill: true, }); var TAU = utils.TAU; Hemisphere.prototype.create = function( /* options */) { // call super Ellipse.prototype.create.apply( this, arguments ); // composite shape, create child shapes this.apex = new Anchor({ addTo: this, translate: { z: this.diameter / 2 }, }); // vector used for calculation this.renderCentroid = new Vector(); }; Hemisphere.prototype.updateSortValue = function() { // centroid of hemisphere is 3/8 between origin and apex this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 3/8 ); this.sortValue = this.renderCentroid.z; }; Hemisphere.prototype.render = function( ctx, renderer ) { this.renderDome( ctx, renderer ); // call super Ellipse.prototype.render.apply( this, arguments ); }; Hemisphere.prototype.renderDome = function( ctx, renderer ) { if ( !this.visible ) { return; } var elem = this.getDomeRenderElement( ctx, renderer ); var contourAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x ); var domeRadius = this.diameter / 2 * this.renderNormal.magnitude(); var x = this.renderOrigin.x; var y = this.renderOrigin.y; if ( renderer.isCanvas ) { // canvas var startAngle = contourAngle + TAU/4; var endAngle = contourAngle - TAU/4; ctx.beginPath(); ctx.arc( x, y, domeRadius, startAngle, endAngle ); } else if ( renderer.isSvg ) { // svg contourAngle = ( contourAngle - TAU/4 ) / TAU * 360; this.domeSvgElement.setAttribute( 'd', 'M ' + -domeRadius + ',0 A ' + domeRadius + ',' + domeRadius + ' 0 0 1 ' + domeRadius + ',0' ); this.domeSvgElement.setAttribute( 'transform', 'translate(' + x + ',' + y + ' ) rotate(' + contourAngle + ')' ); } renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, this.color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Hemisphere.prototype.getDomeRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.domeSvgElement ) { // create svgElement this.domeSvgElement = document.createElementNS( svgURI, 'path' ); this.domeSvgElement.setAttribute( 'stroke-linecap', 'round' ); this.domeSvgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.domeSvgElement; }; return Hemisphere; } ) ); /** * Cylinder composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./path-command'), require('./shape'), require('./group'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Cylinder = factory( Zdog, Zdog.PathCommand, Zdog.Shape, Zdog.Group, Zdog.Ellipse ); } }( this, function factory( utils, PathCommand, Shape, Group, Ellipse ) { function noop() {} // ----- CylinderGroup ----- // var CylinderGroup = Group.subclass({ color: '#333', updateSort: true, }); CylinderGroup.prototype.create = function() { Group.prototype.create.apply( this, arguments ); this.pathCommands = [ new PathCommand( 'move', [ {} ] ), new PathCommand( 'line', [ {} ] ), ]; }; CylinderGroup.prototype.render = function( ctx, renderer ) { this.renderCylinderSurface( ctx, renderer ); Group.prototype.render.apply( this, arguments ); }; CylinderGroup.prototype.renderCylinderSurface = function( ctx, renderer ) { if ( !this.visible ) { return; } // render cylinder surface var elem = this.getRenderElement( ctx, renderer ); var frontBase = this.frontBase; var rearBase = this.rearBase; var scale = frontBase.renderNormal.magnitude(); var strokeWidth = frontBase.diameter * scale + frontBase.getLineWidth(); // set path command render points this.pathCommands[0].renderPoints[0].set( frontBase.renderOrigin ); this.pathCommands[1].renderPoints[0].set( rearBase.renderOrigin ); if ( renderer.isCanvas ) { ctx.lineCap = 'butt'; // nice } renderer.renderPath( ctx, elem, this.pathCommands ); renderer.stroke( ctx, elem, true, this.color, strokeWidth ); renderer.end( ctx, elem ); if ( renderer.isCanvas ) { ctx.lineCap = 'round'; // reset } }; var svgURI = 'http://www.w3.org/2000/svg'; CylinderGroup.prototype.getRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.svgElement ) { // create svgElement this.svgElement = document.createElementNS( svgURI, 'path' ); } return this.svgElement; }; // prevent double-creation in parent.copyGraph() // only create in Cylinder.create() CylinderGroup.prototype.copyGraph = noop; // ----- CylinderEllipse ----- // var CylinderEllipse = Ellipse.subclass(); CylinderEllipse.prototype.copyGraph = noop; // ----- Cylinder ----- // var Cylinder = Shape.subclass({ diameter: 1, length: 1, frontFace: undefined, fill: true, }); var TAU = utils.TAU; Cylinder.prototype.create = function( /* options */) { // call super Shape.prototype.create.apply( this, arguments ); // composite shape, create child shapes // CylinderGroup to render cylinder surface then bases this.group = new CylinderGroup({ addTo: this, color: this.color, visible: this.visible, }); var baseZ = this.length / 2; var baseColor = this.backface || true; // front outside base this.frontBase = this.group.frontBase = new Ellipse({ addTo: this.group, diameter: this.diameter, translate: { z: baseZ }, rotate: { y: TAU/2 }, color: this.color, stroke: this.stroke, fill: this.fill, backface: this.frontFace || baseColor, visible: this.visible, }); // back outside base this.rearBase = this.group.rearBase = this.frontBase.copy({ translate: { z: -baseZ }, rotate: { y: 0 }, backface: baseColor, }); }; // Cylinder shape does not render anything Cylinder.prototype.render = function() {}; // ----- set child properties ----- // var childProperties = [ 'stroke', 'fill', 'color', 'visible' ]; childProperties.forEach( function( property ) { // use proxy property for custom getter & setter var _prop = '_' + property; Object.defineProperty( Cylinder.prototype, property, { get: function() { return this[ _prop ]; }, set: function( value ) { this[ _prop ] = value; // set property on children if ( this.frontBase ) { this.frontBase[ property ] = value; this.rearBase[ property ] = value; this.group[ property ] = value; } }, } ); } ); // TODO child property setter for backface, frontBaseColor, & rearBaseColor return Cylinder; } ) ); /** * Cone composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./path-command'), require('./anchor'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Cone = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor, Zdog.Ellipse ); } }( this, function factory( utils, Vector, PathCommand, Anchor, Ellipse ) { var Cone = Ellipse.subclass({ length: 1, fill: true, }); var TAU = utils.TAU; Cone.prototype.create = function( /* options */) { // call super Ellipse.prototype.create.apply( this, arguments ); // composite shape, create child shapes this.apex = new Anchor({ addTo: this, translate: { z: this.length }, }); // vectors used for calculation this.renderApex = new Vector(); this.renderCentroid = new Vector(); this.tangentA = new Vector(); this.tangentB = new Vector(); this.surfacePathCommands = [ new PathCommand( 'move', [ {} ] ), // points set in renderConeSurface new PathCommand( 'line', [ {} ] ), new PathCommand( 'line', [ {} ] ), ]; }; Cone.prototype.updateSortValue = function() { // center of cone is one third of its length this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 1/3 ); this.sortValue = this.renderCentroid.z; }; Cone.prototype.render = function( ctx, renderer ) { this.renderConeSurface( ctx, renderer ); Ellipse.prototype.render.apply( this, arguments ); }; Cone.prototype.renderConeSurface = function( ctx, renderer ) { if ( !this.visible ) { return; } this.renderApex.set( this.apex.renderOrigin ) .subtract( this.renderOrigin ); var scale = this.renderNormal.magnitude(); var apexDistance = this.renderApex.magnitude2d(); var normalDistance = this.renderNormal.magnitude2d(); // eccentricity var eccenAngle = Math.acos( normalDistance/scale ); var eccen = Math.sin( eccenAngle ); var radius = this.diameter / 2 * scale; // does apex extend beyond eclipse of face var isApexVisible = radius * eccen < apexDistance; if ( !isApexVisible ) { return; } // update tangents var apexAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x ) + TAU/2; var projectLength = apexDistance/eccen; var projectAngle = Math.acos( radius/projectLength ); // set tangent points var tangentA = this.tangentA; var tangentB = this.tangentB; tangentA.x = Math.cos( projectAngle ) * radius * eccen; tangentA.y = Math.sin( projectAngle ) * radius; tangentB.set( this.tangentA ); tangentB.y *= -1; tangentA.rotateZ( apexAngle ); tangentB.rotateZ( apexAngle ); tangentA.add( this.renderOrigin ); tangentB.add( this.renderOrigin ); this.setSurfaceRenderPoint( 0, tangentA ); this.setSurfaceRenderPoint( 1, this.apex.renderOrigin ); this.setSurfaceRenderPoint( 2, tangentB ); // render var elem = this.getSurfaceRenderElement( ctx, renderer ); renderer.renderPath( ctx, elem, this.surfacePathCommands ); renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, this.color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Cone.prototype.getSurfaceRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.surfaceSvgElement ) { // create svgElement this.surfaceSvgElement = document.createElementNS( svgURI, 'path' ); this.surfaceSvgElement.setAttribute( 'stroke-linecap', 'round' ); this.surfaceSvgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.surfaceSvgElement; }; Cone.prototype.setSurfaceRenderPoint = function( index, point ) { var renderPoint = this.surfacePathCommands[ index ].renderPoints[0]; renderPoint.set( point ); }; return Cone; } ) ); /** * Box composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./anchor'), require('./shape'), require('./rect') ); } else { // browser global var Zdog = root.Zdog; Zdog.Box = factory( Zdog, Zdog.Anchor, Zdog.Shape, Zdog.Rect ); } }( this, function factory( utils, Anchor, Shape, Rect ) { // ----- BoxRect ----- // var BoxRect = Rect.subclass(); // prevent double-creation in parent.copyGraph() // only create in Box.create() BoxRect.prototype.copyGraph = function() {}; // ----- Box ----- // var TAU = utils.TAU; var faceNames = [ 'frontFace', 'rearFace', 'leftFace', 'rightFace', 'topFace', 'bottomFace', ]; var boxDefaults = utils.extend( {}, Shape.defaults ); delete boxDefaults.path; faceNames.forEach( function( faceName ) { boxDefaults[ faceName ] = true; } ); utils.extend( boxDefaults, { width: 1, height: 1, depth: 1, fill: true, } ); var Box = Anchor.subclass( boxDefaults ); /* eslint-disable no-self-assign */ Box.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); // HACK reset fill to trigger face setter this.fill = this.fill; }; Box.prototype.updatePath = function() { // reset all faces to trigger setters faceNames.forEach( function( faceName ) { this[ faceName ] = this[ faceName ]; }, this ); }; /* eslint-enable no-self-assign */ faceNames.forEach( function( faceName ) { var _faceName = '_' + faceName; Object.defineProperty( Box.prototype, faceName, { get: function() { return this[ _faceName ]; }, set: function( value ) { this[ _faceName ] = value; this.setFace( faceName, value ); }, } ); } ); Box.prototype.setFace = function( faceName, value ) { var rectProperty = faceName + 'Rect'; var rect = this[ rectProperty ]; // remove if false if ( !value ) { this.removeChild( rect ); return; } // update & add face var options = this.getFaceOptions( faceName ); options.color = typeof value == 'string' ? value : this.color; if ( rect ) { // update previous rect.setOptions( options ); } else { // create new rect = this[ rectProperty ] = new BoxRect( options ); } rect.updatePath(); this.addChild( rect ); }; Box.prototype.getFaceOptions = function( faceName ) { return { frontFace: { width: this.width, height: this.height, translate: { z: this.depth / 2 }, }, rearFace: { width: this.width, height: this.height, translate: { z: -this.depth / 2 }, rotate: { y: TAU/2 }, }, leftFace: { width: this.depth, height: this.height, translate: { x: -this.width / 2 }, rotate: { y: -TAU/4 }, }, rightFace: { width: this.depth, height: this.height, translate: { x: this.width / 2 }, rotate: { y: TAU/4 }, }, topFace: { width: this.width, height: this.depth, translate: { y: -this.height / 2 }, rotate: { x: -TAU/4 }, }, bottomFace: { width: this.width, height: this.depth, translate: { y: this.height / 2 }, rotate: { x: TAU/4 }, }, }[ faceName ]; }; // ----- set face properties ----- // var childProperties = [ 'color', 'stroke', 'fill', 'backface', 'front', 'visible' ]; childProperties.forEach( function( property ) { // use proxy property for custom getter & setter var _prop = '_' + property; Object.defineProperty( Box.prototype, property, { get: function() { return this[ _prop ]; }, set: function( value ) { this[ _prop ] = value; faceNames.forEach( function( faceName ) { var rect = this[ faceName + 'Rect' ]; var isFaceColor = typeof this[ faceName ] == 'string'; var isColorUnderwrite = property == 'color' && isFaceColor; if ( rect && !isColorUnderwrite ) { rect[ property ] = value; } }, this ); }, } ); } ); return Box; } ) ); /** * Index */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./canvas-renderer'), require('./svg-renderer'), require('./vector'), require('./anchor'), require('./dragger'), require('./illustration'), require('./path-command'), require('./shape'), require('./group'), require('./rect'), require('./rounded-rect'), require('./ellipse'), require('./polygon'), require('./hemisphere'), require('./cylinder'), require('./cone'), require('./box') ); } else if ( typeof define == 'function' && define.amd ) { /* globals define */ // AMD define( 'zdog', [], root.Zdog ); } /* eslint-disable max-params */ } )( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor, Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect, Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box ) { /* eslint-enable max-params */ Zdog.CanvasRenderer = CanvasRenderer; Zdog.SvgRenderer = SvgRenderer; Zdog.Vector = Vector; Zdog.Anchor = Anchor; Zdog.Dragger = Dragger; Zdog.Illustration = Illustration; Zdog.PathCommand = PathCommand; Zdog.Shape = Shape; Zdog.Group = Group; Zdog.Rect = Rect; Zdog.RoundedRect = RoundedRect; Zdog.Ellipse = Ellipse; Zdog.Polygon = Polygon; Zdog.Hemisphere = Hemisphere; Zdog.Cylinder = Cylinder; Zdog.Cone = Cone; Zdog.Box = Box; return Zdog; } ); ================================================ FILE: js/anchor.js ================================================ /** * Anchor */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./canvas-renderer'), require('./svg-renderer') ); } else { // browser global var Zdog = root.Zdog; Zdog.Anchor = factory( Zdog, Zdog.Vector, Zdog.CanvasRenderer, Zdog.SvgRenderer ); } }( this, function factory( utils, Vector, CanvasRenderer, SvgRenderer ) { var TAU = utils.TAU; var onePoint = { x: 1, y: 1, z: 1 }; function Anchor( options ) { this.create( options || {} ); } Anchor.prototype.create = function( options ) { this.children = []; // set defaults & options utils.extend( this, this.constructor.defaults ); this.setOptions( options ); // transform this.translate = new Vector( options.translate ); this.rotate = new Vector( options.rotate ); this.scale = new Vector( onePoint ).multiply( this.scale ); // origin this.origin = new Vector(); this.renderOrigin = new Vector(); if ( this.addTo ) { this.addTo.addChild( this ); } }; Anchor.defaults = {}; Anchor.optionKeys = Object.keys( Anchor.defaults ).concat([ 'rotate', 'translate', 'scale', 'addTo', ]); Anchor.prototype.setOptions = function( options ) { var optionKeys = this.constructor.optionKeys; for ( var key in options ) { if ( optionKeys.indexOf( key ) != -1 ) { this[ key ] = options[ key ]; } } }; Anchor.prototype.addChild = function( shape ) { if ( this.children.indexOf( shape ) != -1 ) { return; } shape.remove(); // remove previous parent shape.addTo = this; // keep parent reference this.children.push( shape ); }; Anchor.prototype.removeChild = function( shape ) { var index = this.children.indexOf( shape ); if ( index != -1 ) { this.children.splice( index, 1 ); } }; Anchor.prototype.remove = function() { if ( this.addTo ) { this.addTo.removeChild( this ); } }; // ----- update ----- // Anchor.prototype.update = function() { // update self this.reset(); // update children this.children.forEach( function( child ) { child.update(); } ); this.transform( this.translate, this.rotate, this.scale ); }; Anchor.prototype.reset = function() { this.renderOrigin.set( this.origin ); }; Anchor.prototype.transform = function( translation, rotation, scale ) { this.renderOrigin.transform( translation, rotation, scale ); // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); } ); }; Anchor.prototype.updateGraph = function() { this.update(); this.updateFlatGraph(); this.flatGraph.forEach( function( item ) { item.updateSortValue(); } ); // z-sort this.flatGraph.sort( Anchor.shapeSorter ); }; Anchor.shapeSorter = function( a, b ) { return a.sortValue - b.sortValue; }; // custom getter to check for flatGraph before using it Object.defineProperty( Anchor.prototype, 'flatGraph', { get: function() { if ( !this._flatGraph ) { this.updateFlatGraph(); } return this._flatGraph; }, set: function( graph ) { this._flatGraph = graph; }, } ); Anchor.prototype.updateFlatGraph = function() { this.flatGraph = this.getFlatGraph(); }; // return Array of self & all child graph items Anchor.prototype.getFlatGraph = function() { var flatGraph = [ this ]; return this.addChildFlatGraph( flatGraph ); }; Anchor.prototype.addChildFlatGraph = function( flatGraph ) { this.children.forEach( function( child ) { var childFlatGraph = child.getFlatGraph(); Array.prototype.push.apply( flatGraph, childFlatGraph ); } ); return flatGraph; }; Anchor.prototype.updateSortValue = function() { this.sortValue = this.renderOrigin.z; }; // ----- render ----- // Anchor.prototype.render = function() {}; // TODO refactor out CanvasRenderer so its not a dependency within anchor.js Anchor.prototype.renderGraphCanvas = function( ctx ) { if ( !ctx ) { throw new Error( 'ctx is ' + ctx + '. ' + 'Canvas context required for render. Check .renderGraphCanvas( ctx ).' ); } this.flatGraph.forEach( function( item ) { item.render( ctx, CanvasRenderer ); } ); }; Anchor.prototype.renderGraphSvg = function( svg ) { if ( !svg ) { throw new Error( 'svg is ' + svg + '. ' + 'SVG required for render. Check .renderGraphSvg( svg ).' ); } this.flatGraph.forEach( function( item ) { item.render( svg, SvgRenderer ); } ); }; // ----- misc ----- // Anchor.prototype.copy = function( options ) { // copy options var itemOptions = {}; var optionKeys = this.constructor.optionKeys; optionKeys.forEach( function( key ) { itemOptions[ key ] = this[ key ]; }, this ); // add set options utils.extend( itemOptions, options ); var ItemClass = this.constructor; return new ItemClass( itemOptions ); }; Anchor.prototype.copyGraph = function( options ) { var clone = this.copy( options ); this.children.forEach( function( child ) { child.copyGraph({ addTo: clone, }); } ); return clone; }; Anchor.prototype.normalizeRotate = function() { this.rotate.x = utils.modulo( this.rotate.x, TAU ); this.rotate.y = utils.modulo( this.rotate.y, TAU ); this.rotate.z = utils.modulo( this.rotate.z, TAU ); }; // ----- subclass ----- // function getSubclass( Super ) { return function( defaults ) { // create constructor function Item( options ) { this.create( options || {} ); } Item.prototype = Object.create( Super.prototype ); Item.prototype.constructor = Item; Item.defaults = utils.extend( {}, Super.defaults ); utils.extend( Item.defaults, defaults ); // create optionKeys Item.optionKeys = Super.optionKeys.slice( 0 ); // add defaults keys to optionKeys, dedupe Object.keys( Item.defaults ).forEach( function( key ) { if ( !Item.optionKeys.indexOf( key ) != 1 ) { Item.optionKeys.push( key ); } } ); Item.subclass = getSubclass( Item ); return Item; }; } Anchor.subclass = getSubclass( Anchor ); return Anchor; } ) ); ================================================ FILE: js/boilerplate.js ================================================ /*! * Zdog v1.1.3 * Round, flat, designer-friendly pseudo-3D engine * Licensed MIT * https://zzz.dog * Copyright 2020 Metafizzy */ /** * Boilerplate & utils */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog = factory(); } }( this, function factory() { var Zdog = {}; Zdog.TAU = Math.PI * 2; Zdog.extend = function( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; }; Zdog.lerp = function( a, b, alpha ) { return ( b - a ) * alpha + a; }; Zdog.modulo = function( num, div ) { return ( ( num % div ) + div ) % div; }; var powerMultipliers = { 2: function( a ) { return a * a; }, 3: function( a ) { return a * a * a; }, 4: function( a ) { return a * a * a * a; }, 5: function( a ) { return a * a * a * a * a; }, }; Zdog.easeInOut = function( alpha, power ) { if ( power == 1 ) { return alpha; } alpha = Math.max( 0, Math.min( 1, alpha ) ); var isFirstHalf = alpha < 0.5; var slope = isFirstHalf ? alpha : 1 - alpha; slope /= 0.5; // make easing steeper with more multiples var powerMultiplier = powerMultipliers[ power ] || powerMultipliers[2]; var curve = powerMultiplier( slope ); curve /= 2; return isFirstHalf ? curve : 1 - curve; }; return Zdog; } ) ); ================================================ FILE: js/box.js ================================================ /** * Box composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./anchor'), require('./shape'), require('./rect') ); } else { // browser global var Zdog = root.Zdog; Zdog.Box = factory( Zdog, Zdog.Anchor, Zdog.Shape, Zdog.Rect ); } }( this, function factory( utils, Anchor, Shape, Rect ) { // ----- BoxRect ----- // var BoxRect = Rect.subclass(); // prevent double-creation in parent.copyGraph() // only create in Box.create() BoxRect.prototype.copyGraph = function() {}; // ----- Box ----- // var TAU = utils.TAU; var faceNames = [ 'frontFace', 'rearFace', 'leftFace', 'rightFace', 'topFace', 'bottomFace', ]; var boxDefaults = utils.extend( {}, Shape.defaults ); delete boxDefaults.path; faceNames.forEach( function( faceName ) { boxDefaults[ faceName ] = true; } ); utils.extend( boxDefaults, { width: 1, height: 1, depth: 1, fill: true, } ); var Box = Anchor.subclass( boxDefaults ); /* eslint-disable no-self-assign */ Box.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); // HACK reset fill to trigger face setter this.fill = this.fill; }; Box.prototype.updatePath = function() { // reset all faces to trigger setters faceNames.forEach( function( faceName ) { this[ faceName ] = this[ faceName ]; }, this ); }; /* eslint-enable no-self-assign */ faceNames.forEach( function( faceName ) { var _faceName = '_' + faceName; Object.defineProperty( Box.prototype, faceName, { get: function() { return this[ _faceName ]; }, set: function( value ) { this[ _faceName ] = value; this.setFace( faceName, value ); }, } ); } ); Box.prototype.setFace = function( faceName, value ) { var rectProperty = faceName + 'Rect'; var rect = this[ rectProperty ]; // remove if false if ( !value ) { this.removeChild( rect ); return; } // update & add face var options = this.getFaceOptions( faceName ); options.color = typeof value == 'string' ? value : this.color; if ( rect ) { // update previous rect.setOptions( options ); } else { // create new rect = this[ rectProperty ] = new BoxRect( options ); } rect.updatePath(); this.addChild( rect ); }; Box.prototype.getFaceOptions = function( faceName ) { return { frontFace: { width: this.width, height: this.height, translate: { z: this.depth / 2 }, }, rearFace: { width: this.width, height: this.height, translate: { z: -this.depth / 2 }, rotate: { y: TAU/2 }, }, leftFace: { width: this.depth, height: this.height, translate: { x: -this.width / 2 }, rotate: { y: -TAU/4 }, }, rightFace: { width: this.depth, height: this.height, translate: { x: this.width / 2 }, rotate: { y: TAU/4 }, }, topFace: { width: this.width, height: this.depth, translate: { y: -this.height / 2 }, rotate: { x: -TAU/4 }, }, bottomFace: { width: this.width, height: this.depth, translate: { y: this.height / 2 }, rotate: { x: TAU/4 }, }, }[ faceName ]; }; // ----- set face properties ----- // var childProperties = [ 'color', 'stroke', 'fill', 'backface', 'front', 'visible' ]; childProperties.forEach( function( property ) { // use proxy property for custom getter & setter var _prop = '_' + property; Object.defineProperty( Box.prototype, property, { get: function() { return this[ _prop ]; }, set: function( value ) { this[ _prop ] = value; faceNames.forEach( function( faceName ) { var rect = this[ faceName + 'Rect' ]; var isFaceColor = typeof this[ faceName ] == 'string'; var isColorUnderwrite = property == 'color' && isFaceColor; if ( rect && !isColorUnderwrite ) { rect[ property ] = value; } }, this ); }, } ); } ); return Box; } ) ); ================================================ FILE: js/canvas-renderer.js ================================================ /** * CanvasRenderer */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.CanvasRenderer = factory(); } }( this, function factory() { var CanvasRenderer = { isCanvas: true }; CanvasRenderer.begin = function( ctx ) { ctx.beginPath(); }; CanvasRenderer.move = function( ctx, elem, point ) { ctx.moveTo( point.x, point.y ); }; CanvasRenderer.line = function( ctx, elem, point ) { ctx.lineTo( point.x, point.y ); }; CanvasRenderer.bezier = function( ctx, elem, cp0, cp1, end ) { ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y ); }; CanvasRenderer.closePath = function( ctx ) { ctx.closePath(); }; CanvasRenderer.setPath = function() {}; CanvasRenderer.renderPath = function( ctx, elem, pathCommands, isClosed ) { this.begin( ctx, elem ); pathCommands.forEach( function( command ) { command.render( ctx, elem, CanvasRenderer ); } ); if ( isClosed ) { this.closePath( ctx, elem ); } }; CanvasRenderer.stroke = function( ctx, elem, isStroke, color, lineWidth ) { if ( !isStroke ) { return; } ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.stroke(); }; CanvasRenderer.fill = function( ctx, elem, isFill, color ) { if ( !isFill ) { return; } ctx.fillStyle = color; ctx.fill(); }; CanvasRenderer.end = function() {}; return CanvasRenderer; } ) ); ================================================ FILE: js/cone.js ================================================ /** * Cone composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./path-command'), require('./anchor'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Cone = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor, Zdog.Ellipse ); } }( this, function factory( utils, Vector, PathCommand, Anchor, Ellipse ) { var Cone = Ellipse.subclass({ length: 1, fill: true, }); var TAU = utils.TAU; Cone.prototype.create = function( /* options */) { // call super Ellipse.prototype.create.apply( this, arguments ); // composite shape, create child shapes this.apex = new Anchor({ addTo: this, translate: { z: this.length }, }); // vectors used for calculation this.renderApex = new Vector(); this.renderCentroid = new Vector(); this.tangentA = new Vector(); this.tangentB = new Vector(); this.surfacePathCommands = [ new PathCommand( 'move', [ {} ] ), // points set in renderConeSurface new PathCommand( 'line', [ {} ] ), new PathCommand( 'line', [ {} ] ), ]; }; Cone.prototype.updateSortValue = function() { // center of cone is one third of its length this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 1/3 ); this.sortValue = this.renderCentroid.z; }; Cone.prototype.render = function( ctx, renderer ) { this.renderConeSurface( ctx, renderer ); Ellipse.prototype.render.apply( this, arguments ); }; Cone.prototype.renderConeSurface = function( ctx, renderer ) { if ( !this.visible ) { return; } this.renderApex.set( this.apex.renderOrigin ) .subtract( this.renderOrigin ); var scale = this.renderNormal.magnitude(); var apexDistance = this.renderApex.magnitude2d(); var normalDistance = this.renderNormal.magnitude2d(); // eccentricity var eccenAngle = Math.acos( normalDistance/scale ); var eccen = Math.sin( eccenAngle ); var radius = this.diameter / 2 * scale; // does apex extend beyond eclipse of face var isApexVisible = radius * eccen < apexDistance; if ( !isApexVisible ) { return; } // update tangents var apexAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x ) + TAU/2; var projectLength = apexDistance/eccen; var projectAngle = Math.acos( radius/projectLength ); // set tangent points var tangentA = this.tangentA; var tangentB = this.tangentB; tangentA.x = Math.cos( projectAngle ) * radius * eccen; tangentA.y = Math.sin( projectAngle ) * radius; tangentB.set( this.tangentA ); tangentB.y *= -1; tangentA.rotateZ( apexAngle ); tangentB.rotateZ( apexAngle ); tangentA.add( this.renderOrigin ); tangentB.add( this.renderOrigin ); this.setSurfaceRenderPoint( 0, tangentA ); this.setSurfaceRenderPoint( 1, this.apex.renderOrigin ); this.setSurfaceRenderPoint( 2, tangentB ); // render var elem = this.getSurfaceRenderElement( ctx, renderer ); renderer.renderPath( ctx, elem, this.surfacePathCommands ); renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, this.color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Cone.prototype.getSurfaceRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.surfaceSvgElement ) { // create svgElement this.surfaceSvgElement = document.createElementNS( svgURI, 'path' ); this.surfaceSvgElement.setAttribute( 'stroke-linecap', 'round' ); this.surfaceSvgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.surfaceSvgElement; }; Cone.prototype.setSurfaceRenderPoint = function( index, point ) { var renderPoint = this.surfacePathCommands[ index ].renderPoints[0]; renderPoint.set( point ); }; return Cone; } ) ); ================================================ FILE: js/cylinder.js ================================================ /** * Cylinder composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./path-command'), require('./shape'), require('./group'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Cylinder = factory( Zdog, Zdog.PathCommand, Zdog.Shape, Zdog.Group, Zdog.Ellipse ); } }( this, function factory( utils, PathCommand, Shape, Group, Ellipse ) { function noop() {} // ----- CylinderGroup ----- // var CylinderGroup = Group.subclass({ color: '#333', updateSort: true, }); CylinderGroup.prototype.create = function() { Group.prototype.create.apply( this, arguments ); this.pathCommands = [ new PathCommand( 'move', [ {} ] ), new PathCommand( 'line', [ {} ] ), ]; }; CylinderGroup.prototype.render = function( ctx, renderer ) { this.renderCylinderSurface( ctx, renderer ); Group.prototype.render.apply( this, arguments ); }; CylinderGroup.prototype.renderCylinderSurface = function( ctx, renderer ) { if ( !this.visible ) { return; } // render cylinder surface var elem = this.getRenderElement( ctx, renderer ); var frontBase = this.frontBase; var rearBase = this.rearBase; var scale = frontBase.renderNormal.magnitude(); var strokeWidth = frontBase.diameter * scale + frontBase.getLineWidth(); // set path command render points this.pathCommands[0].renderPoints[0].set( frontBase.renderOrigin ); this.pathCommands[1].renderPoints[0].set( rearBase.renderOrigin ); if ( renderer.isCanvas ) { ctx.lineCap = 'butt'; // nice } renderer.renderPath( ctx, elem, this.pathCommands ); renderer.stroke( ctx, elem, true, this.color, strokeWidth ); renderer.end( ctx, elem ); if ( renderer.isCanvas ) { ctx.lineCap = 'round'; // reset } }; var svgURI = 'http://www.w3.org/2000/svg'; CylinderGroup.prototype.getRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.svgElement ) { // create svgElement this.svgElement = document.createElementNS( svgURI, 'path' ); } return this.svgElement; }; // prevent double-creation in parent.copyGraph() // only create in Cylinder.create() CylinderGroup.prototype.copyGraph = noop; // ----- CylinderEllipse ----- // var CylinderEllipse = Ellipse.subclass(); CylinderEllipse.prototype.copyGraph = noop; // ----- Cylinder ----- // var Cylinder = Shape.subclass({ diameter: 1, length: 1, frontFace: undefined, fill: true, }); var TAU = utils.TAU; Cylinder.prototype.create = function( /* options */) { // call super Shape.prototype.create.apply( this, arguments ); // composite shape, create child shapes // CylinderGroup to render cylinder surface then bases this.group = new CylinderGroup({ addTo: this, color: this.color, visible: this.visible, }); var baseZ = this.length / 2; var baseColor = this.backface || true; // front outside base this.frontBase = this.group.frontBase = new Ellipse({ addTo: this.group, diameter: this.diameter, translate: { z: baseZ }, rotate: { y: TAU/2 }, color: this.color, stroke: this.stroke, fill: this.fill, backface: this.frontFace || baseColor, visible: this.visible, }); // back outside base this.rearBase = this.group.rearBase = this.frontBase.copy({ translate: { z: -baseZ }, rotate: { y: 0 }, backface: baseColor, }); }; // Cylinder shape does not render anything Cylinder.prototype.render = function() {}; // ----- set child properties ----- // var childProperties = [ 'stroke', 'fill', 'color', 'visible' ]; childProperties.forEach( function( property ) { // use proxy property for custom getter & setter var _prop = '_' + property; Object.defineProperty( Cylinder.prototype, property, { get: function() { return this[ _prop ]; }, set: function( value ) { this[ _prop ] = value; // set property on children if ( this.frontBase ) { this.frontBase[ property ] = value; this.rearBase[ property ] = value; this.group[ property ] = value; } }, } ); } ); // TODO child property setter for backface, frontBaseColor, & rearBaseColor return Cylinder; } ) ); ================================================ FILE: js/dragger.js ================================================ /** * Dragger */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.Dragger = factory(); } }( this, function factory() { // quick & dirty drag event stuff // messes up if multiple pointers/touches // check for browser window #85 var hasWindow = typeof window != 'undefined'; // event support, default to mouse events var downEvent = 'mousedown'; var moveEvent = 'mousemove'; var upEvent = 'mouseup'; if ( hasWindow ) { if ( window.PointerEvent ) { // PointerEvent, Chrome downEvent = 'pointerdown'; moveEvent = 'pointermove'; upEvent = 'pointerup'; } else if ( 'ontouchstart' in window ) { // Touch Events, iOS Safari downEvent = 'touchstart'; moveEvent = 'touchmove'; upEvent = 'touchend'; } } function noop() {} function Dragger( options ) { this.create( options || {} ); } Dragger.prototype.create = function( options ) { this.onDragStart = options.onDragStart || noop; this.onDragMove = options.onDragMove || noop; this.onDragEnd = options.onDragEnd || noop; this.bindDrag( options.startElement ); }; Dragger.prototype.bindDrag = function( element ) { element = this.getQueryElement( element ); if ( !element ) { return; } // disable browser gestures #53 element.style.touchAction = 'none'; element.addEventListener( downEvent, this ); }; Dragger.prototype.getQueryElement = function( element ) { if ( typeof element == 'string' ) { // with string, query selector element = document.querySelector( element ); } return element; }; Dragger.prototype.handleEvent = function( event ) { var method = this[ 'on' + event.type ]; if ( method ) { method.call( this, event ); } }; Dragger.prototype.onmousedown = Dragger.prototype.onpointerdown = function( event ) { this.dragStart( event, event ); }; Dragger.prototype.ontouchstart = function( event ) { this.dragStart( event, event.changedTouches[0] ); }; Dragger.prototype.dragStart = function( event, pointer ) { event.preventDefault(); this.dragStartX = pointer.pageX; this.dragStartY = pointer.pageY; if ( hasWindow ) { window.addEventListener( moveEvent, this ); window.addEventListener( upEvent, this ); } this.onDragStart( pointer ); }; Dragger.prototype.ontouchmove = function( event ) { // HACK, moved touch may not be first this.dragMove( event, event.changedTouches[0] ); }; Dragger.prototype.onmousemove = Dragger.prototype.onpointermove = function( event ) { this.dragMove( event, event ); }; Dragger.prototype.dragMove = function( event, pointer ) { event.preventDefault(); var moveX = pointer.pageX - this.dragStartX; var moveY = pointer.pageY - this.dragStartY; this.onDragMove( pointer, moveX, moveY ); }; Dragger.prototype.onmouseup = Dragger.prototype.onpointerup = Dragger.prototype.ontouchend = Dragger.prototype.dragEnd = function( /* event */) { window.removeEventListener( moveEvent, this ); window.removeEventListener( upEvent, this ); this.onDragEnd(); }; return Dragger; } ) ); ================================================ FILE: js/ellipse.js ================================================ /** * Ellipse */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Ellipse = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var Ellipse = Shape.subclass({ diameter: 1, width: undefined, height: undefined, quarters: 4, closed: false, }); Ellipse.prototype.setPath = function() { var width = this.width != undefined ? this.width : this.diameter; var height = this.height != undefined ? this.height : this.diameter; var x = width/2; var y = height/2; this.path = [ { x: 0, y: -y }, { arc: [ // top right { x: x, y: -y }, { x: x, y: 0 }, ] }, ]; // bottom right if ( this.quarters > 1 ) { this.path.push({ arc: [ { x: x, y: y }, { x: 0, y: y }, ] }); } // bottom left if ( this.quarters > 2 ) { this.path.push({ arc: [ { x: -x, y: y }, { x: -x, y: 0 }, ] }); } // top left if ( this.quarters > 3 ) { this.path.push({ arc: [ { x: -x, y: -y }, { x: 0, y: -y }, ] }); } }; return Ellipse; } ) ); ================================================ FILE: js/group.js ================================================ /** * Group */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./anchor') ); } else { // browser global var Zdog = root.Zdog; Zdog.Group = factory( Zdog.Anchor ); } }( this, function factory( Anchor ) { var Group = Anchor.subclass({ updateSort: false, visible: true, }); // ----- update ----- // Group.prototype.updateSortValue = function() { var sortValueTotal = 0; this.flatGraph.forEach( function( item ) { item.updateSortValue(); sortValueTotal += item.sortValue; } ); // average sort value of all points // def not geometrically correct, but works for me this.sortValue = sortValueTotal / this.flatGraph.length; if ( this.updateSort ) { this.flatGraph.sort( Anchor.shapeSorter ); } }; // ----- render ----- // Group.prototype.render = function( ctx, renderer ) { if ( !this.visible ) { return; } this.flatGraph.forEach( function( item ) { item.render( ctx, renderer ); } ); }; // actual group flatGraph only used inside group Group.prototype.updateFlatGraph = function() { // do not include self var flatGraph = []; this.flatGraph = this.addChildFlatGraph( flatGraph ); }; // do not include children, group handles rendering & sorting internally Group.prototype.getFlatGraph = function() { return [ this ]; }; return Group; } ) ); ================================================ FILE: js/hemisphere.js ================================================ /** * Hemisphere composite shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./anchor'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Hemisphere = factory( Zdog, Zdog.Vector, Zdog.Anchor, Zdog.Ellipse ); } }( this, function factory( utils, Vector, Anchor, Ellipse ) { var Hemisphere = Ellipse.subclass({ fill: true, }); var TAU = utils.TAU; Hemisphere.prototype.create = function( /* options */) { // call super Ellipse.prototype.create.apply( this, arguments ); // composite shape, create child shapes this.apex = new Anchor({ addTo: this, translate: { z: this.diameter / 2 }, }); // vector used for calculation this.renderCentroid = new Vector(); }; Hemisphere.prototype.updateSortValue = function() { // centroid of hemisphere is 3/8 between origin and apex this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 3/8 ); this.sortValue = this.renderCentroid.z; }; Hemisphere.prototype.render = function( ctx, renderer ) { this.renderDome( ctx, renderer ); // call super Ellipse.prototype.render.apply( this, arguments ); }; Hemisphere.prototype.renderDome = function( ctx, renderer ) { if ( !this.visible ) { return; } var elem = this.getDomeRenderElement( ctx, renderer ); var contourAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x ); var domeRadius = this.diameter / 2 * this.renderNormal.magnitude(); var x = this.renderOrigin.x; var y = this.renderOrigin.y; if ( renderer.isCanvas ) { // canvas var startAngle = contourAngle + TAU/4; var endAngle = contourAngle - TAU/4; ctx.beginPath(); ctx.arc( x, y, domeRadius, startAngle, endAngle ); } else if ( renderer.isSvg ) { // svg contourAngle = ( contourAngle - TAU/4 ) / TAU * 360; this.domeSvgElement.setAttribute( 'd', 'M ' + -domeRadius + ',0 A ' + domeRadius + ',' + domeRadius + ' 0 0 1 ' + domeRadius + ',0' ); this.domeSvgElement.setAttribute( 'transform', 'translate(' + x + ',' + y + ' ) rotate(' + contourAngle + ')' ); } renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, this.color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Hemisphere.prototype.getDomeRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.domeSvgElement ) { // create svgElement this.domeSvgElement = document.createElementNS( svgURI, 'path' ); this.domeSvgElement.setAttribute( 'stroke-linecap', 'round' ); this.domeSvgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.domeSvgElement; }; return Hemisphere; } ) ); ================================================ FILE: js/illustration.js ================================================ /** * Illustration */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./anchor'), require('./dragger') ); } else { // browser global var Zdog = root.Zdog; Zdog.Illustration = factory( Zdog, Zdog.Anchor, Zdog.Dragger ); } }( this, function factory( utils, Anchor, Dragger ) { function noop() {} var TAU = utils.TAU; var Illustration = Anchor.subclass({ element: undefined, centered: true, zoom: 1, dragRotate: false, resize: false, onPrerender: noop, onDragStart: noop, onDragMove: noop, onDragEnd: noop, onResize: noop, }); utils.extend( Illustration.prototype, Dragger.prototype ); Illustration.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); Dragger.prototype.create.call( this, options ); this.setElement( this.element ); this.setDragRotate( this.dragRotate ); this.setResize( this.resize ); }; Illustration.prototype.setElement = function( element ) { element = this.getQueryElement( element ); if ( !element ) { throw new Error( 'Zdog.Illustration element required. Set to ' + element ); } var nodeName = element.nodeName.toLowerCase(); if ( nodeName == 'canvas' ) { this.setCanvas( element ); } else if ( nodeName == 'svg' ) { this.setSvg( element ); } }; Illustration.prototype.setSize = function( width, height ) { width = Math.round( width ); height = Math.round( height ); if ( this.isCanvas ) { this.setSizeCanvas( width, height ); } else if ( this.isSvg ) { this.setSizeSvg( width, height ); } }; Illustration.prototype.setResize = function( resize ) { this.resize = resize; // create resize event listener if ( !this.resizeListener ) { this.resizeListener = this.onWindowResize.bind( this ); } // add/remove event listener if ( resize ) { window.addEventListener( 'resize', this.resizeListener ); this.onWindowResize(); } else { window.removeEventListener( 'resize', this.resizeListener ); } }; // TODO debounce this? Illustration.prototype.onWindowResize = function() { this.setMeasuredSize(); this.onResize( this.width, this.height ); }; Illustration.prototype.setMeasuredSize = function() { var width, height; var isFullscreen = this.resize == 'fullscreen'; if ( isFullscreen ) { width = window.innerWidth; height = window.innerHeight; } else { var rect = this.element.getBoundingClientRect(); width = rect.width; height = rect.height; } this.setSize( width, height ); }; // ----- render ----- // Illustration.prototype.renderGraph = function( item ) { if ( this.isCanvas ) { this.renderGraphCanvas( item ); } else if ( this.isSvg ) { this.renderGraphSvg( item ); } }; // combo method Illustration.prototype.updateRenderGraph = function( item ) { this.updateGraph(); this.renderGraph( item ); }; // ----- canvas ----- // Illustration.prototype.setCanvas = function( element ) { this.element = element; this.isCanvas = true; // update related properties this.ctx = this.element.getContext('2d'); // set initial size this.setSizeCanvas( element.width, element.height ); }; Illustration.prototype.setSizeCanvas = function( width, height ) { this.width = width; this.height = height; // up-rez for hi-DPI devices var pixelRatio = this.pixelRatio = window.devicePixelRatio || 1; this.element.width = this.canvasWidth = width * pixelRatio; this.element.height = this.canvasHeight = height * pixelRatio; var needsHighPixelRatioSizing = pixelRatio > 1 && !this.resize; if ( needsHighPixelRatioSizing ) { this.element.style.width = width + 'px'; this.element.style.height = height + 'px'; } }; Illustration.prototype.renderGraphCanvas = function( item ) { item = item || this; this.prerenderCanvas(); Anchor.prototype.renderGraphCanvas.call( item, this.ctx ); this.postrenderCanvas(); }; Illustration.prototype.prerenderCanvas = function() { var ctx = this.ctx; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.clearRect( 0, 0, this.canvasWidth, this.canvasHeight ); ctx.save(); if ( this.centered ) { var centerX = this.width / 2 * this.pixelRatio; var centerY = this.height / 2 * this.pixelRatio; ctx.translate( centerX, centerY ); } var scale = this.pixelRatio * this.zoom; ctx.scale( scale, scale ); this.onPrerender( ctx ); }; Illustration.prototype.postrenderCanvas = function() { this.ctx.restore(); }; // ----- svg ----- // Illustration.prototype.setSvg = function( element ) { this.element = element; this.isSvg = true; this.pixelRatio = 1; // set initial size from width & height attributes var width = element.getAttribute('width'); var height = element.getAttribute('height'); this.setSizeSvg( width, height ); }; Illustration.prototype.setSizeSvg = function( width, height ) { this.width = width; this.height = height; var viewWidth = width / this.zoom; var viewHeight = height / this.zoom; var viewX = this.centered ? -viewWidth/2 : 0; var viewY = this.centered ? -viewHeight/2 : 0; this.element.setAttribute( 'viewBox', viewX + ' ' + viewY + ' ' + viewWidth + ' ' + viewHeight ); if ( this.resize ) { // remove size attributes, let size be determined by viewbox this.element.removeAttribute('width'); this.element.removeAttribute('height'); } else { this.element.setAttribute( 'width', width ); this.element.setAttribute( 'height', height ); } }; Illustration.prototype.renderGraphSvg = function( item ) { item = item || this; empty( this.element ); this.onPrerender( this.element ); Anchor.prototype.renderGraphSvg.call( item, this.element ); }; function empty( element ) { while ( element.firstChild ) { element.removeChild( element.firstChild ); } } // ----- drag ----- // Illustration.prototype.setDragRotate = function( item ) { if ( !item ) { return; } else if ( item === true ) { /* eslint consistent-this: "off" */ item = this; } this.dragRotate = item; this.bindDrag( this.element ); }; Illustration.prototype.dragStart = function( /* event, pointer */) { this.dragStartRX = this.dragRotate.rotate.x; this.dragStartRY = this.dragRotate.rotate.y; Dragger.prototype.dragStart.apply( this, arguments ); }; Illustration.prototype.dragMove = function( event, pointer ) { var moveX = pointer.pageX - this.dragStartX; var moveY = pointer.pageY - this.dragStartY; var displaySize = Math.min( this.width, this.height ); var moveRY = moveX/displaySize * TAU; var moveRX = moveY/displaySize * TAU; this.dragRotate.rotate.x = this.dragStartRX - moveRX; this.dragRotate.rotate.y = this.dragStartRY - moveRY; Dragger.prototype.dragMove.apply( this, arguments ); }; return Illustration; } ) ); ================================================ FILE: js/index.js ================================================ /** * Index */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./canvas-renderer'), require('./svg-renderer'), require('./vector'), require('./anchor'), require('./dragger'), require('./illustration'), require('./path-command'), require('./shape'), require('./group'), require('./rect'), require('./rounded-rect'), require('./ellipse'), require('./polygon'), require('./hemisphere'), require('./cylinder'), require('./cone'), require('./box') ); } else if ( typeof define == 'function' && define.amd ) { /* globals define */ // AMD define( 'zdog', [], root.Zdog ); } /* eslint-disable max-params */ } )( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor, Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect, Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box ) { /* eslint-enable max-params */ Zdog.CanvasRenderer = CanvasRenderer; Zdog.SvgRenderer = SvgRenderer; Zdog.Vector = Vector; Zdog.Anchor = Anchor; Zdog.Dragger = Dragger; Zdog.Illustration = Illustration; Zdog.PathCommand = PathCommand; Zdog.Shape = Shape; Zdog.Group = Group; Zdog.Rect = Rect; Zdog.RoundedRect = RoundedRect; Zdog.Ellipse = Ellipse; Zdog.Polygon = Polygon; Zdog.Hemisphere = Hemisphere; Zdog.Cylinder = Cylinder; Zdog.Cone = Cone; Zdog.Box = Box; return Zdog; } ); ================================================ FILE: js/path-command.js ================================================ /** * PathCommand */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./vector') ); } else { // browser global var Zdog = root.Zdog; Zdog.PathCommand = factory( Zdog.Vector ); } }( this, function factory( Vector ) { function PathCommand( method, points, previousPoint ) { this.method = method; this.points = points.map( mapVectorPoint ); this.renderPoints = points.map( mapNewVector ); this.previousPoint = previousPoint; this.endRenderPoint = this.renderPoints[ this.renderPoints.length - 1 ]; // arc actions come with previous point & corner point // but require bezier control points if ( method == 'arc' ) { this.controlPoints = [ new Vector(), new Vector() ]; } } function mapVectorPoint( point ) { if ( point instanceof Vector ) { return point; } else { return new Vector( point ); } } function mapNewVector( point ) { return new Vector( point ); } PathCommand.prototype.reset = function() { // reset renderPoints back to orignal points position var points = this.points; this.renderPoints.forEach( function( renderPoint, i ) { var point = points[i]; renderPoint.set( point ); } ); }; PathCommand.prototype.transform = function( translation, rotation, scale ) { this.renderPoints.forEach( function( renderPoint ) { renderPoint.transform( translation, rotation, scale ); } ); }; PathCommand.prototype.render = function( ctx, elem, renderer ) { return this[ this.method ]( ctx, elem, renderer ); }; PathCommand.prototype.move = function( ctx, elem, renderer ) { return renderer.move( ctx, elem, this.renderPoints[0] ); }; PathCommand.prototype.line = function( ctx, elem, renderer ) { return renderer.line( ctx, elem, this.renderPoints[0] ); }; PathCommand.prototype.bezier = function( ctx, elem, renderer ) { var cp0 = this.renderPoints[0]; var cp1 = this.renderPoints[1]; var end = this.renderPoints[2]; return renderer.bezier( ctx, elem, cp0, cp1, end ); }; var arcHandleLength = 9/16; PathCommand.prototype.arc = function( ctx, elem, renderer ) { var prev = this.previousPoint; var corner = this.renderPoints[0]; var end = this.renderPoints[1]; var cp0 = this.controlPoints[0]; var cp1 = this.controlPoints[1]; cp0.set( prev ).lerp( corner, arcHandleLength ); cp1.set( end ).lerp( corner, arcHandleLength ); return renderer.bezier( ctx, elem, cp0, cp1, end ); }; return PathCommand; } ) ); ================================================ FILE: js/polygon.js ================================================ /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Polygon = factory( Zdog, Zdog.Shape ); } }( this, function factory( utils, Shape ) { var Polygon = Shape.subclass({ sides: 3, radius: 0.5, }); var TAU = utils.TAU; Polygon.prototype.setPath = function() { this.path = []; for ( var i = 0; i < this.sides; i++ ) { var theta = i / this.sides * TAU - TAU/4; var x = Math.cos( theta ) * this.radius; var y = Math.sin( theta ) * this.radius; this.path.push({ x: x, y: y }); } }; return Polygon; } ) ); ================================================ FILE: js/rect.js ================================================ /** * Rect */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.Rect = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var Rect = Shape.subclass({ width: 1, height: 1, }); Rect.prototype.setPath = function() { var x = this.width / 2; var y = this.height / 2; /* eslint key-spacing: "off" */ this.path = [ { x: -x, y: -y }, { x: x, y: -y }, { x: x, y: y }, { x: -x, y: y }, ]; }; return Rect; } ) ); ================================================ FILE: js/rounded-rect.js ================================================ /** * RoundedRect */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./shape') ); } else { // browser global var Zdog = root.Zdog; Zdog.RoundedRect = factory( Zdog.Shape ); } }( this, function factory( Shape ) { var RoundedRect = Shape.subclass({ width: 1, height: 1, cornerRadius: 0.25, closed: false, }); RoundedRect.prototype.setPath = function() { /* eslint id-length: [ "error", { "min": 2, "exceptions": [ "x", "y" ] }], key-spacing: "off" */ var xA = this.width / 2; var yA = this.height / 2; var shortSide = Math.min( xA, yA ); var cornerRadius = Math.min( this.cornerRadius, shortSide ); var xB = xA - cornerRadius; var yB = yA - cornerRadius; var path = [ // top right corner { x: xB, y: -yA }, { arc: [ { x: xA, y: -yA }, { x: xA, y: -yB }, ] }, ]; // bottom right corner if ( yB ) { path.push({ x: xA, y: yB }); } path.push({ arc: [ { x: xA, y: yA }, { x: xB, y: yA }, ] }); // bottom left corner if ( xB ) { path.push({ x: -xB, y: yA }); } path.push({ arc: [ { x: -xA, y: yA }, { x: -xA, y: yB }, ] }); // top left corner if ( yB ) { path.push({ x: -xA, y: -yB }); } path.push({ arc: [ { x: -xA, y: -yA }, { x: -xB, y: -yA }, ] }); // back to top right corner if ( xB ) { path.push({ x: xB, y: -yA }); } this.path = path; }; return RoundedRect; } ) ); ================================================ FILE: js/shape.js ================================================ /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./path-command'), require('./anchor') ); } else { // browser global var Zdog = root.Zdog; Zdog.Shape = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor ); } }( this, function factory( utils, Vector, PathCommand, Anchor ) { var Shape = Anchor.subclass({ stroke: 1, fill: false, color: '#333', closed: true, visible: true, path: [ {} ], front: { z: 1 }, backface: true, }); Shape.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); // front this.front = new Vector( options.front || this.front ); this.renderFront = new Vector( this.front ); this.renderNormal = new Vector(); }; var actionNames = [ 'move', 'line', 'bezier', 'arc', ]; Shape.prototype.updatePath = function() { this.setPath(); this.updatePathCommands(); }; // place holder for Ellipse, Rect, etc. Shape.prototype.setPath = function() {}; // parse path into PathCommands Shape.prototype.updatePathCommands = function() { var previousPoint; this.pathCommands = this.path.map( function( pathPart, i ) { // pathPart can be just vector coordinates -> { x, y, z } // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] } var keys = Object.keys( pathPart ); var method = keys[0]; var points = pathPart[ method ]; // default to line if no instruction var isInstruction = keys.length == 1 && actionNames.indexOf( method ) != -1; if ( !isInstruction ) { method = 'line'; points = pathPart; } // munge single-point methods like line & move without arrays var isLineOrMove = method == 'line' || method == 'move'; var isPointsArray = Array.isArray( points ); if ( isLineOrMove && !isPointsArray ) { points = [ points ]; } // first action is always move method = i === 0 ? 'move' : method; // arcs require previous last point var command = new PathCommand( method, points, previousPoint ); // update previousLastPoint previousPoint = command.endRenderPoint; return command; } ); }; // ----- update ----- // Shape.prototype.reset = function() { this.renderOrigin.set( this.origin ); this.renderFront.set( this.front ); // reset command render points this.pathCommands.forEach( function( command ) { command.reset(); } ); }; Shape.prototype.transform = function( translation, rotation, scale ) { // calculate render points backface visibility & cone/hemisphere shapes this.renderOrigin.transform( translation, rotation, scale ); this.renderFront.transform( translation, rotation, scale ); this.renderNormal.set( this.renderOrigin ).subtract( this.renderFront ); // transform points this.pathCommands.forEach( function( command ) { command.transform( translation, rotation, scale ); } ); // transform children this.children.forEach( function( child ) { child.transform( translation, rotation, scale ); } ); }; Shape.prototype.updateSortValue = function() { // sort by average z of all points // def not geometrically correct, but works for me var pointCount = this.pathCommands.length; var firstPoint = this.pathCommands[0].endRenderPoint; var lastPoint = this.pathCommands[ pointCount - 1 ].endRenderPoint; // ignore the final point if self closing shape var isSelfClosing = pointCount > 2 && firstPoint.isSame( lastPoint ); if ( isSelfClosing ) { pointCount -= 1; } var sortValueTotal = 0; for ( var i = 0; i < pointCount; i++ ) { sortValueTotal += this.pathCommands[i].endRenderPoint.z; } this.sortValue = sortValueTotal/pointCount; }; // ----- render ----- // Shape.prototype.render = function( ctx, renderer ) { var length = this.pathCommands.length; if ( !this.visible || !length ) { return; } // do not render if hiding backface this.isFacingBack = this.renderNormal.z > 0; if ( !this.backface && this.isFacingBack ) { return; } if ( !renderer ) { throw new Error( 'Zdog renderer required. Set to ' + renderer ); } // render dot or path var isDot = length == 1; if ( renderer.isCanvas && isDot ) { this.renderCanvasDot( ctx, renderer ); } else { this.renderPath( ctx, renderer ); } }; var TAU = utils.TAU; // Safari does not render lines with no size, have to render circle instead Shape.prototype.renderCanvasDot = function( ctx ) { var lineWidth = this.getLineWidth(); if ( !lineWidth ) { return; } ctx.fillStyle = this.getRenderColor(); var point = this.pathCommands[0].endRenderPoint; ctx.beginPath(); var radius = lineWidth/2; ctx.arc( point.x, point.y, radius, 0, TAU ); ctx.fill(); }; Shape.prototype.getLineWidth = function() { if ( !this.stroke ) { return 0; } if ( this.stroke == true ) { return 1; } return this.stroke; }; Shape.prototype.getRenderColor = function() { // use backface color if applicable var isBackfaceColor = typeof this.backface == 'string' && this.isFacingBack; var color = isBackfaceColor ? this.backface : this.color; return color; }; Shape.prototype.renderPath = function( ctx, renderer ) { var elem = this.getRenderElement( ctx, renderer ); var isTwoPoints = this.pathCommands.length == 2 && this.pathCommands[1].method == 'line'; var isClosed = !isTwoPoints && this.closed; var color = this.getRenderColor(); renderer.renderPath( ctx, elem, this.pathCommands, isClosed ); renderer.stroke( ctx, elem, this.stroke, color, this.getLineWidth() ); renderer.fill( ctx, elem, this.fill, color ); renderer.end( ctx, elem ); }; var svgURI = 'http://www.w3.org/2000/svg'; Shape.prototype.getRenderElement = function( ctx, renderer ) { if ( !renderer.isSvg ) { return; } if ( !this.svgElement ) { // create svgElement this.svgElement = document.createElementNS( svgURI, 'path' ); this.svgElement.setAttribute( 'stroke-linecap', 'round' ); this.svgElement.setAttribute( 'stroke-linejoin', 'round' ); } return this.svgElement; }; return Shape; } ) ); ================================================ FILE: js/svg-renderer.js ================================================ /** * SvgRenderer */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory(); } else { // browser global root.Zdog.SvgRenderer = factory(); } }( this, function factory() { var SvgRenderer = { isSvg: true }; // round path coordinates to 3 decimals var round = SvgRenderer.round = function( num ) { return Math.round( num * 1000 ) / 1000; }; function getPointString( point ) { return round( point.x ) + ',' + round( point.y ) + ' '; } SvgRenderer.begin = function() {}; SvgRenderer.move = function( svg, elem, point ) { return 'M' + getPointString( point ); }; SvgRenderer.line = function( svg, elem, point ) { return 'L' + getPointString( point ); }; SvgRenderer.bezier = function( svg, elem, cp0, cp1, end ) { return 'C' + getPointString( cp0 ) + getPointString( cp1 ) + getPointString( end ); }; SvgRenderer.closePath = function( /* elem */) { return 'Z'; }; SvgRenderer.setPath = function( svg, elem, pathValue ) { elem.setAttribute( 'd', pathValue ); }; SvgRenderer.renderPath = function( svg, elem, pathCommands, isClosed ) { var pathValue = ''; pathCommands.forEach( function( command ) { pathValue += command.render( svg, elem, SvgRenderer ); } ); if ( isClosed ) { pathValue += this.closePath( svg, elem ); } this.setPath( svg, elem, pathValue ); }; SvgRenderer.stroke = function( svg, elem, isStroke, color, lineWidth ) { if ( !isStroke ) { return; } elem.setAttribute( 'stroke', color ); elem.setAttribute( 'stroke-width', lineWidth ); }; SvgRenderer.fill = function( svg, elem, isFill, color ) { var fillColor = isFill ? color : 'none'; elem.setAttribute( 'fill', fillColor ); }; SvgRenderer.end = function( svg, elem ) { svg.appendChild( elem ); }; return SvgRenderer; } ) ); ================================================ FILE: js/vector.js ================================================ /** * Vector */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./boilerplate') ); } else { // browser global var Zdog = root.Zdog; Zdog.Vector = factory( Zdog ); } }( this, function factory( utils ) { function Vector( position ) { this.set( position ); } var TAU = utils.TAU; // 'pos' = 'position' Vector.prototype.set = function( pos ) { this.x = pos && pos.x || 0; this.y = pos && pos.y || 0; this.z = pos && pos.z || 0; return this; }; // set coordinates without sanitizing // vec.write({ y: 2 }) only sets y coord Vector.prototype.write = function( pos ) { if ( !pos ) { return this; } this.x = pos.x != undefined ? pos.x : this.x; this.y = pos.y != undefined ? pos.y : this.y; this.z = pos.z != undefined ? pos.z : this.z; return this; }; Vector.prototype.rotate = function( rotation ) { if ( !rotation ) { return; } this.rotateZ( rotation.z ); this.rotateY( rotation.y ); this.rotateX( rotation.x ); return this; }; Vector.prototype.rotateZ = function( angle ) { rotateProperty( this, angle, 'x', 'y' ); }; Vector.prototype.rotateX = function( angle ) { rotateProperty( this, angle, 'y', 'z' ); }; Vector.prototype.rotateY = function( angle ) { rotateProperty( this, angle, 'x', 'z' ); }; function rotateProperty( vec, angle, propA, propB ) { if ( !angle || angle % TAU === 0 ) { return; } var cos = Math.cos( angle ); var sin = Math.sin( angle ); var a = vec[ propA ]; var b = vec[ propB ]; vec[ propA ] = a * cos - b * sin; vec[ propB ] = b * cos + a * sin; } Vector.prototype.isSame = function( pos ) { if ( !pos ) { return false; } return this.x === pos.x && this.y === pos.y && this.z === pos.z; }; Vector.prototype.add = function( pos ) { if ( !pos ) { return this; } this.x += pos.x || 0; this.y += pos.y || 0; this.z += pos.z || 0; return this; }; Vector.prototype.subtract = function( pos ) { if ( !pos ) { return this; } this.x -= pos.x || 0; this.y -= pos.y || 0; this.z -= pos.z || 0; return this; }; Vector.prototype.multiply = function( pos ) { if ( pos == undefined ) { return this; } // multiple all values by same number if ( typeof pos == 'number' ) { this.x *= pos; this.y *= pos; this.z *= pos; } else { // multiply object this.x *= pos.x != undefined ? pos.x : 1; this.y *= pos.y != undefined ? pos.y : 1; this.z *= pos.z != undefined ? pos.z : 1; } return this; }; Vector.prototype.transform = function( translation, rotation, scale ) { this.multiply( scale ); this.rotate( rotation ); this.add( translation ); return this; }; Vector.prototype.lerp = function( pos, alpha ) { this.x = utils.lerp( this.x, pos.x || 0, alpha ); this.y = utils.lerp( this.y, pos.y || 0, alpha ); this.z = utils.lerp( this.z, pos.z || 0, alpha ); return this; }; Vector.prototype.magnitude = function() { var sum = this.x * this.x + this.y * this.y + this.z * this.z; return getMagnitudeSqrt( sum ); }; function getMagnitudeSqrt( sum ) { // PERF: check if sum ~= 1 and skip sqrt if ( Math.abs( sum - 1 ) < 0.00000001 ) { return 1; } return Math.sqrt( sum ); } Vector.prototype.magnitude2d = function() { var sum = this.x * this.x + this.y * this.y; return getMagnitudeSqrt( sum ); }; Vector.prototype.copy = function() { return new Vector( this ); }; return Vector; } ) ); ================================================ FILE: package.json ================================================ { "name": "zdog", "version": "1.1.3", "description": "Round, flat, designer-friendly pseudo-3D engine", "main": "js/index.js", "unpkg": "dist/zdog.dist.min.js", "files": [ "dist/*.*", "js/*.*", "!js/.*", "!dist/.*" ], "devDependencies": { "eslint": "^8.7.0", "eslint-plugin-metafizzy": "^1.0.0", "uglify-js": "^3.6.3" }, "scripts": { "bundle": "node tasks/bundle", "dist": "npm run bundle && npm run uglify", "lint": "npx eslint .", "lintFix": "npx eslint . --fix", "preversion": "npm run lint", "version": "node tasks/version && npm run dist && git add -A dist js", "test": "npm run lint", "uglify": "npx uglifyjs dist/zdog.dist.js -o dist/zdog.dist.min.js --mangle --comments /^!/" }, "repository": { "type": "git", "url": "git+https://github.com/metafizzy/zdog.git" }, "keywords": [ "3D", "canvas", "svg" ], "author": "David DeSandro", "license": "MIT", "bugs": { "url": "https://github.com/metafizzy/zdog/issues" }, "homepage": "https://zzz.dog" } ================================================ FILE: tasks/.eslintrc.js ================================================ /* eslint-env node */ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/node', env: { browser: false, commonjs: true, }, rules: { }, }; ================================================ FILE: tasks/bundle.js ================================================ const fs = require('fs'); const execSync = require('child_process').execSync; // get file paths from index.js const indexPath = 'js/index.js'; let indexSrc = fs.readFileSync( `./${indexPath}`, 'utf8' ); let cjsBlockRegex = /module\.exports = factory\([\w ,'.\-()/\n]+;/i; let cjsBlockMatch = indexSrc.match( cjsBlockRegex ); let paths = cjsBlockMatch[0].match( /require\('([.\-/\w]+)'\)/gi ); paths = paths.map( function( path ) { return path.replace( "require('.", 'js' ).replace( "')", '.js' ); } ); paths.push( indexPath ); execSync(`cat ${paths.join(' ')} > dist/zdog.dist.js`); console.log('bundled dist/zdog.dist.js'); ================================================ FILE: tasks/version.js ================================================ const fs = require('fs'); const version = require('../package.json').version; const boilerplatePath = 'js/boilerplate.js'; let boilerplateSrc = fs.readFileSync( boilerplatePath, 'utf8' ); boilerplateSrc = boilerplateSrc.replace( /\n \* Zdog v\d+\.\d+\.\d+/, `\n * Zdog v${version}` ); fs.writeFileSync( boilerplatePath, boilerplateSrc, 'utf8' ); console.log(`updated ${boilerplatePath} to ${version}`);