From a8e80cc8d1c90b11cbc68186a5a716338c4f66c2 Mon Sep 17 00:00:00 2001 From: woozygoozy Date: Sun, 12 Nov 2023 16:53:51 -0500 Subject: [PATCH] feat: working on improving lottie parser --- .vscode/launch.json | 12 ++ package-lock.json | 209 +++++++++++++++++++++ package.json | 3 + src/LottieInfoParser.js | 212 +++++++++++++++------ src/LottieTreeParser.css | 107 +++++++++++ src/LottieTreeParser.js | 158 ++++++++++++++++ src/LottieTreeParserN.js | 244 +++++++++++++++++++++++++ src/PlayerUI.js | 15 +- src/lottieSchema/lottieJSONschema.json | 3 +- 9 files changed, 909 insertions(+), 54 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/LottieTreeParser.css create mode 100644 src/LottieTreeParser.js create mode 100644 src/LottieTreeParserN.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9d3da8e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "attach", + "name": "Attach to Tauri Dev", + "pid": "${command:pickProcess}", // This will allow you to pick the process to attach to + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/package-lock.json b/package-lock.json index e5e1141..f4179b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,16 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "clone": "^2.1.2", "font-awesome": "^4.7.0", "lottie-web": "^5.12.2", "react": "^18.2.0", "react-best-gradient-color-picker": "^2.2.24", "react-color": "^2.19.3", + "react-d3-tree": "^3.6.1", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "react-tree-graph": "^8.0.1", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -2014,6 +2017,23 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@bkrem/react-transition-group": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bkrem/react-transition-group/-/react-transition-group-1.3.3.tgz", + "integrity": "sha512-nUZaumHu/MMolELv+MhEEQzQtKsnfpbKBHtam/NK53tGICwU19tuffEXW8BLhm9HhQfN1H3+C0bsJv8Z7vzwEA==", + "dependencies": { + "chain-function": "^1.0.0", + "dom-helpers": "^3.3.1", + "loose-envify": "^1.3.1", + "prop-types": "^15.5.6", + "react-lifecycles-compat": "^3.0.4", + "warning": "^3.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4877,6 +4897,11 @@ "@types/node": "*" } }, + "node_modules/@types/d3-hierarchy": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz", + "integrity": "sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==" + }, "node_modules/@types/eslint": { "version": "8.44.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", @@ -6875,6 +6900,11 @@ "node": ">=4" } }, + "node_modules/chain-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", + "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==" + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -7018,6 +7048,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7761,6 +7799,117 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", + "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==" + }, + "node_modules/d3-hierarchy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", + "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8053,6 +8202,14 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -19641,6 +19798,31 @@ "react": "*" } }, + "node_modules/react-d3-tree": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-d3-tree/-/react-d3-tree-3.6.1.tgz", + "integrity": "sha512-tl7P3Wg5q/ep6c7z6wPgf3vpqaxZ1yQi/rpvIePj3vt7XGwdP3Vdr9GBIZXw7K2CrMknoyoDIbPSNaZtZZ6MIg==", + "dependencies": { + "@bkrem/react-transition-group": "^1.3.3", + "@types/d3-hierarchy": "^1.1.8", + "clone": "^2.1.1", + "d3-hierarchy": "^1.1.9", + "d3-selection": "^3.0.0", + "d3-shape": "^1.3.7", + "d3-zoom": "^3.0.0", + "dequal": "^2.0.2", + "uuid": "^8.3.1" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x", + "react-dom": "16.x || 17.x || 18.x" + } + }, + "node_modules/react-d3-tree/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -19780,6 +19962,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -19860,6 +20047,20 @@ } } }, + "node_modules/react-tree-graph": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-tree-graph/-/react-tree-graph-8.0.1.tgz", + "integrity": "sha512-+QoKfuDvtfyXbtvMfTzPZyo/Olot58yWzgEjRGAbaf8LZDNOvFg1EV2yHO8wAJlQ08Q/fTDiwXVS0gnwfSCs5A==", + "dependencies": { + "@babel/runtime": "^7.19.4", + "d3-ease": "^2.0.0", + "d3-hierarchy": "^2.0.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3 || ^16 || ^17 || ^18" + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -22780,6 +22981,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 9dd8ea9..abe3b0d 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,16 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "clone": "^2.1.2", "font-awesome": "^4.7.0", "lottie-web": "^5.12.2", "react": "^18.2.0", "react-best-gradient-color-picker": "^2.2.24", "react-color": "^2.19.3", + "react-d3-tree": "^3.6.1", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "react-tree-graph": "^8.0.1", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/LottieInfoParser.js b/src/LottieInfoParser.js index 2cccf0d..5e184e9 100644 --- a/src/LottieInfoParser.js +++ b/src/LottieInfoParser.js @@ -1,69 +1,177 @@ import React, { useEffect } from 'react'; -const isLoggingEnabled = true; // Set to false to disable logging -//ERROR LOGGING -function log(...messages) { - if (isLoggingEnabled) { - console.log(...messages); +// Map numerical layer types to descriptive names +const layerTypeMapping = { + 0: 'Precomp', + 1: 'Solid', + 2: 'Image', + 3: 'Null', + 4: 'Shape', + 5: 'Text', + 6: 'Audio', + 7: 'Video placeholder', + 8: 'Image sequence', + 9: 'Video', + 10: 'Image placeholder', + 11: 'Guide', + 12: 'Adjustment', + 13: 'Camera', + 14: 'Light', + 15: 'Data', + // Add additional layer types as needed +}; + +// Map property keys to human-readable names +const propertyKeyMapping = { + s: 'Start Value', + e: 'End Value', + i: 'In Tangent', + o: 'Out Tangent', + t: 'Time', + a: 'Anchor Point', + ix: 'Property Index', + x: 'Expression', + l: 'Length', + d: "Dashes", + k: 'Keyframes', + ks: 'Transform', + p: 'Position', + r: 'Rotation', + s: 'Scale', + o: 'Opacity', + c: 'Color', + ty: 'Type', + sk: 'Skew', + sa: 'Skew Axis', + rc: "Rectangle", + el: "Ellipse", + sr: "Polygon / Star", + sh: "Path", + fl: "Fill", + st: "Stroke", + gf: "Gradient fill", + gs: "Gradient stroke", + gr: "Group", + tr: "Transform", + st: "Stroke", + // Add additional property mappings as needed +}; + +// Improved property parsing with context-specific handling of 'a' and 'k' +const parseProperties = (properties, parentKey = '') => { + const parsed = {}; + for (const key in properties) { + let readableKey = propertyKeyMapping[key] || key; + + // Context-specific renaming + if (parentKey === 'ks') { + if (key === 'p') readableKey = 'Position'; + else if (key === 'r') readableKey = 'Rotation'; + else if (key === 's') readableKey = 'Scale'; + else if (key === 'a') readableKey = 'Anchor Point'; + else if (key === 'o') readableKey = 'Opacity'; + } else { + if (key === 'a') { + readableKey = 'Animated'; + parsed[readableKey] = properties[key] === 1; + continue; + } + } + + // Handle 'k' property based on the value of 'a' + if (key === 'k') { + if (properties.a === 1) { + // 'k' contains arrays of keyframes + parsed[readableKey] = properties[key]; + } else { + // 'k' contains a static value + parsed[readableKey] = Array.isArray(properties[key]) ? properties[key] : [properties[key]]; + } + } else { + parsed[readableKey] = properties[key]; + } } -} + return parsed; +}; + +// ... rest of the shape and layer parsing functions remain the same + +// Updated shape parsing to properly handle properties +const parseShapeProperties = (shapes) => { + return shapes.map(shape => { + const parsedShape = { + name: shape.nm, + type: shape.ty, + properties: {} + }; + + // Parse each property in the shape + for (const propKey in shape) { + if (propKey === 'it') { + parsedShape.properties = shape[propKey].map(item => parseProperties(item, propKey)); + } else { + parsedShape.properties[propKey] = shape[propKey]; + } + } + + return parsedShape; + }); +}; + +// Update layer parsing to handle shape properties +const parseLayers = (layers, level = 0) => { + return layers.map(layer => { + const parsedLayer = { + name: layer.nm, + type: layerTypeMapping[layer.ty] || `Type ${layer.ty}`, + inPoint: layer.ip, + outPoint: layer.op, + level: level, + properties: parseProperties(layer.ks, 'ks'), + // sublayers: layer.layers ? parseLayers(layer.layers, level + 1) : [] + }; + + if (layer.ty === 4) { // Shape layers + parsedLayer.shapes = parseShapeProperties(layer.shapes); + } + + return parsedLayer; + }); +}; const LottieInfoParser = ({ animationData, fileSize, onParsed }) => { useEffect(() => { if (animationData) { - // Parse the animation data and calculate necessary details - const framerate = animationData.fr || 'Not specified'; + // Extract relevant data from animationData + const layersHierarchy = parseLayers(animationData.layers); const durationFrames = animationData.op || 0; const durationSeconds = (animationData.op / animationData.fr) || 0; const markersExist = animationData.markers && animationData.markers.length > 0 ? 'YES' : 'NO'; const nativeDimensions = `${animationData.w} x ${animationData.h}`; - // Extract markers if present - const markers = animationData.markers ? animationData.markers.map(marker => ({ - name: marker.cm, // Use 'cm' for marker name - startFrame: marker.tm, // Use 'tm' for start frame/time - endFrame: marker.tm + marker.dr // Calculate end frame using 'tm' and 'dr' - })) : []; - log(markers); - - // Recursive function to parse layer information - const parseLayers = (layers, level = 0) => { - return layers.map(layer => { - const layerInfo = { - name: layer.nm, - type: layer.ty, // Determine layer type - inPoint: layer.ip, - outPoint: layer.op, - animatedProperties: layer.ks, // Example: keyframes - level: level - }; - - if (layer.ty === 'precomp' && layer.layers) { - layerInfo.sublayers = parseLayers(layer.layers, level + 1); - } - - return layerInfo; - }); -}; + // Call onParsed with parsed details + if (onParsed) { + onParsed({ + durationFrames, + durationSeconds, + framerate: animationData.fr, + layersHierarchy, + markersExist, + nativeDimensions + }); + } + } + }, [animationData, onParsed]); + + if (!animationData) { + return ( + +
Please load a Lottie...
+
+ ); + } -const layersHierarchy = parseLayers(animationData.layers); - -// If an onParsed callback is provided, call it with the parsed details -if (onParsed) { - onParsed({ - framerate, - durationFrames, - durationSeconds, - markersExist, - nativeDimensions, - markers, - layersHierarchy - }); -} - } - }, [animationData, onParsed]); - // Return JSX to display the parsed details return (
Animation Size:
diff --git a/src/LottieTreeParser.css b/src/LottieTreeParser.css new file mode 100644 index 0000000..3d2a96a --- /dev/null +++ b/src/LottieTreeParser.css @@ -0,0 +1,107 @@ +/* LottieTreeParser.css */ + +.modalBackdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + /* Semi-transparent backdrop */ + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + /* Ensure it's above other content */ +} + +.modalContent { + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + width: 80%; + /* Adjust as needed */ + max-height: 90%; + /* Adjusted for better modal layout */ + overflow: auto; + /* Scroll for both horizontal and vertical overflow */ + display: flex; + flex-direction: column; + align-items: center; +} + +.treeContainer { + width: 100%; + /* Full width of the modal content */ + height: 80vh; + /* Adjust based on your needs */ + overflow: scroll; + /* Allows scrolling within the tree container if needed */ + padding: 10px; + box-sizing: border-box; +} +/* Style adjustments for the tree nodes */ +.node { + cursor: pointer; + fill: #007bff; + /* Node color */ + stroke: #fff; + /* Node border color */ +} + +.node text { + font-size: 14px; + /* Increased text size */ + fill: #333; + /* A dark color for the text */ +} + +.foreignObject div { + color: #333; + /* Text color inside the foreignObject */ + font-size: 12px; + /* Text size */ + overflow: hidden; + /* Adjust as necessary */ + width: 100%; + /* Ensure the div uses all the allocated space */ + height: auto; + /* Let the height adjust based on content */ + background: #fff; + /* Background to contrast the text */ +} + +/* Ensure that SVG text elements are visible */ +.node g text { + visibility: visible; +} + +.node circle { + fill: #007bff; + /* Circle color for nodes */ + stroke: #fff; + /* Circle border color */ +} + +.link { + fill: none; + stroke: #ccc; + /* Color of the links/lines between nodes */ + stroke-width: 2px; + /* Width of the links/lines */ +} + +/* Additional styling for node focus and interaction */ +.node:hover { + fill: #0056b3; + /* Change color on hover */ +} + +/* Style for foreignObject elements within nodes */ +.foreignObject { + overflow: visible; + /* Ensure the content is fully visible */ +} + +/* Add more styling as needed */ \ No newline at end of file diff --git a/src/LottieTreeParser.js b/src/LottieTreeParser.js new file mode 100644 index 0000000..31d9400 --- /dev/null +++ b/src/LottieTreeParser.js @@ -0,0 +1,158 @@ +import React, { useState, useEffect } from 'react'; +import { Tree } from 'react-d3-tree'; +import './LottieTreeParser.css'; + +const LottieTreeParser = ({ animationData, onClose }) => { + const [treeData, setTreeData] = useState(null); + + // Your provided property mappings + const basePropertyMappings = { + fr: 'framerate', + ip: 'inPoint', + op: 'outPoint', + w: 'width', + h: 'height', + nm: 'name', + mn: 'matchName', + ddd: 'is3D', + }; + + const layerPropertyMappings = { + ddd: 'is3D', + ind: 'index', + ty: 'type', + nm: 'name', + ip: 'inPoint', + op: 'outPoint', + st: 'startTime', + hd: 'isHidden', + }; + + const ksPropertyMappings = { + o: 'opacity', + r: 'rotation', + p: 'position', + a: 'anchor', + s: 'scale', + sk: 'skew', + sa: 'skewAxis', + }; + + const fontPropertyMappings = { + fFamily: 'fontFamily', + fWeight: 'fontWeight', + fStyle: 'fontStyle', + fName: 'fontName', + }; + + const assetPropertyMappings = { + id: 'id', + nm: 'name', + p: 'datapath', + u: 'path', + e: 'isEmbedded', + w: 'width', + h: 'height', + }; + + const mapProperties = (properties, mappings) => { + return Object.keys(mappings).reduce((acc, key) => { + if (properties[key] !== undefined) { + acc[mappings[key]] = properties[key]; + } + return acc; + }, {}); + }; + + const mapLayerToTreeNode = (layer) => { + const node = { + name: layer.nm || 'Unnamed Layer', + type: getLayerType(layer.ty), + properties: mapProperties(layer, layerPropertyMappings), + children: [] + }; + + if (layer.ks) { + node.transform = mapProperties(layer.ks, ksPropertyMappings); + } + + if (layer.shapes) { // For shape layers + layer.shapes.forEach(shape => { + node.children.push(mapShapeProperties(shape)); + }); + } else if (layer.t) { // For text layers + node.textData = mapProperties(layer.t.d, fontPropertyMappings); + } + + return node; + }; + + const getLayerType = (typeCode) => { + const types = { + 0: 'Precomp', 1: 'Solid', 2: 'Image', 3: 'Null', 4: 'Shape', + 5: 'Text', 6: 'Audio', 7: 'Video placeholder', 8: 'Image sequence', + 9: 'Video', 10: 'Image placeholder', 11: 'Guide', + 12: 'Adjustment', 13: 'Camera', 14: 'Light', 15: 'Data' + }; + return types[typeCode] || `Type ${typeCode}`; + }; + + const mapShapeProperties = (shape) => { + return { + name: shape.nm, + type: shape.ty, + properties: mapProperties(shape, assetPropertyMappings) + }; + }; + + const renderCustomNodeElement = ({ nodeDatum, toggleNode }) => ( + + + + {nodeDatum.name} + + +
+

{nodeDatum.type}

+ {Object.entries(nodeDatum.properties || {}).map(([key, value]) => ( +

{key}: {value}

+ ))} +
+
+
+ ); + + useEffect(() => { + if (animationData && animationData.layers) { + const root = { + name: animationData.nm || 'Root', + properties: mapProperties(animationData, basePropertyMappings), + children: animationData.layers.map(mapLayerToTreeNode) + }; + setTreeData([root]); // Note: Tree expects an array + } + }, [animationData]); + + return ( +
+
+ + {treeData && ( +
+ +
+ )} +
+
+ ); +}; + +export default LottieTreeParser; \ No newline at end of file diff --git a/src/LottieTreeParserN.js b/src/LottieTreeParserN.js new file mode 100644 index 0000000..fc97bfc --- /dev/null +++ b/src/LottieTreeParserN.js @@ -0,0 +1,244 @@ +import React, { useState, useEffect } from 'react'; +import { Tree } from 'react-d3-tree'; +import './LottieTreeParser.css'; + +const propertyKeyMapping = { + // Global layer properties + 'nm': { name: 'Name', type: 'string' }, // Name of the object + 'ty': { name: 'Type', type: 'integer' }, // Type of layer (0: 'Precomp', 1: 'Solid', etc.) + 'mn': { name: 'Match Name', type: 'string' }, // Internal AE match name + 'ddd': { name: '3D Layer', type: 'boolean' }, // Indicates 3D (0: not 3D, 1: 3D) + 'ip': { name: 'In Point', type: 'integer' }, // In point frame number + 'op': { name: 'Out Point', type: 'integer' }, // Out point frame number + 'w': { name: 'Width', type: 'integer' }, // Width in pixels + 'h': { name: 'Height', type: 'integer' }, // Height in pixels + 'fr': { name: 'Frame Rate', type: 'integer' }, // Frame rate + 'ind': { name: 'Index', type: 'integer' }, // Layer index + + // Transform properties (ks) + 'ks': { + 'p': { name: 'Position', type: 'array' }, + 's': { name: 'Scale', type: 'array' }, + 'r': { name: 'Rotation', type: 'integer' }, + 'a': { name: 'Anchor Point', type: 'array' }, + 'o': { name: 'Opacity', type: 'integer' } + }, + + // Shape layer properties + 'rc': { name: 'Rectangle', type: 'string' }, + 'el': { name: 'Ellipse', type: 'string' }, + 'sr': { name: 'Polygon / Star', type: 'string' }, + 'sh': { name: 'Path', type: 'string' }, + 'fl': { name: 'Fill', type: 'string' }, + 'st': { name: 'Stroke', type: 'string' }, + 'gf': { name: 'Gradient Fill', type: 'string' }, + 'gs': { name: 'Gradient Stroke', type: 'string' }, + 'gr': { name: 'Group', type: 'string' }, + + // Shape transform properties (tr) + 'tr': { + 'p': { name: 'Position', type: 'array' }, + 's': { name: 'Scale', type: 'array' }, + 'r': { name: 'Rotation', type: 'integer' }, + 'a': { name: 'Anchor Point', type: 'array' }, + 'o': { name: 'Opacity', type: 'integer' }, + 'sk': { name: 'Skew', type: 'integer' }, + 'sa': { name: 'Skew Axis', type: 'integer' } + }, + + // Text layer properties (when parent is 's') + 's': { + 's': { name: 'Size', type: 'integer' }, + 'f': { name: 'Font Family', type: 'string' }, + 't': { name: 'Text Content', type: 'string' }, + 'ca': { name: 'All Caps', type: 'boolean' }, + 'j': { name: 'Justify', type: 'integer' }, + 'tr': { name: 'Tracking', type: 'integer' }, + 'lh': { name: 'Line Height', type: 'integer' }, + 'ls': { name: 'Baseline Shift', type: 'integer' }, + 'fc': { name: 'Font Color', type: 'array' }, + 'sc': { name: 'Stroke Color', type: 'array' } + } + // Add more mappings as needed for other specific properties and contexts +}; +const parseProperties = (properties, context = '') => { + const parsed = {}; + for (const key in properties) { + if (!properties.hasOwnProperty(key)) continue; + + const propertyContext = context ? context[key] : propertyKeyMapping[key]; + if (propertyContext) { + if (typeof propertyContext === 'object' && propertyContext.type === 'object') { + // Recursive parsing for nested objects + parsed[propertyContext.name] = parseProperties(properties[key], propertyContext); + } else if (Array.isArray(properties[key])) { + // Recursive parsing for arrays + parsed[propertyContext.name] = properties[key].map(item => { + if (typeof item === 'object') { + return parseProperties(item, context); + } + return item; + }); + } else { + // Direct mapping for simple properties + const propertyName = propertyContext.name || key; + parsed[propertyName] = properties[key]; + } + } else { + // Default handling for unmapped keys + if (typeof properties[key] === 'object' && properties[key] !== null) { + // Recursive parsing for unmapped nested objects + parsed[key] = parseProperties(properties[key]); + } else { + parsed[key] = properties[key]; + } + } + } + return parsed; +}; + +/**************** LAYER PARSING HADNLERS **************/ + +const parseLayerTransforms = (layer) => { + const parsedTransforms = { + Name: layer.nm, + Type: propertyKeyMapping[layer.ty]?.name || `Type ${layer.ty}`, + InPoint: layer.ip, + OutPoint: layer.op + }; + + // Parse keyframe transform properties + for (const key in layer.ks) { + const propertyInfo = propertyKeyMapping.ks[key]; + if (propertyInfo) { + const propertyName = propertyInfo.name || key; + const transformProperty = layer.ks[key]; + + // Check if the property is keyframed + if (transformProperty.a === 1) { + // Property is keyframed; parse each keyframe + parsedTransforms[propertyName] = transformProperty.k.map(keyframe => { + // Parse each keyframe object + return keyframe; + }); + } else { + // Static property; not keyframed + parsedTransforms[propertyName] = transformProperty.k; + } + } else { + // Default handling for unmapped keys + parsedTransforms[key] = layer.ks[key]; + } + } + + return parsedTransforms; +}; +const parseShapeLayer = (layer) => { + const parseShapeProperties = (shape) => { + const parsedShape = { Name: shape.nm, Type: propertyKeyMapping[shape.ty]?.name || shape.ty }; + + for (const key in shape) { + if (shape.hasOwnProperty(key)) { + const propertyInfo = propertyKeyMapping[key]; + if (propertyInfo) { + const propertyName = propertyInfo.name || key; + parsedShape[propertyName] = shape[key]; + } else { + parsedShape[key] = shape[key]; + } + } + } + + return parsedShape; + }; + + return { + ...parseLayerTransforms(layer), + Shapes: layer.shapes.map(parseShapeProperties) + }; +}; +const parseTextLayer = (layer) => { + const parseTextData = (textData) => { + const parsedTextData = {}; + for (const key in textData) { + if (textData.hasOwnProperty(key)) { + const propertyInfo = propertyKeyMapping[key] || propertyKeyMapping['s'][key]; + if (propertyInfo) { + const propertyName = propertyInfo.name || key; + parsedTextData[propertyName] = textData[key]; + } else { + parsedTextData[key] = textData[key]; + } + } + } + return parsedTextData; + }; + + return { + ...parseLayerTransforms(layer), + TextData: parseTextData(layer.t.d) + }; +}; +// Main function to parse layers based on their type +const parseLayers = (layers) => { + return layers.map(layer => { + switch (layer.ty) { + case 4: // Shape Layer + return parseShapeLayer(layer); + case 5: // Text Layer + return parseTextLayer(layer); + default: + // For all other layer types, parse only the keyframe transform properties + return parseLayerTransforms(layer); + } + }); +}; + +const LottieDataParser = ({ animationData, onClose }) => { + const [treeData, setTreeData] = useState(null); + + useEffect(() => { + if (animationData) { + const parsedData = parseLayers(animationData.layers); + console.log('Parsed Lottie Data:', parsedData); // Log the parsed data + + setTreeData([{ + name: 'Lottie Animation', + children: parsedData + }]); + } + }, [animationData]); + + const renderCustomNodeElement = ({ nodeDatum, toggleNode }) => ( + + + + {nodeDatum.name} + + {/* Additional custom rendering logic */} + + ); + + return ( +
+
+ + {treeData && ( +
+ +
+ )} +
+
+ ); +}; + +export default LottieDataParser; \ No newline at end of file diff --git a/src/PlayerUI.js b/src/PlayerUI.js index c1aa406..933dd7e 100644 --- a/src/PlayerUI.js +++ b/src/PlayerUI.js @@ -7,6 +7,8 @@ import { ReactComponent as ProgressBar } from './svgUIelements/progressBar.svg'; import { ReactComponent as ProgressBarHandle } from './svgUIelements/progressBarHandle.svg'; import ColorPicker from 'react-best-gradient-color-picker'; import LottieInfoParser from './LottieInfoParser'; +import LottieTreeParser from './LottieTreeParserN'; + const isLoggingEnabled = true; // Set to false to disable logging // ERROR LOGGING @@ -34,6 +36,10 @@ const PlayerUI = ({ animationData, version, fileSize }) => { durationFrames: 0, durationSeconds: 0, }); + const [isTreeModalVisible, setIsTreeModalVisible] = useState(false); + const toggleTreeModal = () => { + setIsTreeModalVisible(!isTreeModalVisible); + }; const [markerStartFrame, setMarkerStartFrame] = useState(null); @@ -279,10 +285,17 @@ const PlayerUI = ({ animationData, version, fileSize }) => { )} {/* New Code Compare button */} - + {isTreeModalVisible && ( + + )} + {isPickerVisible && (