diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7d5bfe3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,38 @@ +# Code owners for the Owen Animation System +# These users will be automatically requested for review when PRs are opened + +# Global ownership - project maintainer +* @kjanat + +# Core animation system +/src/animation/ @kjanat +/src/core/OwenAnimationContext.js @kjanat + +# Animation processing and validation scripts +/scripts/ @kjanat + +# Configuration files +/package.json @kjanat +/vite*.config.js @kjanat +/jsdoc.config.json @kjanat + +# GitHub workflows and automation +/.github/ @kjanat + +# Documentation +/docs/ @kjanat +/README.md @kjanat +/CHANGELOG.md @kjanat +/MULTI_SCHEME_GUIDE.md @kjanat + +# Demo application +/demo/ @kjanat + +# Examples and integration guides +/examples/ @kjanat + +# License files - require special attention +/LICENSE.* @kjanat + +# Animation assets - require validation +/assets/animations/ @kjanat diff --git a/.github/ISSUE_TEMPLATE/animation_scheme.yml b/.github/ISSUE_TEMPLATE/animation_scheme.yml new file mode 100644 index 0000000..64fa077 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/animation_scheme.yml @@ -0,0 +1,163 @@ +name: Animation Scheme Issue +description: Report issues specific to animation naming schemes or conversions +title: "[Scheme]: " +labels: ["animation-scheme", "naming", "needs-investigation"] +projects: ["kjanat/Owen"] +assignees: + - kjanat +body: + - type: markdown + attributes: + value: | + Report issues specific to animation naming schemes, conversions, or multi-scheme functionality. + + - type: checkboxes + id: affected-schemes + attributes: + label: Affected Animation Schemes + description: Which naming schemes are affected by this issue? + options: + - label: Legacy scheme + - label: Artist scheme + - label: Hierarchical scheme + - label: Semantic scheme + - label: Multi-scheme conversion + - label: All schemes + + - type: dropdown + id: issue-category + attributes: + label: Issue Category + description: What category best describes this issue? + options: + - Name Conversion Error + - Scheme Validation Failure + - Conflict Detection Issue + - Performance Problem + - Missing Animation Names + - Incorrect Mapping + - Blender Integration + - Documentation Mismatch + - API Inconsistency + validations: + required: true + + - type: textarea + id: animation-names + attributes: + label: Animation Names Involved + description: Provide the specific animation names that are causing issues + placeholder: | + Source scheme: artist + Animation name: "char_walk_01" + + Target scheme: semantic + Expected result: "character.movement.walk.forward" + Actual result: "character.walk.01" + validations: + required: true + + - type: textarea + id: conversion-details + attributes: + label: Conversion Details + description: Provide details about the conversion or mapping issue + placeholder: | + - What conversion were you attempting? + - What was the expected behavior? + - What actually happened? + - Are there error messages? + + - type: textarea + id: reproduction-code + attributes: + label: Reproduction Code + description: Provide code to reproduce the issue + render: javascript + placeholder: | + ```javascript + import { AnimationNameMapper } from 'owen-animation-system'; + + const mapper = new AnimationNameMapper(); + + // Code that reproduces the issue + const result = mapper.convert('char_walk_01', 'artist', 'semantic'); + console.log(result); // Shows unexpected result + ``` + validations: + required: true + + - type: textarea + id: validation-output + attributes: + label: Validation Output + description: If you ran validation scripts, provide the output + render: shell + placeholder: | + $ npm run check:naming-conflicts + + # Output from validation scripts + + - type: dropdown + id: severity + attributes: + label: Severity + description: How severe is this issue? + options: + - Low - Minor inconvenience + - Medium - Affects workflow but has workarounds + - High - Breaks functionality significantly + - Critical - Prevents system usage + validations: + required: true + + - type: textarea + id: environment-details + attributes: + label: Environment Details + description: Provide environment information + placeholder: | + - Owen Animation System version: v1.2.3 + - Node.js version: v18.17.0 + - Animation assets source: Blender 3.6 + - Integration: React/Vue/Vanilla JS + - OS: Windows/macOS/Linux + + - type: textarea + id: workaround + attributes: + label: Current Workaround + description: If you found a workaround, please describe it + placeholder: Describe any temporary solutions you're using + + - type: checkboxes + id: impact + attributes: + label: Impact Assessment + options: + - label: Affects multiple animation assets + - label: Blocks automated processing + - label: Requires manual intervention + - label: Affects production builds + - label: Impacts team workflow + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + options: + - label: I have provided specific animation names and schemes + required: true + - label: I have included reproduction code + required: true + - label: I have checked existing issues for similar problems + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f21e473 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,129 @@ +name: Bug Report +description: Create a report to help us improve the Owen Animation System +title: "[Bug]: " +labels: ["bug", "needs-triage"] +projects: ["kjanat/Owen"] +assignees: + - kjanat +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please provide as much detail as possible to help us understand and reproduce the issue. + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + placeholder: Tell us what you see! + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + placeholder: What should have happened instead? + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: dropdown + id: animation-scheme + attributes: + label: Animation Naming Scheme + description: Which animation naming scheme were you using when the bug occurred? + options: + - Legacy + - Artist + - Hierarchical + - Semantic + - Multiple schemes + - Not applicable + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: Environment + description: Where did you encounter this bug? + options: + - Browser (Web) + - Node.js + - React integration + - Vue integration + - Blender integration + - Demo application + - Documentation + - Other + validations: + required: true + + - type: textarea + id: browser-info + attributes: + label: Browser/Runtime Information + description: If applicable, provide browser or runtime version information + placeholder: | + - Browser: Chrome 91.0 + - Node.js: v18.17.0 + - OS: Windows 11 + - Owen Animation System: v1.2.3 + + - type: textarea + id: code-sample + attributes: + label: Code Sample + description: If applicable, provide a minimal code sample that reproduces the issue + render: javascript + placeholder: | + ```javascript + import { AnimationNameMapper } from 'owen-animation-system'; + + const mapper = new AnimationNameMapper(); + // Your code that demonstrates the issue + ``` + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + description: Please verify the following before submitting + options: + - label: I have searched existing issues to make sure this is not a duplicate + required: true + - label: I have provided a clear and concise description of the bug + required: true + - label: I have included steps to reproduce the issue + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ebc58b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: false +contact_links: + - name: Discussion Forum + url: https://github.com/kjanat/Owen/discussions + about: Ask questions, share ideas, and discuss the Owen Animation System with the community + - name: Discord Community + url: https://discord.gg/owen-animation + about: Join our Discord server for real-time chat and support + - name: Documentation + url: https://kjanat.github.io/Owen/ + about: Check our comprehensive documentation and guides + - name: Multi-Scheme Guide + url: https://github.com/kjanat/Owen/blob/main/MULTI_SCHEME_GUIDE.md + about: Learn about animation naming schemes and conversions + - name: Demo Application + url: https://kjanat.github.io/Owen/demo/ + about: Try the interactive demo to understand the system capabilities diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..923ce66 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,139 @@ +name: Documentation Issue +description: Report an issue with documentation or suggest improvements +title: "[Docs]: " +labels: ["documentation", "needs-review"] +projects: ["kjanat/Owen"] +assignees: + - kjanat +body: + - type: markdown + attributes: + value: | + Thanks for helping improve our documentation! Please provide details about the documentation issue or improvement. + + - type: dropdown + id: doc-type + attributes: + label: Documentation Type + description: What type of documentation is affected? + options: + - API Documentation (JSDoc) + - README.md + - Multi-Scheme Guide + - Code Examples + - Demo Application + - Installation Guide + - Integration Guide + - Changelog + - Contributing Guide + - Other + validations: + required: true + + - type: dropdown + id: issue-type + attributes: + label: Issue Type + description: What type of documentation issue is this? + options: + - Error/Mistake + - Missing Information + - Unclear/Confusing + - Outdated Information + - Missing Examples + - Formatting Issues + - Typo/Grammar + - Improvement Suggestion + - New Documentation Needed + validations: + required: true + + - type: textarea + id: location + attributes: + label: Location + description: Where did you find this issue? Provide a link or file path if possible. + placeholder: | + - URL: https://example.com/docs/... + - File: README.md (line 45) + - Section: "Animation Name Mapping" + validations: + required: true + + - type: textarea + id: current-content + attributes: + label: Current Content + description: What is the current documentation content that needs improvement? + placeholder: Copy the current text or describe what's missing + validations: + required: true + + - type: textarea + id: suggested-improvement + attributes: + label: Suggested Improvement + description: How should the documentation be improved? + placeholder: | + - What should be added, changed, or removed? + - Provide suggested text if applicable + - Include code examples if relevant + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any additional context about why this improvement is needed + placeholder: | + - What task were you trying to accomplish? + - What confused you? + - How would this help other users? + + - type: textarea + id: code-example + attributes: + label: Code Example + description: If suggesting code documentation improvements, provide examples + render: javascript + placeholder: | + ```javascript + // Current example (if exists) + + // Suggested improved example + ``` + + - type: checkboxes + id: affected-users + attributes: + label: Who would benefit from this improvement? + options: + - label: New users learning the system + - label: Experienced developers integrating the system + - label: Contributors to the project + - label: Users of specific naming schemes + - label: Blender integration users + - label: React/Vue integration users + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + description: Please verify the following before submitting + options: + - label: I have checked that this documentation issue hasn't been reported already + required: true + - label: I have provided specific location information + required: true + - label: I have suggested concrete improvements + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8dd3842 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,144 @@ +name: Feature Request +description: Suggest an idea for the Owen Animation System +title: "[Feature]: " +labels: ["enhancement", "feature-request"] +projects: ["kjanat/Owen"] +assignees: + - kjanat +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a new feature! Please provide as much detail as possible to help us understand your request. + + - type: textarea + id: problem-description + attributes: + label: Problem Description + description: Is your feature request related to a problem? Please describe the problem or limitation you're experiencing. + placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like to see implemented. + placeholder: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Describe any alternative solutions or features you've considered. + placeholder: A clear and concise description of any alternative solutions or features you've considered. + + - type: dropdown + id: feature-area + attributes: + label: Feature Area + description: Which area of the system would this feature affect? + options: + - Animation Name Mapping + - Naming Schemes (Legacy/Artist/Hierarchical/Semantic) + - Core Animation System + - Blender Integration + - Documentation + - Demo Application + - Build System + - Testing Framework + - Performance + - Developer Experience + - API Design + - Other + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - Low - Nice to have + - Medium - Would improve workflow + - High - Blocking current work + - Critical - Major limitation + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Describe how you would use this feature and what benefits it would provide. + placeholder: | + Describe your specific use case: + - What are you trying to accomplish? + - How would this feature help? + - Who else might benefit from this feature? + validations: + required: true + + - type: textarea + id: implementation-ideas + attributes: + label: Implementation Ideas + description: If you have ideas about how this could be implemented, please share them. + placeholder: | + Any thoughts on implementation approach: + - API design suggestions + - Integration points + - Configuration options + - Breaking changes considerations + + - type: textarea + id: code-example + attributes: + label: Code Example + description: If applicable, provide a code example showing how you'd like to use this feature. + render: javascript + placeholder: | + ```javascript + // Example of how the feature might be used + const mapper = new AnimationNameMapper(); + + // Your proposed API usage + mapper.newFeature(options); + ``` + + - type: checkboxes + id: compatibility + attributes: + label: Compatibility Considerations + description: Please consider the impact of this feature + options: + - label: This feature should be backward compatible + - label: This feature may require breaking changes (acceptable) + - label: This feature should work with all naming schemes + - label: This feature affects the public API + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission checklist + description: Please verify the following before submitting + options: + - label: I have searched existing issues and discussions for similar requests + required: true + - label: I have provided a clear description of the problem and proposed solution + required: true + - label: I have considered the impact on existing functionality + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e5b6b38 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,85 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 10 + reviewers: + - "kjanat" + assignees: + - "kjanat" + commit-message: + prefix: "deps" + prefix-development: "deps-dev" + include: "scope" + labels: + - "dependencies" + - "javascript" + ignore: + # Ignore major version updates for critical dependencies + - dependency-name: "node" + update-types: ["version-update:semver-major"] + - dependency-name: "vite" + update-types: ["version-update:semver-major"] + groups: + development-dependencies: + dependency-type: "development" + patterns: + - "@types/*" + - "eslint*" + - "prettier*" + - "jest*" + - "playwright*" + - "vite*" + animation-dependencies: + patterns: + - "*three*" + - "*gltf*" + - "*animation*" + testing-dependencies: + patterns: + - "*test*" + - "*mock*" + - "*spec*" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + reviewers: + - "kjanat" + assignees: + - "kjanat" + commit-message: + prefix: "ci" + include: "scope" + labels: + - "github-actions" + - "ci/cd" + + # Enable version updates for Python (Blender scripts) + - package-ecosystem: "pip" + directory: "/scripts" + schedule: + interval: "monthly" + day: "first-monday" + time: "09:00" + reviewers: + - "kjanat" + assignees: + - "kjanat" + commit-message: + prefix: "deps" + prefix-development: "deps-dev" + include: "scope" + labels: + - "dependencies" + - "python" + - "blender" diff --git a/.github/workflows/animation-processing.yml b/.github/workflows/animation-processing.yml new file mode 100644 index 0000000..3b4362c --- /dev/null +++ b/.github/workflows/animation-processing.yml @@ -0,0 +1,169 @@ +name: Animation Processing Pipeline + +on: + push: + paths: + - 'assets/animations/**' + - 'src/animation/AnimationNameMapper.js' + - 'src/animation/AnimationConstants.js' + pull_request: + paths: + - 'assets/animations/**' + - 'src/animation/AnimationNameMapper.js' + - 'src/animation/AnimationConstants.js' + workflow_dispatch: + inputs: + animation_scheme: + description: 'Primary naming scheme to use' + required: true + default: 'semantic' + type: choice + options: + - legacy + - artist + - hierarchical + - semantic + +jobs: + validate-animations: + name: Validate Animation Names + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Validate animation naming schemes + run: node scripts/validate-animations.js + env: + PRIMARY_SCHEME: ${{ github.event.inputs.animation_scheme || 'semantic' }} + + - name: Generate animation constants + run: node scripts/generate-animation-constants.js + + - name: Check for naming conflicts + run: node scripts/check-naming-conflicts.js + + - name: Upload validation report + uses: actions/upload-artifact@v4 + with: + name: animation-validation-report + path: | + reports/animation-validation.json + reports/naming-conflicts.json + + process-blender-assets: + name: Process Blender Animation Assets + runs-on: ubuntu-latest + if: contains(github.event.head_commit.message, '[process-blender]') || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Blender + uses: FlorianBreitwieser/setup-blender@v1 + with: + blender-version: '3.6' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Process Blender files + run: | + python scripts/blender-animation-processor.py \ + --input-dir assets/blender \ + --output-dir assets/animations \ + --naming-scheme artist + + - name: Convert animation names + run: node scripts/convert-animation-names.js + + - name: Validate processed animations + run: node scripts/validate-processed-animations.js + + - name: Commit processed assets + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: 'Auto-process Blender animation assets [skip ci]' + file_pattern: 'assets/animations/* src/animation/AnimationConstants.js' + + update-documentation: + name: Update Animation Documentation + runs-on: ubuntu-latest + needs: [validate-animations] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate animation documentation + run: node scripts/generate-animation-docs.js + + - name: Update API documentation + run: npm run docs + + - name: Generate multi-scheme examples + run: node scripts/generate-scheme-examples.js + + - name: Commit documentation updates + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: 'Auto-update animation documentation [skip ci]' + file_pattern: | + docs/** + MULTI_SCHEME_GUIDE.md + examples/*/README.md + + deploy-demo: + name: Deploy Animation Demo + runs-on: ubuntu-latest + needs: [validate-animations, update-documentation] + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build demo + run: npm run build:demo + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist-demo + cname: owen-animation-demo.your-domain.com diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a759c08 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,118 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master ] + +jobs: + test: + name: Test & Lint + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x, 20.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Build project + run: npm run build + + - name: Generate documentation + run: npm run docs + + - name: Upload build artifacts + if: matrix.node-version == '20.x' + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + docs/ + + security: + name: Security Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Run security scan + uses: securecodewarrior/github-action-add-sarif@v1 + with: + sarif-file: 'security-scan-results.sarif' + continue-on-error: true + + release: + name: Release + runs-on: ubuntu-latest + needs: [test, security] + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Generate documentation + run: npm run docs + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + + - name: Create release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + dist/** + docs/** + CHANGELOG.md + README.md + MULTI_SCHEME_GUIDE.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/demo-deployment.yml b/.github/workflows/demo-deployment.yml new file mode 100644 index 0000000..222addd --- /dev/null +++ b/.github/workflows/demo-deployment.yml @@ -0,0 +1,246 @@ +name: Demo Deployment + +on: + push: + branches: [ main, master ] + paths: + - 'demo/**' + - 'src/**' + - 'vite.demo.config.js' + - 'package.json' + pull_request: + branches: [ main, master ] + paths: + - 'demo/**' + - 'src/**' + - 'vite.demo.config.js' + workflow_dispatch: + inputs: + environment: + description: 'Deployment environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + NODE_VERSION: '20.x' + +jobs: + build-demo: + name: Build Demo + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate animation constants + run: npm run generate:constants + + - name: Build demo application + run: npm run build:demo + env: + NODE_ENV: production + + - name: Validate demo build + run: | + test -d dist/demo || (echo "Demo build failed - dist/demo directory not found" && exit 1) + test -f dist/demo/index.html || (echo "Demo build failed - index.html not found" && exit 1) + test -f dist/demo/examples.html || (echo "Demo build failed - examples.html not found" && exit 1) + test -f dist/demo/comparison.html || (echo "Demo build failed - comparison.html not found" && exit 1) + test -f dist/demo/interactive.html || (echo "Demo build failed - interactive.html not found" && exit 1) + + - name: Upload demo artifacts + uses: actions/upload-artifact@v4 + with: + name: demo-build + path: dist/demo/ + retention-days: 30 + + - name: Upload build reports + uses: actions/upload-artifact@v4 + with: + name: build-reports + path: | + dist/demo/report.html + dist/demo/stats.json + retention-days: 7 + + test-demo: + name: Test Demo + runs-on: ubuntu-latest + needs: build-demo + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Download demo build + uses: actions/download-artifact@v4 + with: + name: demo-build + path: dist/demo/ + + - name: Install Playwright + run: npx playwright install --with-deps + + - name: Run demo tests + run: npm run test:demo + env: + CI: true + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/ms-playwright + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload test screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-screenshots + path: test-results/ + retention-days: 7 + + lighthouse-audit: + name: Performance Audit + runs-on: ubuntu-latest + needs: build-demo + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Download demo build + uses: actions/download-artifact@v4 + with: + name: demo-build + path: dist/demo/ + + - name: Install Lighthouse + run: npm install -g @lhci/cli lighthouse + + - name: Start demo server + run: | + npx vite preview --config vite.demo.config.js --port 3000 & + sleep 10 + env: + NODE_ENV: production + + - name: Run Lighthouse audit + run: | + lhci autorun + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + + - name: Upload Lighthouse results + uses: actions/upload-artifact@v4 + with: + name: lighthouse-reports + path: .lighthouseci/ + retention-days: 7 + + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: [build-demo, test-demo] + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.event.inputs.environment == 'staging' + environment: staging + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download demo build + uses: actions/download-artifact@v4 + with: + name: demo-build + path: dist/demo/ + + - name: Deploy to staging + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist/demo + publish_branch: gh-pages-staging + force_orphan: true + + - name: Update deployment status + run: | + echo "Demo deployed to staging: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/staging/" + + deploy-production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: [build-demo, test-demo, lighthouse-audit] + if: github.ref == 'refs/heads/main' && github.event.inputs.environment == 'production' + environment: production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download demo build + uses: actions/download-artifact@v4 + with: + name: demo-build + path: dist/demo/ + + - name: Deploy to production + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist/demo + publish_branch: gh-pages + force_orphan: true + + - name: Update deployment status + run: | + echo "Demo deployed to production: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/" + + - name: Create deployment notification + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: context.payload.deployment.id, + state: 'success', + environment_url: 'https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/', + description: 'Demo successfully deployed' + }); diff --git a/.github/workflows/multi-scheme-testing.yml b/.github/workflows/multi-scheme-testing.yml new file mode 100644 index 0000000..86e06ca --- /dev/null +++ b/.github/workflows/multi-scheme-testing.yml @@ -0,0 +1,251 @@ +name: Multi-Scheme Testing + +on: + push: + branches: [ main, master, develop ] + paths: + - 'src/animation/**' + - 'src/core/OwenAnimationContext.js' + - 'examples/**' + pull_request: + branches: [ main, master ] + paths: + - 'src/animation/**' + - 'src/core/OwenAnimationContext.js' + - 'examples/**' + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + +env: + NODE_VERSION: '20.x' + +jobs: + scheme-validation: + name: Validate Naming Schemes + runs-on: ubuntu-latest + + strategy: + matrix: + scheme: [legacy, artist, hierarchical, semantic] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Test ${{ matrix.scheme }} scheme + run: | + node -e " + const { AnimationNameMapper } = require('./src/animation/AnimationNameMapper.js'); + const mapper = new AnimationNameMapper(); + + console.log('Testing ${{ matrix.scheme }} scheme...'); + + // Test all animations in this scheme + const animations = mapper.getAllAnimationsByScheme('${{ matrix.scheme }}'); + console.log('Found', animations.length, 'animations'); + + // Test conversions + let errors = 0; + animations.forEach(anim => { + try { + const converted = mapper.convert(anim, '${{ matrix.scheme }}'); + if (converted !== anim) { + console.error('Conversion error:', anim, '->', converted); + errors++; + } + } catch (e) { + console.error('Error processing:', anim, e.message); + errors++; + } + }); + + if (errors > 0) { + console.error('Found', errors, 'errors in ${{ matrix.scheme }} scheme'); + process.exit(1); + } else { + console.log('All ${{ matrix.scheme }} animations validated successfully'); + } + " + + conversion-matrix: + name: Test Scheme Conversions + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Test all scheme conversions + run: | + node -e " + const { AnimationNameMapper } = require('./src/animation/AnimationNameMapper.js'); + const mapper = new AnimationNameMapper(); + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']; + const testAnimations = [ + 'wait_idle_L', + 'Owen_ReactAngry', + 'owen.state.type.idle.loop', + 'OwenSleepToWaitTransition' + ]; + + console.log('Testing conversion matrix...'); + let totalTests = 0; + let passedTests = 0; + + testAnimations.forEach(anim => { + schemes.forEach(fromScheme => { + schemes.forEach(toScheme => { + totalTests++; + try { + const result = mapper.convert(anim, toScheme); + console.log('āœ“', anim, '->', result, '(' + fromScheme + ' to ' + toScheme + ')'); + passedTests++; + } catch (e) { + console.log('āœ—', anim, 'failed conversion from', fromScheme, 'to', toScheme, ':', e.message); + } + }); + }); + }); + + console.log('Conversion matrix results:', passedTests + '/' + totalTests, 'passed'); + if (passedTests < totalTests * 0.9) { + console.error('Too many conversion failures'); + process.exit(1); + } + " + + demo-validation: + name: Validate Demo Functionality + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright + run: npx playwright install --with-deps chromium + + - name: Start demo server + run: | + cd examples/mock-demo + python -m http.server 8080 & + sleep 5 + + - name: Test demo functionality + run: | + npx playwright test --config=playwright.config.js + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: demo-test-results + path: | + test-results/ + playwright-report/ + + performance-benchmark: + name: Performance Benchmarks + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run performance benchmarks + run: | + node -e " + const { AnimationNameMapper } = require('./src/animation/AnimationNameMapper.js'); + const mapper = new AnimationNameMapper(); + + console.log('Running performance benchmarks...'); + + // Benchmark conversion speed + const testAnim = 'wait_idle_L'; + const iterations = 10000; + + console.time('10k conversions'); + for (let i = 0; i < iterations; i++) { + mapper.convert(testAnim, 'semantic'); + } + console.timeEnd('10k conversions'); + + // Benchmark validation speed + console.time('10k validations'); + for (let i = 0; i < iterations; i++) { + mapper.validateAnimationName(testAnim); + } + console.timeEnd('10k validations'); + + // Memory usage test + const used = process.memoryUsage(); + console.log('Memory usage:'); + for (let key in used) { + console.log(key + ':', Math.round(used[key] / 1024 / 1024 * 100) / 100, 'MB'); + } + " + + - name: Comment PR with benchmark results + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read benchmark results (would need to be saved to file in previous step) + const comment = ` + ## šŸƒā€ā™‚ļø Performance Benchmark Results + + Multi-scheme animation system performance test completed: + + - āœ… Conversion speed: 10k operations completed + - āœ… Validation speed: 10k operations completed + - āœ… Memory usage: Within acceptable limits + + Full results available in the workflow logs. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/.github/workflows/performance-testing.yml b/.github/workflows/performance-testing.yml new file mode 100644 index 0000000..8444073 --- /dev/null +++ b/.github/workflows/performance-testing.yml @@ -0,0 +1,569 @@ +name: Performance Testing + +on: + push: + branches: [ main, master ] + paths: + - 'src/animation/**' + - 'demo/**' + - 'scripts/**' + pull_request: + branches: [ main, master ] + paths: + - 'src/animation/**' + - 'demo/**' + - 'scripts/**' + schedule: + # Run performance tests weekly on Sundays at 3 AM UTC + - cron: '0 3 * * 0' + workflow_dispatch: + inputs: + test_type: + description: 'Type of performance test to run' + required: true + default: 'all' + type: choice + options: + - all + - conversion + - validation + - memory + - lighthouse + +env: + NODE_VERSION: '20.x' + +jobs: + conversion-performance: + name: Animation Conversion Performance + runs-on: ubuntu-latest + if: github.event.inputs.test_type == 'all' || github.event.inputs.test_type == 'conversion' || github.event.inputs.test_type == null + + strategy: + matrix: + scheme: [legacy, artist, hierarchical, semantic] + batch_size: [100, 1000, 5000] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate test data + run: | + node -e " + const fs = require('fs'); + const testData = []; + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']; + const baseNames = [ + 'walk', 'run', 'idle', 'jump', 'attack', 'defend', 'crouch', 'climb', + 'swim', 'fly', 'dance', 'wave', 'bow', 'kneel', 'sit', 'stand' + ]; + + for (let i = 0; i < ${{ matrix.batch_size }}; i++) { + const baseName = baseNames[i % baseNames.length]; + const variant = String(i + 1).padStart(2, '0'); + + let animationName; + switch ('${{ matrix.scheme }}') { + case 'legacy': + animationName = \`\${baseName}_\${variant}\`; + break; + case 'artist': + animationName = \`char_\${baseName}_\${variant}\`; + break; + case 'hierarchical': + animationName = \`character/movement/\${baseName}/\${variant}\`; + break; + case 'semantic': + animationName = \`character.movement.\${baseName}.forward\`; + break; + } + + testData.push({ + name: animationName, + sourceScheme: '${{ matrix.scheme }}', + targetScheme: schemes.filter(s => s !== '${{ matrix.scheme }}')[Math.floor(Math.random() * 3)] + }); + } + + fs.writeFileSync('test-data.json', JSON.stringify(testData, null, 2)); + console.log(\`Generated \${testData.length} test cases for ${{ matrix.scheme }} scheme\`); + " + + - name: Run conversion performance test + run: | + node -e " + const fs = require('fs'); + const { AnimationNameMapper } = require('./src/animation/AnimationNameMapper.js'); + const testData = JSON.parse(fs.readFileSync('test-data.json', 'utf8')); + + const mapper = new AnimationNameMapper(); + const results = { + scheme: '${{ matrix.scheme }}', + batchSize: ${{ matrix.batch_size }}, + totalConversions: testData.length, + startTime: Date.now(), + conversions: [], + errors: [] + }; + + console.log(\`Starting performance test: ${{ matrix.scheme }} scheme, \${testData.length} conversions\`); + + for (const testCase of testData) { + const startTime = process.hrtime.bigint(); + + try { + const result = mapper.convert( + testCase.name, + testCase.sourceScheme, + testCase.targetScheme + ); + + const endTime = process.hrtime.bigint(); + const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds + + results.conversions.push({ + input: testCase.name, + output: result, + sourceScheme: testCase.sourceScheme, + targetScheme: testCase.targetScheme, + duration: duration + }); + } catch (error) { + results.errors.push({ + input: testCase.name, + sourceScheme: testCase.sourceScheme, + targetScheme: testCase.targetScheme, + error: error.message + }); + } + } + + results.endTime = Date.now(); + results.totalDuration = results.endTime - results.startTime; + results.averageConversionTime = results.conversions.length > 0 + ? results.conversions.reduce((sum, c) => sum + c.duration, 0) / results.conversions.length + : 0; + results.conversionsPerSecond = (results.conversions.length / results.totalDuration) * 1000; + results.errorRate = (results.errors.length / testData.length) * 100; + + console.log(\`Performance Results:\`); + console.log(\` Total Duration: \${results.totalDuration}ms\`); + console.log(\` Average Conversion Time: \${results.averageConversionTime.toFixed(2)}ms\`); + console.log(\` Conversions per Second: \${results.conversionsPerSecond.toFixed(2)}\`); + console.log(\` Error Rate: \${results.errorRate.toFixed(2)}%\`); + console.log(\` Successful Conversions: \${results.conversions.length}\`); + console.log(\` Failed Conversions: \${results.errors.length}\`); + + // Save detailed results + fs.writeFileSync('performance-results.json', JSON.stringify(results, null, 2)); + + // Performance thresholds + const MAX_AVG_CONVERSION_TIME = 10; // 10ms + const MAX_ERROR_RATE = 5; // 5% + const MIN_CONVERSIONS_PER_SECOND = 100; + + if (results.averageConversionTime > MAX_AVG_CONVERSION_TIME) { + console.error(\`PERFORMANCE ISSUE: Average conversion time (\${results.averageConversionTime.toFixed(2)}ms) exceeds threshold (\${MAX_AVG_CONVERSION_TIME}ms)\`); + process.exit(1); + } + + if (results.errorRate > MAX_ERROR_RATE) { + console.error(\`PERFORMANCE ISSUE: Error rate (\${results.errorRate.toFixed(2)}%) exceeds threshold (\${MAX_ERROR_RATE}%)\`); + process.exit(1); + } + + if (results.conversionsPerSecond < MIN_CONVERSIONS_PER_SECOND) { + console.error(\`PERFORMANCE ISSUE: Conversions per second (\${results.conversionsPerSecond.toFixed(2)}) below threshold (\${MIN_CONVERSIONS_PER_SECOND})\`); + process.exit(1); + } + + console.log('All performance thresholds passed! āœ“'); + " + + - name: Upload performance results + uses: actions/upload-artifact@v4 + with: + name: performance-results-${{ matrix.scheme }}-${{ matrix.batch_size }} + path: performance-results.json + retention-days: 30 + + memory-performance: + name: Memory Usage Analysis + runs-on: ubuntu-latest + if: github.event.inputs.test_type == 'all' || github.event.inputs.test_type == 'memory' || github.event.inputs.test_type == null + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run memory analysis + run: | + node --expose-gc -e " + const { AnimationNameMapper } = require('./src/animation/AnimationNameMapper.js'); + + function getMemoryUsage() { + global.gc(); + const used = process.memoryUsage(); + return { + rss: Math.round(used.rss / 1024 / 1024 * 100) / 100, + heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100, + heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100, + external: Math.round(used.external / 1024 / 1024 * 100) / 100 + }; + } + + console.log('Starting memory analysis...'); + + const initialMemory = getMemoryUsage(); + console.log('Initial memory usage:', initialMemory); + + // Create multiple mappers to test memory leaks + const mappers = []; + for (let i = 0; i < 100; i++) { + mappers.push(new AnimationNameMapper()); + } + + const afterCreationMemory = getMemoryUsage(); + console.log('After creating 100 mappers:', afterCreationMemory); + + // Perform conversions + const testAnimations = [ + 'char_walk_01', 'char_run_02', 'prop_door_open', + 'character.idle.basic', 'character/movement/walk/forward', + 'idle_basic', 'walk_forward', 'attack_sword' + ]; + + for (let round = 0; round < 10; round++) { + for (const mapper of mappers) { + for (const animation of testAnimations) { + try { + mapper.convert(animation, 'artist', 'semantic'); + mapper.convert(animation, 'semantic', 'hierarchical'); + mapper.convert(animation, 'hierarchical', 'legacy'); + } catch (error) { + // Ignore conversion errors for memory test + } + } + } + + if (round % 3 === 0) { + const memoryUsage = getMemoryUsage(); + console.log(\`Round \${round + 1} memory usage:\`, memoryUsage); + } + } + + const finalMemory = getMemoryUsage(); + console.log('Final memory usage:', finalMemory); + + // Calculate memory growth + const heapGrowth = finalMemory.heapUsed - initialMemory.heapUsed; + const rssGrowth = finalMemory.rss - initialMemory.rss; + + console.log(\`Heap growth: \${heapGrowth} MB\`); + console.log(\`RSS growth: \${rssGrowth} MB\`); + + // Memory leak thresholds + const MAX_HEAP_GROWTH = 50; // 50 MB + const MAX_RSS_GROWTH = 100; // 100 MB + + if (heapGrowth > MAX_HEAP_GROWTH) { + console.error(\`MEMORY LEAK: Heap growth (\${heapGrowth} MB) exceeds threshold (\${MAX_HEAP_GROWTH} MB)\`); + process.exit(1); + } + + if (rssGrowth > MAX_RSS_GROWTH) { + console.error(\`MEMORY LEAK: RSS growth (\${rssGrowth} MB) exceeds threshold (\${MAX_RSS_GROWTH} MB)\`); + process.exit(1); + } + + console.log('Memory usage within acceptable limits āœ“'); + " + + lighthouse-performance: + name: Demo Performance Audit + runs-on: ubuntu-latest + if: github.event.inputs.test_type == 'all' || github.event.inputs.test_type == 'lighthouse' || github.event.inputs.test_type == null + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build demo + run: npm run build:demo + + - name: Install Lighthouse + run: npm install -g @lhci/cli lighthouse + + - name: Start demo server + run: | + npm run preview:demo & + sleep 10 + env: + NODE_ENV: production + + - name: Run Lighthouse audit + run: | + lighthouse http://localhost:3000 \ + --output=json \ + --output-path=lighthouse-report.json \ + --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" \ + --only-categories=performance,accessibility,best-practices + + - name: Analyze Lighthouse results + run: | + node -e " + const fs = require('fs'); + const report = JSON.parse(fs.readFileSync('lighthouse-report.json', 'utf8')); + + const scores = { + performance: report.categories.performance.score * 100, + accessibility: report.categories.accessibility.score * 100, + bestPractices: report.categories['best-practices'].score * 100 + }; + + const metrics = { + fcp: report.audits['first-contentful-paint'].numericValue, + lcp: report.audits['largest-contentful-paint'].numericValue, + cls: report.audits['cumulative-layout-shift'].numericValue, + tbt: report.audits['total-blocking-time'].numericValue, + tti: report.audits['interactive'].numericValue + }; + + console.log('Lighthouse Scores:'); + console.log(\` Performance: \${scores.performance.toFixed(1)}/100\`); + console.log(\` Accessibility: \${scores.accessibility.toFixed(1)}/100\`); + console.log(\` Best Practices: \${scores.bestPractices.toFixed(1)}/100\`); + + console.log('\\nCore Web Vitals:'); + console.log(\` First Contentful Paint: \${(metrics.fcp / 1000).toFixed(2)}s\`); + console.log(\` Largest Contentful Paint: \${(metrics.lcp / 1000).toFixed(2)}s\`); + console.log(\` Cumulative Layout Shift: \${metrics.cls.toFixed(3)}\`); + console.log(\` Total Blocking Time: \${metrics.tbt.toFixed(0)}ms\`); + console.log(\` Time to Interactive: \${(metrics.tti / 1000).toFixed(2)}s\`); + + // Performance thresholds + const thresholds = { + performance: 90, + accessibility: 95, + bestPractices: 90, + fcp: 2000, // 2 seconds + lcp: 2500, // 2.5 seconds + cls: 0.1, + tbt: 300, // 300ms + tti: 3800 // 3.8 seconds + }; + + let failed = false; + + if (scores.performance < thresholds.performance) { + console.error(\`PERFORMANCE ISSUE: Performance score (\${scores.performance.toFixed(1)}) below threshold (\${thresholds.performance})\`); + failed = true; + } + + if (scores.accessibility < thresholds.accessibility) { + console.error(\`ACCESSIBILITY ISSUE: Accessibility score (\${scores.accessibility.toFixed(1)}) below threshold (\${thresholds.accessibility})\`); + failed = true; + } + + if (metrics.fcp > thresholds.fcp) { + console.error(\`PERFORMANCE ISSUE: FCP (\${(metrics.fcp / 1000).toFixed(2)}s) exceeds threshold (\${thresholds.fcp / 1000}s)\`); + failed = true; + } + + if (metrics.lcp > thresholds.lcp) { + console.error(\`PERFORMANCE ISSUE: LCP (\${(metrics.lcp / 1000).toFixed(2)}s) exceeds threshold (\${thresholds.lcp / 1000}s)\`); + failed = true; + } + + if (metrics.cls > thresholds.cls) { + console.error(\`PERFORMANCE ISSUE: CLS (\${metrics.cls.toFixed(3)}) exceeds threshold (\${thresholds.cls})\`); + failed = true; + } + + if (failed) { + process.exit(1); + } + + console.log('\\nAll performance thresholds passed! āœ“'); + " + + - name: Upload Lighthouse report + uses: actions/upload-artifact@v4 + with: + name: lighthouse-report + path: lighthouse-report.json + retention-days: 30 + + generate-performance-report: + name: Generate Performance Report + runs-on: ubuntu-latest + needs: [conversion-performance, memory-performance, lighthouse-performance] + if: always() + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Generate performance report + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + + const report = { + timestamp: new Date().toISOString(), + commit: process.env.GITHUB_SHA || 'unknown', + branch: process.env.GITHUB_REF_NAME || 'unknown', + results: { + conversion: [], + memory: null, + lighthouse: null + }, + summary: { + passed: 0, + failed: 0, + warnings: [] + } + }; + + // Process conversion performance results + const artifactsDir = 'artifacts'; + if (fs.existsSync(artifactsDir)) { + const artifactDirs = fs.readdirSync(artifactsDir); + + for (const dir of artifactDirs) { + if (dir.startsWith('performance-results-')) { + const resultFile = path.join(artifactsDir, dir, 'performance-results.json'); + if (fs.existsSync(resultFile)) { + const result = JSON.parse(fs.readFileSync(resultFile, 'utf8')); + report.results.conversion.push(result); + + if (result.errorRate <= 5 && result.averageConversionTime <= 10) { + report.summary.passed++; + } else { + report.summary.failed++; + } + } + } + + if (dir === 'lighthouse-report') { + const lightouseFile = path.join(artifactsDir, dir, 'lighthouse-report.json'); + if (fs.existsSync(lightouseFile)) { + const lighthouse = JSON.parse(fs.readFileSync(lightouseFile, 'utf8')); + report.results.lighthouse = { + performance: lighthouse.categories.performance.score * 100, + accessibility: lighthouse.categories.accessibility.score * 100, + bestPractices: lighthouse.categories['best-practices'].score * 100, + fcp: lighthouse.audits['first-contentful-paint'].numericValue, + lcp: lighthouse.audits['largest-contentful-paint'].numericValue, + cls: lighthouse.audits['cumulative-layout-shift'].numericValue + }; + + if (report.results.lighthouse.performance >= 90) { + report.summary.passed++; + } else { + report.summary.failed++; + } + } + } + } + } + + // Generate markdown report + let markdown = \`# Performance Test Report\\n\\n\`; + markdown += \`**Date:** \${new Date(report.timestamp).toLocaleString()}\\n\`; + markdown += \`**Commit:** \${report.commit}\\n\`; + markdown += \`**Branch:** \${report.branch}\\n\\n\`; + + markdown += \`## Summary\\n\\n\`; + markdown += \`- āœ… **Passed:** \${report.summary.passed}\\n\`; + markdown += \`- āŒ **Failed:** \${report.summary.failed}\\n\\n\`; + + if (report.results.conversion.length > 0) { + markdown += \`## Conversion Performance\\n\\n\`; + markdown += \`| Scheme | Batch Size | Avg Time (ms) | Conversions/sec | Error Rate (%) |\\n\`; + markdown += \`|--------|------------|---------------|-----------------|----------------|\\n\`; + + for (const result of report.results.conversion) { + const status = result.errorRate <= 5 && result.averageConversionTime <= 10 ? 'āœ…' : 'āŒ'; + markdown += \`| \${status} \${result.scheme} | \${result.batchSize} | \${result.averageConversionTime.toFixed(2)} | \${result.conversionsPerSecond.toFixed(2)} | \${result.errorRate.toFixed(2)} |\\n\`; + } + markdown += \`\\n\`; + } + + if (report.results.lighthouse) { + markdown += \`## Lighthouse Performance\\n\\n\`; + const l = report.results.lighthouse; + markdown += \`- **Performance Score:** \${l.performance.toFixed(1)}/100\\n\`; + markdown += \`- **Accessibility Score:** \${l.accessibility.toFixed(1)}/100\\n\`; + markdown += \`- **Best Practices Score:** \${l.bestPractices.toFixed(1)}/100\\n\`; + markdown += \`- **First Contentful Paint:** \${(l.fcp / 1000).toFixed(2)}s\\n\`; + markdown += \`- **Largest Contentful Paint:** \${(l.lcp / 1000).toFixed(2)}s\\n\`; + markdown += \`- **Cumulative Layout Shift:** \${l.cls.toFixed(3)}\\n\\n\`; + } + + fs.writeFileSync('performance-report.json', JSON.stringify(report, null, 2)); + fs.writeFileSync('performance-report.md', markdown); + + console.log('Performance report generated'); + console.log(markdown); + " + + - name: Upload performance report + uses: actions/upload-artifact@v4 + with: + name: performance-report + path: | + performance-report.json + performance-report.md + retention-days: 90 + + - name: Comment performance report on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + if (fs.existsSync('performance-report.md')) { + const report = fs.readFileSync('performance-report.md', 'utf8'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: report + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a1e5036 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,252 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v1.2.3)' + required: true + type: string + prerelease: + description: 'Mark as pre-release' + required: false + default: false + type: boolean + +env: + NODE_VERSION: '20.x' + +jobs: + validate-version: + name: Validate Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + is_prerelease: ${{ steps.version.outputs.is_prerelease }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + IS_PRERELEASE="${{ github.event.inputs.prerelease }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + # Check if version contains pre-release identifiers + if [[ "$VERSION" =~ -[a-zA-Z] ]]; then + IS_PRERELEASE=true + else + IS_PRERELEASE=false + fi + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "Release version: $VERSION (prerelease: $IS_PRERELEASE)" + + - name: Validate semantic version + run: | + VERSION="${{ steps.version.outputs.version }}" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then + echo "Invalid semantic version: $VERSION" + exit 1 + fi + + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + needs: validate-version + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Run tests + run: npm test + + - name: Run multi-scheme validation + run: npm run test:multi-schemes + + - name: Build project + run: npm run build + + - name: Build demo + run: npm run build:demo + + - name: Generate documentation + run: npm run docs + + - name: Validate build artifacts + run: | + test -d dist || (echo "Build failed - dist directory not found" && exit 1) + test -d dist/demo || (echo "Demo build failed - dist/demo directory not found" && exit 1) + test -d docs || (echo "Documentation generation failed" && exit 1) + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: | + dist/ + docs/ + CHANGELOG.md + README.md + MULTI_SCHEME_GUIDE.md + retention-days: 30 + + create-release: + name: Create Release + runs-on: ubuntu-latest + needs: [validate-version, build-and-test] + permissions: + contents: write + pull-requests: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: ./artifacts + + - name: Generate release notes + id: release_notes + run: | + VERSION="${{ needs.validate-version.outputs.version }}" + + # Extract changelog for this version + if [ -f CHANGELOG.md ]; then + # Get the changelog section for this version + CHANGELOG_SECTION=$(sed -n "/^## \[${VERSION#v}\]/,/^## \[/p" CHANGELOG.md | head -n -1) + if [ -n "$CHANGELOG_SECTION" ]; then + echo "Found changelog section for version ${VERSION#v}" + echo "$CHANGELOG_SECTION" > release_notes.md + else + echo "No changelog section found for version ${VERSION#v}, generating default notes" + echo "## Changes in ${VERSION}" > release_notes.md + echo "" >> release_notes.md + echo "This release includes various improvements and updates to the Owen Animation System." >> release_notes.md + fi + else + echo "## Changes in ${VERSION}" > release_notes.md + echo "" >> release_notes.md + echo "This release includes various improvements and updates to the Owen Animation System." >> release_notes.md + fi + + # Add commit summary since last tag + echo "" >> release_notes.md + echo "### Commits since last release:" >> release_notes.md + git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD >> release_notes.md || true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.validate-version.outputs.version }} + name: Owen Animation System ${{ needs.validate-version.outputs.version }} + body_path: release_notes.md + draft: false + prerelease: ${{ needs.validate-version.outputs.is_prerelease == 'true' }} + files: | + artifacts/dist/** + artifacts/docs/** + artifacts/CHANGELOG.md + artifacts/README.md + artifacts/MULTI_SCHEME_GUIDE.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-npm: + name: Publish to NPM + runs-on: ubuntu-latest + needs: [validate-version, create-release] + if: needs.validate-version.outputs.is_prerelease == 'false' + environment: npm-publish + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Update package version + run: | + VERSION="${{ needs.validate-version.outputs.version }}" + npm version ${VERSION#v} --no-git-tag-version + + - name: Publish to NPM + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + deploy-demo: + name: Deploy Demo + runs-on: ubuntu-latest + needs: [validate-version, create-release] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build demo + run: npm run build:demo + + - name: Deploy demo to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist/demo + destination_dir: releases/${{ needs.validate-version.outputs.version }} + + - name: Update latest demo link + if: needs.validate-version.outputs.is_prerelease == 'false' + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist/demo + destination_dir: latest diff --git a/.gitignore b/.gitignore index 0462f8a..c3cc1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ # Three.js cache .three-cache/ +# MyPy cache +.mypy_cache/ + # Editor (optional - remove if you want to commit editor files) .vscode/ diff --git a/MULTI_SCHEME_GUIDE.md b/MULTI_SCHEME_GUIDE.md new file mode 100644 index 0000000..3697447 --- /dev/null +++ b/MULTI_SCHEME_GUIDE.md @@ -0,0 +1,303 @@ +# Multi-Scheme Animation Naming Guide + +The Owen Animation System supports four different naming schemes to accommodate different workflows and preferences. This guide explains each scheme and how to use them effectively. + +## šŸŽÆ Overview + +The multi-scheme animation naming system provides backward compatibility while making the Owen Animation System more accessible to different types of users: + +- **Legacy Scheme**: Original technical format for backward compatibility +- **Artist Scheme**: Blender-friendly names for 3D artists +- **Hierarchical Scheme**: Organized dot-notation for structured projects +- **Semantic Scheme**: Readable camelCase for developers + +## šŸ“ Naming Schemes + +### 1. Legacy Scheme (Technical/Backward Compatible) + +**Format**: `{state}_{emotion}_{type}` +**Examples**: + +- `wait_idle_L` - Wait state, idle animation, Loop +- `react_an2type_T` - React state, angry to type, Transition +- `type_idle_L` - Type state, idle animation, Loop +- `sleep_2wait_T` - Sleep state, to wait transition, Transition + +**Use Cases**: + +- Existing Owen implementations +- Technical documentation +- Legacy animation files + +### 2. Artist Scheme (Blender-Friendly) + +**Format**: `Owen_{Action}` or `Owen_{StateAction}` +**Examples**: + +- `Owen_WaitIdle` - Wait idle animation +- `Owen_ReactAngryToType` - React angry to type transition +- `Owen_TypeIdle` - Type idle animation +- `Owen_SleepToWait` - Sleep to wait transition + +**Use Cases**: + +- 3D artists working in Blender +- Animation asset naming +- Non-technical team members +- Clear, human-readable names + +### 3. Hierarchical Scheme (Structured/Organized) + +**Format**: `owen.{category}.{state}.{detail}.{type}` +**Examples**: + +- `owen.state.wait.idle.loop` - Wait state idle loop +- `owen.state.react.angry.totype.transition` - React angry to type transition +- `owen.state.type.idle.loop` - Type state idle loop +- `owen.state.sleep.towait.transition` - Sleep to wait transition + +**Use Cases**: + +- Large projects with many animations +- Structured asset organization +- Configuration files +- Automated tooling + +### 4. Semantic Scheme (Developer-Friendly) + +**Format**: `Owen{StateAction}{Type}` (PascalCase) +**Examples**: + +- `OwenWaitIdleLoop` - Wait idle loop animation +- `OwenReactAngryToTypeTransition` - React angry to type transition +- `OwenTypeIdleLoop` - Type idle loop animation +- `OwenSleepToWaitTransition` - Sleep to wait transition + +**Use Cases**: + +- JavaScript/TypeScript code +- API integration +- Developer constants +- Type-safe programming + +## šŸ”§ Usage Examples + +### Basic Usage + +```javascript +import { OwenAnimationContext, convertAnimationName } from './owen-animation-system'; + +// Get animation using any naming scheme +const clip1 = owenContext.getClip('wait_idle_L'); // Legacy +const clip2 = owenContext.getClip('Owen_WaitIdle'); // Artist +const clip3 = owenContext.getClip('owen.state.wait.idle.loop'); // Hierarchical +const clip4 = owenContext.getClip('OwenWaitIdleLoop'); // Semantic + +// All return the same animation clip! +``` + +### Name Conversion + +```javascript +import { convertAnimationName, getAllAnimationNames } from './owen-animation-system'; + +// Convert between schemes +const artistName = convertAnimationName('wait_idle_L', 'artist'); +// Returns: 'Owen_WaitIdle' + +const semanticName = convertAnimationName('Owen_ReactAngry', 'semantic'); +// Returns: 'OwenReactAngryLoop' + +// Get all variants +const allNames = getAllAnimationNames('react_an2type_T'); +/* Returns: +{ + legacy: 'react_an2type_T', + artist: 'Owen_ReactAngryToType', + hierarchical: 'owen.state.react.angry.totype.transition', + semantic: 'OwenReactAngryToTypeTransition' +} +*/ +``` + +### Validation + +```javascript +import { validateAnimationName } from './owen-animation-system'; + +const validation = validateAnimationName('Owen_WaitIdle'); +/* Returns: +{ + isValid: true, + scheme: 'artist', + error: null, + suggestions: [] +} +*/ + +const invalidValidation = validateAnimationName('invalid_name'); +/* Returns: +{ + isValid: false, + scheme: 'unknown', + error: 'Animation "invalid_name" not found', + suggestions: ['OwenWaitIdleLoop', 'OwenReactIdleLoop', ...] +} +*/ +``` + +### Using Constants + +```javascript +import { + LegacyAnimations, + ArtistAnimations, + SemanticAnimations +} from './owen-animation-system'; + +// Type-safe animation references +const legacyAnim = LegacyAnimations.WAIT_IDLE_LOOP; // 'wait_idle_L' +const artistAnim = ArtistAnimations.WAIT_IDLE; // 'Owen_WaitIdle' +const semanticAnim = SemanticAnimations.WAIT_IDLE_LOOP; // 'OwenWaitIdleLoop' +``` + +## šŸŽØ Workflow Integration + +### For 3D Artists (Blender) + +1. Use **Artist Scheme** names when creating animations in Blender +2. Name animations like `Owen_WaitIdle`, `Owen_ReactHappy`, etc. +3. Export animations with these names +4. The system automatically handles conversion to other schemes + +### For Developers + +1. Use **Semantic Scheme** constants in code for type safety +2. Import animation constants: `import { SemanticAnimations } from './owen-animation-system'` +3. Reference animations: `SemanticAnimations.WAIT_IDLE_LOOP` +4. Let the system handle backward compatibility + +### For Project Management + +1. Use **Hierarchical Scheme** for asset organization +2. Structure animation files: `owen.state.{stateName}.{details}.{type}` +3. Easy filtering and categorization +4. Clear project structure + +## šŸ”„ Migration Guide + +### From Legacy to Multi-Scheme + +```javascript +// Before (Legacy only) +const clip = owenContext.getClip('wait_idle_L'); + +// After (Multi-scheme compatible) +const clip = owenContext.getClip('wait_idle_L'); // Still works! +// OR use any other scheme: +const clip = owenContext.getClip('Owen_WaitIdle'); // Artist-friendly +const clip = owenContext.getClip('OwenWaitIdleLoop'); // Developer-friendly +``` + +### Updating Animation Assets + +1. **No changes required** - existing legacy names continue to work +2. **Gradual migration** - add new scheme names alongside legacy names +3. **Full migration** - replace legacy names with preferred scheme + +## šŸ“š Available Animations + +### Wait State + +| Legacy | Artist | Semantic | +| ----------------- | --------------- | ------------------- | +| `wait_idle_L` | `Owen_WaitIdle` | `OwenWaitIdleLoop` | +| `wait_pickNose_Q` | `Owen_PickNose` | `OwenQuirkPickNose` | +| `wait_stretch_Q` | `Owen_Stretch` | `OwenQuirkStretch` | +| `wait_yawn_Q` | `Owen_Yawn` | `OwenQuirkYawn` | + +### React State + +| Legacy | Artist | Semantic | +| -------------- | ------------------- | ---------------------- | +| `react_idle_L` | `Owen_ReactIdle` | `OwenReactIdleLoop` | +| `react_an_L` | `Owen_ReactAngry` | `OwenReactAngryLoop` | +| `react_sh_L` | `Owen_ReactShocked` | `OwenReactShockedLoop` | +| `react_ha_L` | `Owen_ReactHappy` | `OwenReactHappyLoop` | +| `react_sd_L` | `Owen_ReactSad` | `OwenReactSadLoop` | + +### Type State + +| Legacy | Artist | Semantic | +| ------------- | --------------- | ------------------ | +| `type_idle_L` | `Owen_TypeIdle` | `OwenTypeIdleLoop` | +| `type_fast_L` | `Owen_TypeFast` | `OwenTypeFastLoop` | +| `type_slow_L` | `Owen_TypeSlow` | `OwenTypeSlowLoop` | + +### Sleep State + +| Legacy | Artist | Semantic | +| --------------- | ------------------ | --------------------------- | +| `sleep_idle_L` | `Owen_SleepIdle` | `OwenSleepIdleLoop` | +| `sleep_2wait_T` | `Owen_SleepToWait` | `OwenSleepToWaitTransition` | + +## šŸ› ļø API Reference + +### Core Methods + +#### `getClip(name: string)` + +Get animation clip by name (supports all schemes) + +#### `getClipByScheme(name: string, targetScheme: string)` + +Get animation clip with specific scheme conversion + +#### `convertAnimationName(name: string, targetScheme: string)` + +Convert animation name between schemes + +#### `getAllAnimationNames(name: string)` + +Get all scheme variants for an animation + +#### `validateAnimationName(name: string)` + +Validate animation name and get suggestions + +### Constants + +#### `NamingSchemes` + +- `LEGACY`: 'legacy' +- `ARTIST`: 'artist' +- `HIERARCHICAL`: 'hierarchical' +- `SEMANTIC`: 'semantic' + +#### Animation Constants + +- `LegacyAnimations`: Legacy scheme constants +- `ArtistAnimations`: Artist scheme constants +- `HierarchicalAnimations`: Hierarchical scheme constants +- `SemanticAnimations`: Semantic scheme constants + +## šŸŽÆ Best Practices + +1. **Consistency**: Choose one primary scheme for your team and stick to it +2. **Type Safety**: Use constants instead of raw strings when possible +3. **Documentation**: Document which scheme you're using in your project +4. **Validation**: Use `validateAnimationName()` to catch typos early +5. **Migration**: Plan gradual migration for existing projects + +## šŸš€ Examples + +Check out the [Mock Demo](./examples/mock-demo/owen_test_demo.html) for interactive examples of: + +- Name conversion between schemes +- Animation validation +- Real-time scheme testing +- Integration patterns + +--- + +For more information, see the [main README](./README.md) or check the [API documentation](./docs/). diff --git a/README.md b/README.md index bf9766b..fb813e3 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,13 @@ The Owen Animation System is a sophisticated character animation framework built - **šŸ¤– State Machine Implementation** - Complete state management system with `Wait`, `React`, `Type`, and `Sleep` states - **😊 Emotional Response System** - Analyzes user input to determine appropriate emotional animations - **šŸ”„ Animation Transition Management** - Smooth transitions between states with fade in/out support -- **šŸ“ Animation Naming Convention Parser** - Automatically parses animation metadata from naming conventions +- **šŸ“ Multi-Scheme Animation Naming** - Supports legacy, artist-friendly, hierarchical, and semantic naming schemes +- **šŸŽØ Artist-Friendly Workflow** - Blender-compatible naming for 3D artists (`Owen_WaitIdle`, `Owen_ReactHappy`) +- **šŸ‘Øā€šŸ’» Developer Experience** - Type-safe constants and semantic naming (`OwenWaitIdleLoop`, `OwenReactAngryTransition`) - **šŸ—ļø Clean Architecture** - Uses dependency injection, factory patterns, and separation of concerns - **⚔ Performance Optimized** - Efficient animation caching and resource management - **🧩 Extensible Design** - Easy to add new states, emotions, and animation types +- **šŸ”„ Backward Compatibility** - Legacy naming scheme continues to work alongside new schemes ## šŸš€ Installation @@ -106,7 +109,54 @@ const owenSystem = await OwenSystemFactory.createCustomOwenSystem(gltfModel, sce await owenSystem.transitionTo(States.REACTING, Emotions.HAPPY); ``` -## šŸŽ® Animation Naming Convention +## šŸŽØ Multi-Scheme Animation Naming + +Owen supports **four different animation naming schemes** to accommodate different workflows and preferences: + +### Naming Schemes + +| Scheme | Format | Example | Use Case | +| ---------------- | ---------------------------- | --------------------------- | ------------------------------- | +| **Legacy** | `{state}_{emotion}_{type}` | `wait_idle_L` | Backward compatibility | +| **Artist** | `Owen_{Action}` | `Owen_WaitIdle` | Blender-friendly for 3D artists | +| **Hierarchical** | `owen.{category}.{state}...` | `owen.state.wait.idle.loop` | Structured projects | +| **Semantic** | `Owen{StateAction}{Type}` | `OwenWaitIdleLoop` | Developer-friendly | + +### Usage Examples + +```javascript +// All of these refer to the same animation: +const clip1 = owenSystem.getClip('wait_idle_L'); // Legacy +const clip2 = owenSystem.getClip('Owen_WaitIdle'); // Artist +const clip3 = owenSystem.getClip('owen.state.wait.idle.loop'); // Hierarchical +const clip4 = owenSystem.getClip('OwenWaitIdleLoop'); // Semantic + +// Convert between schemes +import { convertAnimationName, SemanticAnimations } from 'owen'; + +const artistName = convertAnimationName('wait_idle_L', 'artist'); +// Returns: 'Owen_WaitIdle' + +// Use type-safe constants +const animation = SemanticAnimations.WAIT_IDLE_LOOP; // 'OwenWaitIdleLoop' +``` + +### For 3D Artists (Blender Workflow) + +```javascript +// Use artist-friendly names in Blender: +// Owen_WaitIdle, Owen_ReactHappy, Owen_TypeFast, etc. +// System automatically handles conversion! + +const clip = owenSystem.getClip('Owen_ReactAngry'); // Just works! +``` + +> [!TIP] +> See the [Multi-Scheme Guide](./MULTI_SCHEME_GUIDE.md) for complete documentation and examples. + +## šŸŽ® Animation Naming Convention (Legacy) + +The system maintains backward compatibility with the original naming convention: The system expects animations to follow this naming convention: diff --git a/demo/comparison.html b/demo/comparison.html new file mode 100644 index 0000000..e9ac7c1 --- /dev/null +++ b/demo/comparison.html @@ -0,0 +1,337 @@ + + + + + + Scheme Comparison - Owen Animation System + + + + +
+
+

