aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json30
-rw-r--r--report.20190510.131414.316.001.json592
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/views/ContextMenu.scss1
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/animationtimeline/Keyframe.scss105
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx560
-rw-r--r--src/client/views/animationtimeline/Timeline.scss322
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx633
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.scss94
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx78
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.scss107
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.tsx181
-rw-r--r--src/client/views/animationtimeline/Track.scss15
-rw-r--r--src/client/views/animationtimeline/Track.tsx380
-rw-r--r--src/client/views/collections/CollectionMapView.tsx8
-rw-r--r--src/client/views/collections/CollectionSubView.tsx22
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx8
-rw-r--r--src/client/views/nodes/FieldView.tsx5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx6
-rw-r--r--src/new_fields/Doc.ts1
22 files changed, 2538 insertions, 627 deletions
diff --git a/package-lock.json b/package-lock.json
index 176d1f27e..5449a9d69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -283,14 +283,14 @@
"integrity": "sha512-fGCl/gwB7jmKCRI2FhgIBeIa/LCSUcjlEcckH2Bc2dIjhJ+2nspp+22lubxcseN6jjrmP42kkXt/reAPe+KJkQ=="
},
"@tensorflow/tfjs-converter": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.7.2.tgz",
- "integrity": "sha512-6ckNQ3n36X20/+YDCTictfKftPH3urf5I+wc0QNlsSJhik3QiliYJy+v5hmjQGSJ4ow2oyW5T2lUsy9aNoNaTg=="
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.7.4.tgz",
+ "integrity": "sha512-B/Ux9I3osI0CXoESGR0Xe5C6BsEfC04+g2xn5zVaW9KEuVEnGEgnuBQxgijRFzkqTwoyLv4ptAmjyIghVARX0Q=="
},
"@tensorflow/tfjs-core": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.2.tgz",
- "integrity": "sha512-2o5jjXLHrMY4f+r7aOfJZbKqrchEYrRk2mhlFD3RgKqtSKUpfv15YLQr4uc0uMbNSwuZbjWwrmaL9yyaBWYSAQ==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.4.tgz",
+ "integrity": "sha512-3G4VKJ6nPs7iCt6gs3bjRj8chihKrYWenf63R0pm7D9MhlrVoX/tpN4LYVMGgBL7jHPxMLKdOkoAZJrn/J88HQ==",
"requires": {
"@types/offscreencanvas": "~2019.3.0",
"@types/seedrandom": "2.4.27",
@@ -6300,9 +6300,9 @@
}
},
"google-maps-react": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/google-maps-react/-/google-maps-react-2.0.2.tgz",
- "integrity": "sha512-6cYauGwt22haDUrWxKQ6yoNOqjiuxHo8YYcmb+aBvNICokdXmZOUB6Ah4vD5VexMVlrwP2PFqA/D8sHpEB52KA=="
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/google-maps-react/-/google-maps-react-2.0.6.tgz",
+ "integrity": "sha512-M8Eo9WndfQEfxcmm6yRq03qdJgw1x6rQmJ9DN+a+xPQ3K7yNDGkVDbinrf4/8vcox7nELbeopbm4bpefKewWfQ=="
},
"google-p12-pem": {
"version": "2.0.4",
@@ -6809,9 +6809,9 @@
}
},
"ibm-watson": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/ibm-watson/-/ibm-watson-5.4.0.tgz",
- "integrity": "sha512-rgM42p/XL12yYSOvrmgp+NYZlaNdaIeyKl8TlF982J8dEnkXgYhYF7k05Fg1nZ0ZZ80AF1Fja/PW2mB53AjNGA==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/ibm-watson/-/ibm-watson-5.5.0.tgz",
+ "integrity": "sha512-gcijKSFTOMLcjEnSpeUa4/31naeMSZsz8t/wYNrR5c8gp1VN9LbJ3vhn3nFislfLYopTkNnWzya7w8oMhW4SGg==",
"requires": {
"@types/async": "^2.4.2",
"@types/extend": "^3.0.1",
@@ -6828,9 +6828,9 @@
},
"dependencies": {
"@types/node": {
- "version": "11.15.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.10.tgz",
- "integrity": "sha512-FHli7aK/ViA02vbWKmiNejJW4BCJxCVb6macS5gi71fST+UrrdqcES6Lh5rx23hU1QCBeUNXfPquYr9jJv7FuA=="
+ "version": "11.15.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.12.tgz",
+ "integrity": "sha512-iefeBfpmhoYaZfj+gJM5z9H9eiTwhuzhPsJgH/flx4HP2SBI2FNDra1D3vKljqPLGDr9Cazvh9gP9Xszc30ncA=="
}
}
},
diff --git a/report.20190510.131414.316.001.json b/report.20190510.131414.316.001.json
deleted file mode 100644
index 390db8ba6..000000000
--- a/report.20190510.131414.316.001.json
+++ /dev/null
@@ -1,592 +0,0 @@
-
-{
- "header": {
- "event": "Allocation failed - JavaScript heap out of memory",
- "trigger": "FatalError",
- "filename": "report.20190510.131414.316.001.json",
- "dumpEventTime": "2019-05-10T13:14:14Z",
- "processId": 316,
- "commandLine": [
- "C:\\Program Files\\nodejs\\node.exe",
- "--max-old-space-size=2048",
- "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\ts-node-dev\\lib\\wrap.js",
- "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\fork-ts-checker-webpack-plugin\\lib\\service.js"
- ],
- "nodejsVersion": "v11.11.0",
- "wordSize": 64,
- "arch": "x64",
- "platform": "win32",
- "componentVersions": {
- "node": "11.11.0",
- "v8": "7.0.276.38-node.18",
- "uv": "1.26.0",
- "zlib": "1.2.11",
- "brotli": "1.0.7",
- "ares": "1.15.0",
- "modules": "67",
- "nghttp2": "1.34.0",
- "napi": "4",
- "llhttp": "1.1.1",
- "http_parser": "2.8.0",
- "openssl": "1.1.1a",
- "cldr": "34.0",
- "icu": "63.1",
- "tz": "2018e",
- "unicode": "11.0"
- },
- "release": {
- "name": "node",
- "headersUrl": "https://nodejs.org/download/release/v11.11.0/node-v11.11.0-headers.tar.gz",
- "sourceUrl": "https://nodejs.org/download/release/v11.11.0/node-v11.11.0.tar.gz",
- "libUrl": "https://nodejs.org/download/release/v11.11.0/win-x64/node.lib"
- },
- "osName": "Windows_NT",
- "osRelease": "10.0.14393",
- "osVersion": "Windows Server 2016 Datacenter",
- "osMachine": "x86_64",
- "host": "dashserver"
- },
- "javascriptStack": {
- "message": "No stack.",
- "stack": [
- "Unavailable."
- ]
- },
- "nativeStack": [
- {
- "pc": "0x00007ff6d814bc4e",
- "symbol": "SSL_SESSION_get0_peer+15022"
- },
- {
- "pc": "0x00007ff6d814ab04",
- "symbol": "SSL_SESSION_get0_peer+10596"
- },
- {
- "pc": "0x00007ff6d814a336",
- "symbol": "SSL_SESSION_get0_peer+8598"
- },
- {
- "pc": "0x00007ff6d823703a",
- "symbol": "uv_loop_fork+86202"
- },
- {
- "pc": "0x00007ff6d861e49e",
- "symbol": "v8::internal::FatalProcessOutOfMemory+798"
- },
- {
- "pc": "0x00007ff6d861e3d7",
- "symbol": "v8::internal::FatalProcessOutOfMemory+599"
- },
- {
- "pc": "0x00007ff6d86c91a4",
- "symbol": "v8::internal::Heap::RootIsImmortalImmovable+14068"
- },
- {
- "pc": "0x00007ff6d86befc8",
- "symbol": "v8::internal::Heap::CollectGarbage+7256"
- },
- {
- "pc": "0x00007ff6d86bd7c8",
- "symbol": "v8::internal::Heap::CollectGarbage+1112"
- },
- {
- "pc": "0x00007ff6d86c70f7",
- "symbol": "v8::internal::Heap::RootIsImmortalImmovable+5703"
- },
- {
- "pc": "0x00007ff6d86c7176",
- "symbol": "v8::internal::Heap::RootIsImmortalImmovable+5830"
- },
- {
- "pc": "0x00007ff6d8848481",
- "symbol": "v8::internal::Factory::NewFillerObject+49"
- },
- {
- "pc": "0x00007ff6d88fdbb6",
- "symbol": "v8::internal::StoreBuffer::StoreBufferOverflow+27190"
- },
- {
- "pc": "0x0000033e2e4d0481",
- "symbol": ""
- }
- ],
- "javascriptHeap": {
- "totalMemory": 380211200,
- "totalCommittedMemory": 380211200,
- "usedMemory": 365246984,
- "availableMemory": 1844483352,
- "memoryLimit": 2217857988,
- "heapSpaces": {
- "read_only_space": {
- "memorySize": 524288,
- "committedMemory": 524288,
- "capacity": 515584,
- "used": 33520,
- "available": 482064
- },
- "new_space": {
- "memorySize": 1048576,
- "committedMemory": 1048576,
- "capacity": 1031168,
- "used": 1031088,
- "available": 80
- },
- "old_space": {
- "memorySize": 341512192,
- "committedMemory": 341512192,
- "capacity": 335760416,
- "used": 329680232,
- "available": 6080184
- },
- "code_space": {
- "memorySize": 4194304,
- "committedMemory": 4194304,
- "capacity": 3775360,
- "used": 3762688,
- "available": 12672
- },
- "map_space": {
- "memorySize": 1585152,
- "committedMemory": 1585152,
- "capacity": 1550096,
- "used": 852880,
- "available": 697216
- },
- "large_object_space": {
- "memorySize": 31346688,
- "committedMemory": 31346688,
- "capacity": 1867097712,
- "used": 29886576,
- "available": 1837211136
- },
- "new_large_object_space": {
- "memorySize": 0,
- "committedMemory": 0,
- "capacity": 0,
- "used": 0,
- "available": 0
- }
- }
- },
- "libuv": [
- ],
- "environmentVariables": {
- "ACLOCAL_PATH": "C:\\Program Files\\Git\\mingw64\\share\\aclocal;C:\\Program Files\\Git\\usr\\share\\aclocal",
- "ALLUSERSPROFILE": "C:\\ProgramData",
- "APPDATA": "C:\\Users\\dash\\AppData\\Roaming",
- "CHECK_SYNTACTIC_ERRORS": "false",
- "CLIENTNAME": "DESKTOP-7P5JHEB",
- "COMMONPROGRAMFILES": "C:\\Program Files\\Common Files",
- "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
- "CommonProgramW6432": "C:\\Program Files\\Common Files",
- "COMPILER_OPTIONS": "{}",
- "COMPUTERNAME": "dashserver",
- "COMSPEC": "C:\\Windows\\system32\\cmd.exe",
- "CONFIG_SITE": "C:/Program Files/Git/mingw64/etc/config.site",
- "CONTEXT": "C:\\Users\\dash\\Documents\\Dash-Web",
- "DISPLAY": "needs-to-be-defined",
- "EXEPATH": "C:\\Program Files\\Git",
- "FPS_BROWSER_APP_PROFILE_STRING": "Internet Explorer",
- "FPS_BROWSER_USER_PROFILE_STRING": "Default",
- "HOME": "C:\\Users\\dash",
- "HOMEDRIVE": "C:",
- "HOMEPATH": "\\Users\\dash",
- "HOSTNAME": "dashserver",
- "INFOPATH": "C:\\Program Files\\Git\\usr\\local\\info;C:\\Program Files\\Git\\usr\\share\\info;C:\\Program Files\\Git\\usr\\info;C:\\Program Files\\Git\\share\\info",
- "INIT_CWD": "C:\\Users\\dash\\Documents\\Dash-Web",
- "LANG": "en_US.UTF-8",
- "LOCALAPPDATA": "C:\\Users\\dash\\AppData\\Local",
- "LOGONSERVER": "\\\\dashserver",
- "MANPATH": "C:\\Program Files\\Git\\mingw64\\local\\man;C:\\Program Files\\Git\\mingw64\\share\\man;C:\\Program Files\\Git\\usr\\local\\man;C:\\Program Files\\Git\\usr\\share\\man;C:\\Program Files\\Git\\usr\\man;C:\\Program Files\\Git\\share\\man",
- "MEMORY_LIMIT": "2048",
- "MINGW_CHOST": "x86_64-w64-mingw32",
- "MINGW_PACKAGE_PREFIX": "mingw-w64-x86_64",
- "MINGW_PREFIX": "C:/Program Files/Git/mingw64",
- "MSYSTEM": "MINGW64",
- "MSYSTEM_CARCH": "x86_64",
- "MSYSTEM_CHOST": "x86_64-w64-mingw32",
- "MSYSTEM_PREFIX": "C:/Program Files/Git/mingw64",
- "NODE": "C:\\Program Files\\nodejs\\node.exe",
- "NODE_ENV": "development",
- "npm_config_access": "",
- "npm_config_allow_same_version": "",
- "npm_config_also": "",
- "npm_config_always_auth": "",
- "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}",
- "npm_config_audit": "true",
- "npm_config_audit_level": "low",
- "npm_config_auth_type": "legacy",
- "npm_config_bin_links": "true",
- "npm_config_browser": "",
- "npm_config_ca": "",
- "npm_config_cache": "C:\\Users\\dash\\AppData\\Roaming\\npm-cache",
- "npm_config_cache_lock_retries": "10",
- "npm_config_cache_lock_stale": "60000",
- "npm_config_cache_lock_wait": "10000",
- "npm_config_cache_max": "Infinity",
- "npm_config_cache_min": "10",
- "npm_config_cafile": "",
- "npm_config_cert": "",
- "npm_config_cidr": "",
- "npm_config_color": "true",
- "npm_config_commit_hooks": "true",
- "npm_config_depth": "Infinity",
- "npm_config_description": "true",
- "npm_config_dev": "",
- "npm_config_dry_run": "",
- "npm_config_editor": "notepad.exe",
- "npm_config_engine_strict": "",
- "npm_config_fetch_retries": "2",
- "npm_config_fetch_retry_factor": "10",
- "npm_config_fetch_retry_maxtimeout": "60000",
- "npm_config_fetch_retry_mintimeout": "10000",
- "npm_config_force": "",
- "npm_config_git": "git",
- "npm_config_git_tag_version": "true",
- "npm_config_global": "",
- "npm_config_globalconfig": "C:\\Users\\dash\\AppData\\Roaming\\npm\\etc\\npmrc",
- "npm_config_globalignorefile": "C:\\Users\\dash\\AppData\\Roaming\\npm\\etc\\npmignore",
- "npm_config_global_style": "",
- "npm_config_group": "",
- "npm_config_ham_it_up": "",
- "npm_config_heading": "npm",
- "npm_config_https_proxy": "",
- "npm_config_if_present": "",
- "npm_config_ignore_prepublish": "",
- "npm_config_ignore_scripts": "",
- "npm_config_init_author_email": "",
- "npm_config_init_author_name": "",
- "npm_config_init_author_url": "",
- "npm_config_init_license": "ISC",
- "npm_config_init_module": "C:\\Users\\dash\\.npm-init.js",
- "npm_config_init_version": "1.0.0",
- "npm_config_json": "",
- "npm_config_key": "",
- "npm_config_legacy_bundling": "",
- "npm_config_link": "",
- "npm_config_local_address": "",
- "npm_config_loglevel": "notice",
- "npm_config_logs_max": "10",
- "npm_config_long": "",
- "npm_config_maxsockets": "50",
- "npm_config_message": "%s",
- "npm_config_metrics_registry": "https://registry.npmjs.org/",
- "npm_config_node_gyp": "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js",
- "npm_config_node_options": "",
- "npm_config_node_version": "11.11.0",
- "npm_config_noproxy": "",
- "npm_config_offline": "",
- "npm_config_onload_script": "",
- "npm_config_only": "",
- "npm_config_optional": "true",
- "npm_config_otp": "",
- "npm_config_package_lock": "true",
- "npm_config_package_lock_only": "",
- "npm_config_parseable": "",
- "npm_config_prefer_offline": "",
- "npm_config_prefer_online": "",
- "npm_config_prefix": "C:\\Users\\dash\\AppData\\Roaming\\npm",
- "npm_config_preid": "",
- "npm_config_production": "",
- "npm_config_progress": "true",
- "npm_config_proxy": "",
- "npm_config_read_only": "",
- "npm_config_rebuild_bundle": "true",
- "npm_config_registry": "https://registry.npmjs.org/",
- "npm_config_rollback": "true",
- "npm_config_save": "true",
- "npm_config_save_bundle": "",
- "npm_config_save_dev": "",
- "npm_config_save_exact": "",
- "npm_config_save_optional": "",
- "npm_config_save_prefix": "^",
- "npm_config_save_prod": "",
- "npm_config_scope": "",
- "npm_config_scripts_prepend_node_path": "warn-only",
- "npm_config_script_shell": "",
- "npm_config_searchexclude": "",
- "npm_config_searchlimit": "20",
- "npm_config_searchopts": "",
- "npm_config_searchstaleness": "900",
- "npm_config_send_metrics": "",
- "npm_config_shell": "C:\\Windows\\system32\\cmd.exe",
- "npm_config_shrinkwrap": "true",
- "npm_config_sign_git_commit": "",
- "npm_config_sign_git_tag": "",
- "npm_config_sso_poll_frequency": "500",
- "npm_config_sso_type": "oauth",
- "npm_config_strict_ssl": "true",
- "npm_config_tag": "latest",
- "npm_config_tag_version_prefix": "v",
- "npm_config_timing": "",
- "npm_config_tmp": "C:\\Users\\dash\\AppData\\Local\\Temp\\2",
- "npm_config_umask": "0000",
- "npm_config_unicode": "",
- "npm_config_unsafe_perm": "true",
- "npm_config_update_notifier": "true",
- "npm_config_usage": "",
- "npm_config_user": "",
- "npm_config_userconfig": "C:\\Users\\dash\\.npmrc",
- "npm_config_user_agent": "npm/6.7.0 node/v11.11.0 win32 x64",
- "npm_config_version": "",
- "npm_config_versions": "",
- "npm_config_viewer": "browser",
- "npm_execpath": "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js",
- "npm_lifecycle_event": "start",
- "npm_lifecycle_script": "ts-node-dev -- src/server/index.ts",
- "npm_node_execpath": "C:\\Program Files\\nodejs\\node.exe",
- "npm_package_dependencies_async": "^2.6.2",
- "npm_package_dependencies_babel_runtime": "^6.26.0",
- "npm_package_dependencies_bcrypt_nodejs": "0.0.3",
- "npm_package_dependencies_bluebird": "^3.5.3",
- "npm_package_dependencies_body_parser": "^1.18.3",
- "npm_package_dependencies_bootstrap": "^4.3.1",
- "npm_package_dependencies_class_transformer": "^0.2.0",
- "npm_package_dependencies_connect_flash": "^0.1.1",
- "npm_package_dependencies_connect_mongo": "^2.0.3",
- "npm_package_dependencies_cookie_parser": "^1.4.4",
- "npm_package_dependencies_cookie_session": "^2.0.0-beta.3",
- "npm_package_dependencies_crypto_browserify": "^3.11.0",
- "npm_package_dependencies_d3_format": "^1.3.2",
- "npm_package_dependencies_express": "^4.16.4",
- "npm_package_dependencies_expressjs": "^1.0.1",
- "npm_package_dependencies_express_flash": "0.0.2",
- "npm_package_dependencies_express_session": "^1.15.6",
- "npm_package_dependencies_express_validator": "^5.3.1",
- "npm_package_dependencies_flexlayout_react": "^0.3.3",
- "npm_package_dependencies_font_awesome": "^4.7.0",
- "npm_package_dependencies_formidable": "^1.2.1",
- "npm_package_dependencies_golden_layout": "^1.5.9",
- "npm_package_dependencies_html_to_image": "^0.1.0",
- "npm_package_dependencies_i": "^0.3.6",
- "npm_package_dependencies_jsonwebtoken": "^8.5.0",
- "npm_package_dependencies_jsx_to_string": "^1.4.0",
- "npm_package_dependencies_lodash": "^4.17.11",
- "npm_package_dependencies_mobile_detect": "^1.4.3",
- "npm_package_dependencies_mobx": "^5.9.0",
- "npm_package_dependencies_mobx_react": "^5.3.5",
- "npm_package_dependencies_mobx_react_devtools": "^6.1.1",
- "npm_package_dependencies_mongodb": "^3.1.13",
- "npm_package_dependencies_mongoose": "^5.4.18",
- "npm_package_dependencies_nodemailer": "^5.1.1",
- "npm_package_dependencies_nodemon": "^1.18.10",
- "npm_package_dependencies_node_sass": "^4.12.0",
- "npm_package_dependencies_normalize_css": "^8.0.1",
- "npm_package_dependencies_npm": "^6.9.0",
- "npm_package_dependencies_passport": "^0.4.0",
- "npm_package_dependencies_passport_local": "^1.0.0",
- "npm_package_dependencies_prosemirror_commands": "^1.0.7",
- "npm_package_dependencies_prosemirror_example_setup": "^1.0.1",
- "npm_package_dependencies_prosemirror_history": "^1.0.4",
- "npm_package_dependencies_prosemirror_keymap": "^1.0.1",
- "npm_package_dependencies_prosemirror_model": "^1.7.0",
- "npm_package_dependencies_prosemirror_schema_basic": "^1.0.0",
- "npm_package_dependencies_prosemirror_schema_list": "^1.0.2",
- "npm_package_dependencies_prosemirror_state": "^1.2.2",
- "npm_package_dependencies_prosemirror_transform": "^1.1.3",
- "npm_package_dependencies_prosemirror_view": "^1.8.3",
- "npm_package_dependencies_pug": "^2.0.3",
- "npm_package_dependencies_raw_loader": "^1.0.0",
- "npm_package_dependencies_react": "^16.8.4",
- "npm_package_dependencies_react_bootstrap": "^1.0.0-beta.5",
- "npm_package_dependencies_react_bootstrap_dropdown_menu": "^1.1.15",
- "npm_package_dependencies_react_color": "^2.17.0",
- "npm_package_dependencies_react_dimensions": "^1.3.1",
- "npm_package_dependencies_react_dom": "^16.8.4",
- "npm_package_dependencies_react_golden_layout": "^1.0.6",
- "npm_package_dependencies_react_image_lightbox": "^5.1.0",
- "npm_package_dependencies_react_jsx_parser": "^1.15.0",
- "npm_package_dependencies_react_measure": "^2.2.4",
- "npm_package_dependencies_react_mosaic": "0.0.20",
- "npm_package_dependencies_react_pdf": "^4.0.2",
- "npm_package_dependencies_react_pdf_highlighter": "^2.1.2",
- "npm_package_dependencies_react_pdf_js": "^4.0.2",
- "npm_package_dependencies_react_simple_dropdown": "^3.2.3",
- "npm_package_dependencies_react_split_pane": "^0.1.85",
- "npm_package_dependencies_react_table": "^6.9.2",
- "npm_package_dependencies_request": "^2.88.0",
- "npm_package_dependencies_request_promise": "^4.2.4",
- "npm_package_dependencies_serializr": "^1.5.1",
- "npm_package_dependencies_socket_io": "^2.2.0",
- "npm_package_dependencies_socket_io_client": "^2.2.0",
- "npm_package_dependencies_solr_node": "^1.1.3",
- "npm_package_dependencies_typescript_collections": "^1.3.2",
- "npm_package_dependencies_url_loader": "^1.1.2",
- "npm_package_dependencies_uuid": "^3.3.2",
- "npm_package_dependencies_xoauth2": "^1.2.0",
- "npm_package_dependencies__fortawesome_fontawesome_free_solid": "^5.0.13",
- "npm_package_dependencies__fortawesome_fontawesome_svg_core": "^1.2.15",
- "npm_package_dependencies__fortawesome_free_solid_svg_icons": "^5.7.2",
- "npm_package_dependencies__fortawesome_react_fontawesome": "^0.1.4",
- "npm_package_dependencies__hig_flyout": "^1.0.3",
- "npm_package_dependencies__hig_theme_context": "^2.1.3",
- "npm_package_dependencies__hig_theme_data": "^2.3.3",
- "npm_package_dependencies__trendmicro_react_dropdown": "^1.3.0",
- "npm_package_dependencies__types_async": "^2.4.1",
- "npm_package_dependencies__types_bcrypt_nodejs": "0.0.30",
- "npm_package_dependencies__types_bluebird": "^3.5.25",
- "npm_package_dependencies__types_body_parser": "^1.17.0",
- "npm_package_dependencies__types_connect_flash": "0.0.34",
- "npm_package_dependencies__types_cookie_parser": "^1.4.1",
- "npm_package_dependencies__types_cookie_session": "^2.0.36",
- "npm_package_dependencies__types_d3_format": "^1.3.1",
- "npm_package_dependencies__types_express": "^4.16.1",
- "npm_package_dependencies__types_express_flash": "0.0.0",
- "npm_package_dependencies__types_express_session": "^1.15.12",
- "npm_package_dependencies__types_express_validator": "^3.0.0",
- "npm_package_dependencies__types_formidable": "^1.0.31",
- "npm_package_dependencies__types_jquery": "^3.3.29",
- "npm_package_dependencies__types_jsonwebtoken": "^8.3.2",
- "npm_package_dependencies__types_lodash": "^4.14.121",
- "npm_package_dependencies__types_mobile_detect": "^1.3.4",
- "npm_package_dependencies__types_mongodb": "^3.1.22",
- "npm_package_dependencies__types_mongoose": "^5.3.21",
- "npm_package_dependencies__types_node": "^10.12.30",
- "npm_package_dependencies__types_nodemailer": "^4.6.6",
- "npm_package_dependencies__types_passport": "^1.0.0",
- "npm_package_dependencies__types_passport_local": "^1.0.33",
- "npm_package_dependencies__types_prosemirror_commands": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_history": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_inputrules": "^1.0.2",
- "npm_package_dependencies__types_prosemirror_keymap": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_menu": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_model": "^1.7.0",
- "npm_package_dependencies__types_prosemirror_schema_basic": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_schema_list": "^1.0.1",
- "npm_package_dependencies__types_prosemirror_state": "^1.2.3",
- "npm_package_dependencies__types_prosemirror_transform": "^1.1.0",
- "npm_package_dependencies__types_prosemirror_view": "^1.3.1",
- "npm_package_dependencies__types_pug": "^2.0.4",
- "npm_package_dependencies__types_react": "^16.8.7",
- "npm_package_dependencies__types_react_color": "^2.14.1",
- "npm_package_dependencies__types_react_measure": "^2.0.4",
- "npm_package_dependencies__types_react_table": "^6.7.22",
- "npm_package_dependencies__types_request": "^2.48.1",
- "npm_package_dependencies__types_request_promise": "^4.1.42",
- "npm_package_dependencies__types_socket_io": "^2.1.2",
- "npm_package_dependencies__types_socket_io_client": "^1.4.32",
- "npm_package_dependencies__types_typescript": "^2.0.0",
- "npm_package_dependencies__types_uuid": "^3.4.4",
- "npm_package_dependencies__types_webpack": "^4.4.25",
- "npm_package_description": "Install Node.js, then, from the project directory, run",
- "npm_package_devDependencies_awesome_typescript_loader": "^5.2.1",
- "npm_package_devDependencies_chai": "^4.2.0",
- "npm_package_devDependencies_copy_webpack_plugin": "^4.6.0",
- "npm_package_devDependencies_css_loader": "^2.1.1",
- "npm_package_devDependencies_file_loader": "^3.0.1",
- "npm_package_devDependencies_fork_ts_checker_webpack_plugin": "^1.0.2",
- "npm_package_devDependencies_mocha": "^5.2.0",
- "npm_package_devDependencies_sass_loader": "^7.1.0",
- "npm_package_devDependencies_scss_loader": "0.0.1",
- "npm_package_devDependencies_style_loader": "^0.23.1",
- "npm_package_devDependencies_tslint": "^5.15.0",
- "npm_package_devDependencies_tslint_loader": "^3.5.4",
- "npm_package_devDependencies_ts_loader": "^5.3.3",
- "npm_package_devDependencies_ts_node": "^7.0.1",
- "npm_package_devDependencies_ts_node_dev": "^1.0.0-pre.32",
- "npm_package_devDependencies_typescript": "^3.4.1",
- "npm_package_devDependencies_webpack": "^4.29.6",
- "npm_package_devDependencies_webpack_cli": "^3.2.3",
- "npm_package_devDependencies_webpack_dev_middleware": "^3.6.1",
- "npm_package_devDependencies_webpack_dev_server": "^3.3.1",
- "npm_package_devDependencies_webpack_hot_middleware": "^2.24.3",
- "npm_package_devDependencies__types_chai": "^4.1.7",
- "npm_package_devDependencies__types_mocha": "^5.2.6",
- "npm_package_devDependencies__types_react_dom": "^16.8.2",
- "npm_package_devDependencies__types_webpack_dev_middleware": "^2.0.2",
- "npm_package_devDependencies__types_webpack_hot_middleware": "^2.16.4",
- "npm_package_gitHead": "f38d2de249ddf2cad533e2f92197738835688a73",
- "npm_package_main": "index.js",
- "npm_package_name": "dash",
- "npm_package_readmeFilename": "README.md",
- "npm_package_scripts_build": "webpack --env production",
- "npm_package_scripts_debug": "ts-node-dev --inspect -- src/server/index.ts",
- "npm_package_scripts_start": "ts-node-dev -- src/server/index.ts",
- "npm_package_scripts_test": "mocha -r ts-node/register test/**/*.ts",
- "npm_package_scripts_tsc": "tsc",
- "npm_package_version": "1.0.0",
- "NUMBER_OF_PROCESSORS": "1",
- "OLDPWD": "C:/Program Files/Git/",
- "ORIGINAL_PATH": "C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Users\\dash\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Program Files\\Microsoft\\Web Platform Installer;C:\\Program Files\\Microsoft SQL Server\\130\\Tools\\Binn;C:\\Program Files\\dotnet;C:\\Program Files\\nodejs;C:\\Users\\dash\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\dash\\AppData\\Roaming\\npm",
- "ORIGINAL_TEMP": "C:/Users/dash/AppData/Local/Temp/2",
- "ORIGINAL_TMP": "C:/Users/dash/AppData/Local/Temp/2",
- "OS": "Windows_NT",
- "PATH": "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\npm-lifecycle\\node-gyp-bin;C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\.bin;C:\\Users\\dash\\bin;C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\local\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Users\\dash\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Program Files\\Microsoft\\Web Platform Installer;C:\\Program Files\\Microsoft SQL Server\\130\\Tools\\Binn;C:\\Program Files\\dotnet;C:\\Program Files\\nodejs;C:\\Users\\dash\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\dash\\AppData\\Roaming\\npm;C:\\Program Files\\Git\\usr\\bin\\vendor_perl;C:\\Program Files\\Git\\usr\\bin\\core_perl",
- "PATHEXT": ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JSE;.WSF;.WSH;.MSC",
- "PKG_CONFIG_PATH": "C:\\Program Files\\Git\\mingw64\\lib\\pkgconfig;C:\\Program Files\\Git\\mingw64\\share\\pkgconfig",
- "PLINK_PROTOCOL": "ssh",
- "PROCESSOR_ARCHITECTURE": "AMD64",
- "PROCESSOR_IDENTIFIER": "Intel64 Family 6 Model 63 Stepping 2, GenuineIntel",
- "PROCESSOR_LEVEL": "6",
- "PROCESSOR_REVISION": "3f02",
- "ProgramData": "C:\\ProgramData",
- "PROGRAMFILES": "C:\\Program Files",
- "ProgramFiles(x86)": "C:\\Program Files (x86)",
- "ProgramW6432": "C:\\Program Files",
- "PROMPT": "$P$G",
- "PSModulePath": "C:\\Program Files\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules;C:\\Program Files\\WindowsPowerShell\\Modules\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\ResourceManager\\AzureResourceManager\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\ServiceManagement\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\Storage\\",
- "PUBLIC": "C:\\Users\\Public",
- "PWD": "C:/Users/dash/Documents/Dash-Web",
- "SESSIONNAME": "RDP-Tcp#0",
- "SHELL": "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
- "SHLVL": "2",
- "SSH_ASKPASS": "C:/Program Files/Git/mingw64/libexec/git-core/git-gui--askpass",
- "SYSTEMDRIVE": "C:",
- "SYSTEMROOT": "C:\\Windows",
- "TEMP": "C:\\Users\\dash\\AppData\\Local\\Temp\\2",
- "TERM": "xterm",
- "TMP": "C:\\Users\\dash\\AppData\\Local\\Temp\\2",
- "TMPDIR": "C:\\Users\\dash\\AppData\\Local\\Temp\\2",
- "TSCONFIG": "C:\\Users\\dash\\Documents\\Dash-Web\\tsconfig.json",
- "TSLINT": "true",
- "TSLINTAUTOFIX": "false",
- "TYPESCRIPT_PATH": "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\typescript\\lib\\typescript.js",
- "USERDOMAIN": "dashserver",
- "USERDOMAIN_ROAMINGPROFILE": "dashserver",
- "USERNAME": "dash",
- "USERPROFILE": "C:\\Users\\dash",
- "USE_INCREMENTAL_API": "true",
- "VUE": "false",
- "WATCH": "",
- "WINDIR": "C:\\Windows",
- "WORK_DIVISION": "1",
- "_": "C:/Program Files/nodejs/node.exe"
- },
- "sharedObjects": [
- "C:\\Program Files\\nodejs\\node.exe",
- "C:\\Windows\\SYSTEM32\\ntdll.dll",
- "C:\\Windows\\System32\\KERNEL32.DLL",
- "C:\\Windows\\System32\\KERNELBASE.dll",
- "C:\\Windows\\System32\\PSAPI.DLL",
- "C:\\Windows\\System32\\WS2_32.dll",
- "C:\\Windows\\System32\\sechost.dll",
- "C:\\Windows\\System32\\RPCRT4.dll",
- "C:\\Windows\\System32\\ADVAPI32.dll",
- "C:\\Windows\\System32\\msvcrt.dll",
- "C:\\Windows\\System32\\USER32.dll",
- "C:\\Windows\\System32\\win32u.dll",
- "C:\\Windows\\System32\\GDI32.dll",
- "C:\\Windows\\System32\\gdi32full.dll",
- "C:\\Windows\\System32\\CRYPT32.dll",
- "C:\\Windows\\System32\\ucrtbase.dll",
- "C:\\Windows\\System32\\MSASN1.dll",
- "C:\\Windows\\SYSTEM32\\dbghelp.dll",
- "C:\\Windows\\SYSTEM32\\IPHLPAPI.DLL",
- "C:\\Windows\\SYSTEM32\\USERENV.dll",
- "C:\\Windows\\System32\\profapi.dll",
- "C:\\Windows\\SYSTEM32\\bcrypt.dll",
- "C:\\Windows\\SYSTEM32\\WINMM.dll",
- "C:\\Windows\\SYSTEM32\\WINMMBASE.dll",
- "C:\\Windows\\System32\\cfgmgr32.dll",
- "C:\\Windows\\System32\\IMM32.DLL",
- "C:\\Windows\\System32\\powrprof.dll",
- "C:\\Windows\\system32\\mswsock.dll",
- "C:\\Windows\\System32\\kernel.appcore.dll",
- "C:\\Windows\\System32\\bcryptprimitives.dll",
- "C:\\Windows\\system32\\napinsp.dll",
- "C:\\Windows\\SYSTEM32\\DNSAPI.dll",
- "C:\\Windows\\System32\\NSI.dll",
- "C:\\Windows\\System32\\winrnr.dll",
- "C:\\Windows\\system32\\NLAapi.dll"
- ]
-} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f96e3bcd1..811bb5fb2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1014,3 +1014,4 @@ export namespace DocUtils {
}
Scripting.addGlobal("Docs", Docs);
+
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 30938688d..1bf242d93 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -7,6 +7,7 @@
box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
flex-direction: column;
background: whitesmoke;
+ padding-top: 10px;
padding-bottom: 10px;
border-radius: 15px;
border: solid #BBBBBBBB 1px;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 20238985d..be46e0107 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -42,6 +42,7 @@ import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { ScriptField } from '../../new_fields/ScriptField';
+import { TimelineMenu } from './animationtimeline/TimelineMenu';
@observer
export class MainView extends React.Component {
@@ -163,6 +164,9 @@ export class MainView extends React.Component {
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
}
+ if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) {
+ TimelineMenu.Instance.closeMenu();
+ }
});
globalPointerUp = () => this.isPointerDown = false;
@@ -580,6 +584,7 @@ export class MainView extends React.Component {
<MarqueeOptionsMenu />
<RichTextMenu />
<OverlayView />
+ <TimelineMenu />
</div >);
}
}
diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss
new file mode 100644
index 000000000..84c8de287
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.scss
@@ -0,0 +1,105 @@
+@import "./../globalCssVariables.scss";
+
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.bar {
+ height: 100%;
+ width: 5px;
+ position: absolute;
+
+ // pointer-events: none;
+ .menubox {
+ width: 200px;
+ height: 200px;
+ top: 50%;
+ position: relative;
+ background-color: $light-color;
+
+ .menutable {
+ tr:nth-child(odd) {
+ background-color: $light-color-secondary;
+ }
+ }
+ }
+
+ .leftResize {
+ left: -10px;
+ border: 3px solid black;
+ }
+
+ .rightResize {
+ right: -10px;
+ border: 3px solid black;
+ }
+
+ .keyframe-indicator {
+ height: 20px;
+ width: 20px;
+ top: calc(50% - 10px);
+ background-color: white;
+ -ms-transform: rotate(45deg);
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ z-index: 1000;
+ position: absolute;
+ }
+
+ .keyframe-information {
+ display: none;
+ position: relative;
+ // z-index: 100000;
+ // background: $timelineDark;
+ width: 100px;
+ // left: -50px;
+ height: 100px;
+ // top: 40px;
+ }
+
+ .keyframeCircle {
+ left: -10px;
+ border: 3px solid $timelineDark;
+ }
+
+ .fadeLeft {
+ left: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to left, $timelineColor 10%, $light-color);
+ }
+
+ .fadeRight {
+ right: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to right, $timelineColor 10%, $light-color);
+ }
+
+ .divider {
+ height: 100%;
+ width: 1px;
+ position: absolute;
+ background-color: black;
+ cursor: col-resize;
+ pointer-events: none;
+ }
+
+ .keyframe {
+ height: 100%;
+ position: absolute;
+ }
+
+ .fadeIn-container,
+ .fadeOut-container,
+ .body-container {
+ position: absolute;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
new file mode 100644
index 000000000..a3407f653
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -0,0 +1,560 @@
+import * as React from "react";
+import "./Keyframe.scss";
+import "./Timeline.scss";
+import "../globalCssVariables.scss";
+import { observer } from "mobx-react";
+import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction, trace } from "mobx";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { Transform } from "../../util/Transform";
+import { TimelineMenu } from "./TimelineMenu";
+import { Docs } from "../../documents/Documents";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { emptyPath } from "../../../Utils";
+
+
+
+/**
+ * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
+ */
+export namespace KeyframeFunc {
+
+ export enum KeyframeType {
+ end = "end",
+ fade = "fade",
+ default = "default",
+ }
+
+ export enum Direction {
+ left = "left",
+ right = "right"
+ }
+
+ export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => {
+ let leftMost: (RegionData | undefined) = undefined;
+ let rightMost: (RegionData | undefined) = undefined;
+ regions.forEach(region => {
+ const neighbor = RegionData(region);
+ if (currentRegion.position! > neighbor.position) {
+ if (!leftMost || neighbor.position > leftMost.position) {
+ leftMost = neighbor;
+ }
+ } else if (currentRegion.position! < neighbor.position) {
+ if (!rightMost || neighbor.position < rightMost.position) {
+ rightMost = neighbor;
+ }
+ }
+ });
+ if (dir === Direction.left) {
+ return leftMost;
+ } else if (dir === Direction.right) {
+ return rightMost;
+ }
+ };
+
+ export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+ let leftKf: Opt<Doc>;
+ let time: number = 0;
+ const keyframes = DocListCast(region.keyframes!);
+ keyframes.map((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
+ leftKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return leftKf;
+ };
+
+
+ export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right
+ let rightKf: Opt<Doc>;
+ let time: number = Infinity;
+ DocListCast(region.keyframes!).forEach((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
+ rightKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return rightKf;
+ };
+
+ export const defaultKeyframe = () => {
+ const regiondata = new Doc(); //creating regiondata in MILI
+ regiondata.duration = 4000;
+ regiondata.position = 0;
+ regiondata.fadeIn = 1000;
+ regiondata.fadeOut = 1000;
+ regiondata.functions = new List<Doc>();
+ regiondata.hasData = false;
+ return regiondata;
+ };
+
+
+ export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
+ const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+ switch (unit) {
+ case "mili":
+ return time;
+ case "sec":
+ return dir === "pixel" ? time / 1000 : time * 1000;
+ case "min":
+ return dir === "pixel" ? time / 60000 : time * 60000;
+ case "hr":
+ return dir === "pixel" ? time / 3600000 : time * 3600000;
+ default:
+ return time;
+ }
+ };
+}
+
+export const RegionDataSchema = createSchema({
+ position: defaultSpec("number", 0),
+ duration: defaultSpec("number", 0),
+ keyframes: listSpec(Doc),
+ fadeIn: defaultSpec("number", 0),
+ fadeOut: defaultSpec("number", 0),
+ functions: listSpec(Doc),
+ hasData: defaultSpec("boolean", false)
+});
+export type RegionData = makeInterface<[typeof RegionDataSchema]>;
+export const RegionData = makeInterface(RegionDataSchema);
+
+interface IProps {
+ node: Doc;
+ RegionData: Doc;
+ collection: Doc;
+ tickSpacing: number;
+ tickIncrement: number;
+ time: number;
+ changeCurrentBarX: (x: number) => void;
+ transform: Transform;
+ makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc;
+}
+
+
+/**
+ *
+ * This class handles the green region stuff
+ * Key facts:
+ *
+ * Structure looks like this
+ *
+ * region as a whole
+ * <------------------------------REGION------------------------------->
+ *
+ * region broken down
+ *
+ * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void.........
+ * (start) (Fade 2)
+ * (fade 1) (finish)
+ *
+ *
+ * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
+ * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
+ * If you want to know the exact location of the computer, message me.
+ *
+ * @author Andrew Kim
+ */
+@observer
+export class Keyframe extends React.Component<IProps> {
+
+ @observable private _bar = React.createRef<HTMLDivElement>();
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+
+ @computed private get regiondata() { return RegionData(this.props.RegionData); }
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); }
+ @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+
+ constructor(props: any) {
+ super(props);
+ }
+ componentDidMount() {
+ setTimeout(() => { //giving it a temporary 1sec delay...
+ if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
+ const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end);
+ const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
+ const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade);
+ const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end);
+ (fadeIn.key as Doc).opacity = 1;
+ (fadeOut.key as Doc).opacity = 1;
+ (start.key as Doc).opacity = 0.1;
+ (finish.key as Doc).opacity = 0.1;
+ this.forceUpdate(); //not needed, if setTimeout is gone...
+ }, 1000);
+ }
+
+ @action
+ onBarPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this.createKeyframe(clientX);
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onBarPointerMove);
+ document.addEventListener("pointerup", (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onBarPointerMove);
+ });
+ }
+ }
+
+ @action
+ onBarPointerMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0) {
+ this._mouseToggled = true;
+ }
+ const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
+ const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions!);
+ const prevX = this.regiondata.position;
+ const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (futureX <= 0) {
+ this.regiondata.position = 0;
+ } else if ((left && left.position + left.duration >= futureX)) {
+ this.regiondata.position = left.position + left.duration;
+ } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+ this.regiondata.position = right.position - this.regiondata.duration;
+ } else {
+ this.regiondata.position = futureX;
+ }
+ const movement = this.regiondata.position - prevX;
+ this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement);
+ }
+
+ @action
+ onResizeLeft = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeLeft);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeLeft);
+ });
+ }
+
+ @action
+ onResizeRight = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeRight);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeRight);
+ });
+ }
+
+ @action
+ onDragResizeLeft = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
+ if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
+ this.regiondata.position = leftRegion.position + leftRegion.duration;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration);
+ } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) {
+ this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn;
+ } else if (NumCast(this.keyframes[0].time) + offset <= 0) {
+ this.regiondata.position = 0;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time);
+ } else {
+ this.regiondata.duration -= offset;
+ this.regiondata.position += offset;
+ }
+ this.keyframes[0].time = this.regiondata.position;
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+
+
+ @action
+ onDragResizeRight = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
+ const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time);
+ if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way
+ this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut;
+ } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) {
+ this.regiondata.duration = rightRegion.position - this.regiondata.position;
+ } else {
+ this.regiondata.duration += offset;
+ }
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ }
+
+
+ @action
+ createKeyframe = async (clientX: number) => {
+ this._mouseToggled = true;
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+ const position = this.regiondata.position;
+ this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default);
+ this.regiondata.hasData = true;
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
+
+ }
+ }
+
+
+ @action
+ moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+ /**
+ * custom keyframe context menu items (when clicking on the keyframe circle)
+ */
+ @action
+ makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Show Data", () => {
+ runInAction(() => {
+ const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
+ CollectionDockingView.AddRightSplit(kvp, emptyPath);
+ });
+ }),
+ TimelineMenu.Instance.addItem("button", "Delete", () => {
+ runInAction(() => {
+ (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+ this.forceUpdate();
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", "Move", (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ const kfIndex: number = this.keyframes.indexOf(kf);
+ if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.keyframes[kfIndex].time = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ });
+ TimelineMenu.Instance.addMenu("Keyframe");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ /**
+ * context menu for region (anywhere on the green region).
+ */
+ @action
+ makeRegionMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Remove Region", () =>
+ Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+ TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeIn = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeOut = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => {
+ runInAction(() => {
+ const prevPosition = this.regiondata.position;
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.position = parseInt(val, 10);
+ this.updateKeyframes(this.regiondata.position - prevPosition);
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ val += this.regiondata.position;
+ if ((val < 0) || (val > pos && val < pos + dur)) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.duration = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addMenu("Region");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ @action
+ updateKeyframes = (incr: number, filter: number[] = []) => {
+ this.keyframes.forEach(kf => {
+ if (!filter.includes(this.keyframes.indexOf(kf))) {
+ kf.time = NumCast(kf.time) + incr;
+ }
+ });
+ }
+
+ /**
+ * hovering effect when hovered (hidden div darkens)
+ */
+ @action
+ onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "1";
+ Doc.BrushDoc(this.props.node);
+ }
+
+ /**
+ * hovering effect when hovered out (hidden div becomes invisible)
+ */
+ @action
+ onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "0";
+ Doc.UnBrushDoc(this.props.node);
+ }
+
+
+ ///////////////////////UI STUFF /////////////////////////
+
+
+ /**
+ * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades)
+ * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable)
+ */
+ drawKeyframes = () => {
+ const keyframeDivs: JSX.Element[] = [];
+ return DocListCast(this.regiondata.keyframes).map(kf => {
+ if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) {
+ return <>
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ <div className="keyframeCircle keyframe-indicator"
+ onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }}
+ onContextMenu={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.makeKeyframeMenu(kf, e.nativeEvent);
+ }}
+ onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
+ </div>
+ </div>
+ <div className="keyframe-information" />
+ </>;
+ } else {
+ return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider" />
+ </div>
+ }
+ });
+ }
+
+ /**
+ * drawing the hidden divs that partition different intervals within a region.
+ */
+ @action
+ drawKeyframeDividers = () => {
+ const keyframeDividers: JSX.Element[] = [];
+ DocListCast(this.regiondata.keyframes).forEach(kf => {
+ const index = this.keyframes.indexOf(kf);
+ if (index !== this.keyframes.length - 1) {
+ const right = this.keyframes[index + 1];
+ const bodyRef = React.createRef<HTMLDivElement>();
+ const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ keyframeDividers.push(
+ <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
+ onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }}
+ onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (index !== 0 || index !== this.keyframes.length - 2) {
+ this._mouseToggled = true;
+ }
+ this.makeRegionMenu(kf, e.nativeEvent);
+ }}>
+ </div>
+ );
+ }
+ });
+ return keyframeDividers;
+ }
+
+ /**
+ * rendering that green region
+ */
+ //154, 206, 223
+ render() {
+ trace();
+ console.log(this.props.RegionData.position);
+ console.log(this.regiondata.position);
+ console.log(this.pixelPosition);
+ return (
+ <div className="bar" ref={this._bar} style={{
+ transform: `translate(${this.pixelPosition}px)`,
+ width: `${this.pixelDuration}px`,
+ background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )`
+ }}
+ onPointerDown={this.onBarPointerDown}>
+ <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div>
+ {/* <div className="keyframe-information"></div> */}
+ <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div>
+ {/* <div className="keyframe-information"></div> */}
+ {this.drawKeyframes()}
+ {this.drawKeyframeDividers()}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
new file mode 100644
index 000000000..f90249771
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -0,0 +1,322 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timeline-toolbox {
+ position: absolute;
+ margin: 0px;
+ padding: 0px;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ // justify-content: space-evenly;
+ align-items: center;
+ top: 3px;
+ width: 100%;
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ }
+
+ .playbackControls {
+ display: flex;
+ margin-left: 30px;
+ max-width: 84px;
+ width: 84px;
+
+ .timeline-icon {
+ color: $timelineColor;
+ margin-left: 3px;
+ }
+
+ }
+
+ .grid-box {
+ display: flex;
+ // grid-template-columns: [first] 20% [line2] 20% [line3] 60%;
+ width: calc(100% - 150px);
+ // width: 100%;
+ margin-left: 10px;
+
+ .time-box {
+ margin-left: 10px;
+ min-width: 140px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .resetView-tool {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+ color: $timelineDark;
+ }
+
+ .resetView-tool:hover {
+ -webkit-transform: scale(1.1);
+ -ms-transform: scale(1.1);
+ transform: scale(1.1);
+ transition: .2s ease;
+ }
+ }
+
+ .mode-box {
+ display: flex;
+ margin-left: 5px;
+ }
+
+ .overview-box {
+ width: 80%;
+ display: flex;
+ }
+
+ div {
+ padding: 0px;
+ // margin-left: 10px;
+ }
+ }
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .animation-text {
+ // font-size: 16px;
+ height: auto;
+ width: auto;
+ white-space: nowrap;
+ font-size: 14px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ }
+
+ .round-toggle {
+ height: 20px;
+ width: 40px;
+ min-width: 40px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 20px;
+ animation-fill-mode: forwards;
+ animation-duration: 500ms;
+ top: 30px;
+ margin-left: 5px;
+
+ input {
+ position: absolute;
+ opacity: 0;
+ height: 0;
+ width: 0;
+ }
+
+ .round-toggle-slider {
+ height: 17px;
+ width: 17px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 50%;
+ transition: transform 500ms ease-in-out;
+ margin-left: 0px;
+ // margin-top: 0.5px;
+ }
+ }
+
+}
+
+.time-input {
+ height: 20px;
+ // width: 120px;
+ width: 100%;
+ white-space: nowrap;
+ font-size: 12px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-left: 5px;
+ margin-left: 5px;
+}
+
+.tick {
+ height: 100%;
+ width: 2px;
+ background-color: black;
+ color: black;
+}
+
+.number-label {
+ color: black;
+ transform: rotate(-90deg) translate(-15px, 8px);
+ font-size: .85em;
+}
+
+.timeline-container {
+ width: 100%;
+ height: 300px;
+ position: absolute;
+ background-color: $light-color-secondary;
+ border-bottom: 2px solid $timelineDark;
+ transition: transform 500ms ease;
+
+ .info-container {
+ margin-top: 50px;
+ right: 20px;
+ position: absolute;
+ height: calc(100% - 100px);
+ width: calc(100% - 140px);
+ overflow: hidden;
+
+ .scrubberbox {
+ position: absolute;
+ background-color: transparent;
+ height: 30px;
+ width: 100%;
+
+ }
+
+ .scrubber {
+ top: 30px;
+ height: 100%;
+ width: 2px;
+ position: absolute;
+ z-index: 1001;
+ background-color: black;
+ pointer-events: none;
+
+ .scrubberhead {
+ top: -20px;
+ height: 20px;
+ width: 20px;
+ background-color: white;
+ border-radius: 50%;
+ border: 3px solid black;
+ left: -9px;
+ position: absolute;
+ pointer-events: all;
+ }
+ }
+
+ .trackbox {
+ top: 30px;
+ height: calc(100% - 30px);
+ // height: 100%;
+ // height: 100%;
+ width: 100%;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ overflow: hidden;
+ // overflow-y: scroll;
+ background-color: white;
+ position: absolute;
+ // box-shadow: -10px 0px 10px 10px red;
+ }
+
+ }
+
+ .currentTime {
+ // background: red;
+ font-size: 12px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 40px;
+ top: 40px;
+ position: relative;
+ width: 100px;
+ margin-left: 20px;
+ }
+
+ .title-container {
+ // margin-top: 80px;
+ margin-top: 40px;
+ margin-left: 20px;
+ height: calc(100% - 100px - 30px);
+ // height: 100%;
+ width: 100px;
+ background-color: white;
+ overflow: hidden;
+ border-left: 2px solid black;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ border-right: 2px solid $timelineDark;
+
+ .datapane {
+ top: 0px;
+ width: 100px;
+ height: 30%;
+ border: 1px solid $dark-color;
+ font-size: 12px;
+ line-height: 11px;
+ background-color: $timelineDark;
+ color: white;
+ position: relative;
+ float: left;
+ padding: 3px;
+ border-style: solid;
+ overflow-y: scroll;
+ overflow-x: hidden;
+
+ p {
+ hyphens: auto;
+ }
+
+ }
+ }
+
+ .resize {
+ bottom: 0px;
+ position: absolute;
+ height: 20px;
+ width: 40px;
+ left: calc(50% - 25px);
+ color: $timelineDark;
+ }
+}
+
+
+
+.overview {
+ position: absolute;
+ height: 50px;
+ width: 200px;
+ background-color: black;
+
+ .container {
+ position: absolute;
+ float: left 0px;
+ top: 25%;
+ height: 75%;
+ width: 100%;
+ background-color: grey;
+ }
+}
+
+
+.timeline-checker {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ position: absolute;
+ display: flex;
+ padding: 10px 10px;
+
+ div {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ margin: 0px 10px;
+ cursor: pointer
+ }
+
+ .check {
+ width: 50px;
+ height: 50px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
new file mode 100644
index 000000000..677267ca0
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -0,0 +1,633 @@
+import * as React from "react";
+import "./Timeline.scss";
+import { listSpec } from "../../../new_fields/Schema";
+import { observer } from "mobx-react";
+import { Track } from "./Track";
+import { observable, action, computed, runInAction, IReactionDisposer, reaction, trace } from "mobx";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPlayCircle, faBackward, faForward, faGripLines, faPauseCircle, faEyeSlash, faEye, faCheckCircle, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
+import { ContextMenu } from "../ContextMenu";
+import { TimelineOverview } from "./TimelineOverview";
+import { FieldViewProps } from "../nodes/FieldView";
+import { KeyframeFunc } from "./Keyframe";
+import { Utils } from "../../../Utils";
+
+/**
+ * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are
+ * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make
+ * any logical changes here. Most work is needed on UI.
+ *
+ * The hierarchy works this way:
+ *
+ * Timeline.tsx --> Track.tsx --> Keyframe.tsx
+ | |
+ | TimelineMenu.tsx (timeline's custom contextmenu)
+ |
+ |
+ TimelineOverview.tsx (youtube like dragging thing is play mode, complex dragging thing in editing mode)
+
+
+ Most style changes are in SCSS file.
+ If you have any questions, email me or text me.
+ @author Andrew Kim
+ */
+
+
+@observer
+export class Timeline extends React.Component<FieldViewProps> {
+
+
+ //readonly constants
+ private readonly DEFAULT_TICK_SPACING: number = 50;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private readonly MAX_CONTAINER_HEIGHT: number = 800;
+ private readonly DEFAULT_TICK_INCREMENT: number = 1000;
+
+ //height variables
+ private DEFAULT_CONTAINER_HEIGHT: number = 330;
+ private MIN_CONTAINER_HEIGHT: number = 205;
+
+ //react refs
+ @observable private _trackbox = React.createRef<HTMLDivElement>();
+ @observable private _titleContainer = React.createRef<HTMLDivElement>();
+ @observable private _timelineContainer = React.createRef<HTMLDivElement>();
+ @observable private _infoContainer = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleRef = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleContainerRef = React.createRef<HTMLDivElement>();
+ @observable private _timeInputRef = React.createRef<HTMLInputElement>();
+
+ //boolean vars and instance vars
+ @observable private _currentBarX: number = 0;
+ @observable private _windSpeed: number = 1;
+ @observable private _isPlaying: boolean = false; //scrubber playing
+ @observable private _totalLength: number = 0;
+ @observable private _visibleLength: number = 0;
+ @observable private _visibleStart: number = 0;
+ @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
+ @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
+ @observable private _time = 100000; //DEFAULT
+ @observable private _playButton = faPlayCircle;
+ @observable private _timelineVisible = false;
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+ @observable private _titleHeight = 0;
+
+ // so a reaction can be made
+ @observable public _isAuthoring = this.props.Document.isATOn;
+
+ /**
+ * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit.
+ */
+ @computed
+ private get children(): List<Doc> {
+ const extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type));
+ if (extendedDocument) {
+ if (this.props.Document.data_ext) {
+ return Cast((Cast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"], Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>;
+ } else {
+ return new List<Doc>();
+ }
+ }
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>;
+ }
+
+ /////////lifecycle functions////////////
+ componentWillMount() {
+ const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight
+ this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max
+ this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset
+ this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset
+ }
+
+ componentDidMount() {
+ runInAction(() => {
+ if (!this.props.Document.AnimationLength) { //if animation length did not exist
+ this.props.Document.AnimationLength = this._time; //set it to default time
+ } else {
+ this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before
+ }
+ this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself)
+ this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts
+ this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on
+ this.toggleHandle();
+ });
+ }
+
+ componentWillUnmount() {
+ runInAction(() => {
+ this.props.Document.AnimationLength = this._time; //save animation length
+ });
+ }
+ /////////////////////////////////////////////////
+
+ /**
+ * React Functional Component
+ * Purpose: For drawing Tick marks across the timeline in authoring mode
+ */
+ @action
+ drawTicks = () => {
+ const ticks = [];
+ for (let i = 0; i < this._time / this._tickIncrement; i++) {
+ ticks.push(<div key={Utils.GenerateGuid()} className="tick" style={{ transform: `translate(${i * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p className="number-label">{this.toReadTime(i * this._tickIncrement)}</p></div>);
+ }
+ return ticks;
+ }
+
+ /**
+ * changes the scrubber to actual pixel position
+ */
+ @action
+ changeCurrentBarX = (pixel: number) => {
+ pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel;
+ }
+
+ //for playing
+ @action
+ onPlay = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.play();
+ }
+
+ /**
+ * when playbutton is clicked
+ */
+ @action
+ play = () => {
+ if (this._isPlaying) {
+ this._isPlaying = false;
+ this._playButton = faPlayCircle;
+ } else {
+ this._isPlaying = true;
+ this._playButton = faPauseCircle;
+ const playTimeline = () => {
+ if (this._isPlaying) {
+ if (this._currentBarX >= this._totalLength) {
+ this.changeCurrentBarX(0);
+ } else {
+ this.changeCurrentBarX(this._currentBarX + this._windSpeed);
+ }
+ setTimeout(playTimeline, 15);
+ }
+ };
+ playTimeline();
+ }
+ }
+
+
+ /**
+ * fast forward the timeline scrubbing
+ */
+ @action
+ windForward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed < 64) { //max speed is 32
+ this._windSpeed = this._windSpeed * 2;
+ }
+ }
+
+ /**
+ * rewind the timeline scrubbing
+ */
+ @action
+ windBackward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed > 1 / 16) { // min speed is 1/8
+ this._windSpeed = this._windSpeed / 2;
+ }
+ }
+
+ /**
+ * scrubber down
+ */
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ });
+ }
+
+ /**
+ * when there is any scrubber movement
+ */
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberbox = this._infoContainer.current!;
+ const left = scrubberbox.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale;
+ this.changeCurrentBarX(offsetX + this._visibleStart); //changes scrubber to clicked scrubber position
+ }
+
+ /**
+ * when panning the timeline (in editing mode)
+ */
+ @action
+ onPanDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX - this._trackbox.current!.getBoundingClientRect().left);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onPanMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onPanMove);
+ if (!this._doubleClickEnabled) {
+ this._mouseToggled = false;
+ }
+ });
+
+ }
+ }
+
+ /**
+ * when moving the timeline (in editing mode)
+ */
+ @action
+ onPanMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0 || e.movementY !== 0) {
+ this._mouseToggled = true;
+ }
+ const trackbox = this._trackbox.current!;
+ const titleContainer = this._titleContainer.current!;
+ this.movePanX(this._visibleStart - e.movementX);
+ trackbox.scrollTop = trackbox.scrollTop - e.movementY;
+ titleContainer.scrollTop = titleContainer.scrollTop - e.movementY;
+ if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) {
+ this._visibleStart -= e.movementX;
+ this._totalLength -= e.movementX;
+ this._time -= KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ this.props.Document.AnimationLength = this._time;
+ }
+
+ }
+
+
+ @action
+ movePanX = (pixel: number) => {
+ const infoContainer = this._infoContainer.current!;
+ infoContainer.scrollLeft = pixel;
+ this._visibleStart = infoContainer.scrollLeft;
+ }
+
+ /**
+ * resizing timeline (in editing mode) (the hamburger drag icon)
+ */
+ @action
+ onResizeDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onResizeMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onResizeMove);
+ });
+ }
+
+ @action
+ onResizeMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom;
+ // let offset = 0;
+ if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MIN_CONTAINER_HEIGHT;
+ } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MAX_CONTAINER_HEIGHT;
+ } else {
+ this._containerHeight += offset;
+ }
+ }
+
+ /**
+ * for displaying time to standard min:sec
+ */
+ @action
+ toReadTime = (time: number): string => {
+ time = time / 1000;
+ const inSeconds = Math.round(time * 100) / 100;
+
+ const min: (string | number) = Math.floor(inSeconds / 60);
+ const sec: (string | number) = (Math.round((inSeconds % 60) * 100) / 100);
+ let secString = sec.toFixed(2);
+
+ if (Math.floor(sec / 10) === 0) {
+ secString = "0" + secString;
+ }
+
+ return `${min}:${secString}`;
+ }
+
+
+ /**
+ * context menu function.
+ * opens the timeline or closes the timeline.
+ * Used in: Freeform
+ */
+ timelineContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({
+ description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => {
+ this._timelineVisible = !this._timelineVisible;
+ }), icon: this._timelineVisible ? faEyeSlash : faEye
+ });
+ }
+
+
+ /**
+ * timeline zoom function
+ * use mouse middle button to zoom in/out the timeline
+ */
+ @action
+ onWheelZoom = (e: React.WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left;
+ const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement);
+ const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ e.deltaY < 0 ? this.zoom(true) : this.zoom(false);
+ const currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._infoContainer.current!.scrollLeft = currPixel - offset;
+ this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0;
+ this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0;
+ this.changeCurrentBarX(currCurrent);
+ }
+
+
+ resetView(doc: Doc) {
+ doc._panX = doc._customOriginX ?? 0;
+ doc._panY = doc._customOriginY ?? 0;
+ doc.scale = doc._customOriginScale ?? 1;
+ }
+
+ setView(doc: Doc) {
+ doc._customOriginX = doc._panX;
+ doc._customOriginY = doc._panY;
+ doc._customOriginScale = doc.scale;
+ }
+ /**
+ * zooming mechanism (increment and spacing changes)
+ */
+ @action
+ zoom = (dir: boolean) => {
+ let spacingChange = this._tickSpacing;
+ let incrementChange = this._tickIncrement;
+ if (dir) {
+ if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)) {
+ if (this._tickSpacing >= 100) {
+ incrementChange /= 2;
+ spacingChange = 50;
+ } else {
+ spacingChange += 5;
+ }
+ }
+ } else {
+ if (this._tickSpacing <= 50) {
+ spacingChange = 100;
+ incrementChange *= 2;
+ } else {
+ spacingChange -= 5;
+ }
+ }
+ const finalLength = spacingChange * (this._time / incrementChange);
+ if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width) {
+ this._totalLength = finalLength;
+ this._tickSpacing = spacingChange;
+ this._tickIncrement = incrementChange;
+ }
+ }
+
+ /**
+ * tool box includes the toggle buttons at the top of the timeline (both editing mode and play mode)
+ */
+ private timelineToolBox = (scale: number, totalTime: number) => {
+ const size = 40 * scale; //50 is default
+ const iconSize = 25;
+
+ //decides if information should be omitted because the timeline is very small
+ // if its less than 950 pixels then it's going to be overlapping
+ let shouldCompress = false;
+ const width: number = this.props.PanelWidth();
+ if (width < 850) {
+ shouldCompress = true;
+ }
+
+ let modeString, overviewString, lengthString;
+ const modeType = this.props.Document.isATOn ? "Author" : "Play";
+
+ if (!shouldCompress) {
+ modeString = "Mode: " + modeType;
+ overviewString = "Overview:";
+ lengthString = "Length: ";
+ }
+ else {
+ modeString = modeType;
+ overviewString = "";
+ lengthString = "";
+ }
+
+ // let rightInfo = this.timeIndicator;
+
+ return (
+ <div key="timeline_toolbox" className="timeline-toolbox" style={{ height: `${size}px` }}>
+ <div className="playbackControls">
+ <div className="timeline-icon" key="timeline_windBack" onClick={this.windBackward} title="Slow Down Animation"> <FontAwesomeIcon icon={faBackward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key=" timeline_play" onClick={this.onPlay} title="Play/Pause"> <FontAwesomeIcon icon={this._playButton} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key="timeline_windForward" onClick={this.windForward} title="Speed Up Animation"> <FontAwesomeIcon icon={faForward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ </div>
+ <div className="grid-box overview-tool">
+ <div className="overview-box">
+ <div key="overview-text" className="animation-text">{overviewString}</div>
+ <TimelineOverview tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} time={this._time} parent={this} isAuthoring={BoolCast(this.props.Document.isATOn)} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX} />
+ </div>
+ <div className="mode-box overview-tool">
+ <div key="animation-text" className="animation-text">{modeString}</div>
+ <div key="round-toggle" ref={this._roundToggleContainerRef} className="round-toggle">
+ <div key="round-toggle-slider" ref={this._roundToggleRef} className="round-toggle-slider" onPointerDown={this.toggleChecked}> </div>
+ </div>
+ </div>
+ <div className="time-box overview-tool" style={{ display: this._timelineVisible ? "flex" : "none" }}>
+ {this.timeIndicator(lengthString, totalTime)}
+ <div className="resetView-tool" title="Return to Default View" onClick={() => this.resetView(this.props.Document)}><FontAwesomeIcon icon="compress-arrows-alt" size="lg" /></div>
+ <div className="resetView-tool" style={{ display: this._isAuthoring ? "flex" : "none" }} title="Set Default View" onClick={() => this.setView(this.props.Document)}><FontAwesomeIcon icon="expand-arrows-alt" size="lg" /></div>
+
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ timeIndicator(lengthString: string, totalTime: number) {
+ if (this.props.Document.isATOn) {
+ return (
+ <div key="time-text" className="animation-text" style={{ visibility: this.props.Document.isATOn ? "visible" : "hidden", display: this.props.Document.isATOn ? "flex" : "none" }}>{`Total: ${this.toReadTime(totalTime)}`}</div>
+ );
+ }
+ else {
+ return (
+ <div style={{ flexDirection: "column" }}>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>{`Current: ${this.getCurrentTime()}`}</div>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>{`Total: ${this.toReadTime(this._time)}`}</div>
+ </div>
+ );
+ }
+ }
+
+ /**
+ * when the user decides to click the toggle button (either user wants to enter editing mode or play mode)
+ */
+ @action
+ private toggleChecked = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.toggleHandle();
+ }
+
+ /**
+ * turns on the toggle button (the purple slide button that changes from editing mode and play mode
+ */
+ private toggleHandle = () => {
+ const roundToggle = this._roundToggleRef.current!;
+ const roundToggleContainer = this._roundToggleContainerRef.current!;
+ const timelineContainer = this._timelineContainer.current!;
+ if (BoolCast(this.props.Document.isATOn)) {
+ //turning on playmode...
+ roundToggle.style.transform = "translate(0px, 0px)";
+ roundToggle.style.animationName = "turnoff";
+ roundToggleContainer.style.animationName = "turnoff";
+ roundToggleContainer.style.backgroundColor = "white";
+ timelineContainer.style.top = `${-this._containerHeight}px`;
+ this.props.Document.isATOn = false;
+ this._isAuthoring = false;
+ this.toPlay();
+ } else {
+ //turning on authoring mode...
+ roundToggle.style.transform = "translate(20px, 0px)";
+ roundToggle.style.animationName = "turnon";
+ roundToggleContainer.style.animationName = "turnon";
+ roundToggleContainer.style.backgroundColor = "#9acedf";
+ timelineContainer.style.top = "0px";
+ this.props.Document.isATOn = true;
+ this._isAuthoring = true;
+ this.toAuthoring();
+ }
+ }
+
+
+ @action.bound
+ changeLengths() {
+ if (this._infoContainer.current) {
+ this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts
+ }
+ }
+
+ // @computed
+ getCurrentTime = () => {
+ let current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ if (current > this._time) {
+ current = this._time;
+ }
+ return this.toReadTime(current);
+ }
+
+ @observable private mapOfTracks: (Track | null)[] = [];
+
+ @action
+ findLongestTime = () => {
+ let longestTime: number = 0;
+ this.mapOfTracks.forEach(track => {
+ if (track) {
+ const lastTime = track.getLastRegionTime();
+ if (this.children.length !== 0) {
+ if (longestTime <= lastTime) {
+ longestTime = lastTime;
+ }
+ }
+ } else {
+ //TODO: remove undefineds and duplicates
+ }
+ });
+ // console.log(longestTime);
+ return longestTime;
+ }
+
+ @action
+ toAuthoring = () => {
+ let longestTime = this.findLongestTime();
+ if (longestTime === 0) longestTime = 1;
+ const adjustedTime = Math.ceil(longestTime / 100000) * 100000;
+ // console.log(adjustedTime);
+ this._totalLength = KeyframeFunc.convertPixelTime(adjustedTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._time = adjustedTime;
+ }
+
+ @action
+ toPlay = () => {
+ const longestTime = this.findLongestTime();
+ this._time = longestTime;
+ this._totalLength = KeyframeFunc.convertPixelTime(this._time, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ }
+
+ /**
+ * if you have any question here, just shoot me an email or text.
+ * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
+ */
+ render() {
+ setTimeout(() => {
+ this.changeLengths();
+ // this.toPlay();
+ // this._time = longestTime;
+ }, 0);
+
+ const longestTime = this.findLongestTime();
+ trace();
+ // change visible and total width
+ return (
+ <div style={{ visibility: this._timelineVisible ? "visible" : "hidden" }}>
+ <div key="timeline_wrapper" style={{ visibility: BoolCast(this.props.Document.isATOn && this._timelineVisible) ? "visible" : "hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)" }}>
+ <div key="timeline_container" className="timeline-container" ref={this._timelineContainer} style={{ height: `${this._containerHeight}px`, top: `0px` }}>
+ <div key="timeline_info" className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
+ {this.drawTicks()}
+ <div key="timeline_scrubber" className="scrubber" style={{ transform: `translate(${this._currentBarX}px)` }}>
+ <div key="timeline_scrubberhead" className="scrubberhead" onPointerDown={this.onScrubberDown} ></div>
+ </div>
+ <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{ width: `${this._totalLength}px` }}>
+ {DocListCast(this.children).map(doc =>
+ <Track ref={ref => this.mapOfTracks.push(ref)} node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} collection={this.props.Document} timelineVisible={this._timelineVisible} />
+ )}
+ </div>
+ </div>
+ <div className="currentTime">Current: {this.getCurrentTime()}</div>
+ <div key="timeline_title" className="title-container" ref={this._titleContainer}>
+ {DocListCast(this.children).map(doc => <div style={{ height: `${(this._titleHeight)}px` }} className="datapane" onPointerOver={() => { Doc.BrushDoc(doc); }} onPointerOut={() => { Doc.UnBrushDoc(doc); }}><p>{doc.title}</p></div>)}
+ </div>
+ <div key="timeline_resize" onPointerDown={this.onResizeDown}>
+ <FontAwesomeIcon className="resize" icon={faGripLines} />
+ </div>
+ </div>
+ </div>
+ {this.timelineToolBox(1, longestTime)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss
new file mode 100644
index 000000000..7ee0a43d5
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.scss
@@ -0,0 +1,94 @@
+@import "./../globalCssVariables.scss";
+
+
+.timeline-menu-container{
+ position: absolute;
+ display: flex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+ background: whitesmoke;
+ z-index: 10000;
+ width: 200px;
+ padding-bottom: 10px;
+ border-radius: 15px;
+
+ border: solid #BBBBBBBB 1px;
+
+
+
+ .timeline-menu-input{
+ font: $sans-serif;
+ font-size: 13px;
+ width:100%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-left: 10px;
+ background-color: transparent;
+ border-width: 0px;
+ transition: border-width 500ms;
+ }
+
+ .timeline-menu-input:hover{
+ border-width: 2px;
+ }
+
+
+
+
+ .timeline-menu-header{
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ text-transform: uppercase;
+ background: $dark-color;
+ letter-spacing: 2px;
+
+ .timeline-menu-header-desc{
+ font:$sans-serif;
+ font-size: 13px;
+ text-align: center;
+ color: whitesmoke;
+ }
+ }
+
+
+ .timeline-menu-item {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: whitesmoke;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-style: none;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ color: grey;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-right: 20px;
+ padding-left: 10px;
+ }
+
+ .timeline-menu-item:hover {
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ border-top-style: solid;
+ background: $darker-alt-accent;
+ }
+
+ .timeline-menu-desc {
+ padding-left: 10px;
+ font:$sans-serif;
+ font-size: 13px;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
new file mode 100644
index 000000000..59c25596e
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -0,0 +1,78 @@
+import * as React from "react";
+import {observable, action, runInAction} from "mobx";
+import {observer} from "mobx-react";
+import "./TimelineMenu.scss";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons";
+import { Utils } from "../../../Utils";
+
+
+@observer
+export class TimelineMenu extends React.Component {
+ public static Instance:TimelineMenu;
+
+ @observable private _opacity = 0;
+ @observable private _x = 0;
+ @observable private _y = 0;
+ @observable private _currentMenu:JSX.Element[] = [];
+
+ constructor (props:Readonly<{}>){
+ super(props);
+ TimelineMenu.Instance = this;
+ }
+
+ @action
+ openMenu = (x?:number, y?:number) => {
+ this._opacity = 1;
+ x ? this._x = x : this._x = 0;
+ y ? this._y = y : this._y = 0;
+ }
+
+ @action
+ closeMenu = () => {
+ this._opacity = 0;
+ this._currentMenu = [];
+ this._x = -1000000;
+ this._y = -1000000;
+ }
+
+ @action
+ addItem = (type: "input" | "button", title: string, event: (e:any, ...args:any[]) => void) => {
+ if (type === "input"){
+ let inputRef = React.createRef<HTMLInputElement>();
+ let text = "";
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => {
+ e.stopPropagation();
+ text = e.target.value;
+ }} onKeyDown={(e) => {
+ if (e.keyCode === 13) {
+ event(text);
+ this.closeMenu();
+ e.stopPropagation();
+ }
+ }}/></div>);
+ } else if (type === "button") {
+ let buttonRef = React.createRef<HTMLDivElement>();
+ this._currentMenu.push( <div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ event(e);
+ this.closeMenu();
+ }}>{title}</p></div>);
+ }
+ }
+
+ @action
+ addMenu = (title:string) => {
+ this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
+ }
+
+ render() {
+ return (
+ <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} >
+ {this._currentMenu}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss
new file mode 100644
index 000000000..283163ea7
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.scss
@@ -0,0 +1,107 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timelineOverview-bounding {
+ width: 100%;
+ margin-right: 10px;
+}
+
+.timeline-flex {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+}
+
+.timeline-overview-container {
+ // padding: 0px;
+ margin-right: 5px;
+ // margin: 0px;
+ margin-left: 5px;
+ // width: 300px;
+ width: 100%;
+ height: 25px;
+ background: white;
+ position: relative;
+ border: 2px solid $timelineDark;
+ // width: 100%;
+
+ .timeline-overview-visible {
+ position: absolute;
+ height: 21px;
+ background: $timelineColor;
+ display: inline-block;
+ margin: 0px;
+ padding: 0px;
+ // top: 1px;
+ }
+
+ .timeline-overview-scrubber-container {
+ margin: 0px;
+ padding: 0px;
+ position: absolute;
+ height: 100%;
+ width: 2px;
+ top: 0px;
+ left: 0px;
+ z-index: 1001;
+ background-color: black;
+ display: inline-block;
+
+ .timeline-overview-scrubber-head {
+ padding: 0px;
+ margin: 0px;
+ position: absolute;
+ height: 10px;
+ width: 10px;
+ background-color: white;
+ border-radius: 50%;
+ border: 2px solid black;
+ left: -4px;
+ // top: -30px;
+ top: -10px;
+ }
+ }
+}
+
+
+
+.timeline-play-bar {
+ position: relative;
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: 4px;
+ background-color: $timelineColor;
+ border-radius: 20px;
+ cursor: pointer;
+
+ .timeline-play-head {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: white;
+ border: 3px solid $timelineColor;
+ left: 0px;
+ top: -8px;
+ cursor: pointer;
+ }
+}
+
+.timeline-play-tail {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ height: 4px;
+ width: 0px;
+ z-index: 1000;
+ background-color: $timelineDark;
+ border-radius: 20px;
+ margin-top: -4px;
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
new file mode 100644
index 000000000..31e248823
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -0,0 +1,181 @@
+import * as React from "react";
+import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import "./TimelineOverview.scss";
+import * as $ from 'jquery';
+import { Timeline } from "./Timeline";
+import { Keyframe, KeyframeFunc } from "./Keyframe";
+
+
+interface TimelineOverviewProps {
+ totalLength: number;
+ visibleLength: number;
+ visibleStart: number;
+ currentBarX: number;
+ isAuthoring: boolean;
+ parent: Timeline;
+ changeCurrentBarX: (pixel: number) => void;
+ movePanX: (pixel: number) => any;
+ time: number;
+ tickSpacing: number;
+ tickIncrement: number;
+}
+
+
+@observer
+export class TimelineOverview extends React.Component<TimelineOverviewProps>{
+ @observable private _visibleRef = React.createRef<HTMLDivElement>();
+ @observable private _scrubberRef = React.createRef<HTMLDivElement>();
+ @observable private authoringContainer = React.createRef<HTMLDivElement>();
+ @observable private playbackContainer = React.createRef<HTMLDivElement>();
+ @observable private overviewBarWidth: number = 0;
+ @observable private playbarWidth: number = 0;
+ @observable private activeOverviewWidth: number = 0;
+ @observable private _authoringReaction?: IReactionDisposer;
+ @observable private visibleTime: number = 0;
+ @observable private currentX: number = 0;
+ @observable private visibleStart: number = 0;
+ private readonly DEFAULT_HEIGHT = 50;
+ private readonly DEFAULT_WIDTH = 300;
+
+ componentDidMount = () => {
+ this.setOverviewWidth();
+
+ this._authoringReaction = reaction(
+ () => this.props.parent._isAuthoring,
+ () => {
+ if (!this.props.parent._isAuthoring) {
+ runInAction(() => {
+ this.setOverviewWidth();
+ });
+ }
+ },
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._authoringReaction && this._authoringReaction();
+ }
+
+ @action
+ setOverviewWidth() {
+ const width1 = this.authoringContainer.current?.clientWidth;
+ const width2 = this.playbackContainer.current?.clientWidth;
+ if (width1 && width1 !== 0) this.overviewBarWidth = width1;
+ if (width2 && width2 !== 0) this.playbarWidth = width2;
+
+ if (this.props.isAuthoring) {
+ this.activeOverviewWidth = this.overviewBarWidth;
+ }
+ else {
+ this.activeOverviewWidth = this.playbarWidth;
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPanX);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPanX = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX;
+ this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength);
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberRef = this._scrubberRef.current!;
+ const left = scrubberRef.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left);
+ this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX);
+ }
+
+ @action
+ onScrubberUp = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ getTimes() {
+ const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ this.visibleTime = vis;
+ this.currentX = x;
+ this.visibleStart = start;
+ }
+
+ render() {
+ this.setOverviewWidth();
+ this.getTimes();
+
+ const percentVisible = this.visibleTime / this.props.time;
+ const visibleBarWidth = percentVisible * this.activeOverviewWidth;
+
+ const percentScrubberStart = this.currentX / this.props.time;
+ let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth;
+ if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth;
+
+ const percentBarStart = this.visibleStart / this.props.time;
+ const barStart = percentBarStart * this.activeOverviewWidth;
+
+ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth;
+ if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth;
+
+ const timeline = this.props.isAuthoring ? [
+
+ <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}>
+ <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>,
+ <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}>
+ <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div>
+ </div>
+ </div>
+ ] : [
+ <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}>
+ <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div>
+ </div>,
+ <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div>
+ ];
+ return (
+ <div className="timeline-flex">
+ <div className="timelineOverview-bounding">
+ {timeline}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+
diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss
new file mode 100644
index 000000000..aec587a79
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.scss
@@ -0,0 +1,15 @@
+@import "./../globalCssVariables.scss";
+
+.track-container {
+
+ .track {
+ .inner {
+ top: 0px;
+ width: calc(100%);
+ background-color: $light-color;
+ border: 1px solid $dark-color;
+ position: relative;
+ z-index: 100;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
new file mode 100644
index 000000000..79eb60fae
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -0,0 +1,380 @@
+import { action, computed, intercept, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { Copy } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { Transform } from "../../util/Transform";
+import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe";
+import "./Track.scss";
+
+interface IProps {
+ node: Doc;
+ currentBarX: number;
+ transform: Transform;
+ collection: Doc;
+ time: number;
+ tickIncrement: number;
+ tickSpacing: number;
+ timelineVisible: boolean;
+ changeCurrentBarX: (x: number) => void;
+}
+
+@observer
+export class Track extends React.Component<IProps> {
+ @observable private _inner = React.createRef<HTMLDivElement>();
+ @observable private _currentBarXReaction: any;
+ @observable private _timelineVisibleReaction: any;
+ @observable private _autoKfReaction: any;
+ @observable private _newKeyframe: boolean = false;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private _trackHeight = 0;
+ private primitiveWhitelist = [
+ "x",
+ "y",
+ "_width",
+ "_height",
+ "opacity",
+ ];
+ private objectWhitelist = [
+ "data"
+ ];
+
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); }
+
+ async componentDidMount() {
+ const regions = await DocListCastAsync(this.props.node.regions);
+ if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
+ //these two lines are exactly same from timeline.tsx
+ const relativeHeight = window.innerHeight / 20;
+ this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //for responsiveness
+ this._timelineVisibleReaction = this.timelineVisibleReaction();
+ this._currentBarXReaction = this.currentBarXReaction();
+ if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
+ this.props.node.hidden = false;
+ this.props.node.opacity = 1;
+ // this.autoCreateKeyframe();
+ }
+
+ /**
+ * mainly for disposing reactions
+ */
+ componentWillUnmount() {
+ this._currentBarXReaction?.();
+ this._timelineVisibleReaction?.();
+ this._autoKfReaction?.();
+ }
+ ////////////////////////////////
+
+
+ getLastRegionTime = () => {
+ let lastTime: number = 0;
+ let lastRegion: Opt<Doc>;
+ this.regions.forEach(region => {
+ const time = NumCast(region.position);
+ if (lastTime <= time) {
+ lastTime = time;
+ lastRegion = region;
+ }
+ });
+ return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0;
+ }
+
+ /**
+ * keyframe save logic. Needs to be changed so it's more efficient
+ *
+ */
+ @action
+ saveKeyframe = async () => {
+ let keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List<Doc>;
+ let kfIndex = keyframes.indexOf(this.saveStateKf!);
+ let kf = keyframes[kfIndex] as Doc; //index in the keyframe
+ if (this._newKeyframe) {
+ DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => {
+ this.copyDocDataToKeyFrame(kf);
+ kf.opacity = (index === 0 || index === 3) ? 0.1 : 1;
+ });
+ this._newKeyframe = false;
+ }
+ if (!kf) return;
+ if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades
+ this.copyDocDataToKeyFrame(kf);
+ let leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists
+ let rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists
+ if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades
+ let edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ leftkf && this.copyDocDataToKeyFrame(leftkf);
+ edge && (edge!.opacity = 0.1);
+ leftkf && (leftkf!.opacity = 1);
+ }
+ if (rightkf?.type === KeyframeFunc.KeyframeType.fade) {
+ let edge = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ rightkf && this.copyDocDataToKeyFrame(rightkf);
+ edge && (edge.opacity = 0.1);
+ rightkf && (rightkf.opacity = 1);
+ }
+ }
+ keyframes[kfIndex] = kf;
+ this.saveStateKf = undefined;
+ this.saveStateRegion = undefined;
+ }
+
+
+ /**
+ * autocreates keyframe
+ */
+ @action
+ autoCreateKeyframe = () => {
+ const objects = this.objectWhitelist.map(key => this.props.node[key]);
+ intercept(this.props.node, change => {
+ console.log(change);
+ return change;
+ });
+ return reaction(() => {
+ return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ }, (changed, reaction) => {
+ //check for region
+ const region = this.findRegion(this.time);
+ if (region !== undefined) { //if region at scrub time exist
+ let r = region as RegionData; //for some region is returning undefined... which is not the case
+ if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot
+ this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default);
+ }
+ }
+ }, { fireImmediately: false });
+ }
+
+
+
+ // @observable private _storedState:(Doc | undefined) = undefined;
+ // /**
+ // * reverting back to previous state before editing on AT
+ // */
+ // @action
+ // revertState = () => {
+ // if (this._storedState) this.applyKeys(this._storedState);
+ // }
+
+
+ /**
+ * Reaction when scrubber bar changes
+ * made into function so it's easier to dispose later
+ */
+ @action
+ currentBarXReaction = () => {
+ return reaction(() => this.props.currentBarX, () => {
+ const regiondata = this.findRegion(this.time);
+ if (regiondata) {
+ this.props.node.hidden = false;
+ // if (!this._autoKfReaction) {
+ // // console.log("creating another reaction");
+ // // this._autoKfReaction = this.autoCreateKeyframe();
+ // }
+ this.timeChange();
+ } else {
+ this.props.node.hidden = true;
+ this.props.node.opacity = 0;
+ //if (this._autoKfReaction) this._autoKfReaction();
+ }
+ });
+ }
+
+ /**
+ * when timeline is visible, reaction is ran so states are reverted
+ */
+ @action
+ timelineVisibleReaction = () => {
+ return reaction(() => {
+ return this.props.timelineVisible;
+ }, isVisible => {
+ if (isVisible) {
+ this.regions.filter(region => !region.hasData).forEach(region => {
+ for (let i = 0; i < 4; i++) {
+ this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
+ if (i === 0 || i === 3) { //manually inputing fades
+ DocListCast(region.keyframes)[i].opacity = 0.1;
+ }
+ }
+ });
+ } else {
+ console.log("reverting state");
+ //this.revertState();
+ }
+ });
+ }
+
+ @observable private saveStateKf: (Doc | undefined) = undefined;
+ @observable private saveStateRegion: (Doc | undefined) = undefined;
+
+ /**w
+ * when scrubber position changes. Need to edit the logic
+ */
+ @action
+ timeChange = async () => {
+ if (this.saveStateKf !== undefined) {
+ await this.saveKeyframe();
+ } else if (this._newKeyframe) {
+ await this.saveKeyframe();
+ }
+ let regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on
+ if (regiondata) {
+ let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
+ let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
+ let currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
+ if (currentkf) {
+ console.log("is current");
+ await this.applyKeys(currentkf);
+ this.saveStateKf = currentkf;
+ this.saveStateRegion = regiondata;
+ } else if (leftkf && rightkf) {
+ await this.interpolate(leftkf, rightkf);
+ }
+ }
+ }
+
+ /**
+ * applying changes (when saving the keyframe)
+ * need to change the logic here
+ */
+ @action
+ private applyKeys = async (kf: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (!kf[key]) {
+ this.props.node[key] = undefined;
+ } else {
+ let stored = kf[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+
+ /**
+ * calculating current keyframe, if the scrubber is right on the keyframe
+ */
+ @action
+ calcCurrent = (region: Doc) => {
+ let currentkf: (Doc | undefined) = undefined;
+ let keyframes = DocListCast(region.keyframes!);
+ keyframes.forEach((kf) => {
+ if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf;
+ });
+ return currentkf;
+ }
+
+
+ /**
+ * basic linear interpolation function
+ */
+ @action
+ interpolate = async (left: Doc, right: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (left[key] && right[key] && typeof (left[key]) === "number" && typeof (right[key]) === "number") { //if it is number, interpolate
+ let dif = NumCast(right[key]) - NumCast(left[key]);
+ let deltaLeft = this.time - NumCast(left.time);
+ let ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
+ this.props.node[key] = NumCast(left[key]) + (dif * ratio);
+ } else { // case data
+ let stored = left[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+ /**
+ * finds region that corresponds to specific time (is there a region at this time?)
+ * linear O(n) (maybe possible to optimize this with other Data structures?)
+ */
+ findRegion = (time: number) => {
+ return this.regions?.find(rd => (time >= NumCast(rd.position) && time <= (NumCast(rd.position) + NumCast(rd.duration))));
+ }
+
+
+ /**
+ * double click on track. Signalling keyframe creation.
+ */
+ @action
+ onInnerDoubleClick = (e: React.MouseEvent) => {
+ let inner = this._inner.current!;
+ let offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale);
+ this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+
+ /**
+ * creates a region (KEYFRAME.TSX stuff).
+ */
+ @action
+ createRegion = (time: number) => {
+ if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions)
+ let regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data
+
+ regiondata.position = time; //set position
+ let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);
+
+ if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region
+ regiondata.duration = rightRegion.position - regiondata.position;
+ }
+ if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
+ Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata);
+ this._newKeyframe = true;
+ this.saveStateRegion = regiondata;
+ return regiondata;
+ }
+ }
+ }
+
+ @action
+ makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time
+ const trackKeyFrames = DocListCast(regiondata.keyframes)!;
+ const existingkf = trackKeyFrames.find(TK => TK.time === time);
+ if (existingkf) return existingkf;
+ //else creates a new doc.
+ const newKeyFrame: Doc = new Doc();
+ newKeyFrame.time = time;
+ newKeyFrame.type = type;
+ this.copyDocDataToKeyFrame(newKeyFrame);
+ //assuming there are already keyframes (for keeping keyframes in order, sorted by time)
+ if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame);
+ trackKeyFrames.map(kf => NumCast(kf.time)).forEach((kfTime, index) => {
+ if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
+ regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
+ }
+ });
+ return newKeyFrame;
+ }
+
+ @action
+ copyDocDataToKeyFrame = (doc: Doc) => {
+ this.primitiveWhitelist.map(key => {
+ const originalVal = this.props.node[key];
+ doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
+ });
+ }
+
+ /**
+ * UI sstuff here. Not really much to change
+ */
+ render() {
+ return (
+ <div className="track-container">
+ <div className="track">
+ <div className="inner" ref={this._inner} style={{ height: `${this._trackHeight}px` }}
+ onDoubleClick={this.onInnerDoubleClick}
+ onPointerOver={() => Doc.BrushDoc(this.props.node)}
+ onPointerOut={() => Doc.UnBrushDoc(this.props.node)} >
+ {this.regions?.map((region, i) => {
+ return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
+ })}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index 7b7828d7d..971224482 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -1,4 +1,4 @@
-import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react";
+import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react";
import { observer } from "mobx-react";
import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
@@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => {
};
@observer
-class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) {
+class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
private _cancelAddrReq = new Map<string, boolean>();
private _cancelLocReq = new Map<string, boolean>();
@@ -221,7 +221,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
zoom={center.zoom || 10}
initialCenter={center}
center={center}
- onIdle={(_props?: MapProps, map?: google.maps.Map) => {
+ onIdle={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead)
map?.setZoom(center?.zoom || 10);
@@ -233,7 +233,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
}))();
}
}}
- onDragend={(_props?: MapProps, map?: google.maps.Map) => {
+ onDragend={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead)
map?.setCenter({ lat: center?.lat!, lng: center?.lng! });
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index fb7535d9f..1bfd408f8 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,30 +1,30 @@
import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { basename } from 'path';
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
+import { GestureUtils } from "../../../pen-gestures/GestureUtils";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Upload } from "../../../server/SharedMediaTypes";
import { Utils } from "../../../Utils";
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { DocServer } from "../../DocServer";
-import { DocumentType } from "../../documents/DocumentTypes";
import { Docs, DocumentOptions } from "../../documents/Documents";
-import { DragManager, dropActionType } from "../../util/DragManager";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Networking } from "../../Network";
+import { DragManager } from "../../util/DragManager";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
import { CollectionView } from "./CollectionView";
import React = require("react");
-import { basename } from 'path';
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { ImageUtils } from "../../util/Import & Export/ImageUtils";
-import { Networking } from "../../Network";
-import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { InteractionUtils } from "../../util/InteractionUtils";
-import { Upload } from "../../../server/SharedMediaTypes";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 5203eb55f..e4581eb46 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -33,6 +33,17 @@
outline-color: black;
}
+ .collectionViewBaseChrome-button{
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: purple;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 10px;
+ }
.collectionViewBaseChrome-cmdPicker {
margin-left: 3px;
margin-right: 0px;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 92b27a0c6..d85233041 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -44,6 +44,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import { CollectionViewType } from "../CollectionView";
+import { Timeline } from "../../animationtimeline/Timeline";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -94,6 +95,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive
@observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
@observable _clusterSets: (Doc[])[] = [];
+ @observable _timelineRef = React.createRef<Timeline>();
@computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }
@computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@@ -1091,6 +1093,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const options = ContextMenu.Instance.findByDescription("Options...");
const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
+ this._timelineRef.current!.timelineContextMenu(e);
optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
@@ -1126,8 +1129,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
input.click();
}
});
-
- ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
+ ContextMenu.Instance.addItem({ description: "Options ...", subitems: optionItems, icon: "eye" });
+ this._timelineRef.current!.timelineContextMenu(e);
}
private childViews = () => {
@@ -1168,6 +1171,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
{this.children}
</CollectionFreeFormViewPannableContents>
+ <Timeline ref={this._timelineRef} {...this.props} />
</MarqueeView>;
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a3790d38b..0b9edbcd3 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -51,6 +51,11 @@ export interface FieldViewProps {
ContentScaling: () => number;
ChromeHeight?: () => number;
childLayoutTemplate?: () => Opt<Doc>;
+ // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
+ height?: number;
+ width?: number;
+ background?: string;
+ color?: string;
}
@observer
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 248b4f467..c4e387e5a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1224,10 +1224,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
- background: StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
+ height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
+ background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
- color: StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
+ color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
pointerEvents: interactive ? "none" : undefined,
fontSize: Cast(this.layoutDoc._fontSize, "number", null),
fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 337274774..153af933a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -254,6 +254,7 @@ export namespace Doc {
// return Cast(field, ctor);
// });
// }
+
export function RunCachedUpdate(doc: Doc, field: string) {
const update = doc[CachedUpdates][field];
if (update) {