├── .babelrc ├── .bowerrc ├── .gitignore ├── .scss-lint.yml ├── .travis.yml ├── AndroidIconAnimator.sublime-project ├── LICENSE ├── README.md ├── app ├── components │ ├── canvas │ │ ├── canvas-ruler.js │ │ ├── canvas.html │ │ ├── canvas.js │ │ └── canvas.scss │ ├── filehandlers │ │ ├── filedroptarget.js │ │ ├── filehandlers.scss │ │ └── fileopenhandler.js │ ├── layertimeline │ │ ├── consts.js │ │ ├── layertimeline.html │ │ ├── layertimeline.js │ │ ├── layertimeline.scss │ │ └── timelinegrid.js │ ├── propertyinspector │ │ ├── propertyinspector.html │ │ ├── propertyinspector.js │ │ └── propertyinspector.scss │ ├── scrollgroup │ │ └── scrollgroup.js │ └── splitter │ │ ├── splitter.js │ │ └── splitter.scss ├── favicon.ico ├── icons │ ├── add_animation.svg │ ├── add_layer.svg │ ├── animation.svg │ ├── animation_block.svg │ ├── artwork.svg │ ├── collection.svg │ ├── layer.svg │ ├── layer_group.svg │ ├── mask_layer.svg │ └── path_layer.svg ├── images │ ├── pauseplay-white@2x.png │ └── playpause-white@2x.png ├── index.html ├── pages │ └── studio │ │ ├── dialog-svg-drop.html │ │ ├── studio.html │ │ ├── studio.js │ │ ├── studio.scss │ │ └── studiostate.js ├── scripts │ ├── AnimationRenderer.js │ ├── AvdSerializer.js │ ├── ColorUtil.js │ ├── DragHelper.js │ ├── ElementResizeWatcher.js │ ├── MathUtil.js │ ├── ModelUtil.js │ ├── RenderUtil.js │ ├── SvgLoader.js │ ├── SvgPathData.js │ ├── UiUtil.js │ ├── VectorDrawableLoader.js │ ├── app.js │ ├── icons.js │ ├── materialtheme.js │ ├── model │ │ ├── Animation.js │ │ ├── AnimationBlock.js │ │ ├── Artwork.js │ │ ├── BaseLayer.js │ │ ├── LayerGroup.js │ │ ├── MaskLayer.js │ │ ├── PathLayer.js │ │ ├── index.js │ │ └── properties │ │ │ ├── ColorProperty.js │ │ │ ├── EnumProperty.js │ │ │ ├── FractionProperty.js │ │ │ ├── IdProperty.js │ │ │ ├── NumberProperty.js │ │ │ ├── PathDataProperty.js │ │ │ ├── Property.js │ │ │ ├── StringProperty.js │ │ │ └── index.js │ ├── routes.js │ └── xmlserializer.js └── styles │ ├── angular-material-overrides.scss │ ├── app.scss │ ├── globals.scss │ ├── material-colors.scss │ ├── material-icons.scss │ ├── material-shadows.scss │ ├── root.scss │ └── variables.scss ├── art └── screencap.gif ├── bower.json ├── examples ├── back_simple.iconanim ├── barchart_in.iconanim ├── menu_to_back.iconanim ├── search_to_back.iconanim ├── search_to_close.iconanim ├── simple_path_morph.iconanim └── visibility_strike.iconanim ├── gulpfile.js ├── package-lock.json ├── package.json └── test └── test-colorutil.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['es2015'] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": ".tmp/lib" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _sandbox 2 | .sass-cache 3 | .tmp 4 | node_modules 5 | !app/node_modules 6 | bower_components 7 | dist 8 | .DS_Store 9 | .publish 10 | *.sublime-workspace 11 | *~ 12 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | Comment: 3 | # 1. Copyright notices should be preserved in production CSS. 4 | # 2. Some files have their name in a top comment, so as to distinguish the 5 | # rules in live CSS, so allow "blah.scss */" comments to be preserved. 6 | # 3. Preserve CSSJanus "@noflip" annotations. 7 | allowed: '(Copyright)|(\.scss[ */]*$)|(@noflip)' 8 | DeclarationOrder: 9 | enabled: false 10 | DisableLinterReason: 11 | enabled: true 12 | IdSelector: 13 | enabled: false 14 | ImportantRule: 15 | enabled: false 16 | MergeableSelector: 17 | # Only lint on mergeable selectors that are exactly the same. 18 | force_nesting: false 19 | NameFormat: 20 | enabled: false 21 | NestingDepth: 22 | # The default is 3. We're allowing 6 to keep some noise down. 23 | max_depth: 6 24 | PropertySortOrder: 25 | enabled: false 26 | QualifyingElement: 27 | enabled: false 28 | SelectorDepth: 29 | enabled: false 30 | # The default is 3. We're allowing 6 to keep some noise down. 31 | max_depth: 6 32 | SelectorFormat: 33 | # We already have different conventions, like under_scores or camelCase, 34 | # and converting your app from these to hyphened-words could involve 35 | # refactoring your HTML and your JavaScript as well; these are lints 36 | # that cannot be fixed _within_ the file. 37 | enabled: false 38 | Shorthand: 39 | # 3 sides are less readable than 4. 40 | allowed_shorthands: [1,2,4] 41 | SpaceAfterPropertyColon: 42 | enabled: false 43 | # Since we have line length limits in place, sometimes (like with image 44 | # URLs) we need to newline after a property colon. 45 | style: one_space_or_newline 46 | TrailingSemicolon: 47 | enabled: false 48 | TransitionAll: 49 | enabled: true 50 | UnnecessaryMantissa: 51 | enabled: false 52 | UrlFormat: 53 | enabled: false 54 | VendorPrefix: 55 | enabled: false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/languages/javascript-with-nodejs/ 2 | language: node_js 3 | node_js: 4 | - "node" 5 | before_script: 6 | - npm install -g gulp 7 | script: gulp -------------------------------------------------------------------------------- /AndroidIconAnimator.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "folder_exclude_patterns": ["node_modules/", ".tmp/", "bower_components/", "dist/"], 7 | "file_exclude_patterns": ["*~"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | **Warning: This app and repository is unmaintained. Check out the very awesome [Shape Shifter](https://github.com/alexjlockwood/ShapeShifter), the successor to this tool.** 4 | ----- 5 | 6 | # Android Icon Animator 7 | 8 | A web-based tool that lets you design icon animations and other animated vector art for Android. 9 | Exports to [Animated Vector Drawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html) 10 | format for Android. 11 | 12 | Not intended to replace After Effects or other professional animation tools, but very useful for 13 | simple animations. 14 | 15 | ![Screen capture of tool](art/screencap.gif) 16 | 17 | ## Build instructions 18 | 19 | If you want to contribute, you can build and serve the web app locally as follows: 20 | 21 | 1. First install [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/). 22 | 23 | 2. Install `bower` and `gulp`: 24 | 25 | ``` 26 | $ npm install -g bower gulp 27 | ``` 28 | 29 | 3. Clone the repository and in the root directory, run: 30 | 31 | ``` 32 | $ npm install 33 | ``` 34 | 35 | 4. To build and serve the web app locally, run: 36 | 37 | ``` 38 | $ gulp serve 39 | ``` 40 | -------------------------------------------------------------------------------- /app/components/canvas/canvas-ruler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const EXTRA_PADDING = 12; 18 | const GRID_INTERVALS_PX = [1, 2, 4, 8, 16, 24, 48, 100, 100, 250]; 19 | const LABEL_OFFSET = 12; 20 | const TICK_SIZE = 6; 21 | 22 | 23 | angular.module('AVDStudio').directive('canvasRuler', function() { 24 | return { 25 | restrict: 'E', 26 | scope: {}, 27 | template: '', 28 | replace: true, 29 | require: '^studioCanvas', 30 | link: function(scope, element, attrs, studioCanvasCtrl) { 31 | let $canvas = element; 32 | let canvas = $canvas.get(0); 33 | let isHorizontal = (attrs.orientation == 'horizontal'); 34 | let artworkWidth, artworkHeight; 35 | let mouseX, mouseY; 36 | 37 | $canvas 38 | .addClass('canvas-ruler') 39 | .addClass('orientation-' + attrs.orientation); 40 | 41 | // most scope methods called by canvas 42 | 43 | scope.hideMouse = () => { 44 | mouseX = -1; 45 | mouseY = -1; 46 | scope.redraw(); 47 | }; 48 | 49 | scope.showMousePosition = (x, y) => { 50 | mouseX = x; 51 | mouseY = y; 52 | scope.redraw(); 53 | }; 54 | 55 | scope.setArtworkSize = size => { 56 | artworkWidth = size.width; 57 | artworkHeight = size.height; 58 | scope.redraw(); 59 | }; 60 | 61 | scope.redraw = () => { 62 | let width = $canvas.width(); 63 | let height = $canvas.height(); 64 | $canvas.attr('width', width * window.devicePixelRatio); 65 | $canvas.attr('height', height * window.devicePixelRatio); 66 | 67 | let ctx = canvas.getContext('2d'); 68 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 69 | ctx.translate( 70 | isHorizontal ? EXTRA_PADDING : 0, 71 | isHorizontal ? 0 : EXTRA_PADDING); 72 | 73 | let zoom = Math.max(1, isHorizontal 74 | ? (width - EXTRA_PADDING * 2) / artworkWidth 75 | : (height - EXTRA_PADDING * 2) / artworkHeight); 76 | 77 | // compute grid spacing (40 = minimum grid spacing in pixels) 78 | let interval = 0; 79 | let spacingArtPx = GRID_INTERVALS_PX[interval]; 80 | while ((spacingArtPx * zoom) < 40 || interval >= GRID_INTERVALS_PX.length) { 81 | ++interval; 82 | spacingArtPx = GRID_INTERVALS_PX[interval]; 83 | } 84 | 85 | let spacingRulerPx = spacingArtPx * zoom; 86 | 87 | // text labels 88 | ctx.fillStyle = 'rgba(255,255,255,.3)'; 89 | ctx.font = '10px Roboto'; 90 | if (isHorizontal) { 91 | ctx.textBaseline = 'alphabetic'; 92 | ctx.textAlign = 'center'; 93 | for (let x = 0, t = 0; 94 | x <= (width - EXTRA_PADDING * 2); 95 | x += spacingRulerPx, t += spacingArtPx) { 96 | ctx.fillText(t, x, height - LABEL_OFFSET); 97 | ctx.fillRect(x - 0.5, height - TICK_SIZE, 1, TICK_SIZE); 98 | } 99 | } else { 100 | ctx.textBaseline = 'middle'; 101 | ctx.textAlign = 'right'; 102 | for (let y = 0, t = 0; 103 | y <= (height - EXTRA_PADDING * 2); 104 | y += spacingRulerPx, t += spacingArtPx) { 105 | ctx.fillText(t, width - LABEL_OFFSET, y); 106 | ctx.fillRect(width - TICK_SIZE, y - 0.5, TICK_SIZE, 1); 107 | } 108 | } 109 | 110 | ctx.fillStyle = 'rgba(255,255,255,.7)'; 111 | if (isHorizontal && mouseX >= 0) { 112 | ctx.fillText(mouseX, mouseX * zoom, height - LABEL_OFFSET); 113 | } else if (!isHorizontal && mouseY >= 0) { 114 | ctx.fillText(mouseY, width - LABEL_OFFSET, mouseY * zoom); 115 | } 116 | } 117 | 118 | studioCanvasCtrl.registerRuler(scope); 119 | scope.$on('$destroy', () => studioCanvasCtrl.unregisterRuler(scope)); 120 | } 121 | }; 122 | }); 123 | -------------------------------------------------------------------------------- /app/components/canvas/canvas.html: -------------------------------------------------------------------------------- 1 |
6 |
7 | 8 | 9 | 10 | 11 |
12 |
-------------------------------------------------------------------------------- /app/components/canvas/canvas.scss: -------------------------------------------------------------------------------- 1 | .studio-canvas { 2 | &:not(.preview-mode) { 3 | background-color: material-color('blue-grey', '900'); 4 | 5 | .canvas-container { 6 | position: relative; 7 | } 8 | 9 | $rulerWidth: 32px; 10 | 11 | .canvas-ruler { 12 | position: absolute; 13 | } 14 | 15 | .canvas-ruler.orientation-horizontal { 16 | left: -12px; 17 | width: calc(100% + 24px); 18 | top: -$rulerWidth; 19 | height: $rulerWidth; 20 | } 21 | 22 | .canvas-ruler.orientation-vertical { 23 | top: -12px; 24 | height: calc(100% + 24px); 25 | left: -$rulerWidth; 26 | width: $rulerWidth; 27 | } 28 | 29 | .rendering-canvas { 30 | background-color: #fff; 31 | box-shadow: material-shadow(2); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/filehandlers/filedroptarget.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const State = { 18 | NONE: 0, 19 | DRAGGING: 1, 20 | LOADING: 2, 21 | }; 22 | 23 | class FileDropTargetController { 24 | constructor($scope, $element, $attrs) { 25 | this.scope_ = $scope; 26 | this.element_ = $element; 27 | this.element_.addClass('file-drop-target'); 28 | 29 | this.onDropFile_ = $attrs.fileDropTarget 30 | ? (fileInfo => $scope.$eval($attrs.fileDropTarget, {fileInfo})) 31 | : (() => {}); 32 | 33 | this.state_ = State.NONE; 34 | 35 | // set up drag event listeners, with debouncing because dragging over/out of each child 36 | // triggers these events on the element 37 | 38 | let notDraggingTimeout_; 39 | 40 | let setDragging_ = dragging => { 41 | if (dragging) { 42 | // when moving from child to child, dragenter is sent before dragleave 43 | // on previous child 44 | window.setTimeout(() => { 45 | if (notDraggingTimeout_) { 46 | window.clearTimeout(notDraggingTimeout_); 47 | notDraggingTimeout_ = null; 48 | } 49 | this.setState_(State.DRAGGING); 50 | }, 0); 51 | } else { 52 | if (notDraggingTimeout_) { 53 | window.clearTimeout(notDraggingTimeout_); 54 | } 55 | notDraggingTimeout_ = window.setTimeout(() => this.setState_(State.NONE), 100); 56 | } 57 | }; 58 | 59 | this.element_ 60 | .on('dragenter', event => { 61 | event.preventDefault(); 62 | setDragging_(true); 63 | return false; 64 | }) 65 | .on('dragover', event => { 66 | event.preventDefault(); 67 | event.originalEvent.dataTransfer.dropEffect = 'copy'; 68 | return false; 69 | }) 70 | .on('dragleave', event => { 71 | event.preventDefault(); 72 | setDragging_(false); 73 | return false; 74 | }) 75 | .on('drop', event => { 76 | event.preventDefault(); 77 | this.setState_(State.NONE); 78 | this.handleDropFiles_(event.originalEvent.dataTransfer.files); 79 | return false; 80 | }); 81 | } 82 | 83 | setState_(state) { 84 | this.state_ = state; 85 | this.element_.toggleClass('is-dragging-over', this.state_ === State.DRAGGING); 86 | this.element_.toggleClass('is-loading', this.state_ === State.LOADING); 87 | } 88 | 89 | handleDropFiles_(fileList) { 90 | fileList = Array.from(fileList || []); 91 | fileList = fileList.filter(file => 92 | (file.type === 'image/svg+xml' 93 | || file.type === 'application/json' 94 | || file.type === 'application/xml' 95 | || file.type === 'text/xml' 96 | || file.name.match(/\.iconanim$/))); 97 | if (!fileList.length) { 98 | return; 99 | } 100 | 101 | let file = fileList[0]; 102 | 103 | let fileReader = new FileReader(); 104 | 105 | fileReader.onload = event => { 106 | this.setState_(State.NONE); 107 | this.scope_.$apply(() => this.onDropFile_({ 108 | textContent: event.target.result, 109 | name: file.name, 110 | type: file.type 111 | })); 112 | }; 113 | 114 | fileReader.onerror = event => { 115 | this.setState_(State.NONE); 116 | switch (event.target.error.code) { 117 | case event.target.error.NOT_FOUND_ERR: 118 | alert('File not found!'); 119 | break; 120 | case event.target.error.NOT_READABLE_ERR: 121 | alert('File is not readable'); 122 | break; 123 | case event.target.error.ABORT_ERR: 124 | break; // noop 125 | default: 126 | alert('An error occurred reading this file.'); 127 | } 128 | }; 129 | 130 | fileReader.onabort = function(e) { 131 | this.setState_(State.NONE); 132 | alert('File read cancelled'); 133 | }; 134 | 135 | this.setState_(State.LOADING); 136 | fileReader.readAsText(file); 137 | } 138 | } 139 | 140 | 141 | angular.module('AVDStudio').directive('fileDropTarget', () => { 142 | return { 143 | restrict: 'A', 144 | controller: FileDropTargetController 145 | }; 146 | }); 147 | -------------------------------------------------------------------------------- /app/components/filehandlers/filehandlers.scss: -------------------------------------------------------------------------------- 1 | .file-drop-target { 2 | &.is-dragging-over::after { 3 | content: ''; 4 | position: absolute; 5 | left: 0; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | z-index: 9999; 10 | animation: pulsate-color .66s ease 0s infinite; 11 | background-color: rgba($colorPrimary600, .5); 12 | box-shadow: 0 0 0 8px $colorPrimary600 inset; 13 | pointer-events: none; 14 | transform: translateZ(0); 15 | } 16 | 17 | @keyframes pulsate-color { 18 | 0% { opacity: .5; } 19 | 50% { opacity: 1; } 20 | 100% { opacity: .5; } 21 | } 22 | } 23 | 24 | .file-open-proxy { 25 | position: relative; 26 | 27 | input[type="file"] { 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | right: 0; 32 | bottom: 0; 33 | opacity: 0; 34 | cursor: pointer; 35 | } 36 | 37 | ::-webkit-file-upload-button { 38 | cursor:pointer; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/filehandlers/fileopenhandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | class FileOpenHandlerController { 18 | constructor($scope, $element, $attrs) { 19 | this.scope_ = $scope; 20 | this.element_ = $element; 21 | this.element_.addClass('file-open-proxy'); 22 | 23 | this.onOpenFile_ = $attrs.fileOpenHandler 24 | ? (fileInfo => $scope.$eval($attrs.fileOpenHandler, {fileInfo})) 25 | : (() => {}); 26 | } 27 | 28 | onLink() { 29 | this.inputElement_ = this.element_.find('input'); 30 | this.inputElement_.on('change', () => { 31 | let files = this.inputElement_.get(0).files; 32 | if (files.length) { 33 | this.handleDropFiles_(files); 34 | } 35 | }); 36 | } 37 | 38 | handleDropFiles_(fileList) { 39 | fileList = Array.from(fileList || []); 40 | fileList = fileList.filter(file => 41 | (file.type === 'image/svg+xml' 42 | || file.type === 'application/json' 43 | || file.type === 'application/xml' 44 | || file.type === 'text/xml' 45 | || file.name.match(/\.iconanim$/))); 46 | if (!fileList.length) { 47 | return; 48 | } 49 | 50 | let file = fileList[0]; 51 | 52 | let fileReader = new FileReader(); 53 | 54 | fileReader.onload = event => { 55 | this.scope_.$apply(() => this.onOpenFile_({ 56 | textContent: event.target.result, 57 | name: file.name, 58 | type: file.type 59 | })); 60 | }; 61 | 62 | fileReader.onerror = event => { 63 | switch (event.target.error.code) { 64 | case event.target.error.NOT_FOUND_ERR: 65 | alert('File not found!'); 66 | break; 67 | case event.target.error.NOT_READABLE_ERR: 68 | alert('File is not readable'); 69 | break; 70 | case event.target.error.ABORT_ERR: 71 | break; // noop 72 | default: 73 | alert('An error occurred reading this file.'); 74 | } 75 | }; 76 | 77 | fileReader.onabort = function(e) { 78 | alert('File read cancelled'); 79 | }; 80 | 81 | fileReader.readAsText(file); 82 | } 83 | } 84 | 85 | 86 | angular.module('AVDStudio').directive('fileOpenHandler', () => { 87 | return { 88 | restrict: 'A', 89 | controller: FileOpenHandlerController, 90 | require: '^fileOpenHandler', 91 | link: ($element, $scope, $attrs, ctrl) => { 92 | ctrl.onLink($element); 93 | } 94 | }; 95 | }); 96 | -------------------------------------------------------------------------------- /app/components/layertimeline/consts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const TimelineConsts = { 18 | TIMELINE_ANIMATION_PADDING: 20, // 20px 19 | }; 20 | -------------------------------------------------------------------------------- /app/components/layertimeline/layertimeline.scss: -------------------------------------------------------------------------------- 1 | .studio-layer-timeline { 2 | background-color: #fff; 3 | height: 300px; 4 | display: flex; 5 | flex-direction: row; 6 | flex-shrink: 0; 7 | overflow: hidden; 8 | box-shadow: material-shadow(8); 9 | z-index: 1; 10 | position: relative; 11 | 12 | $headerHeight: 40px; 13 | $timelineAnimationPadding: 20px; 14 | 15 | // basic resets 16 | 17 | ul { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | li { 23 | list-style: none; 24 | } 25 | 26 | // general vertical metrics 27 | 28 | .slt-layers-list, 29 | .slt-timeline-animation-rows { 30 | padding: 8px 0; 31 | } 32 | 33 | .slt-layer { 34 | box-sizing: border-box; 35 | line-height: 20px; 36 | height: 20px; 37 | } 38 | 39 | .slt-property { 40 | line-height: 24px; 41 | height: 24px; 42 | } 43 | 44 | .slt-properties { 45 | margin: 4px 0; 46 | 47 | &:empty { 48 | display: none; 49 | } 50 | } 51 | 52 | // layers list 53 | 54 | .slt-layers { 55 | width: 300px; 56 | box-shadow: material-shadow(2); 57 | position: relative; 58 | z-index: 3; 59 | user-select: none; 60 | flex: 0 0 auto; 61 | 62 | md-icon { 63 | width: 16px; 64 | height: 16px; 65 | font-size: 16px; 66 | } 67 | 68 | @mixin layer-list-button { 69 | margin: 0; 70 | padding: 2px; 71 | height: 20px; 72 | width: 20px; 73 | line-height: 16px; 74 | min-height: 20px; 75 | 76 | md-icon { 77 | vertical-align: top; 78 | } 79 | } 80 | 81 | .slt-layers-list-scroller { 82 | position: relative; 83 | overflow-x: hidden; 84 | overflow-y: auto; 85 | -ms-overflow-style: none; 86 | &::-webkit-scrollbar { display: none; } 87 | } 88 | 89 | .slt-layers-list { 90 | padding-left: 8px; 91 | } 92 | 93 | .slt-children { 94 | padding: 0; 95 | margin: 0 0 0 20px; 96 | } 97 | 98 | .slt-layer { 99 | display: flex; 100 | flex-direction: row; 101 | align-items: center; 102 | padding: 2px; 103 | 104 | font-size: 12px; 105 | cursor: pointer; 106 | color: $colorBlackTextSecondary; 107 | outline: none; 108 | border-radius: 2px 0 0 2px; 109 | 110 | &:focus { 111 | box-shadow: 0 0 0 1px $colorFocusBorder inset; 112 | } 113 | 114 | &.is-selected { 115 | background-color: $colorSelection; 116 | box-shadow: none; 117 | 118 | &, 119 | & md-icon { 120 | color: #fff; 121 | } 122 | } 123 | 124 | md-icon { 125 | flex: 0 0 auto; 126 | margin-right: 4px; 127 | } 128 | } 129 | 130 | .slt-layer:hover, 131 | .slt-layer.is-selected { 132 | .slt-layer-action-button { 133 | opacity: 1; 134 | } 135 | } 136 | 137 | .slt-layer-expanded-toggle { 138 | @include layer-list-button; 139 | 140 | visibility: hidden; 141 | 142 | &.is-visible { 143 | visibility: visible; 144 | } 145 | } 146 | 147 | .slt-layer-id { 148 | flex: 1 1 0; 149 | min-width: 0; 150 | overflow: hidden; 151 | text-overflow: ellipsis; 152 | } 153 | 154 | .slt-layer-add-property-menu { 155 | padding: 0; 156 | display: flex; 157 | } 158 | 159 | .slt-layer-action-button { 160 | @include layer-list-button; 161 | opacity: .2; 162 | } 163 | 164 | md-menu { 165 | display: flex; 166 | } 167 | 168 | .slt-layer-more-actions { 169 | opacity: 0; 170 | } 171 | 172 | .slt-layer-visibility-toggle { 173 | &.is-checked { 174 | opacity: 0; 175 | } 176 | &:not(.is-checked) { 177 | opacity: .7; 178 | } 179 | } 180 | 181 | .slt-layer-type-group { 182 | font-weight: 500; 183 | } 184 | 185 | .slt-properties { 186 | margin-left: 40px; // indent by both the icon (20px) and expand toggle (20px) 187 | margin-right: -2px; // hide inset shadow 188 | padding-right: 2px; // offset margin 189 | box-shadow: 0 0 0 1px $thinBorderColor inset; 190 | border-radius: 2px; 191 | } 192 | 193 | .slt-property { 194 | color: $colorBlackTextSecondary; 195 | padding-left: 8px; 196 | font-size: 12px; 197 | 198 | &:not(:last-child) { 199 | box-shadow: 0 -1px 0 $thinBorderColor inset; 200 | } 201 | 202 | .slt-property-name { 203 | flex: 1 1 0; 204 | min-width: 0; 205 | overflow: hidden; 206 | text-overflow: ellipsis; 207 | } 208 | 209 | .md-button { 210 | @include layer-list-button; 211 | margin: 2px; 212 | } 213 | 214 | md-icon { 215 | color: $colorBlackTextDisabled; 216 | } 217 | } 218 | 219 | .slt-header { 220 | padding: 0 0 0 4px; 221 | overflow: hidden; 222 | 223 | .slt-layers-menu-group-button { 224 | position: relative; 225 | border: 0; 226 | height: $headerHeight; 227 | font-size: 12px; 228 | font-weight: 500; 229 | color: $colorBlackTextSecondary; 230 | line-height: $headerHeight; 231 | padding: 0 8px 0 8px; 232 | margin: 0; 233 | outline: 0; 234 | background-color: transparent; 235 | 236 | &:focus { 237 | background-color: rgba(#000, .08); 238 | } 239 | } 240 | 241 | md-menu { 242 | padding: 0; 243 | 244 | .md-icon-button { 245 | margin: 0; 246 | } 247 | 248 | md-icon { 249 | vertical-align: top; 250 | } 251 | } 252 | } 253 | 254 | .slt-layers-list-drag-indicator { 255 | position: absolute; 256 | height: 2px; 257 | left: 0; 258 | right: 0; 259 | margin-top: -1px; 260 | background-color: $colorPrimary400; 261 | pointer-events: none; 262 | 263 | &::before { 264 | position: absolute; 265 | content: ''; 266 | left: -4px; 267 | top: -3px; 268 | height: 8px; 269 | width: 8px; 270 | background-color: $colorPrimary400; 271 | border-radius: 50%; 272 | } 273 | } 274 | } 275 | 276 | // empty/placeholder pattern 277 | 278 | .slt-layers-empty { 279 | padding: 32px; 280 | text-align: center; 281 | 282 | font-size: 14px; 283 | line-height: 20px; 284 | color: $colorBlackTextDisabled; 285 | } 286 | 287 | // timeline 288 | 289 | .slt-timeline { 290 | background-color: material-color('grey', '300'); 291 | overflow-x: auto; 292 | overflow-y: hidden; 293 | flex: 1; 294 | user-select: none; 295 | cursor: default; 296 | 297 | display: flex; 298 | flex-direction: row; 299 | align-items: stretch; 300 | 301 | .slt-timeline-animation-scroller { 302 | position: relative; 303 | overflow-x: hidden; 304 | overflow-y: auto; 305 | -ms-overflow-style: none; 306 | &::-webkit-scrollbar { display: none; } 307 | } 308 | 309 | .slt-timeline-animation { 310 | position: relative; 311 | background-color: material-color('grey', '100'); 312 | margin-left: 4px; 313 | box-sizing: content-box; 314 | width: 100px; 315 | overflow: hidden; 316 | flex: 0 0 auto; 317 | box-shadow: 318 | 4px 0 0 material-color('grey', '500'), 319 | -4px 0 0 material-color('grey', '500'); 320 | 321 | opacity: .5; 322 | 323 | &.is-active { 324 | opacity: 1; 325 | } 326 | } 327 | 328 | .slt-timeline-animation-rows { 329 | padding-left: $timelineAnimationPadding; 330 | padding-right: $timelineAnimationPadding; 331 | } 332 | 333 | .slt-timeline-animation-new-container { 334 | flex: 0 0 auto; 335 | width: 128px; 336 | margin: 24px; 337 | } 338 | 339 | .slt-timeline-animation-new { 340 | color: $colorBlackTextSecondary; 341 | cursor: pointer; 342 | padding: 8px 12px; 343 | outline: 0; 344 | 345 | md-icon { 346 | width: 24px; 347 | height: 24px; 348 | box-sizing: content-box; 349 | margin-bottom: 4px; 350 | } 351 | 352 | span { 353 | font-size: 12px; 354 | text-transform: none; 355 | letter-spacing: 0; 356 | line-height: 16px; 357 | font-weight: 500; 358 | display: flex; 359 | flex-direction: column; 360 | align-items: center; 361 | } 362 | } 363 | 364 | .slt-properties { 365 | background-color: #fff; 366 | border-radius: 2px; 367 | box-shadow: 0 0 0 1px $thinBorderColor inset; 368 | } 369 | 370 | .slt-property { 371 | position: relative; 372 | 373 | &:not(:last-child) { 374 | box-shadow: 0 -1px 0 $thinBorderColor inset; 375 | } 376 | } 377 | 378 | .slt-timeline-block { 379 | position: absolute; 380 | background-color: material-color('grey', '500'); 381 | height: 12px; 382 | border-radius: 6px; 383 | top: 50%; 384 | transform: translate(0, -50%); 385 | outline: 0; 386 | cursor: pointer; 387 | 388 | .slt-timeline-block-edge { 389 | position: absolute; 390 | top: 0; 391 | bottom: 0; 392 | width: 6px; 393 | cursor: ew-resize; 394 | } 395 | 396 | .slt-timeline-block-edge-start { 397 | left: 0; 398 | } 399 | 400 | .slt-timeline-block-edge-end { 401 | right: 0; 402 | } 403 | } 404 | 405 | .slt-timeline-animation.is-active .slt-timeline-block { 406 | background-color: material-color('green', '200'); 407 | 408 | &.is-selected { 409 | background-color: $colorSelectedAnimationBlock; 410 | } 411 | } 412 | 413 | .slt-timeline-grid { 414 | position: absolute; 415 | left: 0; 416 | top: 0; 417 | width: 100%; 418 | height: 100%; 419 | pointer-events: none; 420 | z-index: 1; 421 | } 422 | 423 | .slt-header { 424 | display: flex; 425 | flex-direction: column; 426 | margin: 0; 427 | 428 | .slt-timeline-animation-meta { 429 | height: $headerHeight / 2 - 4px; 430 | line-height: $headerHeight / 2 - 4px; 431 | margin: 2px -4px; 432 | padding: 0 4px; 433 | border-radius: 2px; 434 | cursor: pointer; 435 | outline: 0; 436 | align-self: flex-start; 437 | display: flex; 438 | flex-direction: row; 439 | 440 | &.is-selected, 441 | &.is-selected .slt-timeline-animation-name { 442 | background-color: $colorSelection; 443 | color: $colorWhiteTextPrimary; 444 | } 445 | } 446 | 447 | .slt-timeline-animation-name { 448 | color: $colorBlackTextPrimary; 449 | margin-right: 4px; 450 | font-weight: 500; 451 | } 452 | 453 | .slt-timeline-header-grid { 454 | position: absolute; 455 | left: 0; 456 | top: 50%; 457 | width: 100%; 458 | height: 50%; 459 | z-index: 1; 460 | cursor: pointer; 461 | } 462 | } 463 | } 464 | 465 | // headers 466 | 467 | .slt-header { 468 | position: relative; 469 | flex: 0 0 auto; 470 | height: $headerHeight; 471 | box-sizing: border-box; 472 | width: 100%; 473 | background-color: material-color('grey', '100'); 474 | font-size: 12px; 475 | line-height: $headerHeight; 476 | color: material-color('grey', '700'); 477 | padding: 0 16px; 478 | box-shadow: material-shadow(2); 479 | z-index: 2; 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /app/components/layertimeline/timelinegrid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {MathUtil} from 'MathUtil'; 18 | import {DragHelper} from 'DragHelper'; 19 | 20 | import {TimelineConsts} from './consts'; 21 | 22 | 23 | const GRID_INTERVALS_MS = [ 24 | 10, 25, 50, 100, 250, 500, 25 | 1000, 2500, 5000, 10000, 30000, 60000 26 | ]; 27 | 28 | 29 | angular.module('AVDStudio').directive('studioTimelineGrid', function() { 30 | return { 31 | restrict: 'E', 32 | scope: { 33 | isActive: '=', 34 | activeTime: '=', 35 | animation: '=', 36 | onScrub: '&' 37 | }, 38 | template: '', 39 | replace: true, 40 | require: '^studioLayerTimeline', 41 | link: function(scope, element, attrs, layerTimelineCtrl) { 42 | let $canvas = element; 43 | let canvas = $canvas.get(0); 44 | 45 | let isHeader = 'isHeader' in attrs; 46 | 47 | scope.$watch(() => scope.redraw_()); 48 | 49 | if ('onScrub' in attrs) { 50 | let handleScrubEvent_ = (event) => { 51 | let x = event.clientX; 52 | x -= $canvas.offset().left; 53 | let time = (x - TimelineConsts.TIMELINE_ANIMATION_PADDING) 54 | / ($canvas.width() - TimelineConsts.TIMELINE_ANIMATION_PADDING * 2) 55 | * scope.animation.duration; 56 | time = MathUtil.constrain(time, 0, scope.animation.duration); 57 | scope.onScrub({ 58 | animation: scope.animation, 59 | time, 60 | options: {disableSnap: !!event.altKey} 61 | }); 62 | }; 63 | 64 | $canvas.on('mousedown', event => scope.$apply(() => { 65 | handleScrubEvent_(event); 66 | new DragHelper({ 67 | downEvent: event, 68 | direction: 'horizontal', 69 | skipSlopCheck: true, 70 | onDrag: event => scope.$apply(() => handleScrubEvent_(event)), 71 | }); 72 | event.preventDefault(); 73 | return false; 74 | })); 75 | } 76 | 77 | scope.redraw_ = () => { 78 | if (!$canvas.is(':visible')) { 79 | return; 80 | } 81 | 82 | let width = $canvas.width(); 83 | let height = $canvas.height(); 84 | let horizZoom = layerTimelineCtrl.horizZoom; 85 | $canvas.attr('width', width * window.devicePixelRatio); 86 | $canvas.attr('height', isHeader ? height * window.devicePixelRatio : 1); 87 | 88 | let ctx = canvas.getContext('2d'); 89 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 90 | ctx.translate(TimelineConsts.TIMELINE_ANIMATION_PADDING, 0); 91 | 92 | // compute grid spacing (40 = minimum grid spacing in pixels) 93 | let interval = 0; 94 | let spacingMs = GRID_INTERVALS_MS[interval]; 95 | while ((spacingMs * horizZoom) < 40 || interval >= GRID_INTERVALS_MS.length) { 96 | ++interval; 97 | spacingMs = GRID_INTERVALS_MS[interval]; 98 | } 99 | 100 | let spacingPx = spacingMs * horizZoom; 101 | 102 | if (isHeader) { 103 | // text labels 104 | ctx.fillStyle = 'rgba(0,0,0,0.4)'; 105 | ctx.textAlign = 'center'; 106 | ctx.textBaseline = 'middle'; 107 | ctx.font = '10px Roboto'; 108 | for (let x = 0, t = 0; x <= width; x += spacingPx, t += spacingMs) { 109 | //ctx.fillRect(x - 0.5, 0, 1, height); 110 | ctx.fillText(`${t / 1000}s`, x, height / 2); 111 | } 112 | 113 | if (scope.isActive) { 114 | ctx.fillStyle = 'rgba(244, 67, 54, .7)'; 115 | ctx.beginPath(); 116 | ctx.arc(scope.activeTime * horizZoom, height / 2, 4, 0, 2 * Math.PI, false); 117 | ctx.fill(); 118 | ctx.closePath(); 119 | ctx.fillRect(scope.activeTime * horizZoom - 1, height / 2 + 4, 2, height); 120 | } 121 | 122 | } else { 123 | // grid lines 124 | ctx.fillStyle = 'rgba(0,0,0,0.1)'; 125 | for (let x = spacingPx; 126 | x < width - TimelineConsts.TIMELINE_ANIMATION_PADDING * 2; 127 | x += spacingPx) { 128 | ctx.fillRect(x - 0.5, 0, 1, 1); 129 | } 130 | 131 | if (scope.isActive) { 132 | ctx.fillStyle = 'rgba(244, 67, 54, .7)'; 133 | ctx.fillRect(scope.activeTime * horizZoom - 1, 0, 2, 1); 134 | } 135 | } 136 | } 137 | 138 | scope.redraw_(); 139 | } 140 | }; 141 | }); 142 | -------------------------------------------------------------------------------- /app/components/propertyinspector/propertyinspector.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | Select something below to edit its properties 5 |
6 |
10 |
11 | 14 |
15 | {{ctrl.selectionDescription}} 16 | {{ctrl.selectionInfo.subDescription}} 17 |
18 |
19 |
20 |
24 | No shared properties to view or edit 25 |
26 |
27 |
{{ip.propertyName}}
28 |
29 |
32 |
33 | 34 | {{ip.displayValue}} 35 | 36 |
37 | 47 | 51 | 54 | 55 | 56 | 57 | {{option.label}} 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 |
67 |
68 |
-------------------------------------------------------------------------------- /app/components/propertyinspector/propertyinspector.scss: -------------------------------------------------------------------------------- 1 | .studio-property-inspector { 2 | background-color: material-color('grey', '200'); 3 | width: 320px; 4 | box-shadow: material-shadow(4); 5 | user-select: none; 6 | cursor: default; 7 | position: relative; 8 | 9 | .spi-header { 10 | background-color: #fff; 11 | box-shadow: material-shadow(2); 12 | z-index: 1; 13 | display: flex; 14 | flex-direction: row; 15 | padding: 16px; 16 | 17 | .spi-selection-icon { 18 | color: $colorBlackTextSecondary; 19 | margin-right: 12px; 20 | width: 32px; 21 | height: 32px; 22 | } 23 | 24 | .spi-selection-description-container { 25 | padding: (32px - 24px) / 2 0; 26 | } 27 | 28 | .spi-selection-description { 29 | color: $colorBlackTextPrimary; 30 | font-weight: 500; 31 | font-size: 20px; 32 | line-height: 24px; 33 | } 34 | 35 | .spi-selection-sub-description { 36 | color: $colorBlackTextSecondary; 37 | font-size: 14px; 38 | line-height: 20px; 39 | 40 | &:empty { 41 | display: none; 42 | } 43 | } 44 | } 45 | 46 | .spi-body { 47 | overflow-y: auto; 48 | padding: 8px 0; 49 | 50 | .spi-property { 51 | display: flex; 52 | flex-direction: row; 53 | padding: 4px 16px; 54 | min-height: 24px; 55 | } 56 | 57 | .spi-property-name { 58 | font-size: 14px; 59 | line-height: 24px; 60 | flex: 1 1 0; 61 | color: $colorBlackTextSecondary; 62 | } 63 | 64 | .spi-property-value { 65 | flex: 2 1 0; 66 | min-width: 0; 67 | font-size: 14px; 68 | line-height: 24px; 69 | } 70 | 71 | .spi-property-value-static, 72 | .spi-property-value-editor input, 73 | .spi-property-value-menu-target { 74 | border: 0; 75 | border-radius: 2px; 76 | box-shadow: 0 0 0 1px $thinBorderColor inset; 77 | font-size: 14px; 78 | line-height: 20px; 79 | height: 24px; 80 | box-sizing: border-box; 81 | padding: 2px 6px; 82 | outline: 0; 83 | } 84 | 85 | .spi-property-value-static { 86 | overflow-x: scroll; 87 | white-space: nowrap; 88 | overflow-y: hidden; 89 | -ms-overflow-style: none; 90 | &::-webkit-scrollbar { display: none; } 91 | user-select: all; 92 | cursor: text; 93 | } 94 | 95 | .spi-property-value-editor input, 96 | .spi-property-value-menu-target { 97 | background-color: #fff; 98 | 99 | &:focus { 100 | box-shadow: 0 0 0 1px $colorFocusBorder inset; 101 | } 102 | } 103 | 104 | .spi-property-value-menu { 105 | margin: 0; 106 | padding: 0; 107 | } 108 | 109 | .spi-property-value-menu-target { 110 | border: 0; 111 | text-align: left; 112 | position: relative; 113 | padding-right: 24px; 114 | 115 | &::after { 116 | @include material-icons; 117 | content: 'arrow_drop_down'; 118 | position: absolute; 119 | font-size: 24px; 120 | right: 0; 121 | top: 50%; 122 | transform: translate(0, -50%); 123 | color: $colorBlackTextDisabled; 124 | } 125 | } 126 | 127 | .spi-property-value-menu-current-value { 128 | display: block; 129 | width: 100%; 130 | overflow: hidden; 131 | text-overflow: ellipsis; 132 | white-space: nowrap; 133 | } 134 | 135 | .spi-property-color-preview { 136 | width: 16px; 137 | height: 16px; 138 | margin: 4px 8px 4px 4px; 139 | border-radius: 50%; 140 | box-shadow: 0 0 0 1px $thinBorderColor inset; 141 | } 142 | } 143 | 144 | // empty/placeholder pattern 145 | 146 | .spi-empty { 147 | padding: 32px; 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | text-align: center; 152 | 153 | font-size: 14px; 154 | line-height: 20px; 155 | color: $colorBlackTextDisabled; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/components/scrollgroup/scrollgroup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | let groups = {}; 18 | 19 | class ScrollGroupController { 20 | constructor($scope, $element, $attrs) { 21 | let scrollGroup = $attrs.scrollGroup || ''; 22 | groups[scrollGroup] = groups[scrollGroup] || []; 23 | groups[scrollGroup].push($element); 24 | 25 | $element.on('scroll', () => { 26 | let scrollTop = $element.scrollTop(); 27 | groups[scrollGroup].forEach( 28 | el => (el !== $element) ? el.scrollTop(scrollTop) : null); 29 | }); 30 | 31 | $scope.$on('$destroy', () => { 32 | groups[scrollGroup].splice(groups[scrollGroup].indexOf($element), 1); 33 | }); 34 | } 35 | } 36 | 37 | 38 | angular.module('AVDStudio').directive('scrollGroup', () => { 39 | return { 40 | restrict: 'A', 41 | controller: ScrollGroupController 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /app/components/splitter/splitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {DragHelper} from 'DragHelper'; 18 | 19 | 20 | class SplitterController { 21 | constructor($scope, $element, $attrs) { 22 | this.edge_ = $attrs.edge; 23 | this.min_ = Number($attrs.min) || 100; 24 | this.persistKey_ = $attrs.persistId ? `\$\$splitter::${$attrs.persistId}` : null; 25 | this.orientation_ = (this.edge_ == 'left' || this.edge_ == 'right') 26 | ? 'vertical' 27 | : 'horizontal'; 28 | this.element_ = $element; 29 | this.parent_ = $element.parent(); 30 | this.dragging_ = false; 31 | 32 | if (this.orientation_ == 'vertical') { 33 | this.sizeGetter_ = () => this.parent_.width(); 34 | this.sizeSetter_ = size => this.parent_.width(size); 35 | this.clientXY_ = 'clientX'; 36 | 37 | } else { 38 | this.sizeGetter_ = () => this.parent_.height(); 39 | this.sizeSetter_ = size => this.parent_.height(size); 40 | this.clientXY_ = 'clientY'; 41 | } 42 | 43 | this.addClasses_(); 44 | this.setupEventListeners_(); 45 | this.deserializeState_(); 46 | } 47 | 48 | deserializeState_() { 49 | if (this.persistKey_ in localStorage) { 50 | this.setSize_(Number(localStorage[this.persistKey_])); 51 | } 52 | } 53 | 54 | addClasses_() { 55 | this.element_ 56 | .addClass(`splt-${this.orientation_}`) 57 | .addClass(`splt-edge-${this.edge_}`); 58 | } 59 | 60 | setupEventListeners_() { 61 | this.element_.on('mousedown', event => { 62 | this.downXY_ = event[this.clientXY_]; 63 | this.downSize_ = this.sizeGetter_(); 64 | event.preventDefault(); 65 | 66 | new DragHelper({ 67 | downEvent: event, 68 | direction: (this.orientation_ == 'vertical') ? 'horizontal' : 'vertical', 69 | draggingCursor: (this.orientation_ == 'vertical') ? 'col-resize' : 'row-resize', 70 | 71 | onBeginDrag: event => this.element_.addClass('is-dragging'), 72 | onDrop: event => this.element_.removeClass('is-dragging'), 73 | onDrag: (event, delta) => { 74 | let sign = (this.edge_ == 'left' || this.edge_ == 'top') ? -1 : 1; 75 | this.setSize_(Math.max(this.min_, 76 | this.downSize_ + sign * delta[(this.orientation_ == 'vertical') ? 'x' : 'y'])); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | setSize_(size) { 83 | if (this.persistKey_) { 84 | localStorage[this.persistKey_] = size; 85 | } 86 | this.sizeSetter_(size); 87 | } 88 | } 89 | 90 | 91 | angular.module('AVDStudio').directive('studioSplitter', () => { 92 | return { 93 | restrict: 'E', 94 | scope: {}, 95 | template: '
', 96 | replace: true, 97 | bindToController: true, 98 | controller: SplitterController, 99 | controllerAs: 'ctrl' 100 | }; 101 | }); 102 | -------------------------------------------------------------------------------- /app/components/splitter/splitter.scss: -------------------------------------------------------------------------------- 1 | .studio-splitter { 2 | position: absolute; 3 | z-index: 100; 4 | 5 | $splitterWidth: 4px; 6 | 7 | &:hover, 8 | &.is-dragging { 9 | background-color: rgba(#000, .1); 10 | } 11 | 12 | &.splt-vertical { 13 | top: 0; 14 | bottom: 0; 15 | width: $splitterWidth; 16 | cursor: col-resize; 17 | } 18 | 19 | &.splt-horizontal { 20 | left: 0; 21 | right: 0; 22 | height: $splitterWidth; 23 | cursor: row-resize; 24 | } 25 | 26 | &.splt-edge-left { 27 | left: 0; 28 | } 29 | 30 | &.splt-edge-right { 31 | right: 0; 32 | } 33 | 34 | &.splt-edge-top { 35 | top: 0; 36 | } 37 | 38 | &.splt-edge-bottom { 39 | bottom: 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romannurik/AndroidIconAnimator/336e461c7d771ebf459c07620e0f5fe6f4e0f7f2/app/favicon.ico -------------------------------------------------------------------------------- /app/icons/add_animation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | add_animation 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/icons/add_layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | add_layer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/icons/animation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | animation 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/icons/animation_block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | animation_block 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/icons/artwork.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | artwork 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/icons/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | collection 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/icons/layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | layer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/icons/layer_group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | layer_group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/icons/mask_layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mask_layer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/icons/path_layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | path_layer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/images/pauseplay-white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romannurik/AndroidIconAnimator/336e461c7d771ebf459c07620e0f5fe6f4e0f7f2/app/images/pauseplay-white@2x.png -------------------------------------------------------------------------------- /app/images/playpause-white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romannurik/AndroidIconAnimator/336e461c7d771ebf459c07620e0f5fe6f4e0f7f2/app/images/playpause-white@2x.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Icon Animator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/pages/studio/dialog-svg-drop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Do you want to add the SVG layers to this animation, or create a new animation? 4 | 5 | 6 | Cancel 7 | Start from scratch 8 | Add layers 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/pages/studio/studio.html: -------------------------------------------------------------------------------- 1 |
6 | 7 |
8 | warning 9 |
10 | This tool is no longer being updated 11 | 12 | Try the new 13 | Shape Shifter tool, 14 | which adds awesome path-morphing functionality and more! 15 | Learn more 16 | 17 |
18 | {{ studioCtrl.versionInfo.version }} 19 |
20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 | 30 | fast_rewind 31 | Rewind (R) 32 | 33 |
34 | 36 |
40 |
41 | 42 | {{studioCtrl.isPlaying() ? 'Pause' : 'Play'}} current animation (Spacebar) 43 | 44 |
45 |
46 | 47 | visibility 48 | Preview mode (P) 49 | 50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 |
67 | 71 | close 72 | 73 | 74 |
75 | 76 |
77 | 78 |
79 |
80 | -------------------------------------------------------------------------------- /app/pages/studio/studio.scss: -------------------------------------------------------------------------------- 1 | .page-studio { 2 | .preview-banner { 3 | padding: 12px 16px; 4 | font-size: 14px; 5 | line-height: 20px; 6 | background-color: #fff; 7 | color: rgba(0,0,0,.54); 8 | box-shadow: material-shadow(1); 9 | z-index: 1; 10 | display: flex; 11 | flex-direction: row; 12 | align-items: flex-start; 13 | 14 | .material-icons { 15 | font-size: 24px; 16 | margin-right: 8px; 17 | color: material-color('red', '700'); 18 | } 19 | 20 | a { 21 | color: material-color('blue', '600'); 22 | } 23 | 24 | .message-text { 25 | flex: 1; 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .message-header { 31 | font-weight: 500; 32 | color: material-color('red', '700'); 33 | } 34 | 35 | .version-info { 36 | background-color: material-color('red', '100'); 37 | color: material-color('red', '900'); 38 | font-weight: 500; 39 | padding: 4px 6px; 40 | border-radius: 2px; 41 | } 42 | } 43 | 44 | .canvas-area { 45 | position: relative; 46 | 47 | .canvas-ruler { 48 | transition: opacity $animTimeFast ease, visibility 0s linear $animTimeFast; 49 | opacity: 0; 50 | visibility: hidden; 51 | } 52 | 53 | &:hover .canvas-ruler { 54 | transition: opacity $animTimeFast ease; 55 | opacity: 1; 56 | visibility: visible; 57 | } 58 | 59 | } 60 | 61 | .floating-canvas-bar { 62 | position: absolute; 63 | left: 0; 64 | bottom: 0; 65 | right: 0; 66 | padding: 16px; 67 | 68 | .md-fab { 69 | margin: 0; 70 | } 71 | } 72 | 73 | .play-pause-icon { 74 | $numAnimationFrames: 21; 75 | 76 | position: absolute; 77 | overflow: hidden; 78 | left: 50%; 79 | top: 50%; 80 | width: 24px; 81 | height: 24px; 82 | transform: translate(-50%, -50%); 83 | 84 | &::after { 85 | content: ''; 86 | display: block; 87 | pointer-events: none; 88 | position: absolute; 89 | left: 0; 90 | top: 0; 91 | width: ($numAnimationFrames * 24px); 92 | height: 24px; 93 | background-size: ($numAnimationFrames * 24px) 24px; 94 | background-position: 0% 0%; 95 | background-image: url('../images/pauseplay-white@2x.png'); 96 | transform: translateX(($numAnimationFrames - 1) * -24px); 97 | animation-duration: (1s * $numAnimationFrames / 60); 98 | animation-timing-function: steps($numAnimationFrames - 1); 99 | } 100 | 101 | &.can-animate::after { 102 | animation-name: pauseplay; 103 | } 104 | 105 | &.can-animate.is-playing::after { 106 | background-image: url('../images/playpause-white@2x.png'); 107 | animation-name: playpause; 108 | } 109 | 110 | @keyframes playpause { 111 | from {transform: translateX(0);} 112 | to {transform: translateX(($numAnimationFrames - 1) * -24px);} 113 | } 114 | 115 | @keyframes pauseplay { 116 | from {transform: translateX(0);} 117 | to {transform: translateX(($numAnimationFrames - 1) * -24px);} 118 | } 119 | } 120 | 121 | .preview-container { 122 | position: relative; 123 | } 124 | 125 | .close-preview-button { 126 | margin: 0; 127 | position: absolute; 128 | left: 16px; 129 | top: 16px; 130 | } 131 | 132 | .loading { 133 | position: fixed; 134 | left: 0; 135 | top: 0; 136 | right: 0; 137 | bottom: 0; 138 | display: flex; 139 | align-items: center; 140 | justify-content: center; 141 | z-index: 100; 142 | margin: 0; 143 | background-color: rgba(#fff, .9); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/scripts/AnimationRenderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Artwork, Animation} from 'model'; 18 | import {ModelUtil} from 'ModelUtil'; 19 | 20 | 21 | const DEFAULT_LAYER_PROPERTY_STATE = { 22 | activeBlock: null, 23 | interpolatedValue: false 24 | }; 25 | 26 | 27 | export class AnimationRenderer { 28 | constructor(artwork, animation) { 29 | this.originalArtwork = artwork; 30 | this.animation = animation; 31 | this.renderedArtwork = new Artwork(artwork, {linkSelectedState: true}); 32 | this.animDataByLayer = ModelUtil.getOrderedAnimationBlocksByLayerIdAndProperty(animation); 33 | 34 | Object.keys(this.animDataByLayer).forEach(layerId => { 35 | this.animDataByLayer[layerId] = { 36 | originalLayer: this.originalArtwork.findLayerById(layerId), 37 | renderedLayer: this.renderedArtwork.findLayerById(layerId), 38 | orderedBlocks: this.animDataByLayer[layerId] 39 | }; 40 | }); 41 | 42 | this.setAnimationTime(0); 43 | } 44 | 45 | setAnimationTime(time) { 46 | for (let layerId in this.animDataByLayer) { 47 | let animData = this.animDataByLayer[layerId]; 48 | animData.renderedLayer._ar = animData.renderedLayer._ar || {}; 49 | 50 | for (let propertyName in animData.orderedBlocks) { 51 | let blocks = animData.orderedBlocks[propertyName]; 52 | let _ar = Object.assign({}, DEFAULT_LAYER_PROPERTY_STATE); 53 | 54 | // compute rendered value at given time 55 | let property = animData.originalLayer.animatableProperties[propertyName]; 56 | let value = animData.originalLayer[propertyName]; 57 | for (let i = 0; i < blocks.length; ++i) { 58 | let block = blocks[i]; 59 | if (time < block.startTime) { 60 | break; 61 | } else if (time < block.endTime) { 62 | let fromValue = ('fromValue' in block) ? block.fromValue : value; 63 | let f = (time - block.startTime) / (block.endTime - block.startTime); 64 | f = block.interpolator.interpolate(f); 65 | value = property.interpolateValue(fromValue, block.toValue, f); 66 | _ar.activeBlock = block; 67 | _ar.interpolatedValue = true; 68 | break; 69 | } 70 | 71 | value = block.toValue; 72 | _ar.activeBlock = block; 73 | } 74 | 75 | animData.renderedLayer[propertyName] = value; 76 | 77 | // cached data 78 | animData.renderedLayer._ar[propertyName] = animData.renderedLayer._ar[propertyName] || {}; 79 | animData.renderedLayer._ar[propertyName] = _ar; 80 | } 81 | } 82 | 83 | this.animTime = time; 84 | } 85 | 86 | getLayerPropertyValue(layerId, propertyName) { 87 | return this.renderedArtwork.findLayerById(layerId)[propertyName]; 88 | } 89 | 90 | getLayerPropertyState(layerId, propertyName) { 91 | let layerAnimData = this.animDataByLayer[layerId]; 92 | return layerAnimData 93 | ? layerAnimData.renderedLayer._ar[propertyName] || {} 94 | : Object.assign({}, DEFAULT_LAYER_PROPERTY_STATE); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/scripts/AvdSerializer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import xmlserializer from 'xmlserializer'; 18 | 19 | import {Artwork, PathLayer, LayerGroup, MaskLayer, DefaultValues} from './model'; 20 | 21 | const XMLNS_NS = 'http://www.w3.org/2000/xmlns/'; 22 | const ANDROID_NS = 'http://schemas.android.com/apk/res/android'; 23 | const AAPT_NS = 'http://schemas.android.com/aapt'; 24 | 25 | 26 | let conditionalAttr_ = (node, attr, value, skipValue) => { 27 | if (value !== undefined 28 | && value !== null 29 | && (skipValue === undefined || value !== skipValue)) { 30 | node.setAttributeNS(ANDROID_NS, attr, value); 31 | } 32 | }; 33 | 34 | 35 | let serializeXmlNode_ = xmlNode => { 36 | let xmlStr = xmlserializer.serializeToString(xmlNode, {indent:4, multiAttributeIndent:4}); 37 | return xmlStr; //new XMLSerializer().serializeToString(xmlNode); 38 | // return vkbeautify.xml(xmlStr, 4); 39 | }; 40 | 41 | 42 | export const AvdSerializer = { 43 | 44 | /** 45 | * Serializes an Artwork to a vector drawable XML file. 46 | */ 47 | artworkToVectorDrawableXmlString(artwork) { 48 | let xmlDoc = document.implementation.createDocument(null, 'vector'); 49 | let rootNode = xmlDoc.documentElement; 50 | AvdSerializer.artworkToXmlNode_(artwork, rootNode, xmlDoc); 51 | return serializeXmlNode_(rootNode); 52 | }, 53 | 54 | /** 55 | * Serializes a given Artwork and Animation to an animatedvector drawable XML file. 56 | */ 57 | artworkAnimationToAvdXmlString(artwork, animation) { 58 | let xmlDoc = document.implementation.createDocument(null, 'animated-vector'); 59 | let rootNode = xmlDoc.documentElement; 60 | rootNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); 61 | rootNode.setAttributeNS(XMLNS_NS, 'xmlns:aapt', AAPT_NS); 62 | 63 | // create drawable node containing the artwork 64 | let artworkContainerNode = xmlDoc.createElementNS(AAPT_NS, 'aapt:attr'); 65 | artworkContainerNode.setAttribute('name', 'android:drawable'); 66 | rootNode.appendChild(artworkContainerNode); 67 | 68 | let artworkNode = xmlDoc.createElement('vector'); 69 | AvdSerializer.artworkToXmlNode_(artwork, artworkNode, xmlDoc); 70 | artworkContainerNode.appendChild(artworkNode); 71 | 72 | // create animation nodes (one per layer) 73 | let animBlocksByLayer = {}; 74 | animation.blocks.forEach(block => { 75 | animBlocksByLayer[block.layerId] = animBlocksByLayer[block.layerId] || []; 76 | animBlocksByLayer[block.layerId].push(block); 77 | }); 78 | 79 | for (let layerId in animBlocksByLayer) { 80 | let targetNode = xmlDoc.createElement('target'); 81 | targetNode.setAttributeNS(ANDROID_NS, 'android:name', layerId); 82 | rootNode.appendChild(targetNode); 83 | 84 | let animationNode = xmlDoc.createElementNS(AAPT_NS, 'aapt:attr'); 85 | animationNode.setAttribute('name', 'android:animation'); 86 | targetNode.appendChild(animationNode); 87 | 88 | let blocksForLayer = animBlocksByLayer[layerId]; 89 | let blockContainerNode = animationNode; 90 | let multiBlock = false; 91 | if (blocksForLayer.length > 1) { 92 | multiBlock = true; 93 | 94 | // for multiple property animations on a single layer 95 | blockContainerNode = xmlDoc.createElement('set'); 96 | blockContainerNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); 97 | animationNode.appendChild(blockContainerNode); 98 | } 99 | 100 | let layer = artwork.findLayerById(layerId); 101 | let animatableProperties = layer.animatableProperties; 102 | 103 | blocksForLayer.forEach(block => { 104 | let blockNode = xmlDoc.createElement('objectAnimator'); 105 | if (!multiBlock) { 106 | blockNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); 107 | } 108 | blockNode.setAttributeNS(ANDROID_NS, 'android:propertyName', block.propertyName); 109 | conditionalAttr_(blockNode, 'android:startOffset', block.startTime, 0); 110 | conditionalAttr_(blockNode, 'android:duration', block.endTime - block.startTime); 111 | conditionalAttr_(blockNode, 'android:valueFrom', block.fromValue); 112 | conditionalAttr_(blockNode, 'android:valueTo', block.toValue); 113 | conditionalAttr_(blockNode, 'android:valueType', 114 | animatableProperties[block.propertyName].animatorValueType); 115 | conditionalAttr_(blockNode, 'android:interpolator', block.interpolator.androidRef); 116 | blockContainerNode.appendChild(blockNode); 117 | }); 118 | } 119 | 120 | return serializeXmlNode_(rootNode); 121 | }, 122 | 123 | /** 124 | * Helper method that serializes an Artwork to a destinationNode in an xmlDoc. 125 | * The destinationNode should be a node. 126 | */ 127 | artworkToXmlNode_(artwork, destinationNode, xmlDoc) { 128 | destinationNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); 129 | destinationNode.setAttributeNS(ANDROID_NS, 'android:width', `${artwork.width}dp`); 130 | destinationNode.setAttributeNS(ANDROID_NS, 'android:height', `${artwork.height}dp`); 131 | destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportWidth', `${artwork.width}`); 132 | destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportHeight', `${artwork.height}`); 133 | conditionalAttr_(destinationNode, 'android:alpha', artwork.alpha, 1); 134 | 135 | artwork.walk((layer, parentNode) => { 136 | if (layer instanceof Artwork) { 137 | return parentNode; 138 | 139 | } else if (layer instanceof PathLayer) { 140 | let node = xmlDoc.createElement('path'); 141 | conditionalAttr_(node, 'android:name', layer.id); 142 | conditionalAttr_(node, 'android:pathData', layer.pathData.pathString); 143 | conditionalAttr_(node, 'android:fillColor', layer.fillColor, ''); 144 | conditionalAttr_(node, 'android:fillAlpha', layer.fillAlpha, 1); 145 | conditionalAttr_(node, 'android:strokeColor', layer.strokeColor, ''); 146 | conditionalAttr_(node, 'android:strokeAlpha', layer.strokeAlpha, 1); 147 | conditionalAttr_(node, 'android:strokeWidth', layer.strokeWidth, 0); 148 | conditionalAttr_(node, 'android:trimPathStart', layer.trimPathStart, 0); 149 | conditionalAttr_(node, 'android:trimPathEnd', layer.trimPathEnd, 1); 150 | conditionalAttr_(node, 'android:trimPathOffset', layer.trimPathOffset, 0); 151 | conditionalAttr_(node, 'android:strokeLineCap', layer.strokeLinecap, DefaultValues.LINECAP); 152 | conditionalAttr_(node, 'android:strokeLineJoin', layer.strokeLinejoin, 153 | DefaultValues.LINEJOIN); 154 | conditionalAttr_(node, 'android:strokeMiterLimit', layer.strokeMiterLimit, 155 | DefaultValues.MITER_LIMIT); 156 | parentNode.appendChild(node); 157 | return parentNode; 158 | 159 | } else if (layer instanceof MaskLayer) { 160 | let node = xmlDoc.createElement('clip-path'); 161 | conditionalAttr_(node, 'android:name', layer.id); 162 | conditionalAttr_(node, 'android:pathData', layer.pathData.pathString); 163 | parentNode.appendChild(node); 164 | return parentNode; 165 | 166 | } else if (layer instanceof LayerGroup) { 167 | let node = xmlDoc.createElement('group'); 168 | conditionalAttr_(node, 'android:name', layer.id); 169 | conditionalAttr_(node, 'android:pivotX', layer.pivotX, 0); 170 | conditionalAttr_(node, 'android:pivotY', layer.pivotY, 0); 171 | conditionalAttr_(node, 'android:translateX', layer.translateX, 0); 172 | conditionalAttr_(node, 'android:translateY', layer.translateY, 0); 173 | conditionalAttr_(node, 'android:scaleX', layer.scaleX, 1); 174 | conditionalAttr_(node, 'android:scaleY', layer.scaleY, 1); 175 | conditionalAttr_(node, 'android:rotation', layer.rotation, 0); 176 | parentNode.appendChild(node); 177 | return node; 178 | } 179 | }, destinationNode); 180 | }, 181 | }; 182 | -------------------------------------------------------------------------------- /app/scripts/ColorUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {default as tinycolor} from 'tinycolor2'; 18 | 19 | const BRIGHTNESS_THRESHOLD = 130; // for isColorDark 20 | 21 | 22 | export const ColorUtil = { 23 | parseAndroidColor(val) { 24 | val = (val || '').replace(/^\s*#?|\s*$/g, ''); 25 | let dict = {a:255}; 26 | 27 | if (val.length == 3) { 28 | dict.r = parseInt(val.substring(0, 1), 16) * 17; 29 | dict.g = parseInt(val.substring(1, 2), 16) * 17; 30 | dict.b = parseInt(val.substring(2, 3), 16) * 17; 31 | } else if (val.length == 4) { 32 | dict.a = parseInt(val.substring(0, 1), 16) * 17; 33 | dict.r = parseInt(val.substring(1, 2), 16) * 17; 34 | dict.g = parseInt(val.substring(2, 3), 16) * 17; 35 | dict.b = parseInt(val.substring(3, 4), 16) * 17; 36 | } else if (val.length == 6) { 37 | dict.r = parseInt(val.substring(0, 2), 16); 38 | dict.g = parseInt(val.substring(2, 4), 16); 39 | dict.b = parseInt(val.substring(4, 6), 16); 40 | } else if (val.length == 8) { 41 | dict.a = parseInt(val.substring(0, 2), 16); 42 | dict.r = parseInt(val.substring(2, 4), 16); 43 | dict.g = parseInt(val.substring(4, 6), 16); 44 | dict.b = parseInt(val.substring(6, 8), 16); 45 | } else { 46 | return null; 47 | } 48 | 49 | return (isNaN(dict.r) || isNaN(dict.g) || isNaN(dict.b) || isNaN(dict.a)) 50 | ? null 51 | : dict; 52 | }, 53 | 54 | toAndroidString(dict) { 55 | let str = '#'; 56 | if (dict.a != 255) { 57 | str += ((dict.a < 16) ? '0' : '') + dict.a.toString(16); 58 | } 59 | 60 | str += ((dict.r < 16) ? '0' : '') + dict.r.toString(16) 61 | + ((dict.g < 16) ? '0' : '') + dict.g.toString(16) 62 | + ((dict.b < 16) ? '0' : '') + dict.b.toString(16); 63 | return str; 64 | }, 65 | 66 | svgToAndroidColor(color) { 67 | if (color == 'none') { 68 | return null; 69 | } 70 | color = tinycolor(color); 71 | let colorHex = color.toHex(); 72 | let alphaHex = color.toHex8().substr(6); 73 | return '#' + (alphaHex != 'ff' ? alphaHex : '') + colorHex; 74 | }, 75 | 76 | androidToCssColor(androidColor, multAlpha) { 77 | multAlpha = (multAlpha === undefined) ? 1 : multAlpha; 78 | if (!androidColor) { 79 | return 'transparent'; 80 | } 81 | 82 | let d = ColorUtil.parseAndroidColor(androidColor); 83 | return `rgba(${d.r},${d.g},${d.b},${(d.a * multAlpha / 255).toFixed(2)})`; 84 | }, 85 | 86 | isAndroidColorDark(androidColor) { 87 | if (!androidColor) { 88 | return false; 89 | } 90 | 91 | let d = ColorUtil.parseAndroidColor(androidColor); 92 | return ((30 * d.r + 59 * d.g + 11 * d.b) / 100) <= BRIGHTNESS_THRESHOLD; 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /app/scripts/DragHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const DRAG_SLOP = 4; // pixels 18 | 19 | 20 | export class DragHelper { 21 | constructor(opts) { 22 | opts = opts || {}; 23 | 24 | this.direction_ = opts.direction || 'both'; 25 | this.downX_ = opts.downEvent.clientX; 26 | this.downY_ = opts.downEvent.clientY; 27 | this.skipSlopCheck_ = !!opts.skipSlopCheck; 28 | 29 | this.onBeginDrag_ = opts.onBeginDrag || (() => {}); 30 | this.onDrag_ = opts.onDrag || (() => {}); 31 | this.onDrop_ = opts.onDrop || (() => {}); 32 | 33 | this.dragging_ = false; 34 | this.draggingScrim_ = null; 35 | 36 | this.draggingCursor = opts.draggingCursor || 'grabbing'; 37 | 38 | let mouseMoveHandler_ = event => { 39 | if (!this.dragging_ && this.shouldBeginDragging_(event)) { 40 | this.dragging_ = true; 41 | this.draggingScrim_ = this.buildDraggingScrim_().appendTo(document.body); 42 | this.draggingCursor = this.draggingCursor_; 43 | this.onBeginDrag_(event); 44 | } 45 | 46 | if (this.dragging_) { 47 | this.onDrag_(event, { 48 | x: event.clientX - this.downX_, 49 | y: event.clientY - this.downY_ 50 | }); 51 | } 52 | }; 53 | 54 | let mouseUpHandler_ = event => { 55 | $(window) 56 | .off('mousemove', mouseMoveHandler_) 57 | .off('mouseup', mouseUpHandler_); 58 | if (this.dragging_) { 59 | this.onDrag_(event, { 60 | x: event.clientX - this.downX_, 61 | y: event.clientY - this.downY_ 62 | }); 63 | 64 | this.onDrop_(); 65 | 66 | this.draggingScrim_.remove(); 67 | this.draggingScrim_ = null; 68 | this.dragging_ = false; 69 | 70 | event.stopPropagation(); 71 | event.preventDefault(); 72 | return false; 73 | } 74 | }; 75 | 76 | $(window) 77 | .on('mousemove', mouseMoveHandler_) 78 | .on('mouseup', mouseUpHandler_); 79 | } 80 | 81 | shouldBeginDragging_(mouseMoveEvent) { 82 | if (this.skipSlopCheck_) { 83 | return true; 84 | } 85 | 86 | let begin = false; 87 | if (this.direction_ == 'both' || this.direction_ == 'horizontal') { 88 | begin = begin || (Math.abs(mouseMoveEvent.clientX - this.downX_) > DRAG_SLOP); 89 | } 90 | if (this.direction_ == 'both' || this.direction_ == 'vertical') { 91 | begin = begin || (Math.abs(mouseMoveEvent.clientY - this.downY_) > DRAG_SLOP); 92 | } 93 | return begin; 94 | } 95 | 96 | set draggingCursor(cursor) { 97 | if (cursor == 'grabbing') { 98 | cursor = `-webkit-${cursor}`; 99 | } 100 | 101 | this.draggingCursor_ = cursor; 102 | if (this.draggingScrim_) { 103 | this.draggingScrim_.css({cursor}); 104 | } 105 | } 106 | 107 | buildDraggingScrim_() { 108 | return $('
') 109 | .css({ 110 | position: 'fixed', 111 | left: 0, 112 | top: 0, 113 | right: 0, 114 | bottom: 0, 115 | zIndex: 9999 116 | }); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /app/scripts/ElementResizeWatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Based on http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/ 18 | 19 | export class ElementResizeWatcher { 20 | constructor(element, listener) { 21 | this.element_ = $(element); 22 | 23 | // create resize listener 24 | let rafHandle; 25 | 26 | this.onResize_ = event => { 27 | var el = event.target || event.srcElement; 28 | if (rafHandle) { 29 | el.cancelAnimationFrame(rafHandle); 30 | } 31 | 32 | rafHandle = el.requestAnimationFrame(() => listener()); 33 | }; 34 | 35 | // add listener 36 | if (getComputedStyle(this.element_.get(0)).position == 'static') { 37 | this.element_.css({position: 'relative'}); 38 | } 39 | 40 | this.proxyElement_ = $('') 41 | .css({ 42 | display: 'block', 43 | position: 'absolute', 44 | left: 0, 45 | top: 0, 46 | width: '100%', 47 | height: '100%', 48 | overflow: 'hidden', 49 | pointerEvents: 'none', 50 | zIndex: -1 51 | }) 52 | .attr('type', 'text/html') 53 | .attr('data', 'about:blank') 54 | .on('load', () => { 55 | this.proxyDefaultView_ = this.proxyElement_.get(0).contentDocument.defaultView; 56 | this.proxyDefaultView_.addEventListener('resize', this.onResize_); 57 | }) 58 | .appendTo(this.element_); 59 | } 60 | 61 | destroy() { 62 | if (this.proxyDefaultView_) { 63 | this.proxyDefaultView_.removeEventListener('resize', this.onResize_); 64 | } 65 | 66 | this.proxyElement_.remove(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/scripts/MathUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const MathUtil = { 18 | progress(val, min, max) { 19 | return MathUtil.constrain((val - min) / (max - min), 0, 1); 20 | }, 21 | 22 | constrain(val, min, max) { 23 | if (val < min) { 24 | return min; 25 | } else if (val > max) { 26 | return max; 27 | } else { 28 | return val; 29 | } 30 | }, 31 | 32 | interpolate(start, end, f) { 33 | return start + (end - start) * f; 34 | }, 35 | 36 | dist(x1, y1, x2, y2) { 37 | return Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /app/scripts/ModelUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const ModelUtil = { 18 | getOrderedAnimationBlocksByLayerIdAndProperty(animation) { 19 | let animationBlocksByLayerId = {}; 20 | 21 | animation.blocks.forEach(block => { 22 | let blocksByProperty = animationBlocksByLayerId[block.layerId]; 23 | if (!blocksByProperty) { 24 | blocksByProperty = {}; 25 | animationBlocksByLayerId[block.layerId] = blocksByProperty; 26 | } 27 | 28 | blocksByProperty[block.propertyName] = blocksByProperty[block.propertyName] || []; 29 | blocksByProperty[block.propertyName].push(block); 30 | }); 31 | 32 | for (let layerId in animationBlocksByLayerId) { 33 | let blocksByProperty = animationBlocksByLayerId[layerId]; 34 | for (let propertyName in blocksByProperty) { 35 | blocksByProperty[propertyName].sort((a, b) => a.startTime - b.startTime); 36 | } 37 | } 38 | 39 | return animationBlocksByLayerId; 40 | }, 41 | 42 | getUniqueId(opts) { 43 | opts = opts || {}; 44 | opts.prefix = opts.prefix || ''; 45 | opts.objectById = opts.objectById || (() => null); 46 | opts.targetObject = opts.targetObject || null; 47 | 48 | let n = 0; 49 | let id_ = () => opts.prefix + (n ? `_${n}` : ''); 50 | while (true) { 51 | let o = opts.objectById(id_()); 52 | if (!o || o == opts.targetObject) { 53 | break; 54 | } 55 | 56 | ++n; 57 | } 58 | 59 | return id_(); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /app/scripts/RenderUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const IDENTITY_TRANSFORM_MATRIX = [1, 0, 0, 1, 0, 0]; 18 | 19 | export const RenderUtil = { 20 | transformMatrixForLayer(layer) { 21 | let cosR = Math.cos(layer.rotation * Math.PI / 180); 22 | let sinR = Math.sin(layer.rotation * Math.PI / 180); 23 | 24 | // first negative pivot, then scale, rotate, translate, and pivot 25 | // notes: 26 | // translate: [1, 0, 0, 1, x, y] 27 | // scale: [sx, 0, 0, sy, 0, 0] 28 | // rotate: [cos, sin, -sin, cos, 0, 0] 29 | 30 | return [ 31 | cosR * layer.scaleX, 32 | sinR * layer.scaleX, 33 | -sinR * layer.scaleY, 34 | cosR * layer.scaleY, 35 | (layer.pivotX + layer.translateX) 36 | - cosR * layer.scaleX * layer.pivotX 37 | + sinR * layer.scaleY * layer.pivotY, 38 | (layer.pivotY + layer.translateY) 39 | - cosR * layer.scaleY * layer.pivotY 40 | - sinR * layer.scaleX * layer.pivotX 41 | ]; 42 | }, 43 | 44 | flattenTransforms(transforms) { 45 | return (transforms || []).reduce( 46 | (m, transform) => transformMatrix_(transform, m), 47 | IDENTITY_TRANSFORM_MATRIX); 48 | }, 49 | 50 | transformPoint(matrices, p) { 51 | if (!matrices || !matrices.length) { 52 | return Object.assign({}, p); 53 | } 54 | 55 | return matrices.reduce((p, m) => ({ 56 | // [a c e] [p.x] 57 | // [b d f] * [p.y] 58 | // [0 0 1] [ 1 ] 59 | x: m[0] * p.x + m[2] * p.y + m[4], 60 | y: m[1] * p.x + m[3] * p.y + m[5] 61 | }), p); 62 | }, 63 | 64 | computeStrokeWidthMultiplier(transformMatrix) { 65 | // from getMatrixScale in 66 | // https://android.googlesource.com/platform/frameworks/base/+/master/libs/hwui/VectorDrawable.cpp 67 | 68 | // Given unit vectors A = (0, 1) and B = (1, 0). 69 | // After matrix mapping, we got A' and B'. Let theta = the angel b/t A' and B'. 70 | // Therefore, the final scale we want is min(|A'| * sin(theta), |B'| * sin(theta)), 71 | // which is (|A'| * |B'| * sin(theta)) / max (|A'|, |B'|); 72 | // If max (|A'|, |B'|) = 0, that means either x or y has a scale of 0. 73 | // 74 | // For non-skew case, which is most of the cases, matrix scale is computing exactly the 75 | // scale on x and y axis, and take the minimal of these two. 76 | // For skew case, an unit square will mapped to a parallelogram. And this function will 77 | // return the minimal height of the 2 bases. 78 | 79 | // first remove translate elements from matrix 80 | transformMatrix[4] = transformMatrix[5] = 0; 81 | 82 | let vecA = RenderUtil.transformPoint([transformMatrix], {x:0, y:1}); 83 | let vecB = RenderUtil.transformPoint([transformMatrix], {x:1, y:0}); 84 | let scaleX = Math.hypot(vecA.x, vecA.y); 85 | let scaleY = Math.hypot(vecB.x, vecB.y); 86 | let crossProduct = vecA.y * vecB.x - vecA.x * vecB.y; // vector cross product 87 | let maxScale = Math.max(scaleX, scaleY); 88 | let matrixScale = 0; 89 | if (maxScale > 0) { 90 | matrixScale = Math.abs(crossProduct) / maxScale; 91 | } 92 | return matrixScale; 93 | } 94 | }; 95 | 96 | 97 | // formula generated w/ wolfram alpha 98 | // returns the product of 2D transformation matrices s and t 99 | 100 | function transformMatrix_(s, t) { 101 | return [t[0] * s[0] + t[1] * s[2], 102 | t[0] * s[1] + t[1] * s[3], 103 | s[0] * t[2] + s[2] * t[3], 104 | s[1] * t[2] + t[3] * s[3], 105 | s[0] * t[4] + s[4] + s[2] * t[5], 106 | s[1] * t[4] + s[3] * t[5] + s[5]]; 107 | } 108 | -------------------------------------------------------------------------------- /app/scripts/SvgLoader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Artwork, DefaultValues} from './model'; 18 | import {IdProperty} from './model/properties'; 19 | import {ColorUtil} from './ColorUtil'; 20 | import {SvgPathData} from './SvgPathData'; 21 | import {ModelUtil} from './ModelUtil'; 22 | 23 | 24 | export const SvgLoader = { 25 | loadArtworkFromSvgString(svgString) { 26 | let parser = new DOMParser(); 27 | let doc = parser.parseFromString(svgString, 'image/svg+xml'); 28 | 29 | let usedIds = {}; 30 | 31 | let nodeToLayerData_ = (node, context) => { 32 | if (!node) { 33 | return null; 34 | } 35 | 36 | if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.COMMENT_NODE) { 37 | return null; 38 | } 39 | 40 | let makeFinalNodeId_ = typeIdPrefix => { 41 | let finalId = ModelUtil.getUniqueId({ 42 | prefix: IdProperty.sanitize(node.id || typeIdPrefix), 43 | objectById: id => usedIds[id], 44 | }); 45 | usedIds[finalId] = true; 46 | return finalId; 47 | }; 48 | 49 | let layerData = {}; 50 | 51 | let simpleAttr_ = (nodeAttr, contextAttr) => { 52 | if (node.attributes[nodeAttr]) { 53 | context[contextAttr] = node.attributes[nodeAttr].value; 54 | } 55 | }; 56 | 57 | // set attributes 58 | simpleAttr_('stroke', 'strokeColor'); 59 | simpleAttr_('stroke-width', 'strokeWidth'); 60 | simpleAttr_('stroke-linecap', 'strokeLinecap'); 61 | simpleAttr_('stroke-linejoin', 'strokeLinejoin'); 62 | simpleAttr_('stroke-miterlimit', 'strokeMiterLimit'); 63 | simpleAttr_('stroke-opacity', 'strokeAlpha'); 64 | simpleAttr_('fill', 'fillColor'); 65 | simpleAttr_('fill-opacity', 'fillAlpha'); 66 | 67 | // add transforms 68 | 69 | if (node.transform) { 70 | let transforms = Array.from(node.transform.baseVal); 71 | transforms.reverse(); 72 | context.transforms = context.transforms ? context.transforms.slice() : []; 73 | context.transforms.splice(0, 0, ...transforms); 74 | } 75 | 76 | // see if this is a path 77 | let path; 78 | if (node instanceof SVGPathElement) { 79 | path = node.attributes.d.value; 80 | 81 | } else if (node instanceof SVGRectElement) { 82 | let l = lengthPx_(node.x), 83 | t = lengthPx_(node.y), 84 | r = l + lengthPx_(node.width), 85 | b = t + lengthPx_(node.height); 86 | // TODO: handle corner radii 87 | path = `M ${l},${t} ${r},${t} ${r},${b} ${l},${b} Z`; 88 | 89 | } else if (node instanceof SVGLineElement) { 90 | let x1 = lengthPx_(node.x1), 91 | y1 = lengthPx_(node.y1), 92 | x2 = lengthPx_(node.x2), 93 | y2 = lengthPx_(node.y2); 94 | path = `M ${x1},${y1} ${x2},${y2} Z`; 95 | 96 | } else if (node instanceof SVGPolygonElement || node instanceof SVGPolylineElement) { 97 | path = 'M ' + Array.from(node.points).map(pt => pt.x +',' + pt.y).join(' '); 98 | if (node instanceof SVGPolygonElement) { 99 | path += ' Z'; 100 | } 101 | 102 | } else if (node instanceof SVGCircleElement) { 103 | let cx = lengthPx_(node.cx), 104 | cy = lengthPx_(node.cy), 105 | r = lengthPx_(node.r); 106 | path = `M ${cx},${cy-r} A ${r} ${r} 0 1 0 ${cx},${cy+r} A ${r} ${r} 0 1 0 ${cx},${cy-r} Z`; 107 | 108 | } else if (node instanceof SVGEllipseElement) { 109 | let cx = lengthPx_(node.cx), 110 | cy = lengthPx_(node.cy), 111 | rx = lengthPx_(node.rx), 112 | ry = lengthPx_(node.ry); 113 | path = `M ${cx},${cy-ry} A ${rx} ${ry} 0 1 0 ${cx},${cy+ry} ` + 114 | `A ${rx} ${ry} 0 1 0 ${cx},${cy-ry} Z`; 115 | } 116 | 117 | if (path) { 118 | // transform all points 119 | if (context.transforms && context.transforms.length) { 120 | let pathData = new SvgPathData(path); 121 | pathData.transform(context.transforms); 122 | path = pathData.pathString; 123 | } 124 | 125 | // create a path layer 126 | return Object.assign(layerData, { 127 | id: makeFinalNodeId_('path'), 128 | pathData: path, 129 | fillColor: ('fillColor' in context) ? ColorUtil.svgToAndroidColor(context.fillColor) : "#ff000000", 130 | fillAlpha: ('fillAlpha' in context) ? context.fillAlpha : 1, 131 | strokeColor: ('strokeColor' in context) ? ColorUtil.svgToAndroidColor(context.strokeColor) : null, 132 | strokeAlpha: ('strokeAlpha' in context) ? context.strokeAlpha : 1, 133 | strokeWidth: ('strokeWidth' in context) ? context.strokeWidth : 1, 134 | strokeLinecap: context.strokeLinecap || DefaultValues.LINECAP, 135 | strokeLinejoin: context.strokeLinejoin || DefaultValues.LINEJOIN, 136 | strokeMiterLimit: ('strokeMiterLimit' in context) ? context.strokeMiterLimit : DefaultValues.MITER_LIMIT, 137 | }); 138 | } 139 | 140 | if (node.childNodes.length) { 141 | let layers = Array.from(node.childNodes) 142 | .map(child => nodeToLayerData_(child, Object.assign({}, context))) 143 | .filter(layer => !!layer); 144 | if (layers && layers.length) { 145 | // create a group (there are valid children) 146 | return Object.assign(layerData, { 147 | id: makeFinalNodeId_('group'), 148 | type: 'group', 149 | layers: layers 150 | }); 151 | } else { 152 | return null; 153 | } 154 | } 155 | }; 156 | 157 | let docElContext = {}; 158 | let width = lengthPx_(doc.documentElement.width); 159 | let height = lengthPx_(doc.documentElement.height); 160 | 161 | if (doc.documentElement.viewBox) { 162 | width = doc.documentElement.viewBox.baseVal.width; 163 | height = doc.documentElement.viewBox.baseVal.height; 164 | 165 | // fake a translate transform for the viewbox 166 | docElContext.transforms = [ 167 | { 168 | matrix: { 169 | a: 1, 170 | b: 0, 171 | c: 0, 172 | d: 1, 173 | e: -doc.documentElement.viewBox.baseVal.x, 174 | f: -doc.documentElement.viewBox.baseVal.y 175 | } 176 | } 177 | ]; 178 | } 179 | 180 | let rootLayer = nodeToLayerData_(doc.documentElement, docElContext); 181 | 182 | let artwork = { 183 | width, 184 | height, 185 | layers: (rootLayer ? rootLayer.layers : null) || [], 186 | alpha: doc.documentElement.getAttribute('opacity') || 1, 187 | }; 188 | 189 | return new Artwork(artwork); 190 | } 191 | }; 192 | 193 | 194 | function lengthPx_(svgLength) { 195 | if (svgLength.baseVal) { 196 | svgLength = svgLength.baseVal; 197 | } 198 | svgLength.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); 199 | return svgLength.valueInSpecifiedUnits; 200 | } 201 | 202 | -------------------------------------------------------------------------------- /app/scripts/UiUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const UiUtil = { 18 | waitForElementWidth_(el, timeout = 1000) { 19 | let start = Number(new Date()); 20 | let $el = $(el); 21 | return new Promise((resolve, reject) => { 22 | let tryResolve_ = () => { 23 | if (Number(new Date()) - start > timeout) { 24 | reject(); 25 | return; 26 | } 27 | 28 | let width = $el.width(); 29 | if (width) { 30 | resolve(width); 31 | } else { 32 | setTimeout(() => tryResolve_(), 0); 33 | } 34 | }; 35 | 36 | tryResolve_(); 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /app/scripts/VectorDrawableLoader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Artwork, DefaultValues} from './model'; 18 | import {IdProperty} from './model/properties'; 19 | import {ModelUtil} from './ModelUtil'; 20 | 21 | 22 | export const VectorDrawableLoader = { 23 | loadArtworkFromXmlString(xmlString) { 24 | let parser = new DOMParser(); 25 | let doc = parser.parseFromString(xmlString, 'application/xml'); 26 | 27 | let usedIds = {}; 28 | 29 | let nodeToLayerData_ = (node) => { 30 | if (!node) { 31 | return null; 32 | } 33 | if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.COMMENT_NODE) { 34 | return null; 35 | } 36 | 37 | let makeFinalNodeId_ = (node, typeIdPrefix) => { 38 | let name = node.getAttribute('android:name'); 39 | let finalId = ModelUtil.getUniqueId({ 40 | prefix: IdProperty.sanitize(name || typeIdPrefix), 41 | objectById: id => usedIds[id], 42 | }); 43 | usedIds[finalId] = true; 44 | return finalId; 45 | }; 46 | 47 | let layerData = {}; 48 | 49 | if (node.tagName === 'path') { 50 | return Object.assign(layerData, { 51 | id: makeFinalNodeId_(node, 'path'), 52 | pathData: node.getAttribute('android:pathData') || null, 53 | fillColor: node.getAttribute('android:fillColor') || null, 54 | fillAlpha: node.getAttribute('android:fillAlpha') || 1, 55 | strokeColor: node.getAttribute('android:strokeColor') || null, 56 | strokeAlpha: node.getAttribute('android:strokeAlpha') || 1, 57 | strokeWidth: node.getAttribute('android:strokeWidth') || 0, 58 | strokeLinecap: node.getAttribute('android:strokeLineCap') || DefaultValues.LINECAP, 59 | strokeLinejoin: node.getAttribute('android:strokeLineJoin') || DefaultValues.LINEJOIN, 60 | strokeMiterLimit: 61 | node.getAttribute('android:strokeMiterLimit') || DefaultValues.MITER_LIMIT, 62 | trimPathStart: node.getAttribute('android:trimPathStart') || 0, 63 | trimPathEnd: node.getAttribute('android:trimPathEnd') || 1, 64 | trimPathOffset: node.getAttribute('android:trimPathOffset') || 0, 65 | }); 66 | } 67 | 68 | if (node.childNodes.length) { 69 | let layers = Array.from(node.childNodes) 70 | .map(child => nodeToLayerData_(child)) 71 | .filter(layer => !!layer); 72 | if (layers && layers.length) { 73 | // create a group (there are valid children) 74 | return Object.assign(layerData, { 75 | id: makeFinalNodeId_(node, 'group'), 76 | type: 'group', 77 | rotation: node.getAttribute('android:rotation') || 0, 78 | scaleX: node.getAttribute('android:scaleX') || 1, 79 | scaleY: node.getAttribute('android:scaleY') || 1, 80 | pivotX: node.getAttribute('android:pivotX') || 0, 81 | pivotY: node.getAttribute('android:pivotY') || 0, 82 | translateX: node.getAttribute('android:translateX') || 0, 83 | translateY: node.getAttribute('android:translateY') || 0, 84 | layers, 85 | }); 86 | } else { 87 | return null; 88 | } 89 | } 90 | }; 91 | 92 | let rootLayer = nodeToLayerData_(doc.documentElement); 93 | let id = IdProperty.sanitize(doc.documentElement.getAttribute('android:name') || 'vector'); 94 | usedIds[id] = true; 95 | let width = doc.documentElement.getAttribute('android:viewportWidth'); 96 | let height = doc.documentElement.getAttribute('android:viewportHeight'); 97 | let alpha = doc.documentElement.getAttribute('android:alpha') || 1; 98 | let artwork = { 99 | id, 100 | width, 101 | height, 102 | layers: (rootLayer ? rootLayer.layers : null) || [], 103 | alpha, 104 | }; 105 | return new Artwork(artwork); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('AVDStudio', ['ngMaterial', 'ngRoute']) 18 | .config(require('./materialtheme')) 19 | .config(require('./icons')) 20 | .config(require('./routes').routeConfig); 21 | 22 | // core app 23 | angular.module('AVDStudio').controller('AppCtrl', class AppCtrl { 24 | constructor($scope) {} 25 | }); 26 | 27 | // all components 28 | require('../components/**/*.js', {mode: 'expand'}); 29 | 30 | // all pages 31 | require('../pages/**/*.js', {mode: 'expand'}); 32 | -------------------------------------------------------------------------------- /app/scripts/icons.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = function($mdIconProvider) { 18 | $mdIconProvider.iconSet('avdstudio', 'images/icons.svg'); 19 | }; 20 | -------------------------------------------------------------------------------- /app/scripts/materialtheme.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = function($mdThemingProvider) { 18 | $mdThemingProvider.theme('default') 19 | .primaryPalette('blue') 20 | .accentPalette('blue'); 21 | $mdThemingProvider.theme('dark') 22 | .primaryPalette('blue') 23 | .accentPalette('blue') 24 | .dark(); 25 | $mdThemingProvider.setDefaultTheme('default'); 26 | }; -------------------------------------------------------------------------------- /app/scripts/model/Animation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, IdProperty, NumberProperty} from './properties'; 18 | 19 | import {AnimationBlock} from './AnimationBlock'; 20 | 21 | /** 22 | * An animation represents a collection of layer property tweens for a given artwork. 23 | */ 24 | @Property.register([ 25 | new IdProperty('id'), 26 | new NumberProperty('duration', {min:100, max:60000}), 27 | ]) 28 | export class Animation { 29 | constructor(obj = {}) { 30 | this.id = obj.id || null; 31 | this.blocks = (obj.blocks || []).map(obj => new AnimationBlock(obj)); 32 | this.duration = obj.duration || 100; 33 | } 34 | 35 | get blocks() { 36 | return this.blocks_ || []; 37 | } 38 | 39 | set blocks(blocks) { 40 | this.blocks_ = blocks; 41 | this.blocks_.forEach(block => block.parent = this); 42 | } 43 | 44 | get typeString() { 45 | return 'animation'; 46 | } 47 | 48 | get typeIdPrefix() { 49 | return 'anim'; 50 | } 51 | 52 | get typeIcon() { 53 | return 'animation'; 54 | } 55 | 56 | toJSON() { 57 | return { 58 | id: this.id, 59 | duration: this.duration, 60 | blocks: this.blocks.map(block => block.toJSON()) 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/scripts/model/AnimationBlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {default as bezierEasing} from 'bezier-easing'; 18 | 19 | import {SvgPathData} from '../SvgPathData'; 20 | import {Property, StubProperty, NumberProperty, EnumProperty} from './properties'; 21 | 22 | const FAST_OUT_SLOW_IN_EASING = bezierEasing(.4, 0, .2, 1); 23 | const FAST_OUT_LINEAR_IN_EASING = bezierEasing(.4, 0, 1, 1); 24 | const LINEAR_OUT_SLOW_IN_EASING = bezierEasing(0, 0, .2, 1); 25 | 26 | const ENUM_INTERPOLATOR_OPTIONS = [ 27 | { 28 | value: 'ACCELERATE_DECELERATE', 29 | label: 'Accelerate/decelerate', 30 | androidRef: '@android:anim/accelerate_decelerate_interpolator', 31 | interpolate: f => Math.cos((f + 1) * Math.PI) / 2.0 + 0.5, 32 | }, 33 | { 34 | value: 'ACCELERATE', 35 | label: 'Accelerate', 36 | androidRef: '@android:anim/accelerate_interpolator', 37 | interpolate: f => f * f, 38 | }, 39 | { 40 | value: 'DECELERATE', 41 | label: 'Decelerate', 42 | androidRef: '@android:anim/decelerate_interpolator', 43 | interpolate: f => (1 - (1 - f) * (1 - f)), 44 | }, 45 | { 46 | value: 'ANTICIPATE', 47 | label: 'Anticipate', 48 | androidRef: '@android:anim/anticipate_interpolator', 49 | interpolate: f => f * f * ((2 + 1) * f - 2), 50 | }, 51 | { 52 | value: 'LINEAR', 53 | label: 'Linear', 54 | androidRef: '@android:anim/linear_interpolator', 55 | interpolate: f => f, 56 | }, 57 | { 58 | value: 'OVERSHOOT', 59 | label: 'Overshoot', 60 | androidRef: '@android:anim/overshoot_interpolator', 61 | interpolate: f => (f - 1) * (f - 1) * ((2 + 1) * (f - 1) + 2) + 1 62 | }, 63 | { 64 | value: 'FAST_OUT_SLOW_IN', 65 | label: 'Fast out, slow in', 66 | androidRef: '@android:interpolator/fast_out_slow_in', 67 | interpolate: f => FAST_OUT_SLOW_IN_EASING(f) 68 | }, 69 | { 70 | value: 'FAST_OUT_LINEAR_IN', 71 | label: 'Fast out, linear in', 72 | androidRef: '@android:interpolator/fast_out_linear_in', 73 | interpolate: f => FAST_OUT_LINEAR_IN_EASING(f) 74 | }, 75 | { 76 | value: 'LINEAR_OUT_SLOW_IN', 77 | label: 'Linear out, slow in', 78 | androidRef: '@android:interpolator/linear_out_slow_in', 79 | interpolate: f => LINEAR_OUT_SLOW_IN_EASING(f) 80 | }, 81 | //BOUNCE: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/animation/BounceInterpolator.java 82 | //ANTICIPATE_OVERSHOOT: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/animation/AnticipateOvershootInterpolator.java 83 | //PATH: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/view/animation/PathInterpolator.java 84 | ]; 85 | 86 | /** 87 | * An animation block is an individual layer property tween (property animation). 88 | */ 89 | @Property.register([ 90 | new StubProperty('fromValue'), 91 | new StubProperty('toValue'), 92 | new NumberProperty('startTime', {min:0, integer:true}), 93 | new NumberProperty('endTime', {min:0, integer:true}), 94 | new EnumProperty('interpolator', ENUM_INTERPOLATOR_OPTIONS, {storeEntireOption:true}), 95 | ]) 96 | export class AnimationBlock { 97 | constructor(obj = {}) { 98 | this.layerId = obj.layerId || null; 99 | this.propertyName = obj.propertyName || null; 100 | let isPathData = (this.propertyName == 'pathData'); 101 | if ('fromValue' in obj) { 102 | this.fromValue = isPathData ? new SvgPathData(obj.fromValue) : obj.fromValue; 103 | } 104 | this.toValue = isPathData ? new SvgPathData(obj.toValue) : obj.toValue; 105 | this.startTime = obj.startTime || 0; 106 | this.endTime = obj.endTime || 0; 107 | if (this.startTime > this.endTime) { 108 | let tmp = this.endTime; 109 | this.endTime = this.startTime; 110 | this.startTime = tmp; 111 | } 112 | this.interpolator = obj.interpolator || 'ACCELERATE_DECELERATE'; 113 | } 114 | 115 | get typeString() { 116 | return 'block'; 117 | } 118 | 119 | get typeIdPrefix() { 120 | return 'block'; 121 | } 122 | 123 | get typeIcon() { 124 | return 'animation_block'; 125 | } 126 | 127 | toJSON() { 128 | return { 129 | layerId: this.layerId, 130 | propertyName: this.propertyName, 131 | fromValue: valueToJson_(this.fromValue), 132 | toValue: valueToJson_(this.toValue), 133 | startTime: this.startTime, 134 | endTime: this.endTime, 135 | interpolator: this.interpolator.value, 136 | }; 137 | } 138 | } 139 | 140 | function valueToJson_(val) { 141 | if (typeof val == 'object' && 'toJSON' in val) { 142 | return val.toJSON(); 143 | } 144 | 145 | return val; 146 | } 147 | -------------------------------------------------------------------------------- /app/scripts/model/Artwork.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, IdProperty, ColorProperty, NumberProperty, FractionProperty} from './properties'; 18 | import {BaseLayer} from './BaseLayer'; 19 | import {LayerGroup} from './LayerGroup'; 20 | 21 | /** 22 | * An artwork is the root layer group for a vector, defined mostly by 23 | * a width, height, and its children. 24 | */ 25 | @Property.register([ 26 | new IdProperty('id'), 27 | new ColorProperty('canvasColor'), 28 | new NumberProperty('width', {min:4, max:1024, integer:true}), 29 | new NumberProperty('height', {min:4, max:1024, integer:true}), 30 | new FractionProperty('alpha', {animatable: true}), 31 | ], {reset:true}) 32 | export class Artwork extends LayerGroup { 33 | constructor(obj = {}, opts = {}) { 34 | super(obj, opts); 35 | this.id = this.id || this.typeIdPrefix; 36 | this.canvasColor = obj.fillColor || null; 37 | this.width = obj.width || 100; 38 | this.height = obj.height || 100; 39 | this.alpha = obj.alpha || 1; 40 | } 41 | 42 | computeBounds() { 43 | return { l: 0, t: 0, r: this.width, b: this.height }; 44 | } 45 | 46 | get typeString() { 47 | return 'artwork'; 48 | } 49 | 50 | get typeIdPrefix() { 51 | return 'vector'; 52 | } 53 | 54 | get typeIcon() { 55 | return 'artwork'; 56 | } 57 | 58 | findLayerById(id) { 59 | if (this.id === id) { 60 | return this; 61 | } 62 | return super.findLayerById(id); 63 | } 64 | 65 | toJSON() { 66 | return { 67 | id: this.id, 68 | canvasColor: this.canvasColor, 69 | width: this.width, 70 | height: this.height, 71 | alpha: this.alpha, 72 | layers: this.layers.map(layer => layer.toJSON()) 73 | }; 74 | } 75 | } 76 | 77 | BaseLayer.LAYER_CLASSES_BY_TYPE['artwork'] = Artwork; 78 | -------------------------------------------------------------------------------- /app/scripts/model/BaseLayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, IdProperty} from './properties'; 18 | 19 | /** 20 | * Base class for any node in the tree, including path layers, layer groups, and artworks. 21 | */ 22 | @Property.register([ 23 | new IdProperty('id') 24 | ]) 25 | export class BaseLayer { 26 | constructor(obj = {}, opts = {}) { 27 | this.parent = null; 28 | this.id = obj.id || null; 29 | if (opts && opts.linkSelectedState) { 30 | this.selectedStateLinkedObj_ = obj; 31 | } 32 | 33 | // meta 34 | this.visible = ('visible' in obj) ? obj.visible : true; 35 | this.expanded = true; 36 | } 37 | 38 | get selected() { 39 | return this.selectedStateLinkedObj_ 40 | ? this.selectedStateLinkedObj_.selected_ 41 | : this.selected_; 42 | } 43 | 44 | computeBounds() { 45 | return null; 46 | } 47 | 48 | getSibling_(offs) { 49 | if (!this.parent || !this.parent.layers) { 50 | return null; 51 | } 52 | 53 | let index = this.parent.layers.indexOf(this); 54 | if (index < 0) { 55 | return null; 56 | } 57 | 58 | index += offs; 59 | if (index < 0 || index >= this.parent.layers.length) { 60 | return null; 61 | } 62 | 63 | return this.parent.layers[index]; 64 | } 65 | 66 | get previousSibling() { 67 | return this.getSibling_(-1); 68 | } 69 | 70 | get nextSibling() { 71 | return this.getSibling_(1); 72 | } 73 | 74 | remove() { 75 | if (!this.parent || !this.parent.layers) { 76 | return; 77 | } 78 | 79 | let index = this.parent.layers.indexOf(this); 80 | if (index >= 0) { 81 | this.parent.layers.splice(index, 1); 82 | } 83 | 84 | this.parent = null; 85 | } 86 | 87 | walk(fn, context) { 88 | let visit_ = (layer, context) => { 89 | let childContext = fn(layer, context); 90 | if (layer.layers) { 91 | walkLayerGroup_(layer, childContext); 92 | } 93 | }; 94 | 95 | let walkLayerGroup_ = (layerGroup, context) => { 96 | layerGroup.layers.forEach(layer => visit_(layer, context)); 97 | }; 98 | 99 | visit_(this, context); 100 | } 101 | 102 | toJSON() { 103 | return { 104 | id: this.id, 105 | type: this.typeString, 106 | visible: this.visible, 107 | }; 108 | } 109 | 110 | static load(obj = {}, opts) { 111 | if (obj instanceof BaseLayer) { 112 | return new obj.constructor(obj, opts); 113 | } 114 | 115 | return new BaseLayer.LAYER_CLASSES_BY_TYPE[obj.type || 'path'](obj, opts); 116 | } 117 | } 118 | 119 | // filled in by derived classes 120 | BaseLayer.LAYER_CLASSES_BY_TYPE = {}; 121 | -------------------------------------------------------------------------------- /app/scripts/model/LayerGroup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, NumberProperty} from './properties'; 18 | import {BaseLayer} from './BaseLayer'; 19 | 20 | /** 21 | * A group ('folder') containing other layers. 22 | */ 23 | @Property.register([ 24 | new NumberProperty('rotation', {animatable: true}), 25 | new NumberProperty('scaleX', {animatable: true}), 26 | new NumberProperty('scaleY', {animatable: true}), 27 | new NumberProperty('pivotX', {animatable: true}), 28 | new NumberProperty('pivotY', {animatable: true}), 29 | new NumberProperty('translateX', {animatable: true}), 30 | new NumberProperty('translateY', {animatable: true}), 31 | ]) 32 | export class LayerGroup extends BaseLayer { 33 | constructor(obj = {}, opts = {}) { 34 | super(obj, opts); 35 | this.layers = (obj.layers || []).map(obj => BaseLayer.load(obj, opts)); 36 | this.rotation = obj.rotation || 0; 37 | this.scaleX = ('scaleX' in obj) ? obj.scaleX : 1; 38 | this.scaleY = ('scaleY' in obj) ? obj.scaleY : 1; 39 | this.pivotX = obj.pivotX || 0; 40 | this.pivotY = obj.pivotY || 0; 41 | this.translateX = obj.translateX || 0; 42 | this.translateY = obj.translateY || 0; 43 | 44 | // meta 45 | this.expanded = ('expanded' in obj) ? obj.expanded : true; 46 | } 47 | 48 | computeBounds() { 49 | let bounds = null; 50 | this.layers.forEach(child => { 51 | let childBounds = child.computeBounds(); 52 | if (!childBounds) { 53 | return; 54 | } 55 | 56 | if (!bounds) { 57 | bounds = Object.assign({}, childBounds); 58 | } else { 59 | bounds.l = Math.min(childBounds.l, bounds.l); 60 | bounds.t = Math.min(childBounds.t, bounds.t); 61 | bounds.r = Math.max(childBounds.r, bounds.r); 62 | bounds.b = Math.max(childBounds.b, bounds.b); 63 | } 64 | }); 65 | return bounds; 66 | } 67 | 68 | get layers() { 69 | return this.layers_ || []; 70 | } 71 | 72 | set layers(layers) { 73 | this.layers_ = layers; 74 | this.layers_.forEach(layer => layer.parent = this); 75 | } 76 | 77 | get typeString() { 78 | return 'group'; 79 | } 80 | 81 | get typeIdPrefix() { 82 | return 'group'; 83 | } 84 | 85 | get typeIcon() { 86 | return 'layer_group'; 87 | } 88 | 89 | findLayerById(id) { 90 | for (let i = 0; i < this.layers.length; i++) { 91 | let layer = this.layers[i]; 92 | if (layer.id === id) { 93 | return layer; 94 | } else if (layer.findLayerById) { 95 | layer = layer.findLayerById(id); 96 | if (layer) { 97 | return layer; 98 | } 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | 105 | toJSON() { 106 | return Object.assign(super.toJSON(), { 107 | rotation: this.rotation, 108 | scaleX: this.scaleX, 109 | scaleY: this.scaleY, 110 | pivotX: this.pivotX, 111 | pivotY: this.pivotY, 112 | translateX: this.translateX, 113 | translateY: this.translateY, 114 | layers: this.layers.map(layer => layer.toJSON()), 115 | expanded: this.expanded, 116 | }); 117 | } 118 | } 119 | 120 | BaseLayer.LAYER_CLASSES_BY_TYPE['group'] = LayerGroup; 121 | -------------------------------------------------------------------------------- /app/scripts/model/MaskLayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, PathDataProperty} from './properties'; 18 | import {BaseLayer} from './BaseLayer'; 19 | 20 | /** 21 | * A mask layer (mask defined by a path) that clips/masks layers that follow it 22 | * within its layer group. 23 | */ 24 | @Property.register([ 25 | new PathDataProperty('pathData', {animatable: true}), 26 | ]) 27 | export class MaskLayer extends BaseLayer { 28 | constructor(obj = {}, opts = {}) { 29 | super(obj, opts); 30 | this.pathData = obj.pathData || ''; 31 | } 32 | 33 | computeBounds() { 34 | return Object.assign({}, (this.pathData && this.pathData.bounds) ? this.pathData.bounds : null); 35 | } 36 | 37 | get typeString() { 38 | return 'mask'; 39 | } 40 | 41 | get typeIdPrefix() { 42 | return 'mask'; 43 | } 44 | 45 | get typeIcon() { 46 | return 'mask_layer'; 47 | } 48 | 49 | toJSON() { 50 | return Object.assign(super.toJSON(), { 51 | pathData: this.pathData.pathString 52 | }); 53 | } 54 | } 55 | 56 | BaseLayer.LAYER_CLASSES_BY_TYPE['mask'] = MaskLayer; 57 | -------------------------------------------------------------------------------- /app/scripts/model/PathLayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property, PathDataProperty, NumberProperty, ColorProperty, 18 | FractionProperty, EnumProperty} from './properties'; 19 | import {BaseLayer} from './BaseLayer'; 20 | 21 | export const DefaultValues = { 22 | LINECAP: 'butt', 23 | LINEJOIN: 'miter', 24 | MITER_LIMIT: 4, 25 | }; 26 | 27 | const ENUM_LINECAP_OPTIONS = [ 28 | {value: 'butt', label: 'Butt'}, 29 | {value: 'square', label: 'Square'}, 30 | {value: 'round', label: 'Round'}, 31 | ]; 32 | 33 | const ENUM_LINEJOIN_OPTIONS = [ 34 | {value: 'miter', label: 'Miter'}, 35 | {value: 'round', label: 'Round'}, 36 | {value: 'bevel', label: 'Bevel'}, 37 | ]; 38 | 39 | /** 40 | * A path layer, which is the main building block for visible content in a vector 41 | * artwork. 42 | */ 43 | @Property.register([ 44 | new PathDataProperty('pathData', {animatable: true}), 45 | new ColorProperty('fillColor', {animatable: true}), 46 | new FractionProperty('fillAlpha', {animatable: true}), 47 | new ColorProperty('strokeColor', {animatable: true}), 48 | new FractionProperty('strokeAlpha', {animatable: true}), 49 | new NumberProperty('strokeWidth', {min:0, animatable: true}), 50 | new EnumProperty('strokeLinecap', ENUM_LINECAP_OPTIONS), 51 | new EnumProperty('strokeLinejoin', ENUM_LINEJOIN_OPTIONS), 52 | new NumberProperty('strokeMiterLimit', {min:1}), 53 | new FractionProperty('trimPathStart', {animatable: true}), 54 | new FractionProperty('trimPathEnd', {animatable: true}), 55 | new FractionProperty('trimPathOffset', {animatable: true}), 56 | ]) 57 | export class PathLayer extends BaseLayer { 58 | constructor(obj = {}, opts = {}) { 59 | super(obj, opts); 60 | this.pathData = obj.pathData || ''; 61 | this.fillColor = obj.fillColor || null; 62 | this.fillAlpha = ('fillAlpha' in obj) ? obj.fillAlpha : 1; 63 | this.strokeColor = obj.strokeColor || ''; 64 | this.strokeAlpha = ('strokeAlpha' in obj) ? obj.strokeAlpha : 1; 65 | this.strokeWidth = obj.strokeWidth || 0; 66 | this.strokeLinecap = obj.strokeLinecap || DefaultValues.LINECAP; 67 | this.strokeLinejoin = obj.strokeLinejoin || DefaultValues.LINEJOIN; 68 | this.strokeMiterLimit = obj.strokeMiterLimit || DefaultValues.MITER_LIMIT; 69 | this.trimPathStart = obj.trimPathStart || 0; 70 | this.trimPathEnd = ('trimPathEnd' in obj && typeof obj.trimPathEnd == 'number') 71 | ? obj.trimPathEnd : 1; 72 | this.trimPathOffset = obj.trimPathOffset || 0; 73 | } 74 | 75 | computeBounds() { 76 | return Object.assign({}, (this.pathData && this.pathData.bounds) ? this.pathData.bounds : null); 77 | } 78 | 79 | get typeString() { 80 | return 'path'; 81 | } 82 | 83 | get typeIdPrefix() { 84 | return 'path'; 85 | } 86 | 87 | get typeIcon() { 88 | return 'path_layer'; 89 | } 90 | 91 | toJSON() { 92 | return Object.assign(super.toJSON(), { 93 | pathData: this.pathData.pathString, 94 | fillColor: this.fillColor, 95 | fillAlpha: this.fillAlpha, 96 | strokeColor: this.strokeColor, 97 | strokeAlpha: this.strokeAlpha, 98 | strokeWidth: this.strokeWidth, 99 | strokeLinecap: this.strokeLinecap, 100 | strokeLinejoin: this.strokeLinejoin, 101 | strokeMiterLimit: this.strokeMiterLimit, 102 | trimPathStart: this.trimPathStart, 103 | trimPathEnd: this.trimPathEnd, 104 | trimPathOffset: this.trimPathOffset 105 | }); 106 | } 107 | } 108 | 109 | BaseLayer.LAYER_CLASSES_BY_TYPE['path'] = PathLayer; 110 | -------------------------------------------------------------------------------- /app/scripts/model/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export {Animation} from './Animation'; 18 | export {AnimationBlock} from './AnimationBlock'; 19 | export {Artwork} from './Artwork'; 20 | export {BaseLayer} from './BaseLayer'; 21 | export {LayerGroup} from './LayerGroup'; 22 | export {MaskLayer} from './MaskLayer'; 23 | export {PathLayer, DefaultValues} from './PathLayer'; 24 | -------------------------------------------------------------------------------- /app/scripts/model/properties/ColorProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {ColorUtil} from 'ColorUtil'; 18 | import {MathUtil} from 'MathUtil'; 19 | 20 | import {Property} from './Property'; 21 | 22 | export class ColorProperty extends Property { 23 | interpolateValue(start, end, f) { 24 | start = ColorUtil.parseAndroidColor(start); 25 | end = ColorUtil.parseAndroidColor(end); 26 | return ColorUtil.toAndroidString({ 27 | r: MathUtil.constrain(Math.round(Property.simpleInterpolate(start.r, end.r, f)), 0, 255), 28 | g: MathUtil.constrain(Math.round(Property.simpleInterpolate(start.g, end.g, f)), 0, 255), 29 | b: MathUtil.constrain(Math.round(Property.simpleInterpolate(start.b, end.b, f)), 0, 255), 30 | a: MathUtil.constrain(Math.round(Property.simpleInterpolate(start.a, end.a, f)), 0, 255) 31 | }); 32 | } 33 | 34 | trySetEditedValue(obj, propertyName, value) { 35 | if (!value) { 36 | obj[propertyName] = null; 37 | return; 38 | } 39 | 40 | let processedValue = ColorUtil.parseAndroidColor(value); 41 | if (!processedValue) { 42 | processedValue = ColorUtil.parseAndroidColor(ColorUtil.svgToAndroidColor(value)); 43 | } 44 | 45 | obj[propertyName] = ColorUtil.toAndroidString(processedValue); 46 | } 47 | 48 | get animatorValueType() { 49 | return 'colorType'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/scripts/model/properties/EnumProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property} from './Property'; 18 | 19 | export class EnumProperty extends Property { 20 | constructor(name, options, config = {}) { 21 | super(name, config); 22 | this.optionsByValue_ = {}; 23 | this.options_ = (options || []).map(option => { 24 | let newOption = {}; 25 | if (typeof option === 'string') { 26 | newOption = { 27 | value: option, 28 | label: option 29 | }; 30 | option = newOption; 31 | } 32 | 33 | if (!('label' in option)) { 34 | option.label = option.value; 35 | } 36 | 37 | this.optionsByValue_[option.value] = option; 38 | return option; 39 | }); 40 | 41 | config = config || {}; 42 | if (config.storeEntireOption) { 43 | this.storeEntireOption = config.storeEntireOption; 44 | } 45 | } 46 | 47 | getter_(obj, propertyName, value) { 48 | let backingPropertyName = `${propertyName}_`; 49 | return obj[backingPropertyName]; 50 | } 51 | 52 | setter_(obj, propertyName, value) { 53 | let backingPropertyName = `${propertyName}_`; 54 | 55 | obj[backingPropertyName] = this.storeEntireOption 56 | ? this.getOptionForValue_(value) 57 | : this.getOptionForValue_(value).value; 58 | } 59 | 60 | getOptionForValue_(value) { 61 | if (!value) { 62 | return null; 63 | } 64 | 65 | if (typeof value === 'string') { 66 | return this.optionsByValue_[value]; 67 | } else if ('value' in value) { 68 | return value; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | displayValueForValue(value) { 75 | if (!value) { 76 | return ''; 77 | } 78 | 79 | return this.getOptionForValue_(value).label; 80 | } 81 | 82 | get options() { 83 | return this.options_; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/scripts/model/properties/FractionProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {NumberProperty} from './NumberProperty'; 18 | 19 | export class FractionProperty extends NumberProperty { 20 | constructor(name, config = {}) { 21 | config.min = 0; 22 | config.max = 1; 23 | super(name, config); 24 | } 25 | 26 | get animatorValueType() { 27 | return 'floatType'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/scripts/model/properties/IdProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property} from './Property'; 18 | 19 | export class IdProperty extends Property { 20 | trySetEditedValue(obj, propertyName, value) { 21 | obj[propertyName] = IdProperty.sanitize(value); 22 | } 23 | 24 | static sanitize(value) { 25 | value = (value || '') 26 | .toLowerCase() 27 | .replace(/^\s+|\s+$/g, '') 28 | .replace(/[\s-]+/g, '_') 29 | .replace(/[^\w_]+/g, ''); 30 | return value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/scripts/model/properties/NumberProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property} from './Property'; 18 | 19 | export class NumberProperty extends Property { 20 | constructor(name, config = {}) { 21 | super(name, config); 22 | this.config = config; 23 | } 24 | 25 | trySetEditedValue(obj, propertyName, value) { 26 | value = parseFloat(value); 27 | if (!isNaN(value)) { 28 | if ('min' in this.config) { 29 | value = Math.max(this.config.min, value); 30 | } 31 | if ('max' in this.config) { 32 | value = Math.min(this.config.max, value); 33 | } 34 | if (this.config.integer) { 35 | value = Math.floor(value); 36 | } 37 | obj[propertyName] = value; 38 | } 39 | } 40 | 41 | displayValueForValue(value) { 42 | if (typeof value === 'number') { 43 | return (Number.isInteger(value) 44 | ? value.toString() 45 | : Number(value.toFixed(3)).toString()) 46 | .replace(/-/g, '\u2212'); 47 | } 48 | return value; 49 | } 50 | 51 | setter_(obj, propertyName, value) { 52 | if (typeof value === 'string') { 53 | value = Number(value); 54 | } 55 | 56 | if (typeof value === 'number') { 57 | if (!isNaN(value)) { 58 | if ('min' in this.config) { 59 | value = Math.max(this.config.min, value); 60 | } 61 | if ('max' in this.config) { 62 | value = Math.min(this.config.max, value); 63 | } 64 | if (this.config.integer) { 65 | value = Math.floor(value); 66 | } 67 | } 68 | } 69 | 70 | let backingPropertyName = `${propertyName}_`; 71 | obj[backingPropertyName] = value; 72 | } 73 | 74 | interpolateValue(start, end, f) { 75 | return Property.simpleInterpolate(start, end, f); 76 | } 77 | 78 | get animatorValueType() { 79 | return 'floatType'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/scripts/model/properties/PathDataProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {SvgPathData} from 'SvgPathData'; 18 | 19 | import {Property} from './Property'; 20 | 21 | export class PathDataProperty extends Property { 22 | interpolateValue(start, end, f) { 23 | return SvgPathData.interpolate(start, end, f); 24 | } 25 | 26 | displayValueForValue(val) { 27 | return val.pathString; 28 | } 29 | 30 | getEditableValue(obj, propertyName) { 31 | return obj[propertyName] ? obj[propertyName].pathString : ''; 32 | } 33 | 34 | trySetEditedValue(obj, propertyName, stringValue) { 35 | obj[propertyName] = new SvgPathData(stringValue); 36 | } 37 | 38 | getter_(obj, propertyName) { 39 | let backingPropertyName = `${propertyName}_`; 40 | return obj[backingPropertyName]; 41 | } 42 | 43 | setter_(obj, propertyName, value) { 44 | let backingPropertyName = `${propertyName}_`; 45 | let pathData; 46 | if (!value || value instanceof SvgPathData) { 47 | pathData = value; 48 | } else { 49 | pathData = new SvgPathData(value); 50 | } 51 | 52 | obj[backingPropertyName] = pathData; 53 | } 54 | 55 | cloneValue(val) { 56 | return JSON.parse(JSON.stringify(val)); 57 | } 58 | 59 | get animatorValueType() { 60 | return 'pathType'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/scripts/model/properties/Property.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class Property { 18 | constructor(name, config = {}) { 19 | this.name = name; 20 | this.config = config; 21 | this.animatable = config.animatable; 22 | this.inspectable = config.inspectable; 23 | } 24 | 25 | interpolateValue(start, end, f) { 26 | return start; 27 | } 28 | 29 | getEditableValue(obj, propertyName) { 30 | return obj[propertyName]; 31 | } 32 | 33 | trySetEditedValue(obj, propertyName, value) { 34 | obj[propertyName] = value; 35 | } 36 | 37 | getter_(obj, propertyName, value) { 38 | let backingPropertyName = `${propertyName}_`; 39 | return obj[backingPropertyName]; 40 | } 41 | 42 | setter_(obj, propertyName, value) { 43 | let backingPropertyName = `${propertyName}_`; 44 | obj[backingPropertyName] = value; 45 | } 46 | 47 | displayValueForValue(val) { 48 | return val; 49 | } 50 | 51 | cloneValue(val) { 52 | return val; 53 | } 54 | 55 | static simpleInterpolate(start, end, f) { 56 | return start + (end - start) * f; 57 | } 58 | 59 | static register(props, {reset = false} = {}) { 60 | return function(cls) { 61 | props.forEach(prop => { 62 | if (!(prop instanceof StubProperty)) { 63 | Object.defineProperty(cls.prototype, prop.name, { 64 | get() { 65 | return prop.getter_(this, prop.name); 66 | }, 67 | set(value) { 68 | prop.setter_(this, prop.name, value); 69 | } 70 | }); 71 | } 72 | }); 73 | 74 | let animatableProperties = {}; 75 | let inspectableProperties = {}; 76 | 77 | if (!reset) { 78 | Object.assign(animatableProperties, cls.prototype.animatableProperties); 79 | Object.assign(inspectableProperties, cls.prototype.inspectableProperties); 80 | } 81 | 82 | props.forEach(prop => { 83 | if (prop.animatable) { 84 | animatableProperties[prop.name] = prop; 85 | } 86 | 87 | if (!prop.inspectable) { 88 | inspectableProperties[prop.name] = prop; 89 | } 90 | }); 91 | 92 | Object.defineProperty(cls.prototype, 'animatableProperties', { 93 | get: () => Object.assign({}, animatableProperties) 94 | }); 95 | 96 | Object.defineProperty(cls.prototype, 'inspectableProperties', { 97 | get: () => Object.assign({}, inspectableProperties) 98 | }); 99 | }; 100 | } 101 | } 102 | 103 | export class StubProperty extends Property {} 104 | 105 | -------------------------------------------------------------------------------- /app/scripts/model/properties/StringProperty.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Property} from './Property'; 18 | 19 | export class StringProperty extends Property { 20 | } 21 | -------------------------------------------------------------------------------- /app/scripts/model/properties/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export {ColorProperty} from './ColorProperty'; 18 | export {EnumProperty} from './EnumProperty'; 19 | export {FractionProperty} from './FractionProperty'; 20 | export {IdProperty} from './IdProperty'; 21 | export {NumberProperty} from './NumberProperty'; 22 | export {PathDataProperty} from './PathDataProperty'; 23 | export {Property, StubProperty} from './Property'; 24 | export {StringProperty} from './StringProperty'; 25 | -------------------------------------------------------------------------------- /app/scripts/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports.routeConfig = function($locationProvider, $routeProvider) { 18 | $locationProvider.html5Mode(true); 19 | 20 | $routeProvider 21 | .otherwise({ 22 | templateUrl: 'pages/studio/studio.html' 23 | }); 24 | }; 25 | 26 | Object.assign(module.exports, { 27 | studio: () => `/` 28 | }); 29 | -------------------------------------------------------------------------------- /app/scripts/xmlserializer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Based on https://github.com/cburgmer/xmlserializer/blob/master/lib/serializer.js 18 | // Other options for pretty-printing: 19 | // - https://github.com/travisleithead/xmlserialization-polyfill 20 | // - https://github.com/prettydiff/prettydiff/blob/master/lib/markuppretty.js 21 | // - https://github.com/vkiryukhin/vkBeautify 22 | 23 | var removeInvalidCharacters = function (content) { 24 | // See http://www.w3.org/TR/xml/#NT-Char for valid XML 1.0 characters 25 | return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); 26 | }; 27 | 28 | var serializeAttributeValue = function (value) { 29 | return value 30 | .replace(/&/g, '&') 31 | .replace(//g, '>') 33 | .replace(/"/g, '"') 34 | .replace(/'/g, '''); 35 | }; 36 | 37 | var serializeTextContent = function (content) { 38 | return content 39 | .replace(/&/g, '&') 40 | .replace(//g, '>'); 42 | }; 43 | 44 | var serializeAttribute = function (attr) { 45 | var value = attr.value; 46 | 47 | return attr.name + '="' + serializeAttributeValue(value) + '"'; 48 | }; 49 | 50 | var getTagName = function (node) { 51 | var tagName = node.tagName; 52 | 53 | // Aid in serializing of original HTML documents 54 | if (node.namespaceURI === 'http://www.w3.org/1999/xhtml') { 55 | tagName = tagName.toLowerCase(); 56 | } 57 | return tagName; 58 | }; 59 | 60 | var serializeNamespace = function (node, options) { 61 | var nodeHasXmlnsAttr = Array.prototype.map.call(node.attributes || node.attrs, function (attr) { 62 | return attr.name; 63 | }) 64 | .indexOf('xmlns') >= 0; 65 | // Serialize the namespace as an xmlns attribute whenever the element 66 | // doesn't already have one and the inherited namespace does not match 67 | // the element's namespace. 68 | if (!nodeHasXmlnsAttr && node.namespaceURI && 69 | (options.isRootNode/* || 70 | node.namespaceURI !== node.parentNode.namespaceURI*/)) { 71 | return ' xmlns="' + node.namespaceURI + '"'; 72 | } else { 73 | return ''; 74 | } 75 | }; 76 | 77 | var serializeChildren = function (node, options) { 78 | return Array.prototype.map.call(node.childNodes, function (childNode) { 79 | return nodeTreeToXHTML(childNode, options); 80 | }).join(''); 81 | }; 82 | 83 | var serializeTag = function (node, options) { 84 | var output = ''; 85 | if (options.indent && options._indentLevel) { 86 | output += Array(options._indentLevel * options.indent + 1).join(' '); 87 | } 88 | output += '<' + getTagName(node); 89 | output += serializeNamespace(node, options.isRootNode); 90 | 91 | var attributes = node.attributes || node.attrs; 92 | Array.prototype.forEach.call(attributes, function (attr) { 93 | if (options.multiAttributeIndent && attributes.length > 1) { 94 | output += '\n'; 95 | output += Array((options._indentLevel || 0) * options.indent + options.multiAttributeIndent + 1).join(' '); 96 | } else { 97 | output += ' '; 98 | } 99 | output += serializeAttribute(attr); 100 | }); 101 | 102 | if (node.childNodes.length > 0) { 103 | output += '>'; 104 | if (options.indent) { 105 | output += '\n'; 106 | } 107 | options.isRootNode = false; 108 | options._indentLevel = (options._indentLevel || 0) + 1; 109 | output += serializeChildren(node, options); 110 | --options._indentLevel; 111 | if (options.indent && options._indentLevel) { 112 | output += Array(options._indentLevel * options.indent + 1).join(' '); 113 | } 114 | output += ''; 115 | } else { 116 | output += '/>'; 117 | } 118 | if (options.indent) { 119 | output += '\n'; 120 | } 121 | return output; 122 | }; 123 | 124 | var serializeText = function (node) { 125 | var text = node.nodeValue || node.value || ''; 126 | return serializeTextContent(text); 127 | }; 128 | 129 | var serializeComment = function (node) { 130 | return ''; 134 | }; 135 | 136 | var serializeCDATA = function (node) { 137 | return ''; 138 | }; 139 | 140 | var nodeTreeToXHTML = function (node, options) { 141 | if (node.nodeName === '#document' || 142 | node.nodeName === '#document-fragment') { 143 | return serializeChildren(node, options); 144 | } else { 145 | if (node.tagName) { 146 | return serializeTag(node, options); 147 | } else if (node.nodeName === '#text') { 148 | return serializeText(node); 149 | } else if (node.nodeName === '#comment') { 150 | return serializeComment(node); 151 | } else if (node.nodeName === '#cdata-section') { 152 | return serializeCDATA(node); 153 | } 154 | } 155 | }; 156 | 157 | exports.serializeToString = function (node, options) { 158 | options = options || {}; 159 | options.rootNode = true; 160 | return removeInvalidCharacters(nodeTreeToXHTML(node, options)); 161 | }; 162 | -------------------------------------------------------------------------------- /app/styles/angular-material-overrides.scss: -------------------------------------------------------------------------------- 1 | .md-button:not(.md-icon-button):not(.md-fab) { 2 | margin: 0; 3 | padding: 0 12px; 4 | } 5 | 6 | md-menu-content { 7 | padding: 4px 0; 8 | } 9 | 10 | md-menu-item { 11 | min-height: 32px; 12 | height: 32px; 13 | 14 | .md-button { 15 | line-height: 32px; 16 | min-height: 32px; 17 | font-size: 14px; 18 | } 19 | } 20 | 21 | md-icon { 22 | min-width: 0; 23 | min-height: 0; 24 | } 25 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | // libraries and such 2 | @import 'material-colors'; 3 | @import 'material-shadows'; 4 | @import 'material-icons'; 5 | @import 'angular-material-overrides'; 6 | 7 | // global styles 8 | @import 'variables'; 9 | 10 | // core page layouts and common stuff 11 | @import 'root'; 12 | 13 | // components 14 | @import '../components/**/*'; 15 | 16 | // pages 17 | @import '../pages/**/*'; 18 | -------------------------------------------------------------------------------- /app/styles/globals.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romannurik/AndroidIconAnimator/336e461c7d771ebf459c07620e0f5fe6f4e0f7f2/app/styles/globals.scss -------------------------------------------------------------------------------- /app/styles/material-colors.scss: -------------------------------------------------------------------------------- 1 | $material-colors: ( 2 | 'red': ( 3 | '50': #ffebee, 4 | '100': #ffcdd2, 5 | '200': #ef9a9a, 6 | '300': #e57373, 7 | '400': #ef5350, 8 | '500': #f44336, 9 | '600': #e53935, 10 | '700': #d32f2f, 11 | '800': #c62828, 12 | '900': #b71c1c, 13 | 'a100': #ff8a80, 14 | 'a200': #ff5252, 15 | 'a400': #ff1744, 16 | 'a700': #d50000 17 | ), 18 | 19 | 'pink': ( 20 | '50': #fce4ec, 21 | '100': #f8bbd0, 22 | '200': #f48fb1, 23 | '300': #f06292, 24 | '400': #ec407a, 25 | '500': #e91e63, 26 | '600': #d81b60, 27 | '700': #c2185b, 28 | '800': #ad1457, 29 | '900': #880e4f, 30 | 'a100': #ff80ab, 31 | 'a200': #ff4081, 32 | 'a400': #f50057, 33 | 'a700': #c51162 34 | ), 35 | 36 | 'purple': ( 37 | '50': #f3e5f5, 38 | '100': #e1bee7, 39 | '200': #ce93d8, 40 | '300': #ba68c8, 41 | '400': #ab47bc, 42 | '500': #9c27b0, 43 | '600': #8e24aa, 44 | '700': #7b1fa2, 45 | '800': #6a1b9a, 46 | '900': #4a148c, 47 | 'a100': #ea80fc, 48 | 'a200': #e040fb, 49 | 'a400': #d500f9, 50 | 'a700': #aa00ff 51 | ), 52 | 53 | 'deep-purple': ( 54 | '50': #ede7f6, 55 | '100': #d1c4e9, 56 | '200': #b39ddb, 57 | '300': #9575cd, 58 | '400': #7e57c2, 59 | '500': #673ab7, 60 | '600': #5e35b1, 61 | '700': #512da8, 62 | '800': #4527a0, 63 | '900': #311b92, 64 | 'a100': #b388ff, 65 | 'a200': #7c4dff, 66 | 'a400': #651fff, 67 | 'a700': #6200ea 68 | ), 69 | 70 | 'indigo': ( 71 | '50': #e8eaf6, 72 | '100': #c5cae9, 73 | '200': #9fa8da, 74 | '300': #7986cb, 75 | '400': #5c6bc0, 76 | '500': #3f51b5, 77 | '600': #3949ab, 78 | '700': #303f9f, 79 | '800': #283593, 80 | '900': #1a237e, 81 | 'a100': #8c9eff, 82 | 'a200': #536dfe, 83 | 'a400': #3d5afe, 84 | 'a700': #304ffe 85 | ), 86 | 87 | 'blue': ( 88 | '50': #e3f2fd, 89 | '100': #bbdefb, 90 | '200': #90caf9, 91 | '300': #64b5f6, 92 | '400': #42a5f5, 93 | '500': #2196f3, 94 | '600': #1e88e5, 95 | '700': #1976d2, 96 | '800': #1565c0, 97 | '900': #0d47a1, 98 | 'a100': #82b1ff, 99 | 'a200': #448aff, 100 | 'a400': #2979ff, 101 | 'a700': #2962ff 102 | ), 103 | 104 | 'light-blue': ( 105 | '50': #e1f5fe, 106 | '100': #b3e5fc, 107 | '200': #81d4fa, 108 | '300': #4fc3f7, 109 | '400': #29b6f6, 110 | '500': #03a9f4, 111 | '600': #039be5, 112 | '700': #0288d1, 113 | '800': #0277bd, 114 | '900': #01579b, 115 | 'a100': #80d8ff, 116 | 'a200': #40c4ff, 117 | 'a400': #00b0ff, 118 | 'a700': #0091ea 119 | ), 120 | 121 | 'cyan': ( 122 | '50': #e0f7fa, 123 | '100': #b2ebf2, 124 | '200': #80deea, 125 | '300': #4dd0e1, 126 | '400': #26c6da, 127 | '500': #00bcd4, 128 | '600': #00acc1, 129 | '700': #0097a7, 130 | '800': #00838f, 131 | '900': #006064, 132 | 'a100': #84ffff, 133 | 'a200': #18ffff, 134 | 'a400': #00e5ff, 135 | 'a700': #00b8d4 136 | ), 137 | 138 | 'teal': ( 139 | '50': #e0f2f1, 140 | '100': #b2dfdb, 141 | '200': #80cbc4, 142 | '300': #4db6ac, 143 | '400': #26a69a, 144 | '500': #009688, 145 | '600': #00897b, 146 | '700': #00796b, 147 | '800': #00695c, 148 | '900': #004d40, 149 | 'a100': #a7ffeb, 150 | 'a200': #64ffda, 151 | 'a400': #1de9b6, 152 | 'a700': #00bfa5 153 | ), 154 | 155 | 'green': ( 156 | '50': #e8f5e9, 157 | '100': #c8e6c9, 158 | '200': #a5d6a7, 159 | '300': #81c784, 160 | '400': #66bb6a, 161 | '500': #4caf50, 162 | '600': #43a047, 163 | '700': #388e3c, 164 | '800': #2e7d32, 165 | '900': #1b5e20, 166 | 'a100': #b9f6ca, 167 | 'a200': #69f0ae, 168 | 'a400': #00e676, 169 | 'a700': #00c853 170 | ), 171 | 172 | 'light-green': ( 173 | '50': #f1f8e9, 174 | '100': #dcedc8, 175 | '200': #c5e1a5, 176 | '300': #aed581, 177 | '400': #9ccc65, 178 | '500': #8bc34a, 179 | '600': #7cb342, 180 | '700': #689f38, 181 | '800': #558b2f, 182 | '900': #33691e, 183 | 'a100': #ccff90, 184 | 'a200': #b2ff59, 185 | 'a400': #76ff03, 186 | 'a700': #64dd17 187 | ), 188 | 189 | 'lime': ( 190 | '50': #f9fbe7, 191 | '100': #f0f4c3, 192 | '200': #e6ee9c, 193 | '300': #dce775, 194 | '400': #d4e157, 195 | '500': #cddc39, 196 | '600': #c0ca33, 197 | '700': #afb42b, 198 | '800': #9e9d24, 199 | '900': #827717, 200 | 'a100': #f4ff81, 201 | 'a200': #eeff41, 202 | 'a400': #c6ff00, 203 | 'a700': #aeea00 204 | ), 205 | 206 | 'yellow': ( 207 | '50': #fffde7, 208 | '100': #fff9c4, 209 | '200': #fff59d, 210 | '300': #fff176, 211 | '400': #ffee58, 212 | '500': #ffeb3b, 213 | '600': #fdd835, 214 | '700': #fbc02d, 215 | '800': #f9a825, 216 | '900': #f57f17, 217 | 'a100': #ffff8d, 218 | 'a200': #ffff00, 219 | 'a400': #ffea00, 220 | 'a700': #ffd600 221 | ), 222 | 223 | 'amber': ( 224 | '50': #fff8e1, 225 | '100': #ffecb3, 226 | '200': #ffe082, 227 | '300': #ffd54f, 228 | '400': #ffca28, 229 | '500': #ffc107, 230 | '600': #ffb300, 231 | '700': #ffa000, 232 | '800': #ff8f00, 233 | '900': #ff6f00, 234 | 'a100': #ffe57f, 235 | 'a200': #ffd740, 236 | 'a400': #ffc400, 237 | 'a700': #ffab00 238 | ), 239 | 240 | 'orange': ( 241 | '50': #fff3e0, 242 | '100': #ffe0b2, 243 | '200': #ffcc80, 244 | '300': #ffb74d, 245 | '400': #ffa726, 246 | '500': #ff9800, 247 | '600': #fb8c00, 248 | '700': #f57c00, 249 | '800': #ef6c00, 250 | '900': #e65100, 251 | 'a100': #ffd180, 252 | 'a200': #ffab40, 253 | 'a400': #ff9100, 254 | 'a700': #ff6d00 255 | ), 256 | 257 | 'deep-orange': ( 258 | '50': #fbe9e7, 259 | '100': #ffccbc, 260 | '200': #ffab91, 261 | '300': #ff8a65, 262 | '400': #ff7043, 263 | '500': #ff5722, 264 | '600': #f4511e, 265 | '700': #e64a19, 266 | '800': #d84315, 267 | '900': #bf360c, 268 | 'a100': #ff9e80, 269 | 'a200': #ff6e40, 270 | 'a400': #ff3d00, 271 | 'a700': #dd2c00 272 | ), 273 | 274 | 'brown': ( 275 | '50': #efebe9, 276 | '100': #d7ccc8, 277 | '200': #bcaaa4, 278 | '300': #a1887f, 279 | '400': #8d6e63, 280 | '500': #795548, 281 | '600': #6d4c41, 282 | '700': #5d4037, 283 | '800': #4e342e, 284 | '900': #3e2723 285 | ), 286 | 287 | 'grey': ( 288 | '50': #fafafa, 289 | '100': #f5f5f5, 290 | '200': #eeeeee, 291 | '300': #e0e0e0, 292 | '400': #bdbdbd, 293 | '500': #9e9e9e, 294 | '600': #757575, 295 | '700': #616161, 296 | '800': #424242, 297 | '900': #212121 298 | ), 299 | 300 | 'blue-grey': ( 301 | '50': #eceff1, 302 | '100': #cfd8dc, 303 | '200': #b0bec5, 304 | '300': #90a4ae, 305 | '400': #78909c, 306 | '500': #607d8b, 307 | '600': #546e7a, 308 | '700': #455a64, 309 | '800': #37474f, 310 | '900': #263238, 311 | '1000': #11171a 312 | ) 313 | ); 314 | 315 | @function material-color($color-name, $color-variant: '500') { 316 | $color: map-get(map-get($material-colors, $color-name), $color-variant); 317 | @if $color { 318 | @return $color; 319 | } @else { 320 | // Libsass still doesn't seem to support @error 321 | @warn '=> ERROR: COLOR NOT FOUND! <= | Your $color-name, $color-variant combination did not match any of the values in the $material-colors map.'; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /app/styles/material-icons.scss: -------------------------------------------------------------------------------- 1 | @mixin use-icon-font { 2 | font-weight: normal; 3 | font-style: normal; 4 | font-size: 24px; 5 | // Preferred icon size 6 | display: inline-block; 7 | width: 1em; 8 | height: 1em; 9 | line-height: 1; 10 | text-transform: none; 11 | letter-spacing: normal; 12 | word-wrap: normal; 13 | // Support for all WebKit browsers. 14 | -webkit-font-smoothing: antialiased; 15 | // Support for Safari and Chrome. 16 | text-rendering: optimizeLegibility; 17 | // Support for Firefox. 18 | -moz-osx-font-smoothing: grayscale; 19 | // Support for IE. 20 | -webkit-font-feature-settings: 'liga'; 21 | -moz-font-feature-settings: 'liga'; 22 | font-feature-settings: 'liga'; 23 | // Custom added for GMP 24 | user-select: none; 25 | } 26 | 27 | @mixin material-icons { 28 | @include use-icon-font; 29 | font-family: 'Material Icons'; 30 | } 31 | 32 | .material-icons { 33 | @include material-icons; 34 | } 35 | -------------------------------------------------------------------------------- /app/styles/material-shadows.scss: -------------------------------------------------------------------------------- 1 | // shadow values taken directly from angular material (as of v1.0.0-rc1) 2 | 3 | $shadow-key-umbra-opacity: .2; 4 | $shadow-key-penumbra-opacity: .14; 5 | $shadow-ambient-shadow-opacity: .12; 6 | 7 | $material-box-shadows: ( 8 | 1: (0 1px 3px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 1px 1px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 2px 1px -1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 9 | 2: (0 1px 5px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 2px 2px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 1px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 10 | 3: (0 1px 8px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 3px 4px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 3px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 11 | 4: (0 2px 4px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 4px 5px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 10px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 12 | 5: (0 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 5px 8px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 14px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 13 | 6: (0 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 6px 10px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 18px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 14 | 7: (0 4px 5px -2px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 7px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 2px 16px 1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 15 | 8: (0 5px 5px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 8px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 14px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 16 | 9: (0 5px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 9px 12px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 16px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 17 | 10: (0 6px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 10px 14px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 4px 18px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 18 | 11: (0 6px 7px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 11px 15px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 4px 20px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 19 | 12: (0 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 12px 17px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 22px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 20 | 13: (0 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 13px 19px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 24px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 21 | 14: (0 7px 9px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 14px 21px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 26px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 22 | 15: (0 8px 9px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 15px 22px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 28px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 23 | 16: (0 8px 10px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 16px 24px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 30px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 24 | 17: (0 8px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 17px 26px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 32px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 25 | 18: (0 9px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 18px 28px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 7px 34px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 26 | 19: (0 9px 12px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 19px 29px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 7px 36px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 27 | 20: (0 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 20px 31px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 38px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 28 | 21: (0 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 21px 33px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 40px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 29 | 22: (0 10px 14px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 22px 35px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 42px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 30 | 23: (0 11px 14px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 23px 36px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 9px 44px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 31 | 24: (0 11px 15px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 24px 38px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 9px 46px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)), 32 | ); 33 | 34 | @function material-shadow($z) { 35 | $shadow: map-get($material-box-shadows, $z); 36 | @if $shadow { 37 | @return $shadow; 38 | } @else { 39 | // Libsass still doesn't seem to support @error 40 | @warn '=> ERROR: NO SHADOW SPECIFIED AT GIVEN DEPTH.'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/styles/root.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | code { 11 | font-family: 'Roboto Mono', monospace; 12 | } 13 | 14 | // prevent CSS transitions from running before content is ready 15 | body.no-transitions * { 16 | transition: none !important; 17 | } 18 | 19 | ng-view, 20 | main { 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | color: $colorBlackTextPrimary; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // dimensions 2 | $mobileChromeBreakpoint: 600px; 3 | 4 | // borders and seams 5 | $thinBorderColor: rgba(#000, .12); 6 | $thinBorderColorLighter: rgba(#000, .06); 7 | $thinBorderColorDarker: rgba(#000, .2); 8 | $thinBorderColorWhite: rgba(#fff, .2); 9 | 10 | // colors 11 | $colorBlackTextPrimary: rgba(#000, .87); 12 | $colorBlackTextSecondary: rgba(#000, .54); 13 | $colorBlackTextDisabled: rgba(#000, .38); 14 | $colorBlackTextVeryLight: rgba(#000, .15); 15 | $colorWhiteTextPrimary: rgba(#fff, 1.0); 16 | $colorWhiteTextSecondary: rgba(#fff, .70); 17 | $colorWhiteTextDisabled: rgba(#fff, .5); 18 | 19 | $colorPrimary400: material-color('blue', '400'); 20 | $colorPrimary500: material-color('blue', '500'); 21 | $colorPrimary600: material-color('blue', '600'); 22 | $colorPrimary700: material-color('blue', '700'); 23 | 24 | $colorFocusBorder: material-color('blue', '400'); 25 | $colorSelection: material-color('blue', '700'); 26 | $colorSelectedAnimationBlock: material-color('blue', '500'); 27 | 28 | $colorError: material-color('red', 'a200'); 29 | $colorSuccess: material-color('teal', 'a700'); 30 | 31 | // animation timings 32 | $animTimeMedium: .3s; 33 | $animTimeFast: $animTimeMedium / 2; 34 | $animTimeVeryFast: $animTimeMedium / 3; 35 | $animTimeSlow: $animTimeMedium * 1.5; 36 | -------------------------------------------------------------------------------- /art/screencap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romannurik/AndroidIconAnimator/336e461c7d771ebf459c07620e0f5fe6f4e0f7f2/art/screencap.gif -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "android-icon-animator", 3 | "main": "android-icon-animator", 4 | "version": "0.0.1", 5 | "authors": [], 6 | "ignore": [ 7 | "**/.*", 8 | "node_modules", 9 | "bower_components", 10 | "test", 11 | "tests" 12 | ], 13 | "dependencies": { 14 | "angular": "^1.5.8", 15 | "angular-material": "~1.1", 16 | "angular-route": "^1.5.8", 17 | "jquery": "~2.1.4" 18 | }, 19 | "resolutions": { 20 | "angular": "1.5.8" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/back_simple.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "vector", 4 | "width": 24, 5 | "height": 24, 6 | "layers": [ 7 | { 8 | "id": "page_1", 9 | "type": "group", 10 | "rotation": 0, 11 | "scaleX": 1, 12 | "scaleY": 1, 13 | "pivotX": 0, 14 | "pivotY": 0, 15 | "translateX": 0, 16 | "translateY": 0, 17 | "layers": [ 18 | { 19 | "id": "artboard", 20 | "type": "group", 21 | "rotation": 0, 22 | "scaleX": 1, 23 | "scaleY": 1, 24 | "pivotX": 0, 25 | "pivotY": 0, 26 | "translateX": 0, 27 | "translateY": 0, 28 | "layers": [ 29 | { 30 | "id": "top_arrow", 31 | "type": "path", 32 | "pathData": "M4,12 L12,4", 33 | "fillColor": null, 34 | "fillAlpha": 1, 35 | "strokeColor": "#ff979797", 36 | "strokeAlpha": 1, 37 | "strokeWidth": 2, 38 | "strokeLinecap": "square", 39 | "strokeLinejoin": "miter", 40 | "strokeMiterLimit": 4, 41 | "trimPathStart": 0, 42 | "trimPathEnd": 0, 43 | "trimPathOffset": 0 44 | }, 45 | { 46 | "id": "bottom_arrow", 47 | "type": "path", 48 | "pathData": "M 4 12 L 12 20", 49 | "fillColor": null, 50 | "fillAlpha": 1, 51 | "strokeColor": "#ff979797", 52 | "strokeAlpha": 1, 53 | "strokeWidth": 2, 54 | "strokeLinecap": "square", 55 | "strokeLinejoin": "miter", 56 | "strokeMiterLimit": 4, 57 | "trimPathStart": 0, 58 | "trimPathEnd": 0, 59 | "trimPathOffset": 0 60 | }, 61 | { 62 | "id": "middle", 63 | "type": "path", 64 | "pathData": "M4.5,12 L20,12", 65 | "fillColor": null, 66 | "fillAlpha": 1, 67 | "strokeColor": "#ff979797", 68 | "strokeAlpha": 1, 69 | "strokeWidth": 2, 70 | "strokeLinecap": "butt", 71 | "strokeLinejoin": "miter", 72 | "strokeMiterLimit": 4, 73 | "trimPathStart": 0, 74 | "trimPathEnd": 1, 75 | "trimPathOffset": 0 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] 82 | }, 83 | "animations": [ 84 | { 85 | "id": "anim", 86 | "duration": 2000, 87 | "blocks": [ 88 | { 89 | "layerId": "top_arrow", 90 | "propertyName": "trimPathEnd", 91 | "fromValue": 0, 92 | "toValue": 1, 93 | "startTime": 525, 94 | "endTime": 1256, 95 | "interpolator": "FAST_OUT_SLOW_IN" 96 | }, 97 | { 98 | "layerId": "bottom_arrow", 99 | "propertyName": "trimPathEnd", 100 | "fromValue": 0, 101 | "toValue": 1, 102 | "startTime": 525, 103 | "endTime": 1256, 104 | "interpolator": "FAST_OUT_SLOW_IN" 105 | }, 106 | { 107 | "layerId": "middle", 108 | "propertyName": "trimPathStart", 109 | "fromValue": 1, 110 | "toValue": 0, 111 | "startTime": 0, 112 | "endTime": 550, 113 | "interpolator": "FAST_OUT_SLOW_IN" 114 | } 115 | ] 116 | } 117 | ] 118 | } -------------------------------------------------------------------------------- /examples/barchart_in.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "vector", 4 | "canvasColor": null, 5 | "width": 24, 6 | "height": 24, 7 | "layers": [ 8 | { 9 | "id": "middle", 10 | "type": "path", 11 | "pathData": "M 10,20 14,20 14,4 10,4 Z", 12 | "fillColor": "#ff000000", 13 | "fillAlpha": 1, 14 | "strokeColor": "", 15 | "strokeAlpha": 1, 16 | "strokeWidth": 1, 17 | "strokeLinecap": "butt", 18 | "strokeLinejoin": "miter", 19 | "strokeMiterLimit": 4, 20 | "trimPathStart": 0, 21 | "trimPathEnd": 1, 22 | "trimPathOffset": 0 23 | }, 24 | { 25 | "id": "left", 26 | "type": "path", 27 | "pathData": "M 4,20 8,20 8,20 4,20 Z", 28 | "fillColor": "#ff000000", 29 | "fillAlpha": 1, 30 | "strokeColor": "", 31 | "strokeAlpha": 1, 32 | "strokeWidth": 1, 33 | "strokeLinecap": "butt", 34 | "strokeLinejoin": "miter", 35 | "strokeMiterLimit": 4, 36 | "trimPathStart": 0, 37 | "trimPathEnd": 1, 38 | "trimPathOffset": 0 39 | }, 40 | { 41 | "id": "right", 42 | "type": "path", 43 | "pathData": "M 16,20 16,20 20,20 20,20 Z", 44 | "fillColor": "#ff000000", 45 | "fillAlpha": 1, 46 | "strokeColor": "", 47 | "strokeAlpha": 1, 48 | "strokeWidth": 1, 49 | "strokeLinecap": "butt", 50 | "strokeLinejoin": "miter", 51 | "strokeMiterLimit": 4, 52 | "trimPathStart": 0, 53 | "trimPathEnd": 1, 54 | "trimPathOffset": 0 55 | } 56 | ] 57 | }, 58 | "animations": [ 59 | { 60 | "id": "anim", 61 | "duration": 1000, 62 | "blocks": [ 63 | { 64 | "layerId": "middle", 65 | "propertyName": "pathData", 66 | "fromValue": "M 10,20 14,20 14,20 10,20 Z", 67 | "toValue": "M 10,20 14,20 14,4 10,4 Z", 68 | "startTime": 0, 69 | "endTime": 664, 70 | "interpolator": "FAST_OUT_SLOW_IN" 71 | }, 72 | { 73 | "layerId": "left", 74 | "propertyName": "pathData", 75 | "fromValue": "M 4,20 8,20 8,20 4,20 Z", 76 | "toValue": "M 4,20 8,20 8,12 4,12 Z", 77 | "startTime": 159, 78 | "endTime": 865, 79 | "interpolator": "FAST_OUT_SLOW_IN" 80 | }, 81 | { 82 | "layerId": "right", 83 | "propertyName": "pathData", 84 | "fromValue": "M 16,20 16,20 20,20 20,20 Z", 85 | "toValue": "M 16,9 16,20 20,20 20,9 Z", 86 | "startTime": 208, 87 | "endTime": 907, 88 | "interpolator": "FAST_OUT_SLOW_IN" 89 | } 90 | ] 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /examples/menu_to_back.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "menu_back", 4 | "canvasColor": null, 5 | "width": 24, 6 | "height": 24, 7 | "alpha": 1, 8 | "layers": [ 9 | { 10 | "id": "menu", 11 | "type": "group", 12 | "rotation": 0, 13 | "scaleX": 1, 14 | "scaleY": 1, 15 | "pivotX": 12, 16 | "pivotY": 12, 17 | "translateX": 0, 18 | "translateY": 0, 19 | "layers": [ 20 | { 21 | "id": "bottom_container", 22 | "type": "group", 23 | "rotation": 0, 24 | "scaleX": 1, 25 | "scaleY": 1, 26 | "pivotX": 20, 27 | "pivotY": 17, 28 | "translateX": 0, 29 | "translateY": 0, 30 | "layers": [ 31 | { 32 | "id": "bottom", 33 | "type": "path", 34 | "pathData": "M4,17 L20,17", 35 | "fillColor": null, 36 | "fillAlpha": 1, 37 | "strokeColor": "#000", 38 | "strokeAlpha": 1, 39 | "strokeWidth": 2, 40 | "strokeLinecap": "square", 41 | "strokeLinejoin": "miter", 42 | "strokeMiterLimit": 4, 43 | "trimPathStart": 0, 44 | "trimPathEnd": 1, 45 | "trimPathOffset": 0 46 | } 47 | ] 48 | }, 49 | { 50 | "id": "stem_container", 51 | "type": "group", 52 | "rotation": 0, 53 | "scaleX": 1, 54 | "scaleY": 1, 55 | "pivotX": 12, 56 | "pivotY": 12, 57 | "translateX": 0, 58 | "translateY": 0, 59 | "layers": [ 60 | { 61 | "id": "stem", 62 | "type": "path", 63 | "pathData": "M4,12 L20,12", 64 | "fillColor": null, 65 | "fillAlpha": 1, 66 | "strokeColor": "#000", 67 | "strokeAlpha": 1, 68 | "strokeWidth": 2, 69 | "strokeLinecap": "square", 70 | "strokeLinejoin": "miter", 71 | "strokeMiterLimit": 4, 72 | "trimPathStart": 0, 73 | "trimPathEnd": 1, 74 | "trimPathOffset": 0 75 | } 76 | ] 77 | }, 78 | { 79 | "id": "top_container", 80 | "type": "group", 81 | "rotation": 0, 82 | "scaleX": 1, 83 | "scaleY": 1, 84 | "pivotX": 20, 85 | "pivotY": 7, 86 | "translateX": 0, 87 | "translateY": 0, 88 | "layers": [ 89 | { 90 | "id": "top", 91 | "type": "path", 92 | "pathData": "M4,7 L20,7", 93 | "fillColor": null, 94 | "fillAlpha": 1, 95 | "strokeColor": "#000", 96 | "strokeAlpha": 1, 97 | "strokeWidth": 2, 98 | "strokeLinecap": "square", 99 | "strokeLinejoin": "miter", 100 | "strokeMiterLimit": 4, 101 | "trimPathStart": 0, 102 | "trimPathEnd": 1, 103 | "trimPathOffset": 0 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ] 110 | }, 111 | "animations": [ 112 | { 113 | "id": "menu_to_back", 114 | "duration": 1600, 115 | "blocks": [ 116 | { 117 | "layerId": "top_container", 118 | "propertyName": "translateX", 119 | "fromValue": 0, 120 | "toValue": -1.41421356, 121 | "startTime": 500, 122 | "endTime": 1200, 123 | "interpolator": "ACCELERATE_DECELERATE" 124 | }, 125 | { 126 | "layerId": "top_container", 127 | "propertyName": "translateY", 128 | "fromValue": 0, 129 | "toValue": 5, 130 | "startTime": 500, 131 | "endTime": 1200, 132 | "interpolator": "ACCELERATE_DECELERATE" 133 | }, 134 | { 135 | "layerId": "top_container", 136 | "propertyName": "rotation", 137 | "fromValue": 0, 138 | "toValue": 45, 139 | "startTime": 500, 140 | "endTime": 1200, 141 | "interpolator": "ACCELERATE_DECELERATE" 142 | }, 143 | { 144 | "layerId": "bottom_container", 145 | "propertyName": "translateX", 146 | "fromValue": 0, 147 | "toValue": -1.41421356, 148 | "startTime": 500, 149 | "endTime": 1200, 150 | "interpolator": "ACCELERATE_DECELERATE" 151 | }, 152 | { 153 | "layerId": "bottom_container", 154 | "propertyName": "translateY", 155 | "fromValue": 0, 156 | "toValue": -5, 157 | "startTime": 500, 158 | "endTime": 1200, 159 | "interpolator": "ACCELERATE_DECELERATE" 160 | }, 161 | { 162 | "layerId": "bottom_container", 163 | "propertyName": "rotation", 164 | "fromValue": 0, 165 | "toValue": -45, 166 | "startTime": 500, 167 | "endTime": 1200, 168 | "interpolator": "ACCELERATE_DECELERATE" 169 | }, 170 | { 171 | "layerId": "menu", 172 | "propertyName": "rotation", 173 | "fromValue": 0, 174 | "toValue": 180, 175 | "startTime": 500, 176 | "endTime": 1200, 177 | "interpolator": "ACCELERATE_DECELERATE" 178 | }, 179 | { 180 | "layerId": "bottom", 181 | "propertyName": "trimPathStart", 182 | "fromValue": 0, 183 | "toValue": 0.5, 184 | "startTime": 500, 185 | "endTime": 1200, 186 | "interpolator": "ACCELERATE_DECELERATE" 187 | }, 188 | { 189 | "layerId": "top", 190 | "propertyName": "trimPathStart", 191 | "fromValue": 0, 192 | "toValue": 0.5, 193 | "startTime": 500, 194 | "endTime": 1200, 195 | "interpolator": "ACCELERATE_DECELERATE" 196 | }, 197 | { 198 | "layerId": "stem", 199 | "propertyName": "trimPathEnd", 200 | "fromValue": 1, 201 | "toValue": 0.8, 202 | "startTime": 500, 203 | "endTime": 1200, 204 | "interpolator": "ACCELERATE_DECELERATE" 205 | }, 206 | { 207 | "layerId": "stem_container", 208 | "propertyName": "translateX", 209 | "fromValue": 0, 210 | "toValue": 1, 211 | "startTime": 500, 212 | "endTime": 1200, 213 | "interpolator": "ACCELERATE_DECELERATE" 214 | } 215 | ] 216 | } 217 | ] 218 | } -------------------------------------------------------------------------------- /examples/search_to_back.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "search_back", 4 | "width": 48, 5 | "height": 24, 6 | "layers": [ 7 | { 8 | "type": "group", 9 | "id": "menu", 10 | "pivotX": 12, 11 | "pivotY": 12, 12 | "layers": [ 13 | { 14 | "id": "circle", 15 | "strokeWidth": 2, 16 | "strokeColor": "#000", 17 | "pathData": "M25.39,13.39 C24.0002369,14.7797632 21.9746129,15.3225275 20.0761611,14.8138389 C18.1777093,14.3051503 16.6948497,12.8222907 16.1861611,10.9238389 C15.6774725,9.02538707 16.2202368,6.99976313 17.61,5.61 C19.7583877,3.46161232 23.2416123,3.46161232 25.39,5.61 C27.5383877,7.75838768 27.5383877,11.2416123 25.39,13.39" 18 | }, 19 | { 20 | "id": "stem", 21 | "strokeWidth": 2, 22 | "strokeColor": "#000", 23 | "pathData": "M24.7000008,12.6999998 C24.7000008,12.6999998 31.8173374,19.9066081 31.8173371,19.9066082 C32.7867437,20.7006357 34.4599991,23 37.5,23 C40.5400009,23 43,20.54 43,17.5 C43,14.46 40.5400009,12 37.5,12 L31.8173371,12 C31.8173374,12 18.8477173,12 17,12", 24 | "trimPathStart": 0, 25 | "trimPathEnd": 0.185 26 | }, 27 | { 28 | "id": "top_arrowhead", 29 | "strokeWidth": 2, 30 | "strokeColor": "#000", 31 | "pathData": "M16.7107986,11.2764828 L24.7221527,19.2878361", 32 | "trimPathStart": 0, 33 | "trimPathEnd": 0 34 | }, 35 | { 36 | "id": "bottom_arrowhead", 37 | "strokeWidth": 2, 38 | "strokeColor": "#000", 39 | "pathData": "M16.7017297,12.6957157 L24.7043962,4.69304955", 40 | "trimPathStart": 0, 41 | "trimPathEnd": 0 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | "animations": [ 48 | { 49 | "id": "search_to_back", 50 | "duration": 2500, 51 | "blocks": [ 52 | { 53 | "layerId": "circle", 54 | "propertyName": "trimPathEnd", 55 | "fromValue": 1, 56 | "toValue": 0, 57 | "startTime": 0, 58 | "endTime": 1000 59 | }, 60 | { 61 | "layerId": "stem", 62 | "propertyName": "trimPathEnd", 63 | "fromValue": 0.185, 64 | "toValue": 1, 65 | "startTime": 0, 66 | "endTime": 1500 67 | }, 68 | { 69 | "layerId": "stem", 70 | "propertyName": "trimPathStart", 71 | "fromValue": 0, 72 | "toValue": 0.75, 73 | "startTime": 1000, 74 | "endTime": 1700 75 | }, 76 | { 77 | "layerId": "top_arrowhead", 78 | "propertyName": "trimPathEnd", 79 | "fromValue": 0.2, 80 | "toValue": 1, 81 | "startTime": 1400, 82 | "endTime": 1900 83 | }, 84 | { 85 | "layerId": "bottom_arrowhead", 86 | "propertyName": "trimPathEnd", 87 | "fromValue": 0.2, 88 | "toValue": 1, 89 | "startTime": 1400, 90 | "endTime": 1900 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /examples/search_to_close.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "vector", 4 | "canvasColor": null, 5 | "width": 24, 6 | "height": 24, 7 | "alpha": 1, 8 | "layers": [ 9 | { 10 | "id": "oval_container", 11 | "type": "group", 12 | "rotation": 0, 13 | "scaleX": 1, 14 | "scaleY": 1, 15 | "pivotX": 0, 16 | "pivotY": 0, 17 | "translateX": 0, 18 | "translateY": 0, 19 | "layers": [ 20 | { 21 | "id": "oval", 22 | "type": "path", 23 | "pathData": "M 13.389 13.389 C 15.537 11.241 15.537 7.759 13.389 5.611 C 11.241 3.463 7.759 3.463 5.611 5.611 C 3.463 7.759 3.463 11.241 5.611 13.389 C 7.759 15.537 11.241 15.537 13.389 13.389 Z", 24 | "fillColor": null, 25 | "fillAlpha": 1, 26 | "strokeColor": "#000000", 27 | "strokeAlpha": 1, 28 | "strokeWidth": 1.8, 29 | "strokeLinecap": "butt", 30 | "strokeLinejoin": "miter", 31 | "strokeMiterLimit": 4, 32 | "trimPathStart": 0, 33 | "trimPathEnd": 1, 34 | "trimPathOffset": 0 35 | } 36 | ] 37 | }, 38 | { 39 | "id": "ne_stem", 40 | "type": "path", 41 | "pathData": "M 18 6 L 6 18", 42 | "fillColor": null, 43 | "fillAlpha": 1, 44 | "strokeColor": "#000000", 45 | "strokeAlpha": 1, 46 | "strokeWidth": 1.8, 47 | "strokeLinecap": "butt", 48 | "strokeLinejoin": "miter", 49 | "strokeMiterLimit": 4, 50 | "trimPathStart": 1, 51 | "trimPathEnd": 1, 52 | "trimPathOffset": 0 53 | }, 54 | { 55 | "id": "nw_stem", 56 | "type": "path", 57 | "pathData": "M 6 6 L 20 20", 58 | "fillColor": null, 59 | "fillAlpha": 1, 60 | "strokeColor": "#000000", 61 | "strokeAlpha": 1, 62 | "strokeWidth": 1.8, 63 | "strokeLinecap": "butt", 64 | "strokeLinejoin": "miter", 65 | "strokeMiterLimit": 4, 66 | "trimPathStart": 0.48, 67 | "trimPathEnd": 1, 68 | "trimPathOffset": 0 69 | } 70 | ] 71 | }, 72 | "animations": [ 73 | { 74 | "id": "anim", 75 | "duration": 1000, 76 | "blocks": [ 77 | { 78 | "layerId": "nw_stem", 79 | "propertyName": "trimPathStart", 80 | "fromValue": 0.48, 81 | "toValue": 0, 82 | "startTime": 300, 83 | "endTime": 800, 84 | "interpolator": "FAST_OUT_SLOW_IN" 85 | }, 86 | { 87 | "layerId": "nw_stem", 88 | "propertyName": "trimPathEnd", 89 | "fromValue": 1, 90 | "toValue": 0.86, 91 | "startTime": 300, 92 | "endTime": 800, 93 | "interpolator": "FAST_OUT_SLOW_IN" 94 | }, 95 | { 96 | "layerId": "ne_stem", 97 | "propertyName": "trimPathStart", 98 | "fromValue": 1, 99 | "toValue": 0, 100 | "startTime": 522, 101 | "endTime": 836, 102 | "interpolator": "FAST_OUT_SLOW_IN" 103 | }, 104 | { 105 | "layerId": "oval", 106 | "propertyName": "trimPathStart", 107 | "fromValue": 0, 108 | "toValue": 1, 109 | "startTime": 134, 110 | "endTime": 550, 111 | "interpolator": "ACCELERATE_DECELERATE" 112 | }, 113 | { 114 | "layerId": "oval_container", 115 | "propertyName": "translateX", 116 | "fromValue": 0, 117 | "toValue": -6.7, 118 | "startTime": 300, 119 | "endTime": 800, 120 | "interpolator": "FAST_OUT_SLOW_IN" 121 | }, 122 | { 123 | "layerId": "oval_container", 124 | "propertyName": "translateY", 125 | "fromValue": 0, 126 | "toValue": -6.7, 127 | "startTime": 300, 128 | "endTime": 800, 129 | "interpolator": "FAST_OUT_SLOW_IN" 130 | } 131 | ] 132 | } 133 | ] 134 | } -------------------------------------------------------------------------------- /examples/simple_path_morph.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "pathmorphtest", 4 | "width": 24, 5 | "height": 24, 6 | "layers": [ 7 | { 8 | "id": "foo", 9 | "strokeColor": "#f00", 10 | "strokeWidth": 2, 11 | "strokeLinecap": "round", 12 | "pathData": "M16,3 C7,16 30,20 8,22" 13 | } 14 | ] 15 | }, 16 | "animations": [ 17 | { 18 | "id": "morph_test", 19 | "duration": 1000, 20 | "blocks": [ 21 | { 22 | "layerId": "foo", 23 | "propertyName": "pathData", 24 | "startTime": 0, 25 | "endTime": 1000, 26 | "fromValue": "M16,3 C7,16 30,20 8,22", 27 | "toValue": "M3,5 C12.3026934,-3 30,8 17,21" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /examples/visibility_strike.iconanim: -------------------------------------------------------------------------------- 1 | { 2 | "artwork": { 3 | "id": "vector", 4 | "canvasColor": null, 5 | "width": 24, 6 | "height": 24, 7 | "alpha": 1, 8 | "layers": [ 9 | { 10 | "id": "page_1", 11 | "type": "group", 12 | "rotation": 0, 13 | "scaleX": 1, 14 | "scaleY": 1, 15 | "pivotX": 0, 16 | "pivotY": 0, 17 | "translateX": 0, 18 | "translateY": 0, 19 | "layers": [ 20 | { 21 | "id": "artboard", 22 | "type": "group", 23 | "rotation": 0, 24 | "scaleX": 1, 25 | "scaleY": 1, 26 | "pivotX": 0, 27 | "pivotY": 0, 28 | "translateX": 0, 29 | "translateY": 0, 30 | "layers": [ 31 | { 32 | "id": "strike", 33 | "type": "path", 34 | "pathData": "M 2,4.27 3.27,3 3.27,3 2,4.27 Z", 35 | "fillColor": "#ff000000", 36 | "fillAlpha": 1, 37 | "strokeColor": "", 38 | "strokeAlpha": 1, 39 | "strokeWidth": 1, 40 | "strokeLinecap": "butt", 41 | "strokeLinejoin": "miter", 42 | "strokeMiterLimit": 4, 43 | "trimPathStart": 0, 44 | "trimPathEnd": 1, 45 | "trimPathOffset": 0 46 | }, 47 | { 48 | "id": "strike_negative", 49 | "type": "mask", 50 | "pathData": "M0 0 L24,0 L24,24 L0,24 L0,0 Z M4.54,1.73 L3.27,3 L3.27,3 L4.54,1.73 Z" 51 | }, 52 | { 53 | "id": "shape", 54 | "type": "path", 55 | "pathData": "M12,4.5 C7,4.5 2.73,7.61 1,12 C2.73,16.39 7,19.5 12,19.5 C17,19.5 21.27,16.39 23,12 C21.27,7.61 17,4.5 12,4.5 L12,4.5 Z M12,17 C9.24,17 7,14.76 7,12 C7,9.24 9.24,7 12,7 C14.76,7 17,9.24 17,12 C17,14.76 14.76,17 12,17 L12,17 Z M12,9 C10.34,9 9,10.34 9,12 C9,13.66 10.34,15 12,15 C13.66,15 15,13.66 15,12 C15,10.34 13.66,9 12,9 L12,9 Z", 56 | "fillColor": "#ff000000", 57 | "fillAlpha": 1, 58 | "strokeColor": "", 59 | "strokeAlpha": 1, 60 | "strokeWidth": 1, 61 | "strokeLinecap": "butt", 62 | "strokeLinejoin": "miter", 63 | "strokeMiterLimit": 4, 64 | "trimPathStart": 0, 65 | "trimPathEnd": 1, 66 | "trimPathOffset": 0 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | }, 74 | "animations": [ 75 | { 76 | "id": "anim", 77 | "duration": 2000, 78 | "blocks": [ 79 | { 80 | "layerId": "strike", 81 | "propertyName": "pathData", 82 | "fromValue": "M 2,4.27 3.27,3 3.27,3 2,4.27 Z", 83 | "toValue": "M 19.73,22 21,20.73 3.27,3 2,4.27 Z", 84 | "startTime": 268, 85 | "endTime": 1539, 86 | "interpolator": "ACCELERATE_DECELERATE" 87 | }, 88 | { 89 | "layerId": "strike_negative", 90 | "propertyName": "pathData", 91 | "fromValue": "M0 0 L24,0 L24,24 L0,24 L0,0 Z M4.54,1.73 L3.27,3 L3.27,3 L4.54,1.73 Z", 92 | "toValue": "M0 0 L24,0 L24,24 L0,24 L0,0 Z M4.54,1.73 L3.27,3 L21,20.73 L22.27,19.46 Z", 93 | "startTime": 268, 94 | "endTime": 1539, 95 | "interpolator": "ACCELERATE_DECELERATE" 96 | }, 97 | { 98 | "layerId": "vector", 99 | "propertyName": "alpha", 100 | "fromValue": 1, 101 | "toValue": 0.333, 102 | "startTime": 268, 103 | "endTime": 1539, 104 | "interpolator": "ACCELERATE_DECELERATE" 105 | } 106 | ] 107 | } 108 | ] 109 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | // Include Gulp & Tools We'll Use 20 | var gulp = require('gulp'); 21 | var $ = require('gulp-load-plugins')(); 22 | var del = require('del'); 23 | var fs = require('fs'); 24 | var source = require('vinyl-source-stream'); 25 | var buffer = require('vinyl-buffer'); 26 | var runSequence = require('run-sequence'); 27 | var browserSync = require('browser-sync'); 28 | var browserify = require('browserify'); 29 | var exclude = require('gulp-ignore').exclude; 30 | var reload = browserSync.reload; 31 | var history = require('connect-history-api-fallback'); 32 | var merge = require('merge-stream'); 33 | 34 | 35 | var AUTOPREFIXER_BROWSERS = [ 36 | 'ie >= 10', 37 | 'ie_mob >= 10', 38 | 'ff >= 30', 39 | 'chrome >= 34', 40 | 'safari >= 7', 41 | 'ios >= 7', 42 | 'android >= 4.4' 43 | ]; 44 | 45 | 46 | var DEV_MODE = false; 47 | var BASE_HREF = '/AndroidIconAnimator/'; 48 | 49 | 50 | function errorHandler(error) { 51 | console.error(error.stack); 52 | this.emit('end'); // http://stackoverflow.com/questions/23971388 53 | } 54 | 55 | // Lint JavaScript 56 | gulp.task('scripts', function () { 57 | return browserify('./app/scripts/app.js', { 58 | debug: true, // debug generates sourcemap 59 | basedir: '.', 60 | paths: [ 61 | './app/scripts/', 62 | './node_modules/' 63 | ] 64 | }) 65 | .transform('babelify', { 66 | presets: ['es2015'], 67 | plugins: ['transform-decorators-legacy'] 68 | }) 69 | .transform('require-globify') 70 | .bundle() 71 | .on('error', errorHandler) 72 | .pipe(source('app.js')) 73 | .pipe(buffer()) 74 | .pipe(gulp.dest('.tmp/scripts')) 75 | .pipe($.if(!DEV_MODE, $.uglify({ 76 | mangle:false 77 | }))) 78 | .pipe(gulp.dest('dist/scripts')); 79 | }); 80 | 81 | // Bower 82 | gulp.task('bower', function(cb) { 83 | return $.bower('.tmp/lib') 84 | .pipe(exclude('!**/*.{js,css,map}')) 85 | .pipe(exclude('**/test/**')) 86 | .pipe(exclude('**/tests/**')) 87 | .pipe(exclude('**/modules/**')) 88 | .pipe(exclude('**/demos/**')) 89 | .pipe(exclude('**/src/**')) 90 | .pipe(gulp.dest('dist/lib')); 91 | }); 92 | 93 | // Optimize Images 94 | gulp.task('images', function () { 95 | return gulp.src('app/images/**/*') 96 | .pipe($.cache($.imagemin({ 97 | progressive: true, 98 | interlaced: true 99 | }))) 100 | .pipe(gulp.dest('dist/images')) 101 | .pipe($.size({title: 'images'})); 102 | }); 103 | 104 | // Generate icon set 105 | gulp.task('icons', function () { 106 | return gulp.src('app/icons/**/*.svg') 107 | .pipe($.cache($.imagemin({ 108 | progressive: true, 109 | interlaced: true 110 | }))) 111 | .pipe($.svgNgmaterial({filename: 'icons.svg'})) 112 | .pipe(gulp.dest('dist/images')) 113 | .pipe(gulp.dest('.tmp/images')) 114 | .pipe($.size({title: 'icons'})); 115 | }); 116 | 117 | // Copy All Files At The Root Level (app) and lib 118 | gulp.task('copy', function () { 119 | var s1 = gulp.src([ 120 | 'app/*', 121 | '!app/icons', 122 | '!app/*.html' 123 | ], { 124 | dot: true 125 | }).pipe(gulp.dest('dist')) 126 | .pipe($.size({title: 'copy'})); 127 | 128 | var s2 = gulp.src('app/assets/**/*') 129 | .pipe(gulp.dest('dist/assets')) 130 | .pipe($.size({title: 'assets'})); 131 | 132 | return merge(s1, s2); 133 | }); 134 | 135 | // Libs 136 | gulp.task('lib', function () { 137 | return gulp.src(['app/lib/**/*'], {dot: true}) 138 | .pipe(gulp.dest('dist/lib')) 139 | .pipe($.size({title: 'lib'})); 140 | }); 141 | 142 | // Compile and Automatically Prefix Stylesheets 143 | gulp.task('styles', function () { 144 | // For best performance, don't add Sass partials to `gulp.src` 145 | return gulp.src('app/styles/app.scss') 146 | .pipe($.changed('styles', {extension: '.scss'})) 147 | .pipe($.sassGlob()) 148 | .pipe($.sass({ 149 | style: 'expanded', 150 | precision: 10, 151 | quiet: true 152 | }).on('error', errorHandler)) 153 | .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) 154 | .pipe(gulp.dest('.tmp/styles')) 155 | // Concatenate And Minify Styles 156 | .pipe($.if(!DEV_MODE, $.if('*.css', $.csso()))) 157 | .pipe(gulp.dest('dist/styles')) 158 | .pipe($.size({title: 'styles'})); 159 | }); 160 | 161 | 162 | function currentVersionInfo() { 163 | return new Promise((resolve, reject) => { 164 | if (DEV_MODE) { 165 | resolve({version: 'DEV_BUILD'}); 166 | } else { 167 | $.git.revParse({args: '--short HEAD'}, (err, hash) => { 168 | $.git.exec({args: 'describe --tags'}, (err, tag) => { 169 | tag = tag.replace(/\s/g, ''); 170 | resolve({version: `${tag} (build ${hash})`}); 171 | }); 172 | }); 173 | } 174 | }); 175 | } 176 | 177 | 178 | gulp.task('html', function() { 179 | return currentVersionInfo().then((versionInfo) => 180 | gulp.src('app/**/*.html') 181 | .pipe($.replace(/%%BASE_HREF%%/g, BASE_HREF)) 182 | .pipe($.replace(/%%VERSION%%/g, versionInfo.version)) 183 | .pipe(gulp.dest('.tmp')) 184 | .pipe($.if('*.html', $.minifyHtml({empty:true}))) 185 | .pipe(gulp.dest('dist')) 186 | .pipe($.size({title: 'html'}))); 187 | }); 188 | 189 | // Clean Output Directory 190 | gulp.task('clean', function(cb) { 191 | del.sync(['.tmp', 'dist']); 192 | $.cache.clearAll(); 193 | cb(); 194 | }); 195 | 196 | // Watch Files For Changes & Reload 197 | gulp.task('serve', function (cb) { 198 | DEV_MODE = true; 199 | BASE_HREF = '/'; 200 | runSequence('__serve__', cb); 201 | }); 202 | 203 | gulp.task('__serve__', ['styles', 'scripts', 'icons', 'bower', 'html'], function () { 204 | browserSync({ 205 | notify: false, 206 | // Run as an https by uncommenting 'https: true' 207 | // Note: this uses an unsigned certificate which on first access 208 | // will present a certificate warning in the browser. 209 | // https: true, 210 | server: { 211 | baseDir: ['.tmp', 'app'], 212 | routes: { 213 | '/_sandbox': '_sandbox' 214 | }, 215 | middleware: [history()] 216 | } 217 | }); 218 | 219 | gulp.watch(['app/**/*.html'], ['html', reload]); 220 | gulp.watch(['app/**/*.{scss,css}'], ['styles', reload]); 221 | gulp.watch(['app/**/*.js'], ['scripts', reload]); 222 | gulp.watch(['app/images/**/*'], reload); 223 | gulp.watch(['app/icons/**/*'], ['icons', reload]); 224 | gulp.watch(['app/assets/**/*'], reload); 225 | }); 226 | 227 | // Build and serve the output from the dist build 228 | gulp.task('serve:dist', ['default'], function () { 229 | browserSync({ 230 | notify: false, 231 | // Run as an https by uncommenting 'https: true' 232 | // Note: this uses an unsigned certificate which on first access 233 | // will present a certificate warning in the browser. 234 | // https: true, 235 | server: 'dist' 236 | }); 237 | }); 238 | 239 | // Build Production Files, the Default Task 240 | gulp.task('default', ['clean', 'test'], function (cb) { 241 | runSequence('styles', 242 | ['scripts', 'bower', 'html', 'images', 'icons', 'lib', 'copy'], 243 | cb); 244 | }); 245 | 246 | // Tests 247 | gulp.task('test', function (cb) { 248 | return gulp.src(['test/**/*.js'], {read: false}) 249 | .pipe($.mocha({ 250 | reporter: 'nyan', 251 | require: ['babel-register'], 252 | })); 253 | }); 254 | 255 | // Deploy to GitHub pages 256 | gulp.task('deploy', function() { 257 | return gulp.src('dist/**/*', {dot: true}) 258 | .pipe($.ghPages()); 259 | }); 260 | 261 | // Load custom tasks from the `tasks` directory 262 | try { require('require-dir')('tasks'); } catch (err) {} 263 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 4 | "babel-preset-es2015": "^6.3.13", 5 | "babel-register": "^6.18.0", 6 | "babelify": "^7.2.0", 7 | "browser-sync": "^2.12.8", 8 | "browserify": "^14.4.0", 9 | "connect-history-api-fallback": "^1.1.0", 10 | "del": "^3.0.0", 11 | "globby": "^6.1.0", 12 | "gulp": "^3.9.1", 13 | "gulp-autoprefixer": "^4.0.0", 14 | "gulp-bower": "0.0.13", 15 | "gulp-cache": "^0.4.5", 16 | "gulp-changed": "^3.1.0", 17 | "gulp-csso": "^3.0.0", 18 | "gulp-gh-pages": "^0.5.4", 19 | "gulp-git": "^2.4.0", 20 | "gulp-if": "^2.0.1", 21 | "gulp-ignore": "^2.0.1", 22 | "gulp-imagemin": "^3.0.1", 23 | "gulp-load-plugins": "^1.2.3", 24 | "gulp-minify-html": "^1.0.6", 25 | "gulp-mocha": "^4.3.1", 26 | "gulp-replace": "^0.5.4", 27 | "gulp-sass": "^3.0.0", 28 | "gulp-sass-glob": "^1.0.6", 29 | "gulp-size": "^2.1.0", 30 | "gulp-svg-ngmaterial": "^2.0.2", 31 | "gulp-uglify": "^3.0.0", 32 | "jquery": "^3.1.1", 33 | "merge-stream": "^1.0.0", 34 | "mocha": "^3.2.0", 35 | "opn": "^5.1.0", 36 | "require-dir": "^0.3.0", 37 | "require-globify": "^1.3.0", 38 | "run-sequence": "^1.2.0", 39 | "through2": "^2.0.1", 40 | "vinyl-buffer": "^1.0.0", 41 | "vinyl-source-stream": "^1.1.0" 42 | }, 43 | "engines": { 44 | "node": ">=0.10.0" 45 | }, 46 | "private": true, 47 | "scripts": { 48 | "test": "gulp test", 49 | "postinstall": "bower install" 50 | }, 51 | "dependencies": { 52 | "bezier-easing": "^2.0.3", 53 | "bezier-js": "^2.0.1", 54 | "tinycolor2": "^1.4.1", 55 | "zipjs-browserify": "^1.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/test-colorutil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import assert from 'assert'; 18 | import {ColorUtil} from '../app/scripts/ColorUtil'; 19 | 20 | describe('ColorUtil', () => { 21 | let TESTS_ANDROID_RAW = [ 22 | ['#f000', {r:0,g:0,b:0,a:255}, '#000000'], 23 | ['f00', {r:255,g:0,b:0,a:255}, '#ff0000'], 24 | ['#7f00ff00', {r:0,g:255,b:0,a:127}, '#7f00ff00'], 25 | ['an invalid color', null], 26 | ]; 27 | 28 | let TESTS_ANDROID_CSS = [ 29 | ['#f000', 'rgba(0,0,0,1.00)', '#000000'], 30 | ['f00', 'rgba(255,0,0,1.00)', '#ff0000'], 31 | ['#7f00ff00', 'rgba(0,255,0,0.50)', '#8000ff00'], 32 | ['', 'transparent', '#00000000'], 33 | ]; 34 | 35 | describe('#parseAndroidColor', () => { 36 | TESTS_ANDROID_RAW.forEach(a => { 37 | it(`parsing '${a[0]}' yields ${JSON.stringify(a[1])}`, () => 38 | assert.deepEqual(a[1], ColorUtil.parseAndroidColor(a[0]))); 39 | }); 40 | }); 41 | 42 | describe('#toAndroidString', () => { 43 | TESTS_ANDROID_RAW.forEach(a => { 44 | if (a[1]) { 45 | it(`converting ${JSON.stringify(a[1])} to string yields '${a[2]}'`, () => { 46 | assert.deepEqual(a[2], ColorUtil.toAndroidString(a[1])); 47 | }); 48 | } 49 | }); 50 | }); 51 | 52 | describe('#androidToCssColor', () => { 53 | TESTS_ANDROID_CSS.forEach(a => { 54 | it(`converting '${a[0]}' to CSS color yields '${a[1]}'`, () => 55 | assert.equal(a[1], ColorUtil.androidToCssColor(a[0]))); 56 | }); 57 | }); 58 | 59 | describe('#svgToAndroidColor', () => { 60 | TESTS_ANDROID_CSS.forEach(a => { 61 | it(`converting '${a[1]}' to Android color yields '${a[2]}'`, () => 62 | assert.equal(a[2], ColorUtil.svgToAndroidColor(a[1]))); 63 | }); 64 | }); 65 | }); 66 | --------------------------------------------------------------------------------