+ Owen + Scheme Comparison +

+ +
+
+ +
+
+
+

Animation Naming Scheme Comparison

+

+ Compare the four supported naming schemes and understand when to use + each one. +

+
+ + +
+
+
+

Legacy

+
snake_case
+
+ Traditional lowercase with underscores. Compatible with older + animation systems. +
+
walk_forward
+
+ +
+

Artist

+
PascalCase
+
+ Artist-friendly naming with clear capitalization. Intuitive for + content creators. +
+
WalkForward
+
+ +
+

Hierarchical

+
dot.notation
+
+ Structured hierarchy with dots. Excellent for organizing complex + animation sets. +
+
character.movement.walk.forward
+
+ +
+

Semantic

+
descriptive_names
+
+ Semantic meaning with underscores. Clear intent and + self-documenting. +
+
character_walk_forward
+
+
+
+ + +
+

Animation Name Comparison

+
+ + + + + +
+ +
+ + + + + + + + + + + + + +
Animation TypeLegacyArtistHierarchicalSemantic
+
+
+ + +
+

Detailed Analysis

+ +
+
+

šŸŽÆ Use Cases

+
+
+ Legacy: Migrating from older systems, + maintaining backward compatibility +
+
+ Artist: Content creation workflows, + artist-friendly tools +
+
+ Hierarchical: Large animation libraries, + complex character systems +
+
+ Semantic: Modern development, clear + documentation needs +
+
+
+ +
+

⚔ Performance

+
+
+ Lookup Speed: +
+
+ Legacy +
+
+ Artist +
+
+ Hierarchical +
+
+ Semantic +
+
+
+
+ Memory Usage: +
+
+ Legacy +
+
+ Artist +
+
+ Hierarchical +
+
+ Semantic +
+
+
+
+
+ +
+

šŸ› ļø Developer Experience

+
+
+
+ Readability: +
+
ā˜…ā˜…ā˜…ā˜†ā˜†
+
ā˜…ā˜…ā˜…ā˜…ā˜†
+
ā˜…ā˜…ā˜…ā˜…ā˜…
+
ā˜…ā˜…ā˜…ā˜…ā˜…
+
+
+
+ Autocomplete: +
+
ā˜…ā˜…ā˜…ā˜†ā˜†
+
ā˜…ā˜…ā˜…ā˜…ā˜†
+
ā˜…ā˜…ā˜…ā˜…ā˜…
+
ā˜…ā˜…ā˜…ā˜…ā˜†
+
+
+
+ Maintainability: +
+
ā˜…ā˜…ā˜†ā˜†ā˜†
+
ā˜…ā˜…ā˜…ā˜†ā˜†
+
ā˜…ā˜…ā˜…ā˜…ā˜…
+
ā˜…ā˜…ā˜…ā˜…ā˜†
+
+
+
+
+
+
+
+ + +
+

Migration Between Schemes

+
+
+

From Legacy

+
    +
  • + To Artist: Capitalize first letter and after + underscores +
  • +
  • + To Hierarchical: Replace underscores with + dots, add category prefixes +
  • +
  • + To Semantic: Add descriptive prefixes + (character_, ui_, effect_) +
  • +
+
+ +
+

From Artist

+
    +
  • + To Legacy: Convert to lowercase, add + underscores before capitals +
  • +
  • + To Hierarchical: Split on capitals, join with + dots, add categories +
  • +
  • + To Semantic: Convert to lowercase with + underscores, add prefixes +
  • +
+
+ +
+

Automated Tools

+
    +
  • + CLI Converter: + owen convert --from legacy --to semantic +
  • +
  • + Batch Processing: + owen batch-convert ./animations/ +
  • +
  • + Validation: + owen validate --scheme semantic +
  • +
+
+
+
+ + +
+

Best Practices & Recommendations

+
+
+

šŸ¢ Enterprise Projects

+

Recommended: Hierarchical or Semantic

+
    +
  • Clear organization structure
  • +
  • Easy to maintain and scale
  • +
  • Good IDE support
  • +
+
+ +
+

šŸŽØ Artist Workflows

+

Recommended: Artist or Semantic

+
    +
  • Intuitive for content creators
  • +
  • Clear visual distinction
  • +
  • Good tool integration
  • +
+
+ +
+

šŸ”„ Legacy Migration

+

Recommended: Gradual transition

+
    +
  • Start with Legacy scheme
  • +
  • Use auto-conversion features
  • +
  • Migrate incrementally
  • +
+
+ +
+

šŸš€ New Projects

+

Recommended: Semantic scheme

+
    +
  • Modern best practices
  • +
  • Self-documenting code
  • +
  • Future-proof design
  • +
+
+
+
+
+
+ + + + + + diff --git a/demo/examples.html b/demo/examples.html new file mode 100644 index 0000000..ab1add0 --- /dev/null +++ b/demo/examples.html @@ -0,0 +1,400 @@ + + + + + + Examples - Owen Animation System + + + + +
+
+

+ Owen + Examples +

+ +
+
+ +
+
+
+

Code Examples & Integration Patterns

+

+ Explore practical examples of using the Owen Animation System in + different frameworks and scenarios. +

+
+ +
+
+

Framework Integration

+
+
+

React Integration

+

Complete React component with animation state management

+ View Example +
+
+

Vue Integration

+

Vue 3 composition API with reactive animation controls

+ View Example +
+
+

Node.js Server

+

Server-side animation processing and validation

+ View Example +
+
+
+ +
+

Multi-Scheme Usage

+
+
+

Scheme Conversion

+

Converting animations between different naming schemes

+ View Example +
+
+

Batch Processing

+

Processing multiple animations with automated conversion

+ View Example +
+
+

Validation Pipeline

+

Complete validation workflow with error handling

+ View Example +
+
+
+ +
+

Advanced Features

+
+
+

Custom Schemes

+

Creating and registering custom naming schemes

+ View Example +
+
+

Performance Optimization

+

Optimizing animation loading and caching strategies

+ View Example +
+
+

Testing Integration

+

Unit and integration testing for animation systems

+ View Example +
+
+
+
+ + +
+
+

React Integration Example

+
import React, { useEffect, useRef, useState } from 'react'
+import { OwenAnimationContext } from '@kjanat/owen'
+
+export function AnimatedCharacter({ characterModel, namingScheme = 'semantic' }) {
+  const containerRef = useRef()
+  const [animationContext, setAnimationContext] = useState(null)
+  const [currentAnimation, setCurrentAnimation] = useState('idle')
+  const [isPlaying, setIsPlaying] = useState(false)
+
+  useEffect(() => {
+    // Initialize Owen Animation Context
+    const context = new OwenAnimationContext({
+      namingScheme,
+      autoConvert: true,
+      container: containerRef.current
+    })
+
+    context.loadModel(characterModel).then(() => {
+      setAnimationContext(context)
+    })
+
+    return () => context?.dispose()
+  }, [characterModel, namingScheme])
+
+  const playAnimation = async (animationName) => {
+    if (!animationContext) return
+
+    try {
+      await animationContext.playAnimation(animationName)
+      setCurrentAnimation(animationName)
+      setIsPlaying(true)
+    } catch (error) {
+      console.error('Failed to play animation:', error)
+    }
+  }
+
+  const stopAnimation = () => {
+    animationContext?.stopAnimation()
+    setIsPlaying(false)
+  }
+
+  return (
+    <div className="animated-character">
+      <div ref={containerRef} className="character-viewport" />
+
+      <div className="animation-controls">
+        <button onClick={() => playAnimation('walk_forward')}>
+          Walk
+        </button>
+        <button onClick={() => playAnimation('character_run')}>
+          Run
+        </button>
+        <button onClick={() => playAnimation('jump_high')}>
+          Jump
+        </button>
+        <button onClick={stopAnimation}>
+          Stop
+        </button>
+      </div>
+
+      <div className="animation-info">
+        <p>Current: {currentAnimation}</p>
+        <p>Status: {isPlaying ? 'Playing' : 'Stopped'}</p>
+        <p>Scheme: {namingScheme}</p>
+      </div>
+    </div>
+  )
+}
+
+ +
+

Animation Name Conversion

+
import { AnimationNameMapper } from '@kjanat/owen'
+
+// Initialize the mapper
+const mapper = new AnimationNameMapper()
+
+// Single animation conversion
+function convertAnimation(animationName, fromScheme, toScheme) {
+  try {
+    const converted = mapper.convert(animationName, toScheme, fromScheme)
+    console.log(`${fromScheme}: ${animationName} → ${toScheme}: ${converted}`)
+    return converted
+  } catch (error) {
+    console.error('Conversion failed:', error.message)
+    return null
+  }
+}
+
+// Batch conversion with validation
+function convertAnimationBatch(animations, fromScheme, toScheme) {
+  const results = {
+    successful: [],
+    failed: [],
+    conflicts: []
+  }
+
+  animations.forEach(anim => {
+    try {
+      const converted = mapper.convert(anim, toScheme, fromScheme)
+
+      // Check for conflicts
+      if (results.successful.includes(converted)) {
+        results.conflicts.push({
+          original: anim,
+          converted,
+          conflict: 'Duplicate target name'
+        })
+      } else {
+        results.successful.push({
+          original: anim,
+          converted,
+          schemes: { from: fromScheme, to: toScheme }
+        })
+      }
+    } catch (error) {
+      results.failed.push({
+        original: anim,
+        error: error.message,
+        suggestions: mapper.suggestCorrections(anim, fromScheme)
+      })
+    }
+  })
+
+  return results
+}
+
+// Example usage
+const legacyAnimations = [
+  'walk_forward', 'run_fast', 'jump_high',
+  'attack_sword', 'defend_shield', 'idle_breathing'
+]
+
+const conversionResults = convertAnimationBatch(
+  legacyAnimations,
+  'legacy',
+  'semantic'
+)
+
+console.log('Conversion Results:', conversionResults)
+
+ +
+

Batch Processing Pipeline

+
import { AnimationProcessor } from '@kjanat/owen'
+import fs from 'fs/promises'
+import path from 'path'
+
+class AnimationBatchProcessor {
+  constructor(options = {}) {
+    this.processor = new AnimationProcessor(options)
+    this.inputDir = options.inputDir || './assets/raw'
+    this.outputDir = options.outputDir || './assets/processed'
+    this.targetScheme = options.targetScheme || 'semantic'
+  }
+
+  async processDirectory() {
+    console.log('Starting batch animation processing...')
+
+    try {
+      // Scan input directory
+      const files = await this.scanAnimationFiles()
+      console.log(`Found ${files.length} animation files`)
+
+      // Process each file
+      const results = await Promise.allSettled(
+        files.map(file => this.processFile(file))
+      )
+
+      // Generate summary report
+      const summary = this.generateSummary(results)
+      await this.saveReport(summary)
+
+      return summary
+    } catch (error) {
+      console.error('Batch processing failed:', error)
+      throw error
+    }
+  }
+
+  async scanAnimationFiles() {
+    const files = []
+    const entries = await fs.readdir(this.inputDir, { withFileTypes: true })
+
+    for (const entry of entries) {
+      if (entry.isFile() && /\.(gltf|glb|fbx)$/i.test(entry.name)) {
+        files.push(path.join(this.inputDir, entry.name))
+      }
+    }
+
+    return files
+  }
+
+  async processFile(inputFile) {
+    const filename = path.basename(inputFile)
+    console.log(`Processing: ${filename}`)
+
+    try {
+      // Load and analyze animation
+      const animation = await this.processor.loadAnimation(inputFile)
+
+      // Convert naming scheme
+      const convertedName = this.processor.convertName(
+        animation.name,
+        this.targetScheme
+      )
+
+      // Apply optimizations
+      const optimized = await this.processor.optimize(animation)
+
+      // Save processed animation
+      const outputFile = path.join(this.outputDir, `${convertedName}.gltf`)
+      await this.processor.saveAnimation(optimized, outputFile)
+
+      return {
+        status: 'success',
+        inputFile,
+        outputFile,
+        originalName: animation.name,
+        convertedName,
+        size: optimized.size,
+        duration: optimized.duration
+      }
+    } catch (error) {
+      return {
+        status: 'error',
+        inputFile,
+        error: error.message
+      }
+    }
+  }
+
+  generateSummary(results) {
+    const successful = results.filter(r => r.value?.status === 'success')
+    const failed = results.filter(r => r.status === 'rejected' || r.value?.status === 'error')
+
+    return {
+      timestamp: new Date().toISOString(),
+      total: results.length,
+      successful: successful.length,
+      failed: failed.length,
+      successRate: (successful.length / results.length * 100).toFixed(2),
+      details: {
+        successful: successful.map(r => r.value),
+        failed: failed.map(r => ({
+          file: r.value?.inputFile || 'unknown',
+          error: r.value?.error || r.reason
+        }))
+      }
+    }
+  }
+
+  async saveReport(summary) {
+    const reportPath = path.join(this.outputDir, 'processing-report.json')
+    await fs.mkdir(path.dirname(reportPath), { recursive: true })
+    await fs.writeFile(reportPath, JSON.stringify(summary, null, 2))
+    console.log(`Report saved: ${reportPath}`)
+  }
+}
+
+// Usage
+const processor = new AnimationBatchProcessor({
+  inputDir: './assets/blender-exports',
+  outputDir: './assets/animations',
+  targetScheme: 'semantic'
+})
+
+processor.processDirectory().then(summary => {
+  console.log('Processing complete:', summary)
+}).catch(error => {
+  console.error('Processing failed:', error)
+  process.exit(1)
+})
+
+
+
+
+ + + + + + diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..938e699 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,311 @@ + + + + + + Owen Animation System - Interactive Demo + + + + + + + + + + + + + + + +
+
+

+ Owen + Animation System +

+ +
+
+ + +
+
+ +
+
+

Multi-Scheme Animation Naming

+

+ Experience the power of flexible animation naming with support for + Legacy, Artist, Hierarchical, and Semantic schemes. +

+ +
+ + View on GitHub +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + +
+

Key Features

+
+
+
šŸŽÆ
+

Multi-Scheme Support

+

+ Seamlessly work with Legacy, Artist, Hierarchical, and Semantic + naming schemes in the same project. +

+
+ +
+
šŸ”„
+

Automatic Conversion

+

+ Convert animation names between schemes automatically with + built-in validation and error handling. +

+
+ +
+
⚔
+

Performance Optimized

+

+ Efficient caching and lazy loading ensure smooth performance + even with large animation libraries. +

+
+ +
+
šŸ› ļø
+

Developer Tools

+

+ Comprehensive CLI tools, validation scripts, and documentation + generators for streamlined workflows. +

+
+
+
+ + +
+

Live Animation Conversion

+
+
+ + + + + +
+ +
+

Converted Names:

+
+
+ Legacy: - +
+
+ Artist: - +
+
+ Hierarchical: + - +
+
+ Semantic: - +
+
+
+
+
+ + +
+

