Compare commits
11 Commits
v0.1.0
...
e217642174
| Author | SHA1 | Date | |
|---|---|---|---|
|
e217642174
|
|||
|
cb02f280cb
|
|||
| 0d024c5556 | |||
| 0595bf0a82 | |||
|
ad8dbb95dd
|
|||
|
d513e80c07
|
|||
|
472de05e4b
|
|||
|
52f5a204c4
|
|||
|
60aad20b5e
|
|||
|
734da64b98
|
|||
|
658e1e64b2
|
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@ -0,0 +1,24 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,jsx,ts,tsx}]
|
||||
indent_size = 2
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{json,md}]
|
||||
indent_size = 2
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = true
|
||||
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@v4
|
||||
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@v2
|
||||
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@v4
|
||||
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@v4
|
||||
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@v2
|
||||
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@v4
|
||||
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@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist/demo
|
||||
destination_dir: latest
|
||||
450
.gitignore
vendored
Normal file
450
.gitignore
vendored
Normal file
@ -0,0 +1,450 @@
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Documentation output
|
||||
/docs/
|
||||
|
||||
# Animation assets (if storing locally)
|
||||
/assets/models/
|
||||
/assets/animations/
|
||||
|
||||
# Example build outputs
|
||||
/examples/dist/
|
||||
|
||||
# 3D Models (optional - remove if you want to commit models)
|
||||
*.gltf
|
||||
*.glb
|
||||
*.fbx
|
||||
*.obj
|
||||
*.dae
|
||||
|
||||
# Three.js cache
|
||||
.three-cache/
|
||||
|
||||
# MyPy cache
|
||||
.mypy_cache/
|
||||
|
||||
# Editor (optional - remove if you want to commit editor files)
|
||||
.vscode/
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
### Linux ###
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### WebStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### WebStorm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
.idea/**/azureSettings.xml
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
|
||||
133
CHANGELOG.md
Normal file
133
CHANGELOG.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Changelog
|
||||
|
||||
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
|
||||
|
||||
- 🎨 Standardized code style throughout the codebase
|
||||
- 🔧 Converted semicolons to non-semicolons according to JavaScript Standard Style
|
||||
- 📝 Improved code consistency and readability
|
||||
|
||||
## [1.0.0] - 2025-05-23
|
||||
|
||||
### Added
|
||||
|
||||
- 🎯 Complete modular architecture with proper ES module structure
|
||||
- 🧩 Extensible plugin system for custom states and emotions
|
||||
- 📊 Comprehensive JSDoc documentation across all modules
|
||||
- 🎮 Enhanced interactive demo with keyboard controls
|
||||
- 📦 TypeScript type definitions for all components
|
||||
- 🔧 Configuration system for fine-tuning behavior
|
||||
- 🏗️ Examples directory with various implementation patterns
|
||||
- 🚀 Vite-based development and build system
|
||||
|
||||
### Enhanced
|
||||
|
||||
- ⚡ Optimized animation caching with intelligent preloading
|
||||
- 🤖 Advanced emotional analysis with broader message understanding
|
||||
- 🔄 Sophisticated animation transitions with nested state support
|
||||
- 📝 Extended animation naming convention with nested animations
|
||||
- 🎨 Refined state machine behavior and transitions
|
||||
- 🛠️ Improved development tooling integration
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Core Classes:**
|
||||
- `OwenAnimationContext` - Main system controller
|
||||
- `AnimationClip` - Individual animation management
|
||||
- `AnimationClipFactory` - Animation creation with metadata parsing
|
||||
- `StateHandler` - Abstract base for state implementations
|
||||
- `StateFactory` - Dynamic state handler creation
|
||||
|
||||
- **State Handlers:**
|
||||
- `WaitStateHandler` - Idle state with quirk animations
|
||||
- `ReactStateHandler` - User input response with emotion analysis
|
||||
- `TypeStateHandler` - Typing state with emotional variations
|
||||
- `SleepStateHandler` - Inactive state management
|
||||
|
||||
- **Animation Loaders:**
|
||||
- `AnimationLoader` - Abstract animation loading interface
|
||||
- `GLTFAnimationLoader` - GLTF/GLB model animation loader
|
||||
|
||||
- **Factories:**
|
||||
- `OwenSystemFactory` - Main system assembly factory
|
||||
|
||||
### Features
|
||||
|
||||
- **Animation System:**
|
||||
- Support for Loop (L), Quirk (Q), Transition (T), and Nested animations
|
||||
- Automatic metadata parsing from animation names
|
||||
- Efficient animation caching and resource management
|
||||
- Smooth transitions between states and emotions
|
||||
|
||||
- **State Machine:**
|
||||
- Four core states: Wait, React, Type, Sleep
|
||||
- Emotional state transitions (Neutral, Angry, Shocked, Happy, Sad)
|
||||
- Automatic inactivity detection and sleep transitions
|
||||
- Message analysis for emotional response determination
|
||||
|
||||
- **Developer Experience:**
|
||||
- Comprehensive TypeScript type definitions
|
||||
- JSDoc documentation for all public APIs
|
||||
- Example implementations and demos
|
||||
- ESLint configuration for code quality
|
||||
- Vite development server setup
|
||||
|
||||
### Documentation
|
||||
|
||||
- Complete README with installation and usage instructions
|
||||
- API documentation via JSDoc
|
||||
- Code examples for basic and advanced usage
|
||||
- Animation naming convention guide
|
||||
- Troubleshooting section
|
||||
|
||||
### Examples
|
||||
|
||||
- Basic browser demo with Three.js integration
|
||||
- Simple Node.js example for testing
|
||||
- Interactive controls for state transitions
|
||||
- Mock model implementation for development
|
||||
|
||||
## [0.1.0] - 2025-05-01
|
||||
|
||||
### Added
|
||||
|
||||
- 🎉 First implementation of Owen Animation System
|
||||
- ✨ Basic state machine implementation (Wait, React, Type, Sleep)
|
||||
- 🤖 Simple emotional response system with basic message analysis
|
||||
- 🏗️ Initial architecture with basic dependency injection pattern
|
||||
- 📝 Basic animation naming parser for transitions and states
|
||||
- 🔄 Basic animation transitions between states
|
||||
- ⚡ Simple animation clip caching
|
||||
- 🎮 Basic Three.js integration with GLTFLoader
|
||||
- 🎭 Core state handlers with basic functionality
|
||||
- 🛠️ Development environment foundations
|
||||
|
||||
[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
|
||||
661
LICENSE.AGPL
Normal file
661
LICENSE.AGPL
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
38
LICENSE.COMMERCIAL
Normal file
38
LICENSE.COMMERCIAL
Normal file
@ -0,0 +1,38 @@
|
||||
COMMERCIAL LICENSE AGREEMENT
|
||||
|
||||
Copyright (c) 2025 Kaj Kowalski
|
||||
|
||||
NOTICE: This software is dual-licensed. This commercial license applies only to those who have purchased a commercial license from Kaj Kowalski. If you have not purchased a commercial license, this software is licensed under the AGPL-3.0 license (see LICENSE.AGPL).
|
||||
|
||||
COMMERCIAL LICENSE TERMS
|
||||
|
||||
1. GRANT OF LICENSE
|
||||
Subject to the terms of this agreement and payment of applicable fees, Kaj Kowalski grants you a non-exclusive, non-transferable license to use, modify, and distribute this software for commercial purposes.
|
||||
|
||||
2. PERMITTED USES
|
||||
- Use the software in commercial applications
|
||||
- Modify the software for your internal use
|
||||
- Distribute the software as part of your commercial products
|
||||
- Use the software in SaaS applications without source code disclosure requirements
|
||||
|
||||
3. RESTRICTIONS
|
||||
- You may not sublicense, sell, or transfer this commercial license to third parties
|
||||
- You may not remove or alter any copyright notices
|
||||
- You may not reverse engineer the software beyond what is legally permitted
|
||||
|
||||
4. NO WARRANTY
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||
|
||||
5. LIMITATION OF LIABILITY
|
||||
IN NO EVENT SHALL Kaj Kowalski BE LIABLE FOR ANY DAMAGES ARISING FROM USE OF THIS SOFTWARE.
|
||||
|
||||
6. TERMINATION
|
||||
This license terminates automatically if you breach any terms. Upon termination, you must cease all use and destroy all copies.
|
||||
|
||||
7. CONTACT
|
||||
For commercial licensing inquiries, contact:
|
||||
Email: licensing@kjanat.com
|
||||
Website: github.com/kjanat
|
||||
|
||||
---
|
||||
To obtain a commercial license, please contact us at licensing@kjanat.com
|
||||
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/).
|
||||
400
README.md
400
README.md
@ -1,53 +1,391 @@
|
||||
# Owen
|
||||
# Owen Animation System
|
||||
|
||||
> Looking at this animation brief for Owen, I'll create a Three.js animation system that handles the state machine, animation clips, and transitions described in the document. This will use clean architecture principles with dependency injection and factory patterns.
|
||||
>
|
||||
> I've created a comprehensive Three.js animation system for Owen that implements the state machine and animation handling described in your brief. Here are the key architectural features:
|
||||
A comprehensive Three.js animation system for character state management with clean architecture principles, dependency injection, and factory patterns.
|
||||
|
||||
## **Clean Architecture Principles:**
|
||||
[](https://gitea.kajkowalski.nl/kjanat/Owen/issues)
|
||||
[](https://gitea.kajkowalski.nl/kjanat/Owen/pulls)
|
||||
[](https://gitea.kajkowalski.nl/kjanat/Owen/tags)
|
||||
|
||||
### **Dependency Injection:**
|
||||
## 🎯 Overview
|
||||
|
||||
- `OwenAnimationContext` receives its dependencies (mixer, factory, etc.) through constructor injection
|
||||
- State handlers are injected with the context they need
|
||||
The Owen Animation System is a sophisticated character animation framework built for Three.js that manages complex state machines, emotional responses, and animation transitions. It's designed with clean architecture principles to be maintainable, extensible, and testable.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- **🤖 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
|
||||
- **📝 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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16.0.0 or higher
|
||||
- Three.js compatible 3D model with animations (GLTF/GLB format recommended)
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://gitea.kajkowalski.nl/kjanat/Owen.git
|
||||
cd Owen
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Install dev dependencies
|
||||
npm install --include dev
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { OwenSystemFactory } from "owen";
|
||||
|
||||
// Load your 3D model
|
||||
const loader = new GLTFLoader();
|
||||
const gltf = await loader.loadAsync("path/to/your-model.gltf");
|
||||
|
||||
// Create a Three.js scene
|
||||
const scene = new THREE.Scene();
|
||||
scene.add(gltf.scene);
|
||||
|
||||
// Create the Owen animation system
|
||||
const owenSystem = await OwenSystemFactory.createOwenSystem(gltf, scene);
|
||||
|
||||
// Handle user messages
|
||||
await owenSystem.handleUserMessage("Hello Owen!");
|
||||
|
||||
// Update in your render loop
|
||||
function animate() {
|
||||
const deltaTime = clock.getDelta() * 1000; // Convert to milliseconds
|
||||
owenSystem.update(deltaTime);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Replace `path/to/your-model.gltf` with the actual path to your 3D character model. The system is designed to work with any GLTF model that follows the animation naming convention.
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```javascript
|
||||
import { OwenSystemFactory, States, Emotions, StateHandler } from "owen";
|
||||
|
||||
// Create custom state handler
|
||||
class CustomStateHandler extends StateHandler {
|
||||
async enter(fromState, emotion) {
|
||||
console.log(`Entering custom state from ${fromState}`);
|
||||
// Your custom logic here
|
||||
}
|
||||
|
||||
async exit(toState, emotion) {
|
||||
console.log(`Exiting custom state to ${toState}`);
|
||||
// Your custom logic here
|
||||
}
|
||||
}
|
||||
|
||||
// Register custom states
|
||||
const customStates = new Map();
|
||||
customStates.set("custom", CustomStateHandler);
|
||||
|
||||
// Create system with custom states
|
||||
const owenSystem = await OwenSystemFactory.createCustomOwenSystem(gltfModel, scene, customStates);
|
||||
|
||||
// Manual state transitions
|
||||
await owenSystem.transitionTo(States.REACTING, Emotions.HAPPY);
|
||||
```
|
||||
|
||||
## 🎨 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:
|
||||
|
||||
```txt
|
||||
[state]_[action]_[type]
|
||||
[state]_[action]2[toState]_[emotion]_T
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
- `wait_idle_L` - Wait state idle loop
|
||||
- `wait_quirk1_Q` - Wait state quirk animation
|
||||
- `react_angry2type_an_T` - Transition from react to type with angry emotion
|
||||
- `type_happy_L` - Type state with happy emotion loop
|
||||
- `sleep_wakeup_T` - Sleep wake up transition
|
||||
|
||||
### Animation Types
|
||||
|
||||
- `L` - Loop animation
|
||||
- `Q` - Quirk animation
|
||||
- `T` - Transition animation
|
||||
- `NL` - Nested loop
|
||||
- `NQ` - Nested quirk
|
||||
|
||||
### Emotions
|
||||
|
||||
- `an` - Angry
|
||||
- `sh` - Shocked
|
||||
- `ha` - Happy
|
||||
- `sa` - Sad
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### **Dependency Injection**
|
||||
|
||||
- `OwenAnimationContext` receives dependencies through constructor injection
|
||||
- State handlers are injected with required context
|
||||
- Animation loaders are injected into factories
|
||||
|
||||
### **Factory Patterns:**
|
||||
### **Factory Patterns**
|
||||
|
||||
- `AnimationClipFactory` - Creates animation clips with proper metadata parsing
|
||||
- `AnimationClipFactory` - Creates animation clips with metadata parsing
|
||||
- `StateFactory` - Creates state handlers dynamically
|
||||
- `OwenSystemFactory` - Main factory that assembles the entire system
|
||||
- `OwenSystemFactory` - Main factory that assembles the complete system
|
||||
|
||||
### **State Machine Implementation:**
|
||||
### **State Machine**
|
||||
|
||||
- Each state (`Wait`, `React`, `Type`, `Sleep`) has its own handler class
|
||||
- States manage their own entry/exit logic and transitions
|
||||
- Each state has its own handler class with entry/exit logic
|
||||
- States manage their own transitions and behaviors
|
||||
- Emotional transitions are handled with proper animation sequencing
|
||||
|
||||
## **Key Features:**
|
||||
## 📁 Project Structure
|
||||
|
||||
1. **Animation Naming Convention Parser** - Automatically parses the naming convention from your brief (e.g., `wait_idle_L`, `react_angry2type_an_T`)
|
||||
```sh
|
||||
Owen/
|
||||
├── src/
|
||||
│ ├── constants.js # Animation types, states, emotions
|
||||
│ ├── index.js # Main entry point
|
||||
│ ├── animation/
|
||||
│ │ └── AnimationClip.js # Core animation classes
|
||||
│ ├── core/
|
||||
│ │ └── OwenAnimationContext.js # Main system controller
|
||||
│ ├── factories/
|
||||
│ │ └── OwenSystemFactory.js # System factory
|
||||
│ ├── loaders/
|
||||
│ │ └── AnimationLoader.js # Animation loading interfaces
|
||||
│ └── states/
|
||||
│ ├── StateHandler.js # Base state handler
|
||||
│ ├── StateFactory.js # State factory
|
||||
│ ├── WaitStateHandler.js # Wait state implementation
|
||||
│ ├── ReactStateHandler.js # React state implementation
|
||||
│ ├── TypeStateHandler.js # Type state implementation
|
||||
│ └── SleepStateHandler.js # Sleep state implementation
|
||||
├── examples/
|
||||
│ ├── index.html # Demo HTML page
|
||||
│ └── basic-demo.js # Basic usage example
|
||||
├── package.json
|
||||
├── vite.config.js
|
||||
├── jsdoc.config.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
2. **Emotional State Management** - Handles emotional transitions like angry typing or shocked reactions
|
||||
## 🛠️ Development
|
||||
|
||||
3. **Nested Animation Support** - Supports the nested sequences described in your brief
|
||||
### Running the Development Server
|
||||
|
||||
4. **Activity Monitoring** - Automatically transitions to sleep after inactivity
|
||||
```bash
|
||||
# Start the development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. **Message Analysis** - Analyzes user messages to determine appropriate emotional responses
|
||||
This will start a Vite development server and open the basic demo at `http://localhost:3000`.
|
||||
|
||||
6. **Clean Separation of Concerns:**
|
||||
- Animation loading is separate from clip management
|
||||
- State logic is isolated in individual handlers
|
||||
- The main context orchestrates everything without tight coupling
|
||||
### Building for Production
|
||||
|
||||
## **Usage:**
|
||||
```bash
|
||||
# Build the project
|
||||
npm run build
|
||||
```
|
||||
|
||||
The system is designed to be easily extensible. You can:
|
||||
### Linting
|
||||
|
||||
- Add new states by creating new handler classes
|
||||
- Modify emotional analysis logic
|
||||
- Swap out animation loaders for different formats
|
||||
- Add new animation types by extending the factory
|
||||
```bash
|
||||
# Run ESLint
|
||||
npm run lint
|
||||
|
||||
The code follows the workflows described in your brief, handling the transitions between Wait → React → Type → Wait states, with proper emotional branching and nested animation support.
|
||||
# Fix linting issues automatically
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Generating Documentation
|
||||
|
||||
```bash
|
||||
# Generate JSDoc documentation
|
||||
npm run docs
|
||||
```
|
||||
|
||||
Documentation will be generated in the `docs/` directory.
|
||||
|
||||
### Project Scripts
|
||||
|
||||
- `npm run dev` - Start development server
|
||||
- `npm run build` - Build for production
|
||||
- `npm run preview` - Preview production build
|
||||
- `npm run lint` - Run StandardJS linting
|
||||
- `npm run lint:fix` - Fix StandardJS issues
|
||||
- `npm run docs` - Generate JSDoc documentation
|
||||
- `npm run format` - Format code with Prettier
|
||||
|
||||
## 🎮 Demo Controls
|
||||
|
||||
The basic demo includes these keyboard controls:
|
||||
|
||||
- **1** - Transition to Wait state
|
||||
- **2** - Transition to React state
|
||||
- **3** - Transition to Type state
|
||||
- **4** - Transition to Sleep state
|
||||
- **Space** - Send random test message
|
||||
- **Click** - Register user activity
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Customizing Emotions
|
||||
|
||||
You can extend the emotion system by modifying the message analysis:
|
||||
|
||||
```javascript
|
||||
import { ReactStateHandler } from "owen";
|
||||
|
||||
class CustomReactHandler extends ReactStateHandler {
|
||||
analyzeMessageEmotion(message) {
|
||||
// Your custom emotion analysis logic
|
||||
if (message.includes("excited")) {
|
||||
return Emotions.HAPPY;
|
||||
}
|
||||
return super.analyzeMessageEmotion(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adjusting Timing
|
||||
|
||||
Configure timing values in your application:
|
||||
|
||||
```javascript
|
||||
import { Config } from "owen";
|
||||
|
||||
// Modify default values
|
||||
Config.QUIRK_INTERVAL = 8000; // 8 seconds between quirks
|
||||
Config.INACTIVITY_TIMEOUT = 120000; // 2 minutes until sleep
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Animation not found" errors**
|
||||
|
||||
- Ensure your 3D model contains animations with the correct naming convention
|
||||
- Check that animations are properly exported in your GLTF file
|
||||
|
||||
2. **State transitions not working**
|
||||
|
||||
- Verify that transition animations exist in your model
|
||||
- Check console for error messages about missing clips
|
||||
|
||||
3. **Performance issues**
|
||||
|
||||
- Ensure you're calling `owenSystem.update()` in your render loop
|
||||
- Check that unused animations are properly disposed
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by opening browser console. The system logs state transitions and important events.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/new-feature`
|
||||
3. Commit your changes: `git commit -am 'Add new feature'`
|
||||
4. Push to the branch: `git push origin feature/new-feature`
|
||||
5. Submit a pull request
|
||||
|
||||
### Code Style
|
||||
|
||||
- Follow the existing ESLint configuration
|
||||
- Add JSDoc comments for all public methods
|
||||
- Write unit tests for new features
|
||||
- Maintain the existing architecture patterns
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is dual-licensed under your choice of:
|
||||
|
||||
- **Open Source/Non-Commercial Use**: AGPL-3.0 - see the [LICENSE.AGPL](LICENSE.AGPL) file for details.
|
||||
- **Commercial/Enterprise Use**: Commercial License - see the [LICENSE.COMMERCIAL](LICENSE.COMMERCIAL) file for details. Requires a paid commercial license. Please contact us at [email] for pricing and terms.
|
||||
|
||||
### Quick Guide
|
||||
|
||||
- ✅ Personal/educational use → Use under AGPL-3.0
|
||||
- ✅ Open source projects → Use under AGPL-3.0
|
||||
- ✅ Commercial/proprietary use → Purchase commercial license
|
||||
- ❌ SaaS without source disclosure → Purchase commercial license
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built with [Three.js][Three.js]
|
||||
- Inspired by modern character animation systems
|
||||
- Uses clean architecture principles from Robert C. Martin
|
||||
|
||||
<!-- LINK DEFINITIONS -->
|
||||
[Three.js]: https://threejs.org/ "Three.js - JavaScript 3D Library"
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
331
examples/basic-demo/basic-demo.js
Normal file
331
examples/basic-demo/basic-demo.js
Normal file
@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @fileoverview Basic example of using the Owen Animation System
|
||||
* @author Owen Animation System
|
||||
*/
|
||||
|
||||
import * as THREE from 'three'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { OwenSystemFactory, States } from '../../src/index.js'
|
||||
|
||||
/**
|
||||
* Basic Owen Animation System demo
|
||||
* @class
|
||||
*/
|
||||
class OwenDemo {
|
||||
/**
|
||||
* Create the demo
|
||||
*/
|
||||
constructor () {
|
||||
/**
|
||||
* The Three.js scene
|
||||
* @type {THREE.Scene}
|
||||
*/
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
/**
|
||||
* The Three.js camera
|
||||
* @type {THREE.PerspectiveCamera}
|
||||
*/
|
||||
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
|
||||
/**
|
||||
* The Three.js renderer
|
||||
* @type {THREE.WebGLRenderer}
|
||||
*/
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
|
||||
/**
|
||||
* The Owen animation system
|
||||
* @type {OwenAnimationContext|null}
|
||||
*/
|
||||
this.owenSystem = null
|
||||
|
||||
/**
|
||||
* Clock for tracking time
|
||||
* @type {THREE.Clock}
|
||||
*/
|
||||
this.clock = new THREE.Clock()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the demo
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async init () {
|
||||
// Setup renderer
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
this.renderer.setClearColor(0x1a1a1a)
|
||||
this.renderer.shadowMap.enabled = true
|
||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
document.body.appendChild(this.renderer.domElement)
|
||||
|
||||
// Setup camera
|
||||
this.camera.position.set(0, 1.6, 3)
|
||||
this.camera.lookAt(0, 1, 0)
|
||||
|
||||
// Add lighting
|
||||
this.setupLighting()
|
||||
|
||||
// Load Owen model (replace with your model path)
|
||||
await this.loadOwenModel()
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners()
|
||||
|
||||
// Start render loop
|
||||
this.animate()
|
||||
|
||||
console.log('Owen Demo initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup scene lighting
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
setupLighting () {
|
||||
// Ambient light
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.4)
|
||||
this.scene.add(ambientLight)
|
||||
|
||||
// Directional light
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
directionalLight.position.set(5, 10, 5)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.width = 2048
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
this.scene.add(directionalLight)
|
||||
|
||||
// Fill light
|
||||
const fillLight = new THREE.DirectionalLight(0x8bb7f0, 0.3)
|
||||
fillLight.position.set(-5, 5, -5)
|
||||
this.scene.add(fillLight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Owen character model
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadOwenModel () {
|
||||
try {
|
||||
const loader = new GLTFLoader()
|
||||
|
||||
// Replace 'path/to/owen.gltf' with your actual model path
|
||||
const gltf = await new Promise((resolve, reject) => {
|
||||
loader.load(
|
||||
'path/to/owen.gltf', // Update this path
|
||||
resolve,
|
||||
(progress) => console.log('Loading progress:', progress.loaded / progress.total * 100 + '%'),
|
||||
reject
|
||||
)
|
||||
})
|
||||
|
||||
const model = gltf.scene
|
||||
model.position.set(0, 0, 0)
|
||||
model.scale.setScalar(1)
|
||||
|
||||
// Enable shadows
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
|
||||
this.scene.add(model)
|
||||
|
||||
// Create Owen animation system
|
||||
this.owenSystem = await OwenSystemFactory.createOwenSystem(gltf, this.scene)
|
||||
|
||||
console.log('Owen model loaded and animation system created')
|
||||
this.logSystemInfo()
|
||||
} catch (error) {
|
||||
console.error('Error loading Owen model:', error)
|
||||
|
||||
// Create a placeholder cube for demo purposes
|
||||
this.createPlaceholderModel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a placeholder model for demo purposes
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
createPlaceholderModel () {
|
||||
const geometry = new THREE.BoxGeometry(1, 2, 1)
|
||||
const material = new THREE.MeshPhongMaterial({ color: 0x6699ff })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
cube.position.set(0, 1, 0)
|
||||
cube.castShadow = true
|
||||
cube.receiveShadow = true
|
||||
this.scene.add(cube)
|
||||
|
||||
console.log('Created placeholder model (cube)')
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for user interaction
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
setupEventListeners () {
|
||||
// Keyboard controls
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!this.owenSystem) return
|
||||
|
||||
switch (event.key) {
|
||||
case '1':
|
||||
this.owenSystem.transitionTo(States.WAITING)
|
||||
break
|
||||
case '2':
|
||||
this.owenSystem.transitionTo(States.REACTING)
|
||||
break
|
||||
case '3':
|
||||
this.owenSystem.transitionTo(States.TYPING)
|
||||
break
|
||||
case '4':
|
||||
this.owenSystem.transitionTo(States.SLEEPING)
|
||||
break
|
||||
case ' ':
|
||||
this.sendTestMessage()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Mouse interaction
|
||||
document.addEventListener('click', () => {
|
||||
if (this.owenSystem) {
|
||||
this.owenSystem.onUserActivity()
|
||||
}
|
||||
})
|
||||
|
||||
// Window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
|
||||
// Add instructions to the page
|
||||
this.addInstructions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add on-screen instructions
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
addInstructions () {
|
||||
const instructions = document.createElement('div')
|
||||
instructions.innerHTML = `
|
||||
<div style="position: absolute; top: 10px; left: 10px; color: white; ` +
|
||||
`font-family: monospace; font-size: 14px; line-height: 1.4;">
|
||||
<h3>Owen Animation System Demo</h3>
|
||||
<p><strong>Controls:</strong></p>
|
||||
<p>1 - Wait State</p>
|
||||
<p>2 - React State</p>
|
||||
<p>3 - Type State</p>
|
||||
<p>4 - Sleep State</p>
|
||||
<p>Space - Send Test Message</p>
|
||||
<p>Click - User Activity</p>
|
||||
<br>
|
||||
<p><strong>Current State:</strong> <span id="current-state">-</span></p>
|
||||
<p><strong>Available Transitions:</strong> <span id="transitions">-</span></p>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(instructions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test message to Owen
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
sendTestMessage () {
|
||||
if (!this.owenSystem) return
|
||||
|
||||
const testMessages = [
|
||||
'Hello Owen!',
|
||||
'How are you doing?',
|
||||
'This is urgent!',
|
||||
'Great work!',
|
||||
'Error in the system!',
|
||||
'I\'m feeling sad today'
|
||||
]
|
||||
|
||||
const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)]
|
||||
console.log(`Sending message: "${randomMessage}"`)
|
||||
this.owenSystem.handleUserMessage(randomMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log system information
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
logSystemInfo () {
|
||||
if (!this.owenSystem) return
|
||||
|
||||
console.log('=== Owen System Info ===')
|
||||
console.log('Available States:', this.owenSystem.getAvailableStates())
|
||||
console.log('Available Clips:', this.owenSystem.getAvailableClips())
|
||||
console.log('Current State:', this.owenSystem.getCurrentState())
|
||||
console.log('========================')
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI with current system state
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
updateUI () {
|
||||
if (!this.owenSystem) return
|
||||
|
||||
const currentStateElement = document.getElementById('current-state')
|
||||
const transitionsElement = document.getElementById('transitions')
|
||||
|
||||
if (currentStateElement) {
|
||||
currentStateElement.textContent = this.owenSystem.getCurrentState()
|
||||
}
|
||||
|
||||
if (transitionsElement) {
|
||||
transitionsElement.textContent = this.owenSystem.getAvailableTransitions().join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main animation loop
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
animate () {
|
||||
requestAnimationFrame(() => this.animate())
|
||||
|
||||
const deltaTime = this.clock.getDelta() * 1000 // Convert to milliseconds
|
||||
|
||||
// Update Owen system
|
||||
if (this.owenSystem) {
|
||||
this.owenSystem.update(deltaTime)
|
||||
}
|
||||
|
||||
// Update UI
|
||||
this.updateUI()
|
||||
|
||||
// Render scene
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the demo when the page loads
|
||||
window.addEventListener('load', async () => {
|
||||
const demo = new OwenDemo()
|
||||
try {
|
||||
await demo.init()
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Owen demo:', error)
|
||||
}
|
||||
})
|
||||
|
||||
export default OwenDemo
|
||||
45
examples/basic-demo/index.html
Normal file
45
examples/basic-demo/index.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!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 - Basic Demo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">Loading Owen Animation System...</div>
|
||||
|
||||
<script type="module">
|
||||
import "./basic-demo.js";
|
||||
|
||||
// Hide loading screen after a short delay
|
||||
setTimeout(() => {
|
||||
const loading = document.getElementById("loading");
|
||||
if (loading) {
|
||||
loading.classList.add("hidden");
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
183
examples/basic-demo/simple-example.js
Normal file
183
examples/basic-demo/simple-example.js
Normal file
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @fileoverview Simple usage example for Node.js environment
|
||||
* @author Owen Animation System
|
||||
*/
|
||||
|
||||
import { OwenSystemFactory, States } from '../../src/index.js'
|
||||
|
||||
/**
|
||||
* Simple example of using Owen Animation System
|
||||
* This example shows how to use the system without a browser environment
|
||||
*/
|
||||
class SimpleOwenExample {
|
||||
constructor () {
|
||||
this.owenSystem = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Owen system with a mock model
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async init () {
|
||||
try {
|
||||
// Create a mock GLTF model for demonstration
|
||||
const mockModel = this.createMockModel()
|
||||
|
||||
// Create the Owen system
|
||||
this.owenSystem = await OwenSystemFactory.createBasicOwenSystem(mockModel)
|
||||
|
||||
console.log('✅ Owen Animation System initialized successfully!')
|
||||
console.log('📊 System Info:')
|
||||
console.log(` Available States: ${this.owenSystem.getAvailableStates().join(', ')}`)
|
||||
console.log(` Current State: ${this.owenSystem.getCurrentState()}`)
|
||||
|
||||
// Run some example interactions
|
||||
await this.runExamples()
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize Owen system:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock 3D model for demonstration purposes
|
||||
* @returns {Object} Mock model object
|
||||
*/
|
||||
createMockModel () {
|
||||
return {
|
||||
animations: [
|
||||
{ name: 'wait_idle_L' },
|
||||
{ name: 'wait_quirk1_Q' },
|
||||
{ name: 'wait_quirk2_Q' },
|
||||
{ name: 'react_idle_L' },
|
||||
{ name: 'react_angry_Q' },
|
||||
{ name: 'react_happy_Q' },
|
||||
{ name: 'type_idle_L' },
|
||||
{ name: 'type_angry_L' },
|
||||
{ name: 'sleep_idle_L' },
|
||||
{ name: 'wait_2react_T' },
|
||||
{ name: 'react_2type_T' },
|
||||
{ name: 'type_2wait_T' },
|
||||
{ name: 'wait_2sleep_T' },
|
||||
{ name: 'sleep_2wait_T' }
|
||||
],
|
||||
scene: {},
|
||||
userData: {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run example interactions with the Owen system
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async runExamples () {
|
||||
console.log('\n🎬 Running example interactions...\n')
|
||||
|
||||
// Example 1: Basic state transitions
|
||||
console.log('📝 Example 1: Manual state transitions')
|
||||
await this.demonstrateStateTransitions()
|
||||
|
||||
// Example 2: Message handling
|
||||
console.log('\n📝 Example 2: Message handling with emotions')
|
||||
await this.demonstrateMessageHandling()
|
||||
|
||||
// Example 3: System update loop
|
||||
console.log('\n📝 Example 3: System update simulation')
|
||||
this.demonstrateUpdateLoop()
|
||||
|
||||
console.log('\n✨ All examples completed!')
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrate manual state transitions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async demonstrateStateTransitions () {
|
||||
const states = [States.REACTING, States.TYPING, States.WAITING, States.SLEEPING]
|
||||
|
||||
for (const state of states) {
|
||||
console.log(`🔄 Transitioning to ${state.toUpperCase()} state...`)
|
||||
await this.owenSystem.transitionTo(state)
|
||||
console.log(` ✓ Current state: ${this.owenSystem.getCurrentState()}`)
|
||||
console.log(` ✓ Available transitions: ${this.owenSystem.getAvailableTransitions().join(', ')}`)
|
||||
|
||||
// Simulate some time passing
|
||||
await this.sleep(500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrate message handling with emotional responses
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async demonstrateMessageHandling () {
|
||||
const messages = [
|
||||
{ text: 'Hello Owen!', expected: 'neutral response' },
|
||||
{ text: 'This is urgent!', expected: 'angry/urgent response' },
|
||||
{ text: 'Great work!', expected: 'happy response' },
|
||||
{ text: 'There\'s an error in the system', expected: 'shocked response' },
|
||||
{ text: 'I\'m feeling sad today', expected: 'sad response' }
|
||||
]
|
||||
|
||||
for (const message of messages) {
|
||||
console.log(`💬 Sending message: "${message.text}"`)
|
||||
console.log(` Expected: ${message.expected}`)
|
||||
|
||||
await this.owenSystem.handleUserMessage(message.text)
|
||||
console.log(` ✓ Current state after message: ${this.owenSystem.getCurrentState()}`)
|
||||
|
||||
await this.sleep(300)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrate the system update loop
|
||||
* @returns {void}
|
||||
*/
|
||||
demonstrateUpdateLoop () {
|
||||
console.log('⏱️ Simulating update loop for 3 seconds...')
|
||||
|
||||
let iterations = 0
|
||||
const startTime = Date.now()
|
||||
|
||||
const updateLoop = () => {
|
||||
const deltaTime = 16.67 // ~60 FPS
|
||||
this.owenSystem.update(deltaTime)
|
||||
iterations++
|
||||
|
||||
if (Date.now() - startTime < 3000) {
|
||||
setTimeout(updateLoop, 16)
|
||||
} else {
|
||||
console.log(` ✓ Completed ${iterations} update iterations`)
|
||||
console.log(` ✓ Final state: ${this.owenSystem.getCurrentState()}`)
|
||||
}
|
||||
}
|
||||
|
||||
updateLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple sleep utility for demonstrations
|
||||
* @param {number} ms - Milliseconds to sleep
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example if this file is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
console.log('🚀 Starting Owen Animation System Example\n')
|
||||
|
||||
const example = new SimpleOwenExample()
|
||||
example.init()
|
||||
.then(() => {
|
||||
console.log('\n🎉 Example completed successfully!')
|
||||
console.log('💡 Try modifying this example or check out the browser demo in examples/index.html')
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n💥 Example failed:', error)
|
||||
})
|
||||
}
|
||||
|
||||
export default SimpleOwenExample
|
||||
1532
examples/mock-demo/owen_test_demo.html
Normal file
1532
examples/mock-demo/owen_test_demo.html
Normal file
File diff suppressed because it is too large
Load Diff
23
jsdoc.config.json
Normal file
23
jsdoc.config.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"source": {
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"includePattern": "\\.(js|jsx)$",
|
||||
"exclude": [
|
||||
"node_modules/"
|
||||
]
|
||||
},
|
||||
"opts": {
|
||||
"destination": "./docs/",
|
||||
"recurse": true,
|
||||
"readme": "./README.md"
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"templates": {
|
||||
"cleverLinks": false,
|
||||
"monospaceLinks": false
|
||||
}
|
||||
}
|
||||
@ -1,873 +0,0 @@
|
||||
// Animation Clip Types
|
||||
const ClipTypes = {
|
||||
LOOP: 'L',
|
||||
QUIRK: 'Q',
|
||||
NESTED_LOOP: 'NL',
|
||||
NESTED_QUIRK: 'NQ',
|
||||
NESTED_IN: 'IN_NT',
|
||||
NESTED_OUT: 'OUT_NT',
|
||||
TRANSITION: 'T',
|
||||
};
|
||||
|
||||
// Character States
|
||||
const States = {
|
||||
WAIT: 'wait',
|
||||
REACT: 'react',
|
||||
TYPE: 'type',
|
||||
SLEEP: 'sleep',
|
||||
};
|
||||
|
||||
// Emotions
|
||||
const Emotions = {
|
||||
NEUTRAL: '',
|
||||
ANGRY: 'an',
|
||||
SHOCKED: 'sh',
|
||||
HAPPY: 'ha',
|
||||
SAD: 'sa',
|
||||
};
|
||||
|
||||
/**
|
||||
* Animation Clip Factory - Creates animation clips based on naming convention
|
||||
*/
|
||||
class AnimationClipFactory {
|
||||
constructor(animationLoader) {
|
||||
this.animationLoader = animationLoader;
|
||||
this.clipCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse animation name and create clip metadata
|
||||
* Format: [state]_[action]_[type] or [state]_[action]2[toState]_[emotion]_T
|
||||
*/
|
||||
parseAnimationName(name) {
|
||||
const parts = name.split('_');
|
||||
const state = parts[0];
|
||||
const action = parts[1];
|
||||
|
||||
// Handle transitions with emotions
|
||||
if (parts[2]?.includes('2') && parts[3] === ClipTypes.TRANSITION) {
|
||||
const [fromAction, toState] = parts[2].split('2');
|
||||
const emotion = parts[3] || Emotions.NEUTRAL;
|
||||
return {
|
||||
state,
|
||||
action: fromAction,
|
||||
toState,
|
||||
emotion,
|
||||
type: ClipTypes.TRANSITION,
|
||||
isEmotional: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle regular transitions
|
||||
if (parts[2] === ClipTypes.TRANSITION) {
|
||||
const [fromState, toState] = parts[1].split('2');
|
||||
return {
|
||||
state,
|
||||
action: fromState,
|
||||
toState,
|
||||
type: ClipTypes.TRANSITION,
|
||||
isEmotional: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle nested animations
|
||||
if (parts[2] === ClipTypes.NESTED_IN || parts[2] === ClipTypes.NESTED_OUT) {
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
type: parts[2],
|
||||
isNested: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle nested loops and quirks
|
||||
if (
|
||||
parts[3] === ClipTypes.NESTED_LOOP ||
|
||||
parts[3] === ClipTypes.NESTED_QUIRK
|
||||
) {
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
nestedAction: parts[2],
|
||||
type: parts[3],
|
||||
isNested: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle standard loops and quirks
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
type: parts[2],
|
||||
isStandard: true,
|
||||
};
|
||||
}
|
||||
|
||||
async createClip(name, model) {
|
||||
if (this.clipCache.has(name)) {
|
||||
return this.clipCache.get(name);
|
||||
}
|
||||
|
||||
const metadata = this.parseAnimationName(name);
|
||||
const animation = await this.animationLoader.loadAnimation(name);
|
||||
|
||||
const clip = new AnimationClip(name, animation, metadata);
|
||||
this.clipCache.set(name, clip);
|
||||
|
||||
return clip;
|
||||
}
|
||||
|
||||
async createClipsFromModel(model) {
|
||||
const clips = new Map();
|
||||
const animations = model.animations || [];
|
||||
|
||||
for (const animation of animations) {
|
||||
const clip = await this.createClip(animation.name, model);
|
||||
clips.set(animation.name, clip);
|
||||
}
|
||||
|
||||
return clips;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation Clip - Represents a single animation with metadata
|
||||
*/
|
||||
class AnimationClip {
|
||||
constructor(name, threeAnimation, metadata) {
|
||||
this.name = name;
|
||||
this.animation = threeAnimation;
|
||||
this.metadata = metadata;
|
||||
this.action = null;
|
||||
this.mixer = null;
|
||||
}
|
||||
|
||||
createAction(mixer) {
|
||||
this.mixer = mixer;
|
||||
this.action = mixer.clipAction(this.animation);
|
||||
|
||||
// Configure based on type
|
||||
if (
|
||||
this.metadata.type === ClipTypes.LOOP ||
|
||||
this.metadata.type === ClipTypes.NESTED_LOOP
|
||||
) {
|
||||
this.action.setLoop(THREE.LoopRepeat);
|
||||
} else {
|
||||
this.action.setLoop(THREE.LoopOnce);
|
||||
this.action.clampWhenFinished = true;
|
||||
}
|
||||
|
||||
return this.action;
|
||||
}
|
||||
|
||||
play(fadeInDuration = 0.3) {
|
||||
if (this.action) {
|
||||
this.action.reset();
|
||||
this.action.fadeIn(fadeInDuration);
|
||||
this.action.play();
|
||||
}
|
||||
}
|
||||
|
||||
stop(fadeOutDuration = 0.3) {
|
||||
if (this.action) {
|
||||
this.action.fadeOut(fadeOutDuration);
|
||||
}
|
||||
}
|
||||
|
||||
isPlaying() {
|
||||
return this.action?.isRunning() || false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State Handler Interface
|
||||
*/
|
||||
class StateHandler {
|
||||
constructor(stateName, context) {
|
||||
this.stateName = stateName;
|
||||
this.context = context;
|
||||
this.currentClip = null;
|
||||
this.nestedState = null;
|
||||
}
|
||||
|
||||
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
throw new Error('enter method must be implemented');
|
||||
}
|
||||
|
||||
async exit(toState = null, emotion = Emotions.NEUTRAL) {
|
||||
throw new Error('exit method must be implemented');
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
async handleMessage(message) {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait State Handler
|
||||
*/
|
||||
class WaitStateHandler extends StateHandler {
|
||||
constructor(context) {
|
||||
super(States.WAIT, context);
|
||||
this.idleClip = null;
|
||||
this.quirks = [];
|
||||
this.quirkTimer = 0;
|
||||
this.quirkInterval = 5000; // 5 seconds between quirks
|
||||
}
|
||||
|
||||
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering WAIT state from ${fromState}`);
|
||||
|
||||
// Play idle loop
|
||||
this.idleClip = this.context.getClip('wait_idle_L');
|
||||
if (this.idleClip) {
|
||||
await this.idleClip.play();
|
||||
}
|
||||
|
||||
// Collect available quirks
|
||||
this.quirks = this.context.getClipsByPattern('wait_*_Q');
|
||||
this.quirkTimer = 0;
|
||||
}
|
||||
|
||||
async exit(toState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting WAIT state to ${toState}`);
|
||||
|
||||
if (this.currentClip) {
|
||||
this.currentClip.stop();
|
||||
}
|
||||
|
||||
// Play transition if available
|
||||
const transitionName = `wait_2${toState}_T`;
|
||||
const transition = this.context.getClip(transitionName);
|
||||
if (transition) {
|
||||
await transition.play();
|
||||
await this.waitForClipEnd(transition);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.quirkTimer += deltaTime;
|
||||
|
||||
// Randomly play quirks
|
||||
if (this.quirkTimer > this.quirkInterval && Math.random() < 0.3) {
|
||||
this.playRandomQuirk();
|
||||
this.quirkTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async playRandomQuirk() {
|
||||
if (this.quirks.length === 0) return;
|
||||
|
||||
const quirk = this.quirks[Math.floor(Math.random() * this.quirks.length)];
|
||||
if (this.idleClip) {
|
||||
this.idleClip.stop(0.2);
|
||||
}
|
||||
|
||||
await quirk.play();
|
||||
await this.waitForClipEnd(quirk);
|
||||
|
||||
// Return to idle
|
||||
if (this.idleClip) {
|
||||
this.idleClip.play(0.2);
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return [States.REACT, States.SLEEP];
|
||||
}
|
||||
|
||||
async waitForClipEnd(clip) {
|
||||
return new Promise((resolve) => {
|
||||
const checkEnd = () => {
|
||||
if (!clip.isPlaying()) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkEnd);
|
||||
}
|
||||
};
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React State Handler
|
||||
*/
|
||||
class ReactStateHandler extends StateHandler {
|
||||
constructor(context) {
|
||||
super(States.REACT, context);
|
||||
this.emotion = Emotions.NEUTRAL;
|
||||
}
|
||||
|
||||
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering REACT state with emotion: ${emotion}`);
|
||||
this.emotion = emotion;
|
||||
|
||||
// Play appropriate reaction
|
||||
const reactionClip = this.context.getClip('react_idle_L');
|
||||
if (reactionClip) {
|
||||
await reactionClip.play();
|
||||
}
|
||||
}
|
||||
|
||||
async exit(toState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting REACT state to ${toState} with emotion: ${emotion}`);
|
||||
|
||||
if (this.currentClip) {
|
||||
this.currentClip.stop();
|
||||
}
|
||||
|
||||
// Play emotional transition if available
|
||||
let transitionName;
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
transitionName = `react_${this.emotion}2${toState}_${emotion}_T`;
|
||||
} else {
|
||||
transitionName = `react_2${toState}_T`;
|
||||
}
|
||||
|
||||
const transition = this.context.getClip(transitionName);
|
||||
if (transition) {
|
||||
await transition.play();
|
||||
await this.waitForClipEnd(transition);
|
||||
}
|
||||
}
|
||||
|
||||
async handleMessage(message) {
|
||||
// Analyze message sentiment to determine emotion
|
||||
const emotion = this.analyzeMessageEmotion(message);
|
||||
this.emotion = emotion;
|
||||
|
||||
// Play emotional reaction if needed
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
const emotionalClip = this.context.getClip(`react_${emotion}_L`);
|
||||
if (emotionalClip) {
|
||||
await emotionalClip.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzeMessageEmotion(message) {
|
||||
const text = message.toLowerCase();
|
||||
|
||||
if (
|
||||
text.includes('!') ||
|
||||
text.includes('urgent') ||
|
||||
text.includes('asap')
|
||||
) {
|
||||
return Emotions.SHOCKED;
|
||||
}
|
||||
if (
|
||||
text.includes('error') ||
|
||||
text.includes('problem') ||
|
||||
text.includes('issue')
|
||||
) {
|
||||
return Emotions.ANGRY;
|
||||
}
|
||||
if (
|
||||
text.includes('great') ||
|
||||
text.includes('awesome') ||
|
||||
text.includes('good')
|
||||
) {
|
||||
return Emotions.HAPPY;
|
||||
}
|
||||
|
||||
return Emotions.NEUTRAL;
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return [States.TYPE, States.WAIT];
|
||||
}
|
||||
|
||||
async waitForClipEnd(clip) {
|
||||
return new Promise((resolve) => {
|
||||
const checkEnd = () => {
|
||||
if (!clip.isPlaying()) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkEnd);
|
||||
}
|
||||
};
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type State Handler
|
||||
*/
|
||||
class TypeStateHandler extends StateHandler {
|
||||
constructor(context) {
|
||||
super(States.TYPE, context);
|
||||
this.emotion = Emotions.NEUTRAL;
|
||||
this.isTyping = false;
|
||||
}
|
||||
|
||||
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering TYPE state with emotion: ${emotion}`);
|
||||
this.emotion = emotion;
|
||||
this.isTyping = true;
|
||||
|
||||
// Play appropriate typing animation
|
||||
let typingClipName = 'type_idle_L';
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
typingClipName = `type_${emotion}_L`;
|
||||
}
|
||||
|
||||
const typingClip = this.context.getClip(typingClipName);
|
||||
if (typingClip) {
|
||||
this.currentClip = typingClip;
|
||||
await typingClip.play();
|
||||
}
|
||||
}
|
||||
|
||||
async exit(toState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting TYPE state to ${toState}`);
|
||||
this.isTyping = false;
|
||||
|
||||
if (this.currentClip) {
|
||||
this.currentClip.stop();
|
||||
}
|
||||
|
||||
// Play appropriate exit transition
|
||||
let transitionName;
|
||||
if (this.emotion !== Emotions.NEUTRAL) {
|
||||
transitionName = `type_${this.emotion}2${toState}_T`;
|
||||
} else {
|
||||
transitionName = `type_2${toState}_T`;
|
||||
}
|
||||
|
||||
const transition = this.context.getClip(transitionName);
|
||||
if (transition) {
|
||||
await transition.play();
|
||||
await this.waitForClipEnd(transition);
|
||||
}
|
||||
}
|
||||
|
||||
async finishTyping() {
|
||||
this.isTyping = false;
|
||||
|
||||
// Transition back to wait state
|
||||
return this.context.transitionTo(States.WAIT, this.emotion);
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return [States.WAIT];
|
||||
}
|
||||
|
||||
async waitForClipEnd(clip) {
|
||||
return new Promise((resolve) => {
|
||||
const checkEnd = () => {
|
||||
if (!clip.isPlaying()) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkEnd);
|
||||
}
|
||||
};
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep State Handler
|
||||
*/
|
||||
class SleepStateHandler extends StateHandler {
|
||||
constructor(context) {
|
||||
super(States.SLEEP, context);
|
||||
this.sleepDuration = 0;
|
||||
this.maxSleepDuration = 30000; // 30 seconds max sleep
|
||||
}
|
||||
|
||||
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering SLEEP state`);
|
||||
this.sleepDuration = 0;
|
||||
|
||||
const sleepClip = this.context.getClip('sleep_idle_L');
|
||||
if (sleepClip) {
|
||||
this.currentClip = sleepClip;
|
||||
await sleepClip.play();
|
||||
}
|
||||
}
|
||||
|
||||
async exit(toState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting SLEEP state to ${toState}`);
|
||||
|
||||
if (this.currentClip) {
|
||||
this.currentClip.stop();
|
||||
}
|
||||
|
||||
const transition = this.context.getClip(`sleep_2${toState}_T`);
|
||||
if (transition) {
|
||||
await transition.play();
|
||||
await this.waitForClipEnd(transition);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.sleepDuration += deltaTime;
|
||||
|
||||
// Wake up after max duration or on user activity
|
||||
if (this.sleepDuration > this.maxSleepDuration) {
|
||||
this.context.transitionTo(States.WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return [States.WAIT];
|
||||
}
|
||||
|
||||
async waitForClipEnd(clip) {
|
||||
return new Promise((resolve) => {
|
||||
const checkEnd = () => {
|
||||
if (!clip.isPlaying()) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkEnd);
|
||||
}
|
||||
};
|
||||
checkEnd();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State Factory - Creates state handlers using dependency injection
|
||||
*/
|
||||
class StateFactory {
|
||||
constructor() {
|
||||
this.stateHandlers = new Map();
|
||||
}
|
||||
|
||||
registerStateHandler(stateName, handlerClass) {
|
||||
this.stateHandlers.set(stateName, handlerClass);
|
||||
}
|
||||
|
||||
createStateHandler(stateName, context) {
|
||||
const HandlerClass = this.stateHandlers.get(stateName);
|
||||
if (!HandlerClass) {
|
||||
throw new Error(`Unknown state: ${stateName}`);
|
||||
}
|
||||
|
||||
return new HandlerClass(context);
|
||||
}
|
||||
|
||||
getAvailableStates() {
|
||||
return Array.from(this.stateHandlers.keys());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owen Animation Context - Main controller for the animation system
|
||||
*/
|
||||
class OwenAnimationContext {
|
||||
constructor(model, mixer, animationClipFactory, stateFactory) {
|
||||
this.model = model;
|
||||
this.mixer = mixer;
|
||||
this.animationClipFactory = animationClipFactory;
|
||||
this.stateFactory = stateFactory;
|
||||
|
||||
this.clips = new Map();
|
||||
this.states = new Map();
|
||||
this.currentState = null;
|
||||
this.currentStateName = null;
|
||||
|
||||
this.userActivityTimeout = null;
|
||||
this.lastActivityTime = Date.now();
|
||||
this.inactivityThreshold = 180000; // 3 minutes
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Load all animation clips
|
||||
this.clips = await this.animationClipFactory.createClipsFromModel(
|
||||
this.model
|
||||
);
|
||||
|
||||
// Create actions for all clips
|
||||
for (const clip of this.clips.values()) {
|
||||
clip.createAction(this.mixer);
|
||||
}
|
||||
|
||||
// Initialize state handlers
|
||||
this.initializeStates();
|
||||
|
||||
// Start in wait state
|
||||
await this.transitionTo(States.WAIT);
|
||||
|
||||
console.log('Owen Animation System initialized');
|
||||
}
|
||||
|
||||
initializeStates() {
|
||||
// Register state handlers
|
||||
this.stateFactory.registerStateHandler(States.WAIT, WaitStateHandler);
|
||||
this.stateFactory.registerStateHandler(States.REACT, ReactStateHandler);
|
||||
this.stateFactory.registerStateHandler(States.TYPE, TypeStateHandler);
|
||||
this.stateFactory.registerStateHandler(States.SLEEP, SleepStateHandler);
|
||||
|
||||
// Create state instances
|
||||
for (const stateName of this.stateFactory.getAvailableStates()) {
|
||||
const stateHandler = this.stateFactory.createStateHandler(
|
||||
stateName,
|
||||
this
|
||||
);
|
||||
this.states.set(stateName, stateHandler);
|
||||
}
|
||||
}
|
||||
|
||||
async transitionTo(newStateName, emotion = Emotions.NEUTRAL) {
|
||||
const newState = this.states.get(newStateName);
|
||||
if (!newState) {
|
||||
throw new Error(`Unknown state: ${newStateName}`);
|
||||
}
|
||||
|
||||
// Exit current state
|
||||
if (this.currentState) {
|
||||
await this.currentState.exit(newStateName, emotion);
|
||||
}
|
||||
|
||||
// Enter new state
|
||||
const fromState = this.currentStateName;
|
||||
this.currentState = newState;
|
||||
this.currentStateName = newStateName;
|
||||
|
||||
await this.currentState.enter(fromState, emotion);
|
||||
|
||||
// Reset activity timer
|
||||
this.resetActivityTimer();
|
||||
}
|
||||
|
||||
async handleUserMessage(message) {
|
||||
this.resetActivityTimer();
|
||||
|
||||
// Always go to react state first
|
||||
if (this.currentStateName !== States.REACT) {
|
||||
await this.transitionTo(States.REACT);
|
||||
}
|
||||
|
||||
// Let the react state handle the message
|
||||
await this.currentState.handleMessage(message);
|
||||
|
||||
// Transition to type state after a brief delay
|
||||
setTimeout(async () => {
|
||||
const emotion = this.currentState.emotion || Emotions.NEUTRAL;
|
||||
await this.transitionTo(States.TYPE, emotion);
|
||||
|
||||
// Simulate typing duration based on message length
|
||||
const typingDuration = Math.min(message.length * 100, 5000);
|
||||
setTimeout(async () => {
|
||||
await this.currentState.finishTyping();
|
||||
}, typingDuration);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onUserActivity() {
|
||||
this.resetActivityTimer();
|
||||
|
||||
// Wake up if sleeping
|
||||
if (this.currentStateName === States.SLEEP) {
|
||||
this.transitionTo(States.WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
resetActivityTimer() {
|
||||
this.lastActivityTime = Date.now();
|
||||
|
||||
if (this.userActivityTimeout) {
|
||||
clearTimeout(this.userActivityTimeout);
|
||||
}
|
||||
|
||||
this.userActivityTimeout = setTimeout(() => {
|
||||
this.handleInactivity();
|
||||
}, this.inactivityThreshold);
|
||||
}
|
||||
|
||||
handleInactivity() {
|
||||
if (this.currentStateName === States.WAIT) {
|
||||
this.transitionTo(States.SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Update mixer
|
||||
this.mixer.update(deltaTime);
|
||||
|
||||
// Update current state
|
||||
if (this.currentState) {
|
||||
this.currentState.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
getClip(name) {
|
||||
return this.clips.get(name);
|
||||
}
|
||||
|
||||
getClipsByPattern(pattern) {
|
||||
const regex = new RegExp(pattern.replace('*', '.*'));
|
||||
return Array.from(this.clips.values()).filter((clip) =>
|
||||
regex.test(clip.name)
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentState() {
|
||||
return this.currentStateName;
|
||||
}
|
||||
|
||||
getAvailableTransitions() {
|
||||
return this.currentState?.getAvailableTransitions() || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation Loader Interface - Loads animations from various sources
|
||||
*/
|
||||
class AnimationLoader {
|
||||
async loadAnimation(name) {
|
||||
throw new Error('loadAnimation method must be implemented');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GLTF Animation Loader - Loads animations from GLTF models
|
||||
*/
|
||||
class GLTFAnimationLoader extends AnimationLoader {
|
||||
constructor(gltfLoader) {
|
||||
super();
|
||||
this.gltfLoader = gltfLoader;
|
||||
this.animationCache = new Map();
|
||||
}
|
||||
|
||||
async loadAnimation(name) {
|
||||
if (this.animationCache.has(name)) {
|
||||
return this.animationCache.get(name);
|
||||
}
|
||||
|
||||
// In a real implementation, you would load the specific animation
|
||||
// For this mockup, we'll assume animations are already loaded in the model
|
||||
throw new Error(`Animation ${name} not found in model`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owen System Factory - Main factory for creating the complete Owen system
|
||||
*/
|
||||
class OwenSystemFactory {
|
||||
static async createOwenSystem(gltfModel, scene) {
|
||||
// Create Three.js mixer
|
||||
const mixer = new THREE.AnimationMixer(gltfModel);
|
||||
|
||||
// Create dependencies
|
||||
const gltfLoader = new THREE.GLTFLoader();
|
||||
const animationLoader = new GLTFAnimationLoader(gltfLoader);
|
||||
const animationClipFactory = new AnimationClipFactory(animationLoader);
|
||||
const stateFactory = new StateFactory();
|
||||
|
||||
// Create the main context
|
||||
const owenContext = new OwenAnimationContext(
|
||||
gltfModel,
|
||||
mixer,
|
||||
animationClipFactory,
|
||||
stateFactory
|
||||
);
|
||||
|
||||
// Initialize the system
|
||||
await owenContext.initialize();
|
||||
|
||||
// Add to scene
|
||||
scene.add(gltfModel);
|
||||
|
||||
return owenContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage Example
|
||||
class OwenDemo {
|
||||
constructor() {
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.owenSystem = null;
|
||||
this.clock = new THREE.Clock();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Setup Three.js scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
this.renderer = new THREE.WebGLRenderer();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(this.renderer.domElement);
|
||||
|
||||
// Load Owen model (mockup)
|
||||
const loader = new THREE.GLTFLoader();
|
||||
const gltf = await loader.loadAsync('path/to/owen-model.gltf');
|
||||
|
||||
// Create Owen system
|
||||
this.owenSystem = await OwenSystemFactory.createOwenSystem(
|
||||
gltf.scene,
|
||||
this.scene
|
||||
);
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Start render loop
|
||||
this.animate();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Mouse activity
|
||||
document.addEventListener('mousemove', () => {
|
||||
this.owenSystem.onUserActivity();
|
||||
});
|
||||
|
||||
// Simulate user messages
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const message = prompt('Send message to Owen:');
|
||||
if (message) {
|
||||
this.owenSystem.handleUserMessage(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
const deltaTime = this.clock.getDelta();
|
||||
|
||||
// Update Owen system
|
||||
if (this.owenSystem) {
|
||||
this.owenSystem.update(deltaTime);
|
||||
}
|
||||
|
||||
// Render scene
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the demo
|
||||
const demo = new OwenDemo();
|
||||
demo.init().catch(console.error);
|
||||
|
||||
export {
|
||||
OwenSystemFactory,
|
||||
OwenAnimationContext,
|
||||
StateFactory,
|
||||
AnimationClipFactory,
|
||||
States,
|
||||
Emotions,
|
||||
ClipTypes,
|
||||
};
|
||||
5767
package-lock.json
generated
Normal file
5767
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
Normal file
65
package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@kjanat/owen",
|
||||
"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",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"build": "vite build",
|
||||
"build:demo": "vite build --config vite.demo.config.js",
|
||||
"preview": "vite preview",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix",
|
||||
"docs": "jsdoc -c jsdoc.config.json",
|
||||
"format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'",
|
||||
"validate:animations": "node scripts/validate-animations.js",
|
||||
"generate:constants": "node scripts/generate-animation-constants.js",
|
||||
"check:conflicts": "node scripts/check-naming-conflicts.js",
|
||||
"test:schemes": "node scripts/test-multi-schemes.js",
|
||||
"animation:validate": "npm run validate:animations && npm run check:conflicts",
|
||||
"animation:generate": "npm run generate:constants && npm run validate:animations",
|
||||
"preview:demo": "vite preview --config vite.demo.config.js --port 3000",
|
||||
"test": "npx playwright test",
|
||||
"test:demo": "npx playwright test tests/demo.spec.js",
|
||||
"test:pages": "npx playwright test tests/pages.spec.js",
|
||||
"test:ui": "npx playwright test --ui",
|
||||
"test:headed": "npx playwright test --headed"
|
||||
},
|
||||
"keywords": [
|
||||
"three.js",
|
||||
"animation",
|
||||
"state-machine",
|
||||
"character",
|
||||
"gltf",
|
||||
"3d"
|
||||
],
|
||||
"author": "Kaj \"@kjanat\" Kowalski",
|
||||
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
||||
"dependencies": {
|
||||
"three": "^0.176.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"pre-commit": "^1.2.2",
|
||||
"standard": "*",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"requestAnimationFrame"
|
||||
]
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint:fix",
|
||||
"lint",
|
||||
"docs",
|
||||
"format"
|
||||
]
|
||||
}
|
||||
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 }
|
||||
257
src/animation/AnimationClip.js
Normal file
257
src/animation/AnimationClip.js
Normal file
@ -0,0 +1,257 @@
|
||||
/**
|
||||
* @fileoverview Core animation classes for clip management and creation
|
||||
* @module animation
|
||||
*/
|
||||
|
||||
import * as THREE from 'three'
|
||||
import { ClipTypes, Config } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Represents a single animation clip with metadata and Three.js action
|
||||
* @class
|
||||
*/
|
||||
export class AnimationClip {
|
||||
/**
|
||||
* Create an animation clip
|
||||
* @param {string} name - The name of the animation clip
|
||||
* @param {THREE.AnimationClip} threeAnimation - The Three.js animation clip
|
||||
* @param {Object} metadata - Parsed metadata from animation name
|
||||
*/
|
||||
constructor (name, threeAnimation, metadata) {
|
||||
/**
|
||||
* The name of the animation clip
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = name
|
||||
|
||||
/**
|
||||
* The Three.js animation clip
|
||||
* @type {THREE.AnimationClip}
|
||||
*/
|
||||
this.animation = threeAnimation
|
||||
|
||||
/**
|
||||
* Parsed metadata about the animation
|
||||
* @type {Object}
|
||||
*/
|
||||
this.metadata = metadata
|
||||
|
||||
/**
|
||||
* The Three.js animation action
|
||||
* @type {THREE.AnimationAction|null}
|
||||
*/
|
||||
this.action = null
|
||||
|
||||
/**
|
||||
* The animation mixer
|
||||
* @type {THREE.AnimationMixer|null}
|
||||
*/
|
||||
this.mixer = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure a Three.js action for this clip
|
||||
* @param {THREE.AnimationMixer} mixer - The animation mixer
|
||||
* @returns {THREE.AnimationAction} The created action
|
||||
*/
|
||||
createAction (mixer) {
|
||||
this.mixer = mixer
|
||||
this.action = mixer.clipAction(this.animation)
|
||||
|
||||
// Configure based on type
|
||||
if (
|
||||
this.metadata.type === ClipTypes.LOOP ||
|
||||
this.metadata.type === ClipTypes.NESTED_LOOP
|
||||
) {
|
||||
this.action.setLoop(THREE.LoopRepeat, Infinity)
|
||||
} else {
|
||||
this.action.setLoop(THREE.LoopOnce)
|
||||
this.action.clampWhenFinished = true
|
||||
}
|
||||
|
||||
return this.action
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the animation with optional fade in
|
||||
* @param {number} [fadeInDuration=0.3] - Fade in duration in seconds
|
||||
* @returns {Promise<void>} Promise that resolves when fade in completes
|
||||
*/
|
||||
play (fadeInDuration = Config.DEFAULT_FADE_IN) {
|
||||
if (this.action) {
|
||||
this.action.reset()
|
||||
this.action.fadeIn(fadeInDuration)
|
||||
this.action.play()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the animation with optional fade out
|
||||
* @param {number} [fadeOutDuration=0.3] - Fade out duration in seconds
|
||||
* @returns {Promise<void>} Promise that resolves when fade out completes
|
||||
*/
|
||||
stop (fadeOutDuration = Config.DEFAULT_FADE_OUT) {
|
||||
if (this.action) {
|
||||
this.action.fadeOut(fadeOutDuration)
|
||||
setTimeout(() => {
|
||||
if (this.action) {
|
||||
this.action.stop()
|
||||
}
|
||||
}, fadeOutDuration * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the animation is currently playing
|
||||
* @returns {boolean} True if playing, false otherwise
|
||||
*/
|
||||
isPlaying () {
|
||||
return this.action?.isRunning() || false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating animation clips with parsed metadata
|
||||
* @class
|
||||
*/
|
||||
export class AnimationClipFactory {
|
||||
/**
|
||||
* Create an animation clip factory
|
||||
* @param {AnimationLoader} animationLoader - The animation loader instance
|
||||
*/
|
||||
constructor (animationLoader) {
|
||||
/**
|
||||
* The animation loader for loading animation data
|
||||
* @type {AnimationLoader}
|
||||
*/
|
||||
this.animationLoader = animationLoader
|
||||
|
||||
/**
|
||||
* Cache for created animation clips
|
||||
* @type {Map<string, AnimationClip>}
|
||||
*/
|
||||
this.clipCache = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse animation name and create clip metadata
|
||||
* Format: [state]_[action]_[type] or [state]_[action]2[toState]_[emotion]_T
|
||||
* @param {string} name - The animation name to parse
|
||||
* @returns {Object} Parsed metadata object
|
||||
*/
|
||||
parseAnimationName (name) {
|
||||
const parts = name.split('_')
|
||||
const state = parts[0]
|
||||
const action = parts[1]
|
||||
|
||||
// Handle transitions with emotions
|
||||
if (parts[2]?.includes('2') && parts[3] === ClipTypes.TRANSITION) {
|
||||
const [, toState] = parts[2].split('2')
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
toState,
|
||||
emotion: parts[2] || '',
|
||||
type: ClipTypes.TRANSITION,
|
||||
isTransition: true,
|
||||
hasEmotion: true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular transitions
|
||||
if (parts[2] === ClipTypes.TRANSITION) {
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
type: ClipTypes.TRANSITION,
|
||||
isTransition: true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle nested animations
|
||||
if (parts[2] === ClipTypes.NESTED_IN || parts[2] === ClipTypes.NESTED_OUT) {
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
type: parts[2],
|
||||
nestedType: parts[3],
|
||||
isNested: true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle nested loops and quirks
|
||||
if (
|
||||
parts[3] === ClipTypes.NESTED_LOOP ||
|
||||
parts[3] === ClipTypes.NESTED_QUIRK
|
||||
) {
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
subAction: parts[2],
|
||||
type: parts[3],
|
||||
isNested: true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle standard loops and quirks
|
||||
return {
|
||||
state,
|
||||
action,
|
||||
type: parts[2],
|
||||
isStandard: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an animation clip from a name
|
||||
* @param {string} name - The animation name
|
||||
* @returns {Promise<AnimationClip>} The created animation clip
|
||||
*/
|
||||
async createClip (name) {
|
||||
if (this.clipCache.has(name)) {
|
||||
return this.clipCache.get(name)
|
||||
}
|
||||
|
||||
const metadata = this.parseAnimationName(name)
|
||||
const animation = await this.animationLoader.loadAnimation(name)
|
||||
|
||||
const clip = new AnimationClip(name, animation, metadata)
|
||||
this.clipCache.set(name, clip)
|
||||
|
||||
return clip
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all animation clips from a model's animations
|
||||
* @param {THREE.Object3D} model - The 3D model containing animations
|
||||
* @returns {Promise<Map<string, AnimationClip>>} Map of animation name to clip
|
||||
*/
|
||||
async createClipsFromModel (model) {
|
||||
const clips = new Map()
|
||||
const animations = model.animations || []
|
||||
|
||||
for (const animation of animations) {
|
||||
const clip = await this.createClip(animation.name, model)
|
||||
clips.set(animation.name, clip)
|
||||
}
|
||||
|
||||
return clips
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the clip cache
|
||||
* @returns {void}
|
||||
*/
|
||||
clearCache () {
|
||||
this.clipCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached clip by name
|
||||
* @param {string} name - The animation name
|
||||
* @returns {AnimationClip|undefined} The cached clip or undefined
|
||||
*/
|
||||
getCachedClip (name) {
|
||||
return this.clipCache.get(name)
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
78
src/constants.js
Normal file
78
src/constants.js
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Animation system constants and enumerations
|
||||
* @module constants
|
||||
*/
|
||||
|
||||
/**
|
||||
* Animation clip types based on naming convention
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const ClipTypes = {
|
||||
/** Loop animation */
|
||||
LOOP: 'L',
|
||||
/** Quirk animation */
|
||||
QUIRK: 'Q',
|
||||
/** Nested loop animation */
|
||||
NESTED_LOOP: 'NL',
|
||||
/** Nested quirk animation */
|
||||
NESTED_QUIRK: 'NQ',
|
||||
/** Nested in transition */
|
||||
NESTED_IN: 'IN_NT',
|
||||
/** Nested out transition */
|
||||
NESTED_OUT: 'OUT_NT',
|
||||
/** Transition animation */
|
||||
TRANSITION: 'T'
|
||||
}
|
||||
|
||||
/**
|
||||
* Character animation states
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const States = {
|
||||
/** Waiting/idle state */
|
||||
WAITING: 'wait',
|
||||
/** Reacting to input state */
|
||||
REACTING: 'react',
|
||||
/** Typing response state */
|
||||
TYPING: 'type',
|
||||
/** Sleep/inactive state */
|
||||
SLEEPING: 'sleep'
|
||||
}
|
||||
|
||||
/**
|
||||
* Character emotional states
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Emotions = {
|
||||
/** Neutral emotion */
|
||||
NEUTRAL: '',
|
||||
/** Angry emotion */
|
||||
ANGRY: 'an',
|
||||
/** Shocked emotion */
|
||||
SHOCKED: 'sh',
|
||||
/** Happy emotion */
|
||||
HAPPY: 'ha',
|
||||
/** Sad emotion */
|
||||
SAD: 'sa'
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration values
|
||||
* @readonly
|
||||
* @type {Object}
|
||||
*/
|
||||
export const Config = {
|
||||
/** Default fade in duration for animations (ms) */
|
||||
DEFAULT_FADE_IN: 0.3,
|
||||
/** Default fade out duration for animations (ms) */
|
||||
DEFAULT_FADE_OUT: 0.3,
|
||||
/** Default quirk interval (ms) */
|
||||
QUIRK_INTERVAL: 5000,
|
||||
/** Default inactivity timeout (ms) */
|
||||
INACTIVITY_TIMEOUT: 60000,
|
||||
/** Quirk probability threshold */
|
||||
QUIRK_PROBABILITY: 0.3
|
||||
}
|
||||
433
src/core/OwenAnimationContext.js
Normal file
433
src/core/OwenAnimationContext.js
Normal file
@ -0,0 +1,433 @@
|
||||
/**
|
||||
* @fileoverview Main animation context controller
|
||||
* @module core
|
||||
*/
|
||||
|
||||
import { States, Emotions, Config } from '../constants.js'
|
||||
import { AnimationNameMapper } from '../animation/AnimationNameMapper.js'
|
||||
|
||||
/**
|
||||
* Main controller for the Owen animation system
|
||||
* Manages state transitions, animation playback, and user interactions
|
||||
* @class
|
||||
*/
|
||||
export class OwenAnimationContext {
|
||||
/**
|
||||
* Create an Owen animation context
|
||||
* @param {THREE.Object3D} model - The 3D character model
|
||||
* @param {THREE.AnimationMixer} mixer - The Three.js animation mixer
|
||||
* @param {AnimationClipFactory} animationClipFactory - Factory for creating clips
|
||||
* @param {StateFactory} stateFactory - Factory for creating state handlers
|
||||
*/
|
||||
constructor (model, mixer, animationClipFactory, stateFactory) {
|
||||
/**
|
||||
* The 3D character model
|
||||
* @type {THREE.Object3D}
|
||||
*/
|
||||
this.model = model
|
||||
|
||||
/**
|
||||
* The Three.js animation mixer
|
||||
* @type {THREE.AnimationMixer}
|
||||
*/
|
||||
this.mixer = mixer
|
||||
|
||||
/**
|
||||
* Factory for creating animation clips
|
||||
* @type {AnimationClipFactory}
|
||||
*/
|
||||
this.animationClipFactory = animationClipFactory
|
||||
|
||||
/**
|
||||
* Factory for creating state handlers
|
||||
* @type {StateFactory}
|
||||
*/
|
||||
this.stateFactory = stateFactory
|
||||
|
||||
/**
|
||||
* Multi-scheme animation name mapper
|
||||
* @type {AnimationNameMapper}
|
||||
*/
|
||||
this.nameMapper = new AnimationNameMapper()
|
||||
|
||||
/**
|
||||
* Map of animation clips by name
|
||||
* @type {Map<string, AnimationClip>}
|
||||
*/
|
||||
this.clips = new Map()
|
||||
|
||||
/**
|
||||
* Map of state handlers by name
|
||||
* @type {Map<string, StateHandler>}
|
||||
*/
|
||||
this.states = new Map()
|
||||
|
||||
/**
|
||||
* Current active state
|
||||
* @type {string}
|
||||
*/
|
||||
this.currentState = States.WAITING
|
||||
|
||||
/**
|
||||
* Current active state handler
|
||||
* @type {StateHandler|null}
|
||||
*/
|
||||
this.currentStateHandler = null
|
||||
|
||||
/**
|
||||
* Timer for inactivity detection
|
||||
* @type {number}
|
||||
*/
|
||||
this.inactivityTimer = 0
|
||||
|
||||
/**
|
||||
* Inactivity timeout in milliseconds
|
||||
* @type {number}
|
||||
*/
|
||||
this.inactivityTimeout = Config.INACTIVITY_TIMEOUT
|
||||
|
||||
/**
|
||||
* Whether the system is initialized
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the animation system
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initialize () {
|
||||
if (this.initialized) return
|
||||
|
||||
// Create animation clips from model
|
||||
this.clips = await this.animationClipFactory.createClipsFromModel(this.model)
|
||||
|
||||
// Create actions for all clips
|
||||
for (const [, clip] of this.clips) {
|
||||
clip.createAction(this.mixer)
|
||||
}
|
||||
|
||||
// Initialize state handlers
|
||||
this.initializeStates()
|
||||
|
||||
// Start in wait state
|
||||
await this.transitionTo(States.WAITING)
|
||||
|
||||
this.initialized = true
|
||||
console.log('Owen Animation System initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all state handlers
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
initializeStates () {
|
||||
const stateNames = this.stateFactory.getAvailableStates()
|
||||
|
||||
for (const stateName of stateNames) {
|
||||
const handler = this.stateFactory.createStateHandler(stateName, this)
|
||||
this.states.set(stateName, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to a new state
|
||||
* @param {string} newStateName - The name of the state to transition to
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion for the transition
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If state is not found or transition is invalid
|
||||
*/
|
||||
async transitionTo (newStateName, emotion = Emotions.NEUTRAL) {
|
||||
if (!this.states.has(newStateName)) {
|
||||
throw new Error(`State '${newStateName}' not found`)
|
||||
}
|
||||
|
||||
const oldState = this.currentState
|
||||
const newStateHandler = this.states.get(newStateName)
|
||||
|
||||
console.log(`Transitioning from ${oldState} to ${newStateName}`)
|
||||
|
||||
// Exit current state
|
||||
if (this.currentStateHandler) {
|
||||
await this.currentStateHandler.exit(newStateName, emotion)
|
||||
}
|
||||
|
||||
// Enter new state
|
||||
this.currentState = newStateName
|
||||
this.currentStateHandler = newStateHandler
|
||||
await this.currentStateHandler.enter(oldState, emotion)
|
||||
|
||||
// Reset inactivity timer
|
||||
this.resetActivityTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a user message
|
||||
* @param {string} message - The user message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleUserMessage (message) {
|
||||
console.log(`Handling user message: "${message}"`)
|
||||
|
||||
this.onUserActivity()
|
||||
|
||||
// If sleeping, wake up first
|
||||
if (this.currentState === States.SLEEPING) {
|
||||
await this.transitionTo(States.REACTING)
|
||||
}
|
||||
|
||||
// Let current state handle the message
|
||||
if (this.currentStateHandler) {
|
||||
await this.currentStateHandler.handleMessage(message)
|
||||
}
|
||||
|
||||
// Transition to appropriate next state based on current state
|
||||
if (this.currentState === States.WAITING) {
|
||||
await this.transitionTo(States.REACTING)
|
||||
} else if (this.currentState === States.REACTING) {
|
||||
await this.transitionTo(States.TYPING)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user activity is detected
|
||||
* @returns {void}
|
||||
*/
|
||||
onUserActivity () {
|
||||
this.resetActivityTimer()
|
||||
|
||||
// Wake up if sleeping
|
||||
if (this.currentState === States.SLEEPING) {
|
||||
this.transitionTo(States.WAITING)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the inactivity timer
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
resetActivityTimer () {
|
||||
this.inactivityTimer = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inactivity timeout
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleInactivity () {
|
||||
console.log('Inactivity detected, transitioning to sleep')
|
||||
await this.transitionTo(States.SLEEPING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the animation system (call every frame)
|
||||
* @param {number} deltaTime - Time elapsed since last update (ms)
|
||||
* @returns {void}
|
||||
*/
|
||||
update (deltaTime) {
|
||||
if (!this.initialized) return
|
||||
|
||||
// Update Three.js mixer
|
||||
this.mixer.update(deltaTime / 1000) // Convert to seconds
|
||||
|
||||
// Update current state
|
||||
if (this.currentStateHandler) {
|
||||
this.currentStateHandler.update(deltaTime)
|
||||
}
|
||||
|
||||
// Update inactivity timer
|
||||
this.inactivityTimer += deltaTime
|
||||
if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
|
||||
this.handleInactivity()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Get animation clips matching a pattern
|
||||
* @param {string} pattern - Pattern to match (supports * wildcards)
|
||||
* @returns {AnimationClip[]} Array of matching clips
|
||||
*/
|
||||
getClipsByPattern (pattern) {
|
||||
const regex = new RegExp(pattern.replace(/\*/g, '.*'))
|
||||
const matches = []
|
||||
|
||||
for (const [name, clip] of this.clips) {
|
||||
if (regex.test(name)) {
|
||||
matches.push(clip)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
getCurrentState () {
|
||||
return this.currentState
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state handler
|
||||
* @returns {StateHandler|null} The current state handler
|
||||
*/
|
||||
getCurrentStateHandler () {
|
||||
return this.currentStateHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from current state
|
||||
* @returns {string[]} Array of available state transitions
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
if (this.currentStateHandler) {
|
||||
return this.currentStateHandler.getAvailableTransitions()
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available animation clip names
|
||||
* @returns {string[]} Array of clip names
|
||||
*/
|
||||
getAvailableClips () {
|
||||
return Array.from(this.clips.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available state names
|
||||
* @returns {string[]} Array of state names
|
||||
*/
|
||||
getAvailableStates () {
|
||||
return Array.from(this.states.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the animation system and clean up resources
|
||||
* @returns {void}
|
||||
*/
|
||||
dispose () {
|
||||
// Stop all animations
|
||||
for (const [, clip] of this.clips) {
|
||||
if (clip.action) {
|
||||
clip.action.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Clear caches
|
||||
this.clips.clear()
|
||||
this.states.clear()
|
||||
this.animationClipFactory.clearCache()
|
||||
|
||||
this.initialized = false
|
||||
console.log('Owen Animation System disposed')
|
||||
}
|
||||
}
|
||||
91
src/factories/OwenSystemFactory.js
Normal file
91
src/factories/OwenSystemFactory.js
Normal file
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @fileoverview Main system factory for creating the complete Owen system
|
||||
* @module factories
|
||||
*/
|
||||
|
||||
import * as THREE from 'three'
|
||||
import { OwenAnimationContext } from '../core/OwenAnimationContext.js'
|
||||
import { AnimationClipFactory } from '../animation/AnimationClip.js'
|
||||
import { GLTFAnimationLoader } from '../loaders/AnimationLoader.js'
|
||||
import { StateFactory } from '../states/StateFactory.js'
|
||||
|
||||
/**
|
||||
* Main factory for creating the complete Owen animation system
|
||||
* @class
|
||||
*/
|
||||
export class OwenSystemFactory {
|
||||
/**
|
||||
* Create a complete Owen animation system
|
||||
* @param {THREE.Object3D} gltfModel - The loaded GLTF model
|
||||
* @param {THREE.Scene} scene - The Three.js scene
|
||||
* @param {Object} [options={}] - Configuration options
|
||||
* @param {THREE.GLTFLoader} [options.gltfLoader] - Custom GLTF loader
|
||||
* @returns {Promise<OwenAnimationContext>} The configured Owen system
|
||||
*/
|
||||
static async createOwenSystem (gltfModel, scene, options = {}) {
|
||||
// Create Three.js animation mixer
|
||||
const mixer = new THREE.AnimationMixer(gltfModel)
|
||||
|
||||
// Create GLTF loader if not provided
|
||||
const gltfLoader = options.gltfLoader || new THREE.GLTFLoader()
|
||||
|
||||
// Create animation loader
|
||||
const animationLoader = new GLTFAnimationLoader(gltfLoader)
|
||||
|
||||
// Preload animations from the model
|
||||
await animationLoader.preloadAnimations(gltfModel)
|
||||
|
||||
// Create animation clip factory
|
||||
const animationClipFactory = new AnimationClipFactory(animationLoader)
|
||||
|
||||
// Create state factory
|
||||
const stateFactory = new StateFactory()
|
||||
|
||||
// Create the main Owen context
|
||||
const owenContext = new OwenAnimationContext(
|
||||
gltfModel,
|
||||
mixer,
|
||||
animationClipFactory,
|
||||
stateFactory
|
||||
)
|
||||
|
||||
// Initialize the system
|
||||
await owenContext.initialize()
|
||||
|
||||
return owenContext
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic Owen system with minimal configuration
|
||||
* @param {THREE.Object3D} model - The 3D model
|
||||
* @returns {Promise<OwenAnimationContext>} The configured Owen system
|
||||
*/
|
||||
static async createBasicOwenSystem (model) {
|
||||
const scene = new THREE.Scene()
|
||||
scene.add(model)
|
||||
|
||||
return await OwenSystemFactory.createOwenSystem(model, scene)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Owen system with custom state handlers
|
||||
* @param {THREE.Object3D} gltfModel - The loaded GLTF model
|
||||
* @param {THREE.Scene} scene - The Three.js scene
|
||||
* @param {Map<string, Function>} customStates - Map of state name to handler class
|
||||
* @returns {Promise<OwenAnimationContext>} The configured Owen system
|
||||
*/
|
||||
static async createCustomOwenSystem (gltfModel, scene, customStates) {
|
||||
const system = await OwenSystemFactory.createOwenSystem(gltfModel, scene)
|
||||
|
||||
// Register custom state handlers
|
||||
const stateFactory = system.stateFactory
|
||||
for (const [stateName, handlerClass] of customStates) {
|
||||
stateFactory.registerStateHandler(stateName, handlerClass)
|
||||
}
|
||||
|
||||
// Reinitialize with custom states
|
||||
system.initializeStates()
|
||||
|
||||
return system
|
||||
}
|
||||
}
|
||||
222
src/index.d.ts
vendored
Normal file
222
src/index.d.ts
vendored
Normal file
@ -0,0 +1,222 @@
|
||||
// Type definitions for Owen Animation System
|
||||
// Project: Owen Animation System
|
||||
// Definitions by: Owen Animation System
|
||||
|
||||
export as namespace Owen;
|
||||
|
||||
// Constants
|
||||
export const ClipTypes: {
|
||||
readonly LOOP: 'L';
|
||||
readonly QUIRK: 'Q';
|
||||
readonly NESTED_LOOP: 'NL';
|
||||
readonly NESTED_QUIRK: 'NQ';
|
||||
readonly NESTED_IN: 'IN_NT';
|
||||
readonly NESTED_OUT: 'OUT_NT';
|
||||
readonly TRANSITION: 'T';
|
||||
};
|
||||
|
||||
export const States: {
|
||||
readonly WAITING: 'wait';
|
||||
readonly REACTING: 'react';
|
||||
readonly TYPING: 'type';
|
||||
readonly SLEEPING: 'sleep';
|
||||
};
|
||||
|
||||
export const Emotions: {
|
||||
readonly NEUTRAL: '';
|
||||
readonly ANGRY: 'an';
|
||||
readonly SHOCKED: 'sh';
|
||||
readonly HAPPY: 'ha';
|
||||
readonly SAD: 'sa';
|
||||
};
|
||||
|
||||
export const Config: {
|
||||
DEFAULT_FADE_IN: number;
|
||||
DEFAULT_FADE_OUT: number;
|
||||
QUIRK_INTERVAL: number;
|
||||
INACTIVITY_TIMEOUT: number;
|
||||
QUIRK_PROBABILITY: number;
|
||||
};
|
||||
|
||||
// Interfaces
|
||||
export interface AnimationMetadata {
|
||||
state: string;
|
||||
action: string;
|
||||
type: string;
|
||||
toState?: string;
|
||||
emotion?: string;
|
||||
isTransition?: boolean;
|
||||
hasEmotion?: boolean;
|
||||
isNested?: boolean;
|
||||
isStandard?: boolean;
|
||||
subAction?: string;
|
||||
nestedType?: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class AnimationClip {
|
||||
constructor(name: string, threeAnimation: any, metadata: AnimationMetadata);
|
||||
|
||||
readonly name: string;
|
||||
readonly animation: any;
|
||||
readonly metadata: AnimationMetadata;
|
||||
action: any | null;
|
||||
mixer: any | null;
|
||||
|
||||
createAction(mixer: any): any;
|
||||
play(fadeInDuration?: number): Promise<void>;
|
||||
stop(fadeOutDuration?: number): Promise<void>;
|
||||
isPlaying(): boolean;
|
||||
}
|
||||
|
||||
export class AnimationClipFactory {
|
||||
constructor(animationLoader: AnimationLoader);
|
||||
|
||||
parseAnimationName(name: string): AnimationMetadata;
|
||||
createClip(name: string, model: any): Promise<AnimationClip>;
|
||||
createClipsFromModel(model: any): Promise<Map<string, AnimationClip>>;
|
||||
clearCache(): void;
|
||||
getCachedClip(name: string): AnimationClip | undefined;
|
||||
}
|
||||
|
||||
export abstract class AnimationLoader {
|
||||
abstract loadAnimation(name: string): Promise<any>;
|
||||
}
|
||||
|
||||
export class GLTFAnimationLoader extends AnimationLoader {
|
||||
constructor(gltfLoader: any);
|
||||
|
||||
loadAnimation(name: string): Promise<any>;
|
||||
preloadAnimations(gltfModel: any): Promise<void>;
|
||||
clearCache(): void;
|
||||
getCachedAnimationNames(): string[];
|
||||
}
|
||||
|
||||
export abstract class StateHandler {
|
||||
constructor(stateName: string, context: OwenAnimationContext);
|
||||
|
||||
readonly stateName: string;
|
||||
readonly context: OwenAnimationContext;
|
||||
currentClip: AnimationClip | null;
|
||||
nestedState: any | null;
|
||||
|
||||
abstract enter(fromState?: string | null, emotion?: string): Promise<void>;
|
||||
abstract exit(toState?: string | null, emotion?: string): Promise<void>;
|
||||
update(deltaTime: number): void;
|
||||
handleMessage(message: string): Promise<void>;
|
||||
getAvailableTransitions(): string[];
|
||||
protected waitForClipEnd(clip: AnimationClip): Promise<void>;
|
||||
protected stopCurrentClip(fadeOutDuration?: number): Promise<void>;
|
||||
}
|
||||
|
||||
export class WaitStateHandler extends StateHandler {
|
||||
constructor(context: OwenAnimationContext);
|
||||
|
||||
enter(fromState?: string | null, emotion?: string): Promise<void>;
|
||||
exit(toState?: string | null, emotion?: string): Promise<void>;
|
||||
update(deltaTime: number): void;
|
||||
getAvailableTransitions(): string[];
|
||||
}
|
||||
|
||||
export class ReactStateHandler extends StateHandler {
|
||||
constructor(context: OwenAnimationContext);
|
||||
|
||||
enter(fromState?: string | null, emotion?: string): Promise<void>;
|
||||
exit(toState?: string | null, emotion?: string): Promise<void>;
|
||||
handleMessage(message: string): Promise<void>;
|
||||
getAvailableTransitions(): string[];
|
||||
}
|
||||
|
||||
export class TypeStateHandler extends StateHandler {
|
||||
constructor(context: OwenAnimationContext);
|
||||
|
||||
enter(fromState?: string | null, emotion?: string): Promise<void>;
|
||||
exit(toState?: string | null, emotion?: string): Promise<void>;
|
||||
finishTyping(): Promise<void>;
|
||||
getAvailableTransitions(): string[];
|
||||
getIsTyping(): boolean;
|
||||
setTyping(typing: boolean): void;
|
||||
}
|
||||
|
||||
export class SleepStateHandler extends StateHandler {
|
||||
constructor(context: OwenAnimationContext);
|
||||
|
||||
enter(fromState?: string | null, emotion?: string): Promise<void>;
|
||||
exit(toState?: string | null, emotion?: string): Promise<void>;
|
||||
update(deltaTime: number): void;
|
||||
handleMessage(message: string): Promise<void>;
|
||||
getAvailableTransitions(): string[];
|
||||
isInDeepSleep(): boolean;
|
||||
wakeUp(): Promise<void>;
|
||||
}
|
||||
|
||||
export class StateFactory {
|
||||
constructor();
|
||||
|
||||
registerStateHandler(stateName: string, handlerClass: new (context: OwenAnimationContext) => StateHandler): void;
|
||||
createStateHandler(stateName: string, context: OwenAnimationContext): StateHandler;
|
||||
getAvailableStates(): string[];
|
||||
isStateRegistered(stateName: string): boolean;
|
||||
unregisterStateHandler(stateName: string): boolean;
|
||||
}
|
||||
|
||||
export class OwenAnimationContext {
|
||||
constructor(
|
||||
model: any,
|
||||
mixer: any,
|
||||
animationClipFactory: AnimationClipFactory,
|
||||
stateFactory: StateFactory
|
||||
);
|
||||
|
||||
readonly model: any;
|
||||
readonly mixer: any;
|
||||
readonly animationClipFactory: AnimationClipFactory;
|
||||
readonly stateFactory: StateFactory;
|
||||
readonly clips: Map<string, AnimationClip>;
|
||||
readonly states: Map<string, StateHandler>;
|
||||
currentState: string;
|
||||
currentStateHandler: StateHandler | null;
|
||||
initialized: boolean;
|
||||
|
||||
initialize(): Promise<void>;
|
||||
transitionTo(newStateName: string, emotion?: string): Promise<void>;
|
||||
handleUserMessage(message: string): Promise<void>;
|
||||
onUserActivity(): void;
|
||||
update(deltaTime: number): void;
|
||||
getClip(name: string): AnimationClip | undefined;
|
||||
getClipsByPattern(pattern: string): AnimationClip[];
|
||||
getCurrentState(): string;
|
||||
getCurrentStateHandler(): StateHandler | null;
|
||||
getAvailableTransitions(): string[];
|
||||
getAvailableClips(): string[];
|
||||
getAvailableStates(): string[];
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class OwenSystemFactory {
|
||||
static createOwenSystem(
|
||||
gltfModel: any,
|
||||
scene: any,
|
||||
options?: { gltfLoader?: any; }
|
||||
): Promise<OwenAnimationContext>;
|
||||
|
||||
static createBasicOwenSystem(model: any): Promise<OwenAnimationContext>;
|
||||
|
||||
static createCustomOwenSystem(
|
||||
gltfModel: any,
|
||||
scene: any,
|
||||
customStates: Map<string, new (context: OwenAnimationContext) => StateHandler>
|
||||
): Promise<OwenAnimationContext>;
|
||||
}
|
||||
|
||||
// Default export
|
||||
declare const Owen: {
|
||||
OwenSystemFactory: typeof OwenSystemFactory;
|
||||
OwenAnimationContext: typeof OwenAnimationContext;
|
||||
States: typeof States;
|
||||
Emotions: typeof Emotions;
|
||||
ClipTypes: typeof ClipTypes;
|
||||
Config: typeof Config;
|
||||
};
|
||||
|
||||
export default Owen;
|
||||
58
src/index.js
Normal file
58
src/index.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @fileoverview Main entry point for the Owen Animation System
|
||||
* @module owen
|
||||
*/
|
||||
|
||||
// Core exports
|
||||
// Import for default export
|
||||
import { OwenSystemFactory } from './factories/OwenSystemFactory.js'
|
||||
import { OwenAnimationContext } from './core/OwenAnimationContext.js'
|
||||
import { States, Emotions, ClipTypes, Config } from './constants.js'
|
||||
|
||||
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'
|
||||
|
||||
// State system exports
|
||||
export { StateHandler } from './states/StateHandler.js'
|
||||
export { WaitStateHandler } from './states/WaitStateHandler.js'
|
||||
export { ReactStateHandler } from './states/ReactStateHandler.js'
|
||||
export { TypeStateHandler } from './states/TypeStateHandler.js'
|
||||
export { SleepStateHandler } from './states/SleepStateHandler.js'
|
||||
export { StateFactory } from './states/StateFactory.js'
|
||||
|
||||
// Factory exports
|
||||
export { OwenSystemFactory } from './factories/OwenSystemFactory.js'
|
||||
|
||||
// Constants exports
|
||||
export { ClipTypes, States, Emotions, Config } from './constants.js'
|
||||
|
||||
/**
|
||||
* Default export - the main factory for easy usage
|
||||
*/
|
||||
export default {
|
||||
OwenSystemFactory,
|
||||
OwenAnimationContext,
|
||||
States,
|
||||
Emotions,
|
||||
ClipTypes,
|
||||
Config
|
||||
}
|
||||
94
src/loaders/AnimationLoader.js
Normal file
94
src/loaders/AnimationLoader.js
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @fileoverview Animation loader interfaces and implementations
|
||||
* @module loaders
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract base class for animation loaders
|
||||
* @abstract
|
||||
* @class
|
||||
*/
|
||||
export class AnimationLoader {
|
||||
/**
|
||||
* Load an animation by name
|
||||
* @abstract
|
||||
* @param {string} _name - The animation name to load (unused in base class)
|
||||
* @returns {Promise<THREE.AnimationClip>} The loaded animation clip
|
||||
* @throws {Error} Must be implemented by subclasses
|
||||
*/
|
||||
async loadAnimation (_name) {
|
||||
throw new Error('loadAnimation method must be implemented by subclasses')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GLTF animation loader implementation
|
||||
* @class
|
||||
* @extends AnimationLoader
|
||||
*/
|
||||
export class GLTFAnimationLoader extends AnimationLoader {
|
||||
/**
|
||||
* Create a GLTF animation loader
|
||||
* @param {THREE.GLTFLoader} gltfLoader - The Three.js GLTF loader instance
|
||||
*/
|
||||
constructor (gltfLoader) {
|
||||
super()
|
||||
|
||||
/**
|
||||
* The Three.js GLTF loader
|
||||
* @type {THREE.GLTFLoader}
|
||||
*/
|
||||
this.gltfLoader = gltfLoader
|
||||
|
||||
/**
|
||||
* Cache for loaded animations
|
||||
* @type {Map<string, THREE.AnimationClip>}
|
||||
*/
|
||||
this.animationCache = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an animation from GLTF by name
|
||||
* @param {string} name - The animation name to load
|
||||
* @returns {Promise<THREE.AnimationClip>} The loaded animation clip
|
||||
* @throws {Error} If animation is not found
|
||||
*/
|
||||
async loadAnimation (name) {
|
||||
if (this.animationCache.has(name)) {
|
||||
return this.animationCache.get(name)
|
||||
}
|
||||
|
||||
// In a real implementation, this would load from GLTF files
|
||||
// For now, we'll assume animations are already loaded in the model
|
||||
throw new Error(`Animation '${name}' not found. Implement GLTF loading logic.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload animations from a GLTF model
|
||||
* @param {Object} gltfModel - The loaded GLTF model
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async preloadAnimations (gltfModel) {
|
||||
if (gltfModel.animations) {
|
||||
for (const animation of gltfModel.animations) {
|
||||
this.animationCache.set(animation.name, animation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the animation cache
|
||||
* @returns {void}
|
||||
*/
|
||||
clearCache () {
|
||||
this.animationCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached animation names
|
||||
* @returns {string[]} Array of cached animation names
|
||||
*/
|
||||
getCachedAnimationNames () {
|
||||
return Array.from(this.animationCache.keys())
|
||||
}
|
||||
}
|
||||
159
src/states/ReactStateHandler.js
Normal file
159
src/states/ReactStateHandler.js
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @fileoverview React state handler implementation
|
||||
* @module states
|
||||
*/
|
||||
|
||||
import { StateHandler } from './StateHandler.js'
|
||||
import { States, Emotions } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Handler for the React state
|
||||
* @class
|
||||
* @extends StateHandler
|
||||
*/
|
||||
export class ReactStateHandler extends StateHandler {
|
||||
/**
|
||||
* Create a react state handler
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
*/
|
||||
constructor (context) {
|
||||
super(States.REACTING, context)
|
||||
|
||||
/**
|
||||
* Current emotional state
|
||||
* @type {string}
|
||||
*/
|
||||
this.emotion = Emotions.NEUTRAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the react state
|
||||
* @param {string|null} [_fromState=null] - The previous state (unused)
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering REACTING state with emotion: ${emotion}`)
|
||||
this.emotion = emotion
|
||||
|
||||
// Play appropriate reaction
|
||||
const reactionClip = this.context.getClip('react_idle_L')
|
||||
if (reactionClip) {
|
||||
await reactionClip.play()
|
||||
this.currentClip = reactionClip
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit the react state
|
||||
* @param {string|null} [toState=null] - The next state
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async exit (toState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`)
|
||||
|
||||
if (this.currentClip) {
|
||||
await this.stopCurrentClip()
|
||||
}
|
||||
|
||||
// Play emotional transition if available
|
||||
let transitionName
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
transitionName = `react_${this.emotion}2${toState}_${emotion}_T`
|
||||
} else {
|
||||
transitionName = `react_2${toState}_T`
|
||||
}
|
||||
|
||||
const transition = this.context.getClip(transitionName)
|
||||
if (transition) {
|
||||
await transition.play()
|
||||
await this.waitForClipEnd(transition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a user message in react state
|
||||
* @param {string} message - The user message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleMessage (message) {
|
||||
// Analyze message sentiment to determine emotion
|
||||
const emotion = this.analyzeMessageEmotion(message)
|
||||
this.emotion = emotion
|
||||
|
||||
// Play emotional reaction if needed
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
const emotionalReaction = this.context.getClip(`react_${emotion}_Q`)
|
||||
if (emotionalReaction) {
|
||||
if (this.currentClip) {
|
||||
await this.stopCurrentClip(0.2)
|
||||
}
|
||||
await emotionalReaction.play()
|
||||
await this.waitForClipEnd(emotionalReaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze message to determine emotional response
|
||||
* @private
|
||||
* @param {string} message - The message to analyze
|
||||
* @returns {string} The determined emotion
|
||||
*/
|
||||
analyzeMessageEmotion (message) {
|
||||
const text = message.toLowerCase()
|
||||
|
||||
// Check for urgent/angry indicators
|
||||
if (
|
||||
text.includes('!') ||
|
||||
text.includes('urgent') ||
|
||||
text.includes('asap') ||
|
||||
text.includes('hurry')
|
||||
) {
|
||||
return Emotions.ANGRY
|
||||
}
|
||||
|
||||
// Check for error/shocked indicators
|
||||
if (
|
||||
text.includes('error') ||
|
||||
text.includes('problem') ||
|
||||
text.includes('issue') ||
|
||||
text.includes('bug') ||
|
||||
text.includes('broken')
|
||||
) {
|
||||
return Emotions.SHOCKED
|
||||
}
|
||||
|
||||
// Check for positive/happy indicators
|
||||
if (
|
||||
text.includes('great') ||
|
||||
text.includes('awesome') ||
|
||||
text.includes('good') ||
|
||||
text.includes('excellent') ||
|
||||
text.includes('perfect')
|
||||
) {
|
||||
return Emotions.HAPPY
|
||||
}
|
||||
|
||||
// Check for sad indicators
|
||||
if (
|
||||
text.includes('sad') ||
|
||||
text.includes('disappointed') ||
|
||||
text.includes('failed') ||
|
||||
text.includes('wrong')
|
||||
) {
|
||||
return Emotions.SAD
|
||||
}
|
||||
|
||||
return Emotions.NEUTRAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from react state
|
||||
* @returns {string[]} Array of available state transitions
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
return [States.TYPING, States.WAITING]
|
||||
}
|
||||
}
|
||||
140
src/states/SleepStateHandler.js
Normal file
140
src/states/SleepStateHandler.js
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @fileoverview Sleep state handler implementation
|
||||
* @module states
|
||||
*/
|
||||
|
||||
import { StateHandler } from './StateHandler.js'
|
||||
import { States, Emotions } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Handler for the Sleep state
|
||||
* @class
|
||||
* @extends StateHandler
|
||||
*/
|
||||
export class SleepStateHandler extends StateHandler {
|
||||
/**
|
||||
* Create a sleep state handler
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
*/
|
||||
constructor (context) {
|
||||
super(States.SLEEPING, context)
|
||||
|
||||
/**
|
||||
* Sleep animation clip
|
||||
* @type {AnimationClip|null}
|
||||
*/
|
||||
this.sleepClip = null
|
||||
|
||||
/**
|
||||
* Whether the character is in deep sleep
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isDeepSleep = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the sleep state
|
||||
* @param {string|null} [fromState=null] - The previous state
|
||||
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to enter with (unused)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering SLEEPING state from ${fromState}`)
|
||||
|
||||
// Play sleep transition if available
|
||||
const sleepTransition = this.context.getClip('wait_2sleep_T')
|
||||
if (sleepTransition) {
|
||||
await sleepTransition.play()
|
||||
await this.waitForClipEnd(sleepTransition)
|
||||
}
|
||||
|
||||
// Start sleep loop
|
||||
this.sleepClip = this.context.getClip('sleep_idle_L')
|
||||
if (this.sleepClip) {
|
||||
await this.sleepClip.play()
|
||||
this.currentClip = this.sleepClip
|
||||
}
|
||||
|
||||
this.isDeepSleep = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit the sleep state
|
||||
* @param {string|null} [toState=null] - The next state
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting SLEEPING state to ${toState}`)
|
||||
this.isDeepSleep = false
|
||||
|
||||
if (this.currentClip) {
|
||||
await this.stopCurrentClip()
|
||||
}
|
||||
|
||||
// Play wake up animation
|
||||
const wakeUpClip = this.context.getClip('sleep_wakeup_T')
|
||||
if (wakeUpClip) {
|
||||
await wakeUpClip.play()
|
||||
await this.waitForClipEnd(wakeUpClip)
|
||||
}
|
||||
|
||||
// Play transition to next state if available
|
||||
const transitionName = `sleep_2${toState}_T`
|
||||
const transition = this.context.getClip(transitionName)
|
||||
if (transition) {
|
||||
await transition.play()
|
||||
await this.waitForClipEnd(transition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sleep state
|
||||
* @param {number} _deltaTime - Time elapsed since last update (ms, unused)
|
||||
* @returns {void}
|
||||
*/
|
||||
update (_deltaTime) {
|
||||
// Sleep state doesn't need regular updates
|
||||
// Character remains asleep until external stimulus
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a user message in sleep state (wake up)
|
||||
* @param {string} _message - The user message (unused, just triggers wake up)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleMessage (_message) {
|
||||
// Any message should wake up the character
|
||||
if (this.isDeepSleep) {
|
||||
console.log('Waking up due to user message')
|
||||
// This will trigger a state transition to REACTING
|
||||
await this.context.transitionTo(States.REACTING)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from sleep state
|
||||
* @returns {string[]} Array of available state transitions
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
return [States.WAITING, States.REACTING]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if in deep sleep
|
||||
* @returns {boolean} True if in deep sleep, false otherwise
|
||||
*/
|
||||
isInDeepSleep () {
|
||||
return this.isDeepSleep
|
||||
}
|
||||
|
||||
/**
|
||||
* Force wake up from sleep
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async wakeUp () {
|
||||
if (this.isDeepSleep) {
|
||||
await this.context.transitionTo(States.WAITING)
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/states/StateFactory.js
Normal file
86
src/states/StateFactory.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @fileoverview State factory for creating state handlers
|
||||
* @module states
|
||||
*/
|
||||
|
||||
import { WaitStateHandler } from './WaitStateHandler.js'
|
||||
import { ReactStateHandler } from './ReactStateHandler.js'
|
||||
import { TypeStateHandler } from './TypeStateHandler.js'
|
||||
import { SleepStateHandler } from './SleepStateHandler.js'
|
||||
import { States } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Factory for creating state handlers using dependency injection
|
||||
* @class
|
||||
*/
|
||||
export class StateFactory {
|
||||
/**
|
||||
* Create a state factory
|
||||
*/
|
||||
constructor () {
|
||||
/**
|
||||
* Registry of state handler classes
|
||||
* @type {Map<string, Function>}
|
||||
* @private
|
||||
*/
|
||||
this.stateHandlers = new Map()
|
||||
|
||||
// Register default state handlers
|
||||
this.registerStateHandler(States.WAITING, WaitStateHandler)
|
||||
this.registerStateHandler(States.REACTING, ReactStateHandler)
|
||||
this.registerStateHandler(States.TYPING, TypeStateHandler)
|
||||
this.registerStateHandler(States.SLEEPING, SleepStateHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a state handler class
|
||||
* @param {string} stateName - The name of the state
|
||||
* @param {Function} handlerClass - The handler class constructor
|
||||
* @returns {void}
|
||||
*/
|
||||
registerStateHandler (stateName, handlerClass) {
|
||||
this.stateHandlers.set(stateName, handlerClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a state handler instance
|
||||
* @param {string} stateName - The name of the state
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
* @returns {StateHandler} The created state handler
|
||||
* @throws {Error} If state handler is not registered
|
||||
*/
|
||||
createStateHandler (stateName, context) {
|
||||
const HandlerClass = this.stateHandlers.get(stateName)
|
||||
if (!HandlerClass) {
|
||||
throw new Error(`No handler registered for state: ${stateName}`)
|
||||
}
|
||||
|
||||
return new HandlerClass(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available state names
|
||||
* @returns {string[]} Array of registered state names
|
||||
*/
|
||||
getAvailableStates () {
|
||||
return Array.from(this.stateHandlers.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a state is registered
|
||||
* @param {string} stateName - The state name to check
|
||||
* @returns {boolean} True if registered, false otherwise
|
||||
*/
|
||||
isStateRegistered (stateName) {
|
||||
return this.stateHandlers.has(stateName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a state handler
|
||||
* @param {string} stateName - The state name to unregister
|
||||
* @returns {boolean} True if removed, false if not found
|
||||
*/
|
||||
unregisterStateHandler (stateName) {
|
||||
return this.stateHandlers.delete(stateName)
|
||||
}
|
||||
}
|
||||
126
src/states/StateHandler.js
Normal file
126
src/states/StateHandler.js
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @fileoverview Base state handler class and utilities
|
||||
* @module StateHandler
|
||||
*/
|
||||
|
||||
import { Emotions, Config } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Abstract base class for state handlers
|
||||
* @abstract
|
||||
* @class
|
||||
*/
|
||||
export class StateHandler {
|
||||
/**
|
||||
* Create a state handler
|
||||
* @param {string} stateName - The name of the state
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
*/
|
||||
constructor (stateName, context) {
|
||||
/**
|
||||
* The name of this state
|
||||
* @type {string}
|
||||
*/
|
||||
this.stateName = stateName
|
||||
|
||||
/**
|
||||
* The animation context
|
||||
* @type {OwenAnimationContext}
|
||||
*/
|
||||
this.context = context
|
||||
|
||||
/**
|
||||
* Currently playing animation clip
|
||||
* @type {AnimationClip|null}
|
||||
*/
|
||||
this.currentClip = null
|
||||
|
||||
/**
|
||||
* Nested state information
|
||||
* @type {Object|null}
|
||||
*/
|
||||
this.nestedState = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter this state
|
||||
* @abstract
|
||||
* @param {string|null} [_fromState=null] - The previous state (unused in base class)
|
||||
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to enter with (unused in base class)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} Must be implemented by subclasses
|
||||
*/
|
||||
async enter (_fromState = null, _emotion = Emotions.NEUTRAL) {
|
||||
throw new Error('enter method must be implemented by subclasses')
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit this state
|
||||
* @abstract
|
||||
* @param {string|null} [_toState=null] - The next state (unused in base class)
|
||||
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to exit with (unused in base class)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} Must be implemented by subclasses
|
||||
*/
|
||||
async exit (_toState = null, _emotion = Emotions.NEUTRAL) {
|
||||
throw new Error('exit method must be implemented by subclasses')
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this state (called every frame)
|
||||
* @param {number} _deltaTime - Time elapsed since last update (ms, unused in base class)
|
||||
* @returns {void}
|
||||
*/
|
||||
update (_deltaTime) {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a user message while in this state
|
||||
* @param {string} _message - The user message (unused in base class)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleMessage (_message) {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from this state
|
||||
* @returns {string[]} Array of state names that can be transitioned to
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an animation clip to finish playing
|
||||
* @protected
|
||||
* @param {AnimationClip} clip - The animation clip to wait for
|
||||
* @returns {Promise<void>} Promise that resolves when the clip finishes
|
||||
*/
|
||||
async waitForClipEnd (clip) {
|
||||
return new Promise((resolve) => {
|
||||
const checkFinished = () => {
|
||||
if (!clip.isPlaying()) {
|
||||
resolve()
|
||||
} else {
|
||||
requestAnimationFrame(checkFinished)
|
||||
}
|
||||
}
|
||||
checkFinished()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the currently playing clip
|
||||
* @protected
|
||||
* @param {number} [fadeOutDuration] - Fade out duration
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stopCurrentClip (fadeOutDuration = Config.DEFAULT_FADE_OUT) {
|
||||
if (this.currentClip) {
|
||||
await this.currentClip.stop(fadeOutDuration)
|
||||
this.currentClip = null
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/states/TypeStateHandler.js
Normal file
128
src/states/TypeStateHandler.js
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @fileoverview Type state handler implementation
|
||||
* @module states
|
||||
*/
|
||||
|
||||
import { StateHandler } from './StateHandler.js'
|
||||
import { States, Emotions } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Handler for the Type state
|
||||
* @class
|
||||
* @extends StateHandler
|
||||
*/
|
||||
export class TypeStateHandler extends StateHandler {
|
||||
/**
|
||||
* Create a type state handler
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
*/
|
||||
constructor (context) {
|
||||
super(States.TYPING, context)
|
||||
|
||||
/**
|
||||
* Current emotional state
|
||||
* @type {string}
|
||||
*/
|
||||
this.emotion = Emotions.NEUTRAL
|
||||
|
||||
/**
|
||||
* Whether currently typing
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isTyping = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the type state
|
||||
* @param {string|null} [_fromState=null] - The previous state (unused)
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering TYPING state with emotion: ${emotion}`)
|
||||
this.emotion = emotion
|
||||
this.isTyping = true
|
||||
|
||||
// Play appropriate typing animation
|
||||
let typingClipName = 'type_idle_L'
|
||||
if (emotion !== Emotions.NEUTRAL) {
|
||||
typingClipName = `type_${emotion}_L`
|
||||
}
|
||||
|
||||
const typingClip = this.context.getClip(typingClipName)
|
||||
if (typingClip) {
|
||||
await typingClip.play()
|
||||
this.currentClip = typingClip
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit the type state
|
||||
* @param {string|null} [toState=null] - The next state
|
||||
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to exit with (unused)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting TYPING state to ${toState}`)
|
||||
this.isTyping = false
|
||||
|
||||
if (this.currentClip) {
|
||||
await this.stopCurrentClip()
|
||||
}
|
||||
|
||||
// Play transition if available
|
||||
let transitionName = `type_2${toState}_T`
|
||||
if (this.emotion !== Emotions.NEUTRAL) {
|
||||
transitionName = `type_${this.emotion}2${toState}_T`
|
||||
}
|
||||
|
||||
const transition = this.context.getClip(transitionName)
|
||||
if (transition) {
|
||||
await transition.play()
|
||||
await this.waitForClipEnd(transition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish typing and prepare to transition
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async finishTyping () {
|
||||
if (!this.isTyping) return
|
||||
|
||||
// Play typing finish animation if available
|
||||
const finishClip = this.context.getClip('type_finish_Q')
|
||||
if (finishClip && this.currentClip) {
|
||||
await this.stopCurrentClip(0.2)
|
||||
await finishClip.play()
|
||||
await this.waitForClipEnd(finishClip)
|
||||
}
|
||||
|
||||
this.isTyping = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from type state
|
||||
* @returns {string[]} Array of available state transitions
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
return [States.WAITING, States.REACTING]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently typing
|
||||
* @returns {boolean} True if typing, false otherwise
|
||||
*/
|
||||
getIsTyping () {
|
||||
return this.isTyping
|
||||
}
|
||||
|
||||
/**
|
||||
* Set typing state
|
||||
* @param {boolean} typing - Whether currently typing
|
||||
* @returns {void}
|
||||
*/
|
||||
setTyping (typing) {
|
||||
this.isTyping = typing
|
||||
}
|
||||
}
|
||||
138
src/states/WaitStateHandler.js
Normal file
138
src/states/WaitStateHandler.js
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Wait state handler implementation
|
||||
* @module states
|
||||
*/
|
||||
|
||||
import { StateHandler } from './StateHandler.js'
|
||||
import { States, Emotions, Config } from '../constants.js'
|
||||
|
||||
/**
|
||||
* Handler for the Wait/Idle state
|
||||
* @class
|
||||
* @extends StateHandler
|
||||
*/
|
||||
export class WaitStateHandler extends StateHandler {
|
||||
/**
|
||||
* Create a wait state handler
|
||||
* @param {OwenAnimationContext} context - The animation context
|
||||
*/
|
||||
constructor (context) {
|
||||
super(States.WAITING, context)
|
||||
|
||||
/**
|
||||
* The main idle animation clip
|
||||
* @type {AnimationClip|null}
|
||||
*/
|
||||
this.idleClip = null
|
||||
|
||||
/**
|
||||
* Available quirk animations
|
||||
* @type {AnimationClip[]}
|
||||
*/
|
||||
this.quirks = []
|
||||
|
||||
/**
|
||||
* Timer for quirk animations
|
||||
* @type {number}
|
||||
*/
|
||||
this.quirkTimer = 0
|
||||
|
||||
/**
|
||||
* Interval between quirk attempts (ms)
|
||||
* @type {number}
|
||||
*/
|
||||
this.quirkInterval = Config.QUIRK_INTERVAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the wait state
|
||||
* @param {string|null} [fromState=null] - The previous state
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Entering WAITING state from ${fromState}`)
|
||||
|
||||
// Play idle loop
|
||||
this.idleClip = this.context.getClip('wait_idle_L')
|
||||
if (this.idleClip) {
|
||||
await this.idleClip.play()
|
||||
this.currentClip = this.idleClip
|
||||
}
|
||||
|
||||
// Collect available quirks
|
||||
this.quirks = this.context.getClipsByPattern('wait_*_Q')
|
||||
this.quirkTimer = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit the wait state
|
||||
* @param {string|null} [toState=null] - The next state
|
||||
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
|
||||
console.log(`Exiting WAITING state to ${toState}`)
|
||||
|
||||
if (this.currentClip) {
|
||||
await this.stopCurrentClip()
|
||||
}
|
||||
|
||||
// Play transition if available
|
||||
const transitionName = `wait_2${toState}_T`
|
||||
const transition = this.context.getClip(transitionName)
|
||||
if (transition) {
|
||||
await transition.play()
|
||||
await this.waitForClipEnd(transition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the wait state
|
||||
* @param {number} deltaTime - Time elapsed since last update (ms)
|
||||
* @returns {void}
|
||||
*/
|
||||
update (deltaTime) {
|
||||
this.quirkTimer += deltaTime
|
||||
|
||||
// Randomly play quirks
|
||||
if (this.quirkTimer > this.quirkInterval && Math.random() < Config.QUIRK_PROBABILITY) {
|
||||
this.playRandomQuirk()
|
||||
this.quirkTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a random quirk animation
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async playRandomQuirk () {
|
||||
if (this.quirks.length === 0) return
|
||||
|
||||
const quirk = this.quirks[Math.floor(Math.random() * this.quirks.length)]
|
||||
|
||||
// Fade out idle
|
||||
if (this.idleClip) {
|
||||
await this.idleClip.stop(0.2)
|
||||
}
|
||||
|
||||
// Play quirk
|
||||
await quirk.play()
|
||||
await this.waitForClipEnd(quirk)
|
||||
|
||||
// Return to idle
|
||||
if (this.idleClip) {
|
||||
await this.idleClip.play()
|
||||
this.currentClip = this.idleClip
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available transitions from wait state
|
||||
* @returns {string[]} Array of available state transitions
|
||||
*/
|
||||
getAvailableTransitions () {
|
||||
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')
|
||||
})
|
||||
})
|
||||
23
vite.config.js
Normal file
23
vite.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
root: 'examples',
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
},
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: 'index.html'
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
owen: '/src/index.js'
|
||||
}
|
||||
}
|
||||
})
|
||||
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