diff --git a/package-lock.json b/package-lock.json index bf1e4e2..5c911a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "css-minimizer-webpack-plugin": "^5.0.1", "fs-extra": "^11.1.1", "glob": "^10.3.3", + "html-webpack-plugin": "^5.6.3", "husky": "^9.1.6", "inquirer": "^9.2.10", "jest": "^29.7.0", @@ -2753,6 +2754,13 @@ "@types/node": "*" } }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3724,6 +3732,17 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -4766,6 +4785,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4839,6 +4868,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5649,6 +5689,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -5682,6 +5732,71 @@ "dev": true, "license": "MIT" }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", @@ -8164,6 +8279,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -8255,6 +8377,16 @@ "node": ">=8" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8469,6 +8601,17 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -8880,6 +9023,17 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -8939,6 +9093,17 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -9712,6 +9877,17 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -9953,6 +10129,146 @@ "node": ">=6" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11072,6 +11388,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 1edc7f2..e399bcb 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "css-minimizer-webpack-plugin": "^5.0.1", "fs-extra": "^11.1.1", "glob": "^10.3.3", + "html-webpack-plugin": "^5.6.3", "husky": "^9.1.6", "inquirer": "^9.2.10", "jest": "^29.7.0", diff --git a/project/src/modules/command-center-graphic/command-center-graphic.css b/project/src/modules/command-center-graphic/command-center-graphic.css new file mode 100644 index 0000000..537c269 --- /dev/null +++ b/project/src/modules/command-center-graphic/command-center-graphic.css @@ -0,0 +1,73 @@ +/* Command Center Graphic Styles */ + +/* Ensure tiles can transition smoothly */ +.cc-block-item { + transition: filter 0.3s ease; + position: relative; +} + +/* Allow for special effects on the SVG elements */ +.fuller-cube-outline { + transition: transform 0.3s ease, filter 0.3s ease; + transform-origin: center center; +} + +/* Special styling for when tiles are processing or active */ +.cc-block-item.processing .fuller-cube-outline { + filter: drop-shadow(0 0 5px rgba(105, 240, 174, 0.6)); +} + +/* Special styling for the center "brain" tile */ +.fuller-cube-outline.fco__accent { + transition: transform 0.4s ease, filter 0.4s ease; +} + +/* Container for the connections needs to be positioned properly */ +.network-grid-container { + position: relative; +} + +/* Make sure the grid wrap can handle absolute positioned elements */ +.network-grid-wrap { + position: relative; +} + +/* Additional animation classes that can be applied dynamically */ +.pulse-effect { + animation: pulse 1.5s ease-in-out; +} + +@keyframes pulse { + 0% { transform: scale(1); filter: brightness(1); } + 50% { transform: scale(1.05); filter: brightness(1.2); } + 100% { transform: scale(1); filter: brightness(1); } +} + +/* For active tiles during connections */ +.tile-active { + filter: brightness(1.2) !important; +} + +/* For tiles that are "thinking" or processing */ +.tile-processing { + animation: processing 2s infinite alternate; +} + +@keyframes processing { + 0% { filter: brightness(1); } + 100% { filter: brightness(1.15) saturate(1.2); } +} + +/* Brain pulse ripple effect overlay */ +.brain-pulse-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 70%); + opacity: 0; + pointer-events: none; + z-index: 5; +} \ No newline at end of file diff --git a/project/src/modules/command-center-graphic/command-center-graphic.js b/project/src/modules/command-center-graphic/command-center-graphic.js index 1f31185..7b12ff1 100644 --- a/project/src/modules/command-center-graphic/command-center-graphic.js +++ b/project/src/modules/command-center-graphic/command-center-graphic.js @@ -2,10 +2,51 @@ import { select, selectId } from '../../utils/helpers.js'; import Base from '../base/base.js'; import { gsap } from '../../utils/animation.js'; +import './command-center-graphic.css'; + +/** + * CommandCenterGraphic - Manages animated tile movement patterns + * with configurable animation properties + */ export default class CommandCenterGraphic extends Base { - constructor(elementId, debug = false) { + /** + * Creates a new CommandCenterGraphic instance + * @param {string} elementId - ID of the container element + * @param {Object} config - Optional configuration to override defaults + * @param {boolean} debug - Enable debug logging + */ + constructor(elementId, config = {}, debug = false) { super(debug); + // Default configuration + this.defaultConfig = { + // Timing + initialDelay: 1.5, // Delay before reveal animation (seconds) + animationDelay: 3000, // Delay between iterations (milliseconds) + specialIterationDelay: 1500, // Delay after special iterations (milliseconds) + staggerDelay: 0.022, // Delay between each tile animation (seconds) + + // Durations + revealDuration: 1, + regularDuration: 0.7, + specialDuration: 1, + + // Easing + revealEasing: 'power4.out', // Easing for reveal animation + regularEasing: 'expo.out', // Easing for regular iterations + specialEasing: 'power4.inOut', // Easing for special iterations + + // Pattern + specialIterationInterval: 3, // Every nth iteration is special + + // ScrollTrigger + scrollTriggerStart: 'top 40%', // When to trigger animation + scrollTriggerActions: 'play none none reverse' // ScrollTrigger behavior + }; + + // Merge provided config with defaults + this.config = { ...this.defaultConfig, ...config }; + if (!elementId) { this.log('No element ID provided. Please provide a valid ID.', 'error'); return; @@ -19,115 +60,230 @@ export default class CommandCenterGraphic extends Base { } this.log(`Initializing with ID "${elementId}"`, 'info'); - this.tiles = this.element.querySelectorAll('.cc-block-item .fuller-cube-outline:not(.fco__accent)'); - this.log(`Tiles: ${this.tiles}`, 'info'); + // Convert NodeList to Array for easier manipulation + this.tiles = Array.from(this.element.querySelectorAll('.cc-block-item .fuller-cube-outline:not(.fco__accent)')); + this.log(`Found ${this.tiles.length} tiles`, 'info'); this.centerTile = this.element.querySelector('.cc-block-item .fuller-cube-outline.fco__accent'); this.log(`Center Tile: ${this.centerTile}`, 'info'); - this.animationDelay = 3000; + this.iterationCount = 0; + + // Initialize the component this.init(); } + /** + * Initialize component with reveal animations + */ init() { - // Initialize your module here + // Initialize with reveal animations gsap.set(this.tiles, { x: -20, opacity: 0 }); gsap.set(this.centerTile, { y: 20, opacity: 0 }); + // Animate peripheral tiles gsap.to(this.tiles, { x: 0, opacity: 1, stagger: 0.1, - duration: 1, - ease: 'power4.out', - delay: 1.5, + duration: this.config.revealDuration, + ease: this.config.revealEasing, + delay: this.config.initialDelay, scrollTrigger: { trigger: this.element, - start: 'top 40%', - toggleActions: 'play none none reverse', + start: this.config.scrollTriggerStart, + toggleActions: this.config.scrollTriggerActions, }, onComplete: () => { - this.log(`Starting tile animation...`); - this.startTileAnimation(); + this.log(`Reveal animation complete. Starting tile movement sequence...`); + // After the reveal animation completes, start our animation sequence + setTimeout(() => this.startAnimationSequence(), this.config.animationDelay); }, }); + // Animate center tile gsap.to(this.centerTile, { y: 0, opacity: 1, - duration: 1, - ease: 'power4.out', + duration: this.config.revealDuration, + ease: this.config.revealEasing, scrollTrigger: { trigger: this.element, - start: 'top 40%', - toggleActions: 'play none none reverse', + start: this.config.scrollTriggerStart, + toggleActions: this.config.scrollTriggerActions, }, }); } - startTileAnimation() { - const initialPositions = Array.from(this.tiles).map((tile, index) => { - const rect = tile.getBoundingClientRect(); - // add an attribute to keep track of the original position - tile.setAttribute('data-original-x', rect.left); - tile.setAttribute('data-original-y', rect.top); - tile.setAttribute('data-original-index', index); - - return { x: rect.left, y: rect.top }; - }); - - this.log('Initial Positions:', initialPositions); - - const clockwiseOrder = [0, 1, 2, 4, 7, 6, 5, 3]; - const counterClockwiseOrder = [0, 3, 5, 6, 7, 4, 2, 1]; - - const moveTiles = (order) => { - this.log(`called moveTiles()`); - const newPositions = order.map(index => { - const orderPosition = order.indexOf(index); - const nextIndex = (orderPosition + 1) % order.length; - - this.log(`mapping ${index} to ${order[nextIndex]}`); + /** + * Start continuous animation sequence + */ + startAnimationSequence() { + this.log('Starting continuous animation sequence'); + this.animateNextIteration(); + } - return initialPositions[order[nextIndex]]; + /** + * Animate the next iteration in the sequence + */ + animateNextIteration() { + this.iterationCount++; + const isSpecialIteration = this.iterationCount % this.config.specialIterationInterval === 0; + + this.log(`Animation iteration ${this.iterationCount}, Special: ${isSpecialIteration}`); + + if (isSpecialIteration) { + // Special iteration with special easing and counter-clockwise movement + this.moveTiles('counterClockwise', { + easing: this.config.specialEasing, + duration: this.config.specialDuration }); + + // Schedule the next iteration with shorter delay + setTimeout(() => this.animateNextIteration(), this.config.specialIterationDelay); + } else { + // Normal iteration with regular easing and clockwise movement + this.moveTiles('clockwise', { + easing: this.config.regularEasing, + duration: this.config.regularDuration + }); + + // Schedule the next iteration + setTimeout(() => this.animateNextIteration(), this.config.animationDelay); + } + } - this.log('New Positions:', newPositions); - - this.tiles.forEach((tile, index) => { - if(index > 3) return; - const dx = newPositions[index].x - initialPositions[index].x; - const dy = newPositions[index].y - initialPositions[index].y; + /** + * Move tiles in specified direction with given options + * @param {string} direction - 'clockwise' or 'counterClockwise' + * @param {Object} options - Animation options + */ + moveTiles(direction, options = {}) { + const easing = options.easing || this.config.regularEasing; + const duration = options.duration || this.config.regularDuration; + + this.log(`Moving tiles ${direction} with ${easing} easing`); + + const positions = this.getTilePositions(); + + // Get the appropriate movement pattern based on direction + const moveTargets = this.getMovementPattern(direction); + + // Animate each tile to its target position + this.tiles.forEach((tile, index) => { + // Find where this tile should move to + const targetPosition = positions[moveTargets[index]]; + + // Calculate the required movement + const currentRect = tile.getBoundingClientRect(); + const dx = targetPosition.x - currentRect.left; + const dy = targetPosition.y - currentRect.top; + + this.log(`Moving tile ${index} to ${targetPosition.name} (position ${moveTargets[index]})`); + + // Animate the tile to the new position + gsap.to(tile, { + x: `+=${dx}`, + y: `+=${dy}`, + duration: duration, + // delay: index * this.config.staggerDelay, + ease: easing, + onStart: () => { + this.log(`Starting ${direction} animation for tile ${index}`); + }, + onComplete: () => { + this.log(`Completed ${direction} animation for tile ${index}`); + } + }); + }); + } - this.log(`Moving tile ${index}`); + /** + * Get the movement pattern based on direction + * @param {string} direction - 'clockwise' or 'counterClockwise' + * @returns {Array} Array of target indices for each tile + */ + getMovementPattern(direction) { + if (direction === 'clockwise') { + // Define the clockwise movement pattern + return [ + 1, // DOM tile 0 (top-left) moves to position 1 (top-middle) + 2, // DOM tile 1 (top-middle) moves to position 2 (top-right) + 4, // DOM tile 2 (top-right) moves to position 4 (middle-right) + 0, // DOM tile 3 (middle-left) moves to position 0 (top-left) + 7, // DOM tile 4 (middle-right) moves to position 7 (bottom-right) + 3, // DOM tile 5 (bottom-left) moves to position 3 (middle-left) + 5, // DOM tile 6 (bottom-middle) moves to position 5 (bottom-left) + 6 // DOM tile 7 (bottom-right) moves to position 6 (bottom-middle) + ]; + } else { + // Define the counter-clockwise movement pattern + return [ + 3, // DOM tile 0 (top-left) → position 3 (middle-left) + 0, // DOM tile 1 (top-middle) → position 0 (top-left) + 1, // DOM tile 2 (top-right) → position 1 (top-middle) + 5, // DOM tile 3 (middle-left) → position 5 (bottom-left) + 2, // DOM tile 4 (middle-right) → position 2 (top-right) + 6, // DOM tile 5 (bottom-left) → position 6 (bottom-middle) + 7, // DOM tile 6 (bottom-middle) → position 7 (bottom-right) + 4 // DOM tile 7 (bottom-right) → position 4 (middle-right) + ]; + } + } - gsap.to(tile, { - x: `+=${dx}`, - y: `+=${dy}`, - duration: 1, - ease: 'power4.out', - }); - }); + /** + * Get the current positions of all tiles + * @returns {Array} Array of position objects with coordinates + */ + getTilePositions() { + const positions = [ + { name: 'top-left', index: 0 }, + { name: 'top-middle', index: 1 }, + { name: 'top-right', index: 2 }, + { name: 'middle-left', index: 3 }, + { name: 'middle-right', index: 4 }, + { name: 'bottom-left', index: 5 }, + { name: 'bottom-middle', index: 6 }, + { name: 'bottom-right', index: 7 } + ]; + + // Get the exact coordinates of each position from the current tiles + positions.forEach(pos => { + const tile = this.tiles[pos.index]; + const rect = tile.getBoundingClientRect(); + pos.x = rect.left; + pos.y = rect.top; + this.log(`Position ${pos.name} (${pos.index}): (${pos.x}, ${pos.y})`); + }); + + return positions; + } - initialPositions.splice(0, initialPositions.length, ...newPositions); - }; + /** + * Update configuration values + * @param {Object} newConfig - New configuration values to apply + */ + updateConfig(newConfig = {}) { + this.config = { ...this.config, ...newConfig }; + this.log('Configuration updated:', 'info'); + this.log(this.config, 'info'); + } - const animateTiles = () => { - moveTiles(clockwiseOrder); - setTimeout(() => { - moveTiles(clockwiseOrder); - setTimeout(() => { - moveTiles(counterClockwiseOrder); - setTimeout(() => { - moveTiles(counterClockwiseOrder); - setTimeout(animateTiles, this.animationDelay); - }, this.animationDelay); - }, this.animationDelay); - }, this.animationDelay); - }; + /** + * Reset animation sequence + */ + reset() { + this.iterationCount = 0; + this.log('Animation sequence reset'); + } - // setTimeout(animateTiles, this.animationDelay); - // this.log('setting timeout for animateTiles'); - // setTimeout(() => { - // moveTiles(clockwiseOrder); - // }, this.animationDelay); + /** + * Stop animation sequence + */ + stop() { + // Clear any pending timeouts + if (this.animationTimeout) { + clearTimeout(this.animationTimeout); + this.animationTimeout = null; + } + this.log('Animation sequence stopped'); } } \ No newline at end of file diff --git a/project/src/modules/command-center-graphic/dev-mode-results.json b/project/src/modules/command-center-graphic/dev-mode-results.json new file mode 100644 index 0000000..0b5a393 --- /dev/null +++ b/project/src/modules/command-center-graphic/dev-mode-results.json @@ -0,0 +1,434 @@ +{ + "top-left-to-top-middle": { + "from": { + "position": "top-left", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "top-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-left-to-top-right": { + "from": { + "position": "top-left", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "top-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "top-left-to-middle-left": { + "from": { + "position": "top-left", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-left-to-middle-right": { + "from": { + "position": "top-left", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-left-to-bottom-left": { + "from": { + "position": "top-left", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-left", + "offsetX": 40, + "offsetY": 42.071875000000006 + } + }, + "top-left-to-bottom-middle": { + "from": { + "position": "top-left", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-left-to-bottom-right": { + "from": { + "position": "top-left", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "top-middle-to-top-right": { + "from": { + "position": "top-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "top-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "top-middle-to-middle-left": { + "from": { + "position": "top-middle", + "offsetX": 38.5, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-middle-to-middle-right": { + "from": { + "position": "top-middle", + "offsetX": 38.5, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-middle-to-bottom-left": { + "from": { + "position": "top-middle", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-left", + "offsetX": 40, + "offsetY": 42.071875000000006 + } + }, + "top-middle-to-bottom-middle": { + "from": { + "position": "top-middle", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-middle-to-bottom-right": { + "from": { + "position": "top-middle", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "top-right-to-middle-left": { + "from": { + "position": "top-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-right-to-middle-right": { + "from": { + "position": "top-right", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-right-to-bottom-left": { + "from": { + "position": "top-right", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "bottom-left", + "offsetX": 40, + "offsetY": 42.071875000000006 + } + }, + "top-right-to-bottom-middle": { + "from": { + "position": "top-right", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "top-right-to-bottom-right": { + "from": { + "position": "top-right", + "offsetX": 30, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "middle-left-to-middle-right": { + "from": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "middle-left-to-bottom-left": { + "from": { + "position": "middle-left", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-left", + "offsetX": 40, + "offsetY": 42.071875000000006 + } + }, + "middle-left-to-bottom-middle": { + "from": { + "position": "middle-left", + "offsetX": 38.5, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "middle-left-to-bottom-right": { + "from": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "middle-right-to-bottom-left": { + "from": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "bottom-left", + "offsetX": 40, + "offsetY": 42.071875000000006 + } + }, + "middle-right-to-bottom-middle": { + "from": { + "position": "middle-right", + "offsetX": 38.5, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "middle-right-to-bottom-right": { + "from": { + "position": "middle-right", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "bottom-left-to-bottom-middle": { + "from": { + "position": "bottom-left", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + } + }, + "bottom-left-to-bottom-right": { + "from": { + "position": "bottom-left", + "offsetX": 45, + "offsetY": 31.553906249999997 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "bottom-middle-to-bottom-right": { + "from": { + "position": "bottom-middle", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "bottom-right", + "offsetX": 45, + "offsetY": 31.553906249999997 + } + }, + "top-left-to-center": { + "from": { + "position": "top-left", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "top-middle-to-center": { + "from": { + "position": "top-middle", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "top-right-to-center": { + "from": { + "position": "top-right", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "middle-left-to-center": { + "from": { + "position": "middle-left", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "middle-right-to-center": { + "from": { + "position": "middle-right", + "offsetX": 45, + "offsetY": 36.812890624999994 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "bottom-left-to-center": { + "from": { + "position": "bottom-left", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "center", + "offsetX": 55.00000000000001, + "offsetY": 58.575 + } + }, + "bottom-middle-to-center": { + "from": { + "position": "bottom-middle", + "offsetX": 35, + "offsetY": 47.330859375 + }, + "to": { + "position": "center", + "offsetX": 50, + "offsetY": 53.25 + } + }, + "bottom-right-to-center": { + "from": { + "position": "bottom-right", + "offsetX": 33, + "offsetY": 52.06394531250001 + }, + "to": { + "position": "center", + "offsetX": 55.00000000000001, + "offsetY": 58.575 + } + } +} \ No newline at end of file diff --git a/project/src/modules/command-center-graphic/prompt.txt b/project/src/modules/command-center-graphic/prompt.txt new file mode 100644 index 0000000..0951ee6 --- /dev/null +++ b/project/src/modules/command-center-graphic/prompt.txt @@ -0,0 +1,55 @@ +# Command Center Animation Development Prompt + +## Project Overview +I'm developing a dynamic animation for a command center interface using GSAP. The animation features 8 tiles arranged in a perimeter around a central "brain" tile in a 3x3 grid. I need help evolving this animation from its current state to something more fluid, organic, and visually interesting. + +## Current Implementation +My animation has progressed through two key versions: + +1. **Basic Movement Version**: Tiles move clockwise/counterclockwise around the perimeter of the grid. + - Discovered DOM order vs. visual position mapping issue + - Fixed with explicit position mapping in `moveTargets` array + - Implemented staggered movements with varying timing + +2. **Intelligent Network Concept**: Currently exploring this enhanced version with: + - Four movement patterns (clockwise, counterclockwise, pair swaps, pattern groups) + - SVG connection lines between tiles that animate in/out + - Random processing "pulses" on individual tiles + - Center "brain" tile that periodically influences other tiles + +## Technical Details +- Grid layout follows this structure, with each number representing tile index in DOM order: +``` + [0][1][2] + [3][ ][4] (center excluded) + [5][6][7] +``` +- Movements must account for this specific DOM structure +- Animation features need to respect the current position of each tile +- Currently using GSAP for all animations with standard DOM/SVG manipulation + +## Additional Animation Concepts +Beyond the Intelligent Network, we've developed two other promising concepts: + +1. **Resonance Cascade System**: + - Tiles oscillate subtly from the center + - "Cascade events" trigger chain reactions of movements + - Each tile's movement influences nearby tiles with decreasing intensity + - Elastic/bounce easing creates satisfying rhythmic quality + - Direction of cascade flows periodically reverses + +2. **Quantum State Observer**: + - Tiles occasionally split into semi-transparent "ghost" versions + - SVG morphing between different visual states + - "Entanglement" effects where tiles mirror each other + - Periodic "collapse" moments where animation pauses before resolving to new states + - Subtle color shifts indicating different energy states + +## Next Steps +I'd like to continue refining these animation concepts or explore additional ideas that: +1. Create fluid, somewhat unpredictable movement +2. Maintain a "command center" aesthetic +3. Look visually interesting and engaging +4. Use modern animation techniques within my current technical framework + +When I share my code, please focus on enhancing the animation concepts while maintaining the technical approach I've established. \ No newline at end of file diff --git a/project/src/modules/three-d-slider/three-d-slider.html b/project/src/modules/three-d-slider/three-d-slider.html new file mode 100644 index 0000000..1834572 --- /dev/null +++ b/project/src/modules/three-d-slider/three-d-slider.html @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + +
+
+
+
+

How It Works

+

+ Every step is designed to keep things running smoothly, so you can focus on what you do best. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
06
+
+
+
+
+
+
STEP 
+
06
+
+

Project Wrap

+
+

+ Complete final inspections and hand over the keys with confidence. Fuller’s streamlined closeout + process ensures a smooth transition from build to occupancy, setting the stage for your next + successful project. +

+ See the Fuller Advantage + +
+
+
+
+
+
05
+
+
+
+
+
+
STEP 
+
05
+
+

On-Site Completion

+
+

+ Set the modules, connect utilities, and complete the finishing touches to deliver a fully realized + home. Builders benefit from Fuller’s hands-on training and support to ensure every project runs + smoothly. +

+ Learn About Builder Support + +
+
+
+
+
+
04
+
+
+
+
+
+
STEP 
+
04
+
+

Delivery to Site

+
+

+ When your Fuller home leaves our manufacturing partner’s facility, our logistics team monitors + along the way, ensuring it arrives safely, on schedule, and ready for installation. +

+ + +
+
+
+
+
+
03
+
+
+
+
+
+
STEP 
+
03
+
+

Precision Build

+
+

+ Leveraging our off-site production system in climate-controlled facilities, Fuller builds homes to + superior quality and precision standards. Each unit is rigorously performance-tested and leave the + facility 90% complete, reducing build times by up to 50%. +

+ + +
+
+
+
+
+
02
+
+
+
+
+
+
STEP 
+
02
+
+

Submit Your Order

+
+

+ Finalize your plan and submit it through our intuitive Fuller Portal. Since our plans are built on + BIM, you’ll instantly receive precise material takeoffs, accurate production timelines and design + certainty from day one. +

+ Explore Data-Driven Systems + +
+
+
+
+
+
01
+
+
+
+
+
+
STEP 
+
01
+
+

Choose Your Model

+
+

+ Select the perfect plan for your project from our curated library, then use our configurator to + personalize your home’s design and structure. Explore how our architect-crafted models balance + style, functionality, and scalability to meet market demands. +

+ Learn More + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/project/src/modules/three-d-slider/three-d-slider.js b/project/src/modules/three-d-slider/three-d-slider.js index 6901cdc..dbb1086 100644 --- a/project/src/modules/three-d-slider/three-d-slider.js +++ b/project/src/modules/three-d-slider/three-d-slider.js @@ -1,38 +1,127 @@ import { selectId } from '../../utils/helpers.js'; import { gsap } from '../../utils/animation.js'; +import Base from '../base/base.js'; + import './three-d-slider.css'; -const EASE_FUNCTION = 'expo.out'; -const STEP_ATTR_INDICATOR = 'data-step-indicator-num'; -const STEP_ATTR_SLIDE = 'data-step-num'; +// Class selectors +const SELECTORS = { + INDICATORS_CONTAINER: '.tds-indicators', + INDICATOR: '.tds-indicator', + INDICATOR_ACTIVE: '.tdsi-active', + ACTIVE_STATE: 'tds--active', + BACK_STATE: 'tds--back', + FORWARD_STATE: 'tds--forward', + SLIDES_CONTAINER: '.tds-slides', + SLIDE: '.tds-slide', + NAV_BTN: '.tdss__nav-btn', + NAV_BTN_PREV: '.tdss__nav-btn--prev', + NAV_BTN_NEXT: '.tdss__nav-btn--next', + SECTION_CONTAINER: '.how-it-works-section', + WAYPOINT: '.slide-waypoint' +}; + +// Data attributes +const DATA_ATTRS = { + STEP_INDICATOR: 'data-step-indicator-num', + STEP_SLIDE: 'data-step-num', + WAYPOINT_INDEX: 'data-waypoint-index' +}; + +// Animation and timing configuration +const CONFIG = { + // Animation settings + EASE_FUNCTION: 'expo.out', + ACTIVE_SLIDE_DURATION: 0.5, + INACTIVE_SLIDE_DURATION_BEHIND: 0.5, // Slides behind active + INACTIVE_SLIDE_DURATION_FRONT: 0.4, // Slides in front + DELAY_MULTIPLIER: 0.05, + + // 3D effect settings + Z_OFFSET_PER_SLIDE: -50, + Y_OFFSET_BEHIND: -25, + Y_OFFSET_FRONT: -100, + OPACITY_BASE: 0.7, + OPACITY_DECREMENT: 0.1, + + // Scroll settings + SCROLL_THROTTLE: 100, // ms + ANIMATION_COOLDOWN: 600, // ms + WAYPOINT_COOLDOWN: 800, // ms + SCROLL_HYSTERESIS: 0.2, // 20% threshold for backward movement + + // IntersectionObserver settings + INTERSECTION_THRESHOLD: 0.1, + INTERSECTION_ROOT_MARGIN: '-20% 0px -80% 0px', // Trigger at top 20% of viewport + + // Click handling + CLICK_LOCK_DURATION: 1000 // ms +}; + +export default class ThreeDSlider extends Base { + constructor(elementId, debug = false, customConfig = {}) { + super(debug); -export default class ThreeDSlider { - constructor(elementId) { if (!elementId) return; + this.log(`instantiated with ${elementId}`); + + // Merge custom configuration if provided + this.config = { ...CONFIG, ...customConfig }; + this.element = selectId(elementId); - this.indicators = this.element.querySelectorAll('.tds-indicators .tds-indicator'); - this.slides = this.element.querySelectorAll('.tds-slides .tds-slide'); + this.indicators = this.element.querySelectorAll(`${SELECTORS.INDICATORS_CONTAINER} ${SELECTORS.INDICATOR}`); + this.slides = this.element.querySelectorAll(`${SELECTORS.SLIDES_CONTAINER} ${SELECTORS.SLIDE}`); if (this.slides && this.slides.length !== 0) { this.slides = Array.from(this.slides).reverse(); } - this.prevBtns = this.element.querySelectorAll('.tdss__nav-btn.tdss__nav-btn--prev'); - this.nextBtns = this.element.querySelectorAll('.tdss__nav-btn.tdss__nav-btn--next'); + this.prevBtns = this.element.querySelectorAll(`${SELECTORS.NAV_BTN}${SELECTORS.NAV_BTN_PREV}`); + this.nextBtns = this.element.querySelectorAll(`${SELECTORS.NAV_BTN}${SELECTORS.NAV_BTN_NEXT}`); + + // Initialize scroll-based navigation properties + this.scrollEnabled = true; + this.currentScrollIndex = 0; + this.scrollLocked = false; + + // Track active animations to cancel them when needed + this.activeAnimations = []; + this.animationInProgress = false; + this.pendingAnimationIndex = null; + + // Track the current animating slide to prevent duplicate animations + this.currentlyAnimatingToIndex = null; + + // Flag to disable scroll handling during animation + this.scrollHandlerDisabled = false; + + // Direction of navigation - helps prevent bounce-back + this.lastNavigationDirection = null; // 'forward' or 'backward' + + // Record last waypoint timestamp to prevent immediate scroll handler override + this.lastWaypointTime = 0; + + // Record animation completion time for bounce-back prevention + this.lastAnimationCompleteTime = 0; + this.lastCompletedSlideIndex = undefined; + + // Debug flag - can be set to true to show more visual debugging + this.debugVisuals = false; this.init(); } init() { this.addListeners(); + this.setupScrollNavigation(); } addListeners() { this.indicators.forEach((indicator, index) => { // set active indicator - let activeIndicator = indicator.querySelector('.tdsi-active'); - activeIndicator.classList.add(index === 0 ? 'tds--active' : 'tds--back'); + let activeIndicator = indicator.querySelector(SELECTORS.INDICATOR_ACTIVE); + activeIndicator.classList.add(index === 0 ? SELECTORS.ACTIVE_STATE : SELECTORS.BACK_STATE); indicator.addEventListener('click', (e) => { this.handleIndicatorClick(e.currentTarget); @@ -58,34 +147,351 @@ export default class ThreeDSlider { } } - handleIndicatorClick(indicator) { - let isActive = indicator.querySelector('.tdsi-active').classList.contains('tds--active'); + /** + * Set up scroll-based navigation using IntersectionObserver + */ + setupScrollNavigation() { + this.log('Setting up scroll navigation'); + + // Find the waypoints + const section = this.element.closest(SELECTORS.SECTION_CONTAINER); + if (!section) { + this.log('Could not find parent section'); + return; + } + + this.waypoints = section.querySelectorAll(SELECTORS.WAYPOINT); + + if (!this.waypoints || this.waypoints.length === 0) { + this.log('No waypoints found'); + return; + } + + this.log(`Found ${this.waypoints.length} waypoints`); + + // DEBUGGING: Log the exact position of each waypoint + this.waypoints.forEach((waypoint, index) => { + const rect = waypoint.getBoundingClientRect(); + const sectionRect = section.getBoundingClientRect(); + const relativeTop = rect.top - sectionRect.top; + const percentageInSection = (relativeTop / sectionRect.height) * 100; + + this.log(`Waypoint ${index} position: ${percentageInSection.toFixed(2)}% from section top, + rect: top=${rect.top}, bottom=${rect.bottom}, height=${rect.height}`); + + // Check waypoint CSS positioning + const computedStyle = window.getComputedStyle(waypoint); + this.log(`Waypoint ${index} CSS: position=${computedStyle.position}, + top=${computedStyle.top}, left=${computedStyle.left}`); + }); + + // Add visual debugging markers if enabled + if (this.debugVisuals) { + this.waypoints.forEach((waypoint) => { + waypoint.style.height = '10px'; + waypoint.style.background = 'red'; + waypoint.style.opacity = '0.5'; + waypoint.style.pointerEvents = 'none'; + waypoint.style.zIndex = '9999'; + }); + } + + // Create IntersectionObserver with settings to trigger at top portion of viewport + const observerSettings = { + threshold: this.config.INTERSECTION_THRESHOLD, + rootMargin: this.config.INTERSECTION_ROOT_MARGIN + }; + + this.log(`Creating IntersectionObserver with settings: + threshold: ${observerSettings.threshold} + rootMargin: ${observerSettings.rootMargin}`); + + this.observer = new IntersectionObserver((entries) => { + if (this.scrollLocked || this.scrollHandlerDisabled) { + this.log(`IntersectionObserver triggered but handler disabled`); + return; + } + + // Sort entries by timestamp to process the latest ones first + entries.sort((a, b) => b.time - a.time); + + // Log all entries for debugging + entries.forEach(entry => { + const waypointIndex = parseInt(entry.target.dataset[DATA_ATTRS.WAYPOINT_INDEX.replace('data-', '')]); + const ratio = entry.intersectionRatio; + const rect = entry.boundingClientRect; + const rootRect = entry.rootBounds; + + this.log(`IntersectionObserver entry: + waypoint=${waypointIndex}, + isIntersecting=${entry.isIntersecting}, + ratio=${ratio.toFixed(2)}, + rect: top=${rect.top}, bottom=${rect.bottom}, + rootRect: top=${rootRect.top}, bottom=${rootRect.bottom}, + viewport position: ${((rect.top / window.innerHeight) * 100).toFixed(2)}% from top of viewport`); + }); + + let processedWaypoint = false; + + entries.forEach(entry => { + // Only process entries that are intersecting + if (entry.isIntersecting && !processedWaypoint) { + processedWaypoint = true; + + const waypointIndex = parseInt(entry.target.dataset[DATA_ATTRS.WAYPOINT_INDEX.replace('data-', '')]); + const viewportPosition = ((entry.boundingClientRect.top / window.innerHeight) * 100).toFixed(2); + + this.log(`Waypoint ${waypointIndex} triggered at ${viewportPosition}% from viewport top`); + + // Check if this is a different waypoint than the one currently animating + if (waypointIndex !== this.currentScrollIndex && waypointIndex !== this.currentlyAnimatingToIndex) { + // If previous animation is in progress, cancel it + if (this.animationInProgress) { + this.cancelActiveAnimations(); + this.animationInProgress = false; + this.currentlyAnimatingToIndex = null; + } + + // Update current index and prevent bounce-back + this.currentScrollIndex = waypointIndex; + this.lastWaypointTime = Date.now(); + + // IMPORTANT: Log the current scroll position for comparison + const section = this.element.closest(SELECTORS.SECTION_CONTAINER); + if (section) { + const sectionRect = section.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const scrollProgress = Math.max(0, Math.min(1, -sectionRect.top / (sectionRect.height - viewportHeight))); + this.log(`At waypoint trigger, scroll progress = ${scrollProgress.toFixed(4)}`); + } + + this.navigateToSlide(waypointIndex); + } else { + this.log(`Ignoring waypoint ${waypointIndex} - already current or animating`); + } + } + }); + }, observerSettings); + + // Observe all waypoints + this.waypoints.forEach(waypoint => { + this.observer.observe(waypoint); + this.log(`Observing waypoint ${waypoint.dataset[DATA_ATTRS.WAYPOINT_INDEX.replace('data-', '')]}`); + }); + + // Add throttled scroll handler for smoother transitions between waypoints + this.lastScrollTime = Date.now(); + window.addEventListener('scroll', this.handleScroll.bind(this)); + } + + /** + * Throttled scroll handler for smooth progress between waypoints + */ + handleScroll() { + // Skip if locked or animation in progress + if (this.scrollLocked || this.scrollHandlerDisabled) return; + + // Apply throttling + const now = Date.now(); + if (now - this.lastScrollTime < this.config.SCROLL_THROTTLE) return; + this.lastScrollTime = now; + + // Calculate scroll progress within section + const section = this.element.closest(SELECTORS.SECTION_CONTAINER); + if (!section) return; + + const sectionRect = section.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + // Calculate how far we've scrolled into the section + const scrollProgress = Math.max(0, Math.min(1, -sectionRect.top / (sectionRect.height - viewportHeight))); + + // DEBUGGING: Log more detailed scroll information + this.log(`Scroll handler: + section rect: top=${sectionRect.top}, bottom=${sectionRect.bottom}, height=${sectionRect.height} + viewport height: ${viewportHeight} + raw progress: ${(-sectionRect.top / (sectionRect.height - viewportHeight)).toFixed(4)} + clamped progress: ${scrollProgress.toFixed(4)} + viewport %: ${((Math.abs(sectionRect.top) / viewportHeight) * 100).toFixed(2)}% scrolled into section`); + + // Check if last animation just completed (happens when animation completes and scroll handling re-enables) + const timeSinceAnimationComplete = now - (this.lastAnimationCompleteTime || 0); + if (timeSinceAnimationComplete < this.config.WAYPOINT_COOLDOWN) { + this.log(`Recent animation completed ${timeSinceAnimationComplete}ms ago, + at index ${this.currentScrollIndex}, current progress: ${scrollProgress.toFixed(4)}`); + + // If we just completed an animation and scroll progress is close to a waypoint, + // don't immediately override it with a different slide + if (this.lastCompletedSlideIndex !== undefined) { + // Calculate what the "expected" scroll progress should be for this slide + const expectedProgress = this.lastCompletedSlideIndex / (this.indicators.length - 1); + const progressDifference = Math.abs(scrollProgress - expectedProgress); + + this.log(`Progress difference: ${progressDifference.toFixed(4)}, + expected: ${expectedProgress.toFixed(4)}, + actual: ${scrollProgress.toFixed(4)}`); + + // If we're reasonably close to where we should be, don't override + if (progressDifference < 0.2) { + this.log(`Skipping scroll handler shortly after animation - progress is close enough`); + return; + } + } + } + + // Map progress to slide index + const numSlides = this.indicators.length; + const targetIndex = Math.min(Math.floor(scrollProgress * numSlides), numSlides - 1); + + // Add hysteresis for backward movement to prevent bounce-back + if (targetIndex < this.currentScrollIndex) { + const hysteresisThreshold = this.config.SCROLL_HYSTERESIS; + const requiredProgress = (targetIndex / numSlides) + hysteresisThreshold; + + if (scrollProgress > requiredProgress) { + this.log(`Ignoring small backward movement: ${scrollProgress.toFixed(4)} > ${requiredProgress.toFixed(4)}`); + return; + } + } + + // Log current position for debugging + this.log(`Scroll progress: ${scrollProgress.toFixed(4)}, mapped to slide ${targetIndex}, current slide: ${this.currentScrollIndex}`); + + // Only update if we're moving to a new slide AND not currently animating to this index + if (targetIndex !== this.currentScrollIndex && + targetIndex !== this.currentlyAnimatingToIndex) { + + this.log(`Scroll handler triggering slide ${targetIndex} from ${this.currentScrollIndex}`); + + // Check time since last waypoint trigger + const timeSinceWaypoint = now - (this.lastWaypointTime || 0); + this.log(`Time since last waypoint: ${timeSinceWaypoint}ms`); + + // If a waypoint was recently triggered, don't override it + if (timeSinceWaypoint < this.config.WAYPOINT_COOLDOWN) { + this.log(`Skipping scroll handler - waypoint was triggered recently (${timeSinceWaypoint}ms ago)`); + return; + } + + if (this.animationInProgress) { + this.cancelActiveAnimations(); + this.animationInProgress = false; + this.currentlyAnimatingToIndex = null; + } + + this.currentScrollIndex = targetIndex; + this.navigateToSlide(targetIndex); + } + } + + /** + * Navigate to a specific slide by index + * @param {number} index - The zero-based index of the slide to navigate to + */ + navigateToSlide(index) { + if (index < 0 || index >= this.indicators.length) { + this.log(`Invalid slide index: ${index}`); + return; + } + + // Check if we're already animating to this index to prevent duplicate animations + if (this.currentlyAnimatingToIndex === index) { + this.log(`Already animating to slide ${index}, ignoring duplicate request`); + return; + } + + // Determine direction of navigation + const direction = index > this.currentScrollIndex ? 'forward' : 'backward'; + + // If we're currently animating to a different index and a new animation is requested + if (this.animationInProgress) { + // If we changed direction while animating, ignore the request to prevent bounce-back + if (this.lastNavigationDirection && direction !== this.lastNavigationDirection) { + this.log(`Ignoring ${direction} navigation during ${this.lastNavigationDirection} animation`); + return; + } + + this.log(`Animation in progress to slide ${this.currentlyAnimatingToIndex}, queueing slide ${index}`); + this.pendingAnimationIndex = index; + return; + } + + this.lastNavigationDirection = direction; + this.currentlyAnimatingToIndex = index; + const indicator = this.indicators[index]; + this.handleIndicatorClick(indicator, true); + } + + /** + * Handle click on an indicator + * @param {HTMLElement} indicator - The clicked indicator + * @param {boolean} fromScroll - Whether this was triggered by scroll (true) or click (false) + */ + handleIndicatorClick(indicator, fromScroll = false) { + let isActive = indicator.querySelector(SELECTORS.INDICATOR_ACTIVE).classList.contains(SELECTORS.ACTIVE_STATE); if (isActive) return; + + // Track the request + const requestedStep = parseInt(indicator.getAttribute(DATA_ATTRS.STEP_INDICATOR)); + this.log(`Indicator click for step ${requestedStep}, fromScroll: ${fromScroll}`); + + // If this was triggered by a click (not scroll), temporarily lock scroll navigation + if (!fromScroll) { + // Kill any ongoing animations first + this.cancelActiveAnimations(); + + this.scrollLocked = true; + this.log('Scroll navigation locked due to click'); + + // Scroll to appropriate position + const newStep = parseInt(indicator.getAttribute(DATA_ATTRS.STEP_INDICATOR)); + const section = this.element.closest(SELECTORS.SECTION_CONTAINER); + + if (section) { + const sectionRect = section.getBoundingClientRect(); + const sectionHeight = sectionRect.height; + const sectionTop = window.scrollY + sectionRect.top; + const slidePercentage = (newStep - 1) / (this.indicators.length - 1); + const targetScrollPosition = sectionTop + (slidePercentage * (sectionHeight - window.innerHeight)); + + window.scrollTo({ + top: targetScrollPosition, + behavior: 'smooth' + }); + } + + // Unlock scroll navigation after animation completes + setTimeout(() => { + this.scrollLocked = false; + this.currentScrollIndex = parseInt(indicator.getAttribute(DATA_ATTRS.STEP_INDICATOR)) - 1; + this.log('Scroll navigation unlocked'); + }, this.config.CLICK_LOCK_DURATION); + } // store last active indicator - let prevActiveIndicator = this.element.querySelector('.tds-indicators .tds--active'); - let prevActiveStep = +prevActiveIndicator.parentElement.parentElement.getAttribute(STEP_ATTR_INDICATOR); + let prevActiveIndicator = this.element.querySelector(`${SELECTORS.INDICATORS_CONTAINER} .${SELECTORS.ACTIVE_STATE}`); + let prevActiveStep = +prevActiveIndicator.parentElement.parentElement.getAttribute(DATA_ATTRS.STEP_INDICATOR); // update active indicator - this.indicators.forEach(ind => ind.querySelector('.tdsi-active').classList.remove('tds--active')); - let currentActiveIndicator = indicator.querySelector('.tdsi-active'); - currentActiveIndicator.classList.remove('tds--back'); - currentActiveIndicator.classList.remove('tds--forward'); - currentActiveIndicator.classList.add('tds--active'); + this.indicators.forEach(ind => ind.querySelector(SELECTORS.INDICATOR_ACTIVE).classList.remove(SELECTORS.ACTIVE_STATE)); + let currentActiveIndicator = indicator.querySelector(SELECTORS.INDICATOR_ACTIVE); + currentActiveIndicator.classList.remove(SELECTORS.BACK_STATE); + currentActiveIndicator.classList.remove(SELECTORS.FORWARD_STATE); + currentActiveIndicator.classList.add(SELECTORS.ACTIVE_STATE); // get updated list of slides - let liveSlides = this.element.querySelectorAll('.tds-slides .tds-slide'); + let liveSlides = this.element.querySelectorAll(`${SELECTORS.SLIDES_CONTAINER} ${SELECTORS.SLIDE}`); liveSlides = Array.from(liveSlides).reverse(); // get new active step number & index - let newActiveStep = +indicator.getAttribute(STEP_ATTR_INDICATOR); - let activeSlideIndex = Array.from(liveSlides).findIndex((slide) => newActiveStep === +slide.getAttribute(STEP_ATTR_SLIDE)); + let newActiveStep = +indicator.getAttribute(DATA_ATTRS.STEP_INDICATOR); + let activeSlideIndex = Array.from(liveSlides).findIndex((slide) => newActiveStep === +slide.getAttribute(DATA_ATTRS.STEP_SLIDE)); let movingForward = newActiveStep > prevActiveStep; if (movingForward) { - prevActiveIndicator.classList.add('tds--forward'); + prevActiveIndicator.classList.add(SELECTORS.FORWARD_STATE); } else { - prevActiveIndicator.classList.add('tds--back'); + prevActiveIndicator.classList.add(SELECTORS.BACK_STATE); } // animate all slides @@ -94,7 +500,7 @@ export default class ThreeDSlider { } handlePrevClick(btn) { - let currentStep = +btn.getAttribute(STEP_ATTR_SLIDE); + let currentStep = +btn.getAttribute(DATA_ATTRS.STEP_SLIDE); let prevStep = currentStep - 1; if (prevStep < 1) return; @@ -104,7 +510,7 @@ export default class ThreeDSlider { } handleNextClick(btn) { - let currentStep = +btn.getAttribute(STEP_ATTR_SLIDE); + let currentStep = +btn.getAttribute(DATA_ATTRS.STEP_SLIDE); let nextStep = currentStep + 1; if (nextStep > this.slides.length) return; @@ -114,34 +520,152 @@ export default class ThreeDSlider { } animateSlides(liveSlides, activeSlideIndex, movingForward) { + // Mark animation as in progress + this.animationInProgress = true; + + // Disable scroll handling during animation to prevent bounce-back + this.scrollHandlerDisabled = true; + + this.log(`Starting animation to slide index ${activeSlideIndex}`); + + // Cancel all active animations first + this.cancelActiveAnimations(); + + // Store the new animations + this.activeAnimations = []; + + // Determine the max animation duration for completion tracking + let maxDuration = 0; + liveSlides.forEach((slide, slideIndex) => { let offset = slideIndex - activeSlideIndex; - let delay = movingForward ? (slideIndex * 0.1) : ((this.slides.length - slideIndex - 1) * 0.1); - + // Reduce delays for smoother rapid transitions + let delay = movingForward + ? (slideIndex * this.config.DELAY_MULTIPLIER) + : ((this.slides.length - slideIndex - 1) * this.config.DELAY_MULTIPLIER); + + // Always ensure slides are visible initially gsap.set(slide, { display: 'flex' }); + let animation; + if (offset === 0) { // active slide - gsap.to(slide, { z: 0, y: 0, opacity: 1, ease: EASE_FUNCTION, duration: 0.7, delay }); + animation = gsap.to(slide, { + z: 0, + y: 0, + opacity: 1, + ease: this.config.EASE_FUNCTION, + duration: this.config.ACTIVE_SLIDE_DURATION, + delay + }); + + maxDuration = Math.max(maxDuration, this.config.ACTIVE_SLIDE_DURATION + delay); } else { // non-active slides // offset > 0 ? behind active : in front of active slide - let z = offset > 0 ? offset * -50 : null; - let y = offset > 0 ? offset * -25 : offset * -100; - let opacity = offset > 0 ? (0.7 - (offset * 0.1)) : 0; - let duration = offset > 0 ? 0.7 : 0.5; - + let z = offset > 0 ? offset * this.config.Z_OFFSET_PER_SLIDE : null; + let y = offset > 0 ? offset * this.config.Y_OFFSET_BEHIND : this.config.Y_OFFSET_FRONT; + let opacity = offset > 0 ? (this.config.OPACITY_BASE - (offset * this.config.OPACITY_DECREMENT)) : 0; + let duration = offset > 0 + ? this.config.INACTIVE_SLIDE_DURATION_BEHIND + : this.config.INACTIVE_SLIDE_DURATION_FRONT; + // slides that animate offscreen - gsap.to(slide, { z, y, opacity, duration, delay, ease: EASE_FUNCTION, - onComplete: offset > 0 ? () => {} : () => { gsap.set(slide, { display: 'none' }); } + animation = gsap.to(slide, { + z, + y, + opacity, + duration, + delay, + ease: this.config.EASE_FUNCTION, + onComplete: offset > 0 ? () => {} : () => { + gsap.set(slide, { display: 'none' }); + } }); + + maxDuration = Math.max(maxDuration, duration + delay); } + + // Store the animation for later cancellation if needed + this.activeAnimations.push(animation); }); + + // Set a timeout to mark animation as complete + setTimeout(() => { + this.animationInProgress = false; + this.currentlyAnimatingToIndex = null; // Clear the animating index + + // Store the current scroll position when animation completes + const section = this.element.closest(SELECTORS.SECTION_CONTAINER); + + // Initialize these variables outside the conditional block + let scrollProgress = 0; + let currentScrollPosition = 0; + const currentSlide = activeSlideIndex; + + if (section) { + const sectionRect = section.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + currentScrollPosition = sectionRect.top; + + // Log the scroll position at animation completion + scrollProgress = Math.max(0, Math.min(1, -sectionRect.top / (sectionRect.height - viewportHeight))); + + this.log(`Animation complete: slide ${currentSlide}, scroll position ${currentScrollPosition}, + scroll progress ${scrollProgress.toFixed(4)}`); + } + + // Record animation completion time and slide index for bounce-back prevention + this.lastAnimationCompleteTime = Date.now(); + this.lastCompletedSlideIndex = currentSlide; + + this.log('Animation sequence complete'); + + // Add a longer delay before re-enabling scroll handling + // This prevents scroll measurements from happening during animation settling + setTimeout(() => { + // Get current scroll position + const newScrollPosition = section ? section.getBoundingClientRect().top : 0; + const scrollDelta = Math.abs(newScrollPosition - currentScrollPosition); + + // Set important time marker for bounce-back prevention + this.lastWaypointTime = Date.now(); + + // Re-enable scroll handling + this.scrollHandlerDisabled = false; + + this.log(`Scroll handling re-enabled after animation, scroll delta: ${scrollDelta}px`); + }, this.config.ANIMATION_COOLDOWN); // Delay to ensure stability + + // Process any pending animations that came in while we were animating + if (this.pendingAnimationIndex !== null) { + const pendingIndex = this.pendingAnimationIndex; + this.pendingAnimationIndex = null; + this.log(`Processing pending animation to slide ${pendingIndex}`); + this.navigateToSlide(pendingIndex); + } + }, maxDuration * 1000 + 50); // Add a small buffer + } + + /** + * Cancel any currently active animations + */ + cancelActiveAnimations() { + if (this.activeAnimations.length > 0) { + this.log(`Cancelling ${this.activeAnimations.length} active animations`); + this.activeAnimations.forEach(animation => { + if (animation && animation.kill) { + animation.kill(); + } + }); + this.activeAnimations = []; + } } updateButtonStates() { this.prevBtns.forEach((btn) => { - let currentStep = +btn.getAttribute(STEP_ATTR_SLIDE); + let currentStep = +btn.getAttribute(DATA_ATTRS.STEP_SLIDE); if (currentStep === 1) { btn.disabled = true; btn.style.opacity = 0.5; @@ -152,7 +676,7 @@ export default class ThreeDSlider { }); this.nextBtns.forEach((btn) => { - let currentStep = +btn.getAttribute(STEP_ATTR_SLIDE); + let currentStep = +btn.getAttribute(DATA_ATTRS.STEP_SLIDE); if (currentStep === this.slides.length) { btn.disabled = true; btn.style.opacity = 0.5; diff --git a/project/src/pages/modules.js b/project/src/pages/modules.js new file mode 100644 index 0000000..45145c4 --- /dev/null +++ b/project/src/pages/modules.js @@ -0,0 +1,9 @@ +import { onReady } from '../utils/helpers.js'; +import ContactForm from '../modules/contact-form/contact-form.js'; +import CommandCenterGraphic from '../modules/command-center-graphic/command-center-graphic.js'; + + +onReady(() => { + new ContactForm('contact-form', true); + new CommandCenterGraphic('cc-blocks-container', true); +}); \ No newline at end of file diff --git a/project/src/pages/product.js b/project/src/pages/product.js index 2d2f131..63dd103 100644 --- a/project/src/pages/product.js +++ b/project/src/pages/product.js @@ -13,7 +13,7 @@ onReady(() => { new ContactForm('contact-form', true); new FullerAngleWatcher('.card'); new TextAnimateSections(['first-reveal', 'second-reveal', 'third-reveal', 'home-os', 'prototype-section', 'future-section']); - new ThreeDSlider('process-slider'); + new ThreeDSlider('process-slider', true); new FutureGallery('future-gallery', true); new Slider('slider-wrapper-2', { animationDuration: 1, diff --git a/site/content/command-center-graphic.html b/site/content/command-center-graphic.html new file mode 100644 index 0000000..1e99fc9 --- /dev/null +++ b/site/content/command-center-graphic.html @@ -0,0 +1,320 @@ + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+
+ + + +
+
+
+
+ + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+

Our powerful platform

+

Your Digital
Command Center

+
+

+ At the heart of the Fuller System is a powerful platform that ties it all together. From tracking designs + and supplier orders to monitoring factory progress and on-site delivery, the platform gives builders and + developers full visibility and control. +

+

+ By centralizing every detail, it streamlines workflows, ensures accountability, and simplifies even the + most complex projects. +

+
+
+
+
\ No newline at end of file diff --git a/site/export/401.html b/site/export/401.html index 4770bb6..f8929ee 100644 --- a/site/export/401.html +++ b/site/export/401.html @@ -1,4 +1,4 @@ - + @@ -15,8 +15,9 @@ - - + + +
@@ -59,9 +60,9 @@

Protected Page