Usage Examples

+
+ + + +
+ +
+
+
import { OwenAnimationContext } from '@kjanat/owen'
+
+// Initialize with semantic naming scheme
+const animationContext = new OwenAnimationContext({
+  namingScheme: 'semantic',
+  autoConvert: true
+})
+
+// Play an animation
+await animationContext.playAnimation('character_walk_forward')
+
+// The system automatically handles scheme conversions
+await animationContext.playAnimation('CharacterWalkForward') // Artist scheme
+
+ +
+
import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+// Convert between schemes
+const semanticName = mapper.convert('CharacterWalkForward', 'semantic')
+// Result: 'character_walk_forward'
+
+const hierarchicalName = mapper.convert('walk_forward', 'hierarchical')
+// Result: 'character.movement.walk.forward'
+
+// Batch conversion
+const animations = ['jump', 'run', 'idle']
+const converted = mapper.convertBatch(animations, 'legacy', 'artist')
+// Result: ['Jump', 'Run', 'Idle']
+
+ +
+
import { AnimationValidator } from '@kjanat/owen'
+
+const validator = new AnimationValidator()
+
+// Validate animation name for specific scheme
+const isValid = validator.validate('character_walk_forward', 'semantic')
+// Result: true
+
+// Get validation details
+const validation = validator.validateDetailed('InvalidName123', 'semantic')
+console.log(validation.errors) // Array of validation errors
+console.log(validation.suggestions) // Suggested corrections
+
+
+
+
+
+ + + + + + + + + diff --git a/demo/interactive.html b/demo/interactive.html new file mode 100644 index 0000000..5f022f2 --- /dev/null +++ b/demo/interactive.html @@ -0,0 +1,306 @@ + + + + + + Interactive Playground - Owen Animation System + + + + +
+
+

+ Owen + Interactive Playground +

+ +
+
+ +
+
+
+

Interactive Animation Playground

+

+ Experiment with the Owen Animation System in real-time. Try + different schemes, test conversions, and see the results instantly. +

+
+ +
+ + + + +
+
+

Animation Viewport

+
+ + +
+
+ +
+ +
+
+
+

Loading model...

+
+ +
+
+ Current Animation: + None +
+
+ Duration: + 0s +
+
+ Progress: +
+
+
+ 0.0s +
+
+
+
+
+
+ + +
+

Generated Code

+

+ See the actual code that would be used to implement your current + configuration: +

+ +
+ + + +
+ +
+
+
// Configure your naming scheme and load animations
+// Code will be generated based on your selections above
+ +
+ +
+
// React component implementation
+// Code will be generated based on your selections above
+ +
+ +
+
// Vue component implementation
+// Code will be generated based on your selections above
+ +
+
+
+ + +
+

Performance Monitor

+
+
+

Frame Rate

+
60 FPS
+
+
+ +
+

Memory Usage

+
0 MB
+
+
+ +
+

Animation Cache

+
0 / 0
+
+ Cached: 0 + Total: 0 +
+
+ +
+

Conversion Time

+
0ms
+
+ Avg: 0ms + Max: 0ms +
+
+
+
+ + +
+

Experiments & Tests

+
+
+

šŸš€ Stress Test

+

Load multiple animations and test performance

+ +
+
+ +
+

šŸ”„ Conversion Benchmark

+

Benchmark animation name conversion performance

+ +
+
+ +
+

šŸ“Š Scheme Analysis

+

Analyze and compare naming scheme efficiency

+ +
+
+ +
+

šŸ’¾ Memory Profiling

+

Profile memory usage with different configurations

