├── .++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 – 
2 |
3 | GUI for interactive `html`/`custom points`/`timeline` editing while crafting your animations
4 |
5 | 
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 |
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 |
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 |
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 |
27 |
28 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------