Implement multi-scheme animation name mapper for Owen Animation System
Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
- Added AnimationNameMapper class to handle conversion between different animation naming schemes (legacy, artist, hierarchical, semantic). - Included methods for initialization, pattern matching, conversion, and validation of animation names. - Developed comprehensive unit tests for the animation name converter and demo pages using Playwright. - Created a Vite configuration for the demo application, including asset handling and optimization settings. - Enhanced the demo with features for batch conversion, performance metrics, and responsive design.
This commit is contained in:
38
.github/CODEOWNERS
vendored
Normal file
38
.github/CODEOWNERS
vendored
Normal file
@ -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
|
||||
163
.github/ISSUE_TEMPLATE/animation_scheme.yml
vendored
Normal file
163
.github/ISSUE_TEMPLATE/animation_scheme.yml
vendored
Normal file
@ -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
|
||||
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -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
|
||||
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -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
|
||||
139
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
139
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
@ -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
|
||||
144
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
144
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -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
|
||||
85
.github/dependabot.yml
vendored
Normal file
85
.github/dependabot.yml
vendored
Normal file
@ -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: "monday"
|
||||
time: "09:00"
|
||||
reviewers:
|
||||
- "kjanat"
|
||||
assignees:
|
||||
- "kjanat"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
prefix-development: "deps-dev"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
- "blender"
|
||||
169
.github/workflows/animation-processing.yml
vendored
Normal file
169
.github/workflows/animation-processing.yml
vendored
Normal file
@ -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
|
||||
118
.github/workflows/ci.yml
vendored
Normal file
118
.github/workflows/ci.yml
vendored
Normal file
@ -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 }}
|
||||
246
.github/workflows/demo-deployment.yml
vendored
Normal file
246
.github/workflows/demo-deployment.yml
vendored
Normal file
@ -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'
|
||||
});
|
||||
251
.github/workflows/multi-scheme-testing.yml
vendored
Normal file
251
.github/workflows/multi-scheme-testing.yml
vendored
Normal file
@ -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
|
||||
});
|
||||
569
.github/workflows/performance-testing.yml
vendored
Normal file
569
.github/workflows/performance-testing.yml
vendored
Normal file
@ -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
|
||||
});
|
||||
}
|
||||
252
.github/workflows/release.yml
vendored
Normal file
252
.github/workflows/release.yml
vendored
Normal file
@ -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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,6 +22,9 @@
|
||||
# Three.js cache
|
||||
.three-cache/
|
||||
|
||||
# MyPy cache
|
||||
.mypy_cache/
|
||||
|
||||
# Editor (optional - remove if you want to commit editor files)
|
||||
.vscode/
|
||||
|
||||
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@ -5,6 +5,25 @@ All notable changes to the Owen Animation System will be documented in this file
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.2] - 2025-05-24
|
||||
|
||||
### Added
|
||||
|
||||
- 🔄 Multi-scheme animation naming system with four naming schemes
|
||||
- 🧪 Testing framework for multi-scheme compatibility with Playwright
|
||||
- 📋 GitHub workflows for animation processing and testing
|
||||
- 🚀 Demo pages showing multi-scheme usage
|
||||
- 📄 Multi-scheme animation naming guide (MULTI_SCHEME_GUIDE.md)
|
||||
- 🛠️ Animation validation and conversion scripts
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 Fixed duplicate methods in OwenAnimationContext
|
||||
- 🔧 Fixed linting issues across the codebase
|
||||
- 🐛 Fixed alert references in demo.js with window.alert
|
||||
- 🔧 Removed unused variables in scripts
|
||||
- 🔍 Fixed import issues in AnimationNameMapper
|
||||
|
||||
## [1.0.1] - 2025-05-24
|
||||
|
||||
### Changed
|
||||
@ -108,6 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- 🎭 Core state handlers with basic functionality
|
||||
- 🛠️ Development environment foundations
|
||||
|
||||
[1.0.1]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.1
|
||||
[1.0.0]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.0
|
||||
[0.1.0]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v0.1.0
|
||||
[1.0.0]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.0
|
||||
[1.0.1]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.1
|
||||
[1.0.2]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.2
|
||||
|
||||
303
MULTI_SCHEME_GUIDE.md
Normal file
303
MULTI_SCHEME_GUIDE.md
Normal file
@ -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/).
|
||||
54
README.md
54
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:
|
||||
|
||||
|
||||
337
demo/comparison.html
Normal file
337
demo/comparison.html
Normal file
@ -0,0 +1,337 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Scheme Comparison - Owen Animation System</title>
|
||||
<link rel="stylesheet" href="./styles/main.css" />
|
||||
<link rel="stylesheet" href="./styles/comparison.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="demo-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<span class="logo-text">Owen</span>
|
||||
<span class="logo-subtitle">Scheme Comparison</span>
|
||||
</h1>
|
||||
<nav class="demo-nav">
|
||||
<a href="index.html" class="nav-link">Demo</a>
|
||||
<a href="examples.html" class="nav-link">Examples</a>
|
||||
<a href="comparison.html" class="nav-link active">Comparison</a>
|
||||
<a href="interactive.html" class="nav-link">Interactive</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="comparison-main">
|
||||
<div class="container">
|
||||
<section class="comparison-hero">
|
||||
<h2>Animation Naming Scheme Comparison</h2>
|
||||
<p>
|
||||
Compare the four supported naming schemes and understand when to use
|
||||
each one.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Scheme Overview -->
|
||||
<section class="schemes-overview">
|
||||
<div class="scheme-cards">
|
||||
<div class="scheme-card" data-scheme="legacy">
|
||||
<h3>Legacy</h3>
|
||||
<div class="scheme-pattern">snake_case</div>
|
||||
<div class="scheme-description">
|
||||
Traditional lowercase with underscores. Compatible with older
|
||||
animation systems.
|
||||
</div>
|
||||
<div class="scheme-example">walk_forward</div>
|
||||
</div>
|
||||
|
||||
<div class="scheme-card" data-scheme="artist">
|
||||
<h3>Artist</h3>
|
||||
<div class="scheme-pattern">PascalCase</div>
|
||||
<div class="scheme-description">
|
||||
Artist-friendly naming with clear capitalization. Intuitive for
|
||||
content creators.
|
||||
</div>
|
||||
<div class="scheme-example">WalkForward</div>
|
||||
</div>
|
||||
|
||||
<div class="scheme-card" data-scheme="hierarchical">
|
||||
<h3>Hierarchical</h3>
|
||||
<div class="scheme-pattern">dot.notation</div>
|
||||
<div class="scheme-description">
|
||||
Structured hierarchy with dots. Excellent for organizing complex
|
||||
animation sets.
|
||||
</div>
|
||||
<div class="scheme-example">character.movement.walk.forward</div>
|
||||
</div>
|
||||
|
||||
<div class="scheme-card" data-scheme="semantic">
|
||||
<h3>Semantic</h3>
|
||||
<div class="scheme-pattern">descriptive_names</div>
|
||||
<div class="scheme-description">
|
||||
Semantic meaning with underscores. Clear intent and
|
||||
self-documenting.
|
||||
</div>
|
||||
<div class="scheme-example">character_walk_forward</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Comparison Table -->
|
||||
<section class="comparison-table-section">
|
||||
<h3>Animation Name Comparison</h3>
|
||||
<div class="table-controls">
|
||||
<button class="filter-btn active" data-category="all">All</button>
|
||||
<button class="filter-btn" data-category="movement">
|
||||
Movement
|
||||
</button>
|
||||
<button class="filter-btn" data-category="combat">Combat</button>
|
||||
<button class="filter-btn" data-category="idle">Idle</button>
|
||||
<button class="filter-btn" data-category="interaction">
|
||||
Interaction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table-container">
|
||||
<table class="comparison-table" id="animation-comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Animation Type</th>
|
||||
<th>Legacy</th>
|
||||
<th>Artist</th>
|
||||
<th>Hierarchical</th>
|
||||
<th>Semantic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Table will be populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Detailed Comparison -->
|
||||
<section class="detailed-comparison">
|
||||
<h3>Detailed Analysis</h3>
|
||||
|
||||
<div class="comparison-aspects">
|
||||
<div class="aspect-card">
|
||||
<h4>🎯 Use Cases</h4>
|
||||
<div class="aspect-content">
|
||||
<div class="use-case">
|
||||
<strong>Legacy:</strong> Migrating from older systems,
|
||||
maintaining backward compatibility
|
||||
</div>
|
||||
<div class="use-case">
|
||||
<strong>Artist:</strong> Content creation workflows,
|
||||
artist-friendly tools
|
||||
</div>
|
||||
<div class="use-case">
|
||||
<strong>Hierarchical:</strong> Large animation libraries,
|
||||
complex character systems
|
||||
</div>
|
||||
<div class="use-case">
|
||||
<strong>Semantic:</strong> Modern development, clear
|
||||
documentation needs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aspect-card">
|
||||
<h4>⚡ Performance</h4>
|
||||
<div class="performance-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-name">Lookup Speed:</span>
|
||||
<div class="metric-bars">
|
||||
<div class="metric-bar legacy" style="width: 95%">
|
||||
Legacy
|
||||
</div>
|
||||
<div class="metric-bar artist" style="width: 90%">
|
||||
Artist
|
||||
</div>
|
||||
<div class="metric-bar hierarchical" style="width: 85%">
|
||||
Hierarchical
|
||||
</div>
|
||||
<div class="metric-bar semantic" style="width: 92%">
|
||||
Semantic
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-name">Memory Usage:</span>
|
||||
<div class="metric-bars">
|
||||
<div class="metric-bar legacy" style="width: 88%">
|
||||
Legacy
|
||||
</div>
|
||||
<div class="metric-bar artist" style="width: 85%">
|
||||
Artist
|
||||
</div>
|
||||
<div class="metric-bar hierarchical" style="width: 75%">
|
||||
Hierarchical
|
||||
</div>
|
||||
<div class="metric-bar semantic" style="width: 82%">
|
||||
Semantic
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aspect-card">
|
||||
<h4>🛠️ Developer Experience</h4>
|
||||
<div class="aspect-content">
|
||||
<div class="dx-rating">
|
||||
<div class="dx-item">
|
||||
<span>Readability:</span>
|
||||
<div class="rating-stars">
|
||||
<div class="stars legacy">★★★☆☆</div>
|
||||
<div class="stars artist">★★★★☆</div>
|
||||
<div class="stars hierarchical">★★★★★</div>
|
||||
<div class="stars semantic">★★★★★</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dx-item">
|
||||
<span>Autocomplete:</span>
|
||||
<div class="rating-stars">
|
||||
<div class="stars legacy">★★★☆☆</div>
|
||||
<div class="stars artist">★★★★☆</div>
|
||||
<div class="stars hierarchical">★★★★★</div>
|
||||
<div class="stars semantic">★★★★☆</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dx-item">
|
||||
<span>Maintainability:</span>
|
||||
<div class="rating-stars">
|
||||
<div class="stars legacy">★★☆☆☆</div>
|
||||
<div class="stars artist">★★★☆☆</div>
|
||||
<div class="stars hierarchical">★★★★★</div>
|
||||
<div class="stars semantic">★★★★☆</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Migration Guide -->
|
||||
<section class="migration-guide">
|
||||
<h3>Migration Between Schemes</h3>
|
||||
<div class="migration-matrix">
|
||||
<div class="migration-card">
|
||||
<h4>From Legacy</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>To Artist:</strong> Capitalize first letter and after
|
||||
underscores
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Hierarchical:</strong> Replace underscores with
|
||||
dots, add category prefixes
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Semantic:</strong> Add descriptive prefixes
|
||||
(character_, ui_, effect_)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="migration-card">
|
||||
<h4>From Artist</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>To Legacy:</strong> Convert to lowercase, add
|
||||
underscores before capitals
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Hierarchical:</strong> Split on capitals, join with
|
||||
dots, add categories
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Semantic:</strong> Convert to lowercase with
|
||||
underscores, add prefixes
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="migration-card">
|
||||
<h4>Automated Tools</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>CLI Converter:</strong>
|
||||
<code>owen convert --from legacy --to semantic</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Batch Processing:</strong>
|
||||
<code>owen batch-convert ./animations/</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Validation:</strong>
|
||||
<code>owen validate --scheme semantic</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Best Practices -->
|
||||
<section class="best-practices">
|
||||
<h3>Best Practices & Recommendations</h3>
|
||||
<div class="practices-grid">
|
||||
<div class="practice-card">
|
||||
<h4>🏢 Enterprise Projects</h4>
|
||||
<p><strong>Recommended:</strong> Hierarchical or Semantic</p>
|
||||
<ul>
|
||||
<li>Clear organization structure</li>
|
||||
<li>Easy to maintain and scale</li>
|
||||
<li>Good IDE support</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<h4>🎨 Artist Workflows</h4>
|
||||
<p><strong>Recommended:</strong> Artist or Semantic</p>
|
||||
<ul>
|
||||
<li>Intuitive for content creators</li>
|
||||
<li>Clear visual distinction</li>
|
||||
<li>Good tool integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<h4>🔄 Legacy Migration</h4>
|
||||
<p><strong>Recommended:</strong> Gradual transition</p>
|
||||
<ul>
|
||||
<li>Start with Legacy scheme</li>
|
||||
<li>Use auto-conversion features</li>
|
||||
<li>Migrate incrementally</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="practice-card">
|
||||
<h4>🚀 New Projects</h4>
|
||||
<p><strong>Recommended:</strong> Semantic scheme</p>
|
||||
<ul>
|
||||
<li>Modern best practices</li>
|
||||
<li>Self-documenting code</li>
|
||||
<li>Future-proof design</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="demo-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
© 2024 Owen Animation System. Choose the scheme that fits your
|
||||
workflow.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./js/comparison.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
400
demo/examples.html
Normal file
400
demo/examples.html
Normal file
@ -0,0 +1,400 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Examples - Owen Animation System</title>
|
||||
<link rel="stylesheet" href="./styles/main.css" />
|
||||
<link rel="stylesheet" href="./styles/examples.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="demo-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<span class="logo-text">Owen</span>
|
||||
<span class="logo-subtitle">Examples</span>
|
||||
</h1>
|
||||
<nav class="demo-nav">
|
||||
<a href="index.html" class="nav-link">Demo</a>
|
||||
<a href="examples.html" class="nav-link active">Examples</a>
|
||||
<a href="comparison.html" class="nav-link">Comparison</a>
|
||||
<a href="interactive.html" class="nav-link">Interactive</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="examples-main">
|
||||
<div class="container">
|
||||
<section class="examples-hero">
|
||||
<h2>Code Examples & Integration Patterns</h2>
|
||||
<p>
|
||||
Explore practical examples of using the Owen Animation System in
|
||||
different frameworks and scenarios.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="examples-grid">
|
||||
<div class="example-category">
|
||||
<h3>Framework Integration</h3>
|
||||
<div class="example-cards">
|
||||
<div class="example-card">
|
||||
<h4>React Integration</h4>
|
||||
<p>Complete React component with animation state management</p>
|
||||
<a href="#react-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Vue Integration</h4>
|
||||
<p>Vue 3 composition API with reactive animation controls</p>
|
||||
<a href="#vue-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Node.js Server</h4>
|
||||
<p>Server-side animation processing and validation</p>
|
||||
<a href="#nodejs-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-category">
|
||||
<h3>Multi-Scheme Usage</h3>
|
||||
<div class="example-cards">
|
||||
<div class="example-card">
|
||||
<h4>Scheme Conversion</h4>
|
||||
<p>Converting animations between different naming schemes</p>
|
||||
<a href="#conversion-example" class="example-link"
|
||||
>View Example</a
|
||||
>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Batch Processing</h4>
|
||||
<p>Processing multiple animations with automated conversion</p>
|
||||
<a href="#batch-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Validation Pipeline</h4>
|
||||
<p>Complete validation workflow with error handling</p>
|
||||
<a href="#validation-example" class="example-link"
|
||||
>View Example</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-category">
|
||||
<h3>Advanced Features</h3>
|
||||
<div class="example-cards">
|
||||
<div class="example-card">
|
||||
<h4>Custom Schemes</h4>
|
||||
<p>Creating and registering custom naming schemes</p>
|
||||
<a href="#custom-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Performance Optimization</h4>
|
||||
<p>Optimizing animation loading and caching strategies</p>
|
||||
<a href="#performance-example" class="example-link"
|
||||
>View Example</a
|
||||
>
|
||||
</div>
|
||||
<div class="example-card">
|
||||
<h4>Testing Integration</h4>
|
||||
<p>Unit and integration testing for animation systems</p>
|
||||
<a href="#testing-example" class="example-link">View Example</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Detailed Examples -->
|
||||
<section class="detailed-examples">
|
||||
<div id="react-example" class="example-detail">
|
||||
<h3>React Integration Example</h3>
|
||||
<pre><code class="javascript">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>
|
||||
)
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div id="conversion-example" class="example-detail">
|
||||
<h3>Animation Name Conversion</h3>
|
||||
<pre><code class="javascript">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)</code></pre>
|
||||
</div>
|
||||
|
||||
<div id="batch-example" class="example-detail">
|
||||
<h3>Batch Processing Pipeline</h3>
|
||||
<pre><code class="javascript">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)
|
||||
})</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="demo-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
© 2024 Owen Animation System. All examples are MIT licensed for
|
||||
educational use.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./js/examples.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
311
demo/index.html
Normal file
311
demo/index.html
Normal file
@ -0,0 +1,311 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Owen Animation System - Interactive Demo</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Interactive demonstration of the Owen Animation System with multi-scheme naming support"
|
||||
/>
|
||||
|
||||
<!-- Preload critical resources -->
|
||||
<link rel="preload" href="./styles/main.css" as="style" />
|
||||
<link rel="preload" href="./js/demo.js" as="script" />
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="./styles/main.css" />
|
||||
<link rel="stylesheet" href="./styles/demo.css" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="demo-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<span class="logo-text">Owen</span>
|
||||
<span class="logo-subtitle">Animation System</span>
|
||||
</h1>
|
||||
<nav class="demo-nav">
|
||||
<a href="index.html" class="nav-link active">Demo</a>
|
||||
<a href="examples.html" class="nav-link">Examples</a>
|
||||
<a href="comparison.html" class="nav-link">Comparison</a>
|
||||
<a href="interactive.html" class="nav-link">Interactive</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="demo-main">
|
||||
<div class="container">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-content">
|
||||
<h2>Multi-Scheme Animation Naming</h2>
|
||||
<p>
|
||||
Experience the power of flexible animation naming with support for
|
||||
Legacy, Artist, Hierarchical, and Semantic schemes.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<button id="start-demo" class="btn btn-primary">
|
||||
Start Interactive Demo
|
||||
</button>
|
||||
<a href="https://github.com/kjanat/Owen" class="btn btn-secondary"
|
||||
>View on GitHub</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<canvas id="demo-canvas" width="600" height="400"></canvas>
|
||||
<div class="demo-controls">
|
||||
<div class="control-group">
|
||||
<label for="naming-scheme">Naming Scheme:</label>
|
||||
<select id="naming-scheme">
|
||||
<option value="semantic">Semantic (default)</option>
|
||||
<option value="hierarchical">Hierarchical</option>
|
||||
<option value="artist">Artist</option>
|
||||
<option value="legacy">Legacy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="animation-select">Animation:</label>
|
||||
<select id="animation-select">
|
||||
<option value="">Select an animation...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="play-animation" class="btn btn-small" disabled>
|
||||
Play
|
||||
</button>
|
||||
<button id="pause-animation" class="btn btn-small" disabled>
|
||||
Pause
|
||||
</button>
|
||||
<button id="stop-animation" class="btn btn-small" disabled>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="features-section">
|
||||
<h3>Key Features</h3>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎯</div>
|
||||
<h4>Multi-Scheme Support</h4>
|
||||
<p>
|
||||
Seamlessly work with Legacy, Artist, Hierarchical, and Semantic
|
||||
naming schemes in the same project.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h4>Automatic Conversion</h4>
|
||||
<p>
|
||||
Convert animation names between schemes automatically with
|
||||
built-in validation and error handling.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h4>Performance Optimized</h4>
|
||||
<p>
|
||||
Efficient caching and lazy loading ensure smooth performance
|
||||
even with large animation libraries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🛠️</div>
|
||||
<h4>Developer Tools</h4>
|
||||
<p>
|
||||
Comprehensive CLI tools, validation scripts, and documentation
|
||||
generators for streamlined workflows.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live Demo Section -->
|
||||
<section class="live-demo-section">
|
||||
<h3>Live Animation Conversion</h3>
|
||||
<div class="conversion-demo">
|
||||
<div class="input-section">
|
||||
<label for="input-animation">Enter Animation Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="input-animation"
|
||||
placeholder="e.g., character_walk_forward"
|
||||
/>
|
||||
|
||||
<label for="input-scheme">Current Scheme:</label>
|
||||
<select id="input-scheme">
|
||||
<option value="semantic">Semantic</option>
|
||||
<option value="hierarchical">Hierarchical</option>
|
||||
<option value="artist">Artist</option>
|
||||
<option value="legacy">Legacy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="conversion-results">
|
||||
<h4>Converted Names:</h4>
|
||||
<div class="scheme-results">
|
||||
<div class="scheme-result">
|
||||
<strong>Legacy:</strong> <span id="result-legacy">-</span>
|
||||
</div>
|
||||
<div class="scheme-result">
|
||||
<strong>Artist:</strong> <span id="result-artist">-</span>
|
||||
</div>
|
||||
<div class="scheme-result">
|
||||
<strong>Hierarchical:</strong>
|
||||
<span id="result-hierarchical">-</span>
|
||||
</div>
|
||||
<div class="scheme-result">
|
||||
<strong>Semantic:</strong> <span id="result-semantic">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples Section -->
|
||||
<section class="code-examples-section">
|
||||
<h3>Usage Examples</h3>
|
||||
<div class="code-tabs">
|
||||
<button class="tab-button active" data-tab="basic">
|
||||
Basic Usage
|
||||
</button>
|
||||
<button class="tab-button" data-tab="conversion">
|
||||
Name Conversion
|
||||
</button>
|
||||
<button class="tab-button" data-tab="validation">Validation</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="basic-tab" class="tab-pane active">
|
||||
<pre><code class="javascript">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</code></pre>
|
||||
</div>
|
||||
|
||||
<div id="conversion-tab" class="tab-pane">
|
||||
<pre><code class="javascript">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']</code></pre>
|
||||
</div>
|
||||
|
||||
<div id="validation-tab" class="tab-pane">
|
||||
<pre><code class="javascript">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</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="demo-footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>Owen Animation System</h4>
|
||||
<p>
|
||||
A comprehensive Three.js animation system with multi-scheme naming
|
||||
support.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h4>Documentation</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="../docs/ANIMATION_SYSTEM.md">Animation System Guide</a>
|
||||
</li>
|
||||
<li><a href="../docs/API_REFERENCE.md">API Reference</a></li>
|
||||
<li><a href="../docs/MIGRATION_GUIDE.md">Migration Guide</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h4>Examples</h4>
|
||||
<ul>
|
||||
<li><a href="../examples/basic/README.md">Basic Usage</a></li>
|
||||
<li>
|
||||
<a href="../examples/react/README.md">React Integration</a>
|
||||
</li>
|
||||
<li><a href="../examples/vue/README.md">Vue Integration</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h4>Links</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/kjanat/Owen">GitHub Repository</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://gitea.kajkowalski.nl/kjanat/Owen"
|
||||
>Gitea Repository</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="https://semver.org/">Semantic Versioning</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>
|
||||
© 2024 Kaj "kjanat" Kowalski. Licensed under AGPL-3.0-only OR
|
||||
LicenseRef-Commercial.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="module" src="./js/demo.js"></script>
|
||||
<script type="module" src="./js/animation-demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
306
demo/interactive.html
Normal file
306
demo/interactive.html
Normal file
@ -0,0 +1,306 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Interactive Playground - Owen Animation System</title>
|
||||
<link rel="stylesheet" href="./styles/main.css" />
|
||||
<link rel="stylesheet" href="./styles/interactive.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="demo-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<span class="logo-text">Owen</span>
|
||||
<span class="logo-subtitle">Interactive Playground</span>
|
||||
</h1>
|
||||
<nav class="demo-nav">
|
||||
<a href="index.html" class="nav-link">Demo</a>
|
||||
<a href="examples.html" class="nav-link">Examples</a>
|
||||
<a href="comparison.html" class="nav-link">Comparison</a>
|
||||
<a href="interactive.html" class="nav-link active">Interactive</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="interactive-main">
|
||||
<div class="container">
|
||||
<section class="playground-hero">
|
||||
<h2>Interactive Animation Playground</h2>
|
||||
<p>
|
||||
Experiment with the Owen Animation System in real-time. Try
|
||||
different schemes, test conversions, and see the results instantly.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="playground-layout">
|
||||
<!-- Sidebar Controls -->
|
||||
<aside class="playground-sidebar">
|
||||
<div class="control-section">
|
||||
<h3>Character Model</h3>
|
||||
<select id="model-select">
|
||||
<option value="basic-character">Basic Character</option>
|
||||
<option value="detailed-character">Detailed Character</option>
|
||||
<option value="robot-character">Robot Character</option>
|
||||
</select>
|
||||
<button id="load-model" class="btn btn-small">Load Model</button>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Naming Scheme</h3>
|
||||
<div class="scheme-selector">
|
||||
<label class="scheme-option">
|
||||
<input type="radio" name="scheme" value="legacy" />
|
||||
<span>Legacy (snake_case)</span>
|
||||
</label>
|
||||
<label class="scheme-option">
|
||||
<input type="radio" name="scheme" value="artist" />
|
||||
<span>Artist (PascalCase)</span>
|
||||
</label>
|
||||
<label class="scheme-option">
|
||||
<input type="radio" name="scheme" value="hierarchical" />
|
||||
<span>Hierarchical (dot.notation)</span>
|
||||
</label>
|
||||
<label class="scheme-option">
|
||||
<input type="radio" name="scheme" value="semantic" checked />
|
||||
<span>Semantic (descriptive_names)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Available Animations</h3>
|
||||
<div id="animation-list" class="animation-list">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Playback Controls</h3>
|
||||
<div class="playback-controls">
|
||||
<button id="play-btn" class="btn btn-primary">Play</button>
|
||||
<button id="pause-btn" class="btn">Pause</button>
|
||||
<button id="stop-btn" class="btn">Stop</button>
|
||||
</div>
|
||||
|
||||
<div class="playback-options">
|
||||
<label>
|
||||
<input type="checkbox" id="loop-animation" checked />
|
||||
Loop Animation
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="range"
|
||||
id="animation-speed"
|
||||
min="0.1"
|
||||
max="3"
|
||||
step="0.1"
|
||||
value="1"
|
||||
/>
|
||||
Speed: <span id="speed-value">1.0x</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3>Conversion Tool</h3>
|
||||
<div class="conversion-tool">
|
||||
<input
|
||||
type="text"
|
||||
id="conversion-input"
|
||||
placeholder="Enter animation name..."
|
||||
/>
|
||||
<select id="source-scheme">
|
||||
<option value="legacy">From Legacy</option>
|
||||
<option value="artist">From Artist</option>
|
||||
<option value="hierarchical">From Hierarchical</option>
|
||||
<option value="semantic">From Semantic</option>
|
||||
</select>
|
||||
<button id="convert-btn" class="btn btn-small">Convert</button>
|
||||
</div>
|
||||
<div id="conversion-results" class="conversion-results">
|
||||
<!-- Results will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Viewport -->
|
||||
<div class="playground-viewport">
|
||||
<div class="viewport-header">
|
||||
<h3>Animation Viewport</h3>
|
||||
<div class="viewport-controls">
|
||||
<button id="fullscreen-btn" class="btn btn-small">
|
||||
Fullscreen
|
||||
</button>
|
||||
<button id="reset-camera" class="btn btn-small">
|
||||
Reset Camera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viewport-container">
|
||||
<canvas id="animation-canvas"></canvas>
|
||||
<div class="viewport-overlay">
|
||||
<div id="loading-indicator" class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading model...</p>
|
||||
</div>
|
||||
|
||||
<div id="animation-info" class="animation-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Current Animation:</span>
|
||||
<span id="current-animation">None</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Duration:</span>
|
||||
<span id="animation-duration">0s</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Progress:</span>
|
||||
<div class="progress-bar">
|
||||
<div id="progress-fill" class="progress-fill"></div>
|
||||
</div>
|
||||
<span id="progress-time">0.0s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Code Generator -->
|
||||
<section class="code-generator">
|
||||
<h3>Generated Code</h3>
|
||||
<p>
|
||||
See the actual code that would be used to implement your current
|
||||
configuration:
|
||||
</p>
|
||||
|
||||
<div class="code-tabs">
|
||||
<button class="code-tab active" data-tab="javascript">
|
||||
JavaScript
|
||||
</button>
|
||||
<button class="code-tab" data-tab="react">React</button>
|
||||
<button class="code-tab" data-tab="vue">Vue</button>
|
||||
</div>
|
||||
|
||||
<div class="code-content">
|
||||
<div id="javascript-code" class="code-panel active">
|
||||
<pre><code id="js-code-output">// Configure your naming scheme and load animations
|
||||
// Code will be generated based on your selections above</code></pre>
|
||||
<button class="copy-code-btn" data-target="js-code-output">
|
||||
Copy Code
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="react-code" class="code-panel">
|
||||
<pre><code id="react-code-output">// React component implementation
|
||||
// Code will be generated based on your selections above</code></pre>
|
||||
<button class="copy-code-btn" data-target="react-code-output">
|
||||
Copy Code
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="vue-code" class="code-panel">
|
||||
<pre><code id="vue-code-output">// Vue component implementation
|
||||
// Code will be generated based on your selections above</code></pre>
|
||||
<button class="copy-code-btn" data-target="vue-code-output">
|
||||
Copy Code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Monitor -->
|
||||
<section class="performance-monitor">
|
||||
<h3>Performance Monitor</h3>
|
||||
<div class="performance-grid">
|
||||
<div class="performance-metric">
|
||||
<h4>Frame Rate</h4>
|
||||
<div class="metric-value" id="fps-counter">60 FPS</div>
|
||||
<div class="metric-chart" id="fps-chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="performance-metric">
|
||||
<h4>Memory Usage</h4>
|
||||
<div class="metric-value" id="memory-usage">0 MB</div>
|
||||
<div class="metric-chart" id="memory-chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="performance-metric">
|
||||
<h4>Animation Cache</h4>
|
||||
<div class="metric-value" id="cache-stats">0 / 0</div>
|
||||
<div class="cache-info">
|
||||
<span>Cached: <span id="cached-count">0</span></span>
|
||||
<span>Total: <span id="total-count">0</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="performance-metric">
|
||||
<h4>Conversion Time</h4>
|
||||
<div class="metric-value" id="conversion-time">0ms</div>
|
||||
<div class="time-breakdown">
|
||||
<span>Avg: <span id="avg-time">0ms</span></span>
|
||||
<span>Max: <span id="max-time">0ms</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Experiments Section -->
|
||||
<section class="experiments-section">
|
||||
<h3>Experiments & Tests</h3>
|
||||
<div class="experiments-grid">
|
||||
<div class="experiment-card">
|
||||
<h4>🚀 Stress Test</h4>
|
||||
<p>Load multiple animations and test performance</p>
|
||||
<button id="stress-test-btn" class="btn">Run Stress Test</button>
|
||||
<div id="stress-test-results" class="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="experiment-card">
|
||||
<h4>🔄 Conversion Benchmark</h4>
|
||||
<p>Benchmark animation name conversion performance</p>
|
||||
<button id="conversion-benchmark-btn" class="btn">
|
||||
Run Benchmark
|
||||
</button>
|
||||
<div id="conversion-benchmark-results" class="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="experiment-card">
|
||||
<h4>📊 Scheme Analysis</h4>
|
||||
<p>Analyze and compare naming scheme efficiency</p>
|
||||
<button id="scheme-analysis-btn" class="btn">
|
||||
Analyze Schemes
|
||||
</button>
|
||||
<div id="scheme-analysis-results" class="test-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="experiment-card">
|
||||
<h4>💾 Memory Profiling</h4>
|
||||
<p>Profile memory usage with different configurations</p>
|
||||
<button id="memory-profile-btn" class="btn">
|
||||
Profile Memory
|
||||
</button>
|
||||
<div id="memory-profile-results" class="test-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="demo-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
© 2024 Owen Animation System. Experiment freely and discover the
|
||||
possibilities.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="module" src="./js/interactive.js"></script>
|
||||
<script type="module" src="./js/playground.js"></script>
|
||||
<script type="module" src="./js/performance-monitor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
602
demo/js/demo.js
Normal file
602
demo/js/demo.js
Normal file
@ -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 = '<option value="">Select an animation...</option>'
|
||||
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 (
|
||||
<div className="animated-character">
|
||||
<div ref={containerRef} className="viewport" />
|
||||
<button onClick={() => playAnimation('${animation}')}>
|
||||
Play Animation
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}`
|
||||
},
|
||||
|
||||
generateVueExample () {
|
||||
const animation = this.selectedAnimation || 'character_walk_forward'
|
||||
return `<template>
|
||||
<div class="animated-character">
|
||||
<div ref="viewport" class="viewport"></div>
|
||||
<button @click="playAnimation('${animation}')">
|
||||
Play Animation
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { OwenAnimationContext } from '@kjanat/owen'
|
||||
|
||||
const viewport = ref(null)
|
||||
const animationContext = ref(null)
|
||||
const currentAnimation = ref('${animation}')
|
||||
|
||||
onMounted(async () => {
|
||||
const context = new OwenAnimationContext({
|
||||
namingScheme: '${this.currentScheme}',
|
||||
container: viewport.value
|
||||
})
|
||||
|
||||
await context.loadModel('./character.gltf')
|
||||
animationContext.value = context
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
animationContext.value?.dispose()
|
||||
})
|
||||
|
||||
const playAnimation = async (animationName) => {
|
||||
if (animationContext.value) {
|
||||
await animationContext.value.playAnimation(animationName)
|
||||
currentAnimation.value = animationName
|
||||
}
|
||||
}
|
||||
</script>`
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
412
demo/styles/comparison.css
Normal file
412
demo/styles/comparison.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
302
demo/styles/demo.css
Normal file
302
demo/styles/demo.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
306
demo/styles/examples.css
Normal file
306
demo/styles/examples.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
444
demo/styles/interactive.css
Normal file
444
demo/styles/interactive.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
472
demo/styles/main.css
Normal file
472
demo/styles/main.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -92,7 +92,7 @@ class SimpleOwenExample {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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...`)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
72
package-lock.json
generated
72
package-lock.json
generated
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "owen-animation-system",
|
||||
"version": "1.0.0",
|
||||
"name": "@kjanat/owen",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "owen-animation-system",
|
||||
"version": "1.0.0",
|
||||
"name": "@kjanat/owen",
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
|
||||
18
package.json
18
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kjanat/owen",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "A comprehensive Three.js animation system for character state management with clean architecture principles",
|
||||
"main": "src/index.js",
|
||||
"types": "src/index.d.ts",
|
||||
@ -9,11 +9,24 @@
|
||||
"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}'"
|
||||
"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",
|
||||
@ -29,6 +42,7 @@
|
||||
"three": "^0.176.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"pre-commit": "^1.2.2",
|
||||
"standard": "*",
|
||||
|
||||
92
playwright.config.js
Normal file
92
playwright.config.js
Normal file
@ -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
|
||||
})
|
||||
268
scripts/blender-animation-processor.py
Normal file
268
scripts/blender-animation-processor.py
Normal file
@ -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()
|
||||
361
scripts/check-naming-conflicts.js
Normal file
361
scripts/check-naming-conflicts.js
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
433
scripts/convert-animation-names.js
Normal file
433
scripts/convert-animation-names.js
Normal file
@ -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 <scheme> 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 <name> Animation name to convert
|
||||
--to, -t <scheme> Target scheme (legacy|artist|hierarchical|semantic)
|
||||
--all-schemes, -a Convert to all schemes
|
||||
--validate, -v Validate the animation name
|
||||
|
||||
FILE CONVERSION:
|
||||
--input, -i <file> Input file with animation names (JSON or line-separated)
|
||||
--output, -o <file> Output file for results (optional)
|
||||
--to, -t <scheme> Target scheme for conversion
|
||||
|
||||
BATCH OPERATIONS:
|
||||
--batch, -b Convert all animations in the system
|
||||
--output, -o <file> 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)
|
||||
})
|
||||
}
|
||||
252
scripts/generate-animation-constants.js
Normal file
252
scripts/generate-animation-constants.js
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
1556
scripts/generate-animation-docs.js
Normal file
1556
scripts/generate-animation-docs.js
Normal file
File diff suppressed because it is too large
Load Diff
802
scripts/generate-scheme-examples.js
Normal file
802
scripts/generate-scheme-examples.js
Normal file
@ -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 (
|
||||
<div>
|
||||
<h3>Animation Player</h3>
|
||||
|
||||
<select value={scheme} onChange={(e) => setScheme(e.target.value)}>
|
||||
<option value="legacy">Legacy</option>
|
||||
<option value="artist">Artist</option>
|
||||
<option value="hierarchical">Hierarchical</option>
|
||||
<option value="semantic">Semantic</option>
|
||||
</select>
|
||||
|
||||
<div>
|
||||
<h4>Available Animations ({scheme} scheme)</h4>
|
||||
{availableAnimations.map(anim => (
|
||||
<button
|
||||
key={anim}
|
||||
onClick={() => playAnimation(anim)}
|
||||
disabled={anim === currentAnimation}
|
||||
>
|
||||
{anim}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p>Currently playing: {currentAnimation}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Vue.js Integration
|
||||
|
||||
\`\`\`vue
|
||||
<template>
|
||||
<div class="animation-controller">
|
||||
<h3>Animation Controller</h3>
|
||||
|
||||
<div class="scheme-selector">
|
||||
<label>Naming Scheme:</label>
|
||||
<select v-model="selectedScheme">
|
||||
<option value="legacy">Legacy</option>
|
||||
<option value="artist">Artist</option>
|
||||
<option value="hierarchical">Hierarchical</option>
|
||||
<option value="semantic">Semantic</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="animation-grid">
|
||||
<div
|
||||
v-for="animation in availableAnimations"
|
||||
:key="animation"
|
||||
class="animation-card"
|
||||
:class="{ active: animation === currentAnimation }"
|
||||
@click="playAnimation(animation)"
|
||||
>
|
||||
{{ animation }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversion-display" v-if="currentAnimation">
|
||||
<h4>Current Animation in All Schemes:</h4>
|
||||
<div v-for="(name, scheme) in allSchemeNames" :key="scheme">
|
||||
<strong>{{ scheme }}:</strong> {{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
|
||||
|
||||
export default {
|
||||
props: ['gltf'],
|
||||
setup(props) {
|
||||
const context = new OwenAnimationContext(props.gltf)
|
||||
const mapper = new AnimationNameMapper()
|
||||
|
||||
const selectedScheme = ref('semantic')
|
||||
const currentAnimation = ref('')
|
||||
|
||||
const availableAnimations = computed(() =>
|
||||
mapper.getAllAnimationsByScheme(selectedScheme.value)
|
||||
)
|
||||
|
||||
const allSchemeNames = computed(() => {
|
||||
if (!currentAnimation.value) return {}
|
||||
|
||||
try {
|
||||
return mapper.getAllNames(currentAnimation.value)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
const playAnimation = (animationName) => {
|
||||
try {
|
||||
const clip = context.getClip(animationName)
|
||||
clip.play()
|
||||
currentAnimation.value = animationName
|
||||
} catch (error) {
|
||||
console.error('Failed to play animation:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Set default animation when scheme changes
|
||||
watch(selectedScheme, (newScheme) => {
|
||||
const animations = mapper.getAllAnimationsByScheme(newScheme)
|
||||
if (animations.length > 0) {
|
||||
playAnimation(animations[0])
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
selectedScheme,
|
||||
currentAnimation,
|
||||
availableAnimations,
|
||||
allSchemeNames,
|
||||
playAnimation
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
\`\`\`
|
||||
|
||||
## 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)
|
||||
})
|
||||
}
|
||||
315
scripts/test-multi-schemes.js
Normal file
315
scripts/test-multi-schemes.js
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
168
scripts/validate-animations.js
Normal file
168
scripts/validate-animations.js
Normal file
@ -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()
|
||||
}
|
||||
441
scripts/validate-processed-animations.js
Normal file
441
scripts/validate-processed-animations.js
Normal file
@ -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 }
|
||||
280
src/animation/AnimationConstants.js
Normal file
280
src/animation/AnimationConstants.js
Normal file
@ -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)
|
||||
}
|
||||
599
src/animation/AnimationNameMapper.js
Normal file
599
src/animation/AnimationNameMapper.js
Normal file
@ -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 }
|
||||
}
|
||||
}
|
||||
@ -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<string, AnimationClip>}
|
||||
@ -178,7 +185,7 @@ export class OwenAnimationContext {
|
||||
|
||||
// Transition to appropriate next state based on current state
|
||||
if (this.currentState === States.WAITING) {
|
||||
await this.transitionTo(States.REACTING);
|
||||
await this.transitionTo(States.REACTING)
|
||||
} else if (this.currentState === States.REACTING) {
|
||||
await this.transitionTo(States.TYPING)
|
||||
}
|
||||
@ -240,12 +247,32 @@ export class OwenAnimationContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
14
src/index.js
14
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'
|
||||
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -26,9 +26,9 @@ 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.WAITING, WaitStateHandler)
|
||||
this.registerStateHandler(States.REACTING, ReactStateHandler)
|
||||
this.registerStateHandler(States.TYPING, TypeStateHandler)
|
||||
this.registerStateHandler(States.SLEEPING, SleepStateHandler)
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
158
tests/demo.spec.js
Normal file
158
tests/demo.spec.js
Normal file
@ -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+/)
|
||||
})
|
||||
})
|
||||
177
tests/pages.spec.js
Normal file
177
tests/pages.spec.js
Normal file
@ -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')
|
||||
})
|
||||
})
|
||||
167
vite.demo.config.js
Normal file
167
vite.demo.config.js
Normal file
@ -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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user