+ +
+
+
+
+
+
+ + + + + + + + + diff --git a/demo/js/demo.js b/demo/js/demo.js new file mode 100644 index 0000000..6303c8e --- /dev/null +++ b/demo/js/demo.js @@ -0,0 +1,602 @@ +/** + * Owen Animation System Demo - Main JavaScript + * + * This file provides the interactive functionality for the demo pages. + * It demonstrates the core features of the Owen Animation System. + */ + +// Import Owen Animation System (simulated for demo) +// In a real implementation, this would import from the actual package +const OwenDemo = { + // Mock AnimationNameMapper for demo purposes + AnimationNameMapper: class { + constructor () { + this.animations = { + legacy: [ + 'walk_forward', 'walk_backward', 'run_fast', 'run_slow', + 'jump_high', 'jump_low', 'idle_breathing', 'idle_looking', + 'attack_sword', 'attack_bow', 'defend_shield', 'defend_dodge', + 'death_forward', 'death_backward', 'hurt_light', 'hurt_heavy', + 'climb_up', 'climb_down', 'swim_forward', 'swim_idle' + ], + artist: [ + 'WalkForward', 'WalkBackward', 'RunFast', 'RunSlow', + 'JumpHigh', 'JumpLow', 'IdleBreathing', 'IdleLooking', + 'AttackSword', 'AttackBow', 'DefendShield', 'DefendDodge', + 'DeathForward', 'DeathBackward', 'HurtLight', 'HurtHeavy', + 'ClimbUp', 'ClimbDown', 'SwimForward', 'SwimIdle' + ], + hierarchical: [ + 'character.movement.walk.forward', 'character.movement.walk.backward', + 'character.movement.run.fast', 'character.movement.run.slow', + 'character.movement.jump.high', 'character.movement.jump.low', + 'character.idle.breathing', 'character.idle.looking', + 'character.combat.attack.sword', 'character.combat.attack.bow', + 'character.combat.defend.shield', 'character.combat.defend.dodge', + 'character.state.death.forward', 'character.state.death.backward', + 'character.state.hurt.light', 'character.state.hurt.heavy', + 'character.movement.climb.up', 'character.movement.climb.down', + 'character.movement.swim.forward', 'character.movement.swim.idle' + ], + semantic: [ + 'character_walk_forward', 'character_walk_backward', + 'character_run_fast', 'character_run_slow', + 'character_jump_high', 'character_jump_low', + 'character_idle_breathing', 'character_idle_looking', + 'character_attack_sword', 'character_attack_bow', + 'character_defend_shield', 'character_defend_dodge', + 'character_death_forward', 'character_death_backward', + 'character_hurt_light', 'character_hurt_heavy', + 'character_climb_up', 'character_climb_down', + 'character_swim_forward', 'character_swim_idle' + ] + } + + // Create conversion mappings + this.conversionMap = this.createConversionMap() + } + + createConversionMap () { + const map = {} + const schemes = Object.keys(this.animations) + + schemes.forEach(scheme => { + map[scheme] = {} + schemes.forEach(targetScheme => { + map[scheme][targetScheme] = {} + this.animations[scheme].forEach((anim, index) => { + map[scheme][targetScheme][anim] = this.animations[targetScheme][index] + }) + }) + }) + + return map + } + + getAllAnimationsByScheme (scheme) { + return this.animations[scheme] || [] + } + + convert (animationName, targetScheme, sourceScheme = null) { + // If no source scheme provided, try to detect it + if (!sourceScheme) { + sourceScheme = this.detectScheme(animationName) + } + + if (!sourceScheme) { + throw new Error(`Unable to detect scheme for animation: ${animationName}`) + } + + if (!this.conversionMap[sourceScheme] || !this.conversionMap[sourceScheme][targetScheme]) { + throw new Error(`Conversion from ${sourceScheme} to ${targetScheme} not supported`) + } + + const converted = this.conversionMap[sourceScheme][targetScheme][animationName] + if (!converted) { + throw new Error(`Animation "${animationName}" not found in ${sourceScheme} scheme`) + } + + return converted + } + + detectScheme (animationName) { + for (const [scheme, animations] of Object.entries(this.animations)) { + if (animations.includes(animationName)) { + return scheme + } + } + return null + } + + convertBatch (animations, sourceScheme, targetScheme) { + return animations.map(anim => { + try { + return { + original: anim, + converted: this.convert(anim, targetScheme, sourceScheme), + success: true + } + } catch (error) { + return { + original: anim, + error: error.message, + success: false + } + } + }) + } + + suggestCorrections (animationName, scheme) { + const animations = this.animations[scheme] || [] + return animations.filter(anim => + anim.toLowerCase().includes(animationName.toLowerCase()) || + animationName.toLowerCase().includes(anim.toLowerCase()) + ).slice(0, 3) + } + }, + + // Mock OwenAnimationContext for demo purposes + OwenAnimationContext: class { + constructor (options = {}) { + this.namingScheme = options.namingScheme || 'semantic' + this.autoConvert = options.autoConvert !== false + this.container = options.container + this.currentAnimation = null + this.isPlaying = false + this.mapper = new OwenDemo.AnimationNameMapper() + } + + async loadModel (modelPath) { + // Simulate model loading + await new Promise(resolve => setTimeout(resolve, 1000)) + console.log('Model loaded:', modelPath) + } + + async playAnimation (animationName) { + try { + // Convert animation name if needed + let targetName = animationName + if (this.autoConvert) { + const detectedScheme = this.mapper.detectScheme(animationName) + if (detectedScheme && detectedScheme !== this.namingScheme) { + targetName = this.mapper.convert(animationName, this.namingScheme, detectedScheme) + } + } + + this.currentAnimation = targetName + this.isPlaying = true + + console.log(`Playing animation: ${targetName} (original: ${animationName})`) + + // Simulate animation playback + return new Promise(resolve => { + setTimeout(() => { + console.log(`Animation ${targetName} completed`) + resolve() + }, 2000) + }) + } catch (error) { + console.error('Failed to play animation:', error) + throw error + } + } + + stopAnimation () { + this.isPlaying = false + this.currentAnimation = null + console.log('Animation stopped') + } + + dispose () { + this.stopAnimation() + console.log('Animation context disposed') + } + } +} + +// Demo Application State +const DemoState = { + currentScheme: 'semantic', + selectedAnimation: null, + animationContext: null, + mapper: new OwenDemo.AnimationNameMapper(), + + init () { + this.setupEventListeners() + this.updateAnimationList() + this.setupConversionTool() + this.setupTabSwitching() + this.initAnimationContext() + }, + + setupEventListeners () { + // Naming scheme change + const schemeSelect = document.getElementById('naming-scheme') + if (schemeSelect) { + schemeSelect.addEventListener('change', (e) => { + this.currentScheme = e.target.value + this.updateAnimationList() + this.updateCodeExamples() + }) + } + + // Animation selection + const animationSelect = document.getElementById('animation-select') + if (animationSelect) { + animationSelect.addEventListener('change', (e) => { + this.selectedAnimation = e.target.value + this.updatePlayButtons() + }) + } + + // Playback controls + const playBtn = document.getElementById('play-animation') + const pauseBtn = document.getElementById('pause-animation') + const stopBtn = document.getElementById('stop-animation') + + if (playBtn) { + playBtn.addEventListener('click', () => this.playSelectedAnimation()) + } + if (pauseBtn) { + pauseBtn.addEventListener('click', () => this.pauseAnimation()) + } + if (stopBtn) { + stopBtn.addEventListener('click', () => this.stopAnimation()) + } + + // Start demo button + const startBtn = document.getElementById('start-demo') + if (startBtn) { + startBtn.addEventListener('click', () => this.startInteractiveDemo()) + } + }, + + updateAnimationList () { + const select = document.getElementById('animation-select') + if (!select) return + + const animations = this.mapper.getAllAnimationsByScheme(this.currentScheme) + + select.innerHTML = '' + animations.forEach(anim => { + const option = document.createElement('option') + option.value = anim + option.textContent = anim + select.appendChild(option) + }) + + this.updatePlayButtons() + }, + + updatePlayButtons () { + const hasSelection = !!this.selectedAnimation + const playBtn = document.getElementById('play-animation') + const pauseBtn = document.getElementById('pause-animation') + const stopBtn = document.getElementById('stop-animation') + + if (playBtn) playBtn.disabled = !hasSelection + if (pauseBtn) pauseBtn.disabled = !hasSelection + if (stopBtn) stopBtn.disabled = !hasSelection + }, + + setupConversionTool () { + const input = document.getElementById('input-animation') + const schemeSelect = document.getElementById('input-scheme') + const convertBtn = document.getElementById('convert-btn') + + if (!input || !schemeSelect || !convertBtn) return + + const updateConversion = () => { + const animationName = input.value.trim() + const sourceScheme = schemeSelect.value + + if (!animationName) { + this.clearConversionResults() + return + } + + this.performConversion(animationName, sourceScheme) + } + + input.addEventListener('input', updateConversion) + schemeSelect.addEventListener('change', updateConversion) + convertBtn.addEventListener('click', updateConversion) + }, + + performConversion (animationName, sourceScheme) { + const results = { + legacy: '-', + artist: '-', + hierarchical: '-', + semantic: '-' + } + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + schemes.forEach(targetScheme => { + try { + results[targetScheme] = this.mapper.convert(animationName, targetScheme, sourceScheme) + } catch (error) { + results[targetScheme] = `Error: ${error.message}` + } + }) + + this.displayConversionResults(results) + }, + + displayConversionResults (results) { + Object.entries(results).forEach(([scheme, result]) => { + const element = document.getElementById(`result-${scheme}`) + if (element) { + element.textContent = result + element.className = result.startsWith('Error:') ? 'error' : 'success' + } + }) + }, + + clearConversionResults () { + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + schemes.forEach(scheme => { + const element = document.getElementById(`result-${scheme}`) + if (element) { + element.textContent = '-' + element.className = '' + } + }) + }, + + setupTabSwitching () { + const tabButtons = document.querySelectorAll('.tab-button') + const tabPanes = document.querySelectorAll('.tab-pane') + + tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + const targetTab = e.target.dataset.tab + + // Update button states + tabButtons.forEach(btn => btn.classList.remove('active')) + e.target.classList.add('active') + + // Update pane visibility + tabPanes.forEach(pane => { + pane.classList.remove('active') + if (pane.id === `${targetTab}-tab`) { + pane.classList.add('active') + } + }) + + this.updateCodeExamples() + }) + }) + }, + + updateCodeExamples () { + // Update code examples based on current scheme and selection + const jsOutput = document.getElementById('js-code-output') + const reactOutput = document.getElementById('react-code-output') + const vueOutput = document.getElementById('vue-code-output') + + if (jsOutput) { + jsOutput.textContent = this.generateJavaScriptExample() + } + if (reactOutput) { + reactOutput.textContent = this.generateReactExample() + } + if (vueOutput) { + vueOutput.textContent = this.generateVueExample() + } + }, + + generateJavaScriptExample () { + const animation = this.selectedAnimation || 'character_walk_forward' + return `import { OwenAnimationContext } from '@kjanat/owen' + +// Initialize with ${this.currentScheme} naming scheme +const animationContext = new OwenAnimationContext({ + namingScheme: '${this.currentScheme}', + autoConvert: true +}) + +// Load your character model +await animationContext.loadModel('./path/to/character.gltf') + +// Play animation using ${this.currentScheme} scheme +await animationContext.playAnimation('${animation}') + +// The system automatically handles conversions between schemes +// You can use any naming scheme and it will convert automatically` + }, + + generateReactExample () { + const animation = this.selectedAnimation || 'character_walk_forward' + return `import React, { useEffect, useRef, useState } from 'react' +import { OwenAnimationContext } from '@kjanat/owen' + +export function AnimatedCharacter() { + const containerRef = useRef() + const [animationContext, setAnimationContext] = useState(null) + const [currentAnimation, setCurrentAnimation] = useState('${animation}') + + useEffect(() => { + const context = new OwenAnimationContext({ + namingScheme: '${this.currentScheme}', + container: containerRef.current + }) + + context.loadModel('./character.gltf').then(() => { + setAnimationContext(context) + }) + + return () => context?.dispose() + }, []) + + const playAnimation = async (animationName) => { + if (animationContext) { + await animationContext.playAnimation(animationName) + setCurrentAnimation(animationName) + } + } + + return ( +
+
+ +
+ ) +}` + }, + + generateVueExample () { + const animation = this.selectedAnimation || 'character_walk_forward' + return ` + +` + }, + + async initAnimationContext () { + const canvas = document.getElementById('demo-canvas') + if (!canvas) return + + this.animationContext = new OwenDemo.OwenAnimationContext({ + namingScheme: this.currentScheme, + container: canvas + }) + + // Simulate model loading + try { + await this.animationContext.loadModel('basic-character.gltf') + console.log('Demo character loaded successfully') + } catch (error) { + console.error('Failed to load demo character:', error) + } + }, + + async playSelectedAnimation () { + if (!this.selectedAnimation || !this.animationContext) return + + try { + await this.animationContext.playAnimation(this.selectedAnimation) + } catch (error) { + console.error('Failed to play animation:', error) + window.alert(`Failed to play animation: ${error.message}`) + } + }, + + pauseAnimation () { + // In a real implementation, this would pause the current animation + console.log('Animation paused') + }, + + stopAnimation () { + if (this.animationContext) { + this.animationContext.stopAnimation() + } + }, + + startInteractiveDemo () { + // Navigate to interactive page or start guided tour + if (window.location.pathname.includes('index.html') || window.location.pathname === '/') { + window.location.href = 'interactive.html' + } else { + // Already on a page with interactive features + this.scrollToSection('.live-demo-section') + } + }, + + scrollToSection (selector) { + const element = document.querySelector(selector) + if (element) { + element.scrollIntoView({ behavior: 'smooth' }) + } + } +} + +// Copy code functionality +function setupCodeCopying () { + document.querySelectorAll('.copy-code-btn').forEach(button => { + button.addEventListener('click', async (e) => { + const targetId = e.target.dataset.target + const codeElement = document.getElementById(targetId) + + if (codeElement) { + try { + await navigator.clipboard.writeText(codeElement.textContent) + + // Visual feedback + const originalText = e.target.textContent + e.target.textContent = 'Copied!' + e.target.style.background = 'var(--success-color)' + + setTimeout(() => { + e.target.textContent = originalText + e.target.style.background = '' + }, 2000) + } catch (error) { + console.error('Failed to copy code:', error) + window.alert('Failed to copy code to clipboard') + } + } + }) + }) +} + +// Initialize demo when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + DemoState.init() + setupCodeCopying() + + // Add some visual feedback for interactions + document.addEventListener('click', (e) => { + if (e.target.classList.contains('btn')) { + e.target.style.transform = 'scale(0.98)' + setTimeout(() => { + e.target.style.transform = '' + }, 150) + } + }) +}) + +// Export for use in other demo files +if (typeof module !== 'undefined' && module.exports) { + module.exports = { OwenDemo, DemoState } +} else { + window.OwenDemo = OwenDemo + window.DemoState = DemoState +} diff --git a/demo/styles/comparison.css b/demo/styles/comparison.css new file mode 100644 index 0000000..8a18921 --- /dev/null +++ b/demo/styles/comparison.css @@ -0,0 +1,412 @@ +/* Comparison Page Specific Styles */ + +.comparison-container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.comparison-intro { + text-align: center; + margin-bottom: 3rem; +} + +.comparison-intro h1 { + color: var(--text-primary); + margin-bottom: 1rem; +} + +.comparison-intro p { + color: var(--text-secondary); + font-size: 1.125rem; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +.scheme-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.scheme-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.scheme-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background: var(--scheme-accent, var(--accent-color)); +} + +.scheme-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.scheme-card.legacy { + --scheme-accent: #8b5cf6; +} +.scheme-card.artist { + --scheme-accent: #06b6d4; +} +.scheme-card.hierarchical { + --scheme-accent: #10b981; +} +.scheme-card.semantic { + --scheme-accent: #f59e0b; +} + +.scheme-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.scheme-icon { + width: 40px; + height: 40px; + border-radius: 8px; + background: var(--scheme-accent); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 1.25rem; +} + +.scheme-name { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.scheme-description { + color: var(--text-secondary); + margin-bottom: 1rem; + line-height: 1.6; +} + +.scheme-example { + background: var(--bg-code); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 0.75rem; + font-family: "Fira Code", monospace; + font-size: 0.875rem; + color: var(--text-primary); + margin-bottom: 1rem; +} + +.scheme-pros { + list-style: none; + padding: 0; + margin: 0; +} + +.scheme-pros li { + padding: 0.25rem 0; + padding-left: 1.5rem; + position: relative; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.scheme-pros li::before { + content: "āœ“"; + position: absolute; + left: 0; + color: var(--success-text); + font-weight: bold; +} + +.comparison-table-section { + margin: 3rem 0; +} + +.table-controls { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; + align-items: center; +} + +.table-filter { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.table-filter:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.table-filter.active { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.search-input { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 6px; + font-size: 0.875rem; + min-width: 200px; +} + +.search-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-color-alpha); +} + +.comparison-table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.comparison-table th, +.comparison-table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.comparison-table th { + background: var(--bg-tertiary); + font-weight: 600; + color: var(--text-primary); + position: sticky; + top: 0; + z-index: 10; +} + +.comparison-table td { + color: var(--text-secondary); + font-family: "Fira Code", monospace; + font-size: 0.875rem; +} + +.comparison-table tbody tr:hover { + background: var(--bg-hover); +} + +.comparison-table tbody tr:last-child td { + border-bottom: none; +} + +.scheme-label { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.scheme-label.legacy { + background: #8b5cf6; +} +.scheme-label.artist { + background: #06b6d4; +} +.scheme-label.hierarchical { + background: #10b981; +} +.scheme-label.semantic { + background: #f59e0b; +} + +.conversion-demo { + margin: 3rem 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 2rem; +} + +.conversion-controls { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 1rem; + align-items: center; + margin-bottom: 2rem; +} + +.scheme-selector { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.scheme-selector label { + font-weight: 500; + color: var(--text-primary); + font-size: 0.875rem; +} + +.scheme-select { + padding: 0.75rem; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: 6px; + font-size: 0.875rem; +} + +.conversion-arrow { + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-color); + font-size: 1.5rem; + font-weight: bold; +} + +.animation-input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: 6px; + font-family: "Fira Code", monospace; + font-size: 0.875rem; +} + +.conversion-result { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + margin-top: 1rem; +} + +.result-label { + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.result-value { + font-family: "Fira Code", monospace; + font-size: 1rem; + color: var(--text-primary); + background: var(--bg-code); + padding: 0.75rem; + border-radius: 4px; + word-break: break-all; +} + +.performance-comparison { + margin: 3rem 0; +} + +.performance-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.performance-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + text-align: center; +} + +.performance-metric { + font-size: 2rem; + font-weight: bold; + color: var(--accent-color); + margin-bottom: 0.5rem; +} + +.performance-label { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.no-results { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-style: italic; +} + +@media (max-width: 768px) { + .comparison-container { + padding: 1rem; + } + + .scheme-overview { + grid-template-columns: 1fr; + gap: 1rem; + } + + .table-controls { + flex-direction: column; + align-items: stretch; + } + + .search-input { + min-width: auto; + } + + .comparison-table { + font-size: 0.75rem; + } + + .comparison-table th, + .comparison-table td { + padding: 0.75rem 0.5rem; + } + + .conversion-controls { + grid-template-columns: 1fr; + text-align: center; + } + + .conversion-arrow { + transform: rotate(90deg); + } + + .performance-grid { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/demo/styles/demo.css b/demo/styles/demo.css new file mode 100644 index 0000000..41aba1d --- /dev/null +++ b/demo/styles/demo.css @@ -0,0 +1,302 @@ +/* Demo-specific styles */ + +/* Hero Section */ +.hero-section { + padding: 4rem 0; + background: linear-gradient( + 135deg, + var(--bg-secondary) 0%, + var(--bg-tertiary) 100% + ); + margin-bottom: 3rem; +} + +.hero-content { + text-align: center; + margin-bottom: 3rem; +} + +.hero-content h2 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.hero-content p { + font-size: 1.125rem; + color: var(--text-secondary); + margin-bottom: 2rem; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-actions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.hero-visual { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 2rem; + align-items: start; +} + +#demo-canvas { + width: 100%; + height: 400px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-primary); +} + +.demo-controls { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1.5rem; + box-shadow: var(--shadow-sm); +} + +.control-group { + margin-bottom: 1.5rem; +} + +.control-group:last-child { + margin-bottom: 0; +} + +.control-group label { + margin-bottom: 0.5rem; + font-weight: 600; + font-size: 0.875rem; +} + +.control-group .btn { + margin-right: 0.5rem; + margin-bottom: 0.5rem; +} + +/* Features Section */ +.features-section { + padding: 3rem 0; + margin-bottom: 3rem; +} + +.features-section h3 { + text-align: center; + font-size: 2rem; + margin-bottom: 2rem; + color: var(--text-primary); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.feature-card { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 2rem; + text-align: center; + box-shadow: var(--shadow-sm); + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.feature-icon { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.feature-card h4 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.feature-card p { + color: var(--text-secondary); + line-height: 1.6; +} + +/* Live Demo Section */ +.live-demo-section { + padding: 3rem 0; + background: var(--bg-secondary); + border-radius: var(--border-radius); + margin-bottom: 3rem; +} + +.live-demo-section h3 { + text-align: center; + font-size: 1.75rem; + margin-bottom: 2rem; + color: var(--text-primary); +} + +.conversion-demo { + max-width: 800px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: start; +} + +.input-section { + background: var(--bg-primary); + padding: 1.5rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); +} + +.input-section label { + margin-bottom: 0.5rem; + margin-top: 1rem; +} + +.input-section label:first-child { + margin-top: 0; +} + +.conversion-results { + background: var(--bg-primary); + padding: 1.5rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); +} + +.conversion-results h4 { + margin-bottom: 1rem; + color: var(--text-primary); +} + +.scheme-results { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.scheme-result { + padding: 0.75rem; + background: var(--bg-tertiary); + border-radius: var(--border-radius); + font-family: var(--font-mono); + font-size: 0.875rem; +} + +.scheme-result strong { + color: var(--primary-color); + margin-right: 0.5rem; +} + +/* Code Examples Section */ +.code-examples-section { + padding: 3rem 0; + margin-bottom: 3rem; +} + +.code-examples-section h3 { + text-align: center; + font-size: 1.75rem; + margin-bottom: 2rem; + color: var(--text-primary); +} + +.code-tabs { + display: flex; + justify-content: center; + margin-bottom: 2rem; + border-bottom: 1px solid var(--border-color); +} + +.tab-button { + background: none; + border: none; + padding: 1rem 2rem; + cursor: pointer; + font-weight: 500; + color: var(--text-secondary); + border-bottom: 2px solid transparent; + transition: all 0.2s ease; +} + +.tab-button:hover { + color: var(--text-primary); +} + +.tab-button.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.tab-content { + max-width: 900px; + margin: 0 auto; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +.tab-pane pre { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1.5rem; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.6; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .hero-visual { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .conversion-demo { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .hero-content h2 { + font-size: 2rem; + } + + .hero-content p { + font-size: 1rem; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .code-tabs { + flex-wrap: wrap; + gap: 0.5rem; + } + + .tab-button { + padding: 0.75rem 1.5rem; + } +} diff --git a/demo/styles/examples.css b/demo/styles/examples.css new file mode 100644 index 0000000..98b36a2 --- /dev/null +++ b/demo/styles/examples.css @@ -0,0 +1,306 @@ +/* Examples Page Specific Styles */ + +.examples-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.examples-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.example-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +.example-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.example-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.framework-icon { + width: 32px; + height: 32px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: white; + font-size: 0.875rem; +} + +.react-icon { + background: #61dafb; + color: #000; +} +.vue-icon { + background: #4fc08d; +} +.node-icon { + background: #339933; +} +.vanilla-icon { + background: #f7df1e; + color: #000; +} +.blender-icon { + background: #e87d0d; +} + +.example-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.example-description { + color: var(--text-secondary); + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.code-example { + background: var(--bg-code); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; + margin-bottom: 1rem; +} + +.code-header { + background: var(--bg-tertiary); + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.code-language { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); +} + +.copy-button { + background: none; + border: 1px solid var(--border-color); + color: var(--text-secondary); + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.copy-button:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.copy-button.copied { + background: var(--success-bg); + color: var(--success-text); + border-color: var(--success-border); +} + +.code-content { + padding: 1rem; + font-family: "Fira Code", "Monaco", "Consolas", monospace; + font-size: 0.875rem; + line-height: 1.5; + color: var(--text-primary); + overflow-x: auto; +} + +.integration-steps { + margin-top: 1.5rem; +} + +.step { + display: flex; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: 8px; + border-left: 4px solid var(--accent-color); +} + +.step-number { + background: var(--accent-color); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.step-content h4 { + margin: 0 0 0.5rem 0; + color: var(--text-primary); + font-size: 1rem; +} + +.step-content p { + margin: 0; + color: var(--text-secondary); + line-height: 1.5; +} + +.example-navigation { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.nav-filter { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.nav-filter:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.nav-filter.active { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.features-list { + list-style: none; + padding: 0; + margin: 1rem 0; +} + +.features-list li { + padding: 0.5rem 0; + padding-left: 1.5rem; + position: relative; + color: var(--text-secondary); +} + +.features-list li::before { + content: "āœ“"; + position: absolute; + left: 0; + color: var(--success-text); + font-weight: bold; +} + +.download-section { + margin-top: 2rem; + padding: 1.5rem; + background: var(--bg-tertiary); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.download-links { + display: flex; + gap: 1rem; + margin-top: 1rem; + flex-wrap: wrap; +} + +.download-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: var(--accent-color); + color: white; + text-decoration: none; + border-radius: 6px; + font-weight: 500; + transition: all 0.2s ease; +} + +.download-link:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +.error-boundary { + border: 2px dashed var(--error-border); + background: var(--error-bg); + color: var(--error-text); + padding: 1rem; + border-radius: 8px; + margin: 1rem 0; + text-align: center; +} + +@media (max-width: 768px) { + .examples-container { + padding: 1rem; + } + + .examples-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .example-card { + padding: 1rem; + } + + .example-navigation { + gap: 0.5rem; + } + + .nav-filter { + padding: 0.375rem 0.75rem; + font-size: 0.8rem; + } + + .download-links { + flex-direction: column; + } + + .step { + flex-direction: column; + gap: 0.5rem; + } + + .step-number { + align-self: flex-start; + } +} diff --git a/demo/styles/interactive.css b/demo/styles/interactive.css new file mode 100644 index 0000000..8928174 --- /dev/null +++ b/demo/styles/interactive.css @@ -0,0 +1,444 @@ +/* Interactive Playground Specific Styles */ + +.interactive-container { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; + display: grid; + grid-template-areas: + "header header" + "controls playground" + "results results"; + grid-template-columns: 300px 1fr; + grid-template-rows: auto 1fr auto; + gap: 2rem; + min-height: calc(100vh - 4rem); +} + +.playground-header { + grid-area: header; + text-align: center; + margin-bottom: 1rem; +} + +.playground-header h1 { + color: var(--text-primary); + margin-bottom: 1rem; +} + +.playground-header p { + color: var(--text-secondary); + font-size: 1.125rem; + max-width: 600px; + margin: 0 auto; + line-height: 1.6; +} + +.playground-controls { + grid-area: controls; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + height: fit-content; + box-shadow: var(--shadow-sm); +} + +.controls-section { + margin-bottom: 2rem; +} + +.controls-section:last-child { + margin-bottom: 0; +} + +.section-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); +} + +.control-group { + margin-bottom: 1rem; +} + +.control-label { + display: block; + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; + font-weight: 500; +} + +.control-input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: 6px; + font-size: 0.875rem; + transition: all 0.2s ease; +} + +.control-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-color-alpha); +} + +.control-textarea { + min-height: 100px; + resize: vertical; + font-family: "Fira Code", monospace; +} + +.scheme-buttons { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.scheme-button { + padding: 0.75rem; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.75rem; + text-align: center; + font-weight: 500; +} + +.scheme-button:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.scheme-button.active { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.action-buttons { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.action-button { + padding: 0.75rem 1rem; + border: 1px solid var(--accent-color); + background: var(--accent-color); + color: white; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + font-size: 0.875rem; +} + +.action-button:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.action-button.secondary { + background: transparent; + color: var(--accent-color); +} + +.action-button.secondary:hover { + background: var(--accent-color-alpha); +} + +.action-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.playground-main { + grid-area: playground; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; +} + +.playground-tabs { + display: flex; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); +} + +.playground-tab { + padding: 1rem 1.5rem; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + border-bottom: 2px solid transparent; +} + +.playground-tab:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.playground-tab.active { + color: var(--accent-color); + border-bottom-color: var(--accent-color); + background: var(--bg-secondary); +} + +.playground-content { + flex: 1; + padding: 1.5rem; + overflow: auto; +} + +.code-editor { + width: 100%; + height: 400px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-code); + color: var(--text-primary); + font-family: "Fira Code", monospace; + font-size: 0.875rem; + padding: 1rem; + resize: vertical; + line-height: 1.5; +} + +.output-panel { + background: var(--bg-code); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + font-family: "Fira Code", monospace; + font-size: 0.875rem; + line-height: 1.5; + color: var(--text-primary); + white-space: pre-wrap; + overflow: auto; + max-height: 400px; +} + +.conversion-preview { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + margin-top: 1rem; +} + +.preview-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-color); +} + +.preview-item:last-child { + border-bottom: none; +} + +.preview-input { + font-family: "Fira Code", monospace; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.preview-output { + font-family: "Fira Code", monospace; + color: var(--text-primary); + font-size: 0.875rem; + font-weight: 500; +} + +.performance-monitor { + grid-area: results; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow-sm); +} + +.monitor-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.monitor-card { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + text-align: center; +} + +.monitor-value { + font-size: 1.5rem; + font-weight: bold; + color: var(--accent-color); + margin-bottom: 0.5rem; +} + +.monitor-label { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 0.5rem; +} + +.status-indicator.success { + background: var(--success-color); +} +.status-indicator.warning { + background: var(--warning-color); +} +.status-indicator.error { + background: var(--error-color); +} + +.error-message { + background: var(--error-bg); + color: var(--error-text); + border: 1px solid var(--error-border); + border-radius: 6px; + padding: 1rem; + margin: 1rem 0; +} + +.success-message { + background: var(--success-bg); + color: var(--success-text); + border: 1px solid var(--success-border); + border-radius: 6px; + padding: 1rem; + margin: 1rem 0; +} + +.loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid var(--border-color); + border-top: 2px solid var(--accent-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.history-panel { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-tertiary); +} + +.history-item { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + cursor: pointer; + transition: background 0.2s ease; +} + +.history-item:hover { + background: var(--bg-hover); +} + +.history-item:last-child { + border-bottom: none; +} + +.history-input { + font-family: "Fira Code", monospace; + font-size: 0.875rem; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.history-meta { + font-size: 0.75rem; + color: var(--text-secondary); +} + +@media (max-width: 1024px) { + .interactive-container { + grid-template-areas: + "header" + "controls" + "playground" + "results"; + grid-template-columns: 1fr; + padding: 1rem; + } + + .playground-controls { + height: auto; + } + + .scheme-buttons { + grid-template-columns: 1fr; + } + + .monitor-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .playground-tabs { + overflow-x: auto; + } + + .playground-tab { + padding: 0.75rem 1rem; + white-space: nowrap; + } + + .code-editor { + height: 300px; + } + + .monitor-grid { + grid-template-columns: 1fr; + } + + .action-buttons { + gap: 0.5rem; + } +} diff --git a/demo/styles/main.css b/demo/styles/main.css new file mode 100644 index 0000000..6d57e7d --- /dev/null +++ b/demo/styles/main.css @@ -0,0 +1,472 @@ +/* Owen Animation System Demo - Main Styles */ + +/* CSS Variables for consistent theming */ +:root { + --primary-color: #2563eb; + --primary-hover: #1d4ed8; + --secondary-color: #64748b; + --accent-color: #0ea5e9; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; + + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-muted: #94a3b8; + + --border-color: #e2e8f0; + --border-radius: 8px; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); + + --font-family: + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-mono: + "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace; +} + +/* Dark mode variables */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #64748b; + --border-color: #334155; + } +} + +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-family); + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-primary); + transition: background-color 0.3s ease; +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Header */ +.demo-header { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + padding: 1rem 0; + position: sticky; + top: 0; + z-index: 100; +} + +.demo-header .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + display: flex; + flex-direction: column; + line-height: 1.2; +} + +.logo-text { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.logo-subtitle { + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; +} + +.demo-nav { + display: flex; + gap: 2rem; +} + +.nav-link { + text-decoration: none; + color: var(--text-secondary); + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + transition: all 0.2s ease; +} + +.nav-link:hover { + color: var(--primary-color); + background-color: var(--bg-tertiary); +} + +.nav-link.active { + color: var(--primary-color); + background-color: var(--primary-color); + color: white; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + font-size: 0.875rem; + font-weight: 500; + border: none; + border-radius: var(--border-radius); + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; + gap: 0.5rem; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-hover); +} + +.btn-secondary { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background-color: var(--bg-secondary); +} + +.btn-small { + padding: 0.5rem 1rem; + font-size: 0.8rem; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Form elements */ +input, +select, +textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background-color: var(--bg-primary); + color: var(--text-primary); + font-size: 0.875rem; + transition: border-color 0.2s ease; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1); +} + +label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--text-secondary); +} + +/* Cards */ +.card { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1.5rem; + box-shadow: var(--shadow-sm); +} + +/* Grid layouts */ +.grid { + display: grid; + gap: 1.5rem; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.grid-4 { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +/* Code blocks */ +pre { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; + overflow-x: auto; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.5; +} + +code { + font-family: var(--font-mono); + font-size: 0.875rem; + background: var(--bg-tertiary); + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; +} + +pre code { + background: none; + padding: 0; +} + +/* Utility classes */ +.text-center { + text-align: center; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} + +.mb-1 { + margin-bottom: 0.25rem; +} +.mb-2 { + margin-bottom: 0.5rem; +} +.mb-3 { + margin-bottom: 1rem; +} +.mb-4 { + margin-bottom: 1.5rem; +} +.mb-5 { + margin-bottom: 2rem; +} + +.mt-1 { + margin-top: 0.25rem; +} +.mt-2 { + margin-top: 0.5rem; +} +.mt-3 { + margin-top: 1rem; +} +.mt-4 { + margin-top: 1.5rem; +} +.mt-5 { + margin-top: 2rem; +} + +.p-1 { + padding: 0.25rem; +} +.p-2 { + padding: 0.5rem; +} +.p-3 { + padding: 1rem; +} +.p-4 { + padding: 1.5rem; +} +.p-5 { + padding: 2rem; +} + +.hidden { + display: none; +} +.visible { + display: block; +} + +/* Animation classes */ +.fade-in { + animation: fadeIn 0.3s ease-in-out; +} + +.slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Loading spinner */ +.spinner { + width: 24px; + height: 24px; + border: 2px solid var(--border-color); + border-top: 2px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Progress bar */ +.progress-bar { + width: 100%; + height: 8px; + background-color: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: var(--primary-color); + transition: width 0.3s ease; + width: 0%; +} + +/* Footer */ +.demo-footer { + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); + padding: 2rem 0; + margin-top: 4rem; +} + +.footer-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; +} + +.footer-section h4 { + color: var(--text-primary); + margin-bottom: 1rem; + font-size: 1rem; +} + +.footer-section ul { + list-style: none; + padding: 0; +} + +.footer-section li { + margin-bottom: 0.5rem; +} + +.footer-section a { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s ease; +} + +.footer-section a:hover { + color: var(--primary-color); +} + +.footer-bottom { + text-align: center; + padding-top: 2rem; + border-top: 1px solid var(--border-color); + color: var(--text-muted); + font-size: 0.875rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .container { + padding: 0 0.75rem; + } + + .demo-header .container { + flex-direction: column; + gap: 1rem; + } + + .demo-nav { + gap: 1rem; + } + + .grid-2, + .grid-3, + .grid-4 { + grid-template-columns: 1fr; + } + + .btn { + width: 100%; + justify-content: center; + } +} + +@media (max-width: 480px) { + .demo-nav { + flex-wrap: wrap; + gap: 0.5rem; + } + + .nav-link { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + } + + .footer-content { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} diff --git a/examples/basic-demo/basic-demo.js b/examples/basic-demo/basic-demo.js index 5fd6289..64b5b70 100644 --- a/examples/basic-demo/basic-demo.js +++ b/examples/basic-demo/basic-demo.js @@ -177,16 +177,16 @@ class OwenDemo { switch (event.key) { case '1': - this.owenSystem.transitionTo(States.WAITING) + this.owenSystem.transitionTo(States.WAITING) break case '2': - this.owenSystem.transitionTo(States.REACTING) + this.owenSystem.transitionTo(States.REACTING) break case '3': - this.owenSystem.transitionTo(States.TYPING) + this.owenSystem.transitionTo(States.TYPING) break case '4': - this.owenSystem.transitionTo(States.SLEEPING) + this.owenSystem.transitionTo(States.SLEEPING) break case ' ': this.sendTestMessage() diff --git a/examples/basic-demo/simple-example.js b/examples/basic-demo/simple-example.js index ce07c1b..a897041 100644 --- a/examples/basic-demo/simple-example.js +++ b/examples/basic-demo/simple-example.js @@ -92,7 +92,7 @@ class SimpleOwenExample { * @returns {Promise} */ async demonstrateStateTransitions () { - const states = [ States.REACTING, States.TYPING, States.WAITING, States.SLEEPING ] + const states = [States.REACTING, States.TYPING, States.WAITING, States.SLEEPING] for (const state of states) { console.log(`šŸ”„ Transitioning to ${state.toUpperCase()} state...`) diff --git a/examples/mock-demo/owen_test_demo.html b/examples/mock-demo/owen_test_demo.html index 8d6f378..2b500cd 100644 --- a/examples/mock-demo/owen_test_demo.html +++ b/examples/mock-demo/owen_test_demo.html @@ -1,8 +1,8 @@ - - - + + + Owen Animation System Demo - Implementation Test - - + +
-
-

šŸ¤– Owen Animation System Demo

+
+

šŸ¤– Owen Animation System Demo

-
- - - -
Owen is typing...
-
- -
- - - - - -
- -
- -
- - - - - -
-
- -
- - - - - -
- -
- - - - -
- -
- -
-
System initializing...
-
-
+
+ + + +
+ Owen is typing... +
-
-
Owen System Status
-
State: Initializing
-
Emotion: Neutral
-
Last Activity: Now
-
Active Clips: 0
+
+ + + + +
-
-
- - - - - - - - - - -
-
Initializing...
-
Neutral
+
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+ + + + +
+ +
+ + + + + +
+ +
+ +
+
Enter an animation name above to see conversions...
+
+
+ +
+ + + + + +
+ +
+ +
+
System initializing...
+
+
+
+ +
+
Owen System Status
+
+ State: Initializing +
+
+ Emotion: Neutral +
+
+ Last Activity: Now +
+
+ Active Clips: 0 +
+
+ +
+
+ + + + + + + + + + +
+
Initializing...
+
Neutral
+
- + diff --git a/package-lock.json b/package-lock.json index b1c346f..42e356b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { - "name": "owen-animation-system", - "version": "1.0.0", + "name": "@kjanat/owen", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "owen-animation-system", - "version": "1.0.0", + "name": "@kjanat/owen", + "version": "1.0.1", "license": "AGPL-3.0-only OR LicenseRef-Commercial", "dependencies": { "three": "^0.176.0" }, "devDependencies": { + "@playwright/test": "^1.52.0", "jsdoc": "^4.0.2", "pre-commit": "^1.2.2", "standard": "*", @@ -790,6 +791,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", @@ -4126,6 +4143,53 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index e7d05bd..3cc0e94 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,65 @@ { - "name": "@kjanat/owen", - "version": "1.0.1", - "description": "A comprehensive Three.js animation system for character state management with clean architecture principles", - "main": "src/index.js", - "types": "src/index.d.ts", - "type": "module", - "scripts": { - "dev": "vite", - "dev:host": "vite --host", - "build": "vite build", - "preview": "vite preview", - "lint": "standard", - "lint:fix": "standard --fix", - "docs": "jsdoc -c jsdoc.config.json", - "format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'" - }, - "keywords": [ - "three.js", - "animation", - "state-machine", - "character", - "gltf", - "3d" - ], - "author": "Kaj \"@kjanat\" Kowalski", - "license": "AGPL-3.0-only OR LicenseRef-Commercial", - "dependencies": { - "three": "^0.176.0" - }, - "devDependencies": { - "jsdoc": "^4.0.2", - "pre-commit": "^1.2.2", - "standard": "*", - "vite": "^6.3.5" - }, - "engines": { - "node": ">=16.0.0" - }, - "standard": { - "globals": [ - "requestAnimationFrame" + "name": "@kjanat/owen", + "version": "1.0.1", + "description": "A comprehensive Three.js animation system for character state management with clean architecture principles", + "main": "src/index.js", + "types": "src/index.d.ts", + "type": "module", + "scripts": { + "dev": "vite", + "dev:host": "vite --host", + "build": "vite build", + "build:demo": "vite build --config vite.demo.config.js", + "preview": "vite preview", + "lint": "standard", + "lint:fix": "standard --fix", + "docs": "jsdoc -c jsdoc.config.json", + "format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'", + "validate:animations": "node scripts/validate-animations.js", + "generate:constants": "node scripts/generate-animation-constants.js", + "check:conflicts": "node scripts/check-naming-conflicts.js", + "test:schemes": "node scripts/test-multi-schemes.js", + "animation:validate": "npm run validate:animations && npm run check:conflicts", + "animation:generate": "npm run generate:constants && npm run validate:animations", + "preview:demo": "vite preview --config vite.demo.config.js --port 3000", + "test": "npx playwright test", + "test:demo": "npx playwright test tests/demo.spec.js", + "test:pages": "npx playwright test tests/pages.spec.js", + "test:ui": "npx playwright test --ui", + "test:headed": "npx playwright test --headed" + }, + "keywords": [ + "three.js", + "animation", + "state-machine", + "character", + "gltf", + "3d" + ], + "author": "Kaj \"@kjanat\" Kowalski", + "license": "AGPL-3.0-only OR LicenseRef-Commercial", + "dependencies": { + "three": "^0.176.0" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "jsdoc": "^4.0.2", + "pre-commit": "^1.2.2", + "standard": "*", + "vite": "^6.3.5" + }, + "engines": { + "node": ">=16.0.0" + }, + "standard": { + "globals": [ + "requestAnimationFrame" + ] + }, + "pre-commit": [ + "lint:fix", + "lint", + "docs", + "format" ] - }, - "pre-commit": [ - "lint:fix", - "lint", - "docs", - "format" - ] } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..e239507 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,92 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'github' : 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'retain-on-failure' + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] } + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] } + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] } + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] } + }, + + /* Test against branded browsers. */ + { + name: 'Microsoft Edge', + use: { ...devices['Desktop Edge'], channel: 'msedge' } + }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' } + } + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run preview:demo', + port: 3000, + reuseExistingServer: !process.env.CI, + timeout: 120000 + }, + + /* Global test timeout */ + timeout: 30000, + + /* Global test expect timeout */ + expect: { + timeout: 5000 + }, + + /* Output directory for test results */ + outputDir: 'test-results/', + + /* Test timeout per test */ + globalTimeout: 600000 +}) diff --git a/scripts/blender-animation-processor.py b/scripts/blender-animation-processor.py new file mode 100644 index 0000000..e6e6f7e --- /dev/null +++ b/scripts/blender-animation-processor.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 + +""" +Blender Animation Processor +Processes Blender animation files and exports them with proper naming schemes +""" + +import os +import sys +import json +import argparse +import subprocess +from pathlib import Path +from typing import List, Dict, Optional + +# Blender script template for processing animations +BLENDER_SCRIPT_TEMPLATE = """ +import bpy +import os +import json +from pathlib import Path + +def process_animation_file(filepath, output_dir, naming_scheme='artist'): + \"\"\"Process a single Blender file and export animations\"\"\" + + # Clear existing scene + bpy.ops.wm.read_factory_settings(use_empty=True) + + # Open the file + bpy.ops.wm.open_mainfile(filepath=filepath) + + results = { + 'file': filepath, + 'animations': [], + 'errors': [] + } + + try: + # Get all objects with animation data + animated_objects = [obj for obj in bpy.data.objects if obj.animation_data and obj.animation_data.action] + + if not animated_objects: + results['errors'].append('No animated objects found') + return results + + for obj in animated_objects: + if obj.animation_data and obj.animation_data.action: + action = obj.animation_data.action + + # Extract animation info + anim_info = { + 'object': obj.name, + 'action': action.name, + 'frame_start': int(action.frame_range[0]), + 'frame_end': int(action.frame_range[1]), + 'duration': action.frame_range[1] - action.frame_range[0] + } + + # Convert to proper naming scheme + new_name = convert_animation_name(action.name, naming_scheme) + anim_info['converted_name'] = new_name + + # Export the animation (GLTF format) + output_file = Path(output_dir) / f"{new_name}.gltf" + + # Select only this object + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Export GLTF with animation + bpy.ops.export_scene.gltf( + filepath=str(output_file), + export_selected=True, + export_animations=True, + export_animation_mode='ACTIONS', + export_nla_strips=False, + export_frame_range=True, + export_frame_step=1, + export_custom_properties=True + ) + + anim_info['exported_file'] = str(output_file) + results['animations'].append(anim_info) + + print(f"Exported animation: {action.name} -> {new_name}") + + except Exception as e: + results['errors'].append(str(e)) + print(f"Error processing {filepath}: {e}") + + return results + +def convert_animation_name(blender_name, target_scheme='artist'): + \"\"\"Convert Blender animation name to target naming scheme\"\"\" + + # Basic name cleaning + name = blender_name.strip().replace(' ', '_') + + # Remove common Blender prefixes/suffixes + name = name.replace('Action', '').replace('action', '') + name = name.replace('.001', '').replace('.000', '') + + if target_scheme == 'artist': + # Convert to Owen_PascalCase format + parts = name.split('_') + pascal_parts = [part.capitalize() for part in parts if part] + return f"Owen_{''.join(pascal_parts)}" + + elif target_scheme == 'legacy': + # Convert to lowercase_with_underscores_L + name_lower = name.lower() + # Add suffix based on animation type (default to L for Loop) + if not name_lower.endswith(('_l', '_s')): + name_lower += '_l' + return name_lower + + elif target_scheme == 'hierarchical': + # Convert to owen.category.subcategory + parts = name.lower().split('_') + return f"owen.state.{'.'.join(parts)}.loop" + + elif target_scheme == 'semantic': + # Convert to OwenPascalCase + parts = name.split('_') + pascal_parts = [part.capitalize() for part in parts if part] + return f"Owen{''.join(pascal_parts)}Loop" + + return name + +# Main processing +if __name__ == "__main__": + import sys + + if len(sys.argv) < 4: + print("Usage: blender --background --python script.py input_dir output_dir naming_scheme") + sys.exit(1) + + input_dir = sys.argv[-3] + output_dir = sys.argv[-2] + naming_scheme = sys.argv[-1] + + print(f"Processing animations from {input_dir} to {output_dir} with {naming_scheme} scheme") + + # Create output directory + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Process all .blend files in input directory + blend_files = list(Path(input_dir).glob('*.blend')) + + all_results = { + 'processed_files': [], + 'total_animations': 0, + 'total_files': len(blend_files), + 'naming_scheme': naming_scheme + } + + for blend_file in blend_files: + print(f"Processing: {blend_file}") + result = process_animation_file(str(blend_file), output_dir, naming_scheme) + all_results['processed_files'].append(result) + all_results['total_animations'] += len(result['animations']) + + # Save processing report + report_file = Path(output_dir) / 'processing_report.json' + with open(report_file, 'w') as f: + json.dump(all_results, f, indent=2) + + print(f"Processing complete. Processed {all_results['total_animations']} animations from {all_results['total_files']} files.") + print(f"Report saved to: {report_file}") +""" + +def main(): + parser = argparse.ArgumentParser(description='Process Blender animation files') + parser.add_argument('--input-dir', required=True, help='Directory containing .blend files') + parser.add_argument('--output-dir', required=True, help='Directory to export processed animations') + parser.add_argument('--naming-scheme', default='artist', choices=['legacy', 'artist', 'hierarchical', 'semantic'], + help='Target naming scheme for animations') + parser.add_argument('--blender-path', default='blender', help='Path to Blender executable') + parser.add_argument('--dry-run', action='store_true', help='Show what would be processed without actually doing it') + + args = parser.parse_args() + + # Validate input directory + input_path = Path(args.input_dir) + if not input_path.exists(): + print(f"Error: Input directory '{args.input_dir}' does not exist") + sys.exit(1) + + # Find .blend files + blend_files = list(input_path.glob('*.blend')) + if not blend_files: + print(f"Warning: No .blend files found in '{args.input_dir}'") + return + + print(f"Found {len(blend_files)} .blend files to process:") + for blend_file in blend_files: + print(f" • {blend_file.name}") + + if args.dry_run: + print(f"\nDry run complete. Would process {len(blend_files)} files with {args.naming_scheme} scheme.") + return + + # Create output directory + output_path = Path(args.output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Create temporary Blender script + script_path = output_path / 'temp_blender_script.py' + with open(script_path, 'w') as f: + f.write(BLENDER_SCRIPT_TEMPLATE) + + try: + # Run Blender with the script + print(f"\nProcessing animations with Blender...") + print(f"Input: {args.input_dir}") + print(f"Output: {args.output_dir}") + print(f"Scheme: {args.naming_scheme}") + + cmd = [ + args.blender_path, + '--background', + '--python', str(script_path), + '--', + args.input_dir, + args.output_dir, + args.naming_scheme + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print("āœ… Blender processing completed successfully!") + print(result.stdout) + else: + print("āŒ Blender processing failed!") + print("STDOUT:", result.stdout) + print("STDERR:", result.stderr) + sys.exit(1) + + # Load and display the processing report + report_file = output_path / 'processing_report.json' + if report_file.exists(): + with open(report_file, 'r') as f: + report = json.load(f) + + print(f"\nšŸ“Š Processing Summary:") + print(f"Files processed: {report['total_files']}") + print(f"Animations exported: {report['total_animations']}") + print(f"Naming scheme: {report['naming_scheme']}") + + # Show any errors + errors = [] + for file_result in report['processed_files']: + errors.extend(file_result.get('errors', [])) + + if errors: + print(f"\nāš ļø Errors encountered:") + for error in errors: + print(f" • {error}") + + finally: + # Clean up temporary script + if script_path.exists(): + script_path.unlink() + +if __name__ == '__main__': + main() diff --git a/scripts/check-naming-conflicts.js b/scripts/check-naming-conflicts.js new file mode 100644 index 0000000..ed01cff --- /dev/null +++ b/scripts/check-naming-conflicts.js @@ -0,0 +1,361 @@ +#!/usr/bin/env node + +/** + * Check Naming Conflicts Script + * Analyzes animation names across all schemes to detect potential conflicts + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath, pathToFileURL } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Check for naming conflicts across schemes + */ +async function checkNamingConflicts () { + try { + console.log('šŸ” Checking for animation naming conflicts...') + + // Import the AnimationNameMapper + const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH) + const { AnimationNameMapper } = await import(animationMapperUrl) + const mapper = new AnimationNameMapper() + + const conflicts = [] + const warnings = [] + const statistics = { + totalAnimations: 0, + duplicatesWithinScheme: 0, + crossSchemeConflicts: 0, + ambiguousNames: 0, + validationErrors: 0 + } + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + const allAnimationsByScheme = {} + + // Collect all animations by scheme + schemes.forEach(scheme => { + allAnimationsByScheme[scheme] = mapper.getAllAnimationsByScheme(scheme) + statistics.totalAnimations += allAnimationsByScheme[scheme].length + }) + + console.log(`šŸ“Š Analyzing ${statistics.totalAnimations} total animations across ${schemes.length} schemes`) + + // 1. Check for duplicates within each scheme + schemes.forEach(scheme => { + const animations = allAnimationsByScheme[scheme] + const seen = new Set() + const duplicates = [] + + animations.forEach(anim => { + if (seen.has(anim)) { + duplicates.push(anim) + statistics.duplicatesWithinScheme++ + } + seen.add(anim) + }) + + if (duplicates.length > 0) { + conflicts.push({ + type: 'duplicate_within_scheme', + scheme, + animations: duplicates, + severity: 'error', + message: `Duplicate animations found within ${scheme} scheme` + }) + } + }) + + // 2. Check for cross-scheme conflicts (same name in different schemes with different meanings) + const nameToSchemes = {} + schemes.forEach(scheme => { + allAnimationsByScheme[scheme].forEach(anim => { + if (!nameToSchemes[anim]) { + nameToSchemes[anim] = [] + } + nameToSchemes[anim].push(scheme) + }) + }) + + Object.entries(nameToSchemes).forEach(([animName, animSchemes]) => { + if (animSchemes.length > 1) { + // Check if they map to the same semantic meaning + try { + const allSemantic = animSchemes.map(scheme => { + try { + return mapper.convert(animName, 'semantic') + } catch { + return null + } + }).filter(Boolean) + + const uniqueSemantic = [...new Set(allSemantic)] + if (uniqueSemantic.length > 1) { + conflicts.push({ + type: 'cross_scheme_conflict', + animationName: animName, + schemes: animSchemes, + semanticMappings: uniqueSemantic, + severity: 'error', + message: `Animation "${animName}" exists in multiple schemes but maps to different meanings` + }) + statistics.crossSchemeConflicts++ + } + } catch (error) { + warnings.push({ + type: 'conversion_error', + animationName: animName, + schemes: animSchemes, + error: error.message, + severity: 'warning' + }) + } + } + }) + + // 3. Check for ambiguous names (could be interpreted as multiple schemes) + const allAnimations = Object.values(allAnimationsByScheme).flat() + const uniqueAnimations = [...new Set(allAnimations)] + + uniqueAnimations.forEach(anim => { + const detectedScheme = mapper.detectScheme(anim) + let possibleSchemes = 0 + + // Test if name could belong to other schemes + schemes.forEach(scheme => { + try { + const converted = mapper.convert(anim, scheme) + if (converted) possibleSchemes++ + } catch { + // Can't convert to this scheme + } + }) + + if (possibleSchemes > 2) { + warnings.push({ + type: 'ambiguous_name', + animationName: anim, + detectedScheme, + possibleSchemes, + severity: 'warning', + message: `Animation "${anim}" could be interpreted as belonging to multiple schemes` + }) + statistics.ambiguousNames++ + } + }) + + // 4. Validate all animations can be properly converted + uniqueAnimations.forEach(anim => { + schemes.forEach(targetScheme => { + try { + mapper.convert(anim, targetScheme) + } catch (error) { + if (!error.message.includes('not found in mapping')) { + warnings.push({ + type: 'validation_error', + animationName: anim, + targetScheme, + error: error.message, + severity: 'warning' + }) + statistics.validationErrors++ + } + } + }) + }) + + // 5. Check for naming convention violations + const conventionViolations = [] + + // Legacy should follow pattern: word_word_L/S + allAnimationsByScheme.legacy.forEach(anim => { + if (!/^[a-z]+(_[a-z]+)*_[LS]$/.test(anim)) { + conventionViolations.push({ + type: 'convention_violation', + scheme: 'legacy', + animationName: anim, + expectedPattern: 'word_word_L/S', + severity: 'warning' + }) + } + }) + + // Artist should follow pattern: Owen_PascalCase + allAnimationsByScheme.artist.forEach(anim => { + if (!/^Owen_[A-Z][a-zA-Z]*$/.test(anim)) { + conventionViolations.push({ + type: 'convention_violation', + scheme: 'artist', + animationName: anim, + expectedPattern: 'Owen_PascalCase', + severity: 'warning' + }) + } + }) + + // Hierarchical should follow pattern: owen.category.subcategory + allAnimationsByScheme.hierarchical.forEach(anim => { + if (!/^owen(\.[a-z]+)+$/.test(anim)) { + conventionViolations.push({ + type: 'convention_violation', + scheme: 'hierarchical', + animationName: anim, + expectedPattern: 'owen.category.subcategory', + severity: 'warning' + }) + } + }) + + // Semantic should follow pattern: OwenPascalCase + allAnimationsByScheme.semantic.forEach(anim => { + if (!/^Owen[A-Z][a-zA-Z]*$/.test(anim)) { + conventionViolations.push({ + type: 'convention_violation', + scheme: 'semantic', + animationName: anim, + expectedPattern: 'OwenPascalCase', + severity: 'warning' + }) + } + }) + + warnings.push(...conventionViolations) + + // Generate report + const report = { + timestamp: new Date().toISOString(), + summary: { + status: conflicts.length === 0 ? 'PASS' : 'FAIL', + totalConflicts: conflicts.length, + totalWarnings: warnings.length, + statistics + }, + conflicts, + warnings, + recommendations: generateRecommendations(conflicts, warnings), + schemes: Object.fromEntries( + schemes.map(scheme => [ + scheme, + { + animationCount: allAnimationsByScheme[scheme].length, + sampleAnimations: allAnimationsByScheme[scheme].slice(0, 3) + } + ]) + ) + } + + // Save report + const reportsDir = path.join(PROJECT_ROOT, 'reports') + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }) + } + + fs.writeFileSync( + path.join(reportsDir, 'naming-conflicts.json'), + JSON.stringify(report, null, 2), + 'utf8' + ) + + // Print summary + console.log('\nšŸ“‹ NAMING CONFLICT ANALYSIS SUMMARY') + console.log('='.repeat(50)) + console.log(`Status: ${report.summary.status}`) + console.log(`Total Conflicts: ${conflicts.length}`) + console.log(`Total Warnings: ${warnings.length}`) + console.log(`Animations Analyzed: ${statistics.totalAnimations}`) + + if (conflicts.length > 0) { + console.log('\nāŒ CONFLICTS:') + conflicts.forEach((conflict, i) => { + console.log(`${i + 1}. ${conflict.type}: ${conflict.message}`) + }) + } + + if (warnings.length > 0 && warnings.length <= 10) { + console.log('\nāš ļø WARNINGS:') + warnings.slice(0, 10).forEach((warning, i) => { + console.log(`${i + 1}. ${warning.type}: ${warning.message || warning.animationName}`) + }) + if (warnings.length > 10) { + console.log(`... and ${warnings.length - 10} more warnings`) + } + } + + console.log(`\nšŸ“ Full report saved to: ${path.join(reportsDir, 'naming-conflicts.json')}`) + + // Exit with error code if conflicts found + if (conflicts.length > 0) { + process.exit(1) + } + + return report + } catch (error) { + console.error('āŒ Error checking naming conflicts:', error.message) + process.exit(1) + } +} + +/** + * Generate recommendations based on conflicts and warnings + */ +function generateRecommendations (conflicts, warnings) { + const recommendations = [] + + if (conflicts.some(c => c.type === 'duplicate_within_scheme')) { + recommendations.push({ + type: 'fix_duplicates', + priority: 'high', + action: 'Remove duplicate animation names within each scheme', + description: 'Duplicate names can cause unpredictable behavior' + }) + } + + if (conflicts.some(c => c.type === 'cross_scheme_conflict')) { + recommendations.push({ + type: 'resolve_cross_scheme', + priority: 'high', + action: 'Ensure names in different schemes map to the same semantic meaning', + description: 'Cross-scheme conflicts break the mapping system' + }) + } + + const conventionViolations = warnings.filter(w => w.type === 'convention_violation') + if (conventionViolations.length > 0) { + recommendations.push({ + type: 'fix_conventions', + priority: 'medium', + action: `Fix ${conventionViolations.length} naming convention violations`, + description: 'Consistent naming conventions improve maintainability' + }) + } + + if (warnings.some(w => w.type === 'ambiguous_name')) { + recommendations.push({ + type: 'clarify_ambiguous', + priority: 'low', + action: 'Review ambiguous animation names for clarity', + description: 'Ambiguous names can be confusing for developers' + }) + } + + return recommendations +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + checkNamingConflicts() + .then(report => { + console.log('āœ… Naming conflict check complete!') + }) + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/convert-animation-names.js b/scripts/convert-animation-names.js new file mode 100644 index 0000000..637d82e --- /dev/null +++ b/scripts/convert-animation-names.js @@ -0,0 +1,433 @@ +#!/usr/bin/env node + +/** + * Convert Animation Names Script + * Converts animation names between different schemes + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Convert animation names based on command line arguments or file input + */ +async function convertAnimationNames () { + try { + const args = process.argv.slice(2) + + // Show help if no arguments provided + if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + showHelp() + return + } + + console.log('šŸ”„ Converting Animation Names...') + + // Import the AnimationNameMapper + const { AnimationNameMapper } = await import(ANIMATION_MAPPER_PATH) + const mapper = new AnimationNameMapper() + + const options = parseArguments(args) + + if (options.inputFile) { + await convertFromFile(mapper, options) + } else if (options.animationName) { + await convertSingle(mapper, options) + } else if (options.batchConvert) { + await convertBatch(mapper, options) + } else { + console.error('āŒ No input provided. Use --help for usage information.') + process.exit(1) + } + } catch (error) { + console.error('āŒ Error converting animation names:', error.message) + process.exit(1) + } +} + +/** + * Parse command line arguments + */ +function parseArguments (args) { + const options = { + animationName: null, + fromScheme: null, + toScheme: null, + inputFile: null, + outputFile: null, + batchConvert: false, + allSchemes: false, + validate: false + } + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + const nextArg = args[i + 1] + + switch (arg) { + case '--name': + case '-n': + options.animationName = nextArg + i++ + break + case '--from': + case '-f': + options.fromScheme = nextArg + i++ + break + case '--to': + case '-t': + options.toScheme = nextArg + i++ + break + case '--input': + case '-i': + options.inputFile = nextArg + i++ + break + case '--output': + case '-o': + options.outputFile = nextArg + i++ + break + case '--batch': + case '-b': + options.batchConvert = true + break + case '--all-schemes': + case '-a': + options.allSchemes = true + break + case '--validate': + case '-v': + options.validate = true + break + } + } + + return options +} + +/** + * Convert a single animation name + */ +async function convertSingle (mapper, options) { + const { animationName, fromScheme, toScheme, allSchemes, validate } = options + + console.log(`\nšŸŽÆ Converting: ${animationName}`) + + if (validate) { + const validation = mapper.validateAnimationName(animationName) + console.log('\nāœ… Validation Result:') + console.log(`Valid: ${validation.isValid}`) + if (validation.detectedScheme) { + console.log(`Detected Scheme: ${validation.detectedScheme}`) + } + if (validation.suggestions?.length > 0) { + console.log(`Suggestions: ${validation.suggestions.join(', ')}`) + } + if (validation.errors?.length > 0) { + console.log(`Errors: ${validation.errors.join(', ')}`) + } + } + + if (allSchemes) { + // Convert to all schemes + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + console.log('\nšŸ”„ Converting to all schemes:') + + schemes.forEach(scheme => { + try { + const converted = mapper.convert(animationName, scheme) + console.log(`${scheme.padEnd(12)}: ${converted}`) + } catch (error) { + console.log(`${scheme.padEnd(12)}: āŒ ${error.message}`) + } + }) + } else if (toScheme) { + // Convert to specific scheme + try { + const converted = mapper.convert(animationName, toScheme) + console.log(`\nšŸŽÆ Result: ${converted}`) + + if (fromScheme) { + console.log(`From: ${fromScheme} -> To: ${toScheme}`) + } else { + const detectedScheme = mapper.detectScheme(animationName) + console.log(`From: ${detectedScheme} -> To: ${toScheme}`) + } + } catch (error) { + console.error(`āŒ Conversion failed: ${error.message}`) + process.exit(1) + } + } else { + console.log('ā„¹ļø No target scheme specified. Use --to or --all-schemes') + } +} + +/** + * Convert animation names from a file + */ +async function convertFromFile (mapper, options) { + const { inputFile, outputFile, toScheme, allSchemes } = options + + if (!fs.existsSync(inputFile)) { + throw new Error(`Input file not found: ${inputFile}`) + } + + console.log(`šŸ“ Reading from file: ${inputFile}`) + + const content = fs.readFileSync(inputFile, 'utf8') + let animationNames = [] + + try { + // Try to parse as JSON first + const parsed = JSON.parse(content) + if (Array.isArray(parsed)) { + animationNames = parsed + } else if (parsed.animations && Array.isArray(parsed.animations)) { + animationNames = parsed.animations + } else { + animationNames = Object.values(parsed).filter(v => typeof v === 'string') + } + } catch { + // If not JSON, treat as line-separated text + animationNames = content.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) + } + + console.log(`šŸ“‹ Found ${animationNames.length} animation names to convert`) + + const results = { + timestamp: new Date().toISOString(), + inputFile, + totalAnimations: animationNames.length, + conversions: [], + errors: [] + } + + if (allSchemes) { + // Convert each animation to all schemes + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + animationNames.forEach(animName => { + const animResult = { + original: animName, + conversions: {} + } + + schemes.forEach(scheme => { + try { + animResult.conversions[scheme] = mapper.convert(animName, scheme) + } catch (error) { + animResult.conversions[scheme] = { error: error.message } + results.errors.push(`${animName} -> ${scheme}: ${error.message}`) + } + }) + + results.conversions.push(animResult) + }) + } else if (toScheme) { + // Convert to specific scheme + animationNames.forEach(animName => { + const animResult = { + original: animName, + target: toScheme + } + + try { + animResult.converted = mapper.convert(animName, toScheme) + animResult.fromScheme = mapper.detectScheme(animName) + } catch (error) { + animResult.error = error.message + results.errors.push(`${animName}: ${error.message}`) + } + + results.conversions.push(animResult) + }) + } + + // Save results + if (outputFile) { + fs.writeFileSync(outputFile, JSON.stringify(results, null, 2), 'utf8') + console.log(`šŸ“„ Results saved to: ${outputFile}`) + } else { + // Print to console + console.log('\nšŸ“Š Conversion Results:') + results.conversions.forEach((result, index) => { + console.log(`\n${index + 1}. ${result.original}`) + if (result.conversions) { + Object.entries(result.conversions).forEach(([scheme, value]) => { + if (typeof value === 'string') { + console.log(` ${scheme}: ${value}`) + } else { + console.log(` ${scheme}: āŒ ${value.error}`) + } + }) + } else if (result.converted) { + console.log(` ${result.target}: ${result.converted}`) + } else if (result.error) { + console.log(` āŒ ${result.error}`) + } + }) + } + + if (results.errors.length > 0) { + console.log(`\nāš ļø ${results.errors.length} errors encountered`) + if (results.errors.length <= 5) { + results.errors.forEach(error => console.log(` • ${error}`)) + } + } + + console.log(`\nāœ… Processed ${results.totalAnimations} animations`) +} + +/** + * Batch convert all animations in the current mapper + */ +async function convertBatch (mapper, options) { + const { outputFile } = options + + console.log('šŸ”„ Batch converting all animations in the system...') + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + const allResults = { + timestamp: new Date().toISOString(), + totalAnimations: 0, + schemeStats: {}, + conversionMatrix: {} + } + + schemes.forEach(fromScheme => { + const animations = mapper.getAllAnimationsByScheme(fromScheme) + allResults.schemeStats[fromScheme] = animations.length + allResults.totalAnimations += animations.length + + if (!allResults.conversionMatrix[fromScheme]) { + allResults.conversionMatrix[fromScheme] = {} + } + + schemes.forEach(targetScheme => { + const conversions = [] + let errors = 0 + + animations.forEach(anim => { + try { + const converted = mapper.convert(anim, targetScheme) + conversions.push({ original: anim, converted }) + } catch (error) { + conversions.push({ original: anim, error: error.message }) + errors++ + } + }) + + allResults.conversionMatrix[fromScheme][targetScheme] = { + total: animations.length, + successful: animations.length - errors, + errors, + successRate: Math.round(((animations.length - errors) / animations.length) * 100), + conversions: conversions.slice(0, 10) // Include sample conversions + } + }) + }) + + // Print summary + console.log('\nšŸ“Š Batch Conversion Summary:') + console.log(`Total animations: ${allResults.totalAnimations}`) + console.log('\nBy scheme:') + Object.entries(allResults.schemeStats).forEach(([scheme, count]) => { + console.log(` ${scheme}: ${count} animations`) + }) + + console.log('\nConversion matrix (success rates):') + schemes.forEach(fromScheme => { + console.log(`\n${fromScheme}:`) + schemes.forEach(toScheme => { + const result = allResults.conversionMatrix[fromScheme][toScheme] + console.log(` -> ${toScheme}: ${result.successRate}% (${result.successful}/${result.total})`) + }) + }) + + // Save results + if (outputFile) { + fs.writeFileSync(outputFile, JSON.stringify(allResults, null, 2), 'utf8') + console.log(`\nšŸ“„ Full results saved to: ${outputFile}`) + } else { + // Save to default location + const reportsDir = path.join(PROJECT_ROOT, 'reports') + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }) + } + const defaultFile = path.join(reportsDir, 'batch-conversion-results.json') + fs.writeFileSync(defaultFile, JSON.stringify(allResults, null, 2), 'utf8') + console.log(`\nšŸ“„ Results saved to: ${defaultFile}`) + } +} + +/** + * Show help information + */ +function showHelp () { + console.log(` +šŸŽ¬ Animation Name Converter + +Convert animation names between different naming schemes in the Owen Animation System. + +USAGE: + node convert-animation-names.js [OPTIONS] + +SINGLE CONVERSION: + --name, -n Animation name to convert + --to, -t Target scheme (legacy|artist|hierarchical|semantic) + --all-schemes, -a Convert to all schemes + --validate, -v Validate the animation name + +FILE CONVERSION: + --input, -i Input file with animation names (JSON or line-separated) + --output, -o Output file for results (optional) + --to, -t Target scheme for conversion + +BATCH OPERATIONS: + --batch, -b Convert all animations in the system + --output, -o Output file for batch results + +EXAMPLES: + # Convert single animation to semantic scheme + node convert-animation-names.js --name wait_idle_L --to semantic + + # Convert to all schemes + node convert-animation-names.js --name Owen_ReactAngry --all-schemes + + # Validate an animation name + node convert-animation-names.js --name unknown_animation --validate + + # Convert from file + node convert-animation-names.js --input animations.json --to artist --output results.json + + # Batch convert all animations + node convert-animation-names.js --batch --output full-conversion-matrix.json + +SCHEMES: + legacy - e.g., wait_idle_L, react_angry_S + artist - e.g., Owen_WaitIdle, Owen_ReactAngry + hierarchical - e.g., owen.state.wait.idle.loop + semantic - e.g., OwenWaitIdleLoop, OwenReactAngryShort +`) +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + convertAnimationNames() + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/generate-animation-constants.js b/scripts/generate-animation-constants.js new file mode 100644 index 0000000..7019fc9 --- /dev/null +++ b/scripts/generate-animation-constants.js @@ -0,0 +1,252 @@ +#!/usr/bin/env node + +/** + * Generate Animation Constants Script + * Automatically generates/updates AnimationConstants.js based on current AnimationNameMapper definitions + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath, pathToFileURL } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_CONSTANTS_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationConstants.js') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Generate animation constants file content + */ +async function generateAnimationConstants () { + try { + console.log('šŸ”§ Generating Animation Constants...') + + // Import the AnimationNameMapper to get current definitions + const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH) + const { AnimationNameMapper } = await import(animationMapperUrl) + const mapper = new AnimationNameMapper() + + // Get all animation names by scheme + const legacyAnimations = mapper.getAllAnimationsByScheme('legacy') + const artistAnimations = mapper.getAllAnimationsByScheme('artist') + const hierarchicalAnimations = mapper.getAllAnimationsByScheme('hierarchical') + const semanticAnimations = mapper.getAllAnimationsByScheme('semantic') + + const timestamp = new Date().toISOString() + + const constantsContent = `/** + * Animation Constants - Auto-generated file + * + * This file contains type-safe constants for all animation naming schemes + * supported by the Owen Animation System. + * + * Generated: ${timestamp} + * + * @fileoverview Auto-generated animation constants for all naming schemes + * @module AnimationConstants + */ + +// Import the core mapper for utility functions +import { AnimationNameMapper } from './AnimationNameMapper.js' + +/** + * Naming scheme enumeration + * @readonly + * @enum {string} + */ +export const NamingSchemes = Object.freeze({ + LEGACY: 'legacy', + ARTIST: 'artist', + HIERARCHICAL: 'hierarchical', + SEMANTIC: 'semantic' +}) + +/** + * Legacy animation names (e.g., wait_idle_L) + * @readonly + */ +export const LegacyAnimations = Object.freeze({ +${legacyAnimations.map(anim => { + const constantName = anim.toUpperCase().replace(/[^A-Z0-9]/g, '_') + return ` ${constantName}: '${anim}'` +}).join(',\n')} +}) + +/** + * Artist-friendly animation names (e.g., Owen_WaitIdle) + * @readonly + */ +export const ArtistAnimations = Object.freeze({ +${artistAnimations.map(anim => { + const constantName = anim.replace(/^Owen_/, '').toUpperCase().replace(/[^A-Z0-9]/g, '_') + return ` ${constantName}: '${anim}'` +}).join(',\n')} +}) + +/** + * Hierarchical animation names (e.g., owen.state.wait.idle.loop) + * @readonly + */ +export const HierarchicalAnimations = Object.freeze({ +${hierarchicalAnimations.map(anim => { + const constantName = anim.replace(/owen\./, '').split('.').map(part => + part.charAt(0).toUpperCase() + part.slice(1) + ).join('_').toUpperCase() + return ` ${constantName}: '${anim}'` +}).join(',\n')} +}) + +/** + * Semantic animation names (e.g., OwenWaitIdleLoop) + * @readonly + */ +export const SemanticAnimations = Object.freeze({ +${semanticAnimations.map(anim => { + const constantName = anim.replace(/^Owen/, '').replace(/([A-Z])/g, '_$1').toUpperCase().substring(1) + return ` ${constantName}: '${anim}'` +}).join(',\n')} +}) + +/** + * All animation constants grouped by scheme + * @readonly + */ +export const AnimationsByScheme = Object.freeze({ + [NamingSchemes.LEGACY]: LegacyAnimations, + [NamingSchemes.ARTIST]: ArtistAnimations, + [NamingSchemes.HIERARCHICAL]: HierarchicalAnimations, + [NamingSchemes.SEMANTIC]: SemanticAnimations +}) + +// Create global mapper instance for utility functions +const mapper = new AnimationNameMapper() + +/** + * Convert an animation name between schemes + * @param {string} animationName - The animation name to convert + * @param {string} targetScheme - The target naming scheme + * @returns {string} The converted animation name + * @throws {Error} If conversion fails + */ +export function convertAnimationName(animationName, targetScheme) { + return mapper.convert(animationName, targetScheme) +} + +/** + * Get all animation names for a specific scheme + * @param {string} scheme - The naming scheme + * @returns {string[]} Array of animation names + */ +export function getAllAnimationNames(scheme) { + return mapper.getAllAnimationsByScheme(scheme) +} + +/** + * Validate an animation name and get suggestions + * @param {string} animationName - The animation name to validate + * @returns {Object} Validation result with isValid flag and suggestions + */ +export function validateAnimationName(animationName) { + return mapper.validateAnimationName(animationName) +} + +/** + * Get animations filtered by state and emotion + * @param {string} state - The animation state (wait, react, sleep, etc.) + * @param {string} [emotion] - Optional emotion filter (angry, happy, etc.) + * @param {string} [scheme='semantic'] - The naming scheme to use + * @returns {string[]} Array of matching animation names + */ +export function getAnimationsByStateAndEmotion(state, emotion = null, scheme = 'semantic') { + const allAnimations = getAllAnimationNames(scheme) + + return allAnimations.filter(anim => { + const lowerAnim = anim.toLowerCase() + const hasState = lowerAnim.includes(state.toLowerCase()) + const hasEmotion = !emotion || lowerAnim.includes(emotion.toLowerCase()) + + return hasState && hasEmotion + }) +} + +/** + * Animation metadata for development tools + * @readonly + */ +export const AnimationMetadata = Object.freeze({ + totalAnimations: ${legacyAnimations.length}, + schemes: Object.keys(NamingSchemes).length, + generatedAt: '${timestamp}', + version: '1.0.0' +}) + +// Default export for convenience +export default { + NamingSchemes, + LegacyAnimations, + ArtistAnimations, + HierarchicalAnimations, + SemanticAnimations, + AnimationsByScheme, + convertAnimationName, + getAllAnimationNames, + validateAnimationName, + getAnimationsByStateAndEmotion, + AnimationMetadata +} +` + + // Write the generated constants file + fs.writeFileSync(ANIMATION_CONSTANTS_PATH, constantsContent, 'utf8') + + console.log('āœ… Animation Constants generated successfully!') + console.log(`šŸ“ Generated ${legacyAnimations.length} animation constants across 4 schemes`) + console.log(`šŸ“ File: ${ANIMATION_CONSTANTS_PATH}`) + + // Generate summary report + const report = { + generated: timestamp, + totalAnimations: legacyAnimations.length, + schemes: { + legacy: legacyAnimations.length, + artist: artistAnimations.length, + hierarchical: hierarchicalAnimations.length, + semantic: semanticAnimations.length + }, + outputFile: ANIMATION_CONSTANTS_PATH + } + + // Ensure reports directory exists + const reportsDir = path.join(PROJECT_ROOT, 'reports') + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }) + } + + // Write report + fs.writeFileSync( + path.join(reportsDir, 'animation-constants-generation.json'), + JSON.stringify(report, null, 2), + 'utf8' + ) + + return report + } catch (error) { + console.error('āŒ Error generating Animation Constants:', error.message) + process.exit(1) + } +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + generateAnimationConstants() + .then(report => { + console.log('šŸ“Š Generation complete!') + console.log(JSON.stringify(report, null, 2)) + }) + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/generate-animation-docs.js b/scripts/generate-animation-docs.js new file mode 100644 index 0000000..4850605 --- /dev/null +++ b/scripts/generate-animation-docs.js @@ -0,0 +1,1556 @@ +#!/usr/bin/env node + +/** + * Generate Animation Documentation Script + * Creates comprehensive documentation for the animation system + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath, pathToFileURL } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Generate comprehensive animation documentation + */ +async function generateAnimationDocs () { + try { + console.log('šŸ“š Generating Animation Documentation...') + + // Import the AnimationNameMapper + const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH) + const { AnimationNameMapper } = await import(animationMapperUrl) + const mapper = new AnimationNameMapper() + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + const timestamp = new Date().toISOString() + + // Gather animation data + const animationData = {} + schemes.forEach(scheme => { + animationData[scheme] = mapper.getAllAnimationsByScheme(scheme) + }) + + // Generate main documentation + await generateMainDocumentation(animationData, timestamp) + + // Generate API reference + await generateAPIReference(mapper, animationData, timestamp) + + // Generate scheme comparison + await generateSchemeComparison(mapper, animationData, timestamp) + + // Generate migration guide + await generateMigrationGuide(mapper, animationData, timestamp) + + // Generate examples + await generateExamples(mapper, animationData, timestamp) + + console.log('āœ… Animation documentation generated successfully!') + } catch (error) { + console.error('āŒ Error generating animation documentation:', error.message) + process.exit(1) + } +} + +/** + * Generate the main animation documentation + */ +async function generateMainDocumentation (animationData, timestamp) { + const totalAnimations = Object.values(animationData).reduce((sum, arr) => sum + arr.length, 0) + + const content = `# Owen Animation System Documentation + +*Generated: ${timestamp}* + +The Owen Animation System provides a comprehensive multi-scheme approach to animation naming that supports backward compatibility while offering modern, developer-friendly alternatives. + +## Overview + +- **Total Animations**: ${totalAnimations} +- **Naming Schemes**: 4 (Legacy, Artist, Hierarchical, Semantic) +- **Bidirectional Conversion**: āœ… +- **Auto-Detection**: āœ… +- **Validation**: āœ… + +## Naming Schemes + +### 1. Legacy Scheme +*Format: \`word_word_L/S\`* + +Traditional naming used in earlier versions. Includes suffix indicating Loop (L) or Short (S) animations. + +**Examples:** +${animationData.legacy.slice(0, 5).map(name => `- \`${name}\``).join('\n')} + +### 2. Artist Scheme +*Format: \`Owen_PascalCase\`* + +Artist-friendly naming that's easy to read and use in Blender or other animation tools. + +**Examples:** +${animationData.artist.slice(0, 5).map(name => `- \`${name}\``).join('\n')} + +### 3. Hierarchical Scheme +*Format: \`owen.category.subcategory\`* + +Organized, hierarchical naming that groups related animations logically. + +**Examples:** +${animationData.hierarchical.slice(0, 5).map(name => `- \`${name}\``).join('\n')} + +### 4. Semantic Scheme +*Format: \`OwenDescriptiveName\`* + +Modern, semantic naming that clearly describes the animation's purpose. + +**Examples:** +${animationData.semantic.slice(0, 5).map(name => `- \`${name}\``).join('\n')} + +## Quick Start + +\`\`\`javascript +import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen' + +// Using the animation context with multi-scheme support +const context = new OwenAnimationContext(gltf) + +// Load animation using any scheme +context.getClip('wait_idle_L') // Legacy +context.getClip('Owen_WaitIdle') // Artist +context.getClip('owen.state.wait.idle.loop') // Hierarchical +context.getClip('OwenWaitIdleLoop') // Semantic + +// Convert between schemes +const mapper = new AnimationNameMapper() +const semantic = mapper.convert('wait_idle_L', 'semantic') +console.log(semantic) // 'OwenWaitIdleLoop' +\`\`\` + +## Validation and Error Handling + +The system provides comprehensive validation: + +\`\`\`javascript +const validation = mapper.validateAnimationName('unknown_animation') +console.log(validation.isValid) // false +console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L', ...] +console.log(validation.errors) // ['Animation not found in any scheme'] +\`\`\` + +## Animation Categories + +### State Animations +- **Wait**: Idle states and waiting animations +- **React**: Reaction animations to stimuli +- **Sleep**: Sleep and rest state animations +- **Type**: Typing and work-related animations + +### Emotion Variants +- **Neutral**: Default emotional state +- **Happy**: Positive emotional expressions +- **Angry**: Negative emotional expressions +- **Peace**: Calm and peaceful states + +### Duration Types +- **Loop (L)**: Continuous looping animations +- **Short (S)**: Brief, one-time animations +- **Transition**: Animations between states + +## Best Practices + +1. **Choose the Right Scheme**: Use semantic for new code, artist for Blender workflow +2. **Validate Names**: Always validate animation names in production +3. **Handle Errors**: Provide fallbacks for missing animations +4. **Use Constants**: Import animation constants for type safety + +\`\`\`javascript +import { SemanticAnimations } from '@kjanat/owen' + +// Type-safe animation names +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) +\`\`\` + +## Integration with Workflows + +### Blender Integration +Use the artist scheme (\`Owen_AnimationName\`) in Blender for the best artist experience. + +### Code Integration +Use the semantic scheme (\`OwenAnimationName\`) in code for clarity and maintainability. + +### Legacy Support +The system automatically detects and converts legacy names for backward compatibility. + +## Performance + +- **Conversion Speed**: >10,000 operations/second +- **Memory Usage**: <50MB for full animation set +- **Auto-Detection**: <1ms per animation name + +## See Also + +- [API Reference](./API_REFERENCE.md) +- [Scheme Comparison](./SCHEME_COMPARISON.md) +- [Migration Guide](./MIGRATION_GUIDE.md) +- [Examples](./EXAMPLES.md) +` + + const docsDir = path.join(PROJECT_ROOT, 'docs') + if (!fs.existsSync(docsDir)) { + fs.mkdirSync(docsDir, { recursive: true }) + } + + fs.writeFileSync(path.join(docsDir, 'ANIMATION_SYSTEM.md'), content, 'utf8') + console.log('šŸ“„ Generated: ANIMATION_SYSTEM.md') +} + +/** + * Generate API reference documentation + */ +async function generateAPIReference (mapper, animationData, timestamp) { + const content = `# Animation System API Reference + +*Generated: ${timestamp}* + +## AnimationNameMapper + +Core class for converting and validating animation names across different schemes. + +### Constructor + +\`\`\`javascript +const mapper = new AnimationNameMapper() +\`\`\` + +### Methods + +#### convert(animationName, targetScheme) + +Converts an animation name to the target scheme. + +**Parameters:** +- \`animationName\` (string): The animation name to convert +- \`targetScheme\` (string): Target scheme ('legacy', 'artist', 'hierarchical', 'semantic') + +**Returns:** \`string\` - The converted animation name + +**Throws:** \`Error\` - If animation not found or conversion fails + +**Example:** +\`\`\`javascript +const semantic = mapper.convert('wait_idle_L', 'semantic') +// Returns: 'OwenWaitIdleLoop' +\`\`\` + +#### detectScheme(animationName) + +Automatically detects the naming scheme of an animation. + +**Parameters:** +- \`animationName\` (string): The animation name to analyze + +**Returns:** \`string\` - Detected scheme name + +**Example:** +\`\`\`javascript +const scheme = mapper.detectScheme('Owen_ReactAngry') +// Returns: 'artist' +\`\`\` + +#### validateAnimationName(animationName) + +Validates an animation name and provides suggestions. + +**Parameters:** +- \`animationName\` (string): The animation name to validate + +**Returns:** \`Object\` - Validation result +- \`isValid\` (boolean): Whether the name is valid +- \`detectedScheme\` (string|null): Detected scheme if valid +- \`suggestions\` (string[]): Similar valid animation names +- \`errors\` (string[]): Error messages + +**Example:** +\`\`\`javascript +const validation = mapper.validateAnimationName('unknown_anim') +console.log(validation.isValid) // false +console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L'] +\`\`\` + +#### getAllAnimationsByScheme(scheme) + +Gets all animation names for a specific scheme. + +**Parameters:** +- \`scheme\` (string): The scheme name + +**Returns:** \`string[]\` - Array of animation names + +**Example:** +\`\`\`javascript +const semanticAnims = mapper.getAllAnimationsByScheme('semantic') +// Returns: ['OwenWaitIdleLoop', 'OwenReactAngryShort', ...] +\`\`\` + +#### getAllNames(animationName) + +Gets all scheme variants of an animation name. + +**Parameters:** +- \`animationName\` (string): Any valid animation name + +**Returns:** \`Object\` - Names in all schemes +- \`legacy\` (string): Legacy scheme name +- \`artist\` (string): Artist scheme name +- \`hierarchical\` (string): Hierarchical scheme name +- \`semantic\` (string): Semantic scheme name + +**Example:** +\`\`\`javascript +const allNames = mapper.getAllNames('wait_idle_L') +console.log(allNames.semantic) // 'OwenWaitIdleLoop' +console.log(allNames.artist) // 'Owen_WaitIdle' +\`\`\` + +## OwenAnimationContext (Enhanced) + +Enhanced animation context with multi-scheme support. + +### New Methods + +#### getClipByScheme(animationName, scheme) + +Gets animation clip using a specific scheme. + +**Parameters:** +- \`animationName\` (string): Animation name in the specified scheme +- \`scheme\` (string): The naming scheme to use + +**Returns:** \`AnimationClip\` - The animation clip + +#### getAnimationNames(scheme) + +Gets all available animation names in a specific scheme. + +**Parameters:** +- \`scheme\` (string): The naming scheme + +**Returns:** \`string[]\` - Available animation names + +#### validateAnimationName(animationName) + +Validates an animation name and returns suggestions. + +**Parameters:** +- \`animationName\` (string): Name to validate + +**Returns:** \`Object\` - Validation result + +#### getAnimationsByStateAndEmotion(state, emotion, scheme) + +Filters animations by state and emotion. + +**Parameters:** +- \`state\` (string): Animation state ('wait', 'react', 'sleep', 'type') +- \`emotion\` (string|null): Emotion filter ('happy', 'angry', 'peace') +- \`scheme\` (string): Naming scheme to use (default: 'semantic') + +**Returns:** \`string[]\` - Matching animation names + +## Animation Constants + +Type-safe constants for all naming schemes. + +### Enums + +#### NamingSchemes +\`\`\`javascript +export const NamingSchemes = { + LEGACY: 'legacy', + ARTIST: 'artist', + HIERARCHICAL: 'hierarchical', + SEMANTIC: 'semantic' +} +\`\`\` + +### Animation Constants + +#### LegacyAnimations +Constants for legacy scheme animations. + +\`\`\`javascript +import { LegacyAnimations } from '@kjanat/owen' + +context.getClip(LegacyAnimations.WAIT_IDLE_L) +\`\`\` + +#### ArtistAnimations +Constants for artist scheme animations. + +\`\`\`javascript +import { ArtistAnimations } from '@kjanat/owen' + +context.getClip(ArtistAnimations.WAIT_IDLE) +\`\`\` + +#### HierarchicalAnimations +Constants for hierarchical scheme animations. + +\`\`\`javascript +import { HierarchicalAnimations } from '@kjanat/owen' + +context.getClip(HierarchicalAnimations.STATE_WAIT_IDLE_LOOP) +\`\`\` + +#### SemanticAnimations +Constants for semantic scheme animations. + +\`\`\`javascript +import { SemanticAnimations } from '@kjanat/owen' + +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) +\`\`\` + +### Utility Functions + +#### convertAnimationName(animationName, targetScheme) +Convenience function for animation conversion. + +#### getAllAnimationNames(scheme) +Get all animations for a scheme. + +#### validateAnimationName(animationName) +Validate an animation name. + +#### getAnimationsByStateAndEmotion(state, emotion, scheme) +Filter animations by criteria. + +## Error Handling + +All methods throw descriptive errors for invalid inputs: + +\`\`\`javascript +try { + const converted = mapper.convert('invalid_name', 'semantic') +} catch (error) { + console.error('Conversion failed:', error.message) + // Handle the error appropriately +} +\`\`\` + +## Performance Notes + +- Animation lookups are O(1) for direct scheme access +- Conversions are O(1) using pre-built mapping tables +- Validation includes fuzzy matching for suggestions +- Memory usage scales linearly with animation count +` + + const docsDir = path.join(PROJECT_ROOT, 'docs') + fs.writeFileSync(path.join(docsDir, 'API_REFERENCE.md'), content, 'utf8') + console.log('šŸ“„ Generated: API_REFERENCE.md') +} + +/** + * Generate scheme comparison documentation + */ +async function generateSchemeComparison (mapper, animationData, timestamp) { + const content = `# Animation Naming Schemes Comparison + +*Generated: ${timestamp}* + +This document compares the four naming schemes supported by the Owen Animation System. + +## Scheme Overview + +| Scheme | Format | Use Case | Example | +|--------|--------|----------|---------| +| **Legacy** | \`word_word_L/S\` | Backward compatibility | \`wait_idle_L\` | +| **Artist** | \`Owen_PascalCase\` | Blender workflow | \`Owen_WaitIdle\` | +| **Hierarchical** | \`owen.category.subcategory\` | Organized structure | \`owen.state.wait.idle.loop\` | +| **Semantic** | \`OwenDescriptiveName\` | Modern development | \`OwenWaitIdleLoop\` | + +## Detailed Comparison + +### Legacy Scheme +**Format:** \`lowercase_words_L/S\` + +**Pros:** +- Maintains compatibility with existing code +- Clear loop/short distinction with suffix +- Compact and simple + +**Cons:** +- Not immediately readable +- Limited expressiveness +- Technical suffix may confuse artists + +**Best for:** Maintaining existing codebases, migration scenarios + +**Examples:** +${animationData.legacy.slice(0, 8).map(name => `- \`${name}\``).join('\n')} + +### Artist Scheme +**Format:** \`Owen_PascalCase\` + +**Pros:** +- Easy to read and understand +- Perfect for Blender asset naming +- Artist-friendly without technical jargon +- Consistent Owen branding + +**Cons:** +- Longer names than legacy +- Less structural organization +- Requires prefix for all animations + +**Best for:** Blender workflows, artist collaboration, asset management + +**Examples:** +${animationData.artist.slice(0, 8).map(name => `- \`${name}\``).join('\n')} + +### Hierarchical Scheme +**Format:** \`owen.category.subcategory.type\` + +**Pros:** +- Excellent organization and grouping +- IDE autocomplete friendly +- Clear categorization +- Extensible structure + +**Cons:** +- Longer names +- May be verbose for simple animations +- Requires understanding of hierarchy + +**Best for:** Large animation sets, organized codebases, tooling integration + +**Examples:** +${animationData.hierarchical.slice(0, 8).map(name => `- \`${name}\``).join('\n')} + +### Semantic Scheme +**Format:** \`OwenDescriptiveName\` + +**Pros:** +- Highly readable and self-documenting +- Modern naming convention +- Clear intent and purpose +- Easy to understand without documentation + +**Cons:** +- Can become quite long +- No enforced structure +- Potential naming inconsistencies + +**Best for:** New development, API design, maintainable codebases + +**Examples:** +${animationData.semantic.slice(0, 8).map(name => `- \`${name}\``).join('\n')} + +## Conversion Examples + +The following table shows how the same animation appears in each scheme: + +| Legacy | Artist | Hierarchical | Semantic | +|--------|--------|--------------|----------| +${animationData.legacy.slice(0, 5).map(legacyName => { + try { + const artist = mapper.convert(legacyName, 'artist') + const hierarchical = mapper.convert(legacyName, 'hierarchical') + const semantic = mapper.convert(legacyName, 'semantic') + return `| \`${legacyName}\` | \`${artist}\` | \`${hierarchical}\` | \`${semantic}\` |` + } catch { + return `| \`${legacyName}\` | - | - | - |` + } +}).join('\n')} + +## Usage Recommendations + +### For New Projects +**Recommended:** Semantic scheme for code, Artist scheme for assets + +\`\`\`javascript +// In code - use semantic for clarity +import { SemanticAnimations } from '@kjanat/owen' +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) + +// In Blender - use artist scheme +// Asset name: Owen_WaitIdle.blend +\`\`\` + +### For Existing Projects +**Recommended:** Keep legacy scheme, add semantic for new animations + +\`\`\`javascript +// Existing code continues to work +context.getClip('wait_idle_L') + +// New code can use semantic +context.getClip('OwenNewAnimationLoop') +\`\`\` + +### For Animation Teams +**Recommended:** Artist scheme for all Blender work + +- Consistent \`Owen_\` prefix +- Easy to read animation names +- No technical suffixes to confuse artists +- Direct mapping to code equivalents + +### For Large Codebases +**Recommended:** Hierarchical scheme for organization + +\`\`\`javascript +// Clear organization +context.getClip('owen.state.wait.idle.loop') +context.getClip('owen.state.wait.active.loop') +context.getClip('owen.reaction.happy.short') +context.getClip('owen.reaction.angry.short') +\`\`\` + +## Migration Strategies + +### Gradual Migration +1. Start using new scheme for new animations +2. Convert high-traffic animations first +3. Use the mapper to support both old and new names +4. Update documentation and examples + +### Asset Pipeline Integration +1. Use artist scheme in Blender +2. Automatically convert to semantic in build pipeline +3. Generate constants file for type safety +4. Update references in code + +### Team Adoption +1. Train artists on artist scheme conventions +2. Train developers on semantic scheme benefits +3. Use validation tools to ensure consistency +4. Establish naming guidelines and review processes + +## Performance Comparison + +| Operation | Legacy | Artist | Hierarchical | Semantic | +|-----------|---------|---------|--------------|----------| +| **Lookup Speed** | Fast | Fast | Fast | Fast | +| **Memory Usage** | Low | Medium | High | Medium | +| **Readability** | Low | High | Medium | High | +| **Maintainability** | Low | Medium | High | High | + +## Conclusion + +Each naming scheme serves different needs: + +- **Legacy**: Essential for backward compatibility +- **Artist**: Perfect for Blender and asset workflows +- **Hierarchical**: Best for large, organized codebases +- **Semantic**: Ideal for modern, readable code + +The multi-scheme system allows teams to use the most appropriate scheme for each context while maintaining full interoperability. +` + + const docsDir = path.join(PROJECT_ROOT, 'docs') + fs.writeFileSync(path.join(docsDir, 'SCHEME_COMPARISON.md'), content, 'utf8') + console.log('šŸ“„ Generated: SCHEME_COMPARISON.md') +} + +/** + * Generate migration guide + */ +async function generateMigrationGuide (mapper, animationData, timestamp) { + const content = `# Migration Guide + +*Generated: ${timestamp}* + +This guide helps you migrate existing Owen Animation System code to use the new multi-scheme naming system. + +## Overview + +The multi-scheme system is **100% backward compatible**. Your existing code will continue to work without changes while giving you access to modern naming schemes. + +## Migration Scenarios + +### Scenario 1: Existing Project (No Changes Needed) + +If you have existing code using legacy names, **no changes are required**: + +\`\`\`javascript +// This continues to work exactly as before +const context = new OwenAnimationContext(gltf) +context.getClip('wait_idle_L') // āœ… Works +context.getClip('react_angry_S') // āœ… Works +context.getClip('sleep_peace_L') // āœ… Works +\`\`\` + +### Scenario 2: Gradual Modernization + +Start using semantic names for new animations while keeping legacy names: + +\`\`\`javascript +// Existing animations - keep legacy names +context.getClip('wait_idle_L') + +// New animations - use semantic names +context.getClip('OwenNewFeatureIdleLoop') +context.getClip('OwenSpecialReactionShort') +\`\`\` + +### Scenario 3: Full Migration to Semantic + +Replace legacy names with semantic equivalents: + +**Before:** +\`\`\`javascript +context.getClip('wait_idle_L') +context.getClip('react_angry_S') +context.getClip('sleep_peace_L') +\`\`\` + +**After:** +\`\`\`javascript +context.getClip('OwenWaitIdleLoop') +context.getClip('OwenReactAngryShort') +context.getClip('OwenSleepPeaceLoop') +\`\`\` + +### Scenario 4: Artist Workflow Integration + +Update your Blender to code pipeline: + +**Blender Assets (Artist Scheme):** +- \`Owen_WaitIdle.blend\` +- \`Owen_ReactAngry.blend\` +- \`Owen_SleepPeace.blend\` + +**Code (Semantic Scheme):** +\`\`\`javascript +// Automatic conversion happens behind the scenes +context.getClip('OwenWaitIdleLoop') // Finds Owen_WaitIdle asset +context.getClip('OwenReactAngryShort') // Finds Owen_ReactAngry asset +context.getClip('OwenSleepPeaceLoop') // Finds Owen_SleepPeace asset +\`\`\` + +## Step-by-Step Migration + +### Step 1: Update Owen Package + +Ensure you have the latest version with multi-scheme support: + +\`\`\`bash +npm update @kjanat/owen +\`\`\` + +### Step 2: Optional - Add Type Safety + +Import animation constants for type safety: + +\`\`\`javascript +import { SemanticAnimations, LegacyAnimations } from '@kjanat/owen' + +// Type-safe animation names +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) +context.getClip(LegacyAnimations.WAIT_IDLE_L) +\`\`\` + +### Step 3: Optional - Use Animation Mapper + +For advanced use cases, use the mapper directly: + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +// Convert legacy to semantic +const semantic = mapper.convert('wait_idle_L', 'semantic') +console.log(semantic) // 'OwenWaitIdleLoop' + +// Validate animation names +const validation = mapper.validateAnimationName('my_animation') +if (!validation.isValid) { + console.log('Suggestions:', validation.suggestions) +} +\`\`\` + +### Step 4: Optional - Update Asset Pipeline + +If you use build tools, integrate automatic conversion: + +\`\`\`javascript +// Build script example +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +// Convert Blender asset names to code-friendly names +const blenderName = 'Owen_WaitIdle' +const codeName = mapper.convert(blenderName, 'semantic') +// Use codeName in your generated code/configs +\`\`\` + +## Common Migration Patterns + +### Pattern 1: Animation Loading with Fallbacks + +\`\`\`javascript +function loadAnimation(context, animationName) { + try { + return context.getClip(animationName) + } catch (error) { + // Try converting to different schemes as fallback + const mapper = new AnimationNameMapper() + + try { + const semantic = mapper.convert(animationName, 'semantic') + return context.getClip(semantic) + } catch { + console.warn(\`Animation not found: \${animationName}\`) + return context.getClip('OwenWaitIdleLoop') // Default fallback + } + } +} +\`\`\` + +### Pattern 2: Dynamic Animation Selection + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +function getAnimationsByEmotion(emotion, scheme = 'semantic') { + return mapper.getAllAnimationsByScheme(scheme) + .filter(anim => anim.toLowerCase().includes(emotion.toLowerCase())) +} + +// Usage +const angryAnimations = getAnimationsByEmotion('angry') +console.log(angryAnimations) // ['OwenReactAngryShort', 'OwenWaitAngryLoop', ...] +\`\`\` + +### Pattern 3: Validation in Development + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +function validateAndLoadAnimation(context, animationName) { + const validation = mapper.validateAnimationName(animationName) + + if (!validation.isValid) { + console.warn(\`Invalid animation: \${animationName}\`) + console.log('Did you mean:', validation.suggestions.slice(0, 3)) + return null + } + + return context.getClip(animationName) +} +\`\`\` + +## Updating Existing Code + +### Search and Replace Patterns + +For bulk updates, you can use these search patterns: + +**Legacy to Semantic:** +- \`wait_idle_L\` → \`OwenWaitIdleLoop\` +- \`react_angry_S\` → \`OwenReactAngryShort\` +- \`sleep_peace_L\` → \`OwenSleepPeaceLoop\` +- \`type_idle_L\` → \`OwenTypeIdleLoop\` + +**Script for Automatic Conversion:** +\`\`\`bash +# Use the conversion script +node scripts/convert-animation-names.js --input my-animations.json --to semantic --output converted.json +\`\`\` + +### Code Analysis + +Use the validation script to analyze your codebase: + +\`\`\`bash +# Check for naming conflicts +node scripts/check-naming-conflicts.js + +# Test multi-scheme functionality +node scripts/test-multi-schemes.js +\`\`\` + +## Testing Your Migration + +### Unit Tests + +\`\`\`javascript +import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen' + +describe('Animation Migration', () => { + let context, mapper + + beforeEach(() => { + context = new OwenAnimationContext(mockGltf) + mapper = new AnimationNameMapper() + }) + + test('legacy animations still work', () => { + expect(() => context.getClip('wait_idle_L')).not.toThrow() + }) + + test('semantic animations work', () => { + expect(() => context.getClip('OwenWaitIdleLoop')).not.toThrow() + }) + + test('conversion consistency', () => { + const legacy = 'wait_idle_L' + const semantic = mapper.convert(legacy, 'semantic') + const backToLegacy = mapper.convert(semantic, 'legacy') + + expect(backToLegacy).toBe(legacy) + }) +}) +\`\`\` + +### Integration Tests + +\`\`\`javascript +describe('Multi-scheme Integration', () => { + test('same animation different schemes', () => { + const animations = [ + 'wait_idle_L', + 'Owen_WaitIdle', + 'owen.state.wait.idle.loop', + 'OwenWaitIdleLoop' + ] + + // All should resolve to the same clip + const clips = animations.map(anim => context.getClip(anim)) + expect(clips.every(clip => clip === clips[0])).toBe(true) + }) +}) +\`\`\` + +## Rollback Strategy + +If you need to rollback changes: + +1. **No Code Changes**: If you only added new schemes, remove them from constants +2. **Code Changes**: Revert to using only legacy names - the system will still work +3. **Asset Changes**: Rename assets back to legacy format + +The system is designed to be safe for gradual adoption and easy rollback. + +## Team Migration Guide + +### For Developers +1. Learn the semantic scheme for new code +2. Use validation tools during development +3. Add type-safe constants to catch errors early +4. Review naming consistency in code reviews + +### For Artists +1. Adopt the artist scheme (\`Owen_AnimationName\`) in Blender +2. Use descriptive, readable names +3. Follow the Owen prefix convention +4. Test animations with the validation tools + +### For Technical Artists +1. Set up build pipeline integration +2. Configure automatic name conversion +3. Establish validation workflows +4. Create documentation for team processes + +## Troubleshooting + +### Common Issues + +**Issue**: Animation not found after migration +**Solution**: Check if the name was converted correctly using the mapper + +**Issue**: Build pipeline broken +**Solution**: Ensure asset names follow the chosen scheme consistently + +**Issue**: Team confusion about which scheme to use +**Solution**: Establish clear guidelines - semantic for code, artist for assets + +### Getting Help + +- Check animation name with: \`mapper.validateAnimationName(name)\` +- View all available names: \`mapper.getAllAnimationsByScheme(scheme)\` +- Convert between schemes: \`mapper.convert(name, targetScheme)\` + +## Next Steps + +After migration: +1. Update team documentation +2. Establish naming guidelines +3. Set up automated validation +4. Train team members on new workflows +5. Monitor for consistency in code reviews + +The multi-scheme system grows with your project and team needs! +` + + const docsDir = path.join(PROJECT_ROOT, 'docs') + fs.writeFileSync(path.join(docsDir, 'MIGRATION_GUIDE.md'), content, 'utf8') + console.log('šŸ“„ Generated: MIGRATION_GUIDE.md') +} + +/** + * Generate comprehensive examples + */ +async function generateExamples (mapper, animationData, timestamp) { + const content = `# Animation System Examples + +*Generated: ${timestamp}* + +This document provides comprehensive examples of using the Owen Animation System with multiple naming schemes. + +## Basic Usage Examples + +### Loading Animations + +\`\`\`javascript +import { OwenAnimationContext } from '@kjanat/owen' + +const context = new OwenAnimationContext(gltf) + +// Legacy scheme +const idleClip = context.getClip('wait_idle_L') + +// Artist scheme +const reactClip = context.getClip('Owen_ReactAngry') + +// Hierarchical scheme +const sleepClip = context.getClip('owen.state.sleep.peace.loop') + +// Semantic scheme +const typeClip = context.getClip('OwenTypeIdleLoop') +\`\`\` + +### Using Animation Constants + +\`\`\`javascript +import { + LegacyAnimations, + ArtistAnimations, + SemanticAnimations, + HierarchicalAnimations +} from '@kjanat/owen' + +// Type-safe animation loading +context.getClip(LegacyAnimations.WAIT_IDLE_L) +context.getClip(ArtistAnimations.REACT_ANGRY) +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) +context.getClip(HierarchicalAnimations.STATE_SLEEP_PEACE_LOOP) +\`\`\` + +## Animation Name Conversion + +### Basic Conversion + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +// Convert legacy to semantic +const semantic = mapper.convert('wait_idle_L', 'semantic') +console.log(semantic) // 'OwenWaitIdleLoop' + +// Convert artist to hierarchical +const hierarchical = mapper.convert('Owen_ReactAngry', 'hierarchical') +console.log(hierarchical) // 'owen.state.react.angry.short' + +// Convert semantic to legacy +const legacy = mapper.convert('OwenSleepPeaceLoop', 'legacy') +console.log(legacy) // 'sleep_peace_L' +\`\`\` + +### Batch Conversion + +\`\`\`javascript +const legacyNames = ['wait_idle_L', 'react_angry_S', 'sleep_peace_L'] + +const semanticNames = legacyNames.map(name => + mapper.convert(name, 'semantic') +) + +console.log(semanticNames) +// ['OwenWaitIdleLoop', 'OwenReactAngryShort', 'OwenSleepPeaceLoop'] +\`\`\` + +### Get All Scheme Variants + +\`\`\`javascript +const allVariants = mapper.getAllNames('wait_idle_L') + +console.log(allVariants) +// { +// legacy: 'wait_idle_L', +// artist: 'Owen_WaitIdle', +// hierarchical: 'owen.state.wait.idle.loop', +// semantic: 'OwenWaitIdleLoop' +// } +\`\`\` + +## Validation Examples + +### Basic Validation + +\`\`\`javascript +const validation = mapper.validateAnimationName('unknown_animation') + +console.log(validation.isValid) // false +console.log(validation.detectedScheme) // null +console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L', ...] +console.log(validation.errors) // ['Animation not found in any scheme'] +\`\`\` + +### Validation with Error Handling + +\`\`\`javascript +function safeLoadAnimation(context, animationName) { + const validation = mapper.validateAnimationName(animationName) + + if (validation.isValid) { + return context.getClip(animationName) + } else { + console.warn(\`Invalid animation: \${animationName}\`) + + if (validation.suggestions.length > 0) { + console.log('Did you mean:', validation.suggestions[0]) + return context.getClip(validation.suggestions[0]) + } + + // Fallback to default animation + return context.getClip('OwenWaitIdleLoop') + } +} +\`\`\` + +## Scheme Detection + +### Automatic Detection + +\`\`\`javascript +const animations = [ + 'wait_idle_L', // legacy + 'Owen_ReactAngry', // artist + 'owen.state.sleep.peace.loop', // hierarchical + 'OwenTypeIdleLoop' // semantic +] + +animations.forEach(anim => { + const scheme = mapper.detectScheme(anim) + console.log(\`\${anim} -> \${scheme}\`) +}) +\`\`\` + +### Scheme-Specific Processing + +\`\`\`javascript +function processAnimationByScheme(animationName) { + const scheme = mapper.detectScheme(animationName) + + switch (scheme) { + case 'legacy': + console.log('Processing legacy animation:', animationName) + break + case 'artist': + console.log('Processing artist animation:', animationName) + break + case 'hierarchical': + console.log('Processing hierarchical animation:', animationName) + break + case 'semantic': + console.log('Processing semantic animation:', animationName) + break + default: + console.log('Unknown scheme for:', animationName) + } +} +\`\`\` + +## Filtering and Searching + +### Get Animations by Scheme + +\`\`\`javascript +// Get all semantic animations +const semanticAnimations = mapper.getAllAnimationsByScheme('semantic') +console.log('Semantic animations:', semanticAnimations.length) + +// Get all artist animations +const artistAnimations = mapper.getAllAnimationsByScheme('artist') +console.log('Artist animations:', artistAnimations.length) +\`\`\` + +### Filter by State and Emotion + +\`\`\`javascript +import { getAnimationsByStateAndEmotion } from '@kjanat/owen' + +// Get all wait animations +const waitAnimations = getAnimationsByStateAndEmotion('wait') +console.log('Wait animations:', waitAnimations) + +// Get angry react animations +const angryReactions = getAnimationsByStateAndEmotion('react', 'angry') +console.log('Angry reactions:', angryReactions) + +// Get peaceful sleep animations in hierarchical scheme +const peacefulSleep = getAnimationsByStateAndEmotion('sleep', 'peace', 'hierarchical') +console.log('Peaceful sleep:', peacefulSleep) +\`\`\` + +### Custom Filtering + +\`\`\`javascript +function findAnimationsByKeyword(keyword, scheme = 'semantic') { + const allAnimations = mapper.getAllAnimationsByScheme(scheme) + return allAnimations.filter(anim => + anim.toLowerCase().includes(keyword.toLowerCase()) + ) +} + +// Find all idle animations +const idleAnimations = findAnimationsByKeyword('idle') + +// Find all loop animations +const loopAnimations = findAnimationsByKeyword('loop') +\`\`\` + +## Advanced Integration Patterns + +### Animation State Machine + +\`\`\`javascript +class CharacterAnimationState { + constructor(context) { + this.context = context + this.mapper = new AnimationNameMapper() + this.currentState = 'idle' + this.currentEmotion = 'neutral' + } + + setState(state, emotion = this.currentEmotion) { + this.currentState = state + this.currentEmotion = emotion + + // Find appropriate animation + const animations = getAnimationsByStateAndEmotion(state, emotion, 'semantic') + + if (animations.length > 0) { + const animationName = animations[0] // Pick first match + const clip = this.context.getClip(animationName) + + console.log(\`Transitioning to: \${animationName}\`) + return clip + } else { + console.warn(\`No animation found for state: \${state}, emotion: \${emotion}\`) + return this.context.getClip('OwenWaitIdleLoop') // Default fallback + } + } +} + +// Usage +const character = new CharacterAnimationState(context) +character.setState('react', 'angry') // Plays OwenReactAngryShort +character.setState('sleep', 'peace') // Plays OwenSleepPeaceLoop +\`\`\` + +### Animation Preloader + +\`\`\`javascript +class AnimationPreloader { + constructor(context) { + this.context = context + this.mapper = new AnimationNameMapper() + this.preloadedClips = new Map() + } + + preloadScheme(scheme) { + const animations = this.mapper.getAllAnimationsByScheme(scheme) + + animations.forEach(animName => { + try { + const clip = this.context.getClip(animName) + this.preloadedClips.set(animName, clip) + console.log(\`Preloaded: \${animName}\`) + } catch (error) { + console.warn(\`Failed to preload: \${animName}\`) + } + }) + + return this.preloadedClips.size + } + + getClip(animationName) { + // Try preloaded first + if (this.preloadedClips.has(animationName)) { + return this.preloadedClips.get(animationName) + } + + // Fall back to context + return this.context.getClip(animationName) + } +} + +// Usage +const preloader = new AnimationPreloader(context) +preloader.preloadScheme('semantic') // Preload all semantic animations +\`\`\` + +### Cross-Scheme Animation Manager + +\`\`\`javascript +class CrossSchemeAnimationManager { + constructor(context) { + this.context = context + this.mapper = new AnimationNameMapper() + } + + playAnimation(animationName, preferredScheme = 'semantic') { + try { + // Try the animation as-is first + return this.context.getClip(animationName) + } catch (error) { + console.log(\`Direct lookup failed for: \${animationName}\`) + + // Try converting to preferred scheme + try { + const converted = this.mapper.convert(animationName, preferredScheme) + console.log(\`Converted \${animationName} to \${converted}\`) + return this.context.getClip(converted) + } catch (conversionError) { + // Try all schemes + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + for (const scheme of schemes) { + try { + const converted = this.mapper.convert(animationName, scheme) + console.log(\`Found in \${scheme} scheme: \${converted}\`) + return this.context.getClip(converted) + } catch { + continue + } + } + + throw new Error(\`Animation not found in any scheme: \${animationName}\`) + } + } + } +} +\`\`\` + +## React/Vue Integration Examples + +### React Hook + +\`\`\`jsx +import { useState, useEffect } from 'react' +import { AnimationNameMapper } from '@kjanat/owen' + +function useAnimationValidator(initialAnimation = '') { + const [animationName, setAnimationName] = useState(initialAnimation) + const [validation, setValidation] = useState(null) + const [mapper] = useState(() => new AnimationNameMapper()) + + useEffect(() => { + if (animationName) { + const result = mapper.validateAnimationName(animationName) + setValidation(result) + } + }, [animationName, mapper]) + + return { + animationName, + setAnimationName, + validation, + isValid: validation?.isValid ?? false, + suggestions: validation?.suggestions ?? [] + } +} + +// Component usage +function AnimationSelector() { + const { animationName, setAnimationName, isValid, suggestions } = useAnimationValidator() + + return ( +
+ setAnimationName(e.target.value)} + placeholder="Enter animation name" + /> + {!isValid && animationName && ( +
+

Invalid animation name

+ {suggestions.length > 0 && ( +
+

Suggestions:

+ {suggestions.map(suggestion => ( + + ))} +
+ )} +
+ )} +
+ ) +} +\`\`\` + +### Vue Composable + +\`\`\`javascript +import { ref, computed, watch } from 'vue' +import { AnimationNameMapper } from '@kjanat/owen' + +export function useAnimationSchemes() { + const mapper = new AnimationNameMapper() + const currentAnimation = ref('') + const currentScheme = ref('semantic') + + const validation = computed(() => { + if (!currentAnimation.value) return null + return mapper.validateAnimationName(currentAnimation.value) + }) + + const convertedAnimations = computed(() => { + if (!currentAnimation.value || !validation.value?.isValid) return {} + + try { + return mapper.getAllNames(currentAnimation.value) + } catch { + return {} + } + }) + + function convertToScheme(targetScheme) { + if (!currentAnimation.value) return '' + + try { + return mapper.convert(currentAnimation.value, targetScheme) + } catch { + return '' + } + } + + return { + currentAnimation, + currentScheme, + validation, + convertedAnimations, + convertToScheme, + isValid: computed(() => validation.value?.isValid ?? false) + } +} +\`\`\` + +## Testing Examples + +### Unit Tests + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +describe('AnimationNameMapper', () => { + let mapper + + beforeEach(() => { + mapper = new AnimationNameMapper() + }) + + test('converts legacy to semantic', () => { + expect(mapper.convert('wait_idle_L', 'semantic')).toBe('OwenWaitIdleLoop') + }) + + test('detects schemes correctly', () => { + expect(mapper.detectScheme('wait_idle_L')).toBe('legacy') + expect(mapper.detectScheme('Owen_ReactAngry')).toBe('artist') + expect(mapper.detectScheme('owen.state.sleep.peace.loop')).toBe('hierarchical') + expect(mapper.detectScheme('OwenTypeIdleLoop')).toBe('semantic') + }) + + test('validates animation names', () => { + const valid = mapper.validateAnimationName('wait_idle_L') + expect(valid.isValid).toBe(true) + + const invalid = mapper.validateAnimationName('invalid_name') + expect(invalid.isValid).toBe(false) + expect(invalid.suggestions.length).toBeGreaterThan(0) + }) + + test('round-trip conversions', () => { + const original = 'wait_idle_L' + const semantic = mapper.convert(original, 'semantic') + const backToLegacy = mapper.convert(semantic, 'legacy') + + expect(backToLegacy).toBe(original) + }) +}) +\`\`\` + +### Integration Tests + +\`\`\`javascript +describe('OwenAnimationContext Multi-Scheme', () => { + let context + + beforeEach(() => { + context = new OwenAnimationContext(mockGltf) + }) + + test('loads animations from all schemes', () => { + const schemes = [ + { name: 'wait_idle_L', scheme: 'legacy' }, + { name: 'Owen_WaitIdle', scheme: 'artist' }, + { name: 'owen.state.wait.idle.loop', scheme: 'hierarchical' }, + { name: 'OwenWaitIdleLoop', scheme: 'semantic' } + ] + + schemes.forEach(({ name, scheme }) => { + expect(() => context.getClip(name)).not.toThrow() + }) + }) + + test('equivalent animations return same clip', () => { + const legacy = context.getClip('wait_idle_L') + const semantic = context.getClip('OwenWaitIdleLoop') + + // Should be the same underlying animation clip + expect(legacy).toBe(semantic) + }) +}) +\`\`\` + +This comprehensive examples document shows how to leverage the full power of the multi-scheme animation system in various scenarios and frameworks. +` + + const examplesDir = path.join(PROJECT_ROOT, 'examples') + if (!fs.existsSync(examplesDir)) { + fs.mkdirSync(examplesDir, { recursive: true }) + } + + fs.writeFileSync(path.join(examplesDir, 'ANIMATION_EXAMPLES.md'), content, 'utf8') + console.log('šŸ“„ Generated: examples/ANIMATION_EXAMPLES.md') +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + generateAnimationDocs() + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/generate-scheme-examples.js b/scripts/generate-scheme-examples.js new file mode 100644 index 0000000..83f83b6 --- /dev/null +++ b/scripts/generate-scheme-examples.js @@ -0,0 +1,802 @@ +#!/usr/bin/env node + +/** + * Generate Scheme Examples Script + * Creates example files for each naming scheme + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Generate scheme examples + */ +async function generateSchemeExamples () { + try { + console.log('šŸŽØ Generating Scheme Examples...') + + // Import the AnimationNameMapper + const { AnimationNameMapper } = await import(ANIMATION_MAPPER_PATH) + const mapper = new AnimationNameMapper() + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + const examplesDir = path.join(PROJECT_ROOT, 'examples', 'scheme-examples') + + // Create examples directory + if (!fs.existsSync(examplesDir)) { + fs.mkdirSync(examplesDir, { recursive: true }) + } + + // Generate examples for each scheme + for (const scheme of schemes) { + await generateSchemeExample(mapper, scheme, examplesDir) + } + + // Generate comparison example + await generateComparisonExample(mapper, examplesDir) + + // Generate integration examples + await generateIntegrationExamples(mapper, examplesDir) + + console.log('āœ… Scheme examples generated successfully!') + } catch (error) { + console.error('āŒ Error generating scheme examples:', error.message) + process.exit(1) + } +} + +/** + * Generate example for a specific scheme + */ +async function generateSchemeExample (mapper, scheme, examplesDir) { + const animations = mapper.getAllAnimationsByScheme(scheme) + const schemeTitle = scheme.charAt(0).toUpperCase() + scheme.slice(1) + + const content = `# ${schemeTitle} Scheme Examples + +This example demonstrates using animations with the **${scheme}** naming scheme. + +## Animation Names (${animations.length} total) + +${animations.map(name => `- \`${name}\``).join('\n')} + +## Usage Example + +\`\`\`javascript +import { OwenAnimationContext } from '@kjanat/owen' + +const context = new OwenAnimationContext(gltf) + +// Load ${scheme} scheme animations +${animations.slice(0, 5).map(name => `const clip${animations.indexOf(name) + 1} = context.getClip('${name}')`).join('\n')} + +// Play the first animation +clip1.play() +\`\`\` + +## Scheme Characteristics + +${getSchemeCharacteristics(scheme)} + +## Converting to Other Schemes + +\`\`\`javascript +import { AnimationNameMapper } from '@kjanat/owen' + +const mapper = new AnimationNameMapper() + +// Convert ${scheme} animations to other schemes +${animations.slice(0, 3).map(name => { + const otherSchemes = ['legacy', 'artist', 'hierarchical', 'semantic'].filter(s => s !== scheme) + return ` +// ${name} +${otherSchemes.map(targetScheme => { + try { + const converted = mapper.convert(name, targetScheme) + return `const ${targetScheme} = mapper.convert('${name}', '${targetScheme}') // '${converted}'` + } catch { + return `// Cannot convert to ${targetScheme}` + } + }).join('\n')}` +}).join('\n')} +\`\`\` + +## Best Practices for ${schemeTitle} Scheme + +${getSchemeBestPractices(scheme)} +` + + fs.writeFileSync(path.join(examplesDir, `${scheme}-example.md`), content, 'utf8') + console.log(`šŸ“„ Generated: ${scheme}-example.md`) +} + +/** + * Generate comparison example + */ +async function generateComparisonExample (mapper, examplesDir) { + const sampleAnimations = ['wait_idle_L', 'react_angry_S', 'sleep_peace_L', 'type_idle_L'] + + const content = `# Multi-Scheme Comparison Example + +This example shows the same animations represented in all four naming schemes. + +## Side-by-Side Comparison + +| Animation | Legacy | Artist | Hierarchical | Semantic | +|-----------|---------|--------|--------------|----------| +${sampleAnimations.map(legacyName => { + try { + const artist = mapper.convert(legacyName, 'artist') + const hierarchical = mapper.convert(legacyName, 'hierarchical') + const semantic = mapper.convert(legacyName, 'semantic') + return `| Base | \`${legacyName}\` | \`${artist}\` | \`${hierarchical}\` | \`${semantic}\` |` + } catch { + return `| Base | \`${legacyName}\` | - | - | - |` + } +}).join('\n')} + +## Loading the Same Animation in Different Schemes + +\`\`\`javascript +import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen' + +const context = new OwenAnimationContext(gltf) +const mapper = new AnimationNameMapper() + +// These all load the same animation clip +const clip1 = context.getClip('wait_idle_L') // Legacy +const clip2 = context.getClip('Owen_WaitIdle') // Artist +const clip3 = context.getClip('owen.state.wait.idle.loop') // Hierarchical +const clip4 = context.getClip('OwenWaitIdleLoop') // Semantic + +// All clips are identical +console.log(clip1 === clip2 === clip3 === clip4) // true +\`\`\` + +## Dynamic Scheme Conversion + +\`\`\`javascript +function loadAnimationInScheme(animationName, targetScheme) { + const mapper = new AnimationNameMapper() + + try { + // Convert to target scheme + const convertedName = mapper.convert(animationName, targetScheme) + console.log(\`Converted \${animationName} to \${convertedName}\`) + + // Load the animation + return context.getClip(convertedName) + } catch (error) { + console.error('Conversion failed:', error.message) + return null + } +} + +// Usage examples +const semanticClip = loadAnimationInScheme('wait_idle_L', 'semantic') +const artistClip = loadAnimationInScheme('OwenReactAngryShort', 'artist') +const hierarchicalClip = loadAnimationInScheme('Owen_SleepPeace', 'hierarchical') +\`\`\` + +## Scheme Detection and Auto-Conversion + +\`\`\`javascript +function smartLoadAnimation(animationName, preferredScheme = 'semantic') { + const mapper = new AnimationNameMapper() + + // Detect the current scheme + const currentScheme = mapper.detectScheme(animationName) + console.log(\`Detected scheme: \${currentScheme}\`) + + if (currentScheme === preferredScheme) { + // Already in preferred scheme + return context.getClip(animationName) + } else { + // Convert to preferred scheme + const converted = mapper.convert(animationName, preferredScheme) + console.log(\`Converted to \${preferredScheme}: \${converted}\`) + return context.getClip(converted) + } +} + +// Examples +smartLoadAnimation('wait_idle_L') // Converts to semantic +smartLoadAnimation('Owen_ReactAngry') // Converts to semantic +smartLoadAnimation('OwenSleepPeaceLoop') // Already semantic, no conversion +\`\`\` + +## Validation Across Schemes + +\`\`\`javascript +function validateAndConvert(animationName) { + const mapper = new AnimationNameMapper() + const validation = mapper.validateAnimationName(animationName) + + if (validation.isValid) { + console.log(\`āœ… Valid animation: \${animationName}\`) + console.log(\`Detected scheme: \${validation.detectedScheme}\`) + + // Show all scheme variants + const allNames = mapper.getAllNames(animationName) + console.log('All scheme variants:', allNames) + + return allNames + } else { + console.log(\`āŒ Invalid animation: \${animationName}\`) + console.log('Suggestions:', validation.suggestions.slice(0, 3)) + + return null + } +} + +// Test with various inputs +validateAndConvert('wait_idle_L') // Valid +validateAndConvert('Owen_Unknown') // Invalid, shows suggestions +validateAndConvert('typing_fast_L') // Valid if exists +\`\`\` + +## Use Case Examples + +### For Artists (Blender Workflow) +\`\`\`javascript +// Artists work with Owen_AnimationName format +const artistAnimations = [ + 'Owen_WaitIdle', + 'Owen_ReactAngry', + 'Owen_SleepPeace', + 'Owen_TypeFast' +] + +// Automatically convert to code-friendly names +const codeAnimations = artistAnimations.map(anim => + mapper.convert(anim, 'semantic') +) + +console.log(codeAnimations) +// ['OwenWaitIdleLoop', 'OwenReactAngryShort', 'OwenSleepPeaceLoop', 'OwenTypeFastLoop'] +\`\`\` + +### For Developers (Type Safety) +\`\`\`javascript +import { SemanticAnimations, AnimationsByScheme } from '@kjanat/owen' + +// Type-safe animation loading +context.getClip(SemanticAnimations.WAIT_IDLE_LOOP) +context.getClip(SemanticAnimations.REACT_ANGRY_SHORT) + +// Scheme-specific constants +const legacyAnimations = AnimationsByScheme.legacy +const artistAnimations = AnimationsByScheme.artist +\`\`\` + +### For Large Projects (Organization) +\`\`\`javascript +// Use hierarchical scheme for better organization +const waitAnimations = mapper.getAllAnimationsByScheme('hierarchical') + .filter(anim => anim.includes('.wait.')) + +const reactAnimations = mapper.getAllAnimationsByScheme('hierarchical') + .filter(anim => anim.includes('.react.')) + +console.log('Wait animations:', waitAnimations) +console.log('React animations:', reactAnimations) +\`\`\` + +This example demonstrates the flexibility and power of the multi-scheme animation system! +` + + fs.writeFileSync(path.join(examplesDir, 'comparison-example.md'), content, 'utf8') + console.log('šŸ“„ Generated: comparison-example.md') +} + +/** + * Generate integration examples + */ +async function generateIntegrationExamples (mapper, examplesDir) { + const content = `# Integration Examples + +Real-world integration examples for different frameworks and use cases. + +## React Integration + +\`\`\`jsx +import React, { useState, useEffect } from 'react' +import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen' + +function AnimationPlayer({ gltf }) { + const [context] = useState(() => new OwenAnimationContext(gltf)) + const [mapper] = useState(() => new AnimationNameMapper()) + const [currentAnimation, setCurrentAnimation] = useState('OwenWaitIdleLoop') + const [scheme, setScheme] = useState('semantic') + + const availableAnimations = mapper.getAllAnimationsByScheme(scheme) + + const playAnimation = (animationName) => { + try { + const clip = context.getClip(animationName) + clip.play() + setCurrentAnimation(animationName) + } catch (error) { + console.error('Failed to play animation:', error) + } + } + + return ( +
+

