├── .++eslintrc ├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitignore ├── FUNDING.yml ├── LICENSE.md ├── README.md ├── app ├── build │ └── mojs-timeline-editor.js ├── css │ ├── assets │ │ ├── colors.postcss.css │ │ ├── globals.postcss.css │ │ └── mixins.postcss.css │ └── blocks │ │ ├── body-panel.postcss.css │ │ ├── body-panel.postcss.css.json │ │ ├── curve-editor.postcss.css │ │ ├── curve-editor.postcss.css.json │ │ ├── easing.postcss.css │ │ ├── easing.postcss.css.json │ │ ├── hide-button.postcss.css │ │ ├── hide-button.postcss.css.json │ │ ├── icon.postcss.css │ │ ├── icon.postcss.css.json │ │ ├── insert-point.postcss.css │ │ ├── insert-point.postcss.css.json │ │ ├── left-panel.postcss.css │ │ ├── left-panel.postcss.css.json │ │ ├── main-panel.postcss.css │ │ ├── main-panel.postcss.css.json │ │ ├── point-line.postcss.css │ │ ├── point-line.postcss.css.json │ │ ├── point-timeline-line.postcss.css │ │ ├── point-timeline-line.postcss.css.json │ │ ├── point.postcss.css │ │ ├── point.postcss.css.json │ │ ├── points-panel.postcss.css │ │ ├── points-panel.postcss.css.json │ │ ├── property-line-add.postcss.css │ │ ├── property-line-add.postcss.css.json │ │ ├── property-line.postcss.css │ │ ├── property-line.postcss.css.json │ │ ├── resize-handle.postcss.css │ │ ├── resize-handle.postcss.css.json │ │ ├── right-panel.postcss.css │ │ ├── right-panel.postcss.css.json │ │ ├── segment-timeline.postcss.css │ │ ├── segment-timeline.postcss.css.json │ │ ├── spot.postcss.css │ │ ├── spot.postcss.css.json │ │ ├── timeline-editor.postcss.css │ │ ├── timeline-editor.postcss.css.json │ │ ├── timeline-handle.postcss.css │ │ ├── timeline-handle.postcss.css.json │ │ ├── timeline-panel.postcss.css │ │ ├── timeline-panel.postcss.css.json │ │ ├── timelines-panel.postcss.css │ │ ├── timelines-panel.postcss.css.json │ │ ├── tools-panel-button.postcss.css │ │ ├── tools-panel-button.postcss.css.json │ │ ├── tools-panel.postcss.css │ │ └── tools-panel.postcss.css.json ├── index.html └── js │ ├── actions │ └── set-selected.babel.js │ ├── app.babel.jsx │ ├── components │ ├── curve-editor.babel.jsx │ ├── easing.babel.jsx │ ├── hide-button.babel.jsx │ ├── icon.babel.jsx │ ├── icons.babel.jsx │ ├── insert-point.babel.jsx │ ├── main-panel │ │ ├── body-panel.babel.jsx │ │ ├── left-panel.babel.jsx │ │ ├── main-panel.babel.jsx │ │ └── right-panel.babel.jsx │ ├── point-timeline-line.babel.jsx │ ├── point.babel.jsx │ ├── points-panel │ │ ├── point-line.babel.jsx │ │ ├── points-panel.babel.jsx │ │ ├── property-line-add.babel.jsx │ │ └── property-line.babel.jsx │ ├── resize-handle.babel.jsx │ ├── segment-timeline.babel.jsx │ ├── spot.babel.jsx │ ├── timeline-editor.babel.jsx │ ├── timeline-handle.babel.jsx │ ├── timeline-panel.babel.jsx │ ├── timelines-panel.babel.jsx │ ├── tools-panel-button.babel.jsx │ └── tools-panel │ │ ├── button.babel.jsx │ │ ├── index.babel.jsx │ │ └── point.babel.jsx │ ├── constants.babel.js │ ├── helpers │ ├── add-pointer-down.babel.js │ ├── add-pointer-up.babel.js │ ├── add-unload.babel.js │ ├── change.babel.js │ ├── clamp.babel.js │ ├── class-name.babel.js │ ├── create-point.babel.js │ ├── create-segment.babel.js │ ├── create-spot.babel.jsx │ ├── decorators │ │ ├── builder.babel.js │ │ ├── class-names.babel.js │ │ └── refs.babel.js │ ├── fallback.babel.js │ ├── get-last.babel.js │ ├── global-reset-event.babel.js │ ├── is-selected-by-connection.babel.js │ ├── is-string.babel.js │ ├── makeID.babel.js │ ├── persist.babel.js │ └── style-decorator.babel.js │ ├── reducers │ ├── controls-reducer.babel.jsx │ ├── index-reducer.babel.js │ ├── main-panel-reducer.babel.js │ ├── points-reducer.babel.js │ ├── progress-reducer.babel.js │ └── selected-spot.babel.jsx │ └── store.babel.js ├── coding-guide.md ├── gulpfile.js ├── help-wanted.md ├── logo.png ├── mockups ├── mojs-timeline.sketch ├── point-editor.sketch ├── timeline-editor@1x.png └── timeline-editor@2x.png ├── package.json └── webpack.config.js /.++eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | 'transform-runtime', 5 | ["transform-react-jsx", { "pragma":"h" }], 6 | 'transform-decorators-legacy' 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | }, 10 | "parserOptions": { 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "globals": {}, 17 | "rules": { 18 | "no-empty": 0, 19 | "no-console": 0, 20 | "no-unused-vars": [0, { "varsIgnorePattern": "^h$" }], 21 | "no-cond-assign": 1, 22 | "semi": 2, 23 | "camelcase": 0, 24 | "comma-style": 2, 25 | "comma-dangle": [2, "never"], 26 | "indent": ["warn", 2], 27 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 28 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 29 | "max-nested-callbacks": [2, 3], 30 | "no-eval": 2, 31 | "no-implied-eval": 2, 32 | "no-new-func": 2, 33 | "guard-for-in": 2, 34 | "eqeqeq": 0, 35 | "no-else-return": 2, 36 | "no-redeclare": 2, 37 | "no-dupe-keys": 2, 38 | "radix": 2, 39 | "strict": [2, "never"], 40 | "no-shadow": 0, 41 | "no-delete-var": 2, 42 | "no-undef-init": 2, 43 | "no-shadow-restricted-names": 2, 44 | "handle-callback-err": 0, 45 | "no-lonely-if": 2, 46 | "keyword-spacing": 2, 47 | "constructor-super": 2, 48 | "no-this-before-super": 2, 49 | "no-dupe-class-members": 2, 50 | "no-const-assign": 2, 51 | "prefer-spread": 2, 52 | "no-useless-concat": 2, 53 | "no-var": 2, 54 | "object-shorthand": 2, 55 | "prefer-arrow-callback": 2 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | app/build/mojs-curve-editor.js merge=ours 2 | app/build/mojs-curve-editor.min.js merge=ours 3 | *.postcss.css.json merge=ours -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/build/ 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xavierfoucrier 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Oleg Solomka, Xavier Foucrier, Jonas Sandstedt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @mojs/timeline-editor – ![coming soon](https://img.shields.io/badge/not-published-inactive) 2 | 3 | GUI for interactive `html`/`custom points`/`timeline` editing while crafting your animations 4 | 5 | ![@mojs/timeline-editor](logo.png "@mojs/timeline-editor") 6 | 7 | ## Installation 8 | 9 | **Not avaliable on `npm` or `CDN`s yet.** 10 | 11 | The `MojsTimelineEditor` depends on `mojs >= 0.225.2`, tween autoupdates available for `mojs >= 0.276.2`. Please make sure you've linked [mojs](https://github.com/mojs/mojs) library first. 12 | 13 | If you installed it with script link - you should have `MojsTimelineEditor` global. 14 | 15 | ## Usage 16 | 17 | *Not written yet.* 18 | 19 | ## Shortcuts 20 | 21 | *Not written yet.* 22 | 23 | ## Development 24 | 25 | To begin development you need to have [node](https://nodejs.org/en/download/) installed 26 | 27 | Install dependencies with [npm](https://www.npmjs.com/): 28 | 29 | ``` 30 | [sudo] npm install 31 | ``` 32 | 33 | To start development env. run 34 | 35 | ``` 36 | npm run serve 37 | ``` 38 | 39 | This command will run the webpack-dev-server in inline mode and rerun build on every .js/.jsx/.postcss.css change. 40 | Also it runs eslint to watch relevance of javascript files to a style-guide. 41 | 42 | **No globally installed packages are needed.** 43 | 44 | > Please make sure you started a `feature branch` with the `feature name` ( from the `dev` branch) before making changes. 45 | -------------------------------------------------------------------------------- /app/css/assets/colors.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | $c-purple: #3A0839; 3 | $c-light-purple: #512750; /*613760*/ 4 | $c-orange: #FF512F; 5 | $c-cyan: #50E3C2; 6 | $c-white: #FFFFFF; 7 | $c-creamy: #FFF5E3; 8 | $c-green: #50E3C2; 9 | -------------------------------------------------------------------------------- /app/css/assets/globals.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import './colors.postcss.css'; 3 | @import './mixins.postcss.css'; 4 | 5 | /*$PX: 1/16rem;*/ 6 | $PX: 1px; 7 | $FPX: 1px; 8 | $GS: 10*$PX; 9 | $BRADIUS: 3*$PX; 10 | 11 | $LEFT_PANEL_WIDTH: 19.5*$GS; /* old was 165px */ 12 | $POINT_SIZE: 6*$PX; 13 | 14 | $TOP_PANELS_SIZE: 22*$PX; 15 | $PLAYER_SIZE: 4*$GS; 16 | -------------------------------------------------------------------------------- /app/css/assets/mixins.postcss.css: -------------------------------------------------------------------------------- 1 | $POINT_LINE_HEIGHT: 24*$PX; 2 | 3 | @define-mixin pointLine { 4 | position: relative; 5 | min-height: $POINT_LINE_HEIGHT; 6 | cursor: pointer; 7 | color: white; 8 | font-size: 9*$FPX; 9 | letter-spacing: .5*$PX; 10 | line-height: $POINT_LINE_HEIGHT; 11 | background: $c-purple; 12 | border-top: 1*$PX solid $c-light-purple; 13 | 14 | &.is-check { 15 | background: $c-white; 16 | color: $c-purple 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/css/blocks/body-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .body-panel { 4 | position: absolute; 5 | top: $TOP_PANELS_SIZE; 6 | left: 0; 7 | right: 0; 8 | bottom: $PLAYER_SIZE; 9 | z-index: 0; 10 | overflow: auto; 11 | 12 | &__left, 13 | &__right { 14 | min-height: 100%; 15 | padding-top: 1*$PX; 16 | } 17 | &__left { 18 | float: left; 19 | width: $LEFT_PANEL_WIDTH; 20 | background: $c-purple; 21 | } 22 | 23 | &__right { 24 | margin-left: $LEFT_PANEL_WIDTH; 25 | background: $c-light-purple; 26 | min-height: 100%; 27 | overflow: auto 28 | /*min-width: 1600*$PX*/ 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/css/blocks/body-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"body-panel":"_body-panel_1plp0_3","body-panel__left":"_body-panel__left_1plp0_1","body-panel__right":"_body-panel__right_1plp0_1"} -------------------------------------------------------------------------------- /app/css/blocks/curve-editor.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import '../assets/globals.postcss.css'; 3 | 4 | 5 | .curve-editor { 6 | outline: $PX solid cyan; 7 | } 8 | -------------------------------------------------------------------------------- /app/css/blocks/curve-editor.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"curve-editor":"_curve-editor_fxmgs_5"} -------------------------------------------------------------------------------- /app/css/blocks/easing.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $height: 14*$PX; 4 | $width: 60*$PX; 5 | $iconSize: 6*$PX; 6 | .easing { 7 | position: absolute; 8 | left: 50%; 9 | top: 50%; 10 | z-index: 1; 11 | margin-left: -$iconSize; 12 | margin-top: -$height/2; 13 | &:hover { opacity: .85; } 14 | 15 | &__full { 16 | width: $width; 17 | height: $height; 18 | background: $c-purple; 19 | display: none; 20 | border-radius: $BRADIUS; 21 | margin-left: -$width/2; 22 | letter-spacing: ; 23 | } 24 | 25 | &__short { 26 | width: $height; 27 | height: $height; 28 | cursor: pointer; 29 | 30 | [data-component="icon"] { 31 | position: absolute; 32 | left: 50%; 33 | top: 50%; 34 | width: $iconSize; 35 | height: $iconSize; 36 | margin-left: -$iconSize/2; 37 | margin-top: -$iconSize/2; 38 | fill: $c-purple; 39 | } 40 | } 41 | 42 | &.is-full { 43 | margin-left: 0; 44 | .easing__full { 45 | display: block; 46 | } 47 | .easing__short { 48 | display: none; 49 | } 50 | } 51 | } 52 | 53 | .label { 54 | position: absolute; 55 | left: -$width/2; 56 | right: 0; 57 | top: 3*$PX; 58 | color: $c-white; 59 | font-size: 7*$FPX; 60 | /*width: 100%;*/ 61 | letter-spacing: .5*$PX; 62 | padding-right: $height + 3; 63 | padding-left: 5*$PX; 64 | 65 | // ellipsis 66 | white-space: nowrap; 67 | overflow: hidden; 68 | text-overflow: ellipsis; 69 | } 70 | 71 | .dropdown { 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | width: 100%; 76 | height: 100%; 77 | z-index: 1; 78 | &__select { 79 | height: 100%; 80 | width: 100%; 81 | 82 | appearance: none; 83 | outline: 0; 84 | border-radius: $BRADIUS; 85 | cursor: pointer; 86 | position: absolute; 87 | z-index: 1; 88 | opacity: 0; 89 | } 90 | } 91 | 92 | .dropdown-icon { 93 | position: absolute; 94 | width: $height; 95 | height: $height; 96 | right: 0; 97 | top: 0; 98 | border-left: 1*$PX solid $c-light-purple; 99 | 100 | $size: 6*$PX; 101 | [data-component="icon"] { 102 | position: absolute; 103 | left: 50%; 104 | top: 50%; 105 | width: $size; 106 | height: $size; 107 | margin-top: -$size/2; 108 | margin-left: -$size/2 - 1; 109 | fill: $c-white; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/css/blocks/easing.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"easing":"_easing_1w7b1_6","easing__full":"_easing__full_1w7b1_44","easing__short":"_easing__short_1w7b1_47","is-full":"_is-full_1w7b1_42","label":"_label_1w7b1_53","dropdown":"_dropdown_1w7b1_71","dropdown__select":"_dropdown__select_1w7b1_1","dropdown-icon":"_dropdown-icon_1w7b1_92"} -------------------------------------------------------------------------------- /app/css/blocks/hide-button.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $button-height: 16*$PX; 4 | $button-width: 22*$PX; 5 | $icon-size: 8*$PX; 6 | 7 | .hide-button { 8 | position: relative;; 9 | top: -$button-height; 10 | left: 50%; 11 | 12 | display: inline-block; 13 | width: $button-width; 14 | height: $button-height; 15 | cursor: pointer; 16 | 17 | &__icon-container { 18 | position: absolute; 19 | top: 0; 20 | right: 0; 21 | bottom: 0; 22 | left: 0; 23 | 24 | border-top-left-radius: $BRADIUS; 25 | border-top-right-radius: $BRADIUS; 26 | background: $c-purple; 27 | } 28 | 29 | & [data-component="icon"] { 30 | position: absolute; 31 | top: ($button-height - $icon-size) / 2; 32 | left: ($button-width - $icon-size) / 2; 33 | display: inline-block; 34 | width: $icon-size; 35 | height: $icon-size; 36 | margin-top: 1*$PX; 37 | transition: transform 0.2s; 38 | } 39 | 40 | &--is-hidden [data-component="icon"] { 41 | margin-top: 0; 42 | transition: transform 0.2s; 43 | transform: rotate( 180deg ); 44 | } 45 | 46 | &:hover { 47 | opacity: .85; 48 | } 49 | 50 | &:active { 51 | opacity: 1; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/css/blocks/hide-button.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"hide-button":"_hide-button_1thvy_7","hide-button__icon-container":"_hide-button__icon-container_1thvy_1","hide-button--is-hidden":"_hide-button--is-hidden_1thvy_1"} -------------------------------------------------------------------------------- /app/css/blocks/icon.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import '../assets/globals.postcss.css'; 3 | 4 | $icon-size: 8*$PX; 5 | 6 | .icon { 7 | position: relative; 8 | width: $icon-size; 9 | height: $icon-size; 10 | cursor: pointer; 11 | fill: $c-white; 12 | display: block; 13 | 14 | & > svg { 15 | position: absolute; 16 | left: 0; 17 | top: 0; 18 | width: 100%; 19 | height: 100%; 20 | fill: inherit; 21 | & > use { 22 | fill: inherit; 23 | } 24 | } 25 | 26 | &:after { 27 | content: ''; 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | right: 0; 32 | bottom: 0; 33 | z-index: 1; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/css/blocks/icon.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"icon":"_icon_1h4ls_6"} -------------------------------------------------------------------------------- /app/css/blocks/insert-point.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .insert-point { 4 | position: absolute; 5 | width: $POINT_SIZE*$PX; 6 | height: $POINT_SIZE*$PX; 7 | border-radius: 50%; 8 | background: $c-orange; 9 | margin-left: -($POINT_SIZE/2)*$PX; 10 | margin-top: -($POINT_SIZE/2)*$PX; 11 | } 12 | -------------------------------------------------------------------------------- /app/css/blocks/insert-point.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"insert-point":"_insert-point_wch35_3"} -------------------------------------------------------------------------------- /app/css/blocks/left-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $panel-width: $LEFT_PANEL_WIDTH*$PX; 4 | .left-panel { 5 | position: absolute; 6 | right: $panel-width; 7 | top: 0; 8 | left: 0; 9 | width: $panel-width; 10 | /*height: 100%;*/ 11 | 12 | background: $c-purple; 13 | } 14 | -------------------------------------------------------------------------------- /app/css/blocks/left-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"left-panel":"_left-panel_1w42h_4"} -------------------------------------------------------------------------------- /app/css/blocks/main-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $player-height: 40*$PX; 4 | .main-panel { 5 | position: fixed; 6 | bottom: 0; 7 | width: 100%; 8 | /*height: 200*$PX;*/ 9 | color: white; 10 | background: $c-purple; 11 | 12 | &--transition { 13 | transition: height 0.4s; 14 | } 15 | 16 | [data-component="timeline-handle"] { 17 | margin-left: $LEFT_PANEL_WIDTH; 18 | font-size: 1px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/css/blocks/main-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"main-panel":"_main-panel_184an_4","main-panel--transition":"_main-panel--transition_184an_1"} -------------------------------------------------------------------------------- /app/css/blocks/point-line.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | /*$padding-left: 5*$PX;*/ 4 | .point-line { 5 | margin-top: 10*$PX; 6 | @mixin pointLine; 7 | border-bottom: 1*$PX solid $c-light-purple; 8 | 9 | .label { 10 | position: absolute; 11 | left: 0; 12 | right: $POINT_LINE_HEIGHT; 13 | line-height: $POINT_LINE_HEIGHT - 3; 14 | } 15 | 16 | &.is-check { 17 | .button { 18 | &:hover { 19 | background: rgba(61,12,59,.2); 20 | } 21 | &__inner { 22 | fill: $c-purple; 23 | } 24 | } 25 | .body, .label { 26 | background: inherit; 27 | } 28 | } 29 | 30 | &.is-open { 31 | .button.is-arrow { 32 | .button__inner { 33 | transform: rotate(180deg); 34 | } 35 | } 36 | .body { 37 | display: block; 38 | } 39 | } 40 | } 41 | 42 | .label { 43 | padding-left: 10*$PX; 44 | background: $c-purple; 45 | height: $POINT_LINE_HEIGHT - 2; 46 | &:hover { 47 | background: $c-light-purple 48 | } 49 | } 50 | 51 | .body { 52 | padding-left: 5*$PX; 53 | /*padding-bottom: 1*$PX;*/ 54 | background: $c-light-purple; 55 | height: auto; 56 | padding-top: $POINT_LINE_HEIGHT - 2; 57 | display: none; 58 | } 59 | 60 | .button { 61 | position: absolute; 62 | top: 0; 63 | right: 0; 64 | width: $POINT_LINE_HEIGHT; 65 | height: $POINT_LINE_HEIGHT - 2; 66 | background: inherit; 67 | border-left: 1px solid $c-light-purple; 68 | 69 | &__inner { 70 | position: absolute; 71 | width: 100%; 72 | height: 100%; 73 | fill: white; 74 | transition: all .15s ease; 75 | } 76 | 77 | $size: 5*$PX; 78 | [data-component="icon"] { 79 | fill: inherit; 80 | position: absolute; 81 | left: 50%; 82 | top: 50%; 83 | width: $size; 84 | height: $size; 85 | margin-top: -$size/2; 86 | margin-left: -$size/2; 87 | } 88 | 89 | &:hover { 90 | background: $c-light-purple; 91 | } 92 | 93 | &.is-spot { 94 | right: $POINT_LINE_HEIGHT; 95 | } 96 | } 97 | 98 | /*.add-spot { 99 | position: absolute; 100 | right: $POINT_LINE_HEIGHT; 101 | width: $POINT_LINE_HEIGHT; 102 | height: $POINT_LINE_HEIGHT; 103 | border-left: 1*$PX solid $c-purple; 104 | 105 | $size: 6*$PX; 106 | [data-component="icon"] { 107 | fill: $c-purple; 108 | position: absolute; 109 | width: $size; 110 | height: $size; 111 | left: 50%; 112 | top: 50%; 113 | margin-left: -$size/2; 114 | margin-top: -$size/2 - 1; 115 | } 116 | }*/ 117 | -------------------------------------------------------------------------------- /app/css/blocks/point-line.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"point-line":"_point-line_12vv1_4","label":"_label_12vv1_9","is-check":"_is-check_12vv1_16","button":"_button_12vv1_17","button__inner":"_button__inner_12vv1_32","body":"_body_12vv1_25","is-open":"_is-open_12vv1_30","is-arrow":"_is-arrow_12vv1_31","is-spot":"_is-spot_12vv1_93"} -------------------------------------------------------------------------------- /app/css/blocks/point-timeline-line.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .point-timeline-line { 4 | min-height: $POINT_LINE_HEIGHT; 5 | margin-top: 10*$PX; 6 | position: relative; 7 | 8 | &__inner { 9 | display: inline-block; 10 | vertical-align: top; 11 | } 12 | 13 | &__header { 14 | width: 100%; 15 | height: $POINT_LINE_HEIGHT; 16 | background: $c-creamy; 17 | border-radius: 3*$PX; 18 | border-top-left-radius: 0; 19 | border-bottom-left-radius: 0; 20 | opacity: .2; 21 | } 22 | 23 | &__body { 24 | height: 0; 25 | overflow: hidden; 26 | } 27 | 28 | &__property { 29 | height: $POINT_LINE_HEIGHT; 30 | } 31 | 32 | &:last-child { 33 | padding-bottom: 0; 34 | } 35 | 36 | &.is-open { 37 | .point-timeline-line { 38 | &__body { 39 | height: auto; 40 | overflow: visible; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/css/blocks/point-timeline-line.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"point-timeline-line":"_point-timeline-line_1hrq6_3","point-timeline-line__inner":"_point-timeline-line__inner_1hrq6_1","point-timeline-line__header":"_point-timeline-line__header_1hrq6_1","point-timeline-line__body":"_point-timeline-line__body_1hrq6_1","point-timeline-line__property":"_point-timeline-line__property_1hrq6_1","is-open":"_is-open_1hrq6_36"} -------------------------------------------------------------------------------- /app/css/blocks/point.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .point { 4 | position: absolute; 5 | width: $POINT_SIZE*$PX; 6 | height: $POINT_SIZE*$PX; 7 | border-radius: 50%; 8 | background: $c-orange; 9 | margin-left: -($POINT_SIZE/2)*$PX; 10 | margin-top: -($POINT_SIZE/2)*$PX; 11 | 12 | $size: 150%; 13 | &:after { 14 | content: ''; 15 | position: absolute; 16 | left: 50%; 17 | top: 50%; 18 | width: $size; 19 | height: $size; 20 | border: 1px solid $c-orange; 21 | transform: translate(-50%, -50%); 22 | /*margin-left: -($size - 100%);*/ 23 | /*margin-top: -($size - 100%);*/ 24 | border-radius: 50%; 25 | opacity: 0; 26 | } 27 | 28 | &.is-selected { 29 | &:after { 30 | opacity: 1; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/css/blocks/point.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"point":"_point_mlokt_3","is-selected":"_is-selected_mlokt_28"} -------------------------------------------------------------------------------- /app/css/blocks/points-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .points-panel { 4 | background: rgba(0,0,0,.1); 5 | } 6 | -------------------------------------------------------------------------------- /app/css/blocks/points-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"points-panel":"_points-panel_xkznz_3"} -------------------------------------------------------------------------------- /app/css/blocks/property-line-add.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $labelWidth: 25%; 4 | .property-line-add { 5 | @mixin pointLine; 6 | width: 100%; 7 | cursor: default; 8 | 9 | &__inputs { 10 | position: absolute; 11 | right: $POINT_LINE_HEIGHT; 12 | left: 0; 13 | } 14 | 15 | &.is-add { 16 | .input, 17 | [data-component="tools-panel-button"], 18 | .name-input-wrapper { 19 | display: block; 20 | } 21 | 22 | .label { 23 | display: none; 24 | } 25 | } 26 | 27 | &.is-valid { 28 | .error-label { display: none; } 29 | .input { 30 | &--name { 31 | border: 1*$PX solid transparent; 32 | } 33 | } 34 | [data-component="tools-panel-button"] { 35 | cursor: pointer; 36 | &:hover { 37 | background: $c-light-purple; 38 | } 39 | [data-component="icon"] { 40 | opacity: 1; 41 | fill: $c-green; 42 | } 43 | } 44 | } 45 | } 46 | 47 | .name-input-wrapper { 48 | position: absolute; 49 | left: 0; 50 | width: 85.5%; 51 | display: none; 52 | height: $POINT_LINE_HEIGHT; 53 | } 54 | 55 | .error-label { 56 | position: absolute; 57 | top: 100%; 58 | left: 50%; 59 | padding: 2*$PX 4*$PX; 60 | font-size: 7*$FPX; 61 | line-height: 1.5; 62 | letter-spacing: .5*$PX; 63 | font-weight: bold; 64 | margin-top: -1*$PX; 65 | background: $c-orange; 66 | /*color: $c-purple;*/ 67 | /*background: $c-purple;*/ 68 | /*color: $c-orange;*/ 69 | /*border: 1*$PX solid $c-orange;*/ 70 | border-bottom-left-radius: $BRADIUS; 71 | border-bottom-right-radius: $BRADIUS; 72 | transform: translateX(-50%); 73 | } 74 | 75 | .label { 76 | position: absolute; 77 | left: 0; 78 | width: $labelWidth; 79 | padding-left: 10*$PX; 80 | line-height: $POINT_LINE_HEIGHT - 1; 81 | &:hover { 82 | cursor: pointer; 83 | /*background: $c-light-purple;*/ 84 | text-decoration: underline; 85 | } 86 | } 87 | 88 | .input { 89 | display: block; 90 | color: white; 91 | background: transparent; 92 | border: none; 93 | height: $POINT_LINE_HEIGHT; 94 | text-align: center; 95 | outline: 0; 96 | font-size: 10*$PX; 97 | padding-top: 0; 98 | padding-bottom: 2*$PX; 99 | position: absolute; 100 | border-left: 1*$PX solid $c-light-purple; 101 | display: none; 102 | 103 | &::selection { 104 | background: $c-orange; 105 | } 106 | 107 | &--name { 108 | width: 100%; 109 | border-left: none; 110 | text-align: left; 111 | padding-left: 10*$PX; 112 | border: 1*$PX solid $c-orange; 113 | } 114 | 115 | &--count { 116 | right: 0; 117 | width: $POINT_LINE_HEIGHT; 118 | } 119 | 120 | } 121 | 122 | /*.button { 123 | position: absolute; 124 | top: 0; 125 | right: 0; 126 | width: $POINT_LINE_HEIGHT; 127 | height: $POINT_LINE_HEIGHT - 1; 128 | background: inherit; 129 | border-left: 1px solid $c-light-purple; 130 | display: none; 131 | cursor: default; 132 | 133 | &__inner { 134 | position: absolute; 135 | width: 100%; 136 | height: 100%; 137 | fill: white; 138 | transition: all .15s ease; 139 | } 140 | 141 | $size: 7*$PX; 142 | [data-component="icon"] { 143 | fill: inherit; 144 | position: absolute; 145 | left: 50%; 146 | top: 50%; 147 | width: $size; 148 | height: $size; 149 | margin-top: -$size/2; 150 | margin-left: -$size/2; 151 | opacity: .5; 152 | cursor: inherit; 153 | } 154 | }*/ 155 | -------------------------------------------------------------------------------- /app/css/blocks/property-line-add.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"property-line-add":"_property-line-add_5nvpy_4","is-check":"_is-check_5nvpy_1","property-line-add__inputs":"_property-line-add__inputs_5nvpy_1","is-add":"_is-add_5nvpy_15","input":"_input_5nvpy_16","name-input-wrapper":"_name-input-wrapper_5nvpy_18","label":"_label_5nvpy_22","is-valid":"_is-valid_5nvpy_27","error-label":"_error-label_5nvpy_28","input--name":"_input--name_5nvpy_1","input--count":"_input--count_5nvpy_1"} -------------------------------------------------------------------------------- /app/css/blocks/property-line.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $labelWidth: 25%; 4 | .property-line { 5 | @mixin pointLine; 6 | width: 100%; 7 | cursor: default; 8 | 9 | &__inputs { 10 | position: absolute; 11 | right: $POINT_LINE_HEIGHT; 12 | left: $labelWidth; 13 | } 14 | } 15 | .label { 16 | position: absolute; 17 | left: 0; 18 | width: $labelWidth; 19 | padding-left: 10*$PX; 20 | line-height: $POINT_LINE_HEIGHT - 1; 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | overflow: hidden; 24 | } 25 | 26 | .input { 27 | display: block; 28 | color: white; 29 | background: transparent; 30 | border: none; 31 | height: $POINT_LINE_HEIGHT; 32 | text-align: center; 33 | outline: 0; 34 | font-size: 10*$PX; 35 | padding-top: 0; 36 | width: 100%; 37 | float: left; 38 | position: relative; 39 | border-left: 1*$PX solid $c-light-purple; 40 | 41 | &::selection { 42 | background: $c-orange; 43 | /*color: $c-purple;*/ 44 | } 45 | 46 | & + .input { 47 | /*&:after { 48 | content: ''; 49 | position: absolute; 50 | left: 0; 51 | height: 50%; 52 | width: 1*$PX; 53 | background: yellow; 54 | }*/ 55 | } 56 | 57 | &[data-width="1/2"] { 58 | width: calc(100%/2); 59 | /*&:first-child { 60 | text-align: right; 61 | padding-right: 5*$PX; 62 | } 63 | &:last-child { 64 | text-align: left; 65 | padding-left: 5*$PX; 66 | }*/ 67 | } 68 | &[data-width="1/3"] { 69 | width: calc(100%/3); 70 | } 71 | &[data-width="1/4"] { 72 | width: calc(100%/4); 73 | } 74 | } 75 | 76 | .button { 77 | position: absolute; 78 | top: 0; 79 | right: 0; 80 | width: $POINT_LINE_HEIGHT; 81 | height: $POINT_LINE_HEIGHT - 1; 82 | background: inherit; 83 | border-left: 1px solid $c-light-purple; 84 | 85 | &__inner { 86 | position: absolute; 87 | width: 100%; 88 | height: 100%; 89 | fill: white; 90 | transition: all .15s ease; 91 | } 92 | 93 | $size: 5*$PX; 94 | [data-component="icon"] { 95 | fill: inherit; 96 | position: absolute; 97 | left: 50%; 98 | top: 50%; 99 | width: $size; 100 | height: $size; 101 | margin-top: -$size/2; 102 | margin-left: -$size/2; 103 | } 104 | 105 | &:hover { 106 | background: $c-light-purple; 107 | } 108 | 109 | &.is-spot { 110 | right: $POINT_LINE_HEIGHT; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/css/blocks/property-line.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"property-line":"_property-line_116ri_4","is-check":"_is-check_116ri_1","property-line__inputs":"_property-line__inputs_116ri_1","label":"_label_116ri_15","input":"_input_116ri_26","button":"_button_116ri_76","button__inner":"_button__inner_116ri_1","is-spot":"_is-spot_116ri_109"} -------------------------------------------------------------------------------- /app/css/blocks/resize-handle.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $handle-width: 32*$PX; 4 | $handle-height: 16*$PX; 5 | $icon-size: 32*$PX; 6 | 7 | .resize-handle { 8 | position: relative; 9 | top: -$handle-height; 10 | left: 50%; 11 | display: inline-block; 12 | width: $handle-width; 13 | height: $handle-height; 14 | margin-left: 5*$PX; 15 | cursor: n-resize; 16 | overflow: hidden; 17 | border-top-left-radius: $BRADIUS; 18 | border-top-right-radius: $BRADIUS; 19 | transform-origin: 50% 100%; 20 | background: $c-purple; 21 | box-shadow: inset 0 0 0 1*$PX $c-light-purple; 22 | 23 | &:after { 24 | content: ''; 25 | position: absolute; 26 | left: 0; 27 | top: 0; 28 | right: 0; 29 | bottom: 0; 30 | z-index: 2; 31 | } 32 | 33 | & [data-component="icon"] { 34 | position: absolute; 35 | top: ($handle-height - $icon-size) / 2; 36 | left: ($handle-width - $icon-size) / 2; 37 | width: $icon-size; 38 | height: $icon-size; 39 | display: inline-block; 40 | } 41 | 42 | &:hover { 43 | opacity: .85; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/css/blocks/resize-handle.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"resize-handle":"_resize-handle_qa5s6_7"} -------------------------------------------------------------------------------- /app/css/blocks/right-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .right-panel { 4 | position: absolute; 5 | right: 0; 6 | top: 0; 7 | left: $LEFT_PANEL_WIDTH*$PX; 8 | z-index: 1; 9 | height: $POINT_LINE_HEIGHT; 10 | /*height: 100%;*/ 11 | 12 | color: $c-white; 13 | } 14 | -------------------------------------------------------------------------------- /app/css/blocks/right-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"right-panel":"_right-panel_1rm1g_3"} -------------------------------------------------------------------------------- /app/css/blocks/segment-timeline.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $borderRadius: 3*$PX; 4 | .segment-timeline { 5 | height: 100%; 6 | display: inline-block; 7 | vertical-align: top; 8 | position: relative; 9 | font-size: 1px; 10 | 11 | $size: 80%; 12 | &__bar { 13 | width: 100%; 14 | /*height: $size;*/ 15 | background: $c-creamy; 16 | border-radius: $borderRadius; 17 | position: relative; 18 | top: (100% - $size)/2; 19 | box-shadow: 2*$PX 3*$PX 0 rgba(0,0,0,.5); 20 | overflow: hidden; 21 | } 22 | 23 | /*&__delay { 24 | background: #BCA5AA; 25 | height: 100%; 26 | position: absolute; 27 | left: 0; 28 | z-index: 1; 29 | border-top-left-radius: $borderRadius; 30 | border-bottom-left-radius: $borderRadius; 31 | }*/ 32 | } 33 | -------------------------------------------------------------------------------- /app/css/blocks/segment-timeline.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"segment-timeline":"_segment-timeline_1r863_4","segment-timeline__bar":"_segment-timeline__bar_1r863_1"} -------------------------------------------------------------------------------- /app/css/blocks/spot.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import '../assets/globals.postcss.css'; 3 | 4 | .spot { 5 | position: relative; 6 | height: 100%; 7 | float: left; 8 | background: #BCA5AA; 9 | height: 20*$PX; 10 | border-top-left-radius: $BRADIUS; 11 | border-bottom-left-radius: $BRADIUS; 12 | 13 | &--end { 14 | display: block; 15 | background: transparent; 16 | } 17 | 18 | $size: 6*$PX; 19 | &__dot { 20 | width: $size; 21 | height: $size; 22 | background: $c-purple; 23 | position: absolute; 24 | z-index: 1; 25 | top: 50%; 26 | right: -($size/2); 27 | margin-top: -($size/2); 28 | cursor: pointer; 29 | transform: rotate(45deg); 30 | 31 | &:hover, 32 | &:active { 33 | background: $c-light-purple; 34 | outline: 1*$PX solid $c-orange; 35 | outline: 2*$PX solid #BCA5AA; 36 | } 37 | 38 | $size: 300%; 39 | &:after { 40 | content: ''; 41 | position: absolute; 42 | width: $size; 43 | height: $size; 44 | margin-left: (100% - $size)/2; 45 | margin-top: (100% - $size)/2; 46 | transform: rotate(45deg); 47 | user-select: none; 48 | } 49 | } 50 | 51 | [data-component="easing"] { 52 | display: none; 53 | } 54 | 55 | &.is-selected { 56 | .spot__dot { 57 | background: $c-orange; 58 | } 59 | } 60 | 61 | &.is-easing { 62 | [data-component="easing"] { 63 | display: block; 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/css/blocks/spot.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"spot":"_spot_101ld_4","spot--end":"_spot--end_101ld_1","spot__dot":"_spot__dot_101ld_56","is-selected":"_is-selected_101ld_55","is-easing":"_is-easing_101ld_61"} -------------------------------------------------------------------------------- /app/css/blocks/timeline-editor.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import '../assets/globals.postcss.css'; 3 | 4 | .timeline-editor { 5 | font-family: Arial, sans-serif; 6 | & * { 7 | box-sizing: border-box; 8 | user-select: none; 9 | } 10 | 11 | &__el { 12 | outline: 1px solid; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/css/blocks/timeline-editor.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"timeline-editor":"_timeline-editor_13ln4_4","timeline-editor__el":"_timeline-editor__el_13ln4_1"} -------------------------------------------------------------------------------- /app/css/blocks/timeline-handle.postcss.css: -------------------------------------------------------------------------------- 1 | 2 | @import '../assets/globals.postcss.css'; 3 | 4 | .timeline-handle { 5 | position: absolute; 6 | min-height: 100%; 7 | width: 1*$PX; 8 | background: $c-orange; 9 | z-index: 20; 10 | 11 | $size: 14*$PX; 12 | &__head { 13 | cursor: pointer; 14 | background: $c-purple; 15 | border: 1*$PX solid $c-orange; 16 | width: $size; 17 | height: $size; 18 | border-radius: 5*$PX; 19 | $bigRadus: 11*$PX; 20 | border-bottom-left-radius: $bigRadus; 21 | border-bottom-right-radius: $bigRadus; 22 | position: absolute; 23 | left: -($size/2); 24 | top: -.8*$size; 25 | 26 | $size: 6*$PX; 27 | [data-component="icon"] { 28 | position: absolute; 29 | width: $size; 30 | height: $size; 31 | left: 50%; 32 | top: 50%; 33 | margin-left: -$size/2; 34 | margin-top: -$size/2 - 1*$PX; 35 | } 36 | &:hover { 37 | [data-component="icon"] { opacity: .85; } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/css/blocks/timeline-handle.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"timeline-handle":"_timeline-handle_1ajze_4","timeline-handle__head":"_timeline-handle__head_1ajze_1"} -------------------------------------------------------------------------------- /app/css/blocks/timeline-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .timeline-panel { 4 | position: relative; 5 | top: -20*$PX; 6 | height: 22*$PX; 7 | background: $c-light-purple; 8 | box-shadow: 0 2*$PX 4*$PX black; 9 | } 10 | 11 | .label { 12 | fill: $c-white; 13 | font-size: 7px; 14 | } 15 | 16 | .main-svg { 17 | width: 100%; 18 | height: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /app/css/blocks/timeline-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"timeline-panel":"_timeline-panel_1hab5_3","label":"_label_1hab5_11","main-svg":"_main-svg_1hab5_16"} -------------------------------------------------------------------------------- /app/css/blocks/timelines-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | .timelines-panel { 4 | display: inline-block; 5 | /*min-width: 100%;*/ 6 | min-width: 1600*$PX; 7 | min-height: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /app/css/blocks/timelines-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"timelines-panel":"_timelines-panel_12eku_3"} -------------------------------------------------------------------------------- /app/css/blocks/tools-panel-button.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | 4 | .tools-panel-button { 5 | position: absolute; 6 | top: 0; 7 | right: 0; 8 | width: $POINT_LINE_HEIGHT; 9 | height: $POINT_LINE_HEIGHT - 1; 10 | background: inherit; 11 | border-left: 1px solid $c-light-purple; 12 | display: none; 13 | cursor: default; 14 | 15 | &__inner { 16 | position: absolute; 17 | width: 100%; 18 | height: 100%; 19 | fill: white; 20 | transition: all .15s ease; 21 | } 22 | 23 | $size: 7*$PX; 24 | [data-component="icon"] { 25 | fill: inherit; 26 | position: absolute; 27 | left: 50%; 28 | top: 50%; 29 | width: $size; 30 | height: $size; 31 | margin-top: -$size/2; 32 | margin-left: -$size/2; 33 | opacity: .5; 34 | cursor: inherit; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/css/blocks/tools-panel-button.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"tools-panel-button":"_tools-panel-button_13ja5_4","tools-panel-button__inner":"_tools-panel-button__inner_13ja5_1"} -------------------------------------------------------------------------------- /app/css/blocks/tools-panel.postcss.css: -------------------------------------------------------------------------------- 1 | @import '../assets/globals.postcss.css'; 2 | 3 | $height: 22; 4 | .tools-panel { 5 | height: $height*$PX; 6 | background: $c-purple; 7 | box-shadow: 0 2*$PX 4*$PX rgba(0,0,0, .5); 8 | padding-right: 5*$PX; 9 | position: relative; 10 | z-index: 1; 11 | } 12 | 13 | .button { 14 | position: relative; 15 | height: $height*$PX; 16 | line-height: ($height+1)*$PX; 17 | float: left; 18 | font-size: 7*$FPX; 19 | font-weight: bold; 20 | letter-spacing: .5*$PX; 21 | padding: 0 7*$PX; 22 | fill: white; 23 | user-select: none; 24 | &:hover { 25 | cursor: pointer; 26 | background: #512750; 27 | } 28 | 29 | &:active, &.is-active { 30 | background: white; 31 | color: $c-purple; 32 | fill: $c-purple; 33 | } 34 | 35 | &.is-logo { 36 | float: right; 37 | fill: $c-orange; 38 | [data-component="icon"] { 39 | width: 8*$PX; 40 | height: 8*$PX; 41 | } 42 | } 43 | 44 | &.is-link { 45 | float: right; 46 | fill: $c-white; 47 | [data-component="icon"] { 48 | width: 8*$PX; 49 | height: 8*$PX; 50 | } 51 | } 52 | 53 | [data-component="icon"] { 54 | fill: inherit; 55 | display: inline-block; 56 | vertical-align: middle; 57 | position: relative; 58 | top: -1*$PX; 59 | width: 6*$PX; 60 | height: 6*$PX; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/css/blocks/tools-panel.postcss.css.json: -------------------------------------------------------------------------------- 1 | {"tools-panel":"_tools-panel_1ys7p_4","button":"_button_1ys7p_13","is-active":"_is-active_1ys7p_29","is-logo":"_is-logo_1ys7p_35","is-link":"_is-link_1ys7p_44"} -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Timeline Editor 6 | 7 | 8 | 9 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/js/actions/set-selected.babel.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const setSelected = (type) => { 4 | return (dispatch, store) => { 5 | console.log(dispatch, store, type); 6 | }; 7 | }; 8 | 9 | export default setSelected; 10 | -------------------------------------------------------------------------------- /app/js/app.babel.jsx: -------------------------------------------------------------------------------- 1 | import {Provider} from 'preact-redux'; 2 | import {render, h} from 'preact'; 3 | import mojs from 'mo-js'; 4 | import MojsPlayer from 'mojs-player'; 5 | 6 | import store from './store'; 7 | import TimelineEditor from './components/timeline-editor'; 8 | import persist from './helpers/persist'; 9 | 10 | /* TODO: 11 | [x] point-timleine.babel.jsx add animation 12 | when start/end points got selected 13 | [x] test if `onClick` handler on components is optimized for mobiles 14 | */ 15 | 16 | render( 17 | 18 | 19 | , 20 | document.body 21 | ); 22 | 23 | persist(store); 24 | 25 | new MojsPlayer({ add: new mojs.Tween }); 26 | 27 | // /* 28 | // API wrapper above the app itself. 29 | // */ 30 | // class API {} 31 | // export default API; 32 | // window.MojsTimelineEditor = API; 33 | -------------------------------------------------------------------------------- /app/js/components/curve-editor.babel.jsx: -------------------------------------------------------------------------------- 1 | import {h, Component} from 'preact'; 2 | import {classNames} from '../helpers/style-decorator'; 3 | import MojsCurveEditor from 'mojs-curve-editor'; 4 | 5 | const CLASSES = require('../../css/blocks/curve-editor.postcss.css.json'); 6 | require('../../css/blocks/curve-editor'); 7 | 8 | @classNames(CLASSES) 9 | class CurveEditor extends Component { 10 | shouldComponentUpdate() { 11 | return false; 12 | } 13 | 14 | render() { 15 | return (
); 16 | } 17 | 18 | componentDidMount() { 19 | const {meta} = this.props; 20 | const {id, spotIndex} = meta; 21 | 22 | this._editor = new MojsCurveEditor({ 23 | name: `timeline_editor_curve_${id}_${spotIndex}`, 24 | isHiddenOnMin: true, 25 | onChange: this._onChange 26 | }); 27 | } 28 | 29 | _onChange(path) { 30 | console.log(path); 31 | } 32 | 33 | componentWillUnmount() { 34 | console.log('will unmount'); 35 | } 36 | 37 | componentDidUnmount() { 38 | console.log('did unmount'); 39 | } 40 | } 41 | 42 | export default CurveEditor; 43 | -------------------------------------------------------------------------------- /app/js/components/easing.babel.jsx: -------------------------------------------------------------------------------- 1 | import {h, Component} from 'preact'; 2 | import {bind} from 'decko'; 3 | 4 | import Icon from './icon'; 5 | import {classNames} from '../helpers/style-decorator'; 6 | import CurveEditor from './curve-editor'; 7 | 8 | const CLASSES = require('../../css/blocks/easing.postcss.css.json'); 9 | require('../../css/blocks/easing'); 10 | 11 | const GAP = '---'; 12 | const DELIMITER = ; 13 | 14 | @classNames(CLASSES) 15 | class Easing extends Component { 16 | render () { 17 | const {state, meta} = this.props; 18 | const {easing} = state; 19 | 20 | return ( 21 |
22 |
23 |
24 | {easing === 'custom' ? : null} 25 |
{easing}
26 |
27 |
28 |
29 | 78 |
79 |
80 | ); 81 | } 82 | 83 | // _renderEasing() { 84 | // const {state, meta} = this.props; 85 | // const {easing} = state; 86 | // 87 | // return (easing === 'custom') 88 | // ? 89 | // :
{easing}
; 90 | // } 91 | 92 | _makeOption(name) { 93 | const {easing} = this.props.state; 94 | return ; 95 | } 96 | 97 | _getClassName() { 98 | const isFull = (this.props.state.easing !== 'none') ? 'is-full' : ''; 99 | return `easing ${isFull}`; 100 | } 101 | 102 | @bind 103 | _onChange(e) { 104 | const {store} = this.context; 105 | const {target} = e; 106 | const {value} = target.options[target.selectedIndex]; 107 | 108 | const data = { ...this.props.meta, easing: value }; 109 | store.dispatch({ type: 'SET_EASING', data }); 110 | } 111 | 112 | @bind 113 | _onEasingAdd() { 114 | const {store} = this.context; 115 | 116 | const data = { ...this.props.meta, easing: 'ease.out' }; 117 | store.dispatch({ type: 'SET_EASING', data }); 118 | } 119 | } 120 | 121 | export default Easing; 122 | -------------------------------------------------------------------------------- /app/js/components/hide-button.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | import Hammer from 'hammerjs'; 4 | import Icon from './icon'; 5 | 6 | const CLASSES = require('../../css/blocks/hide-button.postcss.css.json'); 7 | require('../../css/blocks/hide-button'); 8 | 9 | class HideButton extends Component { 10 | render() { 11 | const p = this.props; 12 | const hideClassName = p.isHidden ? CLASSES['hide-button--is-hidden'] : ''; 13 | const className = `${CLASSES['hide-button']} ${hideClassName}`; 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | componentDidMount() { 24 | const mc = new Hammer.Manager(this.base); 25 | mc.add(new Hammer.Tap); 26 | 27 | mc.on('tap', (e) => { this.props.onTap && this.props.onTap(e); }); 28 | } 29 | } 30 | 31 | export default HideButton; 32 | -------------------------------------------------------------------------------- /app/js/components/icon.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | const CLASSES = require('../../css/blocks/icon.postcss.css.json'); 3 | require('../../css/blocks/icon'); 4 | 5 | class Icon extends Component { 6 | render () { 7 | const { shape } = this.props; 8 | const markup = ` 9 | 10 | 11 | 12 | `; 13 | 14 | return ( 15 |
18 |
19 | ); 20 | } 21 | } 22 | 23 | export default Icon; 24 | -------------------------------------------------------------------------------- /app/js/components/icons.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const Icons = () => { 4 | return
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ` }}>
; 22 | }; 23 | 24 | export default Icons; 25 | -------------------------------------------------------------------------------- /app/js/components/insert-point.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | 4 | const CLASSES = require('../../css/blocks/insert-point.postcss.css.json'); 5 | require('../../css/blocks/insert-point'); 6 | 7 | class Point extends Component { 8 | render () { 9 | const {state} = this.props; 10 | const style = { 11 | display: (this._isVisible()) ? 'block' : 'none', 12 | transform: `translate(${this.state.x}px, ${this.state.y}px)` 13 | }; 14 | 15 | return ( 16 |
19 |
20 | ); 21 | } 22 | 23 | /* Method to find out if the insert point should be visible. */ 24 | _isVisible() { 25 | const {controls} = this.props.state; 26 | const {selected} = controls; 27 | const isPlus = selected === 'plus'; 28 | const isOut = !controls.isMouseInside; 29 | return isOut && isPlus; 30 | } 31 | 32 | @bind 33 | _addPoint() { 34 | const {store} = this.context; 35 | const {state} = this.props; 36 | store.dispatch({ 37 | type: 'ADD_POINT', 38 | data: { ...this.state, time: state.progress } 39 | }); 40 | } 41 | 42 | @bind 43 | _mouseMove(e) { 44 | if (!this._isVisible()) { return; } 45 | const { pageX: x, pageY: y } = e; 46 | this.setState({x, y}); 47 | } 48 | 49 | componentWillMount() { this.setState({ x: 0, y:0 }); } 50 | 51 | componentDidMount() { 52 | document.addEventListener('mousemove', this._mouseMove); 53 | } 54 | 55 | } 56 | 57 | export default Point; 58 | -------------------------------------------------------------------------------- /app/js/components/main-panel/body-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | 4 | import PointsPanel from '../points-panel/points-panel'; 5 | import TimelinesPanel from '../timelines-panel'; 6 | 7 | const CLASSES = require('../../../css/blocks/body-panel.postcss.css.json'); 8 | require('../../../css/blocks/body-panel'); 9 | 10 | class BodyPanel extends Component { 11 | render() { 12 | const {state} = this.props; 13 | 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | } 27 | 28 | export default BodyPanel; 29 | -------------------------------------------------------------------------------- /app/js/components/main-panel/left-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | const CLASSES = require('../../../css/blocks/left-panel.postcss.css.json'); 3 | require('../../../css/blocks/left-panel'); 4 | 5 | import ToolsPanel from '../tools-panel/index'; 6 | 7 | class LeftPanel extends Component { 8 | render () { 9 | const {state} = this.props; 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | } 18 | 19 | export default LeftPanel; 20 | -------------------------------------------------------------------------------- /app/js/components/main-panel/main-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | 4 | import LeftPanel from './left-panel'; 5 | import BodyPanel from './body-panel'; 6 | import RightPanel from './right-panel'; 7 | import TimelineHandle from '../timeline-handle'; 8 | import C from '../../constants'; 9 | 10 | const CLASSES = require('../../../css/blocks/main-panel.postcss.css.json'); 11 | require('../../../css/blocks/main-panel'); 12 | 13 | class MainPanel extends Component { 14 | 15 | constructor() { 16 | super(); 17 | this.state = { deltaY: 0 }; 18 | } 19 | 20 | render () { 21 | const props = this.props; 22 | const {state} = props; 23 | const {entireState} = props; 24 | 25 | let height = this._clampHeight(state.ySize - this.state.deltaY); 26 | // check state of `hide button` regarding current height 27 | this._checkHideButton(height); 28 | 29 | return ( 30 |
33 | 34 | 35 | 36 | 40 | 41 |
42 | ); 43 | } 44 | 45 | @bind 46 | _resizeHeight(deltaY) { 47 | const {state} = this.props; 48 | const {store} = this.context; 49 | 50 | // reset `isTransition` state that is responsible 51 | // for applying a className with transition enabled 52 | if (state.isTransition) { 53 | store.dispatch({ type: 'MAIN_PANEL_RESET_TRANSITION' }); 54 | } 55 | 56 | this.setState({ deltaY: this._clampDeltaY(deltaY) }); 57 | } 58 | 59 | @bind 60 | _resizeHeightEnd() { 61 | const {store} = this.context; 62 | const {deltaY} = this.state; 63 | 64 | const data = this._clampDeltaY(deltaY); 65 | this.setState({ deltaY: 0 }); 66 | store.dispatch({ type: 'MAIN_PANEL_SET_YSIZE', data }); 67 | } 68 | 69 | @bind 70 | _resizeHeightStart() { 71 | const {state} = this.props; 72 | 73 | if (state.ySize !== this._getMinHeight()) { 74 | const {store} = this.context; 75 | store.dispatch({ type: 'MAIN_PANEL_SAVE_YPREV' }); 76 | } 77 | } 78 | 79 | // HELPERS 80 | 81 | _getClassNames() { 82 | const {store} = this.context; 83 | const {state} = this.props; 84 | 85 | const className = CLASSES['main-panel']; 86 | const transitionClass = state.isTransition 87 | ? CLASSES['main-panel--transition'] : ''; 88 | 89 | return `${className} ${transitionClass}`; 90 | } 91 | 92 | _clampHeight(height) { return Math.max(this._getMinHeight(), height); } 93 | 94 | _clampDeltaY(deltaY) { 95 | const {ySize} = this.props; 96 | const minSize = this._getMinHeight(); 97 | return (ySize - deltaY <= minSize) ? ySize - minSize : deltaY; 98 | } 99 | 100 | _checkHideButton(height) { 101 | const {state} = this.props; 102 | const {store} = this.context; 103 | 104 | // if we drag the panel and it is in `isHidden` state, reset that state 105 | if (height > this._getMinHeight() && state.isHidden) { 106 | store.dispatch({ type: 'MAIN_PANEL_SET_HIDDEN', data: false }); 107 | } 108 | // if we drag the panel and it is not in `isHidden` state, set that state 109 | // and reset prevHeight to add user the ability to expand the panel, 110 | // otherwise it will stick at the bottom 111 | if (height === this._getMinHeight() && !state.isHidden) { 112 | store.dispatch({ type: 'MAIN_PANEL_SET_HIDDEN', data: true }); 113 | } 114 | } 115 | 116 | _getMinHeight() { return C.PLAYER_HEIGHT; } 117 | 118 | } 119 | 120 | export default MainPanel; 121 | -------------------------------------------------------------------------------- /app/js/components/main-panel/right-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | 4 | import HideButton from '../hide-button'; 5 | import ResizeHandle from '../resize-handle'; 6 | import TimelinePanel from '../timeline-panel'; 7 | 8 | const CLASSES = require('../../../css/blocks/right-panel.postcss.css.json'); 9 | require('../../../css/blocks/right-panel'); 10 | 11 | class RightPanel extends Component { 12 | render() { 13 | const {state} = this.props; 14 | const {mainPanel, points} = state; 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | @bind 26 | _onHideButton() { 27 | const {store} = this.context; 28 | 29 | store.dispatch({ type: 'MAIN_PANEL_HIDE_TOGGLE' }); 30 | } 31 | } 32 | 33 | export default RightPanel; 34 | -------------------------------------------------------------------------------- /app/js/components/point-timeline-line.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | import {classNames} from '../helpers/style-decorator'; 4 | 5 | const CLASSES = 6 | require('../../css/blocks/point-timeline-line.postcss.css.json'); 7 | require('../../css/blocks/point-timeline-line'); 8 | 9 | import SegmentTimeline from './segment-timeline'; 10 | 11 | @classNames(CLASSES) 12 | class PointTimelineLine extends Component { 13 | render () { 14 | const {state} = this.props; 15 | 16 | return ( 17 |
19 | 20 |
21 |
22 |
23 | { this._renderProperties(state) } 24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | _renderProperties(state) { 32 | const {props} = state; 33 | const results = []; 34 | 35 | const keys = Object.keys(props); 36 | for (let i = 0; i < keys.length; i++) { 37 | const key = keys[i]; 38 | results.push( this._renderProperty(key, props[key]) ); 39 | } 40 | return results; 41 | } 42 | 43 | _renderProperty(key, prop) { 44 | const {state, entireState} = this.props; 45 | const results = []; 46 | 47 | let prevSpot = prop[0]; 48 | for (let i = 0; i < prop.length; i++) { 49 | const spot = prop[i]; 50 | const meta = { id: state.id, prop: key, spotIndex: i }; 51 | results.push( 52 | 53 | ); 54 | prevSpot = spot; 55 | } 56 | 57 | return ( 58 |
59 | {results} 60 |
61 | ); 62 | } 63 | 64 | _getClassName(state) { 65 | const selectClass = (state.isSelected) ? 'is-selected': ''; 66 | const openClass = (state.isOpen) ? 'is-open': ''; 67 | 68 | return `point-timeline-line ${selectClass} ${openClass}`; 69 | } 70 | 71 | } 72 | 73 | export default PointTimelineLine; 74 | -------------------------------------------------------------------------------- /app/js/components/point.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | import Hammer from 'hammerjs'; 4 | import C from '../constants'; 5 | 6 | const CLASSES = require('../../css/blocks/point.postcss.css.json'); 7 | require('../../css/blocks/point'); 8 | 9 | class Point extends Component { 10 | // getInitialState() { return { deltaX: 0, deltaY: 0 }; } 11 | constructor() { 12 | super(); 13 | this.state = { deltaX: 0, deltaY: 0 }; 14 | } 15 | render () { 16 | const {state} = this.props; 17 | let [x, y] = this._getXY(); 18 | 19 | const style = { transform: `translate(${x}px, ${y}px)` }; 20 | 21 | return ( 22 |
27 | ); 28 | } 29 | 30 | _getXY() { 31 | const [x, y] = this._getCoords(); 32 | const {deltaX, deltaY} = this.state; 33 | 34 | return [ x + deltaX, y + deltaY ]; 35 | } 36 | 37 | _getCoords() { 38 | const {state, entireState} = this.props; 39 | const {selectedSpot, points} = entireState; 40 | 41 | if (selectedSpot.id == null) {return state.currentProps[C.POSITION_NAME];} 42 | 43 | const {id, prop, spotIndex, type} = selectedSpot; 44 | return points[id].props[prop][spotIndex][type].value; 45 | } 46 | 47 | _getClassName(state) { 48 | const selectClass = (state.isSelected) ? CLASSES['is-selected']: ''; 49 | return `${CLASSES['point']} ${selectClass}`; 50 | } 51 | 52 | componentDidMount() { 53 | const mc = new Hammer.Manager(this.base); 54 | mc.add(new Hammer.Pan); 55 | 56 | mc.on('pan', this._onPan); 57 | mc.on('panend', this._onPanEnd); 58 | } 59 | 60 | @bind 61 | _onPan(e) { 62 | const { deltaX, deltaY} = e; 63 | this._isPan = true; 64 | 65 | this.setState({ deltaX, deltaY }); 66 | } 67 | 68 | @bind 69 | _onPanEnd(e) { 70 | const {store} = this.context; 71 | const {state, entireState} = this.props; 72 | const {id} = state; 73 | const {selectedSpot} = entireState; 74 | const { deltaX, deltaY } = e; 75 | 76 | if (selectedSpot.id == null) { 77 | store.dispatch({ 78 | type: 'CHANGE_POINT_CURRENT_POSITION', data: { deltaX, deltaY, id } 79 | }); 80 | } else { 81 | store.dispatch({ 82 | type: 'UPDATE_SELECTED_SPOT', 83 | data: { ...selectedSpot, value: this._getXY() } 84 | }); 85 | } 86 | this.setState({ deltaX: 0, deltaY: 0 }); 87 | } 88 | 89 | @bind 90 | _onClick(e) { 91 | if (this._isPan) { return this._isPan = false; } 92 | const {state} = this.props; 93 | const {store} = this.context; 94 | 95 | store.dispatch({ type: 'SELECT_POINT', data: state.id }); 96 | } 97 | } 98 | 99 | export default Point; 100 | -------------------------------------------------------------------------------- /app/js/components/points-panel/point-line.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | 4 | const CLS = require('../../../css/blocks/point-line.postcss.css.json'); 5 | require('../../../css/blocks/point-line'); 6 | 7 | import PropertyLine from './property-line'; 8 | import PropertyLineAdd from './property-line-add'; 9 | import Icon from '../icon'; 10 | 11 | class PointLine extends Component { 12 | render () { 13 | const {state} = this.props; 14 | 15 | return ( 16 |
17 |
18 | {state.name} 19 |
20 | 21 |
23 |
24 | 25 |
26 |
27 | 28 |
30 |
31 | 32 |
33 |
34 |
35 | {this._renderProperties()} 36 |
37 |
38 | ); 39 | } 40 | 41 | _getClassName(state) { 42 | const openClass = (state.isOpen) ? CLS['is-open']: ''; 43 | const checkClass = (state.isSelected) ? CLS['is-check']: ''; 44 | return `${CLS['point-line']} ${openClass} ${checkClass}`; 45 | } 46 | 47 | _renderProperties() { 48 | const {state} = this.props; 49 | const {props} = state; 50 | const names = Object.keys(props); 51 | const results = []; 52 | 53 | for (let i = 0; i < names.length; i++) { 54 | const name = names[i]; 55 | results.push( 56 | 57 | ); 58 | } 59 | 60 | results.push(); 61 | 62 | return results; 63 | } 64 | 65 | @bind 66 | _onCheck() { 67 | const {state} = this.props; 68 | const {store} = this.context; 69 | 70 | // store.dispatch({ type: 'SELECT_POINT', data: state.id }); 71 | } 72 | 73 | @bind 74 | _onAddSpot() { 75 | const {state, entireState} = this.props; 76 | const {store} = this.context; 77 | 78 | const data = { id: state.id, time: entireState.progress }; 79 | store.dispatch({ type: 'ADD_SNAPSHOT', data }); 80 | } 81 | 82 | @bind 83 | _onOpen(e) { 84 | e.stopPropagation(); 85 | const {state} = this.props; 86 | const {store} = this.context; 87 | store.dispatch({ type: 'TOGGLE_OPEN_POINT', data: state.id }); 88 | } 89 | } 90 | 91 | export default PointLine; 92 | -------------------------------------------------------------------------------- /app/js/components/points-panel/points-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | const CLASSES = require('../../../css/blocks/points-panel.postcss.css.json'); 4 | require('../../../css/blocks/points-panel'); 5 | 6 | import PointLine from './point-line'; 7 | 8 | class PointsPanel extends Component { 9 | render () { 10 | const {state} = this.props; 11 | 12 | return ( 13 |
14 | {this._renderPoints(state)} 15 |
16 | ); 17 | } 18 | 19 | _renderPoints(state) { 20 | const {entireState} = this.props; 21 | const props = Object.keys(state); 22 | const points = []; 23 | for (let i=0; i< props.length; i++) { 24 | const key = props[i]; 25 | points.push(); 26 | } 27 | return points; 28 | } 29 | 30 | } 31 | 32 | export default PointsPanel; 33 | -------------------------------------------------------------------------------- /app/js/components/points-panel/property-line-add.babel.jsx: -------------------------------------------------------------------------------- 1 | import {h, Component} from 'preact'; 2 | import {bind} from 'decko'; 3 | import clamp from '../../helpers/clamp'; 4 | import resetEvent from '../../helpers/global-reset-event'; 5 | import ToolsPanelButton from '../tools-panel-button'; 6 | import {classNames, refs, compose} from '../../helpers/style-decorator'; 7 | 8 | const CLS = require('../../../css/blocks/property-line-add.postcss.css.json'); 9 | require('../../../css/blocks/property-line-add'); 10 | 11 | const EXIST_MESSAGE = 'already exist'; 12 | const DEFAULT_STATE = { 13 | count: 1, 14 | name: 'property name', 15 | isAdd: false, 16 | error: null 17 | }; 18 | 19 | @compose(classNames(CLS), refs) 20 | class PropertyLineAdd extends Component { 21 | // getInitialState() { 22 | // this.setState({ x: 0, y: 0 }); 23 | // return {...DEFAULT_STATE, error: this._isExist() ? EXIST_MESSAGE: null }; 24 | // } 25 | 26 | render () { 27 | const {name, count, error} = this.state; 28 | return ( 29 |
e.stopPropagation()}> 30 |
31 | {'+ add'} 32 |
33 |
34 |
35 | 37 | 38 |
39 | 41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | shouldComponentUpdate(_, nextState) { 48 | this._isFocus = !this.state.isAdd && nextState.isAdd; 49 | } 50 | 51 | componentDidUpdate() { 52 | if (this._isFocus) { 53 | this._name.focus && this._name.focus(); 54 | this._name.select && this._name.select(); 55 | } 56 | this._isFocus = false; 57 | } 58 | 59 | componentDidMount() { 60 | this.setState({ 61 | ...DEFAULT_STATE, 62 | error: this._isExist() ? EXIST_MESSAGE: null 63 | }); 64 | 65 | resetEvent.add((e) => { this.setState({ isAdd: false }); }); 66 | } 67 | 68 | @bind 69 | _onNameKeyUp(e) { 70 | if (e.which === 13) { return this._onSubmit(); } 71 | 72 | const name = e.target.value; 73 | const trimmedName = name.trim(); 74 | const error = (trimmedName.length <= 0) ? 'none-empty' 75 | : this._isExist(name) ? EXIST_MESSAGE : null; 76 | 77 | this.setState({ name, error }); 78 | } 79 | 80 | @bind 81 | _onCountKeyUp(e) { 82 | const code = e.which; 83 | if (code === 8) { return; } // backspace 84 | if (code === 13) { return this._onSubmit(); } 85 | 86 | const min = 1; 87 | const max = 4; 88 | 89 | if (code === 38 || code === 40) { 90 | const step = (e.which === 38) ? 1 : (e.which === 40) ? -1 : 0; 91 | const count = clamp(this.state.count + step, min, max); 92 | return this.setState({ count }); 93 | } 94 | 95 | const value = parseInt(e.target.value, 10); 96 | const count = clamp(value || this.state.count, min, max); 97 | this.setState({ count }); 98 | } 99 | 100 | _getClassName() { 101 | const isAdd = (this.state.isAdd) ? 'is-add' : ''; 102 | const valid = (this.state.error == null) ? 'is-valid' : ''; 103 | 104 | return `property-line-add ${isAdd} ${valid}`; 105 | } 106 | 107 | @bind 108 | _onSubmit() { 109 | if (this.state.error != null) { return; } 110 | 111 | const {state} = this.props; 112 | const {store} = this.context; 113 | const data = {...state, property: { ...this.state }}; 114 | 115 | const isExist = this._isExist(); 116 | const isDefault = this.state.name === DEFAULT_STATE.name; 117 | let error = (isDefault || isExist) ? EXIST_MESSAGE : null; 118 | this.setState({ ...DEFAULT_STATE, error }); 119 | store.dispatch({ type: 'ADD_POINT_PROPERTY', data }); 120 | } 121 | 122 | @bind 123 | _onLabelClick(e) { this.setState({ isAdd: true }); } 124 | 125 | _isExist(name=DEFAULT_STATE.name) { 126 | const {state} = this.props; 127 | return state.props[name] != null; 128 | } 129 | } 130 | 131 | export default PropertyLineAdd; 132 | -------------------------------------------------------------------------------- /app/js/components/points-panel/property-line.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | 4 | import Icon from '../icon'; 5 | 6 | import resetEvent from '../../helpers/global-reset-event'; 7 | 8 | const CLASSES = require('../../../css/blocks/property-line.postcss.css.json'); 9 | require('../../../css/blocks/property-line'); 10 | const isMatch = (spot, id, name) => { 11 | return spot.id === id && spot.prop === name; 12 | }; 13 | 14 | class PropertyLine extends Component { 15 | render () { 16 | const p = this.props; 17 | 18 | return ( 19 |
20 |
{p.name}
21 |
22 | {this._renderInputs()} 23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | ); 31 | } 32 | 33 | _renderInputs() { 34 | let value = this._getValue(); 35 | value = (value instanceof Array) ? value : [value]; 36 | 37 | const result = []; 38 | for (let i = 0; i < value.length; i++) { 39 | result.push( 40 | 45 | ); 46 | } 47 | return result; 48 | } 49 | 50 | @bind 51 | _onKeyDown(e) { 52 | const {store} = this.context; 53 | const {state, name, entireState} = this.props; 54 | const {id} = state; 55 | const {selectedSpot} = entireState; 56 | 57 | // if selected spot doesnt match the property line - 58 | // update the current value 59 | if (!isMatch(selectedSpot, id, name)) { return this._onKeyDownCurrent(e); } 60 | 61 | const target = e.target; 62 | const index = parseInt(target.getAttribute('data-index'), 10); 63 | const current = this._getValue(); 64 | 65 | // try to parse the input 66 | const parsed = parseInt(target.value, 10); 67 | // if fail to parse - set it to the current valid value 68 | const value = (parsed != null && !isNaN(parsed)) ? parsed : current[index]; 69 | 70 | // if property holds an array clone it 71 | const newValue = (current instanceof Array) ? [...current] : value; 72 | // and update the item by index 73 | if (newValue instanceof Array) { newValue[index] = value; } 74 | 75 | const data = { ...selectedSpot, value: newValue }; 76 | 77 | let step = (e.altKey) ? 10 : 1; 78 | if (e.shiftKey) { step *= 10; } 79 | 80 | switch (e.which) { 81 | case 38: { 82 | data.value[index] += step; 83 | return store.dispatch({ type: 'UPDATE_SELECTED_SPOT', data }); 84 | } 85 | 86 | case 40: { 87 | data.value[index] -= step; 88 | return store.dispatch({ type: 'UPDATE_SELECTED_SPOT', data }); 89 | } 90 | 91 | default: { 92 | store.dispatch({ type: 'UPDATE_SELECTED_SPOT', data }); 93 | } 94 | } 95 | } 96 | 97 | _onKeyDownCurrent(e) { 98 | const {store} = this.context; 99 | const {state, name, entireState} = this.props; 100 | const {selectedSpot} = entireState; 101 | 102 | const target = e.target; 103 | const index = parseInt(target.getAttribute('data-index'), 10); 104 | const current = this._getValue(); 105 | 106 | // try to parse the input 107 | const parsed = parseInt(target.value, 10); 108 | // if fail to parse - set it to the current valid value 109 | const value = (parsed != null && !isNaN(parsed)) ? parsed : current[index]; 110 | 111 | // if property holds an array clone it 112 | const newValue = (current instanceof Array) ? [...current] : value; 113 | // and update the item by index 114 | if (newValue instanceof Array) { newValue[index] = value; } 115 | 116 | const data = { id: state.id, name, value: newValue }; 117 | let step = (e.altKey) ? 10 : 1; 118 | if (e.shiftKey) { step *= 10; } 119 | 120 | switch (e.which) { 121 | case 38: { 122 | data.value[index] += step; 123 | return store.dispatch({ type: 'UPDATE_SELECTED_SPOT_CURRENT', data }); 124 | } 125 | 126 | case 40: { 127 | data.value[index] -= step; 128 | return store.dispatch({ type: 'UPDATE_SELECTED_SPOT_CURRENT', data }); 129 | } 130 | 131 | default: { 132 | return store.dispatch({ type: 'UPDATE_SELECTED_SPOT_CURRENT', data }); 133 | } 134 | } 135 | } 136 | 137 | _getValue() { 138 | const {name, state, entireState} = this.props; 139 | const {selectedSpot} = entireState; 140 | const {currentProps, id} = state; 141 | 142 | // if selected spot matches the property line - 143 | // get the selected spot values 144 | if (isMatch(selectedSpot, id, name)) { 145 | const {id, prop, spotIndex, type} = selectedSpot; 146 | return entireState.points[id].props[prop][spotIndex][type].value; 147 | } 148 | 149 | return currentProps[name]; 150 | } 151 | 152 | @bind 153 | _onAddSpot(e) { 154 | const {store} = this.context; 155 | const p = this.props; 156 | const {state, entireState} = p; 157 | 158 | store.dispatch({ 159 | type: 'ADD_PROPERTY_SEGMENT', 160 | data: { id: p.id, name: p.name, time: entireState.progress } 161 | }); 162 | } 163 | } 164 | 165 | export default PropertyLine; 166 | -------------------------------------------------------------------------------- /app/js/components/resize-handle.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import Hammer from 'hammerjs'; 3 | import propagating from 'propagating-hammerjs'; 4 | import Icon from './icon'; 5 | 6 | const CLASSES = require('../../css/blocks/resize-handle.postcss.css.json'); 7 | require('../../css/blocks/resize-handle'); 8 | 9 | class ResizeHandle extends Component { 10 | render() { 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | 18 | componentDidMount() { 19 | const mc = propagating(new Hammer.Manager(this.base)); 20 | const p = this.props; 21 | const {store} = this.context; 22 | 23 | mc.add(new Hammer.Pan({ threshold: 0 })); 24 | mc.on('pan', (e) => { p.onResize(e.deltaY); e.stopPropagation(); }) 25 | 26 | .on('panstart', (e) => { 27 | p.onResizeStart && p.onResizeStart(e); 28 | e.stopPropagation(); 29 | }).on('panend', (e) => { 30 | p.onResizeEnd && p.onResizeEnd(e); 31 | e.stopPropagation(); 32 | }); 33 | 34 | } 35 | } 36 | 37 | export default ResizeHandle; 38 | -------------------------------------------------------------------------------- /app/js/components/segment-timeline.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | import Hammer from 'hammerjs'; 4 | import C from '../constants'; 5 | import {classNames} from '../helpers/style-decorator'; 6 | 7 | const CLASSES = require('../../css/blocks/segment-timeline.postcss.css.json'); 8 | require('../../css/blocks/segment-timeline'); 9 | 10 | import Spot from './spot'; 11 | import Easing from './easing'; 12 | 13 | @classNames(CLASSES) 14 | class SegmentTimeline extends Component { 15 | render () { 16 | const {state, meta, entireState} = this.props; 17 | 18 | return ( 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default SegmentTimeline; 33 | -------------------------------------------------------------------------------- /app/js/components/spot.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | import Hammer from 'hammerjs'; 4 | import C from '../constants'; 5 | import isSelectedByConnection from '../helpers/is-selected-by-connection'; 6 | import {classNames, refs, compose} from '../helpers/style-decorator'; 7 | 8 | const CLASSES = require('../../css/blocks/spot.postcss.css.json'); 9 | require('../../css/blocks/spot'); 10 | 11 | @compose(classNames(CLASSES), refs) 12 | class Spot extends Component { 13 | render () { 14 | const {meta, type, state} = this.props; 15 | const {delay, duration} = state; 16 | const {dDelay, dDuration} = this.state; 17 | 18 | const delayWidth = delay/10 + dDelay; 19 | const durationWidth = duration/10 + dDuration; 20 | 21 | const style = { 22 | width: `${(type === 'start') ? delayWidth : durationWidth}em` 23 | }; 24 | 25 | return ( 26 |
27 |
28 | {this.props.children} 29 |
30 | ); 31 | } 32 | 33 | _getClassName() { 34 | const {type} = this.props; 35 | 36 | const endClass = (type === 'end') ? 'spot--end' : ''; 37 | const selectClass = this._isSelected() ? 'is-selected' : ''; 38 | 39 | return `spot ${endClass} ${selectClass} ${this._getEasingClass()}`; 40 | } 41 | 42 | _getEasingClass() { 43 | const {type, state} = this.props; 44 | if (type === 'start') { return ''; } 45 | 46 | const durationWidth = state.duration/10 + this.state.dDuration; 47 | return (durationWidth >= 80) ? 'is-easing' : ''; 48 | } 49 | 50 | _isSelected() { 51 | const {type, state, entireState, meta} = this.props; 52 | const {selectedSpot, points} = entireState; 53 | const {id, spotIndex, type: selType, prop } = selectedSpot; 54 | 55 | return ( meta.id === id && 56 | type === selType && 57 | meta.spotIndex === spotIndex && 58 | meta.prop === prop 59 | ) || isSelectedByConnection({...meta, type}, selectedSpot, points); 60 | } 61 | 62 | componentWillMount() { this.setState({ dDelay: 0, dDuration: 0 }); } 63 | 64 | componentDidMount() { 65 | const mc = new Hammer.Manager(this._dot); 66 | mc.add(new Hammer.Pan); 67 | mc.add(new Hammer.Tap); 68 | mc.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL }); 69 | 70 | mc.on('pan', this._pan); 71 | mc.on('panend', this._panEnd); 72 | mc.on('tap', this._tap); 73 | } 74 | 75 | @bind 76 | _pan(e) { 77 | const direction = this.props.type; 78 | if (direction === 'end') { 79 | const threshold = C.MIN_DURATION; 80 | const min = - this.props.duration + threshold; 81 | const dDuration = (e.deltaX*10 < min) ? min/10 : e.deltaX; 82 | this.setState({ dDuration }); 83 | } 84 | if (direction === 'start') { this.setState({ dDelay: e.deltaX }); } 85 | } 86 | 87 | @bind 88 | _panEnd(e) { 89 | const {meta} = this.props; 90 | const {store} = this.context; 91 | 92 | store.dispatch({ 93 | type: 'SHIFT_SEGMENT', 94 | data: { 95 | delay: this.state.dDelay*10, 96 | duration: this.state.dDuration*10, 97 | ...meta 98 | } 99 | }); 100 | 101 | this.setState({ dDelay: 0, dDuration: 0 }); 102 | } 103 | 104 | @bind 105 | _tap(e) { 106 | const {store} = this.context; 107 | const {meta, type} = this.props; 108 | 109 | store.dispatch({ type: 'SET_SELECTED_SPOT', data: { type, ...meta } }); 110 | } 111 | } 112 | 113 | export default Spot; 114 | -------------------------------------------------------------------------------- /app/js/components/timeline-editor.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import {bind} from 'decko'; 3 | 4 | import MainPanel from './main-panel/main-panel'; 5 | import Icons from './icons'; 6 | import InsertPoint from './insert-point'; 7 | import Point from './point'; 8 | import C from '../constants'; 9 | import {classNames} from '../helpers/style-decorator'; 10 | 11 | // const CLASSES = require('../../css/blocks/timeline-editor.postcss.css.json'); 12 | require('../../css/blocks/timeline-editor'); 13 | 14 | @classNames(require('../../css/blocks/timeline-editor.postcss.css.json')) 15 | class TimelineEditor extends Component { 16 | render () { 17 | const {store} = this.context; 18 | const state = store.getState(); 19 | this._state = state; 20 | 21 | return ( 22 |
23 | 24 |
{this._renderPoints()}
25 |
26 | 27 | 28 |
29 |
30 | ); 31 | } 32 | 33 | _renderPoints() { 34 | const results = []; 35 | const {points} = this._state; 36 | const props = Object.keys(points); 37 | 38 | for (let i = 0; i < props.length; i++) { 39 | const key = props[i]; 40 | results.push( 41 | 42 | ); 43 | } 44 | 45 | return results; 46 | } 47 | 48 | componentDidMount() { 49 | const {store} = this.context; 50 | store.subscribe(this.forceUpdate.bind(this)); 51 | document.addEventListener('mousemove', this._docMouseMove); 52 | } 53 | 54 | @bind 55 | _mouseMove(e) { 56 | /* we cannot `stopPropagation` the event, because `hammerjs` 57 | will not be able to work properly on `resize-handle`, so we 58 | set the `isTimelinePanel` flag instead indicating that we are 59 | inside the `timeline-editor` panel 60 | */ 61 | e.isTimelinePanel = true; 62 | const {store} = this.context; 63 | const {controls} = this._state; 64 | if (controls.isMouseInside) { return; } 65 | 66 | store.dispatch({ type: 'CONTROLS_SET_MOUSE_INSIDE', data: true }); 67 | } 68 | 69 | @bind 70 | _docMouseMove(e) { 71 | if (e.isTimelinePanel) { return; } 72 | const {store} = this.context; 73 | const {controls} = this._state; 74 | 75 | if (controls.isMouseInside) { 76 | store.dispatch({ type: 'CONTROLS_SET_MOUSE_INSIDE', data: false }); 77 | } 78 | } 79 | } 80 | 81 | export default TimelineEditor; 82 | -------------------------------------------------------------------------------- /app/js/components/timeline-handle.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import Hammer from 'hammerjs'; 3 | 4 | import Icon from './icon'; 5 | import clamp from '../helpers/clamp'; 6 | 7 | const CLASSES = require('../../css/blocks/timeline-handle.postcss.css.json'); 8 | require('../../css/blocks/timeline-handle'); 9 | 10 | class TimelineHandle extends Component { 11 | // getInitialState() { return { deltaX: 0 }; } 12 | render() { 13 | const {state} = this.props; 14 | const shift = (state.progress + this.state.deltaX)/10; 15 | const style = { transform: `translateX(${shift}em)` }; 16 | 17 | return ( 18 |
20 |
{ this._head = el; } }> 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | componentWillMount() { this.setState({ deltaX: 0 }); } 29 | 30 | componentDidMount() { 31 | const mc = new Hammer.Manager(this._head); 32 | mc.add(new Hammer.Pan); 33 | 34 | const {store} = this.context; 35 | mc.on('pan', (e) => { 36 | this.setState({ deltaX: this._clampDeltaX(10*e.deltaX, 7000) }); 37 | }); 38 | 39 | mc.on('panstart', (e) => { 40 | store.dispatch({ type: 'RESET_SELECTED_SPOT' }); 41 | }); 42 | 43 | mc.on('panend', (e) => { 44 | const {state} = this.props; 45 | const data = state.progress + this.state.deltaX; 46 | store.dispatch({ type: 'SET_PROGRESS', data }); 47 | this.setState({ deltaX: 0 }); 48 | }); 49 | } 50 | 51 | _clampDeltaX(deltaX, max) { 52 | const {state} = this.props; 53 | deltaX = (state.progress + deltaX < 0) ? -state.progress : deltaX; 54 | deltaX = (state.progress + deltaX > max) 55 | ? max-state.progress : deltaX; 56 | return deltaX; 57 | } 58 | 59 | } 60 | 61 | export default TimelineHandle; 62 | -------------------------------------------------------------------------------- /app/js/components/timeline-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | import C from '../constants'; 4 | 5 | import TimelineHandle from './timeline-handle'; 6 | 7 | const CLASSES = require('../../css/blocks/timeline-panel.postcss.css.json'); 8 | require('../../css/blocks/timeline-panel'); 9 | 10 | class TimelinePanel extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | const DASHES_PER_SEC = 20; 15 | this.state = { 16 | scale: props.scale || 1, 17 | dashesPerSec: DASHES_PER_SEC, 18 | DASH_STEP: 100*(1/DASHES_PER_SEC) 19 | }; 20 | 21 | this._dashesCnt = props.time * this.state.dashesPerSec; 22 | } 23 | 24 | render () { 25 | return ( 26 |
27 | { this._timeline } 28 |
29 | ); 30 | } 31 | 32 | componentWillMount() { 33 | this._timeline = this._createTimeline(); 34 | } 35 | 36 | // will be removed when `preact` issue with nested `svg` will be fixed 37 | componentDidMount() { this._svg.classList.add(CLASSES['main-svg']); } 38 | 39 | @bind 40 | _createTimeline() { 41 | const dashes = this._compileDashes(); 42 | const pointerValues = this._compileLabels(); 43 | const {scale} = this.state; 44 | 45 | let timeline = ( 46 | { this._svg = el; }}> 47 | 48 | {dashes} 49 | {pointerValues} 50 | 51 | 52 | ); 53 | return timeline; 54 | } 55 | 56 | @bind 57 | _createDash(dashNumber) { 58 | const {dashesPerSec, scale, DASH_STEP} = this.state; 59 | const dashType = this._getDashType(dashNumber, dashesPerSec); 60 | 61 | const color = dashType === 'large' ? '#fff' : '#ae9bae'; 62 | const height = dashType === 'large' ? 7 : (dashType === 'middle') ? 6 : 4; 63 | const x = DASH_STEP * dashNumber; 64 | const y = C.TIMELINE_HEIGHT - height; 65 | 66 | return ( 67 | 68 | ); 69 | } 70 | 71 | _getDashType(dashNumber, dashesPerSec) { 72 | const isLarge = !(dashNumber % (dashesPerSec / 2)) || (dashNumber === 0); 73 | const isMiddle = !(dashNumber % (dashesPerSec / 4)); 74 | return isLarge ? 'large' : isMiddle ? 'middle' : 'small'; 75 | } 76 | 77 | _compileDashes() { 78 | let dashes = []; 79 | for (let j = 0; j <= this._dashesCnt; j++) { 80 | dashes.push(this._createDash(j)); 81 | } 82 | 83 | return dashes; 84 | } 85 | 86 | _compileLabels() { 87 | const labels = []; 88 | const {dashesPerSec, scale, DASH_STEP} = this.state; 89 | 90 | for (let j = 0, value = 0; j <= this._dashesCnt; j += dashesPerSec/2, value += 500) { 91 | const textAnchor = (j === 0) ? 'start' : 'middle'; 92 | const x = DASH_STEP * j; 93 | const y = C.TIMELINE_HEIGHT / 2; 94 | 95 | labels.push( 96 | 97 | 98 | {value} 99 | 100 | 101 | ); 102 | } 103 | 104 | return labels; 105 | } 106 | } 107 | 108 | export default TimelinePanel; 109 | -------------------------------------------------------------------------------- /app/js/components/timelines-panel.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | 4 | import PointTimelineLine from './point-timeline-line'; 5 | // import resetEvent from '../helpers/global-reset-event'; 6 | 7 | const CLASSES = require('../../css/blocks/timelines-panel.postcss.css.json'); 8 | require('../../css/blocks/timelines-panel'); 9 | 10 | class TimelinePanel extends Component { 11 | render () { 12 | 13 | return ( 14 |
15 | { this._renderTimelines() } 16 |
17 | ); 18 | } 19 | 20 | _renderTimelines() { 21 | const {state} = this.props; 22 | const {points} = state; 23 | const keys = Object.keys(points); 24 | 25 | const results = []; 26 | for (let i = 0; i < keys.length; i++) { 27 | const key = keys[i]; 28 | results.push( 29 | 30 | ); 31 | } 32 | 33 | return results; 34 | } 35 | // 36 | // componentDidMount() { 37 | // const {store} = this.context; 38 | // console.log('b'); 39 | // resetEvent.add( (e) => { 40 | // store.dispatch({ type: 'RESET_SELECTED_SPOT' }); 41 | // console.log('a'); 42 | // }); 43 | // } 44 | } 45 | 46 | export default TimelinePanel; 47 | -------------------------------------------------------------------------------- /app/js/components/tools-panel-button.babel.jsx: -------------------------------------------------------------------------------- 1 | import {h, Component} from 'preact'; 2 | import Icon from './icon'; 3 | 4 | const C = require('../../css/blocks/tools-panel-button.postcss.css.json'); 5 | require('../../css/blocks/tools-panel-button'); 6 | 7 | class ToolsPanelButton extends Component { 8 | render() { 9 | return ( 10 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | _onSubmit(e) { 20 | if (typeof this.props.onClick === 'function') { 21 | this.props.onClick(e); 22 | } 23 | } 24 | } 25 | 26 | export default ToolsPanelButton; 27 | -------------------------------------------------------------------------------- /app/js/components/tools-panel/button.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | const CLASSES = require('../../../css/blocks/tools-panel.postcss.css.json'); 4 | require('../../../css/blocks/tools-panel'); 5 | 6 | class ToolsPanelButton extends Component { 7 | render() { 8 | const p = this.props; 9 | const className = `${CLASSES['button']} ${p.className || ''}`; 10 | return ( 11 |
12 | {p.children} 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default ToolsPanelButton; 19 | -------------------------------------------------------------------------------- /app/js/components/tools-panel/index.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { bind } from 'decko'; 3 | 4 | import Point from './point'; 5 | import Button from './button'; 6 | import Icon from '../icon'; 7 | 8 | // import setSelected from '../../actions/set-selected'; 9 | // import ResizeHandle from '../resize-handle'; 10 | // import TimelinePanel from '../timeline-panel'; 11 | 12 | const CLASSES = require('../../../css/blocks/tools-panel.postcss.css.json'); 13 | require('../../../css/blocks/tools-panel'); 14 | 15 | /* TODO: 16 | [x] refactor to emit `action creators` in event handlers; 17 | */ 18 | class ToolsPanel extends Component { 19 | render() { 20 | return ( 21 |
22 | 23 | 26 | 29 | 30 | 33 | 34 | 35 | 36 | {/* 37 | 40 | */} 41 |
42 | ); 43 | } 44 | 45 | _getClassFor(type) { 46 | const {state} = this.props; 47 | const {selected} = state; 48 | 49 | return (selected === type) ? CLASSES['is-active'] : ''; 50 | } 51 | 52 | @bind 53 | _setPlus(e) { 54 | const {store} = this.context; 55 | store.dispatch({ type: 'TOOLS_SET_SELECTED', data: 'plus' }); 56 | // store.dispatch(setSelected('OBJECT')); 57 | } 58 | 59 | @bind 60 | _setHtml(e) { 61 | const {store} = this.context; 62 | store.dispatch({ type: 'TOOLS_SET_SELECTED', data: 'html' }); 63 | // store.dispatch(setSelected('HTML')); 64 | } 65 | } 66 | 67 | export default ToolsPanel; 68 | -------------------------------------------------------------------------------- /app/js/components/tools-panel/point.babel.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | const CLASSES = require('../../../css/blocks/tools-panel.postcss.css.json'); 4 | require('../../../css/blocks/tools-panel'); 5 | 6 | class InsertPoint extends Component { 7 | render() { 8 | const p = this.props; 9 | const className = `${CLASSES['point']} ${p.className || ''}`; 10 | 11 | return ( 12 |
13 | ); 14 | } 15 | } 16 | 17 | export default InsertPoint; 18 | -------------------------------------------------------------------------------- /app/js/constants.babel.js: -------------------------------------------------------------------------------- 1 | /* 2 | Global constants. 3 | */ 4 | export default { 5 | NAME: 'MOJS_TIMLINE_EDITOR_Hjs891ksPP', 6 | /* defines if need to persist the state of the editor in localStorage */ 7 | IS_PERSIST_STATE: true, 8 | /* height of `mojs-timeline-player` module */ 9 | PLAYER_HEIGHT: 40, 10 | /* height of a timeline line */ 11 | TIMELINE_HEIGHT: 22, 12 | /* min safe duration of a tween or timeline(2.5 frames)*/ 13 | MIN_DURATION: 40, 14 | /* defines the period in which start and end points 15 | on a timeline will be selected. 16 | */ 17 | SPOT_SELECTION_GAP: 20, 18 | POSITION_NAME: 'x : y' 19 | }; 20 | -------------------------------------------------------------------------------- /app/js/helpers/add-pointer-down.babel.js: -------------------------------------------------------------------------------- 1 | export default (el, fn) => { 2 | if (window.navigator.msPointerEnabled) { 3 | el.addEventListener('MSPointerDown', fn); 4 | } else if ( window.ontouchstart !== undefined ) { 5 | el.addEventListener('touchstart', fn); 6 | el.addEventListener('mousedown', fn); 7 | } else { 8 | el.addEventListener('mousedown', fn); 9 | } 10 | }; -------------------------------------------------------------------------------- /app/js/helpers/add-pointer-up.babel.js: -------------------------------------------------------------------------------- 1 | export default (el, fn) => { 2 | if (window.navigator.msPointerEnabled) { 3 | el.addEventListener('MSPointerUp', fn); 4 | } else if ( window.ontouchstart !== undefined ) { 5 | el.addEventListener('touchend', fn); 6 | el.addEventListener('mouseup', fn); 7 | } else { 8 | el.addEventListener('mouseup', fn); 9 | } 10 | }; -------------------------------------------------------------------------------- /app/js/helpers/add-unload.babel.js: -------------------------------------------------------------------------------- 1 | /* 2 | Function to add cross-browser `unload` event. 3 | @param {Function} Callback for the event. 4 | */ 5 | export default (fn) => { 6 | const unloadEvent = ('onpagehide' in window) ? 'pagehide' : 'beforeunload'; 7 | window.addEventListener( unloadEvent, fn); 8 | }; -------------------------------------------------------------------------------- /app/js/helpers/change.babel.js: -------------------------------------------------------------------------------- 1 | 2 | // const point1 = createPoint({ x: 10, y: 300 }); 3 | // const point2 = createPoint({ x: 100, y: 200 }); 4 | // const point3 = createPoint({ x: 30, y: 20 }); 5 | // 6 | // const OBJ = { 7 | // [point1.id]: point1, 8 | // [point2.id]: point2, 9 | // [point3.id]: point3 10 | // }; 11 | 12 | // console.log(change(OBJ, [point2.id, 'props', 'x', 0, 'end', 'isSelected'], (state) => !state )[point2.id]); 13 | 14 | import getLast from './get-last'; 15 | 16 | const copy = (obj) => { 17 | const type = typeof obj; 18 | return (obj instanceof Array) 19 | ? [...obj] 20 | : (type=== 'object') ? {...obj} : obj; 21 | }; 22 | 23 | export default (obj, path, value) => { 24 | const newState = copy(obj); 25 | let current = newState; 26 | 27 | // copy everything in the current path 28 | for (let i = 0; i < path.length-1; i++) { 29 | const point = path[i]; 30 | current[point] = copy(current[point]); 31 | current = current[point]; 32 | } 33 | 34 | // update the `last property` in the `path` 35 | const leaf = getLast(path); 36 | current[leaf] = (typeof value === 'function') 37 | ? value(copy(current[leaf])) : value; 38 | 39 | return newState; 40 | }; 41 | -------------------------------------------------------------------------------- /app/js/helpers/clamp.babel.js: -------------------------------------------------------------------------------- 1 | 2 | /* Function to clamp some value 3 | @param {Number} Value to clamp. 4 | @param {Number} Min clamp bound. 5 | @param {Number} Max clamp bound. 6 | @returns {Number} Clamped value. 7 | */ 8 | 9 | const clamp = (value, min, max) => { 10 | return Math.min(Math.max(value, min), max); 11 | }; 12 | 13 | export default clamp; 14 | -------------------------------------------------------------------------------- /app/js/helpers/class-name.babel.js: -------------------------------------------------------------------------------- 1 | 2 | export default (CLASS) => { 3 | return (string) => { 4 | string = string.trim(); 5 | string = string.replace(/\s+/, ' '); 6 | 7 | const split = string.split(' '); 8 | let str = ''; 9 | for (let i = 0; i < split.length; i++) { 10 | const className = split[i]; 11 | if (className) { 12 | str += `${CLASS[className]} `; 13 | } 14 | } 15 | return str; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /app/js/helpers/create-point.babel.js: -------------------------------------------------------------------------------- 1 | 2 | import C from '../constants'; 3 | import makeID from './makeID'; 4 | import createSegment from './create-segment'; 5 | 6 | export default (data, i=0) => { 7 | const { x, y, name, time } = data; 8 | 9 | return { 10 | id: makeID(), 11 | name: name || `point${i+1}`, 12 | isOpen: true, 13 | isSelected: false, 14 | currentProps: { [C.POSITION_NAME]: [x, y] }, 15 | // selectedSpot: { 16 | // prop: null, 17 | // segment: 0, 18 | // type: null 19 | // }, 20 | props: { 21 | [C.POSITION_NAME]: [ createSegment({ 22 | startValue: [x, y], 23 | endValue: [x, y], 24 | delay: time 25 | }) ] 26 | } 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /app/js/helpers/create-segment.babel.js: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | 3 | import createSpot from './create-spot'; 4 | import fallback from './fallback'; 5 | 6 | export default (o={}) => { 7 | const delay = o.delay || 0; 8 | const duration = o.duration || C.MIN_DURATION; 9 | const start = o.start || 0; 10 | 11 | return { 12 | index: fallback(o.index, 0), 13 | start: createSpot({ 14 | time: start, 15 | value: o.startValue, 16 | connected: 'prev' 17 | }), 18 | end: createSpot({ 19 | time: start + delay + duration, 20 | value: o.endValue, 21 | connected: 'next' 22 | }), 23 | easing: 'none', 24 | isChanged: false, // if was changed by the user 25 | isSelected: false, 26 | delay, duration 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /app/js/helpers/create-spot.babel.jsx: -------------------------------------------------------------------------------- 1 | // import C from '../constants'; 2 | 3 | export default (o={}) => { 4 | return { 5 | time: 0, 6 | value: 0, 7 | connected: null, 8 | isSelected: false, 9 | ...o 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /app/js/helpers/decorators/builder.babel.js: -------------------------------------------------------------------------------- 1 | import isString from '../is-string'; 2 | 3 | const pushChildren = (item, stack) => { 4 | if (!item || !item.children) { return; } 5 | 6 | const {children} = item; 7 | for (let i = 0; i < children.length; i++) { 8 | const child = children[i]; 9 | if (!child.__isDecoratorApplied && !isString(child)) { stack.push(child); } 10 | } 11 | }; 12 | 13 | /* Function to apply hash name classes to the rendered VNode tree. */ 14 | const applyDecorators = function applyDecorators(renderResult, funs=[]) { 15 | if (typeof renderResult !== 'object' || renderResult == null) { return; } 16 | 17 | const stack = [renderResult]; 18 | while (stack.length > 0) { 19 | const item = stack.pop(); 20 | if (item.__isDecoratorApplied || isString(item)) { continue; } 21 | 22 | for (let i = 0; i < funs.length; i++) { 23 | funs[i].call(this, item.attributes); 24 | } 25 | 26 | item.__isDecoratorApplied = true; 27 | pushChildren(item, stack); 28 | } 29 | 30 | return renderResult; 31 | }; 32 | 33 | const builder = (functions) => { 34 | return (Component) => { 35 | return class StyledComponent extends Component { 36 | render() { 37 | // get original render 38 | let renderResult = super.render(); 39 | // apply functions 40 | applyDecorators.call(this, renderResult, functions); 41 | return renderResult; 42 | } 43 | }; 44 | }; 45 | }; 46 | 47 | export default builder; 48 | -------------------------------------------------------------------------------- /app/js/helpers/decorators/class-names.babel.js: -------------------------------------------------------------------------------- 1 | /* Function to parse a string and suppress `classNames` with `hash names` 2 | from the `CLASSES`. If there is no such `hash name` - leave the className 3 | as is. 4 | */ 5 | export default function classNames(CLASSES) { 6 | return (attrs) => { 7 | if (!attrs || (!attrs.class && !attrs.className)) { return; } 8 | const string = (attrs.class || attrs.className).trim(); 9 | const split = string.split(' '); 10 | 11 | let str = ''; 12 | for (let i = 0; i < split.length; i++) { 13 | const className = split[i]; 14 | if (className) { 15 | const hash = CLASSES[className]; 16 | str += `${(hash == null) ? className : hash} `; 17 | } 18 | } 19 | if (attrs.class) { attrs.class = str; } 20 | else { attrs.className = str; } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /app/js/helpers/decorators/refs.babel.js: -------------------------------------------------------------------------------- 1 | import isString from '../is-string'; 2 | 3 | /* Function to override string `ref` with automatic function. */ 4 | export default function refs(attrs, CLASSES) { 5 | if (!attrs) { return; } 6 | const {ref} = attrs; 7 | if (!ref || typeof ref === 'function') { return; } 8 | 9 | if (isString(ref)) { attrs.ref = el => this[ref] = el; } 10 | } 11 | -------------------------------------------------------------------------------- /app/js/helpers/fallback.babel.js: -------------------------------------------------------------------------------- 1 | 2 | export default (value, fallback) => { 3 | return (value == null) ? fallback : value; 4 | }; 5 | -------------------------------------------------------------------------------- /app/js/helpers/get-last.babel.js: -------------------------------------------------------------------------------- 1 | 2 | export default (arr) => { return arr[arr.length-1]; }; 3 | -------------------------------------------------------------------------------- /app/js/helpers/global-reset-event.babel.js: -------------------------------------------------------------------------------- 1 | import makeID from './makeID'; 2 | const listeners = {}; 3 | 4 | const onClick = (e) => { 5 | const keys = Object.keys(listeners); 6 | for (let i = 0; i < keys.length; i++) { 7 | const key = keys[i]; 8 | const listener = listeners[key]; 9 | if (typeof listener === 'function') { listener(e); } 10 | } 11 | }; 12 | 13 | const add = (fun) => { 14 | const id = makeID(); 15 | listeners[id] = fun; 16 | return id; 17 | }; 18 | 19 | const remove = (id) => { delete listeners[id]; }; 20 | 21 | document.addEventListener('click', onClick); 22 | export default { add, remove }; 23 | -------------------------------------------------------------------------------- /app/js/helpers/is-selected-by-connection.babel.js: -------------------------------------------------------------------------------- 1 | 2 | export default (meta, selectedSpot, points) => { 3 | if (selectedSpot.id == null) { return false; } 4 | const {id, spotIndex, type: selType, prop } = selectedSpot; 5 | const pointsLen = Object.keys(points).length; 6 | const spot = points[id].props[prop][spotIndex][selType]; 7 | 8 | let isSelected = false; 9 | if (spot.connected === 'prev' && spotIndex >= 1) { 10 | isSelected = (meta.spotIndex === spotIndex-1 && 11 | meta.id === id && 12 | meta.type === 'end' && 13 | meta.prop === prop 14 | ); 15 | } 16 | 17 | if (spot.connected === 'next') { 18 | isSelected = (meta.spotIndex === spotIndex+1 && 19 | meta.id === id && 20 | meta.type === 'start' && 21 | meta.prop === prop 22 | ); 23 | } 24 | return isSelected; 25 | }; 26 | -------------------------------------------------------------------------------- /app/js/helpers/is-string.babel.js: -------------------------------------------------------------------------------- 1 | export default (str) => { return typeof str == 'string'; }; 2 | -------------------------------------------------------------------------------- /app/js/helpers/makeID.babel.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | 3 | export default () => { return md5( `${Math.random()}${Math.random()}` ); }; 4 | -------------------------------------------------------------------------------- /app/js/helpers/persist.babel.js: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | import addUnload from './add-unload'; 3 | 4 | /* 5 | Function to store state into localStorage on page `unload` 6 | and restore it on page `load`. 7 | @param {Object} Redux store. 8 | */ 9 | export default (store) => { 10 | if (C.IS_PERSIST_STATE) { 11 | // save to localstorage 12 | addUnload(()=> { 13 | const preState = store.getState(); 14 | try { 15 | localStorage.setItem(C.NAME, JSON.stringify( preState ) ); 16 | } catch (e) { console.error(e); } 17 | }); 18 | // load from localstorage 19 | try { 20 | const stored = localStorage.getItem(C.NAME); 21 | if ( stored ) { 22 | store.dispatch({ type: 'SET_APP_STATE', data: JSON.parse(stored) }); 23 | } 24 | } catch (e) { 25 | console.error(e); 26 | } 27 | } else { 28 | try { 29 | localStorage.removeItem(C.NAME); 30 | } catch (e) { console.error(e); } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/js/helpers/style-decorator.babel.js: -------------------------------------------------------------------------------- 1 | import isString from './is-string'; 2 | import builder from './decorators/builder'; 3 | import refsFunction from './decorators/refs'; 4 | import classNamesFunction from './decorators/class-names'; 5 | 6 | /* Decorator: `classNames`. 7 | Overrides clean class names with CSS Modules `hash` classes. 8 | @param {CLASSES} CSS Modules hash classes map 9 | @sample: 10 | @classNames(CLASSES) 11 | class SomeComponent extends Component { 12 | render() { 13 | return (
); 14 | } 15 | } 16 | // `some-class` will be overwritten with `hash` from the `CLASSES` 17 | */ 18 | const classNamesDecorator = function classNamesDecorator(CLASSES) { 19 | const fun = classNamesFunction(CLASSES); 20 | const decorator = builder([fun]); 21 | decorator.__decorFunction = fun; 22 | return decorator; 23 | }; 24 | export {classNamesDecorator as classNames}; 25 | 26 | /* Decorator: `refs`. 27 | Overrides clean string `_refs` with reference to the rendered element 28 | @sample: 29 | @refs 30 | class SomeComponent extends Component {} 31 | */ 32 | const refsDecorator = builder([refsFunction]); 33 | refsDecorator.__decorFunction = refsFunction; 34 | export {refsDecorator as refs}; 35 | 36 | /* Decorator: `all`. 37 | Includes all decorators at once. 38 | @sample: 39 | @all(CLASSES) 40 | class SomeComponent extends Component {} 41 | */ 42 | export function all(CLASSES) { 43 | return builder([classNamesFunction(CLASSES), refsFunction]); 44 | } 45 | 46 | /* Decorator: `compose`. 47 | Composes decorators you need into one. 48 | @sample: 49 | @compose(classNames(CLASSES), refs) 50 | class SomeComponent extends Component {} 51 | */ 52 | export function compose(...decorators) { 53 | if (decorators.length === 0) { return function id(arg) { return arg; }; } 54 | 55 | return builder(decorators.map( item => item.__decorFunction )); 56 | } 57 | -------------------------------------------------------------------------------- /app/js/reducers/controls-reducer.babel.jsx: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | 3 | const INITIAL_STATE = { 4 | selected: null, 5 | isMouseInside: false 6 | }; 7 | 8 | const controls = (state=INITIAL_STATE, action) => { 9 | const {data} = action; 10 | 11 | switch (action.type) { 12 | 13 | case 'TOOLS_SET_SELECTED': { 14 | const selected = (data === state.selected) ? null : data; 15 | return {...state, selected}; 16 | } 17 | case 'TOOLS_RESET_SELECTED': 18 | case 'ADD_POINT': { 19 | return {...state, selected: null}; 20 | } 21 | 22 | case 'CONTROLS_SET_MOUSE_INSIDE': { 23 | return {...state, isMouseInside: data}; 24 | } 25 | 26 | } 27 | return state; 28 | }; 29 | 30 | export default controls; 31 | -------------------------------------------------------------------------------- /app/js/reducers/index-reducer.babel.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import recycleState from 'redux-recycle'; 3 | import progress from './progress-reducer'; 4 | import mainPanel from './main-panel-reducer'; 5 | import controls from './controls-reducer'; 6 | import points from './points-reducer'; 7 | import selectedSpot from './selected-spot'; 8 | 9 | const reducer = recycleState(combineReducers({ 10 | progress, 11 | mainPanel, 12 | controls, 13 | points, 14 | selectedSpot 15 | }), ['SET_APP_STATE'], (state, action) => action.data ); 16 | 17 | export default reducer; 18 | -------------------------------------------------------------------------------- /app/js/reducers/main-panel-reducer.babel.js: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | 3 | const INTIAL_SIZE = 400; 4 | const INITIAL_STATE = { 5 | prevHeight: INTIAL_SIZE, 6 | ySize: INTIAL_SIZE, 7 | isHidden: false, 8 | isTransition: false 9 | }; 10 | 11 | const mainPanel = (state = INITIAL_STATE, action) => { 12 | switch (action.type) { 13 | case 'MAIN_PANEL_HIDE_TOGGLE': { 14 | const isHidden = !state.isHidden; 15 | const ySize = (isHidden) ? C.PLAYER_HEIGHT : state.prevHeight; 16 | const prevHeight = (isHidden) ? state.ySize : C.PLAYER_HEIGHT; 17 | const isTransition = true; 18 | 19 | return { ...state, isHidden, ySize, prevHeight, isTransition }; 20 | } 21 | case 'MAIN_PANEL_SET_YSIZE': { 22 | return { ...state, ySize: state.ySize - action.data }; 23 | } 24 | case 'MAIN_PANEL_RESET_TRANSITION': { 25 | return { ...state, isTransition: false }; 26 | } 27 | case 'MAIN_PANEL_SAVE_YPREV': { 28 | return { ...state, prevHeight: state.ySize }; 29 | } 30 | case 'MAIN_PANEL_SET_HIDDEN': { 31 | return { ...state, isHidden: action.data }; 32 | } 33 | } 34 | return state; 35 | }; 36 | 37 | export default mainPanel; 38 | -------------------------------------------------------------------------------- /app/js/reducers/points-reducer.babel.js: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | 3 | import getLast from '../helpers/get-last'; 4 | import createPoint from '../helpers/create-point'; 5 | import createSegment from '../helpers/create-segment'; 6 | import change from '../helpers/change'; 7 | 8 | const INITIAL_STATE = {}; 9 | 10 | const resetSelectedPoints = (state) => { 11 | const newState = {}; 12 | const props = Object.keys(state); 13 | 14 | for (let i = 0; i < props.length; i++) { 15 | const prop = props[i]; 16 | const item = state[prop]; 17 | newState[prop] = { ...item, isSelected: false }; 18 | } 19 | return newState; 20 | }; 21 | 22 | const ensureTimeBounds = (prop, i = 0, start=0) => { 23 | if (i >= prop.length) { return; } 24 | const item = prop[i]; 25 | const prevItem = prop[i-1]; 26 | /* calculate start and end bounds */ 27 | item.start.time = start; 28 | item.end.time = start + item.delay + item.duration; 29 | ensureTimeBounds(prop, i+1, item.end.time); 30 | }; 31 | 32 | const addSegment = (segments, name, data, current) => { 33 | const prevSpot = getLast(segments); 34 | const isChanged = segments.length > 1 || segments[0].isChanged; 35 | const isUpdate = !isChanged && segments[0].duration === C.MIN_DURATION; 36 | 37 | if (isUpdate) { 38 | segments[0].end.value = current[name]; 39 | segments[0].duration = (data.time - segments[0].delay); 40 | // if not - create entirely new segement 41 | } else { 42 | const duration = (data.time - prevSpot.end.time); 43 | segments.push( 44 | createSegment({ 45 | index: segments.length, 46 | startValue: prevSpot.end.value, 47 | endValue: current[name], 48 | duration 49 | }) 50 | ); 51 | } 52 | ensureTimeBounds(segments); 53 | return segments; 54 | }; 55 | 56 | const updateSpot = (currentValue, input) => { 57 | const {index, value} = input; 58 | // if value is array - update the item in index 59 | if (!(currentValue instanceof Array)) { return value; } 60 | 61 | const newValue = [...currentValue]; 62 | newValue[index] = value; 63 | return newValue; 64 | }; 65 | 66 | const points = (state=INITIAL_STATE, action) => { 67 | const {data} = action; 68 | 69 | switch (action.type) { 70 | 71 | case 'ADD_POINT': { 72 | const newState = resetSelectedPoints(state); 73 | const point = createPoint(data, Object.keys(newState).length); 74 | newState[point.id] = point; 75 | return newState; 76 | } 77 | case 'SELECT_POINT': { 78 | return change( state, [data, 'isSelected'], state => !state ); 79 | } 80 | case 'TOGGLE_OPEN_POINT': { 81 | return change(state,[ data, 'isOpen' ], state => !state); 82 | } 83 | case 'ADD_SNAPSHOT': { 84 | const {id} = data; 85 | const current = state[id].currentProps; 86 | 87 | const props = Object.keys(current); 88 | let newState = state; 89 | for (let i = 0; i < props.length; i++) { 90 | const name = props[i]; 91 | newState = change(newState, 92 | [id, 'props', name], 93 | segments => addSegment([...segments], name, data, current) 94 | ); 95 | } 96 | 97 | return newState; 98 | } 99 | 100 | case 'ADD_PROPERTY_SEGMENT': { 101 | const {id, name, time} = data; 102 | const current = state[id].currentProps; 103 | 104 | return change(state, 105 | [id, 'props', name], 106 | segments => addSegment([...segments], name, data, current) 107 | ); 108 | } 109 | 110 | case 'CHANGE_POINT_CURRENT_POSITION': { 111 | return change(state, [data.id, 'currentProps'], (obj) => { 112 | const {deltaX: dX, deltaY: dY} = data; 113 | const pos = obj[C.POSITION_NAME]; 114 | return {...obj, [C.POSITION_NAME]: [pos[0] + dX, pos[1] + dY] }; 115 | }); 116 | } 117 | 118 | case 'SHIFT_SEGMENT': { 119 | const {id, prop, spotIndex} = data; 120 | 121 | const newState = change(state, [id, 'props', prop, spotIndex], 122 | (prop) => { 123 | const {delay=0, duration=0} = data; 124 | 125 | return { 126 | ...prop, 127 | duration: Math.max((prop.duration + duration), C.MIN_DURATION), 128 | delay: Math.max((prop.delay + delay), 0) 129 | }; 130 | } 131 | ); 132 | 133 | ensureTimeBounds(newState[data.id].props[data.prop]); 134 | return newState; 135 | } 136 | 137 | case 'ADD_POINT_PROPERTY': { 138 | const {name, count} = data.property; 139 | const value = Array(count).fill(0); 140 | 141 | const newState = change(state, 142 | [data.id, 'props'], 143 | (props) => { 144 | props[name] = [createSegment({ startValue: value, endValue: value })]; 145 | return props; 146 | } 147 | ); 148 | 149 | return change(newState, 150 | [data.id, 'currentProps'], 151 | (props) => { 152 | props[name] = value; 153 | return props; 154 | } 155 | ); 156 | } 157 | 158 | case 'UPDATE_SELECTED_SPOT': { 159 | const {values, id, type, spotIndex, prop, value} = data; 160 | const segments = state[id].props[prop]; 161 | const len = Object.keys(segments).length; 162 | 163 | const newState = change(state, 164 | [id, 'props', prop, spotIndex, type, 'value'], value ); 165 | 166 | const spot = state[id].props[prop][spotIndex][type]; 167 | if (spot.connected === 'prev' && spotIndex > 0) { 168 | return change(newState, 169 | [id, 'props', prop, spotIndex-1, 'end', 'value'], value ); 170 | } 171 | 172 | if (spot.connected === 'next' && spotIndex < len-1) { 173 | return change(newState, 174 | [id, 'props', prop, spotIndex+1, 'start', 'value'], value); 175 | } 176 | 177 | return newState; 178 | } 179 | 180 | case 'UPDATE_SELECTED_SPOT_CURRENT': { 181 | const {value, name} = data; 182 | 183 | return change(state, [data.id, 'currentProps'], 184 | (currentProps) => { 185 | currentProps[name] = value; 186 | return currentProps; 187 | } 188 | ); 189 | } 190 | 191 | case 'SET_EASING': { 192 | const {id, prop, spotIndex, easing} = data; 193 | 194 | return change(state, [id, 'props', prop, spotIndex, 'easing'], easing); 195 | } 196 | 197 | } 198 | 199 | return state; 200 | }; 201 | 202 | export default points; 203 | -------------------------------------------------------------------------------- /app/js/reducers/progress-reducer.babel.js: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | import clamp from '../helpers/clamp'; 3 | 4 | const INITIAL_STATE = 0; 5 | const progress = (state=INITIAL_STATE, action) => { 6 | const {data} = action; 7 | 8 | switch (action.type) { 9 | 10 | case 'SET_PROGRESS': { return data; } 11 | 12 | } 13 | 14 | return state; 15 | }; 16 | 17 | export default progress; 18 | -------------------------------------------------------------------------------- /app/js/reducers/selected-spot.babel.jsx: -------------------------------------------------------------------------------- 1 | import C from '../constants'; 2 | 3 | const INITIAL_STATE = { 4 | id: null, 5 | spotIndex: null, 6 | type: null, 7 | prop: null 8 | }; 9 | 10 | const progress = (state=INITIAL_STATE, action) => { 11 | const {data} = action; 12 | 13 | switch (action.type) { 14 | 15 | case 'SET_SELECTED_SPOT': { return { ...data }; } 16 | 17 | case 'RESET_SELECTED_SPOT': { return INITIAL_STATE; } 18 | 19 | } 20 | 21 | return state; 22 | }; 23 | 24 | export default progress; 25 | -------------------------------------------------------------------------------- /app/js/store.babel.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | import reducer from './reducers/index-reducer'; 3 | 4 | const store = createStore(reducer); 5 | 6 | export default store; -------------------------------------------------------------------------------- /coding-guide.md: -------------------------------------------------------------------------------- 1 | # Project Coding Guides and Tips 2 | 3 | ## Common Rules 4 | 5 | - **maximum line length** should be < 80 6 | - **indentation** should be 2 spaces 7 | - **trim trailing whitespace** it would be great, if your IDE/text editor automatically trim trailing whitespaces 8 | 9 | ## JS 10 | 11 | - all variable declarations should have their **own variable keyword**, e.g. 12 | ```javascript 13 | // bad 14 | const className = CLASSES['main-panel'], 15 | isHiddenClassName = this._calcHiddenClassName(), 16 | isResizingClassName = this.state.isResizing ? CLASSES['main-panel--is-resizing'] : ''; 17 | 18 | 19 | // good 20 | const className = CLASSES['main-panel']; 21 | const isHiddenClassName = this._calcHiddenClassName(); 22 | const isResizingClassName = this.state.isResizing ? CLASSES['main-panel--is-resizing'] : ''; 23 | ``` 24 | 25 | - all **private methods** should be declared **with underscore** in front of the method name 26 | ```javascript 27 | // bad 28 | toggleVisibility() { 29 | ... 30 | } 31 | 32 | 33 | // good 34 | _toggleVisibility() { 35 | ... 36 | } 37 | ``` 38 | 39 | ## Preact 40 | 41 | - All **components** should be created as **ES6 classes** which extend from preact.Component or **Pure functions** 42 | ```javascript 43 | // good 44 | import { h, Component } from 'preact'; 45 | 46 | class LeftPanel extends Component { 47 | render () { 48 | return ( 49 |
LeftPanel
50 | ); 51 | } 52 | } 53 | 54 | export default LeftPanel; 55 | 56 | 57 | // good 58 | import { h } from 'preact'; 59 | 60 | const LeftPanel = () => { 61 | return ( 62 |
LeftPanel
63 | ); 64 | }; 65 | 66 | export default LeftPanel; 67 | ``` 68 | 69 | - as css-modules are used, you need to require both .postcss.css.json and 70 | .post.css(file extension can be omitted) files to make magic happen 71 | ```javascript 72 | // good 73 | import { h, Component } from 'preact'; 74 | const CLASSES = require('../../../css/blocks/left-panel.postcss.css.json'); 75 | require('../../../css/blocks/left-panel'); 76 | 77 | class LeftPanel extends Component { 78 | render () { 79 | return ( 80 |
81 | ); 82 | } 83 | } 84 | 85 | export default LeftPanel; 86 | ``` 87 | 88 | - it's preferred to use round braces arround JSX in render() 89 | ```javascript 90 | // bad 91 | class RightPanel extends Component { 92 | render () { 93 | return
94 | 95 | 96 |
97 | } 98 | } 99 | 100 | 101 | // good 102 | class RightPanel extends Component { 103 | render () { 104 | return ( 105 |
106 | 107 | 108 |
109 | ); 110 | } 111 | } 112 | ``` 113 | 114 | - **@bind** You can bind context of any method by means of a decorator. 115 | ```javascript 116 | // bad 117 | import { h, Component } from 'preact'; 118 | class MainPanel extends Component { 119 | constructor(props) { 120 | super(props); 121 | this.state = { isPlayerPassed: true }; 122 | 123 | this._toggleVisibility = this._toggleVisibility.bind(this); 124 | } 125 | 126 | ... 127 | } 128 | 129 | 130 | // good 131 | import { h, Component } from 'preact'; 132 | import { bind } from 'decko'; 133 | 134 | class MainPanel extends Component { 135 | constructor(props) { 136 | super(props); 137 | this.state = { isPlayerPassed: true }; 138 | } 139 | 140 | @bind 141 | _toggleVisibility() { 142 | this.setState({ isHidden: !this.state.isHidden }); 143 | } 144 | 145 | ... 146 | } 147 | ``` 148 | 149 | *Cheers!* 150 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var livereload = require('gulp-livereload'); 3 | 4 | gulp.task('default', function(){ 5 | livereload.listen(); 6 | gulp.watch('./app/build/mojs-timeline-editor.js', function (e) { 7 | gulp 8 | .src(e.path) 9 | .pipe(livereload()); 10 | }); 11 | }); 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /help-wanted.md: -------------------------------------------------------------------------------- 1 | # Help Wanted Guide 2 | 3 | ## Misc 4 | 5 | - the first contributions are thru `pull requests` - we will give you the ability to push to the repo directly as soon as we will be sure we are on the same page 6 | - start `feature branch` from `dev` branch for something you want to add 7 | - Read [#development](https://github.com/legomushroom/mojs-timeline-editor/blob/master/readme.md#development) guides if you struggle to install the repo locally. 8 | - `2 spaces` identation 9 | - the best way to get answer - comment on the `help wanted` issue, we will discuss it right there, you can also ask something on `slack` channel 10 | - do not hesitate to ask something, we always want to help you 11 | 12 | ## CSS 13 | 14 | - we use `postcss` 15 | - all colors are in [colors.postcss.css](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/css/assets/colors.postcss.css), use only those 16 | - all sizes should be expressed relative to `$PX` variable e.g. `width: 20*$PX; margin-left: 30*$PX` 17 | - all font sizes should be expressed relative to `$FPX` (font px) variable e.g. `font-size: 20*$FPX;` 18 | - to use `colors` and `$PX` variables you need to `@import '../assets/globals.postcss.css';`, please refer to [icon.postcss.css](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/css/blocks/icon.postcss.css) for more info 19 | - save component's css file to [blocks](https://github.com/legomushroom/mojs-timeline-editor/tree/master/app/css/blocks) folder 20 | 21 | ## JS 22 | 23 | - we use [PReact](https://preactjs.com/) instead of `React`, but no worries, it has exact the same syntax and APIs as `React` has. 24 | - you need to `import { h } from 'preact';` in order for `preact` to work, please refer to [icon.babel.jsx](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/js/components/icon.babel.jsx) for more info 25 | - we use `babel` and `stage 0` preset 26 | - all components are stored in [components](https://github.com/legomushroom/mojs-timeline-editor/tree/master/app/js/components) folder and have `babel.jsx` postfix 27 | - we use `css modules` for `CSS`, every time you save the `postcss.css` file, it's `JSON` representation will be generated and placed near the `postcss.css` file, you need to read the `CSS` class name hash and apply it to the component, [icon.babel.jsx](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/js/components/icon.babel.jsx) for more info **(lines 2,3 and 10)** 28 | - we use [hammerjs](http://hammerjs.github.io/) for unify pointer input, `drag`/`pan`, `tap` etc. 29 | - unify `pointer-down`/`pointer-up` events by using [add-pointer-down](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/js/components/add-pointer-down.babel.js)/[add-pointer-up](https://github.com/legomushroom/mojs-timeline-editor/blob/master/app/js/components/add-pointer-up.babel.js) from [helpers](https://github.com/legomushroom/mojs-timeline-editor/tree/master/app/js/helpers) 30 | 31 | *Cheers!* 32 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojs/mojs-timeline-editor/7113b5a990795ad1e45c74e207d402dee65e7898/logo.png -------------------------------------------------------------------------------- /mockups/mojs-timeline.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojs/mojs-timeline-editor/7113b5a990795ad1e45c74e207d402dee65e7898/mockups/mojs-timeline.sketch -------------------------------------------------------------------------------- /mockups/point-editor.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojs/mojs-timeline-editor/7113b5a990795ad1e45c74e207d402dee65e7898/mockups/point-editor.sketch -------------------------------------------------------------------------------- /mockups/timeline-editor@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojs/mojs-timeline-editor/7113b5a990795ad1e45c74e207d402dee65e7898/mockups/timeline-editor@1x.png -------------------------------------------------------------------------------- /mockups/timeline-editor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojs/mojs-timeline-editor/7113b5a990795ad1e45c74e207d402dee65e7898/mockups/timeline-editor@2x.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mojs/timeline-editor", 3 | "description": "GUI for interactive `html`/`custom points`/`timeline` editing while crafting your animations", 4 | "version": "0.19.2", 5 | "license": "MIT", 6 | "private": false, 7 | "scripts": { 8 | "serve": "node node_modules/webpack-dev-server/bin/webpack-dev-server.js --content-base app/ --inline --hot", 9 | "precommit": "npm run eslint", 10 | "eslint": "eslint app/js/**/*.js" 11 | }, 12 | "main": "app/build/mojs-timeline-editor.js", 13 | "keywords": [ 14 | "mojs", 15 | "motion", 16 | "effects", 17 | "animation", 18 | "motion", 19 | "motion graphics", 20 | "timeline editor", 21 | "svg" 22 | ], 23 | "author": { 24 | "name": "Oleg Solomka", 25 | "email": "legomushroom@gmail.com", 26 | "url": "https://twitter.com/legomushroom", 27 | "github": "@legomushroom" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/legomushroom/mojs-timeline-editor.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/legomushroom/mojs-timeline-editor/issues" 35 | }, 36 | "homepage": "https://github.com/mojs/mojs-timeline-editor", 37 | "dependencies": { 38 | "decko": "^1.1.3", 39 | "hammerjs": "^2.0.8", 40 | "immutable-js": "^0.3.1-6", 41 | "mo-js": "^0.265.9", 42 | "mojs-curve-editor": "^1.5.0", 43 | "mojs-player": "^0.43.16", 44 | "preact": "^7.1.0", 45 | "propagating-hammerjs": "^1.4.6", 46 | "redux": "^3.5.2", 47 | "redux-immutablejs": "0.0.8", 48 | "redux-recycle": "^1.2.0", 49 | "redux-undo": "^0.6.1" 50 | }, 51 | "devDependencies": { 52 | "autoprefixer": "^6.3.7", 53 | "babel": "^6.5.2", 54 | "babel-core": "^6.11.4", 55 | "babel-eslint": "^7.1.1", 56 | "babel-loader": "^6.2.4", 57 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 58 | "babel-plugin-transform-jsx": "^2.0.0", 59 | "babel-plugin-transform-react-jsx": "^6.8.0", 60 | "babel-plugin-transform-runtime": "^6.12.0", 61 | "babel-preset-es2015": "^6.9.0", 62 | "babel-preset-stage-0": "^6.5.0", 63 | "babel-preset-stage-2": "^6.11.0", 64 | "browser-sync": "^2.13.0", 65 | "css-loader": "^0.23.1", 66 | "csswring": "^5.1.0", 67 | "del": "^2.2.1", 68 | "eslint": "^3.2.0", 69 | "eslint-config-airbnb": "^9.0.1", 70 | "eslint-loader": "^1.5.0", 71 | "eslint-plugin-import": "^1.12.0", 72 | "eslint-plugin-jsx-a11y": "^2.0.1", 73 | "eslint-plugin-react": "^5.2.2", 74 | "gulp": "^3.9.1", 75 | "gulp-livereload": "^3.8.1", 76 | "husky": "^0.11.9", 77 | "json-loader": "^0.5.4", 78 | "md5": "^2.2.1", 79 | "mo-js": "^0.265.7", 80 | "mojs-curve-editor": "", 81 | "mojs-player": "^0.43.15", 82 | "postcss-automath": "^1.0.0", 83 | "postcss-cssnext": "^2.7.0", 84 | "postcss-loader": "^0.9.1", 85 | "postcss-mixins": "^5.4.0", 86 | "postcss-modules": "^0.5.0", 87 | "preact-redux": "^1.0.1", 88 | "precss": "^1.4.0", 89 | "source-map-loader": "^0.1.5", 90 | "style-loader": "^0.13.1", 91 | "tag-loader": "^0.3.0", 92 | "unminified-webpack-plugin": "^1.1.0", 93 | "webpack": "^1.13.1", 94 | "webpack-dev-server": "^1.14.1" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var autoprefixer = require('autoprefixer'); 4 | var UnminifiedWebpackPlugin = require('unminified-webpack-plugin'); 5 | 6 | module.exports = { 7 | watch: true, 8 | context: __dirname + "/", 9 | entry: [ 10 | __dirname + '/app/js/app.babel.jsx' 11 | ], 12 | module: { 13 | preLoaders: [ 14 | { 15 | exclude: /src\//, 16 | loader: 'source-map' 17 | } 18 | ], 19 | loaders: [ 20 | { test: /\.(json)$/, exclude: /node_modules/, loaders: ['json-loader'] }, 21 | { test: /\.(jsx|.js|babel.jsx|babel.js)$/, 22 | exclude: /node_modules/, 23 | loader: 'babel-loader!eslint' 24 | }, 25 | { test: /\.(postcss.css)$/, loader: "style-loader!css-loader!postcss-loader" }, 26 | { test: /\.html$/, loader: 'raw-loader' }, 27 | { 28 | test: /\.(eot|woff|ttf|svg|png|jpg|wav|mp3)$/, 29 | loader: 'url-loader?limit=30000&name=[name]-[hash].[ext]', 30 | } 31 | ] 32 | }, 33 | postcss: function () { 34 | return { 35 | defaults: [ require('precss'), require('postcss-cssnext'), 36 | require('postcss-modules'), require('postcss-automath'), 37 | require('postcss-mixins') 38 | ], 39 | cleaner: [autoprefixer({ browsers: ['last 2 versions'] })] 40 | }; 41 | }, 42 | output: { 43 | path: __dirname + '/app/build/', 44 | filename: 'mojs-timeline-editor.js', 45 | publicPath: 'build/', 46 | library: 'mojs-timeline-editor', 47 | libraryTarget: 'umd', 48 | umdNamedDefine: true, 49 | }, 50 | plugins: [ 51 | // new webpack.optimize.UglifyJsPlugin({ 52 | // compress: { 53 | // warnings: false 54 | // } 55 | // }), 56 | // new UnminifiedWebpackPlugin() 57 | ], 58 | // devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'inline-source-map', 59 | resolve: { 60 | root: [ path.resolve('./') ], 61 | moduleDirectories: ['node_modules'], 62 | target: 'node', 63 | extensions: [ 64 | '', '.js', '.babel.js', '.babel.jsx', 65 | '.postcss.css', '.css', '.json' 66 | ] 67 | } 68 | }; 69 | --------------------------------------------------------------------------------