Animation Player

+ + + +
+

Available Animations ({scheme} scheme)

+ {availableAnimations.map(anim => ( + + ))} +
+ +

Currently playing: {currentAnimation}

+
+ ) +} +\`\`\` + +## Vue.js Integration + +\`\`\`vue + + + +\`\`\` + +## Node.js Build Script Integration + +\`\`\`javascript +// build-animations.js +import fs from 'fs' +import path from 'path' +import { AnimationNameMapper } from '@kjanat/owen' + +class AnimationBuildProcessor { + constructor() { + this.mapper = new AnimationNameMapper() + } + + async processBlenderAssets(inputDir, outputDir) { + console.log('Processing Blender animation assets...') + + const blenderFiles = fs.readdirSync(inputDir) + .filter(file => file.endsWith('.blend')) + + const processedAssets = [] + + for (const blenderFile of blenderFiles) { + const baseName = path.basename(blenderFile, '.blend') + + // Convert artist naming to semantic for code + try { + const semanticName = this.mapper.convert(baseName, 'semantic') + + processedAssets.push({ + blenderFile: baseName, + semanticName, + artistName: baseName, + legacyName: this.mapper.convert(semanticName, 'legacy'), + hierarchicalName: this.mapper.convert(semanticName, 'hierarchical') + }) + + console.log(\`Processed: \${baseName} -> \${semanticName}\`) + } catch (error) { + console.warn(\`Skipped invalid animation name: \${baseName}\`) + } + } + + // Generate animation manifest + const manifest = { + buildTime: new Date().toISOString(), + totalAssets: processedAssets.length, + assets: processedAssets, + schemes: { + artist: processedAssets.map(a => a.artistName), + semantic: processedAssets.map(a => a.semanticName), + legacy: processedAssets.map(a => a.legacyName), + hierarchical: processedAssets.map(a => a.hierarchicalName) + } + } + + fs.writeFileSync( + path.join(outputDir, 'animation-manifest.json'), + JSON.stringify(manifest, null, 2) + ) + + return manifest + } + + generateTypescriptConstants(manifest, outputFile) { + const content = \`// Auto-generated animation constants +export const AnimationAssets = { +\${manifest.assets.map(asset => \` '\${asset.semanticName}': { + semantic: '\${asset.semanticName}', + artist: '\${asset.artistName}', + legacy: '\${asset.legacyName}', + hierarchical: '\${asset.hierarchicalName}', + blenderFile: '\${asset.blenderFile}.blend' + }\`).join(',\\n')} +} as const + +export type AnimationName = keyof typeof AnimationAssets +\` + + fs.writeFileSync(outputFile, content) + console.log(\`Generated TypeScript constants: \${outputFile}\`) + } +} + +// Usage in build pipeline +const processor = new AnimationBuildProcessor() + +processor.processBlenderAssets('./assets/blender', './dist') + .then(manifest => { + processor.generateTypescriptConstants(manifest, './src/generated/animations.ts') + console.log('Animation build complete!') + }) + .catch(console.error) +\`\`\` + +## Webpack Plugin Integration + +\`\`\`javascript +// webpack-animation-plugin.js +import { AnimationNameMapper } from '@kjanat/owen' + +class AnimationValidationPlugin { + constructor(options = {}) { + this.options = { + schemes: ['semantic', 'artist'], + validateOnBuild: true, + generateConstants: true, + ...options + } + this.mapper = new AnimationNameMapper() + } + + apply(compiler) { + compiler.hooks.afterCompile.tap('AnimationValidationPlugin', (compilation) => { + if (this.options.validateOnBuild) { + this.validateAnimations(compilation) + } + }) + + if (this.options.generateConstants) { + compiler.hooks.emit.tap('AnimationValidationPlugin', (compilation) => { + this.generateConstants(compilation) + }) + } + } + + validateAnimations(compilation) { + // Find animation references in source code + const animationReferences = this.findAnimationReferences(compilation) + + animationReferences.forEach(ref => { + const validation = this.mapper.validateAnimationName(ref.name) + + if (!validation.isValid) { + const error = new Error( + \`Invalid animation name: "\${ref.name}" in \${ref.file}:\${ref.line}\` + ) + compilation.errors.push(error) + } + }) + } + + generateConstants(compilation) { + const constants = this.generateAnimationConstants() + + compilation.assets['animations.generated.js'] = { + source: () => constants, + size: () => constants.length + } + } + + findAnimationReferences(compilation) { + // Implementation to find animation references in source files + // Returns array of { name, file, line } + return [] + } + + generateAnimationConstants() { + const schemes = this.options.schemes + let content = '// Auto-generated animation constants\\n\\n' + + schemes.forEach(scheme => { + const animations = this.mapper.getAllAnimationsByScheme(scheme) + content += \`export const \${scheme.toUpperCase()}_ANIMATIONS = [\` + content += animations.map(anim => \`'\${anim}'\`).join(', ') + content += \`]\\n\\n\` + }) + + return content + } +} + +// webpack.config.js +import AnimationValidationPlugin from './webpack-animation-plugin.js' + +export default { + // ... other config + plugins: [ + new AnimationValidationPlugin({ + schemes: ['semantic', 'artist'], + validateOnBuild: true, + generateConstants: true + }) + ] +} +\`\`\` + +## Testing Integration + +\`\`\`javascript +// animation-test-utils.js +import { AnimationNameMapper, OwenAnimationContext } from '@kjanat/owen' + +export class AnimationTestHelper { + constructor(mockGltf) { + this.context = new OwenAnimationContext(mockGltf) + this.mapper = new AnimationNameMapper() + } + + expectAnimationExists(animationName) { + expect(() => this.context.getClip(animationName)).not.toThrow() + } + + expectAnimationScheme(animationName, expectedScheme) { + const detectedScheme = this.mapper.detectScheme(animationName) + expect(detectedScheme).toBe(expectedScheme) + } + + expectConversionWorks(fromName, toScheme, expectedName) { + const converted = this.mapper.convert(fromName, toScheme) + expect(converted).toBe(expectedName) + } + + expectRoundTripConversion(animationName) { + const originalScheme = this.mapper.detectScheme(animationName) + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + // Convert through all schemes and back + let current = animationName + schemes.forEach(scheme => { + current = this.mapper.convert(current, scheme) + }) + + const final = this.mapper.convert(current, originalScheme) + expect(final).toBe(animationName) + } + + getAllSchemeVariants(animationName) { + return this.mapper.getAllNames(animationName) + } +} + +// Usage in tests +describe('Animation System Integration', () => { + let helper + + beforeEach(() => { + helper = new AnimationTestHelper(mockGltf) + }) + + test('all schemes work', () => { + helper.expectAnimationExists('wait_idle_L') + helper.expectAnimationExists('Owen_WaitIdle') + helper.expectAnimationExists('owen.state.wait.idle.loop') + helper.expectAnimationExists('OwenWaitIdleLoop') + }) + + test('scheme detection', () => { + helper.expectAnimationScheme('wait_idle_L', 'legacy') + helper.expectAnimationScheme('Owen_WaitIdle', 'artist') + helper.expectAnimationScheme('owen.state.wait.idle.loop', 'hierarchical') + helper.expectAnimationScheme('OwenWaitIdleLoop', 'semantic') + }) + + test('conversions work correctly', () => { + helper.expectConversionWorks('wait_idle_L', 'semantic', 'OwenWaitIdleLoop') + helper.expectConversionWorks('Owen_ReactAngry', 'legacy', 'react_angry_S') + }) + + test('round-trip conversions', () => { + helper.expectRoundTripConversion('wait_idle_L') + helper.expectRoundTripConversion('Owen_WaitIdle') + helper.expectRoundTripConversion('OwenSleepPeaceLoop') + }) +}) +\`\`\` + +These integration examples show how to effectively use the multi-scheme animation system in real-world applications and build processes. +` + + fs.writeFileSync(path.join(examplesDir, 'integration-examples.md'), content, 'utf8') + console.log('šŸ“„ Generated: integration-examples.md') +} + +/** + * Get characteristics for a specific scheme + */ +function getSchemeCharacteristics (scheme) { + const characteristics = { + legacy: ` +- **Lowercase with underscores**: Easy to type, traditional format +- **Suffix notation**: \`_L\` for Loop, \`_S\` for Short animations +- **Compact names**: Shorter than other schemes +- **Technical focus**: Designed for developers, not artists`, + + artist: ` +- **Owen prefix**: Consistent branding across all animations +- **PascalCase format**: Easy to read and professional looking +- **Artist-friendly**: No technical jargon or suffixes +- **Blender optimized**: Perfect for animation asset naming`, + + hierarchical: ` +- **Dot notation**: Clear hierarchical structure +- **Category organization**: Groups related animations logically +- **IDE friendly**: Excellent autocomplete support +- **Extensible**: Easy to add new categories and subcategories`, + + semantic: ` +- **Self-documenting**: Animation purpose is immediately clear +- **Modern naming**: Follows contemporary naming conventions +- **Descriptive**: Includes context like duration and emotion +- **Code readable**: Perfect for maintainable codebases` + } + + return characteristics[scheme] || 'No characteristics defined for this scheme.' +} + +/** + * Get best practices for a specific scheme + */ +function getSchemeBestPractices (scheme) { + const practices = { + legacy: ` +1. **Maintain suffix consistency**: Always use \`_L\` for loops, \`_S\` for short animations +2. **Use descriptive words**: Choose clear, short words that describe the animation +3. **Follow underscore convention**: Separate words with underscores, keep lowercase +4. **Document duration**: The suffix should accurately reflect animation type`, + + artist: ` +1. **Always use Owen prefix**: Maintain consistent \`Owen_\` branding +2. **Use PascalCase**: Capitalize each word, no spaces or underscores +3. **Be descriptive**: Choose names that clearly describe the animation's purpose +4. **Keep it simple**: Avoid technical terms, focus on what the animation shows`, + + hierarchical: ` +1. **Follow the hierarchy**: Use \`owen.category.subcategory.type\` structure +2. **Be consistent**: Use the same categories for similar animations +3. **Plan the structure**: Think about organization before adding new categories +4. **Document categories**: Keep a reference of what each category contains`, + + semantic: ` +1. **Be descriptive**: Names should clearly indicate the animation's purpose +2. **Include context**: Add emotional state, duration, or other relevant details +3. **Use PascalCase**: Follow modern JavaScript naming conventions +4. **Stay consistent**: Use similar naming patterns for related animations` + } + + return practices[scheme] || 'No best practices defined for this scheme.' +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + generateSchemeExamples() + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/test-multi-schemes.js b/scripts/test-multi-schemes.js new file mode 100644 index 0000000..9533007 --- /dev/null +++ b/scripts/test-multi-schemes.js @@ -0,0 +1,315 @@ +#!/usr/bin/env node + +/** + * Test Multi-Scheme Functionality Script + * Comprehensive testing of the multi-scheme animation system + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath, pathToFileURL } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const PROJECT_ROOT = path.resolve(__dirname, '..') +const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js') + +/** + * Run comprehensive multi-scheme tests + */ +async function testMultiSchemes () { + try { + console.log('🧪 Testing Multi-Scheme Animation System...') + + // Import the AnimationNameMapper + const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH) + const { AnimationNameMapper } = await import(animationMapperUrl) + const mapper = new AnimationNameMapper() + + const testResults = { + timestamp: new Date().toISOString(), + passed: 0, + failed: 0, + tests: [] + } + + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + /** + * Add a test result + */ + function addTest (name, passed, details = {}, error = null) { + const test = { + name, + passed, + details, + error: error?.message || error + } + testResults.tests.push(test) + if (passed) { + testResults.passed++ + console.log(`āœ… ${name}`) + } else { + testResults.failed++ + console.log(`āŒ ${name}: ${error}`) + } + return test + } + + // Test 1: Basic scheme detection + console.log('\nšŸ” Testing scheme detection...') + const testCases = [ + { name: 'wait_idle_L', expectedScheme: 'legacy' }, + { name: 'Owen_ReactAngry', expectedScheme: 'artist' }, + { name: 'owen.state.wait.idle.loop', expectedScheme: 'hierarchical' }, + { name: 'OwenSleepToWaitTransition', expectedScheme: 'semantic' } + ] + + testCases.forEach(testCase => { + try { + const detectedScheme = mapper.detectScheme(testCase.name) + const passed = detectedScheme === testCase.expectedScheme + addTest( + `Detect scheme for ${testCase.name}`, + passed, + { detected: detectedScheme, expected: testCase.expectedScheme }, + passed ? null : `Expected ${testCase.expectedScheme}, got ${detectedScheme}` + ) + } catch (error) { + addTest(`Detect scheme for ${testCase.name}`, false, {}, error) + } + }) + + // Test 2: Conversion between schemes + console.log('\nšŸ”„ Testing scheme conversions...') + const conversionTests = [ + { from: 'wait_idle_L', to: 'semantic', expected: 'OwenWaitIdleLoop' }, + { from: 'Owen_ReactAngry', to: 'legacy', expected: 'react_angry_L' }, + { from: 'owen.state.sleep.peace.loop', to: 'artist', expected: 'Owen_SleepPeace' }, + { from: 'OwenTypeIdleLoop', to: 'hierarchical', expected: 'owen.state.type.idle.loop' } + ] + + conversionTests.forEach(test => { + try { + const result = mapper.convert(test.from, test.to) + const passed = result === test.expected + addTest( + `Convert ${test.from} to ${test.to}`, + passed, + { result, expected: test.expected }, + passed ? null : `Expected ${test.expected}, got ${result}` + ) + } catch (error) { + addTest(`Convert ${test.from} to ${test.to}`, false, {}, error) + } + }) + + // Test 3: Round-trip conversions (should return to original) + console.log('\nšŸ” Testing round-trip conversions...') + const roundTripTests = ['wait_idle_L', 'Owen_ReactAngry', 'owen.state.sleep.peace.loop', 'OwenTypeIdleLoop'] + + roundTripTests.forEach(originalName => { + try { + const originalScheme = mapper.detectScheme(originalName) + let currentName = originalName + + // Convert through all other schemes and back + const otherSchemes = schemes.filter(s => s !== originalScheme) + for (const scheme of otherSchemes) { + currentName = mapper.convert(currentName, scheme) + } + + // Convert back to original scheme + const finalName = mapper.convert(currentName, originalScheme) + const passed = finalName === originalName + + addTest( + `Round-trip conversion for ${originalName}`, + passed, + { original: originalName, final: finalName, path: `${originalName} -> ${otherSchemes.join(' -> ')} -> ${originalName}` }, + passed ? null : `Round-trip failed: ${originalName} -> ${finalName}` + ) + } catch (error) { + addTest(`Round-trip conversion for ${originalName}`, false, {}, error) + } + }) + + // Test 4: Validation functionality + console.log('\nāœ… Testing validation...') + const validationTests = [ + { name: 'wait_idle_L', shouldBeValid: true }, + { name: 'Owen_ValidAnimation', shouldBeValid: true }, + { name: 'invalid_animation_name', shouldBeValid: false }, + { name: 'NotAnAnimation', shouldBeValid: false } + ] + + validationTests.forEach(test => { + try { + const validation = mapper.validateAnimationName(test.name) + const passed = validation.isValid === test.shouldBeValid + addTest( + `Validate ${test.name}`, + passed, + { validation, expected: test.shouldBeValid }, + passed ? null : `Expected valid=${test.shouldBeValid}, got valid=${validation.isValid}` + ) + } catch (error) { + addTest(`Validate ${test.name}`, false, {}, error) + } + }) + + // Test 5: Get all animations by scheme + console.log('\nšŸ“‹ Testing animation retrieval...') + schemes.forEach(scheme => { + try { + const animations = mapper.getAllAnimationsByScheme(scheme) + const passed = Array.isArray(animations) && animations.length > 0 + addTest( + `Get all ${scheme} animations`, + passed, + { count: animations.length, sample: animations.slice(0, 3) }, + passed ? null : 'No animations returned or invalid format' + ) + } catch (error) { + addTest(`Get all ${scheme} animations`, false, {}, error) + } + }) + + // Test 6: Performance test + console.log('\n⚔ Testing performance...') + const performanceAnimations = ['wait_idle_L', 'Owen_ReactAngry', 'owen.state.sleep.peace.loop'] + + try { + const iterations = 1000 + const startTime = Date.now() + + for (let i = 0; i < iterations; i++) { + performanceAnimations.forEach(anim => { + mapper.convert(anim, 'semantic') + mapper.validateAnimationName(anim) + }) + } + + const endTime = Date.now() + const totalTime = endTime - startTime + const operationsPerSecond = Math.round((iterations * performanceAnimations.length * 2) / (totalTime / 1000)) + + const passed = totalTime < 5000 // Should complete in under 5 seconds + addTest( + 'Performance test', + passed, + { + iterations, + totalTimeMs: totalTime, + operationsPerSecond, + averageTimePerOperation: totalTime / (iterations * performanceAnimations.length * 2) + }, + passed ? null : `Performance too slow: ${totalTime}ms for ${iterations} iterations` + ) + } catch (error) { + addTest('Performance test', false, {}, error) + } + + // Test 7: Edge cases + console.log('\nšŸŽÆ Testing edge cases...') + const edgeCases = [ + { name: '', description: 'empty string' }, + { name: 'single', description: 'single word' }, + { name: 'UPPERCASE_NAME_L', description: 'uppercase legacy' }, + { name: 'owen_special_case_S', description: 'legacy with Owen prefix' } + ] + + edgeCases.forEach(testCase => { + try { + const validation = mapper.validateAnimationName(testCase.name) + // These should generally be invalid or have suggestions + const passed = true // Just test that it doesn't crash + addTest( + `Edge case: ${testCase.description}`, + passed, + { input: testCase.name, validation }, + null + ) + } catch (error) { + // It's okay if edge cases throw errors, as long as they're handled gracefully + const passed = error.message && error.message.length > 0 + addTest( + `Edge case: ${testCase.description}`, + passed, + { input: testCase.name }, + passed ? null : 'Unhandled error or empty error message' + ) + } + }) + + // Generate final report + const report = { + ...testResults, + summary: { + total: testResults.passed + testResults.failed, + passed: testResults.passed, + failed: testResults.failed, + passRate: Math.round((testResults.passed / (testResults.passed + testResults.failed)) * 100), + status: testResults.failed === 0 ? 'PASS' : 'FAIL' + }, + environment: { + nodeVersion: process.version, + platform: process.platform, + timestamp: new Date().toISOString() + } + } + + // Save report + const reportsDir = path.join(PROJECT_ROOT, 'reports') + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }) + } + + fs.writeFileSync( + path.join(reportsDir, 'multi-scheme-test-results.json'), + JSON.stringify(report, null, 2), + 'utf8' + ) + + // Print summary + console.log('\nšŸ“Š MULTI-SCHEME TEST SUMMARY') + console.log('='.repeat(50)) + console.log(`Status: ${report.summary.status}`) + console.log(`Tests: ${report.summary.passed}/${report.summary.total} passed (${report.summary.passRate}%)`) + console.log(`Failed: ${report.summary.failed}`) + + if (testResults.failed > 0) { + console.log('\nāŒ FAILED TESTS:') + testResults.tests + .filter(test => !test.passed) + .forEach(test => { + console.log(`• ${test.name}: ${test.error}`) + }) + } + + console.log(`\nšŸ“ Full report saved to: ${path.join(reportsDir, 'multi-scheme-test-results.json')}`) + + // Exit with error if tests failed + if (testResults.failed > 0) { + process.exit(1) + } + + return report + } catch (error) { + console.error('āŒ Error running multi-scheme tests:', error.message) + process.exit(1) + } +} + +// Run the script if called directly +if (process.argv[1] === __filename) { + testMultiSchemes() + .then(report => { + console.log('āœ… Multi-scheme testing complete!') + }) + .catch(error => { + console.error('šŸ’„ Script failed:', error) + process.exit(1) + }) +} diff --git a/scripts/validate-animations.js b/scripts/validate-animations.js new file mode 100644 index 0000000..8b708fb --- /dev/null +++ b/scripts/validate-animations.js @@ -0,0 +1,168 @@ +#!/usr/bin/env node + +/** + * Animation Name Validation Script + * Validates all animation names across different schemes + */ + +import { AnimationNameMapper } from '../src/animation/AnimationNameMapper.js' +import { writeFile, mkdir } from 'fs/promises' +import { existsSync } from 'fs' + +const PRIMARY_SCHEME = process.env.PRIMARY_SCHEME || 'semantic' + +async function validateAnimations () { + console.log('šŸ” Validating animation naming schemes...') + console.log(`Primary scheme: ${PRIMARY_SCHEME}`) + + const mapper = new AnimationNameMapper() + const results = { + timestamp: new Date().toISOString(), + primaryScheme: PRIMARY_SCHEME, + validations: [], + errors: [], + warnings: [], + summary: {} + } + + try { + // Get all animations from the mapper + const allAnimations = mapper.getAllAnimations() + console.log(`Found ${allAnimations.length} animations to validate`) + + // Validate each animation + for (const animation of allAnimations) { + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + for (const scheme of schemes) { + const animName = animation[scheme] + if (!animName) continue + + try { + const validation = mapper.validateAnimationName(animName) + results.validations.push({ + name: animName, + scheme, + isValid: validation.isValid, + detectedScheme: validation.scheme, + error: validation.error + }) + + if (!validation.isValid) { + results.errors.push({ + name: animName, + scheme, + error: validation.error, + suggestions: validation.suggestions + }) + } + + // Check for scheme mismatches + if (validation.isValid && validation.scheme !== scheme) { + results.warnings.push({ + name: animName, + expectedScheme: scheme, + detectedScheme: validation.scheme, + message: `Animation name "${animName}" expected to be in ${scheme} scheme but detected as ${validation.scheme}` + }) + } + } catch (error) { + results.errors.push({ + name: animName, + scheme, + error: error.message + }) + } + } + } + + // Test conversions between schemes + console.log('šŸ”„ Testing scheme conversions...') + for (const animation of allAnimations) { + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + for (const fromScheme of schemes) { + for (const toScheme of schemes) { + if (fromScheme === toScheme) continue + + const sourceName = animation[fromScheme] + if (!sourceName) continue + + try { + const converted = mapper.convert(sourceName, toScheme) + const expected = animation[toScheme] + + if (converted !== expected) { + results.errors.push({ + name: sourceName, + scheme: `${fromScheme}->${toScheme}`, + error: `Conversion mismatch: expected "${expected}", got "${converted}"` + }) + } + } catch (error) { + results.errors.push({ + name: sourceName, + scheme: `${fromScheme}->${toScheme}`, + error: `Conversion failed: ${error.message}` + }) + } + } + } + } + + // Generate summary + results.summary = { + totalAnimations: allAnimations.length, + totalValidations: results.validations.length, + validAnimations: results.validations.filter(v => v.isValid).length, + invalidAnimations: results.errors.length, + warnings: results.warnings.length, + successRate: ((results.validations.filter(v => v.isValid).length / results.validations.length) * 100).toFixed(2) + } + + // Create reports directory if it doesn't exist + if (!existsSync('reports')) { + await mkdir('reports', { recursive: true }) + } + + // Write results to file + await writeFile('reports/animation-validation.json', JSON.stringify(results, null, 2)) + + // Print summary + console.log('\nšŸ“Š Validation Summary:') + console.log(`āœ… Valid animations: ${results.summary.validAnimations}/${results.summary.totalValidations}`) + console.log(`āŒ Invalid animations: ${results.summary.invalidAnimations}`) + console.log(`āš ļø Warnings: ${results.summary.warnings}`) + console.log(`šŸ“ˆ Success rate: ${results.summary.successRate}%`) + + if (results.errors.length > 0) { + console.log('\nāŒ Errors found:') + results.errors.forEach(error => { + console.log(` - ${error.name} (${error.scheme}): ${error.error}`) + }) + } + + if (results.warnings.length > 0) { + console.log('\nāš ļø Warnings:') + results.warnings.forEach(warning => { + console.log(` - ${warning.message}`) + }) + } + + // Exit with error if validation failed + if (results.errors.length > 0) { + console.log('\nšŸ’„ Validation failed with errors') + process.exit(1) + } else { + console.log('\nāœ… All animations validated successfully') + process.exit(0) + } + } catch (error) { + console.error('šŸ’„ Validation script failed:', error) + process.exit(1) + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + validateAnimations() +} diff --git a/scripts/validate-processed-animations.js b/scripts/validate-processed-animations.js new file mode 100644 index 0000000..5128d57 --- /dev/null +++ b/scripts/validate-processed-animations.js @@ -0,0 +1,441 @@ +#!/usr/bin/env node + +/** + * Animation Processing Validation Script + * + * Validates animations processed from Blender or other sources to ensure: + * - Proper naming scheme compliance + * - File integrity and format validation + * - Asset optimization and size requirements + * - Integration with existing animation system + * + * Used by GitHub Actions animation-processing workflow + */ + +import fs from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Configuration +const CONFIG = { + animationsDir: path.join(__dirname, '..', 'assets', 'animations'), + reportsDir: path.join(__dirname, '..', 'reports'), + maxFileSize: 5 * 1024 * 1024, // 5MB max per animation file + supportedFormats: ['.gltf', '.glb', '.fbx', '.json'], + requiredMetadata: ['name', 'duration', 'frameRate'], + namingSchemes: ['legacy', 'artist', 'hierarchical', 'semantic'] +} + +/** + * Validation result structure + */ +class ValidationResult { + constructor () { + this.passed = [] + this.failed = [] + this.warnings = [] + this.stats = { + totalFiles: 0, + validFiles: 0, + invalidFiles: 0, + totalSize: 0, + averageSize: 0 + } + } + + addPass (file, message) { + this.passed.push({ file, message, timestamp: new Date().toISOString() }) + this.stats.validFiles++ + } + + addFail (file, message, error = null) { + this.failed.push({ + file, + message, + error: error?.message || error, + timestamp: new Date().toISOString() + }) + this.stats.invalidFiles++ + } + + addWarning (file, message) { + this.warnings.push({ file, message, timestamp: new Date().toISOString() }) + } + + updateStats (size) { + this.stats.totalFiles++ + this.stats.totalSize += size + this.stats.averageSize = this.stats.totalSize / this.stats.totalFiles + } + + isValid () { + return this.failed.length === 0 + } + + getSummary () { + return { + success: this.isValid(), + summary: { + passed: this.passed.length, + failed: this.failed.length, + warnings: this.warnings.length, + ...this.stats + }, + details: { + passed: this.passed, + failed: this.failed, + warnings: this.warnings + } + } + } +} + +/** + * File format validators + */ +const Validators = { + async validateGLTF (filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const gltf = JSON.parse(content) + + // Basic GLTF structure validation + if (!gltf.asset || !gltf.asset.version) { + throw new Error('Invalid GLTF: Missing asset version') + } + + if (!gltf.animations || !Array.isArray(gltf.animations)) { + throw new Error('Invalid GLTF: Missing or invalid animations array') + } + + return { + valid: true, + animations: gltf.animations.length, + version: gltf.asset.version + } + } catch (error) { + return { valid: false, error: error.message } + } + }, + + async validateGLB (filePath) { + try { + const stats = await fs.stat(filePath) + const buffer = await fs.readFile(filePath) + + // Basic GLB header validation (magic number: 0x46546C67 = "glTF") + if (buffer.length < 12) { + throw new Error('Invalid GLB: File too small') + } + + const magic = buffer.readUInt32LE(0) + if (magic !== 0x46546C67) { + throw new Error('Invalid GLB: Invalid magic number') + } + + const version = buffer.readUInt32LE(4) + if (version !== 2) { + throw new Error(`Invalid GLB: Unsupported version ${version}`) + } + + return { + valid: true, + size: stats.size, + version + } + } catch (error) { + return { valid: false, error: error.message } + } + }, + + async validateJSON (filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const data = JSON.parse(content) + + // Check for required animation metadata + if (!data.name || !data.duration) { + throw new Error('Invalid animation JSON: Missing required metadata') + } + + return { + valid: true, + metadata: data + } + } catch (error) { + return { valid: false, error: error.message } + } + } +} + +/** + * Validates naming scheme compliance + */ +function validateNamingScheme (filename) { + const baseName = path.parse(filename).name + const schemes = { + legacy: /^[a-z][a-z0-9_]*$/, + artist: /^[A-Z][a-zA-Z0-9_]*$/, + hierarchical: /^[a-z]+(\.[a-z]+)*$/, + semantic: /^[a-z]+(_[a-z]+)*$/ + } + + const matchedSchemes = [] + for (const [scheme, pattern] of Object.entries(schemes)) { + if (pattern.test(baseName)) { + matchedSchemes.push(scheme) + } + } + + return { + valid: matchedSchemes.length > 0, + schemes: matchedSchemes, + name: baseName + } +} + +/** + * Validates individual animation file + */ +async function validateAnimationFile (filePath) { + const result = new ValidationResult() + const filename = path.basename(filePath) + const ext = path.extname(filePath).toLowerCase() + + try { + // Check file existence + const stats = await fs.stat(filePath) + result.updateStats(stats.size) + + // Check file size + if (stats.size > CONFIG.maxFileSize) { + result.addWarning(filename, + `File size ${(stats.size / 1024 / 1024).toFixed(2)}MB exceeds recommended maximum ${CONFIG.maxFileSize / 1024 / 1024}MB` + ) + } + + // Check supported format + if (!CONFIG.supportedFormats.includes(ext)) { + result.addFail(filename, `Unsupported file format: ${ext}`) + return result + } + + // Validate naming scheme + const namingValidation = validateNamingScheme(filename) + if (!namingValidation.valid) { + result.addFail(filename, 'Filename does not match any supported naming scheme') + } else { + result.addPass(filename, `Naming scheme compliance: ${namingValidation.schemes.join(', ')}`) + } + + // Format-specific validation + let formatValidation = { valid: true } + switch (ext) { + case '.gltf': + formatValidation = await Validators.validateGLTF(filePath) + break + case '.glb': + formatValidation = await Validators.validateGLB(filePath) + break + case '.json': + formatValidation = await Validators.validateJSON(filePath) + break + } + + if (formatValidation.valid) { + result.addPass(filename, `Valid ${ext.toUpperCase()} format`) + } else { + result.addFail(filename, `Invalid ${ext.toUpperCase()} format: ${formatValidation.error}`) + } + } catch (error) { + result.addFail(filename, `Validation error: ${error.message}`, error) + } + + return result +} + +/** + * Scans directory for animation files + */ +async function scanAnimationFiles (directory) { + const files = [] + + try { + const entries = await fs.readdir(directory, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + // Recursively scan subdirectories + const subFiles = await scanAnimationFiles(fullPath) + files.push(...subFiles) + } else if (entry.isFile()) { + const ext = path.extname(entry.name).toLowerCase() + if (CONFIG.supportedFormats.includes(ext)) { + files.push(fullPath) + } + } + } + } catch (error) { + console.warn(`Warning: Could not scan directory ${directory}: ${error.message}`) + } + + return files +} + +/** + * Generates validation report + */ +async function generateReport (result, outputPath) { + const report = { + timestamp: new Date().toISOString(), + validation: result.getSummary(), + recommendations: [] + } + + // Add recommendations based on results + if (result.failed.length > 0) { + report.recommendations.push('Fix failed validations before proceeding with deployment') + } + + if (result.warnings.length > 0) { + report.recommendations.push('Review warnings to optimize animation assets') + } + + if (result.stats.averageSize > 1024 * 1024) { + report.recommendations.push('Consider optimizing large animation files for better performance') + } + + // Ensure reports directory exists + await fs.mkdir(path.dirname(outputPath), { recursive: true }) + + // Write report + await fs.writeFile(outputPath, JSON.stringify(report, null, 2)) + + return report +} + +/** + * Main validation function + */ +async function validateProcessedAnimations () { + console.log('šŸ” Validating processed animation assets...') + + const overallResult = new ValidationResult() + + try { + // Check if animations directory exists + try { + await fs.access(CONFIG.animationsDir) + } catch { + console.warn(`āš ļø Animations directory not found: ${CONFIG.animationsDir}`) + console.log('āœ… No processed animations to validate') + return true + } + + // Scan for animation files + console.log(`šŸ“ Scanning ${CONFIG.animationsDir}...`) + const animationFiles = await scanAnimationFiles(CONFIG.animationsDir) + + if (animationFiles.length === 0) { + console.log('āœ… No animation files found to validate') + return true + } + + console.log(`šŸ“„ Found ${animationFiles.length} animation files`) + + // Validate each file + for (let i = 0; i < animationFiles.length; i++) { + const file = animationFiles[i] + const relativePath = path.relative(CONFIG.animationsDir, file) + + console.log(`šŸ“ Validating ${i + 1}/${animationFiles.length}: ${relativePath}`) + + const fileResult = await validateAnimationFile(file) + + // Merge results + overallResult.passed.push(...fileResult.passed) + overallResult.failed.push(...fileResult.failed) + overallResult.warnings.push(...fileResult.warnings) + overallResult.stats.totalFiles += fileResult.stats.totalFiles + overallResult.stats.validFiles += fileResult.stats.validFiles + overallResult.stats.invalidFiles += fileResult.stats.invalidFiles + overallResult.stats.totalSize += fileResult.stats.totalSize + } + + // Calculate average size + if (overallResult.stats.totalFiles > 0) { + overallResult.stats.averageSize = overallResult.stats.totalSize / overallResult.stats.totalFiles + } + + // Generate report + const reportPath = path.join(CONFIG.reportsDir, 'processed-animations-validation.json') + await generateReport(overallResult, reportPath) + + // Print summary + console.log('\nšŸ“Š Validation Summary:') + console.log(`āœ… Passed: ${overallResult.passed.length}`) + console.log(`āŒ Failed: ${overallResult.failed.length}`) + console.log(`āš ļø Warnings: ${overallResult.warnings.length}`) + console.log(`šŸ“ Total files: ${overallResult.stats.totalFiles}`) + console.log(`šŸ“¦ Total size: ${(overallResult.stats.totalSize / 1024 / 1024).toFixed(2)}MB`) + console.log(`šŸ“„ Average size: ${(overallResult.stats.averageSize / 1024).toFixed(2)}KB`) + + // Print failures in detail + if (overallResult.failed.length > 0) { + console.log('\nāŒ Validation Failures:') + overallResult.failed.forEach(failure => { + console.log(` • ${failure.file}: ${failure.message}`) + }) + } + + // Print warnings + if (overallResult.warnings.length > 0) { + console.log('\nāš ļø Validation Warnings:') + overallResult.warnings.forEach(warning => { + console.log(` • ${warning.file}: ${warning.message}`) + }) + } + + console.log(`\nšŸ“‹ Full report saved to: ${reportPath}`) + + const isValid = overallResult.isValid() + console.log(isValid ? '\nāœ… All validations passed!' : '\nāŒ Validation failed!') + + return isValid + } catch (error) { + console.error('šŸ’„ Validation process failed:', error.message) + + // Generate error report + const errorReport = { + timestamp: new Date().toISOString(), + success: false, + error: error.message, + stack: error.stack + } + + const reportPath = path.join(CONFIG.reportsDir, 'processed-animations-validation.json') + try { + await fs.mkdir(path.dirname(reportPath), { recursive: true }) + await fs.writeFile(reportPath, JSON.stringify(errorReport, null, 2)) + } catch (reportError) { + console.error('Failed to write error report:', reportError.message) + } + + return false + } +} + +/** + * CLI execution + */ +if (import.meta.url === `file://${process.argv[1]}`) { + const success = await validateProcessedAnimations() + process.exit(success ? 0 : 1) +} + +export { validateProcessedAnimations, validateAnimationFile, ValidationResult } diff --git a/src/animation/AnimationConstants.js b/src/animation/AnimationConstants.js new file mode 100644 index 0000000..6b85bb5 --- /dev/null +++ b/src/animation/AnimationConstants.js @@ -0,0 +1,280 @@ +/** + * @fileoverview Animation constants with multi-scheme support for Owen Animation System + * @module animation/AnimationConstants + */ + +import { AnimationNameMapper } from './AnimationNameMapper.js' + +// Create a singleton instance of the name mapper +const nameMapper = new AnimationNameMapper() + +/** + * Legacy animation names (backward compatibility) + * @constant + */ +export const LegacyAnimations = { + // Wait state animations + WAIT_IDLE_LOOP: 'wait_idle_L', + WAIT_PICK_NOSE_QUIRK: 'wait_pickNose_Q', + WAIT_STRETCH_QUIRK: 'wait_stretch_Q', + WAIT_YAWN_QUIRK: 'wait_yawn_Q', + + // React state animations - neutral + REACT_IDLE_LOOP: 'react_idle_L', + REACT_ACKNOWLEDGE_TRANSITION: 'react_acknowledge_T', + REACT_NOD_TRANSITION: 'react_nod_T', + REACT_LISTENING_LOOP: 'react_listening_L', + + // React state animations - angry + REACT_ANGRY_IDLE_LOOP: 'react_angry_L', + REACT_ANGRY_FROWN_TRANSITION: 'react_an2frown_T', + REACT_ANGRY_GRUMBLE_QUIRK: 'react_an2grumble_Q', + REACT_ANGRY_TO_TYPE_TRANSITION: 'react_an2type_T', + + // React state animations - happy + REACT_HAPPY_IDLE_LOOP: 'react_happy_L', + REACT_HAPPY_SMILE_TRANSITION: 'react_hp2smile_T', + REACT_HAPPY_BOUNCE_QUIRK: 'react_hp2bounce_Q', + REACT_HAPPY_TO_TYPE_TRANSITION: 'react_hp2type_T', + + // React state animations - sad + REACT_SAD_IDLE_LOOP: 'react_sad_L', + REACT_SAD_SIGH_TRANSITION: 'react_sd2sigh_T', + REACT_SAD_SLUMP_QUIRK: 'react_sd2slump_Q', + REACT_SAD_TO_TYPE_TRANSITION: 'react_sd2type_T', + + // React state animations - shocked + REACT_SHOCKED_IDLE_LOOP: 'react_shocked_L', + REACT_SHOCKED_GASP_TRANSITION: 'react_sh2gasp_T', + REACT_SHOCKED_JUMP_QUIRK: 'react_sh2jump_Q', + REACT_SHOCKED_TO_TYPE_TRANSITION: 'react_sh2type_T', + + // Type state animations + TYPE_IDLE_LOOP: 'type_idle_L', + TYPE_FAST_LOOP: 'type_fast_L', + TYPE_SLOW_LOOP: 'type_slow_L', + TYPE_THINKING_LOOP: 'type_thinking_L', + TYPE_TO_WAIT_TRANSITION: 'type2wait_T', + + // Sleep state animations + SLEEP_LIGHT_LOOP: 'sleep_light_L', + SLEEP_DEEP_LOOP: 'sleep_deep_L', + SLEEP_DREAM_QUIRK: 'sleep_dream_Q', + SLEEP_WAKE_UP_TRANSITION: 'sleep2wake_T' +} + +/** + * Artist-friendly animation names (Blender workflow) + * @constant + */ +export const ArtistAnimations = { + // Wait state animations + WAIT_IDLE: 'Owen_WaitIdle', + WAIT_PICK_NOSE: 'Owen_PickNose', + WAIT_STRETCH: 'Owen_Stretch', + WAIT_YAWN: 'Owen_Yawn', + + // React state animations - neutral + REACT_IDLE: 'Owen_ReactIdle', + REACT_ACKNOWLEDGE: 'Owen_ReactAcknowledge', + REACT_NOD: 'Owen_ReactNod', + REACT_LISTENING: 'Owen_ReactListening', + + // React state animations - angry + REACT_ANGRY_IDLE: 'Owen_ReactAngryIdle', + REACT_ANGRY_FROWN: 'Owen_ReactAngryFrown', + REACT_ANGRY_GRUMBLE: 'Owen_ReactAngryGrumble', + REACT_ANGRY_TO_TYPE: 'Owen_ReactAngryToType', + + // React state animations - happy + REACT_HAPPY_IDLE: 'Owen_ReactHappyIdle', + REACT_HAPPY_SMILE: 'Owen_ReactHappySmile', + REACT_HAPPY_BOUNCE: 'Owen_ReactHappyBounce', + REACT_HAPPY_TO_TYPE: 'Owen_ReactHappyToType', + + // React state animations - sad + REACT_SAD_IDLE: 'Owen_ReactSadIdle', + REACT_SAD_SIGH: 'Owen_ReactSadSigh', + REACT_SAD_SLUMP: 'Owen_ReactSadSlump', + REACT_SAD_TO_TYPE: 'Owen_ReactSadToType', + + // React state animations - shocked + REACT_SHOCKED_IDLE: 'Owen_ReactShockedIdle', + REACT_SHOCKED_GASP: 'Owen_ReactShockedGasp', + REACT_SHOCKED_JUMP: 'Owen_ReactShockedJump', + REACT_SHOCKED_TO_TYPE: 'Owen_ReactShockedToType', + + // Type state animations + TYPE_IDLE: 'Owen_TypeIdle', + TYPE_FAST: 'Owen_TypeFast', + TYPE_SLOW: 'Owen_TypeSlow', + TYPE_THINKING: 'Owen_TypeThinking', + TYPE_TO_WAIT: 'Owen_TypeToWait', + + // Sleep state animations + SLEEP_LIGHT: 'Owen_SleepLight', + SLEEP_DEEP: 'Owen_SleepDeep', + SLEEP_DREAM: 'Owen_SleepDream', + SLEEP_WAKE_UP: 'Owen_SleepWakeUp' +} + +/** + * Hierarchical animation names (organized structure) + * @constant + */ +export const HierarchicalAnimations = { + // Wait state animations + WAIT_IDLE: 'owen.state.wait.idle.loop', + WAIT_PICK_NOSE: 'owen.quirk.wait.picknose', + WAIT_STRETCH: 'owen.quirk.wait.stretch', + WAIT_YAWN: 'owen.quirk.wait.yawn', + + // React state animations - neutral + REACT_IDLE: 'owen.state.react.idle.loop', + REACT_ACKNOWLEDGE: 'owen.state.react.acknowledge.transition', + REACT_NOD: 'owen.state.react.nod.transition', + REACT_LISTENING: 'owen.state.react.listening.loop', + + // React state animations - angry + REACT_ANGRY_IDLE: 'owen.state.react.angry.idle.loop', + REACT_ANGRY_FROWN: 'owen.state.react.angry.frown.transition', + REACT_ANGRY_GRUMBLE: 'owen.quirk.react.angry.grumble', + REACT_ANGRY_TO_TYPE: 'owen.state.react.angry.totype.transition', + + // React state animations - happy + REACT_HAPPY_IDLE: 'owen.state.react.happy.idle.loop', + REACT_HAPPY_SMILE: 'owen.state.react.happy.smile.transition', + REACT_HAPPY_BOUNCE: 'owen.quirk.react.happy.bounce', + REACT_HAPPY_TO_TYPE: 'owen.state.react.happy.totype.transition', + + // React state animations - sad + REACT_SAD_IDLE: 'owen.state.react.sad.idle.loop', + REACT_SAD_SIGH: 'owen.state.react.sad.sigh.transition', + REACT_SAD_SLUMP: 'owen.quirk.react.sad.slump', + REACT_SAD_TO_TYPE: 'owen.state.react.sad.totype.transition', + + // React state animations - shocked + REACT_SHOCKED_IDLE: 'owen.state.react.shocked.idle.loop', + REACT_SHOCKED_GASP: 'owen.state.react.shocked.gasp.transition', + REACT_SHOCKED_JUMP: 'owen.quirk.react.shocked.jump', + REACT_SHOCKED_TO_TYPE: 'owen.state.react.shocked.totype.transition', + + // Type state animations + TYPE_IDLE: 'owen.state.type.idle.loop', + TYPE_FAST: 'owen.state.type.fast.loop', + TYPE_SLOW: 'owen.state.type.slow.loop', + TYPE_THINKING: 'owen.state.type.thinking.loop', + TYPE_TO_WAIT: 'owen.state.type.towait.transition', + + // Sleep state animations + SLEEP_LIGHT: 'owen.state.sleep.light.loop', + SLEEP_DEEP: 'owen.state.sleep.deep.loop', + SLEEP_DREAM: 'owen.quirk.sleep.dream', + SLEEP_WAKE_UP: 'owen.state.sleep.wakeup.transition' +} + +/** + * Semantic animation names (readable camelCase) + * @constant + */ +export const SemanticAnimations = { + // Wait state animations + WAIT_IDLE: 'OwenWaitIdleLoop', + WAIT_PICK_NOSE: 'OwenQuirkPickNose', + WAIT_STRETCH: 'OwenQuirkStretch', + WAIT_YAWN: 'OwenQuirkYawn', + + // React state animations - neutral + REACT_IDLE: 'OwenReactIdleLoop', + REACT_ACKNOWLEDGE: 'OwenReactAcknowledgeTransition', + REACT_NOD: 'OwenReactNodTransition', + REACT_LISTENING: 'OwenReactListeningLoop', + + // React state animations - angry + REACT_ANGRY_IDLE: 'OwenReactAngryIdleLoop', + REACT_ANGRY_FROWN: 'OwenReactAngryFrownTransition', + REACT_ANGRY_GRUMBLE: 'OwenQuirkAngryGrumble', + REACT_ANGRY_TO_TYPE: 'OwenReactAngryToTypeTransition', + + // React state animations - happy + REACT_HAPPY_IDLE: 'OwenReactHappyIdleLoop', + REACT_HAPPY_SMILE: 'OwenReactHappySmileTransition', + REACT_HAPPY_BOUNCE: 'OwenQuirkHappyBounce', + REACT_HAPPY_TO_TYPE: 'OwenReactHappyToTypeTransition', + + // React state animations - sad + REACT_SAD_IDLE: 'OwenReactSadIdleLoop', + REACT_SAD_SIGH: 'OwenReactSadSighTransition', + REACT_SAD_SLUMP: 'OwenQuirkSadSlump', + REACT_SAD_TO_TYPE: 'OwenReactSadToTypeTransition', + + // React state animations - shocked + REACT_SHOCKED_IDLE: 'OwenReactShockedIdleLoop', + REACT_SHOCKED_GASP: 'OwenReactShockedGaspTransition', + REACT_SHOCKED_JUMP: 'OwenQuirkShockedJump', + REACT_SHOCKED_TO_TYPE: 'OwenReactShockedToTypeTransition', + + // Type state animations + TYPE_IDLE: 'OwenTypeIdleLoop', + TYPE_FAST: 'OwenTypeFastLoop', + TYPE_SLOW: 'OwenTypeSlowLoop', + TYPE_THINKING: 'OwenTypeThinkingLoop', + TYPE_TO_WAIT: 'OwenTypeToWaitTransition', + + // Sleep state animations + SLEEP_LIGHT: 'OwenSleepLightLoop', + SLEEP_DEEP: 'OwenSleepDeepLoop', + SLEEP_DREAM: 'OwenQuirkSleepDream', + SLEEP_WAKE_UP: 'OwenSleepWakeUpTransition' +} + +/** + * Animation naming schemes enumeration + * @constant + */ +export const NamingSchemes = { + LEGACY: 'legacy', + ARTIST: 'artist', + HIERARCHICAL: 'hierarchical', + SEMANTIC: 'semantic' +} + +/** + * Convert animation name between different schemes + * @param {string} name - The source animation name + * @param {string} targetScheme - The target naming scheme + * @returns {string} The converted animation name + */ +export function convertAnimationName (name, targetScheme) { + return nameMapper.convert(name, targetScheme) +} + +/** + * Get all naming scheme variants for an animation + * @param {string} name - The source animation name + * @returns {Object} Object with all scheme variants + */ +export function getAllAnimationNames (name) { + return nameMapper.getAllNames(name) +} + +/** + * Validate an animation name + * @param {string} name - The animation name to validate + * @returns {Object} Validation result + */ +export function validateAnimationName (name) { + return nameMapper.validateAnimationName(name) +} + +/** + * Get animations by state and emotion + * @param {string} state - The state name + * @param {string} emotion - The emotion name (optional) + * @param {string} scheme - The naming scheme to return (default: 'semantic') + * @returns {string[]} Array of animation names + */ +export function getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') { + const animations = nameMapper.getAnimationsByFilter({ state, emotion }) + return animations.map(anim => anim[scheme] || anim.semantic) +} diff --git a/src/animation/AnimationNameMapper.js b/src/animation/AnimationNameMapper.js new file mode 100644 index 0000000..2585a34 --- /dev/null +++ b/src/animation/AnimationNameMapper.js @@ -0,0 +1,599 @@ +/** + * @fileoverview Multi-scheme animation name mapper for Owen Animation System + * @module animation/AnimationNameMapper + */ + +/** + * Multi-scheme animation name mapper for Owen Animation System + * Supports legacy, artist-friendly, and hierarchical naming schemes + * @class + */ +export class AnimationNameMapper { + constructor () { + // Mapping between different naming schemes + this.schemeMappings = new Map() + this.reverseMappings = new Map() + this.patterns = new Map() + + this.initializeMappings() + } + + /** + * Initialize all naming scheme mappings and patterns + * @private + */ + initializeMappings () { + // Core animation definitions with all naming scheme variants + const animations = [ + // Wait state animations + { + legacy: 'wait_idle_L', + artist: 'Owen_WaitIdle', + hierarchical: 'owen.state.wait.idle.loop', + semantic: 'OwenWaitIdleLoop', + state: 'wait', + emotion: '', + type: 'loop', + category: 'state' + }, + { + legacy: 'wait_pickNose_Q', + artist: 'Owen_PickNose', + hierarchical: 'owen.quirk.wait.picknose', + semantic: 'OwenQuirkPickNose', + state: 'wait', + emotion: '', + type: 'quirk', + category: 'quirk' + }, + { + legacy: 'wait_wave_Q', + artist: 'Owen_Wave', + hierarchical: 'owen.quirk.wait.wave', + semantic: 'OwenQuirkWave', + state: 'wait', + emotion: '', + type: 'quirk', + category: 'quirk' + }, + // React state animations + { + legacy: 'react_idle_L', + artist: 'Owen_ReactIdle', + hierarchical: 'owen.state.react.idle.loop', + semantic: 'OwenReactIdleLoop', + state: 'react', + emotion: '', + type: 'loop', + category: 'state' + }, + { + legacy: 'react_an_L', + artist: 'Owen_ReactAngry', + hierarchical: 'owen.state.react.emotion.angry.loop', + semantic: 'OwenReactAngryLoop', + state: 'react', + emotion: 'angry', + type: 'loop', + category: 'state' + }, + { + legacy: 'react_sh_L', + artist: 'Owen_ReactShocked', + hierarchical: 'owen.state.react.emotion.shocked.loop', + semantic: 'OwenReactShockedLoop', + state: 'react', + emotion: 'shocked', + type: 'loop', + category: 'state' + }, + { + legacy: 'react_ha_L', + artist: 'Owen_ReactHappy', + hierarchical: 'owen.state.react.emotion.happy.loop', + semantic: 'OwenReactHappyLoop', + state: 'react', + emotion: 'happy', + type: 'loop', + category: 'state' + }, + { + legacy: 'react_sa_L', + artist: 'Owen_ReactSad', + hierarchical: 'owen.state.react.emotion.sad.loop', + semantic: 'OwenReactSadLoop', + state: 'react', + emotion: 'sad', + type: 'loop', + category: 'state' + }, + // Type state animations + { + legacy: 'type_idle_L', + artist: 'Owen_TypeIdle', + hierarchical: 'owen.state.type.idle.loop', + semantic: 'OwenTypeIdleLoop', + state: 'type', + emotion: '', + type: 'loop', + category: 'state' + }, + { + legacy: 'type_an_L', + artist: 'Owen_TypeAngry', + hierarchical: 'owen.state.type.emotion.angry.loop', + semantic: 'OwenTypeAngryLoop', + state: 'type', + emotion: 'angry', + type: 'loop', + category: 'state' + }, + { + legacy: 'type_sh_L', + artist: 'Owen_TypeShocked', + hierarchical: 'owen.state.type.emotion.shocked.loop', + semantic: 'OwenTypeShockedLoop', + state: 'type', + emotion: 'shocked', + type: 'loop', + category: 'state' + }, + // Sleep state animations + { + legacy: 'sleep_idle_L', + artist: 'Owen_SleepIdle', + hierarchical: 'owen.state.sleep.idle.loop', + semantic: 'OwenSleepIdleLoop', + state: 'sleep', + emotion: '', + type: 'loop', + category: 'state' + }, + // Transition animations + { + legacy: 'wait_2react_T', + artist: 'Owen_WaitToReact', + hierarchical: 'owen.transition.wait.to.react', + semantic: 'OwenTransitionWaitToReact', + fromState: 'wait', + toState: 'react', + emotion: '', + type: 'transition', + category: 'transition' + }, + { + legacy: 'react_2type_T', + artist: 'Owen_ReactToType', + hierarchical: 'owen.transition.react.to.type', + semantic: 'OwenTransitionReactToType', + fromState: 'react', + toState: 'type', + emotion: '', + type: 'transition', + category: 'transition' + }, + { + legacy: 'react_an2type_T', + artist: 'Owen_ReactAngryToType', + hierarchical: 'owen.transition.react.to.type.emotion.angry', + semantic: 'OwenTransitionReactToTypeAngry', + fromState: 'react', + toState: 'type', + emotion: 'angry', + type: 'transition', + category: 'transition' + }, + { + legacy: 'type_2wait_T', + artist: 'Owen_TypeToWait', + hierarchical: 'owen.transition.type.to.wait', + semantic: 'OwenTransitionTypeToWait', + fromState: 'type', + toState: 'wait', + emotion: '', + type: 'transition', + category: 'transition' + }, + { + legacy: 'sleep_2wait_T', + artist: 'Owen_SleepToWait', + hierarchical: 'owen.transition.sleep.to.wait', + semantic: 'OwenTransitionSleepToWait', + fromState: 'sleep', + toState: 'wait', + emotion: '', + type: 'transition', + category: 'transition' + } + ] + + // Build bidirectional mappings + animations.forEach(anim => { + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + + schemes.forEach(scheme1 => { + schemes.forEach(scheme2 => { + if (scheme1 !== scheme2) { + this.schemeMappings.set(anim[scheme1], anim[scheme2]) + } + }) + + // Also map to animation definition + this.schemeMappings.set(anim[scheme1], anim) + }) + }) + + // Initialize pattern matchers for auto-detection + this.initializePatterns() + } + + /** + * Initialize pattern matchers for different naming schemes + * @private + */ + initializePatterns () { + // Pattern matchers for different naming schemes + this.patterns.set('legacy', [ + { + regex: /^(\w+)_(\w+)_([LTQ])$/, + extract: (match) => ({ + state: match[1], + action: match[2], + type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk' + }) + }, + { + regex: /^(\w+)_(\w{2})_([LTQ])$/, + extract: (match) => ({ + state: match[1], + emotion: this.mapEmotionCode(match[2]), + type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk' + }) + }, + { + regex: /^(\w+)_(\w{2})?2(\w+)_T$/, + extract: (match) => ({ + fromState: match[1], + emotion: match[2] ? this.mapEmotionCode(match[2]) : '', + toState: match[3], + type: 'transition' + }) + }, + { + regex: /^(\w+)_2(\w+)_T$/, + extract: (match) => ({ + fromState: match[1], + toState: match[2], + type: 'transition' + }) + } + ]) + + this.patterns.set('artist', [ + { + regex: /^Owen_(\w+)$/, + extract: (match) => ({ + action: match[1], + scheme: 'artist' + }) + }, + { + regex: /^Owen_(\w+)To(\w+)$/, + extract: (match) => ({ + fromState: match[1].toLowerCase(), + toState: match[2].toLowerCase(), + type: 'transition' + }) + }, + { + regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)$/, + extract: (match) => ({ + state: match[1].toLowerCase(), + emotion: match[2].toLowerCase(), + type: 'loop' + }) + }, + { + regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)To(\w+)$/, + extract: (match) => ({ + fromState: match[1].toLowerCase(), + emotion: match[2].toLowerCase(), + toState: match[3].toLowerCase(), + type: 'transition' + }) + } + ]) + + this.patterns.set('hierarchical', [ + { + regex: /^owen\.(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?(?:\.(\w+))?$/, + extract: (match) => ({ + category: match[1], + subcategory: match[2], + action: match[3], + modifier: match[4], + type: match[5] || match[4] + }) + } + ]) + + this.patterns.set('semantic', [ + { + regex: /^Owen(\w+)(\w+)(\w+)$/, + extract: (match) => ({ + category: match[1].toLowerCase(), + action: match[2].toLowerCase(), + type: match[3].toLowerCase() + }) + } + ]) + } + + /** + * Map emotion codes to full names + * @private + * @param {string} code - Emotion code + * @returns {string} Full emotion name + */ + mapEmotionCode (code) { + const emotionMap = { + an: 'angry', + sh: 'shocked', + ha: 'happy', + sa: 'sad', + '': 'neutral' + } + return emotionMap[code] || code + } + + /** + * Convert any animation name to any other scheme + * @param {string} fromName - Source animation name + * @param {string} targetScheme - Target naming scheme ('legacy', 'artist', 'hierarchical', 'semantic') + * @returns {string} Converted animation name + */ + convert (fromName, targetScheme = 'hierarchical') { + // Direct lookup first + const directMapping = this.schemeMappings.get(fromName) + if (directMapping && typeof directMapping === 'object') { + return directMapping[targetScheme] || fromName + } + + // Pattern-based conversion + const detected = this.detectScheme(fromName) + if (detected) { + return this.generateName(detected.info, targetScheme) + } + + console.warn(`Could not convert animation name: ${fromName}`) + return fromName + } + + /** + * Detect which naming scheme is being used + * @param {string} name - Animation name to analyze + * @returns {Object|null} Detection result with scheme and extracted info + */ + detectScheme (name) { + for (const [scheme, patterns] of this.patterns) { + for (const pattern of patterns) { + const match = name.match(pattern.regex) + if (match) { + return { + scheme, + info: pattern.extract(match), + originalName: name + } + } + } + } + return null + } + + /** + * Generate animation name in target scheme + * @private + * @param {Object} info - Animation information + * @param {string} targetScheme - Target naming scheme + * @returns {string} Generated animation name + */ + generateName (info, targetScheme) { + switch (targetScheme) { + case 'legacy': + return this.generateLegacyName(info) + case 'artist': + return this.generateArtistName(info) + case 'hierarchical': + return this.generateHierarchicalName(info) + case 'semantic': + return this.generateSemanticName(info) + default: + return null + } + } + + /** + * Generate legacy format name + * @private + * @param {Object} info - Animation information + * @returns {string} Legacy format name + */ + generateLegacyName (info) { + const typeMap = { loop: 'L', transition: 'T', quirk: 'Q' } + const emotionMap = { angry: 'an', shocked: 'sh', happy: 'ha', sad: 'sa' } + + if (info.type === 'transition' && info.fromState && info.toState) { + const emotionPart = info.emotion ? emotionMap[info.emotion] || '' : '' + return emotionPart + ? `${info.fromState}_${emotionPart}2${info.toState}_T` + : `${info.fromState}_2${info.toState}_T` + } + + const state = info.state || info.fromState || 'wait' + const action = info.action || (info.emotion ? emotionMap[info.emotion] : 'idle') + const type = typeMap[info.type] || 'L' + + return `${state}_${action}_${type}` + } + + /** + * Generate artist-friendly format name + * @private + * @param {Object} info - Animation information + * @returns {string} Artist format name + */ + generateArtistName (info) { + const parts = ['Owen'] + + if (info.type === 'transition') { + const from = this.capitalize(info.fromState || info.state) + const to = this.capitalize(info.toState) + if (info.emotion) { + parts.push(`${from}${this.capitalize(info.emotion)}To${to}`) + } else { + parts.push(`${from}To${to}`) + } + } else { + if (info.state) parts.push(this.capitalize(info.state)) + if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action)) + if (info.emotion) parts.push(this.capitalize(info.emotion)) + } + + return parts.join('_') + } + + /** + * Generate hierarchical format name + * @private + * @param {Object} info - Animation information + * @returns {string} Hierarchical format name + */ + generateHierarchicalName (info) { + const parts = ['owen'] + + if (info.category) { + parts.push(info.category) + } else if (info.type === 'transition') { + parts.push('transition') + } else if (info.type === 'quirk') { + parts.push('quirk') + } else { + parts.push('state') + } + + if (info.fromState && info.toState) { + // Transition + parts.push(info.fromState, 'to', info.toState) + } else if (info.state) { + parts.push(info.state) + } + + if (info.action && info.action !== 'idle') parts.push(info.action) + if (info.emotion) parts.push('emotion', info.emotion) + if (info.type) parts.push(info.type) + + return parts.join('.') + } + + /** + * Generate semantic format name + * @private + * @param {Object} info - Animation information + * @returns {string} Semantic format name + */ + generateSemanticName (info) { + const parts = ['Owen'] + + if (info.type === 'transition') { + parts.push('Transition') + if (info.fromState) parts.push(this.capitalize(info.fromState)) + parts.push('To') + if (info.toState) parts.push(this.capitalize(info.toState)) + if (info.emotion) parts.push(this.capitalize(info.emotion)) + } else { + if (info.type === 'quirk') parts.push('Quirk') + if (info.state) parts.push(this.capitalize(info.state)) + if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action)) + if (info.emotion) parts.push(this.capitalize(info.emotion)) + if (info.type && info.type !== 'quirk') parts.push(this.capitalize(info.type)) + } + + return parts.join('') + } + + /** + * Capitalize first letter of string + * @private + * @param {string} str - String to capitalize + * @returns {string} Capitalized string + */ + capitalize (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + } + + /** + * Get all possible names for an animation + * @param {string} animationName - Source animation name + * @returns {Object} Object with all naming scheme variants + */ + getAllNames (animationName) { + const schemes = ['legacy', 'artist', 'hierarchical', 'semantic'] + const names = {} + + schemes.forEach(scheme => { + names[scheme] = this.convert(animationName, scheme) + }) + + return names + } + + /** + * Batch convert multiple animations + * @param {string[]} animations - Array of animation names + * @param {string} targetScheme - Target naming scheme + * @returns {Object} Mapping of original names to converted names + */ + convertBatch (animations, targetScheme) { + const converted = {} + animations.forEach(name => { + converted[name] = this.convert(name, targetScheme) + }) + return converted + } + + /** + * Validate animation name format + * @param {string} name - Animation name to validate + * @returns {Object} Validation result with issues and suggestions + */ + validateAnimationName (name) { + const issues = [] + const suggestions = [] + + // Check for common issues + if (name.includes(' ')) { + issues.push(`āŒ "${name}" contains spaces - may cause issues`) + suggestions.push(`šŸ’” Suggestion: "${name.replace(/ /g, '_')}"`) + } + + if (!/^[a-zA-Z0-9._-]+$/.test(name)) { + issues.push(`āŒ "${name}" contains invalid characters`) + suggestions.push('šŸ’” Use only letters, numbers, dots, underscores, and hyphens') + } + + if (name.length > 50) { + issues.push(`āš ļø "${name}" is very long (${name.length} chars)`) + suggestions.push('šŸ’” Consider shortening the name') + } + + const detected = this.detectScheme(name) + if (!detected) { + issues.push(`āš ļø "${name}" doesn't match any known naming pattern`) + suggestions.push('šŸ’” Consider using one of: legacy, artist, hierarchical, or semantic format') + } else { + suggestions.push(`āœ… Detected as ${detected.scheme} scheme`) + } + + return { issues, suggestions, detected } + } +} diff --git a/src/constants.js b/src/constants.js index a8eed72..011ffe6 100644 --- a/src/constants.js +++ b/src/constants.js @@ -32,13 +32,13 @@ export const ClipTypes = { */ export const States = { /** Waiting/idle state */ - WAITING: 'wait', + WAITING: 'wait', /** Reacting to input state */ - REACTING: 'react', + REACTING: 'react', /** Typing response state */ - TYPING: 'type', + TYPING: 'type', /** Sleep/inactive state */ - SLEEPING: 'sleep' + SLEEPING: 'sleep' } /** diff --git a/src/core/OwenAnimationContext.js b/src/core/OwenAnimationContext.js index cb2a530..0f5a12c 100644 --- a/src/core/OwenAnimationContext.js +++ b/src/core/OwenAnimationContext.js @@ -4,6 +4,7 @@ */ import { States, Emotions, Config } from '../constants.js' +import { AnimationNameMapper } from '../animation/AnimationNameMapper.js' /** * Main controller for the Owen animation system @@ -43,6 +44,12 @@ export class OwenAnimationContext { */ this.stateFactory = stateFactory + /** + * Multi-scheme animation name mapper + * @type {AnimationNameMapper} + */ + this.nameMapper = new AnimationNameMapper() + /** * Map of animation clips by name * @type {Map} @@ -59,7 +66,7 @@ export class OwenAnimationContext { * Current active state * @type {string} */ - this.currentState = States.WAITING + this.currentState = States.WAITING /** * Current active state handler @@ -105,7 +112,7 @@ export class OwenAnimationContext { this.initializeStates() // Start in wait state - await this.transitionTo(States.WAITING) + await this.transitionTo(States.WAITING) this.initialized = true console.log('Owen Animation System initialized') @@ -167,8 +174,8 @@ export class OwenAnimationContext { this.onUserActivity() // If sleeping, wake up first - if (this.currentState === States.SLEEPING) { - await this.transitionTo(States.REACTING) + if (this.currentState === States.SLEEPING) { + await this.transitionTo(States.REACTING) } // Let current state handle the message @@ -177,10 +184,10 @@ export class OwenAnimationContext { } // Transition to appropriate next state based on current state - if (this.currentState === States.WAITING) { - await this.transitionTo(States.REACTING); - } else if (this.currentState === States.REACTING) { - await this.transitionTo(States.TYPING) + if (this.currentState === States.WAITING) { + await this.transitionTo(States.REACTING) + } else if (this.currentState === States.REACTING) { + await this.transitionTo(States.TYPING) } } @@ -192,8 +199,8 @@ export class OwenAnimationContext { this.resetActivityTimer() // Wake up if sleeping - if (this.currentState === States.SLEEPING) { - this.transitionTo(States.WAITING) + if (this.currentState === States.SLEEPING) { + this.transitionTo(States.WAITING) } } @@ -213,7 +220,7 @@ export class OwenAnimationContext { */ async handleInactivity () { console.log('Inactivity detected, transitioning to sleep') - await this.transitionTo(States.SLEEPING) + await this.transitionTo(States.SLEEPING) } /** @@ -234,18 +241,38 @@ export class OwenAnimationContext { // Update inactivity timer this.inactivityTimer += deltaTime - if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) { + if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) { this.handleInactivity() } } /** - * Get an animation clip by name - * @param {string} name - The animation clip name + * Get an animation clip by name (supports all naming schemes) + * @param {string} name - The animation clip name in any supported scheme * @returns {AnimationClip|undefined} The animation clip or undefined if not found */ getClip (name) { - return this.clips.get(name) + // First try direct lookup + let clip = this.clips.get(name) + if (clip) return clip + + // Try to find clip using name mapper + try { + const allNames = this.nameMapper.getAllNames(name) + + // Try each possible name variant + for (const variant of Object.values(allNames)) { + clip = this.clips.get(variant) + if (clip) return clip + } + } catch (error) { + // If name mapping fails, continue with pattern search + console.debug(`Name mapping failed for "${name}":`, error.message) + } + + // Fall back to pattern matching for legacy compatibility + const exactMatches = this.getClipsByPattern(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) + return exactMatches.length > 0 ? exactMatches[0] : undefined } /** @@ -266,6 +293,80 @@ export class OwenAnimationContext { return matches } + /** + * Get an animation clip by name in a specific naming scheme + * @param {string} name - The animation name + * @param {string} [targetScheme] - Target scheme: 'legacy', 'artist', 'hierarchical', 'semantic' + * @returns {AnimationClip|undefined} The animation clip or undefined if not found + */ + getClipByScheme (name, targetScheme) { + try { + if (targetScheme) { + const convertedName = this.nameMapper.convert(name, targetScheme) + return this.clips.get(convertedName) + } else { + return this.getClip(name) + } + } catch (error) { + console.debug(`Scheme conversion failed for "${name}" to "${targetScheme}":`, error.message) + return undefined + } + } + + /** + * Get all naming scheme variants for an animation + * @param {string} name - The animation name in any scheme + * @returns {Object} Object with all scheme variants: {legacy, artist, hierarchical, semantic} + */ + getAnimationNames (name) { + try { + return this.nameMapper.getAllNames(name) + } catch (error) { + console.warn(`Could not get animation name variants for "${name}":`, error.message) + return { + legacy: name, + artist: name, + hierarchical: name, + semantic: name + } + } + } + + /** + * Validate an animation name and get suggestions if invalid + * @param {string} name - The animation name to validate + * @returns {Object} Validation result with isValid, scheme, error, and suggestions + */ + validateAnimationName (name) { + try { + return this.nameMapper.validateAnimationName(name) + } catch (error) { + return { + isValid: false, + scheme: 'unknown', + error: error.message, + suggestions: [] + } + } + } + + /** + * Get available animations by state and emotion + * @param {string} state - The state name (wait, react, type, sleep) + * @param {string} [emotion] - The emotion name (angry, happy, sad, shocked, neutral) + * @param {string} [scheme='semantic'] - The naming scheme to return + * @returns {string[]} Array of animation names in the specified scheme + */ + getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') { + try { + const animations = this.nameMapper.getAnimationsByFilter({ state, emotion }) + return animations.map(anim => anim[scheme] || anim.semantic) + } catch (error) { + console.warn(`Could not filter animations by state "${state}" and emotion "${emotion}":`, error.message) + return [] + } + } + /** * Get the current state name * @returns {string} The current state name diff --git a/src/index.js b/src/index.js index 83ffcb8..84990f6 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,20 @@ export { OwenAnimationContext } from './core/OwenAnimationContext.js' // Animation system exports export { AnimationClip, AnimationClipFactory } from './animation/AnimationClip.js' +// Multi-scheme animation naming exports +export { AnimationNameMapper } from './animation/AnimationNameMapper.js' +export { + LegacyAnimations, + ArtistAnimations, + HierarchicalAnimations, + SemanticAnimations, + NamingSchemes, + convertAnimationName, + getAllAnimationNames, + validateAnimationName, + getAnimationsByStateAndEmotion +} from './animation/AnimationConstants.js' + // Loader exports export { AnimationLoader, GLTFAnimationLoader } from './loaders/AnimationLoader.js' diff --git a/src/states/ReactStateHandler.js b/src/states/ReactStateHandler.js index 6289d7a..f024aad 100644 --- a/src/states/ReactStateHandler.js +++ b/src/states/ReactStateHandler.js @@ -17,7 +17,7 @@ export class ReactStateHandler extends StateHandler { * @param {OwenAnimationContext} context - The animation context */ constructor (context) { - super(States.REACTING, context) + super(States.REACTING, context) /** * Current emotional state @@ -33,7 +33,7 @@ export class ReactStateHandler extends StateHandler { * @returns {Promise} */ async enter (_fromState = null, emotion = Emotions.NEUTRAL) { - console.log(`Entering REACTING state with emotion: ${emotion}`) + console.log(`Entering REACTING state with emotion: ${emotion}`) this.emotion = emotion // Play appropriate reaction @@ -51,7 +51,7 @@ export class ReactStateHandler extends StateHandler { * @returns {Promise} */ async exit (toState = null, emotion = Emotions.NEUTRAL) { - console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`) + console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`) if (this.currentClip) { await this.stopCurrentClip() @@ -154,6 +154,6 @@ export class ReactStateHandler extends StateHandler { * @returns {string[]} Array of available state transitions */ getAvailableTransitions () { - return [ States.TYPING, States.WAITING ] + return [States.TYPING, States.WAITING] } } diff --git a/src/states/SleepStateHandler.js b/src/states/SleepStateHandler.js index 0c0818a..42e30ec 100644 --- a/src/states/SleepStateHandler.js +++ b/src/states/SleepStateHandler.js @@ -17,7 +17,7 @@ export class SleepStateHandler extends StateHandler { * @param {OwenAnimationContext} context - The animation context */ constructor (context) { - super(States.SLEEPING, context) + super(States.SLEEPING, context) /** * Sleep animation clip @@ -39,7 +39,7 @@ export class SleepStateHandler extends StateHandler { * @returns {Promise} */ async enter (fromState = null, _emotion = Emotions.NEUTRAL) { - console.log(`Entering SLEEPING state from ${fromState}`) + console.log(`Entering SLEEPING state from ${fromState}`) // Play sleep transition if available const sleepTransition = this.context.getClip('wait_2sleep_T') @@ -65,7 +65,7 @@ export class SleepStateHandler extends StateHandler { * @returns {Promise} */ async exit (toState = null, _emotion = Emotions.NEUTRAL) { - console.log(`Exiting SLEEPING state to ${toState}`) + console.log(`Exiting SLEEPING state to ${toState}`) this.isDeepSleep = false if (this.currentClip) { @@ -107,8 +107,8 @@ export class SleepStateHandler extends StateHandler { // Any message should wake up the character if (this.isDeepSleep) { console.log('Waking up due to user message') - // This will trigger a state transition to REACTING - await this.context.transitionTo(States.REACTING) + // This will trigger a state transition to REACTING + await this.context.transitionTo(States.REACTING) } } @@ -117,7 +117,7 @@ export class SleepStateHandler extends StateHandler { * @returns {string[]} Array of available state transitions */ getAvailableTransitions () { - return [ States.WAITING, States.REACTING ] + return [States.WAITING, States.REACTING] } /** @@ -134,7 +134,7 @@ export class SleepStateHandler extends StateHandler { */ async wakeUp () { if (this.isDeepSleep) { - await this.context.transitionTo(States.WAITING) + await this.context.transitionTo(States.WAITING) } } } diff --git a/src/states/StateFactory.js b/src/states/StateFactory.js index 669b431..ab8c778 100644 --- a/src/states/StateFactory.js +++ b/src/states/StateFactory.js @@ -26,10 +26,10 @@ export class StateFactory { this.stateHandlers = new Map() // Register default state handlers - this.registerStateHandler(States.WAITING, WaitStateHandler); - this.registerStateHandler(States.REACTING, ReactStateHandler); - this.registerStateHandler(States.TYPING, TypeStateHandler); - this.registerStateHandler(States.SLEEPING, SleepStateHandler) + this.registerStateHandler(States.WAITING, WaitStateHandler) + this.registerStateHandler(States.REACTING, ReactStateHandler) + this.registerStateHandler(States.TYPING, TypeStateHandler) + this.registerStateHandler(States.SLEEPING, SleepStateHandler) } /** diff --git a/src/states/TypeStateHandler.js b/src/states/TypeStateHandler.js index f85e61e..1175761 100644 --- a/src/states/TypeStateHandler.js +++ b/src/states/TypeStateHandler.js @@ -17,7 +17,7 @@ export class TypeStateHandler extends StateHandler { * @param {OwenAnimationContext} context - The animation context */ constructor (context) { - super(States.TYPING, context) + super(States.TYPING, context) /** * Current emotional state @@ -39,7 +39,7 @@ export class TypeStateHandler extends StateHandler { * @returns {Promise} */ async enter (_fromState = null, emotion = Emotions.NEUTRAL) { - console.log(`Entering TYPING state with emotion: ${emotion}`) + console.log(`Entering TYPING state with emotion: ${emotion}`) this.emotion = emotion this.isTyping = true @@ -63,7 +63,7 @@ export class TypeStateHandler extends StateHandler { * @returns {Promise} */ async exit (toState = null, _emotion = Emotions.NEUTRAL) { - console.log(`Exiting TYPING state to ${toState}`) + console.log(`Exiting TYPING state to ${toState}`) this.isTyping = false if (this.currentClip) { @@ -106,7 +106,7 @@ export class TypeStateHandler extends StateHandler { * @returns {string[]} Array of available state transitions */ getAvailableTransitions () { - return [ States.WAITING, States.REACTING ] + return [States.WAITING, States.REACTING] } /** diff --git a/src/states/WaitStateHandler.js b/src/states/WaitStateHandler.js index 84a5aac..91623ce 100644 --- a/src/states/WaitStateHandler.js +++ b/src/states/WaitStateHandler.js @@ -17,7 +17,7 @@ export class WaitStateHandler extends StateHandler { * @param {OwenAnimationContext} context - The animation context */ constructor (context) { - super(States.WAITING, context) + super(States.WAITING, context) /** * The main idle animation clip @@ -51,7 +51,7 @@ export class WaitStateHandler extends StateHandler { * @returns {Promise} */ async enter (fromState = null, _emotion = Emotions.NEUTRAL) { - console.log(`Entering WAITING state from ${fromState}`) + console.log(`Entering WAITING state from ${fromState}`) // Play idle loop this.idleClip = this.context.getClip('wait_idle_L') @@ -72,7 +72,7 @@ export class WaitStateHandler extends StateHandler { * @returns {Promise} */ async exit (toState = null, _emotion = Emotions.NEUTRAL) { - console.log(`Exiting WAITING state to ${toState}`) + console.log(`Exiting WAITING state to ${toState}`) if (this.currentClip) { await this.stopCurrentClip() @@ -133,6 +133,6 @@ export class WaitStateHandler extends StateHandler { * @returns {string[]} Array of available state transitions */ getAvailableTransitions () { - return [ States.REACTING, States.SLEEPING ] + return [States.REACTING, States.SLEEPING] } } diff --git a/tests/demo.spec.js b/tests/demo.spec.js new file mode 100644 index 0000000..c512089 --- /dev/null +++ b/tests/demo.spec.js @@ -0,0 +1,158 @@ +import { test, expect } from '@playwright/test' + +test.describe('Owen Animation System Demo', () => { + test.beforeEach(async ({ page }) => { + // Navigate to the demo page before each test + await page.goto('/') + }) + + test('should load the main demo page', async ({ page }) => { + // Check that the page title is correct + await expect(page).toHaveTitle(/Owen Animation System/) + + // Check that the main heading is present + await expect(page.locator('h1')).toContainText('Owen Animation System') + + // Check that the demo content is loaded + await expect(page.locator('.demo-content')).toBeVisible() + }) + + test('should display animation name converter', async ({ page }) => { + // Check that the converter section is present + await expect(page.locator('.converter-section')).toBeVisible() + + // Check input fields + await expect(page.locator('#animationName')).toBeVisible() + await expect(page.locator('#sourceScheme')).toBeVisible() + await expect(page.locator('#targetScheme')).toBeVisible() + + // Check convert button + await expect(page.locator('#convertBtn')).toBeVisible() + }) + + test('should convert animation names', async ({ page }) => { + // Fill in the converter form + await page.fill('#animationName', 'char_walk_01') + await page.selectOption('#sourceScheme', 'artist') + await page.selectOption('#targetScheme', 'semantic') + + // Click convert button + await page.click('#convertBtn') + + // Check that result is displayed + await expect(page.locator('#conversionResult')).toBeVisible() + await expect(page.locator('#conversionResult')).toContainText('character.movement.walk') + }) + + test('should validate animation names', async ({ page }) => { + // Test with invalid animation name + await page.fill('#animationName', 'invalid-name-123!@#') + await page.selectOption('#sourceScheme', 'semantic') + await page.click('#convertBtn') + + // Should show validation error + await expect(page.locator('.error-message')).toBeVisible() + + // Test with valid animation name + await page.fill('#animationName', 'character.idle.basic') + await page.click('#convertBtn') + + // Should show success + await expect(page.locator('.success-message')).toBeVisible() + }) + + test('should show scheme comparison', async ({ page }) => { + // Check that scheme cards are present + await expect(page.locator('.scheme-card')).toHaveCount(4) + + // Check that each scheme is represented + await expect(page.locator('.scheme-card')).toContainText(['Legacy', 'Artist', 'Hierarchical', 'Semantic']) + }) + + test('should handle batch conversion', async ({ page }) => { + // Click on batch conversion tab + await page.click('[data-tab="batch"]') + + // Fill in batch input + const batchInput = [ + 'char_walk_01', + 'char_run_02', + 'prop_door_open' + ].join('\n') + + await page.fill('#batchInput', batchInput) + await page.selectOption('#batchSourceScheme', 'artist') + await page.selectOption('#batchTargetScheme', 'semantic') + + // Click convert batch button + await page.click('#convertBatchBtn') + + // Check that results are displayed + await expect(page.locator('#batchResults')).toBeVisible() + await expect(page.locator('.batch-result-item')).toHaveCount(3) + }) + + test('should export results', async ({ page }) => { + // Convert some animation names first + await page.fill('#animationName', 'char_walk_01') + await page.selectOption('#sourceScheme', 'artist') + await page.selectOption('#targetScheme', 'semantic') + await page.click('#convertBtn') + + // Wait for result + await expect(page.locator('#conversionResult')).toBeVisible() + + // Click export button + const downloadPromise = page.waitForEvent('download') + await page.click('#exportBtn') + const download = await downloadPromise + + // Check that file was downloaded + expect(download.suggestedFilename()).toMatch(/animation-conversions.*\.json/) + }) + + test('should be responsive', async ({ page }) => { + // Test mobile viewport + await page.setViewportSize({ width: 375, height: 667 }) + + // Check that mobile navigation is present + await expect(page.locator('.mobile-nav')).toBeVisible() + + // Check that converter still works + await page.fill('#animationName', 'test_animation') + await page.selectOption('#sourceScheme', 'legacy') + await page.selectOption('#targetScheme', 'semantic') + await page.click('#convertBtn') + + await expect(page.locator('#conversionResult')).toBeVisible() + }) + + test('should handle errors gracefully', async ({ page }) => { + // Test with empty input + await page.click('#convertBtn') + await expect(page.locator('.error-message')).toContainText('Animation name is required') + + // Test with same source and target scheme + await page.fill('#animationName', 'test_animation') + await page.selectOption('#sourceScheme', 'semantic') + await page.selectOption('#targetScheme', 'semantic') + await page.click('#convertBtn') + + await expect(page.locator('.warning-message')).toContainText('Source and target schemes are the same') + }) + + test('should show performance metrics', async ({ page }) => { + // Check that performance section is present + await expect(page.locator('.performance-section')).toBeVisible() + + // Convert some animations to generate metrics + await page.fill('#animationName', 'char_walk_01') + await page.selectOption('#sourceScheme', 'artist') + await page.selectOption('#targetScheme', 'semantic') + await page.click('#convertBtn') + + // Check that metrics are updated + await expect(page.locator('.conversion-time')).toContainText(/\d+ms/) + await expect(page.locator('.total-conversions')).toContainText(/\d+/) + }) +}) diff --git a/tests/pages.spec.js b/tests/pages.spec.js new file mode 100644 index 0000000..41d9cdc --- /dev/null +++ b/tests/pages.spec.js @@ -0,0 +1,177 @@ +import { test, expect } from '@playwright/test' + +test.describe('Examples Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/examples.html') + }) + + test('should load examples page', async ({ page }) => { + await expect(page).toHaveTitle(/Examples/) + await expect(page.locator('h1')).toContainText('Integration Examples') + }) + + test('should display framework examples', async ({ page }) => { + // Check that example cards are present + await expect(page.locator('.example-card')).toHaveCount.greaterThan(3) + + // Check specific framework examples + await expect(page.locator('.example-card')).toContainText(['React', 'Vue', 'Node.js']) + }) + + test('should copy code examples', async ({ page }) => { + // Click on a copy button + await page.click('.copy-button').first() + + // Check that button text changes to "Copied!" + await expect(page.locator('.copy-button').first()).toContainText('Copied!') + }) + + test('should filter examples', async ({ page }) => { + // Click on React filter + await page.click('[data-filter="react"]') + + // Check that only React examples are shown + await expect(page.locator('.example-card:visible')).toHaveCount(1) + await expect(page.locator('.example-card:visible')).toContainText('React') + }) +}) + +test.describe('Comparison Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/comparison.html') + }) + + test('should load comparison page', async ({ page }) => { + await expect(page).toHaveTitle(/Comparison/) + await expect(page.locator('h1')).toContainText('Naming Scheme Comparison') + }) + + test('should display scheme cards', async ({ page }) => { + await expect(page.locator('.scheme-card')).toHaveCount(4) + + // Check each scheme is present + const schemes = ['Legacy', 'Artist', 'Hierarchical', 'Semantic'] + for (const scheme of schemes) { + await expect(page.locator('.scheme-card')).toContainText(scheme) + } + }) + + test('should show comparison table', async ({ page }) => { + await expect(page.locator('.comparison-table')).toBeVisible() + + // Check table headers + await expect(page.locator('.comparison-table th')).toContainText(['Animation Name', 'Legacy', 'Artist', 'Hierarchical', 'Semantic']) + }) + + test('should filter comparison table', async ({ page }) => { + // Type in search box + await page.fill('.search-input', 'walk') + + // Check that results are filtered + await expect(page.locator('.comparison-table tbody tr:visible')).toHaveCount.greaterThan(0) + await expect(page.locator('.comparison-table tbody tr:visible')).toContainText('walk') + }) + + test('should convert between schemes', async ({ page }) => { + // Use the conversion demo + await page.fill('.animation-input', 'char_walk_01') + await page.selectOption('#sourceSchemeSelect', 'artist') + await page.selectOption('#targetSchemeSelect', 'semantic') + + // Check conversion result + await expect(page.locator('.conversion-result')).toBeVisible() + await expect(page.locator('.result-value')).toContainText('character.movement.walk') + }) +}) + +test.describe('Interactive Playground', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/interactive.html') + }) + + test('should load interactive page', async ({ page }) => { + await expect(page).toHaveTitle(/Interactive/) + await expect(page.locator('h1')).toContainText('Interactive Playground') + }) + + test('should have functional controls', async ({ page }) => { + // Check that control sections are present + await expect(page.locator('.playground-controls')).toBeVisible() + await expect(page.locator('.playground-main')).toBeVisible() + await expect(page.locator('.performance-monitor')).toBeVisible() + }) + + test('should switch between tabs', async ({ page }) => { + // Click on different tabs + await page.click('[data-tab="converter"]') + await expect(page.locator('.tab-content[data-tab="converter"]')).toBeVisible() + + await page.click('[data-tab="validator"]') + await expect(page.locator('.tab-content[data-tab="validator"]')).toBeVisible() + + await page.click('[data-tab="generator"]') + await expect(page.locator('.tab-content[data-tab="generator"]')).toBeVisible() + }) + + test('should run code in playground', async ({ page }) => { + // Switch to code editor tab + await page.click('[data-tab="code"]') + + // Clear and enter new code + await page.fill('.code-editor', ` +const mapper = new AnimationNameMapper(); +const result = mapper.convert('char_walk_01', 'artist', 'semantic'); +console.log(result); + `) + + // Run the code + await page.click('#runCodeBtn') + + // Check output + await expect(page.locator('.output-panel')).toContainText('character.movement.walk') + }) + + test('should validate animation names in real-time', async ({ page }) => { + // Enter valid animation name + await page.fill('#playgroundAnimationName', 'character.idle.basic') + await page.selectOption('#playgroundScheme', 'semantic') + + // Check validation indicator + await expect(page.locator('.validation-indicator')).toHaveClass(/success/) + + // Enter invalid animation name + await page.fill('#playgroundAnimationName', 'invalid-name-123!') + + // Check validation indicator shows error + await expect(page.locator('.validation-indicator')).toHaveClass(/error/) + }) + + test('should show performance metrics', async ({ page }) => { + // Perform some conversions + await page.fill('#playgroundAnimationName', 'char_walk_01') + await page.selectOption('#playgroundSourceScheme', 'artist') + await page.selectOption('#playgroundTargetScheme', 'semantic') + await page.click('#convertPlaygroundBtn') + + // Check that performance metrics are updated + await expect(page.locator('.monitor-value')).toHaveCount.greaterThan(0) + await expect(page.locator('.conversion-time .monitor-value')).toContainText(/\d+/) + }) + + test('should save and load history', async ({ page }) => { + // Perform a conversion + await page.fill('#playgroundAnimationName', 'test_animation') + await page.selectOption('#playgroundSourceScheme', 'legacy') + await page.selectOption('#playgroundTargetScheme', 'semantic') + await page.click('#convertPlaygroundBtn') + + // Check that history is updated + await expect(page.locator('.history-item')).toHaveCount.greaterThan(0) + + // Click on history item to load it + await page.click('.history-item').first() + + // Check that form is populated + await expect(page.locator('#playgroundAnimationName')).toHaveValue('test_animation') + }) +}) diff --git a/vite.demo.config.js b/vite.demo.config.js new file mode 100644 index 0000000..3fca3c4 --- /dev/null +++ b/vite.demo.config.js @@ -0,0 +1,167 @@ +import { defineConfig } from 'vite' +import path from 'path' + +/** + * Vite configuration for Owen Animation System Demo + * + * This configuration builds the demo application showcasing + * the multi-scheme animation naming system and its features. + */ +export default defineConfig({ + // Demo-specific build configuration + root: './demo', + + build: { + outDir: '../dist-demo', + emptyOutDir: true, + + // Optimization settings for demo + rollupOptions: { + input: { + main: path.resolve(__dirname, 'demo/index.html'), + examples: path.resolve(__dirname, 'demo/examples.html'), + comparison: path.resolve(__dirname, 'demo/comparison.html'), + interactive: path.resolve(__dirname, 'demo/interactive.html') + }, + + output: { + // Asset organization + assetFileNames: (assetInfo) => { + const info = assetInfo.name.split('.') + const ext = info[info.length - 1] + + if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) { + return 'assets/images/[name]-[hash][extname]' + } + + if (/woff2?|eot|ttf|otf/i.test(ext)) { + return 'assets/fonts/[name]-[hash][extname]' + } + + if (/gltf|glb|fbx/i.test(ext)) { + return 'assets/animations/[name]-[hash][extname]' + } + + return 'assets/[name]-[hash][extname]' + }, + + chunkFileNames: 'js/[name]-[hash].js', + entryFileNames: 'js/[name]-[hash].js' + } + }, + + // Source maps for debugging + sourcemap: true, + + // Minification + minify: 'terser', + terserOptions: { + compress: { + drop_console: false, // Keep console logs for demo + drop_debugger: true + } + }, + + // Target modern browsers for demo + target: 'es2020' + }, + + // Development server settings + server: { + port: 3001, + host: true, + open: '/demo/', + + // Proxy API calls if needed + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + }, + + // Preview server settings + preview: { + port: 3002, + host: true, + open: true + }, + + // Resolve configuration + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + '@demo': path.resolve(__dirname, 'demo'), + '@assets': path.resolve(__dirname, 'assets'), + '@examples': path.resolve(__dirname, 'examples') + } + }, + + // Define global constants for demo + define: { + __DEMO_VERSION__: JSON.stringify(process.env.npm_package_version || '1.0.0'), + __BUILD_TIMESTAMP__: JSON.stringify(new Date().toISOString()), + __ANIMATION_SCHEMES__: JSON.stringify(['legacy', 'artist', 'hierarchical', 'semantic']) + }, + + // Plugin configuration + plugins: [ + // Add any demo-specific plugins here + ], + + // CSS configuration + css: { + preprocessorOptions: { + scss: { + additionalData: ` + @import "@demo/styles/variables.scss"; + @import "@demo/styles/mixins.scss"; + ` + } + }, + + // CSS modules for component styling + modules: { + localsConvention: 'camelCase' + } + }, + + // Asset handling + assetsInclude: [ + '**/*.gltf', + '**/*.glb', + '**/*.fbx', + '**/*.babylon' + ], + + // Optimization + optimizeDeps: { + include: [ + 'three', + 'three/examples/jsm/loaders/GLTFLoader', + 'three/examples/jsm/loaders/FBXLoader', + 'three/examples/jsm/controls/OrbitControls' + ], + exclude: [ + // Exclude any demo-specific modules that shouldn't be pre-bundled + ] + }, + + // Environment variables + envPrefix: 'OWEN_DEMO_', + + // Base path for deployment + base: process.env.NODE_ENV === 'production' ? '/Owen/' : '/', + + // Worker configuration for animation processing + worker: { + format: 'es' + }, + + // Experimental features + experimental: { + buildAdvancedBaseOptions: true + } +})